Django

Django 실습(9) 동영상 CRUD 구현하기

UserDonghu 2023. 10. 20. 01:54

CBV를 이용해서 모놀로그식 방식으로 동영상 CRUD와 로그인을 구현해보자.

url링크는 다음과 같다.

/tube                                 # 목록
/tube/1                              # 영상 재생과 
/tube/create/                    # 게시물 생성
/tube/update/<int:pk>/    # 게시물 업데이트
/tube/delete/<int:pk>/      # 게시물 삭제
/tube/tag/<str:tag>/          # 해당 태그를 가진 게시물 목록
/tube/?q='keyword'          # 키워드 검색 목록
/accounts/signup/            # 가입
/accounts/login/               # 로그인
/accounts/logout/           # 로그아웃
/accounts/profile/          # 프로필


실습

tube, accounts 앱 추가,

settings.py 수정

'DIRS': [BASE_DIR / 'templates'],

LANGUAGE_CODE = 'ko-kr'

TIME_ZONE = 'Asia/Seoul'

STATIC_URL = 'static/'
STATICFILES_DIRS = [
    BASE_DIR / 'static',
]

MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = '/media/'

 

static 폴더 생성, media 폴더 생성

createsuperuser로 admin계정 생성

 

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'), # ''주소일때, tube앱의 post_list url로 가겠다.
    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) # 미디어 url 추가

 

tube / urls.py 추가

accounts 먼저 구현을 위해서 일단 이렇게만.

from django.urls import path
from . import views

urlpatterns = []

 

accounts / urls.py 추가

from django.urls import path
from . import views

app_name = 'accounts'

urlpatterns = [
    path('signup/', views.signup, name='signup'),
    path('login/', views.login, name='login'),
    path('logout/', views.logout, name='logout'),
    path('profile/', views.profile, name='profile')
]

 

accounts / views.py 수정

CBV로 회원가입, 로그인, 로그아웃, 프로필 구현

from django.shortcuts import render
from django.views.generic import CreateView, TemplateView
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.contrib.auth.views import LoginView, LogoutView
from django.contrib.auth.mixins import LoginRequiredMixin # 로그인 됐을때 뷰에 접근을 허용하는 믹스인

signup = CreateView.as_view(
    form_class = UserCreationForm,
    template_name = 'accounts/form.html',
    success_url = reverse_lazy('accounts:login')
)

login = LoginView.as_view(
    template_name = 'accounts/form.html',
)

logout = LogoutView.as_view(
    next_page = reverse_lazy('accounts:login'),
)

class ProfileView(LoginRequiredMixin, TemplateView):
    template_name = 'accounts/profile.html'

profile = ProfileView.as_view()

 

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>

 

templates / accounts / profile.html 추가

<!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>ID : {{user.username}}</p>
</body>
</html>

 

runserver 후에 login과 logout이 잘 되는지 확인.

 

이제 tube 구현

 

tube / models.py

Post모델, Comment모델, Tag모델 구현

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

 

makemigrations, migrate 해주기

 

tube / admin.py 수정

from django.contrib import admin
from .models import Post, Comment, Tag

admin.site.register(Post)
admin.site.register(Comment)
admin.site.register(Tag)

 

admin으로 접속 후, 잘 되는지 게시물을 올려서 테스트.

 

tube / forms.py 추가

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

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'thumb_image', 'file_upload', 'tags']


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


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

 

tube / urls.py 수정

from django.urls import path
from . import views

app_name = 'tube'

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'),
    path('tag/<str:tag>/', views.post_tag_list, name='post_tag_list'),
    path('comment/new/<int:pk>/', views.comment_new, name='comment_new'),
    path('new/tag/', views.tag_new, name='tag_new'),
]

 

tube / views.py 수정 

중요

from django.shortcuts import render, redirect
from django.views.generic import ListView, DeleteView, UpdateView, DetailView, CreateView
from .models import Post, Comment, Tag
from .forms import PostForm, CommentForm, TagForm
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin

class PostListView(ListView):
    model = Post
    
    def get_queryset(self): # DB에서 보여줄 쿼리셋을 가져오는 함수
        qs = super().get_queryset() # 전체 가져오기
        q = self.request.GET.get('q', '') # get요청일때 q값
        if q: # q값이 있으면
            qs = qs.filter(title__icontains=q) # q를 제목에 포함중인 쿼리셋만 가져오기
        return qs # 리턴
    
post_list = PostListView.as_view()
    
class PostTagListView(ListView): # 태그를 가진 게시물 리스트 보여주기
    model = Post
    
    def get_queryset(self):
        qs = super().get_queryset()
        tag = self.kwargs['tag']
        qs = qs.filter(tags__name__iexact=tag)
        q = self.request.GET.get('q', '')
        if q:
            qs = qs.filter(title__icontains=q)
        return qs
    
post_tag_list = PostTagListView.as_view()

class PostDetailView(DetailView):
    model = Post
    
    def get_context_data(self, **kwargs): # 원하는 쿼리셋이나 object를 추가해서 템플릿으로 전달
        context = super().get_context_data(**kwargs) # 원래 전달할 context데이터
        context['comment_form'] = CommentForm() # context데이터에 comment_form을 추가해서 같이 전달
        return context
    
    def get_object(self, queryset=None): # PostDetailView에서 사용할 object를 변경해서 반환.
        pk = self.kwargs['pk']
        post = Post.objects.get(pk=pk)
        post.view_count += 1
        post.save()
        return super().get_object(queryset)
        
    # def get_object(self): # PostDetailView에서 사용할 object를 변경해서 반환.
    #     pk = self.kwargs['pk'] # kwargs로 전달된 pk 저장
    #     post = super().get_object() # 원래 사용할 object
    #     post.view_count += 1 # view_count를 늘림
    #     post.save() # 저장
    #     return post # 반환
    
post_detail = PostDetailView.as_view()

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    template_name = 'tube/form.html'
    
    def form_valid(self, form): # 폼이 유효한지 확인
        video = form.save(commit=False) # DB에 올리지 않고 임시 저장
        video.author = self.request.user # video의 author을 현재 user로 설정
        return super().form_valid(form) # 폼이 유효한지 확인하고 저장
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs) # 원래 전달할 context데이터
        context['tag_form'] = TagForm() # context데이터에 tag_form 추가해서 같이 전달
        return context
    
    def get_success_url(self): # 성공시 갈 url
        return reverse_lazy('tube:post_detail', kwargs={'pk': self.object.pk}) # kwargs로 방금 만든 게시물 pk를 전달
    
post_new = PostCreateView.as_view()

class PostUpdateView(UserPassesTestMixin, UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'tube/form.html'
    
    def get_success_url(self): # 성공시 갈 url
        return reverse_lazy('tube:post_detail', kwargs={'pk': self.object.pk}) # kwargs로 방금 만든 게시물 pk를 전달
    
    def test_func(self): # UserPassesTestMixin안의 메서드로 True, False로 접근 제한
        return self.get_object().author == self.request.user
    
post_edit = PostUpdateView.as_view()

class PostDeleteView(UserPassesTestMixin, DeleteView):
    model = Post
    
    def get_success_url(self): # 성공시 갈 url
        return reverse_lazy('tube:post_list')
    
    def test_func(self): # UserPassesTestMixin안의 메서드로 True, False로 접근 제한
        return self.get_object().author == self.request.user
    
post_delete = PostDeleteView.as_view()

class CommentCreateView(LoginRequiredMixin, CreateView): # 댓글 달기
    model = Comment
    form_class = CommentForm
    
    def form_valid(self, form):
        pk = self.kwargs['pk']
        post = Post.objects.get(pk=pk)
        comment = form.save(commit=False)
        comment.author = self.request.user
        comment.post = post
        comment.save()
        return redirect('tube:post_detail', pk=pk)
    
comment_new = CommentCreateView.as_view()

class TagCreateView(CreateView): # 태그 생성하기
    model = Tag
    form_class = TagForm
    
    def form_valid(self, form):
        form.save()
        return redirect('tube:post_new')
    
tag_new = TagCreateView.as_view()

 

templates / tube / post_list.html 추가

<h1>Post List</h1>
<form action="" method="GET">
    <input type="text" name="q" value="{{ request.GET.q }}">
    <input type="submit" value="검색">
</form>
<ul>
    {% for post in post_list %}
    <li>
        <a href="{% url 'tube:post_detail' post.pk %}">
            {{ post.title }}
        </a>
    </li>
    {% endfor %}
</ul>
<a href="{% url 'tube:post_new' %}">업로드</a>

 

templates / tube / post_detail.html 추가

<p>{{post.title}}</p>
<p>{{post.content}}</p>
<p>조회수 : {{post.view_count}}</p>
{% if post.file_upload %}
<video src="{{post.file_upload.url}}" controls></video>
{% endif %}
<p>태그 : 
{% for tag in post.tags.all %}
<a href="{% url 'tube:post_tag_list' tag %}">{{tag}}</a>
{% endfor %}
</p>

<hr>
<section>
    <h3>댓글</h3>
    {% for comment in post.comments.all %}
        <p>{{comment.message}}</p>
        <p>{{comment.author}}</p>
        <p>{{comment.updated_at}}</p>
    {% endfor %}
</section>

<section>
    <h3>댓글 작성</h3>
    <form action="{% url 'tube:comment_new' post.pk %}" method="post">
        {% csrf_token %}
        {{ comment_form.as_p }}
        <input type="submit" value="댓글 작성">
    </form>
</section>

<a href="{% url 'tube:post_list' %}">목록</a>
{% if user == post.author %}
<a href="{% url 'tube:post_edit' post.pk %}">수정</a>
<a href="{% url 'tube:post_delete' post.pk %}">삭제</a>
{% endif %}

 

templates / tube / form.html 추가

<form action="{% url 'tube:tag_new' %}" method="post">
    {% csrf_token %}
    <p>태그 추가하기</p>
    {{ tag_form.as_p }}
    <input type="submit" value="추가">
</form>
<form action="" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <input type="submit" value="확인">
</form>

 

templates / tube / post_confirm_delete.html 추가

<h1>글 삭제</h1>

<form method="post">
    {% csrf_token %}
    <p>글을 삭제하시겠습니까?</p>
    <input type="submit" value="네!" class="btn btn-primary">
</form>