[day-40] 유튜브 클론코딩(수정)

Joohyung Park·2024년 3월 4일
0

[모두연] 오름캠프

목록 보기
74/95

이전글

달라진 부분 위주로 설명하겠다.

계정 관련 템플릿

# templates > accounts > form.html

<form action="" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <input type="submit" value="Submit">
</form>

계정 관련 폼에 왜 파일을 업로드 하는 enctype="multipart/form-data" 가 있는지 의문이 들 수 있는데.. 이는 프로필 이미지 용으로 넣은 것이다.

업로드한 파일은 request.FILES 딕셔너리로 접근 가능하다.

프로필

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>프로필</title>
</head>
<body>
    <h1>프로필 페이지입니다.</h1>
    <p>{{user}}의 프로필 페이지입니다.</p>
</body>
</html>

간단한 문구만 나오는 프로필 html 파일이다.

tube앱 모델 관련 수정

모델 수정

# tube > models.py

from django.db import models
from django.contrib.auth.models import User


class Post(models.Model):
    author = models.ForeignKey(
        User, on_delete=models.CASCADE
    )
    title = models.CharField(max_length=100)
    content = models.TextField()
    thumb_image = models.ImageField(
        upload_to='tube/images/%Y/%m/%d/', blank=True)
    file_upload = models.FileField(
        upload_to='tube/files/%Y/%m/%d/', blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)
    view_count = models.PositiveIntegerField(default=0)
    tags = models.ManyToManyField('Tag', blank=True)

    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        return f'/tube/{self.pk}/'
    
class Comment(models.Model):
    post = models.ForeignKey(
        Post, on_delete=models.CASCADE, related_name='comments'
    )
    author = models.ForeignKey(
        User, on_delete=models.CASCADE
    )
    message = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)
    
    def __str__(self):
        return self.message
    
class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    def __str__(self):
        return self.name

view_count(조회수) 필드가 새로 추가되었으며, video_file 필드가 사라진 모습이다.

Comment(댓글) 클래스의 related_namecomments로 설정한 덕분에 Post 클래스에서도 comments로 접근 가능하다.

DB 반영

python manage.py makemigrations
python manage.py migrate

admin 페이지 연결

tube앱 관련 수정

모델 관련 폼 선언

# tube > forms.py

from django import forms
from .models import Post, Comment, Tag

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        # fields = '__all__'
        fields = ['title', 'content', 'thumb_image', 'file_upload'] # counter같은 값은 건들면 안되니까!


class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['message']


class TagForm(forms.ModelForm):
    class Meta:
        model = Tag
        fields = ['name']

각 클래스 안에 Meta 클래스를 선언하여 CBV 모델에 대한 취급 방법(DB와 상호작용하는 방식)을 변경해주었다. 이렇게 선언하면 관리자 페이지의 필드에는 설정한 값만 나오게 된다.

url 선언

# tube > urls.py

from django.urls import path
from . import views

app_name = 'tube'

# {% url 'tube:post_list' %}
# {% url 'tube:post_detail' post.pk %}

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('new/', views.post_new, name='post_new'),
    path('<int:pk>/', views.post_detail, name='post_detail'),
    path('<int:pk>/edit/', views.post_edit, name='post_edit'),
    path('<int:pk>/delete/', views.post_delete, name='post_delete'),
]

프로젝트의 크기가 커지게 되면, app_name = 'tube'와 같이 선언하여 사용한다. 작은 프로젝트에서는 넘어가자.

{% url 'tube:post_detail' post.pk %} 과 같은 주석은 app_name을 적용시켰을 시 url의 모습을 보여준다.

화면 상에 보여줄 내용 선언

from django.shortcuts import render
from django.views.generic import ListView, DeleteView, UpdateView, DetailView, CreateView
from .models import Post, Comment
from .forms import PostForm, CommentForm
from django.urls import reverse_lazy
  • 포스트 목록
class PostListView(ListView):
    model = Post

post_list = PostListView.as_view()
  • 포스트 생성
class PostCreateView(CreateView):
    model = Post
    form_class = PostForm
    success_url = reverse_lazy('tube:post_list')
    template_name = 'tube/form.html'

post_new = PostCreateView.as_view()
  • 포스트 세부사항
class PostDetailView(DetailView):
    model = Post

post_detail = PostDetailView.as_view()
  • 포스트 수정
class PostUpdateView(UpdateView):
    model = Post
    form_class = PostForm
    success_url = reverse_lazy('tube:post_list')
    template_name = 'tube/form.html'

post_edit = PostUpdateView.as_view()
  • 포스트 삭제
class PostDeleteView(DeleteView):
    model = Post
    success_url = reverse_lazy('tube:post_list')

post_delete = PostDeleteView.as_view()

프로젝트 urls.py 수정

# tutorialdjango > 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.views.generic.base import RedirectView

urlpatterns = [
    # path('', RedirectView.as_view(url='tube/'), name='root'),
    path('', RedirectView.as_view(pattern_name='tube:post_list'), name='root'),
    path('admin/', admin.site.urls),
    path('tube/', include('tube.urls')),
    path('accounts/', include('accounts.urls')),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

RedirectView 클래스의 인스턴스를 생성하며, 이 뷰는 사용자를 'tube:post_list'라는 패턴 이름에 매핑된 URL로 리다이렉트 시킨다.

tube앱 수정

템플릿 추가

  • tube > form.html
  • tube > post_confirm_delete.html
  • tube > post_detail.html
  • tube > post_list.html

화면에 보여줄 내용 수정

  • 모듈 선언
# tube > views.py

# write는 로그인 해야만 가능
# update와 delete는 업로드한 사용자여야만 가능

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin # 추가
from django.shortcuts import render
from django.views.generic import ListView, DeleteView, UpdateView, DetailView, CreateView
from .models import Post, Comment
from .forms import PostForm, CommentForm
from django.urls import reverse_lazy
  • 포스트 생성
class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    success_url = reverse_lazy('tube:post_list')
    template_name = 'tube/form.html'

    def form_valid(self, form):
        video = form.save(commit=False) # commit=False는 DB에 저장하지 않고 객체만 반환
        video.author = self.request.user
        return super().form_valid(form) # 이렇게 호출했을 때 저장합니다.

post_new = PostCreateView.as_view()

LoginRequiredMixin 은 로그인 된 사용자만 가능하다는 뜻이다. FBV의 데코레이터의 기능이다.

reverse_lazy 는 그냥 reverse 함수와의 차이를 볼 필요가 있다. reverse 함수는 호출 시점에 즉시 URL 변환하는데 Django의 시작 지점에 reverse함수를 사용하면 아직 로드되지 않은 URL 패턴에 의해 오류가 발생할 수 있다.

반면, reverse_lazy 함수는 함수가 호출되는 시점에는 실제 URL 변환 작업을 수행하지 않고, 변환된 URL이 실제로 필요한 시점에 수행한다는 뜻이다. 기본적으로 변환을 지연(lazy)시킨다고 알고 있으면 된다.

폼의 유효성 검사가 통과된 후, video라는 Post 객체를 생성하지만 바로 저장하지 않고 author 속성을 설정한 후에 저장한다.

  • 포스트 수정

    class PostUpdateView(UserPassesTestMixin, UpdateView):
       model = Post
       form_class = PostForm
       success_url = reverse_lazy('tube:post_list')
       template_name = 'tube/form.html'
    
       def test_func(self): # UserPassesTestMixin에 있고 test_func() 메서드를 오버라이딩, True, False 값으로 접근 제한
           return self.get_object().author == self.request.user
    post_edit = PostUpdateView.as_view()

UserPassesTestMixin를 인자로 받아 특정 테스트를 통화해야 접속 가능하도록 한다. 특정 테스트는 test_func에서 오버라이딩 하고 있으며, 작성자와 현재 유저가 같은 경우에 진행 가능하도록 한다.

  • 포스트 삭제

    class PostDeleteView(UserPassesTestMixin, DeleteView):
       model = Post
       success_url = reverse_lazy('tube:post_list')
    
       def test_func(self): # UserPassesTestMixin에 있고 test_func() 메서드를 오버라이딩, True, False 값으로 접근 제한
           return self.get_object().author == self.request.user
    post_delete = PostDeleteView.as_view()

    삭제의 경우도 수정과 비슷하게 진행된다. 나머지 내용은 이전에 선언한 것과 같다.

Mixin 관련 참고

Mixin 관련 참고사항

# 인증 관련 믹스인:
LoginRequiredMixin: 사용자가 로그인 되어 있을 경우에만 뷰에 접근을 허용합니다.
PermissionRequiredMixin: 사용자에게 특정 권한이 있을 경우에만 뷰에 접근을 허용합니다.
UserPassesTestMixin: test_func() 메서드를 오버라이드하여 사용자가 특정 테스트를 통과할 경우에만 뷰에 접근을 허용합니다.

# 폼 관련 믹스인:
FormMixin: 폼과 관련된 기본 기능 (폼 인스턴스 생성, 폼 데이터 저장 등)을 제공합니다.
ModelFormMixin: 모델 폼과 관련된 작업에 필요한 메서드를 제공합니다.

# 리스트 뷰 관련 믹스인:
MultipleObjectMixin: 여러 객체를 처리하는 뷰 (: 리스트 뷰)에 공통적으로 사용되는 메서드나 속성을 제공합니다.
SingleObjectMixin: 단일 객체를 처리하는 뷰에 필요한 메서드나 속성을 제공합니다.

# 페이징 관련 믹스인:
MultipleObjectPaginationMixin: 여러 객체를 페이지 단위로 표시하기 위한 페이징 처리 기능을 제공합니다.

# 상세 뷰 관련 믹스인:
SingleObjectMixin: 단일 객체에 대한 상세 정보를 제공하는 뷰에서 사용되는 메서드와 속성을 제공합니다.

# 기타 믹스인:
ContextMixin: 컨텍스트 데이터를 뷰에 추가하는 메서드를 제공합니다.
TemplateResponseMixin: 템플릿을 사용하여 응답을 생성하는 메서드를 제공합니다.
RedirectView: 뷰를 다른 URL로 리다이렉트하는 기능을 제공합니다.

댓글 구현하기

tube앱 화면 구성 수정

# views.py
class PostDetailView(DetailView):
    model = Post
    # context_object_name = 'licat_objects' # {{licat_objects.title}} 이런식으로 사용 가능

    def get_context_data(self, **kwargs):
        '''
        여기서 원하는 쿼리셋이나 object를 추가한 후 템플릿으로 전달할 수 있습니다.
        '''
        context = super().get_context_data(**kwargs)
        context['comment_form'] = CommentForm()
        return context

post_detail = PostDetailView.as_view()

템플릿에 전달할 context 데이터를 반환하는 메서드를 오버라이딩하고 있다.
여기서는 CommentForm 인스턴스를 'comment_form' 키에 할당하여 context 데이터에 추가하고 있다.

이를 통해 템플릿에서 {{comment_form}}와 같은 방식으로 'CommentForm' 인스턴스에 접근할 수 있다.

**kwargs에 관해 조금 적어보자면 다음과 같다.

예를 들어, URL 패턴이 path('post/<int:pk>/', views.post_detail)와 같이 정의되어 있으면, <int:pk> 부분에 해당하는 값이 **kwargs를 통해 get_context_data 메소드에 전달된다. 이 경우 **kwargs에는 {'pk': <해당 값>} 형태의 딕셔너리가 전달된다.

views.py 마지막에 댓글 함수 추가

@login_required
def comment_new(request, pk):
    post = Post.objects.get(pk=pk)
    if request.method == 'POST':
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False) # commit=False는 DB에 저장하지 않고 객체만 반환
            comment.post = post
            comment.author = request.user
            comment.save()
            return redirect('tube:post_detail', pk)
    else:
        form = CommentForm()
    return render(request, 'tube/form.html', {
        'form': form,
    })

로그인이 된 상태라면, 함수를 실행한다.

Post 요청이고, 유효한 폼이면 comment(댓글)을 저장하고 세부사항 페이지로 돌아간다.

Get 요청이면, 폼 템플릿을 렌더링하고 HTTP 응답을 반환한다.

조회수 추가하기

views.py 수정

class PostDetailView(DetailView):
    model = Post
    # context_object_name = 'licat_objects' # {{licat_objects.title}} 이런식으로 사용 가능

    def get_context_data(self, **kwargs):
        '''
        여기서 원하는 쿼리셋이나 object를 추가한 후 템플릿으로 전달할 수 있습니다.
        '''
        context = super().get_context_data(**kwargs)
        context['comment_form'] = CommentForm()
        return context
    
    def get_object(self, queryset=None):
        pk = self.kwargs.get('pk')
        post = Post.objects.get(pk=pk)
        post.view_count += 1
        post.save()
        return super().get_object(queryset)

post_detail = PostDetailView.as_view()

get_object 메서드를 통해 조회수를 증가시키는 기능을 추가했다.

뷰에서 사용할 객체를 반환하는 메서드를 오버라이딩한다. URL에서 캡처한 pk 값에 해당하는 'Post' 객체를 조회하고, 그 객체의 'view_count' 속성을 1 증가시킨 후 저장한다.

마지막으로 원래의 'get_object' 메서드를 호출하여 객체를 반환하게 된다.

검색기능 추가하기

views.py 수정

class PostListView(ListView):
    model = Post

    def get_queryset(self):
        qs = super().get_queryset()
        q = self.request.GET.get('q', '')
        if q:
            qs = qs.filter(title__icontains=q)
        return qs

post_list = PostListView.as_view()

def get_queryset(self) 함수는 사용자의 입력(q)에 따라 Post 객체를 title 속성에 대해 부분 일치하는 방식으로 필터링
한다.

i(icontains)가 붙으면 대소문자 구분이 없다.

태그 추가하기

forms.py 수정

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        # fields = '__all__'
        fields = ['title', 'content', 'thumb_image', 'file_upload', 'tags'] # counter같은 값은 건들면 안되니까!

tags 필드를 추가하였다.

profile
익숙해지기 위해 기록합니다

0개의 댓글