본문 바로가기

Django

[Django] 콘텐츠 편집 기능 개발

현재까지는 Admin 사이트 관리자만이 콘텐츠를 생성, 변경할 수 있지만 현재 개발할려고 하는 것은

일반 사용자들도 콘텐츠를 생성 및 변경할 수 있는 기능을 작성할 예정이다.

콘텐츠를 생성 및 변경하는 권한을 모든 사용자에게 부여해서는 안된다.

다음과 같은 권한 부여 요구 사항을 적용할 것이다.

- 콘텐츠에 대한 열람은 모든 사용자가 가능

- 콘텐츠를 새로 생성하는 것은 로그인한 사용자만 가능

- 콘텐츠를 수정 또는 삭제하는 작업은 그 콘텐츠를 생성한 사용자만 가능

 

지금까지 북마크, 블로그, 포토 앱을 만들면서 콘텐츠에 대한 소유자를 고려하지 않았다.

하지만 이번에 개발하는 기능은 콘텐츠에 대한 소유자를 확인해야 하므로 각 콘텐츠 테이블별로

소유자 필드가 필요하다.

 

models.py 수정

# bookmark/models.py

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


class Bookmark(models.Model):
    title = models.CharField('TITLE', max_length=100, blank=True)
    url = models.URLField('URL', unique=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)

    def __str__(self):
        return self.title

로그인한 사용자는 여러 개의 북마크를 생성할 수 있으므로 Bookmark와 User테이블 사이에는 N:1 관계이다

장고에서 N:1 관계는 외래 키로 표현한다.

 

# blog/models.py

from django.db import models
from django.urls import reverse
from django.contrib.auth.models import User
from django.utils.text import slugify
from taggit.managers import TaggableManager


class Post(models.Model):
    title = models.CharField(verbose_name='TITLE', max_length=50)
    slug = models.SlugField('SLUG', unique=True, allow_unicode=True, help_text='one word for title alias.')
    description = models.CharField('DESCRIPTION', max_length=100, blank=True, help_text='simple description text.')
    content = models.TextField('CONTENT')
    create_dt = models.DateTimeField('CREATE DATE', auto_now_add=True)
    modify_dt = models.DateTimeField('MODIFY DATE', auto_now=True)
    tags = TaggableManager(blank=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='OWNER', blank=True, null=True)

    class Meta:
        verbose_name = 'post'
        verbose_name_plural = 'posts'
        db_table = 'blog_posts'
        ordering = ('-modify_dt',)

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('blog:post_detail', args=(self.slug,))

    def get_previous(self):
        return self.get_previous_by_modify_dt()

    def get_next(self):
        return self.get_next_by_modify_dt()

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title, allow_unicode=True)
        super().save(*args, **kwargs)

slug를 자동으로 채우기 위하여 slugify() 함수를 import한다. 또한 save() 메소드를 정의한다.

save() 메소드는 모델 객체의 내용을 데이터베이스에 저장하는 메소드이다.

데이터베이스 테이블에 저장 시 slug 필드를 title 필드로부터 만들어 자동으로 채워준다.

allow_unicode=True 옵션을 주면, 한글 처리도 가능하다.

 

urls.py 수정

# bookmark/urls.py

from django.urls import path
# from bookmark.views import BookmarkLV, BookmarkDV
from bookmark import views


app_name = 'bookmark'
urlpatterns = [
    path('', views.BookmarkLV.as_view(), name='index'),
    path('<int:pk>/', views.BookmarkDV.as_view(), name='detail'),

    # Example: /bookmark/add/
    path('add/',
         views.BookmarkCreateView.as_view(), name="add",
    ),

    # Example: /bookmark/change/
    path('change/',
         views.BookmarkChangeLV.as_view(), name="change",
    ),

    # Example: /bookmark/99/update/
    path('<int:pk>/update/',
         views.BookmarkUpdateView.as_view(), name="update",
    ),

    # Example: /bookmark/99/delete/
    path('<int:pk>/delete/',
         views.BookmarkDeleteView.as_view(), name="delete",
    ),
]
from django.urls import path, re_path
from blog import views


app_name = 'blog'
urlpatterns = [

    # Example: /blog/add/
    path('add/',
         views.PostCreateView.as_view(), name="add",
    ),

    # Example: /blog/change/
    path('change/',
         views.PostChangeLV.as_view(), name="change",
    ),

    # Example: /blog/99/update/
    path('<int:pk>/update/',
         views.PostUpdateView.as_view(), name="update",
    ),

    # Example: /blog/99/delete/
    path('<int:pk>/delete/',
         views.PostDeleteView.as_view(), name="delete",
    ),
]

 

views.py 수정

# bookmark/views.py

from django.views.generic import ListView, DetailView
from bookmark.models import Bookmark
from django.views.generic import CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from mysite.views import OwnerOnlyMixin

class BookmarkCreateView(LoginRequiredMixin, CreateView): 
    model = Bookmark 
    fields = ['title', 'url'] 
    success_url = reverse_lazy('bookmark:index') 

    def form_valid(self, form): 
        form.instance.owner = self.request.user 
        return super().form_valid(form) 


class BookmarkChangeLV(LoginRequiredMixin, ListView): 
    template_name = 'bookmark/bookmark_change_list.html' 

    def get_queryset(self):
        return Bookmark.objects.filter(owner=self.request.user)


class BookmarkUpdateView(OwnerOnlyMixin, UpdateView): 
    model = Bookmark 
    fields = ['title', 'url'] 
    success_url = reverse_lazy('bookmark:index') 


class BookmarkDeleteView(OwnerOnlyMixin, DeleteView): 
    model = Bookmark 
    success_url = reverse_lazy('bookmark:index')

LoginRequiredMixin : @login_required() 데코레이터 기능을 클래스에 적용할 때 사용한다. 사용자가 로그인된 경우는 정상 처리를 하지만, 로그인이 안된 사용자라면 로그인 페이지로 리다이렉트 시킨다.

OwnerOnlyMixin : 소유자만 콘텐츠 수정이 가능하도록 이 믹스인 클래스를 사용한다.

get_queryset() : 화면에 출력할 레코드 리스트를 반환한다. 즉 Bookmark 테이블의 레코드 중에서 owner 필드가 로그인한 사용자인 레코드만 필터링해 그 리스트를 반환한다. 이 줄에 의해 로그인한 사용자가 소유한 콘텐츠만 보이게된다.

# blog/views.py

from django.views.generic import ListView, DetailView, TemplateView
from django.views.generic import ArchiveIndexView, YearArchiveView, MonthArchiveView
from django.views.generic import DayArchiveView, TodayArchiveView
from django.views.generic import FormView
from django.conf import settings
from django.db.models import Q
from django.shortcuts import render

from blog.models import Post
from blog.forms import PostSearchForm

from django.views.generic import CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from mysite.views import OwnerOnlyMixin


class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    fields = ['title', 'slug', 'description', 'content', 'tags']
    initial = {'slug': 'auto-filling-do-not-input'} 
    #fields = ['title', 'description', 'content', 'tags'] 
    success_url = reverse_lazy('blog:index')

    def form_valid(self, form):
        form.instance.owner = self.request.user
        return super().form_valid(form)


class PostChangeLV(LoginRequiredMixin, ListView):
    template_name = 'blog/post_change_list.html'

    def get_queryset(self):
        return Post.objects.filter(owner=self.request.user)


class PostUpdateView(OwnerOnlyMixin, UpdateView):
    model = Post
    fields = ['title', 'slug', 'description', 'content', 'tags']
    success_url = reverse_lazy('blog:index')


class PostDeleteView(OwnerOnlyMixin, DeleteView) :
    model = Post
    success_url = reverse_lazy('blog:index')
# mysite/views.py

from django.views.generic import TemplateView
from django.views.generic import CreateView 
from django.contrib.auth.forms import UserCreationForm 
from django.urls import reverse_lazy

from django.contrib.auth.mixins import AccessMixin
from django.views.defaults import permission_denied


class OwnerOnlyMixin(AccessMixin):
    raise_exception = True
    permission_denied_message = "Owner only can update/delete the object"

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        if self.request.user != self.object.owner:
            self.handle_no_permission()
        return super().get(request, *args, **kwargs)

AccessMixin : 뷰 처리 진입 단계에서 적절한 권한을 갖추었는지 판별할 때 사용하는 믹스인 클래스이다.

raise_exception : 소유자가 아닌 경우 이속성이 True 이면 403 익셉션 처리를 하고, False면 로그인 페이지로 리다이렉트 처리한다.

 

Templates 코딩

# base.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>{% block title %}Django Web Programming{% endblock %}</title>

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">

    {% block extra-style %}{% endblock %}
</head>

<body style="padding-top:90px;">

<nav class="navbar navbar-expand-lg navbar-dark bg-primary fixed-top">
    <span class="navbar-brand mx-5 mb-0 font-weight-bold font-italic">Django - Python Web Programming</span>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent">
        <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto">
            <li class="nav-item mx-1 btn btn-primary">
                <a class="nav-link text-white" href="{% url 'home' %}">Home</a></li>
            <li class="nav-item mx-1 btn btn-primary">
                <a class="nav-link text-white" href="{% url 'bookmark:index' %}">Bookmark</a></li>
            <li class="nav-item mx-1 btn btn-primary">
                <a class="nav-link text-white" href="{% url 'blog:index' %}">Blog</a></li>
            <li class="nav-item mx-1 btn btn-primary">
                <a class="nav-link text-white" href="{% url 'photo:index' %}">Photo</a></li>

            <li class="nav-item dropdown mx-1 btn btn-primary">
                <a class="nav-link dropdown-toggle text-white" href="#" data-toggle="dropdown">Add</a>
                <div class="dropdown-menu">
                    <a class="dropdown-item" href="{% url 'bookmark:add' %}">Bookmark</a>
                    <a class="dropdown-item" href="{% url 'blog:add' %}">Post</a>
                    <div class="dropdown-divider"></div>
                    <a class="dropdown-item" href="">Album</a>
                    <a class="dropdown-item" href="">Photo</a>
                </div>
            </li>

            <li class="nav-item dropdown mx-1 btn btn-primary">
                <a class="nav-link dropdown-toggle text-white" href="#" data-toggle="dropdown">Change</a>
                <div class="dropdown-menu">
                    <a class="dropdown-item" href="{% url 'bookmark:change' %}">Bookmark</a>
                    <a class="dropdown-item" href="{% url 'blog:change' %}">Post</a>
                    <div class="dropdown-divider"></div>
                    <a class="dropdown-item" href="">Album</a>
                    <a class="dropdown-item" href="">Photo</a>
                </div>
            </li>

            <li class="nav-item dropdown mx-1 btn btn-primary">
                <a class="nav-link dropdown-toggle text-white" href="#" data-toggle="dropdown">Util</a>
                <div class="dropdown-menu">
                    <a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a>
                    <div class="dropdown-divider"></div>
                    <a class="dropdown-item" href="{% url 'blog:post_archive' %}">Archive</a>
                    <a class="dropdown-item" href="{% url 'blog:search' %}">Search</a>
                </div>
            </li>
        </ul>

        <form class="form-inline my-2" action="" method="post"> {% csrf_token %}
            <input class="form-control mr-sm-2" type="search" placeholder="global search" name="search_word">
        </form>

        <ul class="navbar-nav ml-5 mr-5">
            <li class="nav-item dropdown mx-1 btn btn-primary">
                {% if user.is_active %}
                <a class="nav-link dropdown-toggle text-white" href="#" data-toggle="dropdown">
            	    <i class="fas fa-user"></i>&ensp;{% firstof user.get_short_name user.get_username %}&ensp;</a>
                <div class="dropdown-menu">
            	    <a class="dropdown-item" href="{% url 'logout' %}">Logout</a>
            	    <a class="dropdown-item" href="{% url 'password_change' %}">Change Password</a>
                </div>
                {% else %}
                <a class="nav-link dropdown-toggle text-white" href="#" data-toggle="dropdown">
            	    <i class="fas fa-user"></i>&ensp;Anonymous&ensp;</a>
                <div class="dropdown-menu">
            	    <a class="dropdown-item" href="{% url 'login' %}">Login</a>
            	    <a class="dropdown-item" href="{% url 'register' %}">Register</a>
                </div>
                {% endif %}
            </li>
        </ul>

    </div>
</nav>


<div class="container">
    {% block content %}{% endblock %}
</div>


{% block footer %}{% endblock %}


<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<script src="https://kit.fontawesome.com/c998a172fe.js"></script>

{% block extra-script %}{% endblock %}

</body>
</html>
# 403.html
{% extends "base.html" %}

{% block title %}403.html{% endblock %}

{% block content %}

    <h1>Permission Denied (403)</h1>
    <br>

    <div class="alert alert-danger">
        <div class="font-weight-bold">{{ exception }}</div>
    </div>

{% endblock content %}

 

'Django' 카테고리의 다른 글

[Django] 회원가입, 로그인 구현  (0) 2022.08.01
[Django] Photo 앱 개발  (0) 2022.08.01
[Django] Blog 앱 확장 - 검색 기능  (0) 2022.07.31
[Django] Blog 앱 확장 - 댓글 달기  (0) 2022.07.30
[Django] Blog 앱 확장 - Tag 달기  (0) 2022.07.30