파이썬 웹프로그래밍 - 블로그 애플리케이션 만들기 (완)

채연·2024년 5월 14일
1

study

목록 보기
6/12
post-thumbnail

스터디 목표

장고를 이용하여 블로그 애플리케이션을 완성한다.

  • mysite/blog/static/css/blog.css
body {
    margin:0;
    padding:0;
    font-family:helvetica, sans-serif;
}

a {
    color:#00abff;
    text-decoration:none;
}

h1 {
    font-weight:normal;
    border-bottom:1px solid #bbb;
    padding:0 0 10px 0;
}

h2 {
    font-weight:normal;
    margin:30px 0 0;
}

#content {
    float:left;
    width:60%;
    padding:0 0 0 30px;
}

#sidebar {
    float:right;
    width:30%;
    padding:10px;
    background:#efefef;
    height:100%;
}

p.date {
    color:#ccc;
    font-family: georgia, serif;
    font-size: 12px;
    font-style: italic;
}

/* pagination */
.pagination {
    margin:40px 0;
    font-weight:bold;
}

/* forms */
label {
    float:left;
    clear:both;
    color:#333;
    margin-bottom:4px;
}
input, textarea {
    clear:both;
    float:left;
    margin:0 0 10px;
    background:#ededed;
    border:0;
    padding:6px 10px;
    font-size:12px;
}
input[type=submit] {
    font-weight:bold;
    background:#00abff;
    color:#fff;
    padding:10px 20px;
    font-size:14px;
    text-transform:uppercase;
}
.errorlist {
    color:#cc0033;
    float:left;
    clear:both;
    padding-left:10px;
}

/* comments */
.comment {
    padding:10px;
}
.comment:nth-child(even) {
    background:#efefef;
}
.comment .info {
    font-weight:bold;
    font-size:12px;
    color:#666;
}
  • mysite/blog/templates/pagination.html
<div class = "pagination">
    <span class = "step-links">
        {% if page.has_previous %}
            <!-- 이전 페이지가 있다면 이동하는 링크 생성 -->
            <a href = "?page={{ page.previous_page_number }}">Previous</a>
        {% endif %}
        <span class="current">
            <!-- 현재 페이지 번호, 전체 페이지 수를 표시 -->
            Page {{ page.number }} of {{ page.paginator.num_pages }}.
        </span>
        {% if page.has_next %}
            <!-- 다음 페이지가 있다면 이동하는 링크 생성 -->
            <a href = "?page={{ page.next_page_number }}">Next</a>
        {% endif %}
    </span>
</div>
  • mysite/blog/templates/blog/base.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <title>{% block title %}{% endblock %}</title>
    <link href = "{% static 'css/blog.css' %}" rel = "stylesheet">
</head>
<body>
    <div id = "content">
        {% block content %}
        {% endblock %}
    </div>
    <div id = "sidebar">
        <h2>My blog</h2>
        <p>This is my blog.</p>
    </div>
</body>
</html>
  • mysite/blog/templates/blog/post/detail.html
{% extends "blog/base.html" %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
    <h1>{{ post.title }}</h1>
    <p class = "date">
        Published {{ post.publish }} by {{ post.author }}
    </p>
    {{ post.body|linebreaks }}
    <p>
        <a href="{% url 'blog:post_share' post.id %}">
            Share this post
        </a>
    </p>
{% endblock %}
  • mysite/blog/templates/blog/post/list.html
{% extends "blog/base.html" %}
{% block title %} My blog {% endblock %}
{% block content %}
    <h1>My blog</h1>
    {% for post in posts %}
        <h2>
            <a href = "{{ post.get_absolute_url }}">
                {{ post.title }}
            </a>
        </h2>
        <p class = "date">
            Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body | truncatewords:30 | linebreaks }}
    {% endfor %}
    <!-- page_obj = 장고에서 기본적으로 제공하는 페이지 객체 -->
    <!-- 페이지 객체를 사용하여 이전/다음 페이지로 이동하는데 필요한 메타데이터 포함 -->
    {% include "pagination.html" with page=page_obj %}
{% endblock %}
  • mysite/blog/templates/blog/post/share.html
{% extends "blog/base.html" %}
<!-- blog/base.html 상속 -->
{% block title %}Share a post{% endblock %}
<!-- 페이지 타이틀 정의 -->

{% block content %} <!-- 본문 정의 -->
    {% if sent %} <!-- 이메일이 전송됐다면 -->
        <h1>E-mail successfully send</h1>
        <p>
            "{{ post.title }}" was successfully send to {{ form.cleaned_data.to }}.
        </p> <!-- 전송된 이메일 제목과 수신자 이메일 주소를 표시 -->
    {% else %} <!-- 이메일이 전송이 실패됐다면 -->
        <h1>Share "{{ post.title }}" by e-mail</h1>
        <form method="post"> <!-- 폼을 표시하기 위해 post 메서드를 사용 -->
            {{ form.as_p }} <!-- 이메일 전송 필드 표시 -->
            {% csrf_token %} <!-- 토큰 추가 (+ 교차 공격을 방지, 보안을 강화하기 위한 숨겨진 필드) -->
            <input type="submit" value="Send e-mail"> <!-- Send e-mail 버튼 클릭 -->
        </form>
    {% endif %} <!-- 조건문 끝 -->
{% endblock %} <!-- 본문 끝 -->
  • mysite/blog/admin.py
from django.contrib import admin
from .models import Post

@admin.register(Post) # admin.site.register() 함수와 동일 가능
class PostAdmin(admin.ModelAdmin) :
    list_display = ['title', 'slug', 'author', 'publish', 'status'] # 게시물 목록 페이지에 표시되는 필드
    list_filter = ['status', 'created', 'publish', 'author'] # 게시물 목록 페이지에 표시되는 필드
    search_fields = ['title', 'body'] # 검색 가능한 필드 목록
    prepopulated_fields = {'slug' : ('title',)} # title 필드 입력하면 slug 필드 자동 채우기
    raw_id_fields = ['author'] # 조회 위젯과 함께 표시
    date_hierarchy = 'publish' # 날짜를 고를 수 있음
    ordering = ['status', 'publish'] # 정렬
  • mysite/blog/forms.py
from django import forms
# 장고 폼 만들기

class EmailPostForm(forms.Form) :
    name = forms.CharField(max_length = 25) # name = 최대 길이 25자인 문자열 인스턴스, 보내는 사람의 이름
    email = forms.EmailField() # email = 이메일 인스턴스, 추천을 보내는 사람의 이메일
    to = forms.EmailField() # 이메일 인스턴스, 수신자의 이메일로 사용
    comments = forms.CharField(required = False, widget = forms.Textarea) # comments = 문자열 인스턴스, 이메일에 포함할 추천 코멘트
    # required를 False로 설정해 이 필드를 선택 사항으로 지정
    # widget은 폼 필드를 어떻게 사용자에게 보여줄지 정의
    # forms.Textarea는 여러 줄의 텍스트를 입력할 수 있는 텍스트 영역 생성 및 제공
  • mysite/blog/models.py
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User # 사용자와 게시물간의 관계
from django.urls import reverse # reverse = URl 패턴 이름을 가지고 해당 URL을 생성하는 함수

class PublishedManager(models.Manager) :
    def get_queryset(self) : # 상태별로 게시물을 필터링하고 PUBLISHED 상태의 게시물만 포함하는 연속된 QuerySet을 반환하는 커스텀 QuerySet을 만들기 위해 메서드를 재정의함
        return (super().get_queryset()
                .filter(status = Post.Status.PUBLISHED)) # PUBLISHED 상태의 모든 게시물을 조회하는 커스텀 관리자

class Post(models.Model) :

    class Status(models.TextChoices) : # 게시물을 임시로 저장하는 기능
        DRAFT = 'DF', 'Draft' # 상태 필드 DRAFT = 임시
        PUBLISHED = 'PB', 'Published' # 상태 필드 PUBLISHED = 게시

    title = models.CharField(max_length = 250) # 제목 (VARCHAR로 변환되는 CharField 필드)
    slug = models.SlugField(max_length = 250, unique_for_date = 'publish')
    # 문자, 숫자, 밑줄 또는 하이픈만 포함하는 짧은 레이블 (ARCHAR로 변환되는 SlugField 필드)

    # unique_for_date를 사용하면 slug 필드가 게시 필드에 지정된 날짜의 중복을 허용하지 않게 됨
    # publish 필드는 DateTimeField의 인스턴스지만 고유 값의 확인은 날짜에만 수행

    author = models.ForeignKey(User,
                               on_delete = models.CASCADE, # 참조된 객체가 삭제될 때 선택할 수 있는 동작을 지정 (-> CASCADE를 사용해서 지시)
                               related_name = 'blog_posts') # User에서 Post로의 역방향 관계의 명칭을 지정

    body = models.TextField() # 본문 저장 (TEXT 컬럼으로 변환되는 TextField 필드)
    publish = models.DateTimeField(default = timezone.now) # 게시물이 게시된 날짜와 시간을 저장
    created = models.DateTimeField(auto_now_add = True) # 날짜가 자동으로 저장
    updated = models.DateTimeField(auto_now = True) # 게시물이 갱신된 마지막 날짜와 시간을 저장

    status = models.CharField(max_length = 2,
                              choices = Status.choices,
                              default = Status.DRAFT)
    # choices 라는 매개 변수가 DRAFT로 임시 저장을 사용(기본값) DRAFT는 임시 저장, published는 저장

    objects = models.Manager() # 기본 관리자
    published = PublishedManager() # 커스텀 관리자

    class Meta :
        ordering = ['-publish'] # publish 기준으로 내림차순
        indexes = [
            models.Index(fields = ['-publish']), # publish 필드에 인덱스 추가, 인덱스를 내림차순으로 정의
        ]

    def __str__(self) : # __str__ -> 객체를 표현하는 문자열을 반환하는 메소드, 장고 관리 사이트 등 여러 위치에서 객체의 이름으로 표시
        return self.title

    def get_absolute_url(self) :
        return reverse('blog:post_detail',
                       args=[self.publish.year,
                             self.publish.month,
                             self.publish.day,
                             self.slug])

    # blog:post_detail이라는 URL 패턴 이름 사용 -> post_detail 뷰에 연결
    # args = [self.id]를 사용해서 Post 객체 즉 해당 포스트의 ID 전달
    # 따라서 이 메서드를 호출하면 해당 포스트의 상세 정보를 보여주는 URL이 반환
    # 게시물 상세 페이지 URL을 참조하기 위해 blog:post_detail을 프로젝트 전역에서 사용 가능
  • mysite/blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'
urlpatterns = [
    # post 뷰 - 함수형
    # path('', views.post_list, name = 'post_list'), # 인수 취하지 않고 post_list 뷰에 매핑(연결)

    # 클래스형
    path('', views.PostListView.as_view(), name = 'post_list'),
    # 계시물 목록을 표시하는 URL
    path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.post_detail, name = 'post_detail'),
    # 게시물의 세부 정보(년, 월, 일, 게시물 슬러그)를 표시하는 URL
    path('<int:post_id>/share/', views.post_share, name = 'post_share')
    # 게시물 ID를 받아 해당 게시물을 공유하는 기능을 처리하는 URL

]
  • mysite/blog/views.py
from django.shortcuts import render, get_object_or_404
from .models import *
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
# from django.http import Http404
from django.views.generic import ListView
from .forms import EmailPostForm
from django.core.mail import send_mail

def post_list(request) :
    post_list = Post.published.all()
    pagintor = Paginator(post_list, 3) # 한 페이지에 3개의 포스트가 있도록 설정
    page_number = request.GET.get('page', 1) # page 매개변수가 요청의 GET 파라미터에 없을 경우 기본 값으로 1을 사용
    # posts = pagintor.page(page_number) # 앞서 만든 Paginator 객체 pagintor를 사용하여 현재 페이지 번호에 해당하는 포스트들을 가져오기, 가져온 포스트들은 posts 변수에 할당
    # posts = Post.published.all() # published 상태의 모든 게시물 조회
    try :
        posts = pagintor.page(page_number) # 요청된 페이지의 게시물을 가져오려 시도
    except PageNotAnInteger :
        posts = pagintor.page(1) # ~ 페이징 오류 처리하기 (정수가 아닌 문자열 등) / 정수가 아닌 경우 결과의 첫 번째 페이지를 다시 염
    except EmptyPage :
        posts = pagintor.page(pagintor.num_pages) # ~ 페이징 오류 처리하기(페이지 번호) / 요청한 페이지가 범위를 벗어나면 결과의 마지막 페이지 반환
    return render(request, 'blog/post/list.html',{'posts' : posts})
# render = 게시물 목록을 렌더링하여 반환 / render() = 요청에 관련된 정보를 템플릿에 전달할 때 사용
# ~ 목록뷰 생성하기

#def post_detail(request, id) :
#    try :
#        post = Post.publish.get(id = id) # get() 메서드를 호출해서 id를 가진 post 객체 조회
#    except Post.DoesNotExit : # 결과가 없는 오류 발생 시
#        raise Http404("No Post found.") # Http404 오류 반환

#    return render(request, 'blog/post/detail.html', {'post' : post})
    # render() 함수로 검색(클릭)된 게시물 렌더링

# try, except = 예외 처리
# try에는 기본적으로 실행하는 코드
# except에는 오류가 발생했을 시 실행하는 코드
# ~ 상세 뷰 생성하기

def post_detail(request, year, month, day, post) :
    post = get_object_or_404(Post,
                             status = Post.Status.PUBLISHED,
                             slug = post,
                             publish__year = year,
                             publish__month = month,
                             publish__day = day)
    return render(request, 'blog/post/detail.html', {'post':post})
# ~ 상세 뷰 생성하기 / URL 패턴 수정하기

class PostListView(ListView) :
    """
    Alternative post list view
    """
    queryset = Post.published.all()
    context_object_name = 'posts'
    paginate_by = 3 # 페이지당 3개의 객체를 반환하도록 페이징을 정의
    template_name = 'blog/post/list.html'

def post_share(request, post_id) :
    # id로 게시물 조회
    post = get_object_or_404(Post, id = post_id, status = Post.Status.PUBLISHED)
    sent = False

    if request.method == 'POST' : # POST 요청과 폼 제출 확인 (폼이 제출됐다면)
        form = EmailPostForm(request.POST) # POST 요청이 맞다면 제출된 데이터로 초기화

        if form.is_valid() : # 유효성 검사를 통과한 폼 필드들
            cd = form.cleaned_data # form.cleaned_data = 유효성 검사를 통과한 폼 필드들의 데이터를 담는 객체
            post_url = request.build_absolute_uri(post.get_absolute_url()) # 게시물의 URL 생성
            subject = f"{cd['name']} recommends you read" \
                      f"{post.title}"
            # 이메일 제목 생성
            message = f"Read {post.title} at {post_url}\n\n" \
                      f"{cd['name']}\'s comments: {cd['comments']}"
            # 본문 생성
            send_mail(subject, message, 'gim88028@gmail.com', [cd['to']])
            sent = True # 이메일 전송

    else :
        form = EmailPostForm() # GET 요청인 경우(폼이 유효하지 않은 경우) 폼을 초기화
    return render(request, 'blog/post/share.html', {'post':post, 'form':form, 'sent':sent})
    # 폼과 게시물 정보를 템플릿에 전달 (+ 전송 여부)
  • mysite/mysite/setting.py
ALLOWED_HOSTS = []

# 이메일 서버 구성
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'admin@gmail.com'
EMAIL_HOST_PASSWORD = '0000 0000 0000 0000'
EMAIL_PORT = 587
EMAIL_USE_TLS = True

# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog.apps.BlogConfig" # blog 애플리케이션 활성화
]
  • mysite/mysite/setting.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls', namespace = 'blog')),
    # blog/ 하위 경로로 포함시키기 위해 blog에 정의된 URL 패턴 참조
]
  • mysite/mysite/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls', namespace = 'blog')),
    # blog/ 하위 경로로 포함시키기 위해 blog에 정의된 URL 패턴 참조
]

실행 결과

Terminal에 python manage.py runserver 명령을 입력하여 출력 결과를 확인한다.


0개의 댓글