달라진 부분 위주로 설명하겠다.
# 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 > 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_name
을 comments
로 설정한 덕분에 Post 클래스에서도 comments
로 접근 가능하다.
python manage.py makemigrations
python manage.py migrate
# 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와 상호작용하는 방식)을 변경해주었다. 이렇게 선언하면 관리자 페이지의 필드에는 설정한 값만 나오게 된다.
# 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()
# 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 > 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()
삭제의 경우도 수정과 비슷하게 진행된다. 나머지 내용은 이전에 선언한 것과 같다.
# 인증 관련 믹스인:
LoginRequiredMixin: 사용자가 로그인 되어 있을 경우에만 뷰에 접근을 허용합니다.
PermissionRequiredMixin: 사용자에게 특정 권한이 있을 경우에만 뷰에 접근을 허용합니다.
UserPassesTestMixin: test_func() 메서드를 오버라이드하여 사용자가 특정 테스트를 통과할 경우에만 뷰에 접근을 허용합니다.
# 폼 관련 믹스인:
FormMixin: 폼과 관련된 기본 기능 (폼 인스턴스 생성, 폼 데이터 저장 등)을 제공합니다.
ModelFormMixin: 모델 폼과 관련된 작업에 필요한 메서드를 제공합니다.
# 리스트 뷰 관련 믹스인:
MultipleObjectMixin: 여러 객체를 처리하는 뷰 (예: 리스트 뷰)에 공통적으로 사용되는 메서드나 속성을 제공합니다.
SingleObjectMixin: 단일 객체를 처리하는 뷰에 필요한 메서드나 속성을 제공합니다.
# 페이징 관련 믹스인:
MultipleObjectPaginationMixin: 여러 객체를 페이지 단위로 표시하기 위한 페이징 처리 기능을 제공합니다.
# 상세 뷰 관련 믹스인:
SingleObjectMixin: 단일 객체에 대한 상세 정보를 제공하는 뷰에서 사용되는 메서드와 속성을 제공합니다.
# 기타 믹스인:
ContextMixin: 컨텍스트 데이터를 뷰에 추가하는 메서드를 제공합니다.
TemplateResponseMixin: 템플릿을 사용하여 응답을 생성하는 메서드를 제공합니다.
RedirectView: 뷰를 다른 URL로 리다이렉트하는 기능을 제공합니다.
# 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': <해당 값>}
형태의 딕셔너리가 전달된다.
@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 응답을 반환한다.
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'
메서드를 호출하여 객체를 반환하게 된다.
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)가 붙으면 대소문자 구분이 없다.
class PostForm(forms.ModelForm):
class Meta:
model = Post
# fields = '__all__'
fields = ['title', 'content', 'thumb_image', 'file_upload', 'tags'] # counter같은 값은 건들면 안되니까!
tags
필드를 추가하였다.