WeniVooks

검색

Django 베이스캠프

CBV CRUD

앞서 배운 함수 기반 뷰(Function Based Views, FBV)를 이용하여 간단한 CRUD를 만드는 방법입니다. 이번 장에서 실습할 내용은 설명 대신 코드로만 제공됩니다. 이해가 어려운 부분이 있다면, 이전 장을 참고하시기 바랍니다.

# 목표
 
1. Django에 CBV를 경험해본다.
2. CBV로 프로젝트를 구현 경험을 쌓는다.
    - 인증
    - CRUD(+인증)
 
# 코드
 
#####################################
mkdir cbv
cd cbv
python -m venv venv
.\venv\Scripts\activate
 
pip install django pillow
 
django-admin startproject config .
python manage.py migrate
 
python manage.py startapp blog
 
 
#####################################
# config/settings.py
 
 
ALLOWED_HOSTS = ['*']
 
 
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",
]
 
"DIRS": [BASE_DIR / "templates"],
 
LANGUAGE_CODE = "ko-kr"
 
TIME_ZONE = "Asia/Seoul"
 
 
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
 
 
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
 
 
#####################################
 
mkdir templates
mkdir static
mkdir media
 
#####################################
# Model 설계
Post
    title
    content
    image
    file_upload
    created_at
    updated_at
 
# URL 설계
다음 복습에서도 아래와 같은 URL이 만들어지도록 해보세요.
    /blog/                    : 블로그 글 목록
    /blog/<int:pk>/           : 블로그 글 상세
    /blog/write/              : 블로그 글 작성
    /blog/edit/<int:pk>/      : 블로그 글 수정
    /blog/delete/<int:pk>/    : 블로그 글 삭제
 
# 앱 설계
앱 이름: blog                URL 연결 이름               HTML 파일 이름
    /blog/                  : blog_list                 post_list.html
    /blog/<int:pk>/         : blog_detail               post_detail.html
    /blog/write/            : blog_write                post_write.html
    /blog/edit/<int:pk>/    : blog_edit                 post_edit.html
    /blog/delete/<int:pk>/  : blog_delete               post_delete.html
 
 
#####################################
# config/urls.py
 
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from django.http import HttpResponse
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("settings/", lambda request: HttpResponse(settings.MEDIA_URL)),
    path("blog/", include("blog.urls")),
]
 
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 
 
#####################################
# blog/urls.py
 
from django.urls import path
from . import views
 
urlpatterns = [
    path("", views.blog_list, name="blog_list"),
    path("<int:pk>/", views.blog_detail, name="blog_detail"),
    path("write/", views.blog_write, name="blog_write"),
    path("edit/<int:pk>/", views.blog_edit, name="blog_edit"),
    path("delete/<int:pk>/", views.blog_delete, name="blog_delete"),
    path("test/", views.test, name="test"),
]
 
#####################################
# blog/views.py
 
from django.views.generic import (
    ListView,
    DetailView,
    CreateView,
    UpdateView,
    DeleteView,
)
from django.urls import reverse_lazy
from django.http import HttpResponse
 
from .models import Post
 
# 클래스 기반 뷰 == 제네릭 뷰는 아닙니다.
# 기본 뷰를 상속받은 클래스 기반이면 다 클래스 기반 뷰 입니다.
# 그중에서도 제네릭 뷰는 특정 목적을 가진(여러가지 기능들을 가지고 있는) 클래스 기반 뷰 입니다.
 
 
class PostList(ListView):
    model = Post
    ordering = "-pk"
    # 최신 글이 맨 위로 오도록 정렬
    # 기본 설정
    # template_name = '<app_name>/<model_name>_list.html' 형식의 기본 템플릿
    # context_object_name = 'object_list'라는 이름으로 템플릿에 객체 리스트를 전달
 
 
class PostDetail(DetailView):
    model = Post
    # 기본 설정
    # template_name = '<app_name>/<model_name>_detail.html' 형식의 기본 템플릿
    # context_object_name = 'object'라는 이름으로 템플릿에 객체를 전달
 
 
class PostCreate(CreateView):
    model = Post
    fields = "__all__"
    success_url = reverse_lazy("blog_list")
    # 기본 설정
    # template_name = '<app_name>/<model_name>_form.html' 형식의 기본 템플릿
    # context_object_name = 'object'라는 이름으로 템플릿에 객체를 전달
    # success_url = reverse_lazy('<app_name>:<model_name>_list') 형식의 기본 리다이렉션 URL
    # fields = '__all__' 모든 필드를 입력받는 기본 폼
    # fields = ('<field_name1>', '<field_name2>', ...) 특정 필드만 입력받는 기본 폼
    # reverse_lazy와 reverse가 있습니다. reverse도 특정 URL을 찾아 리다이렉션 할 수 있습니다. 함수가 실행되는 시점에 이동합니다. 반면, reverse_lazy는 Post가 생성된 이후에 실행됩니다. blog_list로 가기 위해서는 Post가 생성된 다음 URL을 이동해야하기 때문에 reverse_lazy를 사용합니다.
 
 
class PostUpdate(UpdateView):
    model = Post
    fields = "__all__"
    success_url = reverse_lazy("blog_list")
    # 기본 설정
    # template_name = '<app_name>/<model_name>_form.html' 형식의 기본 템플릿
    # context_object_name = 'object'라는 이름으로 템플릿에 객체를 전달
    # success_url = reverse_lazy('<app_name>:<model_name>_list') 형식의 기본 리다이렉션 URL
    # fields = '__all__' 모든 필드를 입력받는 기본 폼
    # fields = ('<field_name1>', '<field_name2>', ...) 특정 필드만 입력받는 기본 폼
 
 
class PostDelete(DeleteView):
    model = Post
    success_url = reverse_lazy("blog_list")
    # 기본 설정
    # template_name = '<app_name>/<model_name>_confirm_delete.html' 형식의 기본 템플릿
    # context_object_name = 'object'라는 이름으로 템플릿에 객체를 전달
    # success_url = reverse_lazy('<app_name>:<model_name>_list') 형식의 기본 리다이렉션 URL
 
 
class PostTest(CreateView):
    model = Post
 
    def get(self, request):
        return HttpResponse("GET 요청을 잘 받았습니다.")
 
    def post(self, request):
        return HttpResponse("POST 요청을 잘 받았습니다.")
 
    # 앞 수업에서는 View를 상속받아 get, post 메서드를 오버라이딩하여 사용했습니다.
    # 이번에는 이 View를 상속받은 CreateView를 상속받아 get, post 메서드를 오버라이딩하여 사용했습니다.
 
 
# 실무에서는 이 as_view() 메서드를 사용하여 URL 패턴에 클래스 기반 뷰를 등록합니다.
# 우리는 FBV와 CBV를 urls.py는 수정하지 않고 수정할 수 있도록 아래와 같이 변수를 만들어 사용하겠습니다.
blog_list = PostList.as_view()
blog_detail = PostDetail.as_view()
blog_write = PostCreate.as_view()
blog_edit = PostUpdate.as_view()
blog_delete = PostDelete.as_view()
test = PostTest.as_view()
 
 
###################################
 
Django에서 ListView와 같은 일반적인 Class-Based Views (CBV)를 사용할 때, 템플릿 이름은 기본적으로 다음과 같은 규칙을 따라 자동으로 생성됩니다.
 
PostList (ListView)
템플릿 이름 규칙: <app_name>/<model_name_소문자>_list.html
기본 템플릿: <app_name>/post_list.html
템플릿 접근 방법:
{% for post in object_list %}
    {{ post.title }}
{% endfor %}
 
 
PostDetail (DetailView)
템플릿 이름 규칙: <app_name>/<model_name_소문자>_detail.html
기본 템플릿: <app_name>/post_detail.html
템플릿 접근 방법: 
{{ object.title }}
 
 
PostCreate (CreateView)
템플릿 이름 규칙: <app_name>/<model_name_소문자>_form.html
기본 템플릿: <app_name>/post_form.html
템플릿 접근 방법:
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Create</button>
</form>
 
 
PostUpdate (UpdateView)
템플릿 이름 규칙: <app_name>/<model_name_소문자>_form.html
기본 템플릿: <app_name>/post_form.html
템플릿 접근 방법:
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Update</button>
</form>
 
 
PostDelete (DeleteView)
템플릿 이름 규칙:  <app_name>/<model_name_소문자>_confirm_delete.html
기본 템플릿: <app_name>/post_confirm_delete.html
템플릿 접근 방법:
<form method="post">
    {% csrf_token %}
    Are you sure you want to delete "{{ object.title }}"?
    <button type="submit">Delete</button>
</form>
 
 
* CreateView와 UpdateView는 같은 템플릿 이름 규칙을 사용합니다. 그래서 둘 다 _form.html을 기본으로 사용합니다.
 
################################
# templates/blog/post_list.html
 
<h2>Post List</h2>
<ul>
    {% for post in object_list %}
        <li>
            <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
        </li>
    {% endfor %}
</ul>
 
################################
# templates/blog/post_detail.html
 
<h2>Post Detail</h2>
<h3>{{ object.title }}</h3>
<p>{{ object.content }}</p>
{% if object.head_image %}
<img src="{{ object.head_image.url }}" alt="{{ object.head_image }}">
{% endif %}
{% if object.file_upload %}
<a href="{{ object.file_upload.url }}">{{ object.file_upload }}</a>
{% endif %}
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
 
<a href="{% url 'blog_list' %}">목록으로 가기</a>
 
 
################################
# templates/blog/post_form.html
 
<h2>Post Form</h2>
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">저장</button>
</form>
 
################################
# templates/blog/post_confirm_delete.html
 
<h2>Delete</h2>
<form method="post">
    {% csrf_token %}
    <p>"{{ object.title }}"을 정말로 삭제하시겠습니까?</p>
    <a href="{% url 'blog_detail' object.pk %}">취소</a>
    <button type="submit">삭제</button>
</form>
 
################################
# blog/models.py
 
from django.db import models
 
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    head_image = models.ImageField(upload_to="blog/images/%Y/%m/%d/", blank=True)
    file_upload = models.FileField(upload_to="blog/files/%Y/%m/%d/", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)
 
    def __str__(self):
        return self.title
 
################################
 
from django.db import models
 
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    head_image = models.ImageField(upload_to="blog/images/%Y/%m/%d/", blank=True)
    file_upload = models.FileField(upload_to="blog/files/%Y/%m/%d/", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)
 
    def __str__(self):
        return self.title
    
 
# 이렇게 upload_to에 함수를 사용하여 동적으로 파일 경로를 생성할 수도 있습니다.
# def get_upload_path(instance, filename, base_path):
#     """
#     파일 업로드 경로를 생성하는 함수
#     instance: 모델 인스턴스
#     filename: 업로드된 파일 이름
#     base_path: 기본 경로 (예: "blog/images" 또는 "blog/files")
#     """
#     import datetime
#     now = datetime.datetime.now()
#     return f"{base_path}/{now.year}/{now.month:02d}/{now.day:02d}/{filename}"
 
# class Post(models.Model):
#     title = models.CharField(max_length=100)
#     content = models.TextField()
#     head_image = models.ImageField(
#         upload_to=lambda instance, filename: get_upload_path(
#             instance, filename, "blog/images"
#         ),
#         blank=True
#     )
#     file_upload = models.FileField(
#         upload_to=lambda instance, filename: get_upload_path(
#             instance, filename, "blog/files"
#         ),
#         blank=True
#     )
#     created_at = models.DateTimeField(auto_now_add=True)
#     updated_at = models.DateField(auto_now=True)
 
#     def __str__(self):
#         return self.title
 
 
################################
# blog/admin.py
 
from django.contrib import admin
from .models import Post
 
admin.site.register(Post)
 
 
################################
 
python manage.py makemigrations
python manage.py migrate
 
################################
 
python manage.py createsuperuser
 
leehojun
leehojun@gmail.com
이호준1234!
 
################################
 
python manage.py runserver
 
admin에서 게시물 10개 생성(나중에 테스트를 위해 10개가 필요합니다.)
 
################################
 
http://127.0.0.1:8000/blog/
http://127.0.0.1:8000/blog/1/
http://127.0.0.1:8000/blog/write/ # form.py를 작성하지 않아도 작동!
http://127.0.0.1:8000/blog/edit/1/
http://127.0.0.1:8000/blog/delete/1/
 
################################
# 스스로 코딩을 하시고 error가 났다라고 한다면, 필수적인 항목을 작성하지 않아서 그렇습니다.
# 아래 2가지 방식 중 하나를 택해서 지정해야 합니다.
# 1.
class PostCreate(CreateView):
    model = Post
    fields = '__all__'
    success_url = '/blog/'  # 이 URL로 리디렉션됩니다. 다만 상세 페이지로는 못갑니다.
 
# 2.
from django.urls import reverse
 
class PostCreate(CreateView):
    model = Post
    fields = '__all__'
 
    def get_success_url(self):
        return reverse('blog_details', args=[str(self.object.pk)])
 
 
################################
# 검색기능 추가
# blog/post_list.html
<h2>Post List</h2>
<form action="" method="get">
    <input type="text" name="q">
    <input type="submit" value="Search">
</form>
<ul>
    {% for post in object_list %}
        <li>
            <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
        </li>
    {% endfor %}
</ul>
 
################################
# blog/views.py
 
class PostList(ListView):
    model = Post
    ordering = "-pk"
    # 최신 글이 맨 위로 오도록 정렬
    # 기본 설정
    # template_name = '<app_name>/<model_name>_list.html' 형식의 기본 템플릿
    # context_object_name = 'object_list'라는 이름으로 템플릿에 객체 리스트를 전달
 
    def get_queryset(self):
        # queryset = super().get_queryset()
        # queryset = Post.objects.all()
        queryset = super().get_queryset()
        q = self.request.GET.get("q", "")
        if q:
            queryset = queryset.filter(Q(title__contains=q) | Q(content__contains=q))
        return queryset
 
################################
 
python manage.py runserver로 지난시간까지 작동 되었던 것 확인
 
ListView의 기본 get_queryset 메서드는 model 속성에서 정의된(model = Post) 모델의 전체 객체 목록을 반환하며 PostList에 선언된 ordering = "-pk" 값 등을 반영한 쿼리셋을 반환합니다.
 
super().get_queryset() 호출은 Post 모델의 전체 객체 목록을 반환합니다.
 
get_queryset 함수와 get_context_data 함수의 기능을 혼동할 수 있습니다. get_queryset 함수는 쿼리셋을 반환하는 함수이며 이 쿼리셋을 템플릿에서 주로 사용합니다. get_context_data 함수는 템플릿에 추가 데이터를 전달할 때 사용하는 함수입니다.
 
################################
 
공식문서
https://docs.djangoproject.com/en/5.1/ref/class-based-views/
ListVeiw : https://docs.djangoproject.com/en/5.1/ref/class-based-views/generic-display/
CreateView : https://docs.djangoproject.com/en/5.1/ref/class-based-views/generic-editing/
 
################################
 
Base views
    View : 최상위 제네릭 뷰, 기본 뷰, django view를 만드는데 필요한 기능 제공
    TemplateView : 템플릿이 주어지면 렌더링을 해주는 뷰
    RedirectView : URL이 주어지면 리다이렉트 해주는 뷰
 
Generic display views
    DetailView : model과 템플릿 받아 조건에 맞는 상세 오브젝트를 보여줍니다.
    ListView : model과 템플릿 받아 전체 오브젝트를 보여줍니다.
 
Generic editing views
    FormView : 폼을 보여주고 처리합니다.
    CreateView : 폼을 보여주고 객체를 생성합니다.
    UpdateView : 폼을 조건에 맞게 보여주고 객체를 수정합니다. 
    DeleteView : 객체를 삭제합니다.
 
Generic date views
    ArchiveIndexView : 조건에 맞는 객체의 날짜 정보를 출력합니다.
    YearArchiveView : 연도에 맞는 객체를 출력합니다.
    MonthArchiveView : 월에 맞는 객체를 출력합니다.
    WeekArchiveView : 주에 맞는 객체를 출력합니다.
    DayArchiveView : 일에 맞는 객체를 출력합니다.
    TodayArchiveView : 오늘 날짜에 객체를 출력합니다.
    DateDetailView : 연, 월, 일 조건에 맞는 객체를 출력합니다.
 
################################
 
# 공식 홈페이지 샘프로 코드 포함
 
Base views
    View : 최상위 제네릭 뷰, 기본 뷰, django view를 만드는데 필요한 기능 제공
    TemplateView : 템플릿이 주어지면 렌더링을 해주는 뷰
    '''
    from django.views.generic.base import TemplateView
    from articles.models import Article
 
    class HomePageView(TemplateView):
        template_name = "home.html"
 
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context["latest_articles"] = Article.objects.all()[:5]
            return context
    '''
    RedirectView : URL이 주어지면 리다이렉트 해주는 뷰
 
Generic display views
    DetailView : model과 템플릿 받아 조건에 맞는 상세 오브젝트를 보여줍니다.
    '''
    from django.utils import timezone
    from django.views.generic.detail import DetailView
    from articles.models import Article
 
    class ArticleDetailView(DetailView):
        model = Article
 
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context["now"] = timezone.now()
            return context
    '''
    '''
    <h1>{{ object.headline }}</h1>
    <p>{{ object.content }}</p>
    <p>Reporter: {{ object.reporter }}</p>
    <p>Published: {{ object.pub_date|date }}</p>
    <p>Date: {{ now|date }}</p>
    '''
    ListView : model과 템플릿 받아 전체 오브젝트를 보여줍니다.
    '''
    from django.utils import timezone
    from django.views.generic.detail import DetailView
    from articles.models import Article
 
    class ArticleDetailView(DetailView):
        model = Article
 
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context["now"] = timezone.now()
            return context
    '''
    '''
    <h1>{{ object.headline }}</h1>
    <p>{{ object.content }}</p>
    <p>Reporter: {{ object.reporter }}</p>
    <p>Published: {{ object.pub_date|date }}</p>
 
    <p>Date: {{ now|date }}</p>
    '''
 
Generic editing views
    FormView : 폼을 보여주고 처리합니다.
    CreateView : 폼을 보여주고 객체를 생성합니다.
    UpdateView : 폼을 조건에 맞게 보여주고 객체를 수정합니다. 
    DeleteView : 객체를 삭제합니다.
 
Generic date views
    ArchiveIndexView : 조건에 맞는 객체의 날짜 정보를 출력합니다.
    YearArchiveView : 연도에 맞는 객체를 출력합니다.
    MonthArchiveView : 월에 맞는 객체를 출력합니다.
    WeekArchiveView : 주에 맞는 객체를 출력합니다.
    DayArchiveView : 일에 맞는 객체를 출력합니다.
    TodayArchiveView : 오늘 날짜에 객체를 출력합니다.
    DateDetailView : 연, 월, 일 조건에 맞는 객체를 출력합니다.
    '''
    # views.py
    from django.views.generic.dates import WeekArchiveView
    from myapp.models import Article
 
    class ArticleWeekArchiveView(WeekArchiveView):
        queryset = Article.objects.all()
        date_field = "pub_date"
        week_format = "%W"
        allow_future = True
    
    # urls.py
    from django.urls import path
    from myapp.views import ArticleWeekArchiveView
 
    urlpatterns = [
        # Example: /2012/week/23/
        path(
            "<int:year>/week/<int:week>/",
            ArticleWeekArchiveView.as_view(),
            name="archive_week",
        ),
    ]
 
    # article_archive_week.html:
    <h1>Week {{ week|date:'W' }}</h1>
 
    <ul>
        {% for article in object_list %}
            <li>{{ article.pub_date|date:"F j, Y" }}: {{ article.title }}</li>
        {% endfor %}
    </ul>
 
    <p>
        {% if previous_week %}
            Previous Week: {{ previous_week|date:"W" }} of year {{ previous_week|date:"Y" }}
        {% endif %}
        {% if previous_week and next_week %}--{% endif %}
        {% if next_week %}
            Next week: {{ next_week|date:"W" }} of year {{ next_week|date:"Y" }}
        {% endif %}
    </p>
    '''
 
###################################
 
# 이렇게 한 이유는 test 코드가 잘 작동되는지 확인하려는 의도가 있고
# CSRF 토큰을 별도 발급하지 않아도 되는 코드이기 때문입니다.
# 어제 미들웨어 설치 하려고 했었던 예제(배보다 배꼽이 더 큰 예제)
 
http://127.0.0.1:8000/blog/test/
http://127.0.0.1:8000/blog/write/로 들어가서 <form action="http://127.0.0.1:8000/blog/test/" method="post">으로 수정한 다음 post로 날려 테스트
 
###################################
 
# 만약 게시글이 1000개이고 한 페이지에 10개씩 보여주어야 한다면 함수형은 아래와 같이 구현했을 것입니다.
 
posts = Post.objects.all()
# posts의 길이를 구하고, 길이를 10으로 나누고, 올림을 하면 페이지 수가 나오고
# 처음에 접속했을 때에는 [:10]을 해서 보여주고, 2페이지로 넘어갔을 때에는 [10:20]을 해서 보여주고....
 
# 이런식으로 구현하는 것을 제네릭 뷰는 자동으로 해줍니다.
 
###################################
 
# blog/views.py
 
class PostList(ListView):
    model = Post
    ordering = "-pk"
    paginate_by = 3 # 이 부분만 추가해주세요. 다른 코드는 만지지 마세요.
 
################################
 
# test
http://127.0.0.1:8000/blog/
http://127.0.0.1:8000/blog/?page=1
http://127.0.0.1:8000/blog/?page=2
http://127.0.0.1:8000/blog/?page=3
http://127.0.0.1:8000/blog/?page=4
 
################################
 
# blog_list.html
 
<h2>Post List</h2>
<form action="" method="get">
    <input name="q" type="text">
    <button type="submit">검색</button>
</form>
<ul>
    {% for post in object_list %}
    <li>
        <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>
 
<!-- 가장 기본적인 페이지네이션 -->
<div>
    {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">이전</a>
    {% endif %}
 
    <span>{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
 
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">다음</a>
    {% endif %}
</div>
 
################################
 
page_obj.number          # 현재 페이지 번호
page_obj.has_previous    # 이전 페이지 존재 여부 (True/False)
page_obj.has_next        # 다음 페이지 존재 여부 (True/False)
page_obj.previous_page_number  # 이전 페이지 번호
page_obj.next_page_number      # 다음 페이지 번호
page_obj.paginator.num_pages   # 전체 페이지 수
page_obj.paginator.count       # 전체 객체(게시물) 수
 
################################
 
<h2>Post List</h2>
<form action="" method="get">
    <input name="q" type="text">
    <button type="submit">검색</button>
</form>
<ul>
    {% for post in object_list %}
    <li>
        <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>
 
<!-- 좀 더 자주사용하는 페이지네이션 -->
{% if is_paginated %}
<div>
    {% if page_obj.has_previous %}
        <a href="?page=1">처음</a>
        <a href="?page={{ page_obj.previous_page_number }}">이전</a>
    {% endif %}
 
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <span>{{ num }}</span>
        {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
 
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">다음</a>
        <a href="?page={{ page_obj.paginator.num_pages }}">마지막</a>
    {% endif %}
</div>
{% endif %}
 
################################
 
# 기존 URL 설계
다음 복습에서도 아래와 같은 URL이 만들어지도록 해보세요.
    /blog/                    : 블로그 글 목록(로그인X)
    /blog/<int:pk>/           : 블로그 글 상세(로그인O)
    /blog/write/              : 블로그 글 작성(로그인O)
    /blog/edit/<int:pk>/      : 블로그 글 수정(로그인O, 작성자만)
    /blog/delete/<int:pk>/    : 블로그 글 삭제(로그인O, 작성자만)
 
# accounts URL 설계
다음 URL이 만들어지도록 해보세요.
    /accounts/signup/         : 회원가입
    /accounts/login/          : 로그인
    /accounts/logout/         : 로그아웃
 
################################
 
python manage.py startapp accounts
 
################################
# config/settings.py
 
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",
    "accounts",
]
 
################################
# config/urls.py
 
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from django.http import HttpResponse
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("settings/", lambda request: HttpResponse(settings.NAME)),
    path("blog/", include("blog.urls")),
    path("accounts/", include("accounts.urls")),
]
 
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 
################################
# accounts/urls.py
 
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
 
urlpatterns = [
    path("signup/", views.RegisterView.as_view(), name="register"),
    path(
        "login/",
        auth_views.LoginView.as_view(template_name="accounts/login.html"),
        name="login",
    ),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]
 
 
################################
# accounts/views.py
 
from django.contrib.auth.forms import UserCreationForm
from django.views.generic import CreateView
from django.urls import reverse_lazy
 
 
class RegisterView(CreateView):
    form_class = UserCreationForm
    template_name = "accounts/register.html"
    success_url = reverse_lazy("login")
 
################################
# templates > accounts > register.html 생성
 
<h2>회원가입</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">가입하기</button>
</form>
 
################################
# templates > accounts > login.html 생성
 
<h2>로그인</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">로그인</button>
</form>
 
################################
 
test
 
http://127.0.0.1:8000/accounts/signup/
http://127.0.0.1:8000/accounts/login/
 
################################
# blog/models.py
 
from django.db import models
 
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    head_image = models.ImageField(upload_to="blog/images/%Y/%m/%d/", blank=True)
    file_upload = models.FileField(upload_to="blog/files/%Y/%m/%d/", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)
    author = models.ForeignKey(
        "auth.User",
        on_delete=models.CASCADE,
        verbose_name="작성자",
        blank=True,
        null=True,
    )
 
    def __str__(self):
        return self.title
 
################################
Mixin은 FBV에 @login_required와 같은 편의 기능을 CBV에서 제공하는 역할을 합니다. 이러한 역할을 하는 믹스인은 아래와 같습니다.
 
Mixin	설명
LoginRequiredMixin	로그인한 사용자만 뷰에 접근할 수 있도록 합니다.
PermissionRequiredMixin	특정 권한을 가진 사용자만 뷰에 접근할 수 있도록 합니다.
UserPassesTestMixin	사용자 정의 함수를 사용하여 사용자의 접근 권한을 제어합니다.
FormMixin	폼을 처리하는 데 필요한 메서드를 제공합니다.
ModelFormMixin	모델 폼을 처리하는 데 필요한 메서드를 제공합니다.
SingleObjectMixin	단일 객체를 처리하는 데 필요한 메서드를 제공합니다.
MultipleObjectMixin	객체 목록을 처리하는 데 필요한 메서드를 제공합니다.
 
################################
# blog/views.py
 
from django.views.generic import (
    ListView,
    DetailView,
    CreateView,
    UpdateView,
    DeleteView,
)
from django.urls import reverse_lazy
from django.http import HttpResponse
from django.db.models import Q
from .models import Post
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
 
 
class PostList(ListView):
    model = Post
    ordering = "-pk"
    paginate_by = 3
 
    def get_queryset(self):
        queryset = super().get_queryset()
        q = self.request.GET.get("q", "")
        if q:
            queryset = queryset.filter(Q(title__contains=q) | Q(content__contains=q))
        return queryset
 
 
class PostDetail(LoginRequiredMixin, DetailView):
    model = Post
    login_url = "/accounts/login/"
 
 
class PostCreate(LoginRequiredMixin, CreateView):
    model = Post
    fields = ["title", "content", "head_image", "file_upload"]
    success_url = reverse_lazy("blog_list")
    login_url = "/accounts/login/"
 
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
 
 
class PostUpdate(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    fields = "__all__"
    success_url = reverse_lazy("blog_list")
    login_url = "/accounts/login/"
 
    def test_func(self):
        post = self.get_object()
        return post.author == self.request.user
 
 
class PostDelete(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Post
    success_url = reverse_lazy("blog_list")
    login_url = "/accounts/login/"
 
    def test_func(self):
        post = self.get_object()
        return post.author == self.request.user
 
 
class PostTest(CreateView):
    model = Post
 
    def get(self, request):
        return HttpResponse("GET 요청을 잘 받았습니다.")
 
    def post(self, request):
        return HttpResponse("POST 요청을 잘 받았습니다.")
 
 
blog_list = PostList.as_view()
blog_detail = PostDetail.as_view()
blog_write = PostCreate.as_view()
blog_edit = PostUpdate.as_view()
blog_delete = PostDelete.as_view()
test = PostTest.as_view()
 
################################
 
LoginRequiredMixin:
로그인한 사용자만 접근할 수 있도록 제한하는 Mixin입니다
함수형 뷰의 @login_required 데코레이터와 동일한 역할을 합니다
 
 
UserPassesTestMixin:
특정 테스트 함수를 통과한 사용자만 접근을 허용하는 Mixin입니다
test_func() 메서드를 오버라이드하여 접근 조건을 정의합니다
false일 경우 403 Forbidden 페이지를 보여줍니다
 
# 인증 관련
LoginRequiredMixin       # 로그인 필수
UserPassesTestMixin     # 사용자 테스트
PermissionRequiredMixin # 권한 확인
 
# 컨텍스트 관련
ContextMixin           # 컨텍스트 데이터 추가
TemplateResponseMixin  # 템플릿 렌더링
 
# 날짜 관련
YearMixin             # 연도별 데이터
MonthMixin            # 월별 데이터
DayMixin             # 일별 데이터
 
################################
 
python manage.py makemigrations
python manage.py migrate
 
python manage.py runserver
 
################################
 
게시물 1번, 2번, 3번에서 작성자를 나로 고치고 오겠습니다.
 
################################
 
# blog/post_list.html
 
<h2>Post List</h2>
<p>환영합니다! {{ user.username }}!!</p>
<p>환영합니다! {{ request.user.username }}!!</p>
<p>환영합니다! {{ request.user.is_authenticated }}!!</p>
 
{# 로그아웃은 post로 보내야 합니다. <a href="">로그아웃</a> 형식으로 보내면 안됩니다. #}
 
<form action="{% url 'logout' %}" method="post">
    {% csrf_token %}
    <button type="submit">로그아웃</button>
</form>
 
 
<a href="{% url 'blog_write' %}">글쓰기</a>
<form action="" method="get">
    <input name="q" type="text">
    <button type="submit">검색</button>
</form>
<ul>
    {% for post in object_list %}
    <li>
        <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>
 
<!-- 좀 더 자주사용하는 페이지네이션 -->
{% if is_paginated %}
<div>
    {% if page_obj.has_previous %}
        <a href="?page=1">처음</a>
        <a href="?page={{ page_obj.previous_page_number }}">이전</a>
    {% endif %}
 
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <span>{{ num }}</span>
        {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
 
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">다음</a>
        <a href="?page={{ page_obj.paginator.num_pages }}">마지막</a>
    {% endif %}
</div>
{% endif %}
 
################################
 
# config/settings.py
 
LOGIN_REDIRECT_URL = '/blog/'
LOGOUT_REDIRECT_URL = '/accounts/login/'
 
################################
# blog/post_detail.html
 
<h2>Post Detail</h2>
<h3>{{ object.title }}</h3>
<p>{{ object.content }}</p>
{% if object.head_image %}
<img src="{{ object.head_image.url }}" alt="{{ object.head_image }}">
{% endif %}
{% if object.file_upload %}
<a href="{{ object.file_upload.url }}">{{ object.file_upload }}</a>
{% endif %}
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
 
{% if object.author == user %}
<a href="{% url 'blog_edit' object.pk %}">수정</a>
<form action="{% url 'blog_delete' object.pk %}" method="post">
    {% csrf_token %}
    <input type="submit" value="삭제">
</form>
{% endif %}
 
<a href="{% url 'blog_list' %}">목록으로 가기</a>
 
################################
'''
이제 다음 기능들이 구현되었습니다.
1. 회원가입, 로그인, 로그아웃
2. 로그인한 사용자만 글 목록과 상세 페이지 접근 가능
3. 글 작성시 작성자 자동 지정
4. 본인이 작성한 글만 수정/삭제 가능
5. 글 목록에서 본인 글에만 수정/삭제 버튼 표시
'''
 
################################
# 1시간 정도는 이전에 했었던 코드 실습 + 고도화
0. 복습
1. Tag, Category, User(이미지) 고도화
2. 프로필
3. 검색(단지 input창에서 검색을 하는 것이 아닌 post_list에서 카테고리, 작성자 등을 사이드바 등에서 클릭했을 때에도 검색이 되게 구현)
# 목표
 
1. Django에 CBV를 경험해본다.
2. CBV로 프로젝트를 구현 경험을 쌓는다.
    - 인증
    - CRUD(+인증)
 
# 코드
 
#####################################
mkdir cbv
cd cbv
python -m venv venv
.\venv\Scripts\activate
 
pip install django pillow
 
django-admin startproject config .
python manage.py migrate
 
python manage.py startapp blog
 
 
#####################################
# config/settings.py
 
 
ALLOWED_HOSTS = ['*']
 
 
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",
]
 
"DIRS": [BASE_DIR / "templates"],
 
LANGUAGE_CODE = "ko-kr"
 
TIME_ZONE = "Asia/Seoul"
 
 
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
 
 
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
 
 
#####################################
 
mkdir templates
mkdir static
mkdir media
 
#####################################
# Model 설계
Post
    title
    content
    image
    file_upload
    created_at
    updated_at
 
# URL 설계
다음 복습에서도 아래와 같은 URL이 만들어지도록 해보세요.
    /blog/                    : 블로그 글 목록
    /blog/<int:pk>/           : 블로그 글 상세
    /blog/write/              : 블로그 글 작성
    /blog/edit/<int:pk>/      : 블로그 글 수정
    /blog/delete/<int:pk>/    : 블로그 글 삭제
 
# 앱 설계
앱 이름: blog                URL 연결 이름               HTML 파일 이름
    /blog/                  : blog_list                 post_list.html
    /blog/<int:pk>/         : blog_detail               post_detail.html
    /blog/write/            : blog_write                post_write.html
    /blog/edit/<int:pk>/    : blog_edit                 post_edit.html
    /blog/delete/<int:pk>/  : blog_delete               post_delete.html
 
 
#####################################
# config/urls.py
 
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from django.http import HttpResponse
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("settings/", lambda request: HttpResponse(settings.MEDIA_URL)),
    path("blog/", include("blog.urls")),
]
 
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 
 
#####################################
# blog/urls.py
 
from django.urls import path
from . import views
 
urlpatterns = [
    path("", views.blog_list, name="blog_list"),
    path("<int:pk>/", views.blog_detail, name="blog_detail"),
    path("write/", views.blog_write, name="blog_write"),
    path("edit/<int:pk>/", views.blog_edit, name="blog_edit"),
    path("delete/<int:pk>/", views.blog_delete, name="blog_delete"),
    path("test/", views.test, name="test"),
]
 
#####################################
# blog/views.py
 
from django.views.generic import (
    ListView,
    DetailView,
    CreateView,
    UpdateView,
    DeleteView,
)
from django.urls import reverse_lazy
from django.http import HttpResponse
 
from .models import Post
 
# 클래스 기반 뷰 == 제네릭 뷰는 아닙니다.
# 기본 뷰를 상속받은 클래스 기반이면 다 클래스 기반 뷰 입니다.
# 그중에서도 제네릭 뷰는 특정 목적을 가진(여러가지 기능들을 가지고 있는) 클래스 기반 뷰 입니다.
 
 
class PostList(ListView):
    model = Post
    ordering = "-pk"
    # 최신 글이 맨 위로 오도록 정렬
    # 기본 설정
    # template_name = '<app_name>/<model_name>_list.html' 형식의 기본 템플릿
    # context_object_name = 'object_list'라는 이름으로 템플릿에 객체 리스트를 전달
 
 
class PostDetail(DetailView):
    model = Post
    # 기본 설정
    # template_name = '<app_name>/<model_name>_detail.html' 형식의 기본 템플릿
    # context_object_name = 'object'라는 이름으로 템플릿에 객체를 전달
 
 
class PostCreate(CreateView):
    model = Post
    fields = "__all__"
    success_url = reverse_lazy("blog_list")
    # 기본 설정
    # template_name = '<app_name>/<model_name>_form.html' 형식의 기본 템플릿
    # context_object_name = 'object'라는 이름으로 템플릿에 객체를 전달
    # success_url = reverse_lazy('<app_name>:<model_name>_list') 형식의 기본 리다이렉션 URL
    # fields = '__all__' 모든 필드를 입력받는 기본 폼
    # fields = ('<field_name1>', '<field_name2>', ...) 특정 필드만 입력받는 기본 폼
    # reverse_lazy와 reverse가 있습니다. reverse도 특정 URL을 찾아 리다이렉션 할 수 있습니다. 함수가 실행되는 시점에 이동합니다. 반면, reverse_lazy는 Post가 생성된 이후에 실행됩니다. blog_list로 가기 위해서는 Post가 생성된 다음 URL을 이동해야하기 때문에 reverse_lazy를 사용합니다.
 
 
class PostUpdate(UpdateView):
    model = Post
    fields = "__all__"
    success_url = reverse_lazy("blog_list")
    # 기본 설정
    # template_name = '<app_name>/<model_name>_form.html' 형식의 기본 템플릿
    # context_object_name = 'object'라는 이름으로 템플릿에 객체를 전달
    # success_url = reverse_lazy('<app_name>:<model_name>_list') 형식의 기본 리다이렉션 URL
    # fields = '__all__' 모든 필드를 입력받는 기본 폼
    # fields = ('<field_name1>', '<field_name2>', ...) 특정 필드만 입력받는 기본 폼
 
 
class PostDelete(DeleteView):
    model = Post
    success_url = reverse_lazy("blog_list")
    # 기본 설정
    # template_name = '<app_name>/<model_name>_confirm_delete.html' 형식의 기본 템플릿
    # context_object_name = 'object'라는 이름으로 템플릿에 객체를 전달
    # success_url = reverse_lazy('<app_name>:<model_name>_list') 형식의 기본 리다이렉션 URL
 
 
class PostTest(CreateView):
    model = Post
 
    def get(self, request):
        return HttpResponse("GET 요청을 잘 받았습니다.")
 
    def post(self, request):
        return HttpResponse("POST 요청을 잘 받았습니다.")
 
    # 앞 수업에서는 View를 상속받아 get, post 메서드를 오버라이딩하여 사용했습니다.
    # 이번에는 이 View를 상속받은 CreateView를 상속받아 get, post 메서드를 오버라이딩하여 사용했습니다.
 
 
# 실무에서는 이 as_view() 메서드를 사용하여 URL 패턴에 클래스 기반 뷰를 등록합니다.
# 우리는 FBV와 CBV를 urls.py는 수정하지 않고 수정할 수 있도록 아래와 같이 변수를 만들어 사용하겠습니다.
blog_list = PostList.as_view()
blog_detail = PostDetail.as_view()
blog_write = PostCreate.as_view()
blog_edit = PostUpdate.as_view()
blog_delete = PostDelete.as_view()
test = PostTest.as_view()
 
 
###################################
 
Django에서 ListView와 같은 일반적인 Class-Based Views (CBV)를 사용할 때, 템플릿 이름은 기본적으로 다음과 같은 규칙을 따라 자동으로 생성됩니다.
 
PostList (ListView)
템플릿 이름 규칙: <app_name>/<model_name_소문자>_list.html
기본 템플릿: <app_name>/post_list.html
템플릿 접근 방법:
{% for post in object_list %}
    {{ post.title }}
{% endfor %}
 
 
PostDetail (DetailView)
템플릿 이름 규칙: <app_name>/<model_name_소문자>_detail.html
기본 템플릿: <app_name>/post_detail.html
템플릿 접근 방법: 
{{ object.title }}
 
 
PostCreate (CreateView)
템플릿 이름 규칙: <app_name>/<model_name_소문자>_form.html
기본 템플릿: <app_name>/post_form.html
템플릿 접근 방법:
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Create</button>
</form>
 
 
PostUpdate (UpdateView)
템플릿 이름 규칙: <app_name>/<model_name_소문자>_form.html
기본 템플릿: <app_name>/post_form.html
템플릿 접근 방법:
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Update</button>
</form>
 
 
PostDelete (DeleteView)
템플릿 이름 규칙:  <app_name>/<model_name_소문자>_confirm_delete.html
기본 템플릿: <app_name>/post_confirm_delete.html
템플릿 접근 방법:
<form method="post">
    {% csrf_token %}
    Are you sure you want to delete "{{ object.title }}"?
    <button type="submit">Delete</button>
</form>
 
 
* CreateView와 UpdateView는 같은 템플릿 이름 규칙을 사용합니다. 그래서 둘 다 _form.html을 기본으로 사용합니다.
 
################################
# templates/blog/post_list.html
 
<h2>Post List</h2>
<ul>
    {% for post in object_list %}
        <li>
            <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
        </li>
    {% endfor %}
</ul>
 
################################
# templates/blog/post_detail.html
 
<h2>Post Detail</h2>
<h3>{{ object.title }}</h3>
<p>{{ object.content }}</p>
{% if object.head_image %}
<img src="{{ object.head_image.url }}" alt="{{ object.head_image }}">
{% endif %}
{% if object.file_upload %}
<a href="{{ object.file_upload.url }}">{{ object.file_upload }}</a>
{% endif %}
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
 
<a href="{% url 'blog_list' %}">목록으로 가기</a>
 
 
################################
# templates/blog/post_form.html
 
<h2>Post Form</h2>
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">저장</button>
</form>
 
################################
# templates/blog/post_confirm_delete.html
 
<h2>Delete</h2>
<form method="post">
    {% csrf_token %}
    <p>"{{ object.title }}"을 정말로 삭제하시겠습니까?</p>
    <a href="{% url 'blog_detail' object.pk %}">취소</a>
    <button type="submit">삭제</button>
</form>
 
################################
# blog/models.py
 
from django.db import models
 
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    head_image = models.ImageField(upload_to="blog/images/%Y/%m/%d/", blank=True)
    file_upload = models.FileField(upload_to="blog/files/%Y/%m/%d/", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)
 
    def __str__(self):
        return self.title
 
################################
 
from django.db import models
 
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    head_image = models.ImageField(upload_to="blog/images/%Y/%m/%d/", blank=True)
    file_upload = models.FileField(upload_to="blog/files/%Y/%m/%d/", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)
 
    def __str__(self):
        return self.title
    
 
# 이렇게 upload_to에 함수를 사용하여 동적으로 파일 경로를 생성할 수도 있습니다.
# def get_upload_path(instance, filename, base_path):
#     """
#     파일 업로드 경로를 생성하는 함수
#     instance: 모델 인스턴스
#     filename: 업로드된 파일 이름
#     base_path: 기본 경로 (예: "blog/images" 또는 "blog/files")
#     """
#     import datetime
#     now = datetime.datetime.now()
#     return f"{base_path}/{now.year}/{now.month:02d}/{now.day:02d}/{filename}"
 
# class Post(models.Model):
#     title = models.CharField(max_length=100)
#     content = models.TextField()
#     head_image = models.ImageField(
#         upload_to=lambda instance, filename: get_upload_path(
#             instance, filename, "blog/images"
#         ),
#         blank=True
#     )
#     file_upload = models.FileField(
#         upload_to=lambda instance, filename: get_upload_path(
#             instance, filename, "blog/files"
#         ),
#         blank=True
#     )
#     created_at = models.DateTimeField(auto_now_add=True)
#     updated_at = models.DateField(auto_now=True)
 
#     def __str__(self):
#         return self.title
 
 
################################
# blog/admin.py
 
from django.contrib import admin
from .models import Post
 
admin.site.register(Post)
 
 
################################
 
python manage.py makemigrations
python manage.py migrate
 
################################
 
python manage.py createsuperuser
 
leehojun
leehojun@gmail.com
이호준1234!
 
################################
 
python manage.py runserver
 
admin에서 게시물 10개 생성(나중에 테스트를 위해 10개가 필요합니다.)
 
################################
 
http://127.0.0.1:8000/blog/
http://127.0.0.1:8000/blog/1/
http://127.0.0.1:8000/blog/write/ # form.py를 작성하지 않아도 작동!
http://127.0.0.1:8000/blog/edit/1/
http://127.0.0.1:8000/blog/delete/1/
 
################################
# 스스로 코딩을 하시고 error가 났다라고 한다면, 필수적인 항목을 작성하지 않아서 그렇습니다.
# 아래 2가지 방식 중 하나를 택해서 지정해야 합니다.
# 1.
class PostCreate(CreateView):
    model = Post
    fields = '__all__'
    success_url = '/blog/'  # 이 URL로 리디렉션됩니다. 다만 상세 페이지로는 못갑니다.
 
# 2.
from django.urls import reverse
 
class PostCreate(CreateView):
    model = Post
    fields = '__all__'
 
    def get_success_url(self):
        return reverse('blog_details', args=[str(self.object.pk)])
 
 
################################
# 검색기능 추가
# blog/post_list.html
<h2>Post List</h2>
<form action="" method="get">
    <input type="text" name="q">
    <input type="submit" value="Search">
</form>
<ul>
    {% for post in object_list %}
        <li>
            <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
        </li>
    {% endfor %}
</ul>
 
################################
# blog/views.py
 
class PostList(ListView):
    model = Post
    ordering = "-pk"
    # 최신 글이 맨 위로 오도록 정렬
    # 기본 설정
    # template_name = '<app_name>/<model_name>_list.html' 형식의 기본 템플릿
    # context_object_name = 'object_list'라는 이름으로 템플릿에 객체 리스트를 전달
 
    def get_queryset(self):
        # queryset = super().get_queryset()
        # queryset = Post.objects.all()
        queryset = super().get_queryset()
        q = self.request.GET.get("q", "")
        if q:
            queryset = queryset.filter(Q(title__contains=q) | Q(content__contains=q))
        return queryset
 
################################
 
python manage.py runserver로 지난시간까지 작동 되었던 것 확인
 
ListView의 기본 get_queryset 메서드는 model 속성에서 정의된(model = Post) 모델의 전체 객체 목록을 반환하며 PostList에 선언된 ordering = "-pk" 값 등을 반영한 쿼리셋을 반환합니다.
 
super().get_queryset() 호출은 Post 모델의 전체 객체 목록을 반환합니다.
 
get_queryset 함수와 get_context_data 함수의 기능을 혼동할 수 있습니다. get_queryset 함수는 쿼리셋을 반환하는 함수이며 이 쿼리셋을 템플릿에서 주로 사용합니다. get_context_data 함수는 템플릿에 추가 데이터를 전달할 때 사용하는 함수입니다.
 
################################
 
공식문서
https://docs.djangoproject.com/en/5.1/ref/class-based-views/
ListVeiw : https://docs.djangoproject.com/en/5.1/ref/class-based-views/generic-display/
CreateView : https://docs.djangoproject.com/en/5.1/ref/class-based-views/generic-editing/
 
################################
 
Base views
    View : 최상위 제네릭 뷰, 기본 뷰, django view를 만드는데 필요한 기능 제공
    TemplateView : 템플릿이 주어지면 렌더링을 해주는 뷰
    RedirectView : URL이 주어지면 리다이렉트 해주는 뷰
 
Generic display views
    DetailView : model과 템플릿 받아 조건에 맞는 상세 오브젝트를 보여줍니다.
    ListView : model과 템플릿 받아 전체 오브젝트를 보여줍니다.
 
Generic editing views
    FormView : 폼을 보여주고 처리합니다.
    CreateView : 폼을 보여주고 객체를 생성합니다.
    UpdateView : 폼을 조건에 맞게 보여주고 객체를 수정합니다. 
    DeleteView : 객체를 삭제합니다.
 
Generic date views
    ArchiveIndexView : 조건에 맞는 객체의 날짜 정보를 출력합니다.
    YearArchiveView : 연도에 맞는 객체를 출력합니다.
    MonthArchiveView : 월에 맞는 객체를 출력합니다.
    WeekArchiveView : 주에 맞는 객체를 출력합니다.
    DayArchiveView : 일에 맞는 객체를 출력합니다.
    TodayArchiveView : 오늘 날짜에 객체를 출력합니다.
    DateDetailView : 연, 월, 일 조건에 맞는 객체를 출력합니다.
 
################################
 
# 공식 홈페이지 샘프로 코드 포함
 
Base views
    View : 최상위 제네릭 뷰, 기본 뷰, django view를 만드는데 필요한 기능 제공
    TemplateView : 템플릿이 주어지면 렌더링을 해주는 뷰
    '''
    from django.views.generic.base import TemplateView
    from articles.models import Article
 
    class HomePageView(TemplateView):
        template_name = "home.html"
 
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context["latest_articles"] = Article.objects.all()[:5]
            return context
    '''
    RedirectView : URL이 주어지면 리다이렉트 해주는 뷰
 
Generic display views
    DetailView : model과 템플릿 받아 조건에 맞는 상세 오브젝트를 보여줍니다.
    '''
    from django.utils import timezone
    from django.views.generic.detail import DetailView
    from articles.models import Article
 
    class ArticleDetailView(DetailView):
        model = Article
 
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context["now"] = timezone.now()
            return context
    '''
    '''
    <h1>{{ object.headline }}</h1>
    <p>{{ object.content }}</p>
    <p>Reporter: {{ object.reporter }}</p>
    <p>Published: {{ object.pub_date|date }}</p>
    <p>Date: {{ now|date }}</p>
    '''
    ListView : model과 템플릿 받아 전체 오브젝트를 보여줍니다.
    '''
    from django.utils import timezone
    from django.views.generic.detail import DetailView
    from articles.models import Article
 
    class ArticleDetailView(DetailView):
        model = Article
 
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context["now"] = timezone.now()
            return context
    '''
    '''
    <h1>{{ object.headline }}</h1>
    <p>{{ object.content }}</p>
    <p>Reporter: {{ object.reporter }}</p>
    <p>Published: {{ object.pub_date|date }}</p>
 
    <p>Date: {{ now|date }}</p>
    '''
 
Generic editing views
    FormView : 폼을 보여주고 처리합니다.
    CreateView : 폼을 보여주고 객체를 생성합니다.
    UpdateView : 폼을 조건에 맞게 보여주고 객체를 수정합니다. 
    DeleteView : 객체를 삭제합니다.
 
Generic date views
    ArchiveIndexView : 조건에 맞는 객체의 날짜 정보를 출력합니다.
    YearArchiveView : 연도에 맞는 객체를 출력합니다.
    MonthArchiveView : 월에 맞는 객체를 출력합니다.
    WeekArchiveView : 주에 맞는 객체를 출력합니다.
    DayArchiveView : 일에 맞는 객체를 출력합니다.
    TodayArchiveView : 오늘 날짜에 객체를 출력합니다.
    DateDetailView : 연, 월, 일 조건에 맞는 객체를 출력합니다.
    '''
    # views.py
    from django.views.generic.dates import WeekArchiveView
    from myapp.models import Article
 
    class ArticleWeekArchiveView(WeekArchiveView):
        queryset = Article.objects.all()
        date_field = "pub_date"
        week_format = "%W"
        allow_future = True
    
    # urls.py
    from django.urls import path
    from myapp.views import ArticleWeekArchiveView
 
    urlpatterns = [
        # Example: /2012/week/23/
        path(
            "<int:year>/week/<int:week>/",
            ArticleWeekArchiveView.as_view(),
            name="archive_week",
        ),
    ]
 
    # article_archive_week.html:
    <h1>Week {{ week|date:'W' }}</h1>
 
    <ul>
        {% for article in object_list %}
            <li>{{ article.pub_date|date:"F j, Y" }}: {{ article.title }}</li>
        {% endfor %}
    </ul>
 
    <p>
        {% if previous_week %}
            Previous Week: {{ previous_week|date:"W" }} of year {{ previous_week|date:"Y" }}
        {% endif %}
        {% if previous_week and next_week %}--{% endif %}
        {% if next_week %}
            Next week: {{ next_week|date:"W" }} of year {{ next_week|date:"Y" }}
        {% endif %}
    </p>
    '''
 
###################################
 
# 이렇게 한 이유는 test 코드가 잘 작동되는지 확인하려는 의도가 있고
# CSRF 토큰을 별도 발급하지 않아도 되는 코드이기 때문입니다.
# 어제 미들웨어 설치 하려고 했었던 예제(배보다 배꼽이 더 큰 예제)
 
http://127.0.0.1:8000/blog/test/
http://127.0.0.1:8000/blog/write/로 들어가서 <form action="http://127.0.0.1:8000/blog/test/" method="post">으로 수정한 다음 post로 날려 테스트
 
###################################
 
# 만약 게시글이 1000개이고 한 페이지에 10개씩 보여주어야 한다면 함수형은 아래와 같이 구현했을 것입니다.
 
posts = Post.objects.all()
# posts의 길이를 구하고, 길이를 10으로 나누고, 올림을 하면 페이지 수가 나오고
# 처음에 접속했을 때에는 [:10]을 해서 보여주고, 2페이지로 넘어갔을 때에는 [10:20]을 해서 보여주고....
 
# 이런식으로 구현하는 것을 제네릭 뷰는 자동으로 해줍니다.
 
###################################
 
# blog/views.py
 
class PostList(ListView):
    model = Post
    ordering = "-pk"
    paginate_by = 3 # 이 부분만 추가해주세요. 다른 코드는 만지지 마세요.
 
################################
 
# test
http://127.0.0.1:8000/blog/
http://127.0.0.1:8000/blog/?page=1
http://127.0.0.1:8000/blog/?page=2
http://127.0.0.1:8000/blog/?page=3
http://127.0.0.1:8000/blog/?page=4
 
################################
 
# blog_list.html
 
<h2>Post List</h2>
<form action="" method="get">
    <input name="q" type="text">
    <button type="submit">검색</button>
</form>
<ul>
    {% for post in object_list %}
    <li>
        <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>
 
<!-- 가장 기본적인 페이지네이션 -->
<div>
    {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">이전</a>
    {% endif %}
 
    <span>{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
 
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">다음</a>
    {% endif %}
</div>
 
################################
 
page_obj.number          # 현재 페이지 번호
page_obj.has_previous    # 이전 페이지 존재 여부 (True/False)
page_obj.has_next        # 다음 페이지 존재 여부 (True/False)
page_obj.previous_page_number  # 이전 페이지 번호
page_obj.next_page_number      # 다음 페이지 번호
page_obj.paginator.num_pages   # 전체 페이지 수
page_obj.paginator.count       # 전체 객체(게시물) 수
 
################################
 
<h2>Post List</h2>
<form action="" method="get">
    <input name="q" type="text">
    <button type="submit">검색</button>
</form>
<ul>
    {% for post in object_list %}
    <li>
        <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>
 
<!-- 좀 더 자주사용하는 페이지네이션 -->
{% if is_paginated %}
<div>
    {% if page_obj.has_previous %}
        <a href="?page=1">처음</a>
        <a href="?page={{ page_obj.previous_page_number }}">이전</a>
    {% endif %}
 
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <span>{{ num }}</span>
        {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
 
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">다음</a>
        <a href="?page={{ page_obj.paginator.num_pages }}">마지막</a>
    {% endif %}
</div>
{% endif %}
 
################################
 
# 기존 URL 설계
다음 복습에서도 아래와 같은 URL이 만들어지도록 해보세요.
    /blog/                    : 블로그 글 목록(로그인X)
    /blog/<int:pk>/           : 블로그 글 상세(로그인O)
    /blog/write/              : 블로그 글 작성(로그인O)
    /blog/edit/<int:pk>/      : 블로그 글 수정(로그인O, 작성자만)
    /blog/delete/<int:pk>/    : 블로그 글 삭제(로그인O, 작성자만)
 
# accounts URL 설계
다음 URL이 만들어지도록 해보세요.
    /accounts/signup/         : 회원가입
    /accounts/login/          : 로그인
    /accounts/logout/         : 로그아웃
 
################################
 
python manage.py startapp accounts
 
################################
# config/settings.py
 
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",
    "accounts",
]
 
################################
# config/urls.py
 
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from django.http import HttpResponse
 
urlpatterns = [
    path("admin/", admin.site.urls),
    path("settings/", lambda request: HttpResponse(settings.NAME)),
    path("blog/", include("blog.urls")),
    path("accounts/", include("accounts.urls")),
]
 
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 
################################
# accounts/urls.py
 
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
 
urlpatterns = [
    path("signup/", views.RegisterView.as_view(), name="register"),
    path(
        "login/",
        auth_views.LoginView.as_view(template_name="accounts/login.html"),
        name="login",
    ),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]
 
 
################################
# accounts/views.py
 
from django.contrib.auth.forms import UserCreationForm
from django.views.generic import CreateView
from django.urls import reverse_lazy
 
 
class RegisterView(CreateView):
    form_class = UserCreationForm
    template_name = "accounts/register.html"
    success_url = reverse_lazy("login")
 
################################
# templates > accounts > register.html 생성
 
<h2>회원가입</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">가입하기</button>
</form>
 
################################
# templates > accounts > login.html 생성
 
<h2>로그인</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">로그인</button>
</form>
 
################################
 
test
 
http://127.0.0.1:8000/accounts/signup/
http://127.0.0.1:8000/accounts/login/
 
################################
# blog/models.py
 
from django.db import models
 
 
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    head_image = models.ImageField(upload_to="blog/images/%Y/%m/%d/", blank=True)
    file_upload = models.FileField(upload_to="blog/files/%Y/%m/%d/", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)
    author = models.ForeignKey(
        "auth.User",
        on_delete=models.CASCADE,
        verbose_name="작성자",
        blank=True,
        null=True,
    )
 
    def __str__(self):
        return self.title
 
################################
Mixin은 FBV에 @login_required와 같은 편의 기능을 CBV에서 제공하는 역할을 합니다. 이러한 역할을 하는 믹스인은 아래와 같습니다.
 
Mixin	설명
LoginRequiredMixin	로그인한 사용자만 뷰에 접근할 수 있도록 합니다.
PermissionRequiredMixin	특정 권한을 가진 사용자만 뷰에 접근할 수 있도록 합니다.
UserPassesTestMixin	사용자 정의 함수를 사용하여 사용자의 접근 권한을 제어합니다.
FormMixin	폼을 처리하는 데 필요한 메서드를 제공합니다.
ModelFormMixin	모델 폼을 처리하는 데 필요한 메서드를 제공합니다.
SingleObjectMixin	단일 객체를 처리하는 데 필요한 메서드를 제공합니다.
MultipleObjectMixin	객체 목록을 처리하는 데 필요한 메서드를 제공합니다.
 
################################
# blog/views.py
 
from django.views.generic import (
    ListView,
    DetailView,
    CreateView,
    UpdateView,
    DeleteView,
)
from django.urls import reverse_lazy
from django.http import HttpResponse
from django.db.models import Q
from .models import Post
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
 
 
class PostList(ListView):
    model = Post
    ordering = "-pk"
    paginate_by = 3
 
    def get_queryset(self):
        queryset = super().get_queryset()
        q = self.request.GET.get("q", "")
        if q:
            queryset = queryset.filter(Q(title__contains=q) | Q(content__contains=q))
        return queryset
 
 
class PostDetail(LoginRequiredMixin, DetailView):
    model = Post
    login_url = "/accounts/login/"
 
 
class PostCreate(LoginRequiredMixin, CreateView):
    model = Post
    fields = ["title", "content", "head_image", "file_upload"]
    success_url = reverse_lazy("blog_list")
    login_url = "/accounts/login/"
 
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
 
 
class PostUpdate(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    fields = "__all__"
    success_url = reverse_lazy("blog_list")
    login_url = "/accounts/login/"
 
    def test_func(self):
        post = self.get_object()
        return post.author == self.request.user
 
 
class PostDelete(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Post
    success_url = reverse_lazy("blog_list")
    login_url = "/accounts/login/"
 
    def test_func(self):
        post = self.get_object()
        return post.author == self.request.user
 
 
class PostTest(CreateView):
    model = Post
 
    def get(self, request):
        return HttpResponse("GET 요청을 잘 받았습니다.")
 
    def post(self, request):
        return HttpResponse("POST 요청을 잘 받았습니다.")
 
 
blog_list = PostList.as_view()
blog_detail = PostDetail.as_view()
blog_write = PostCreate.as_view()
blog_edit = PostUpdate.as_view()
blog_delete = PostDelete.as_view()
test = PostTest.as_view()
 
################################
 
LoginRequiredMixin:
로그인한 사용자만 접근할 수 있도록 제한하는 Mixin입니다
함수형 뷰의 @login_required 데코레이터와 동일한 역할을 합니다
 
 
UserPassesTestMixin:
특정 테스트 함수를 통과한 사용자만 접근을 허용하는 Mixin입니다
test_func() 메서드를 오버라이드하여 접근 조건을 정의합니다
false일 경우 403 Forbidden 페이지를 보여줍니다
 
# 인증 관련
LoginRequiredMixin       # 로그인 필수
UserPassesTestMixin     # 사용자 테스트
PermissionRequiredMixin # 권한 확인
 
# 컨텍스트 관련
ContextMixin           # 컨텍스트 데이터 추가
TemplateResponseMixin  # 템플릿 렌더링
 
# 날짜 관련
YearMixin             # 연도별 데이터
MonthMixin            # 월별 데이터
DayMixin             # 일별 데이터
 
################################
 
python manage.py makemigrations
python manage.py migrate
 
python manage.py runserver
 
################################
 
게시물 1번, 2번, 3번에서 작성자를 나로 고치고 오겠습니다.
 
################################
 
# blog/post_list.html
 
<h2>Post List</h2>
<p>환영합니다! {{ user.username }}!!</p>
<p>환영합니다! {{ request.user.username }}!!</p>
<p>환영합니다! {{ request.user.is_authenticated }}!!</p>
 
{# 로그아웃은 post로 보내야 합니다. <a href="">로그아웃</a> 형식으로 보내면 안됩니다. #}
 
<form action="{% url 'logout' %}" method="post">
    {% csrf_token %}
    <button type="submit">로그아웃</button>
</form>
 
 
<a href="{% url 'blog_write' %}">글쓰기</a>
<form action="" method="get">
    <input name="q" type="text">
    <button type="submit">검색</button>
</form>
<ul>
    {% for post in object_list %}
    <li>
        <a href="{% url 'blog_detail' post.pk %}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>
 
<!-- 좀 더 자주사용하는 페이지네이션 -->
{% if is_paginated %}
<div>
    {% if page_obj.has_previous %}
        <a href="?page=1">처음</a>
        <a href="?page={{ page_obj.previous_page_number }}">이전</a>
    {% endif %}
 
    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
            <span>{{ num }}</span>
        {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
            <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}
 
    {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">다음</a>
        <a href="?page={{ page_obj.paginator.num_pages }}">마지막</a>
    {% endif %}
</div>
{% endif %}
 
################################
 
# config/settings.py
 
LOGIN_REDIRECT_URL = '/blog/'
LOGOUT_REDIRECT_URL = '/accounts/login/'
 
################################
# blog/post_detail.html
 
<h2>Post Detail</h2>
<h3>{{ object.title }}</h3>
<p>{{ object.content }}</p>
{% if object.head_image %}
<img src="{{ object.head_image.url }}" alt="{{ object.head_image }}">
{% endif %}
{% if object.file_upload %}
<a href="{{ object.file_upload.url }}">{{ object.file_upload }}</a>
{% endif %}
<p>{{ object.created_at }}</p>
<p>{{ object.updated_at }}</p>
 
{% if object.author == user %}
<a href="{% url 'blog_edit' object.pk %}">수정</a>
<form action="{% url 'blog_delete' object.pk %}" method="post">
    {% csrf_token %}
    <input type="submit" value="삭제">
</form>
{% endif %}
 
<a href="{% url 'blog_list' %}">목록으로 가기</a>
 
################################
'''
이제 다음 기능들이 구현되었습니다.
1. 회원가입, 로그인, 로그아웃
2. 로그인한 사용자만 글 목록과 상세 페이지 접근 가능
3. 글 작성시 작성자 자동 지정
4. 본인이 작성한 글만 수정/삭제 가능
5. 글 목록에서 본인 글에만 수정/삭제 버튼 표시
'''
 
################################
# 1시간 정도는 이전에 했었던 코드 실습 + 고도화
0. 복습
1. Tag, Category, User(이미지) 고도화
2. 프로필
3. 검색(단지 input창에서 검색을 하는 것이 아닌 post_list에서 카테고리, 작성자 등을 사이드바 등에서 클릭했을 때에도 검색이 되게 구현)
6.3 Class Based Views6.5 CBV CRUD