현재까지는 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> {% firstof user.get_short_name user.get_username %} </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> Anonymous </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 |