EMDI는 지금도 개발중

Python with Django : 파이썬 연습 Blog 앱 만들기 (책 파이썬 웹 프로그래밍, 실전편 참고) 본문

언어/Python

Python with Django : 파이썬 연습 Blog 앱 만들기 (책 파이썬 웹 프로그래밍, 실전편 참고)

EMDI 2020. 3. 31. 09:31

저번 글에서는 파이썬과 장고를 가지고 북마크를 만들어 보았습니다. 이번 글에서는 Blog앱을 만드는 연습을 해보도록 합시다. 우선 Blog 애플리케이션을 만들기 위해 가상환경으로 들어갑니다.

# 가상환경은 저번과 동일하게 Scripts 폴더 내에 있는 activate.bat을 실행하면 됩니다.
C:\Users\milko>C:\Users\milko\VENV\dJangoVenv\Scripts\activate.bat

# 만약 cmd창이 아래와 같이 변경되었다면 가상환경으로 들어온 상태입니다.
(dJangoVenv) C:\Users\milko> 

1. settings.py 파일에 Blog 앱 등록하기

# 가상환경에서 블로그앱을 설정할 폴더로 경로를 이동합니다.
(dJangoVenv) C:\Users\milko>cd C:\Users\milko\django-project\ch99

# 블로그앱 만드는 명령어를 실행합니다.
(dJangoVenv) C:\Users\milko\django-project\ch99>python manage.py startapp blog 

블로그앱을 만드는 명령어가 정상적으로 처리되었으면 해당 경로에 blog라는 폴더 및 python 파일들이 생성되었을겁니다.

# 프로젝트에 포함되는 애플리케이션들은 모두 설정파일에 등록해야 합니다.
# 애플리케이션들을 관리하는 mySite의 settings.py에 blog app을 등록합니다.

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'bookmark.apps.BookmarkConfig',
    'blog.apps.BlogConfig', #추가
]

2. 모델

데이터베이스에 테이블을 생성하도록 해주는 작업

1) 클라이언트 사이트

# blog/models.py

from django.db import models
# reverse() 함수를 사용하기 위해 임포트. reverse() 함수는 URL 패턴을 만들어주는 장고의 내장함수
from django.urls import reverse

# Create your models here.
class Post(models.Model):
    # title 컬럼은 CharField이므로 한 줄로 입력됩니다. 
    title = models.CharField(verbose_name ='TITLE', max_length=50)

    # slug 컬럼은 제목의 별칭.
    slug = models.SlugField('SLUG', unique=True, allow_unicode=True, help_text='one word for title alias.')
    
    # description 컬럼은 빈칸도 가능.
    description = models.CharField('DESCRIPTION', max_length=100, blank=True, help_text='simple description text.')
    
    # content 컬럼은 TextField를 사용했으므로 여러 줄 입력이 가능합니다.
    content = models.TextField('CONTENT', default='')
    
    # create_dt 컬럼은 날짜와 시간을 입력하는 DateTimeField입니다.
    create_dt = models.DateTimeField('CREATE DATE', auto_now_add=True)
    
    # modify_dt 컬럼은 날짜와 시간을 입력하는 DateTimeField입니다.
    modify_dt = models.DateTimeField('MODIFY DATE', auto_now=True)

    #--------------------------------------------------------------------------------
    # 필드 속성 외에 필요한 파라미터가 있으면, Meta 내부 클래스로 정의합니다.
    class Meta:
        verbose_name='post'
        verbose_name_plural='posts'
        db_table = 'blog_posts'
        ordering = ('-modify_dt',)

    # 객체의 문자열 표현 메소드 
    def __str__(self):
        return self.title
    
    # 정의된 객체를 지칭하는 url을 반환
    def get_absolute_url(self):
        return reverse('blog:post_detail', args=(self.slug,))
    
    # 장고의 내장 함수 get_previous_by_modify_dt()를 호출. modify_dt기준으로 최신 포스트 반환.
    def get_previous(self):
        return self.get_previous_by_modify_dt()

    # -modify_dt 컬럼을 기준으로 다음 포스트를 반환.
    def get_next(self):
        return self.get_next_by_modify_dt()

* 슬러그란? 슬러그(Slug)는 페이잔 포스트를 설명하는 핵심 단어의 집합. 원래 신문이나 잡지 등에서 제목을 쓸 때, 중요한 의미를 포함하는 단어만 이용해 제목을 작성하는 방법을 말합니다.

* SlugField 필드 타입 : 슬러그는 보통 제목의 단어들을 하이픈으로 연결해 생성하며, URL에서 pk대신 사용되는 경우가 많다. pk를 사용하면 숫자로만 되어 있어 그 내용을 유추하기 어렵지만, 슬러그를 사용하면 보통의 단어들이라서 이해하기 쉽기 때문이다.

 

2) 관리자 사이트

# blog/admin.py

from django.contrib import admin
# models.py에 아까 설정했던 Post를 임포트
from blog.models import Post

# Register your models here.
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('id', 'title','modify_dt')
    list_filter = ('modify_dt',)
    search_fields = ('title', 'content')
    prepopulated_fields = {'slug': ('title',)}

그 다음은 Admin 사이트에 보이도록 admin.py 파일에 다음처럼 등록합니다.

 

3) 데이터베이스 반영

# Post 테이블을 신규로 정의했으므로, 다음 명령으로 신규 테이블을 데이터베이스 반영.
(dJangoVenv) C:\Users\milko\django-project\ch99>python manage.py makemigrations blog
(dJangoVenv) C:\Users\milko\django-project\ch99>python manage.py migrate

* 마이그레이션하는 도중 오류났던 내용 (without a default / current date as default, use 'django.utils.timezone.now')

1. without a default

전에 bookmark할 땐 잘만 되었던 마이그레이션이 이번에는 정상적으로 처리가 안되네요. 내뱉은 오류를 읽어보니 content 필드에 default가 없다라는 오류였습니다. 책에서는 따로 default를 설정하는 글이 없었는데 흠... 우선 오류때문에 마이그레이션을 못하고 있으니 임의의 값인 default를 부여하도록 합시다

1) Provide a one-off default now (will be set on all existing rows with a null value for this column)

만약 1번을 클릭하셨으면 ('') string의 빈값을 지정해주시던가

2) Quit, and let me add a default in models.py

아니면 deafult = ''를 models.py에 입력해주세요

# 오류났던 부분 수정한 내용1
# content 컬럼은 TextField를 사용했으므로 여러 줄 입력이 가능합니다.
content = models.TextField('CONTENT', default='')

 

2. current date as default, use 'django.utils.timezone.now'

not null의 default 부여를 끝내니 이번에는 datetime에 관련해서 default 오류가 뜨네요.  오류 메시지는 You are trying to add the field 'create_dt' with 'auto_now_add=True' to post without a default; the database needs something to populate existing rows.

# 오류났던 부분 수정한 내용2
# null=True, blank=True를 추가하기
# create_dt 컬럼은 날짜와 시간을 입력하는 DateTimeField입니다.
create_dt = models.DateTimeField('CREATE DATE', auto_now_add=True, null=True, blank=True)    
# modify_dt 컬럼은 날짜와 시간을 입력하는 DateTimeField입니다.
modify_dt = models.DateTimeField('MODIFY DATE', auto_now=True, null=True, blank=True)

위와 같이 DataTimeField에 null과 blank를 True로 지정하고 마이그레이션을 하면 넘어갑니다. 하지만 해당 코딩에는 뭔가 문제가 있는지 훗날 클라이언트 html를 확인할 때 detail에 오류가 뜹니다.  'Post' object has no attribute 'get_previous_by_modify_dt' 저렇게 attribute 오류가 나는 경우에는 나중에 null과 blank 적용한 부분을 제거해주시면 됩니다.

마이그레이션 데이터베이스관련 사항까지 작업을 마쳤으면, 이제 admin사이트에 접속을 시도해보겠습니다. 테스트용 웹 서버 runserver가 실행되지 않았다면 다음 명령으로 실행

(dJangoVenv) C:\Users\milko\django-project\ch99>python manage.py runserver 0.0.0.0:8000

 

홈페이지 적용한 내용을 확인하면 위와 같이 나오는 것을 확인할 수 있습니다.

 

3. URLconf 코딩

URLconf를 수정할 때는 ROOT_URLCONF와 APP_URLCONF 두 곳을 수정해야합니다. ROOT_URLCONF는 디렉터리에 있는 urls.py파일을 의미하며, APP_URLCONF는 각 애플리케이션 디렉터리에 있는 urls.py를 의미합니다.

#mySite/urls.py

from django.contrib import admin
from django.urls import path, include                   #include추가
# from bookmark.views import BookmarkLV, BookmarkDV     #삭제

from django.views.generic import ListView, DetailView
from bookmark.models import Bookmark

urlpatterns = [
    path('admin/', admin.site.urls),
    path('bookmark/', include('bookmark.urls')),    #추가
    path('blog/', include('blog.urls')),            #추가

    # 기존라인 삭제
    # class-based views
    # path('bookmark/', ListView.as_view(model=Bookmark), name='index'),
    # path('bookmark/<int:pk>/', DetailView.as_view(model=Bookmark), name='detail'),

]

첫 번째 글에서 다루었던 Bookmark때는 ROOT_URLCONF에 직접 넣었지만 이제는 애플리케이션이 2개가 생겼다보니 해당 내용을 APP_URLCONFIG로 옮기겠습니다. Inculde()함수를 이용해서 APP_URLCONF로 처리를 위임합니다.

# bookmark/urls.py
from django.urls import path
from bookmark.views import BookmarkLV, BookmarkDV     #삭제

app_name = 'bookmark'
urlpatterns = [
    # class-based views
    path('', BookmarkLV.as_view(), name='index'),
    path('<int:pk>/', BookmarkDV.as_view(), name='detail'),

]
# blog/urls.py
from django.urls import path, re_path
from blog import views

app_name = 'blog'
urlpatterns = [

    # /blog/
    path('', views.PostLV.as_view(), name='index'),

    # /blog/post
    path('post/', views.PostLV.as_view(), name='post_list'),

    # /blog/post/django-example
    # URL /blog/poist/슬러그/ 요청을 처리할 뷰 클래스를 PostDV로 지정합니다.
    re_path(r'^post/(?P<slug>[-\w]+)$', views.PostDV.as_view(), name='post_detail'),

    # /blog/archive/
    path('archive/', views.PostAV.as_view(), name='post_archive'),

    # /blog/archive/2019/
    path('archive/<int:year>/', views.PostYAV.as_view(), name='post_year_archive'),

    # /blog/archive/2019/nov/
    path('archive/<int:year>/<str:month>/', views.PostMAV.as_view(), name='post_month_archive'),
    
    # /blog/archive/2019/nov/10/
    path('archive/<int:year>/<str:month>/<int:day>/', views.PostDAV.as_view(), name='post_day_archive'),

    # /blog/archive/today/
    path('archive/today/', views.PostTAV.as_view(), name='post_today_archive'),
]

4. View 코딩

URLconf에서 지정한 클래스형 제네릭 뷰를 코딩합니다. 이번 블로그앱의 특징은 bookmark때와 다르게 년,월,일 날짜기준으로 찾아주는 날짜 제니릭 뷰를 사용하고 있다는 점입니다.

# blog/views.py
from django.shortcuts import render
from django.views.generic import ListView, DetailView
from django.views.generic.dates import ArchiveIndexView, YearArchiveView, MonthArchiveView
from django.views.generic.dates import DayArchiveView, TodayArchiveView

from blog.models import Post

# Create your views here.
# ListView
class PostLV(ListView):
    model = Post
    template_name = 'blog/post_all.html'
    context_object_name = 'posts'
    paginate_by = 2

# DetailView
class PostDV(DetailView):
    model = Post

# ArchiveView
class PostAV(ArchiveIndexView):
    model = Post
    date_field = 'modify_dt'

class PostYAV(YearArchiveView):
    model = Post
    date_field = 'modify_dt'
    make_object_list = True

class PostMAV(MonthArchiveView):
    model = Post
    date_field = 'modify_dt'

class PostDAV(DayArchiveView):
    model = Post
    date_field = 'modify_dt'

class PostTAV(TodayArchiveView):
    model = Post
    date_field = 'modify_dt'

5. Template 코딩

# Templates\blog 폴더를 생성한 다음 html 만들기
(dJangoVenv) C:\Users\milko>cd C:\Users\milko\django-project\ch99\blog
(dJangoVenv) C:\Users\milko\django-project\ch99\blog>mkdir templates 
(dJangoVenv) C:\Users\milko\django-project\ch99\blog>mkdir templates\blog
# post_all.html

<h1>Blog List</h1>
<br>
{% for post in posts %}
    <h3><a href='{{ post.get_absolute_url }}'>{{ post.title }}</a></h3>
    {{ post.modify_dt|date:"N d, Y" }}
    <p>{{ post.description }}</p>
{% endfor %}

<br>

<div>
    <span>
        {% if page_obj.has_previous %}
            <a href="?page={{page_obj.previous_page_number}}">PreviousPage</a>
        {% endif %}

        Page {{ page_obj.number }} of {{page_obj.paginator.num_pages}}

        {% if page_obj.has_next %}
            <a href="?page={{page_obj.next_page_number}}">NextPage</a>
        {% endif %}
    </span>
</div>
# post_detail.html
<h2>{{ object.title }}</h2>
<p>
    {% if object.get_previous %}
    <a href="{{ object.get_previous.get_absolute_url }}" title="View previous post">
        &laquo;- {{ object.get_previous }}
    </a>
    {% endif %}

    {% if object.get_next %}
    | <a href="{{ object.get_next.get_absolute_url }}" title="View next post">
        {{object.get_next}} -&raquo;
    </a>
    {% endif %}

</p>
<p>{{ object.modify_dt|date:"j F Y" }}</p>
<br>

<div>
    {{ object.content|linebreaks }}
</div>
# post_archive.html
<h1>Post Archives until {% now "N d, Y" %}</h1>
<ul>
    {% for date in date_list %}
    <li style="display:inline;">
        <a href="{% url 'blog:post_year_archive' date|date:'Y' %}">Year {{date|date:"Y"}}</a>
    </li>
    {% endfor %}
</ul>
<br/>

<div>
    <ul>
        {% for post in object_list %}
        <li> {{ post.modify_dt|date:"Y-m-d" }}&nbsp;&nbsp;&nbsp;
            <a href="{{ post.get_absolute_url }}"><strong>{{ post.title}}</strong></a>
        </li>
        {% endfor %}
    </ul>
</div>
# post_archive_year.html

<h1>Post Archives for {{ year|date:"Y" }}</h1>
<ul>
    {% for date in date_list %}
    <li style="display:inline;">
        <a href="{% url 'blog:post_month_archive' year|date:'Y' date|date:'b' %}">{{date|date:"F"}}</a>
    </li>
    {% endfor %}
</ul>
<br/>

<div>
    <ul>
        {% for post in object_list %}
        <li> {{ post.modify_dt|date:"Y-m-d" }}&nbsp;&nbsp;&nbsp;
            <a href="{{ post.get_absolute_url }}"><strong>{{ post.title}}</strong></a>
        </li>
        {% endfor %}
    </ul>
</div>
# post_archive_month.html

<h1>Post Archives for {{ month|date:"N, Y" }}</h1>

<div>
    <ul>
        {% for post in object_list %}
        <li> {{ post.modify_dt|date:"Y-m-d" }}&nbsp;&nbsp;&nbsp;
            <a href="{{ post.get_absolute_url }}"><strong>{{ post.title }}</strong></a>
        </li>
        {% endfor %}
    </ul>
</div>
# post_archive_day.html
<h1>Post Archives for {{ day|date:"N d, Y" }}</h1>

<div>
    <ul>
        {% for post in object_list %}
        <li> {{ post.modify_dt|date:"Y-m-d" }}&nbsp;&nbsp;&nbsp;
            <a href="{{ post.get_absolute_url }}"><strong>{{ post.title }}</strong></a>
        </li>
        {% endfor %}
    </ul>
</div>

6. 결과확인 - admin 사이트

 

7. 결과확인 - 클라이언트

Comments