각 앱의 DB에 들어 있는 레코드들을 장고에서는 콘텐츠라고 지칭하는데 이를 Admin 사이트에서 관리자만이 콘텐츠를 생성, 변경할 수 있었다. 일반 사용자들도 콘텐츠를 생성 및 변경할 수 있도록 한다.
but, 콘텐츠 생성 및 변경하는 권한을 모든 사용자에게 부여해서는 안된다.
콘텐츠에 대한 소유자를 확인해야 하므로 각 콘텐츠 테이블별로 소유자 필드가 필요하다.
Bookmark 및 Post 테이블에 owner 필드(User 테이블에 대한 외래 키)가 추가되도록 설계한다.
필드명: owner
타입: ForeignKey(User)
제약 조건: Null
owner = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
로그인한 사용자는 여러 개의 북마크를 생성할 수 있으므로 Bookmark와 User 테이블 사이는 N:1 관계이므로 외래 키로 표현한다.
기존 레코드가 존재하는 상태에서 owner 필드를 추가하는 것이기 때문에 owner 필드는 Null 값을 가질 수 있도록 해야 한다. 폼에서도 owner 필드는 입력하지 않아도 되도록 blank=True로 설정했다.
모델의 필드가 추가되었으므로 변경 사항을 DB에 저장해준다.
# /bookmark/add/
path('add/', views.BookmarkCreateView.as_view(), name="add"),
# /bookmark/change/
path('change/', views.BookmarkChangeLV.as_view(), name="change"),
# /bookmark/99/update
path('<int:pk>/update/', views.BookmarkUpdateView.as_view(), name="update"),
# /bookmark/99/delete
path('<int:pk>/delete/', views.BookmarkDeleteView.as_view(), name="delete"),
# /blog/add/
path('add/', views.PostCreateView.as_view(), name="add"),
# /blog/change/
path('change/', views.PostChangeLV.as_view(), name="change"),
# /blog/99/update
path('<int:pk>/update/', views.PostUpdateView.as_view(), name="update"),
# /blog/99/delete
path('<int:pk>/delete/', views.PostDeleteView.as_view(), name="delete"),
URLconf에서 지정한 뷰를 코딩해준다.
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')
PostCreateView 클래스의 initual로 폼의 slug 입력 항목에 대한 초기값을 지정한다. slug 필드는 title 필드로부터 자동으로 채워지는 필드로 models.py 파일의 Post 모델 정의에 있는 save() 함수에서 수행된다.
slug 필드를 처리하는 또 다른 방법은 fields 속성에서 제외해 폼에 나타나지 않도록 하는 방법으로 폼에는 보이지 않지만 Post 모델의 save() 함수에 의해 테이블의 레코드에 자동으로 채워진다.
OwnerOnlyMixin 클래스: 콘텐츠의 소유자인지를 판별
편집용 제네릭 뷰를 상속받아 뷰를 작성할 경우 template_name 속성으로 템플릿명을 지정하지 않으면 장고에서 정의한 디폴트 템플릿 명을 사용한다.
<li class="nav-item dropdown mx-1 btn btn-primary">
<a class="nav-link dropdown-toggle text-white" href="#" data-bs-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-bs-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>
{% extends 'base.html' %}
{% block title %}bookmark_detail.html{% endblock %}
{% block content %}
<h1>
Bookmark Create/Update - {{ user }}
</h1>
<p class="font-italic">This is a creation or update form for your bookmark.</p>
{% if form.errors %}
<div class="alert alert-danger">
<div class="font-weight-bold">Wrong! Please correct the error(s) below.</div>
{{ form.errors }}
</div>
{% endif %}
<form action="." method="post" class="card pt-3">
{% csrf_token %}
<div class="col-sm-5">
{{ form.title|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
<div class="col-sm-5">
{{ form.title|add_class:"form-control"|attr:"auto:"autofocus" }}
</div>
</div>
<div class="form-group row">
{{ form.url|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
<div class="col-sm-5">
{{ form.url|add_class:"form-control" }}
</div>
</div>
<div class="form-group">
<div class="offset-sm-2 col-sm-5">
<input type="submit" value="Submit" class="btn btn-info"/>
</div>
</div>
</form>
{% endblock %}
login.html과 거의 동일하다. 중요한 차이점은 login.html 파일의 경우 AuthenticationForm을 사용했지만 bookmark_form.html 파일의 form 변수는 Bookmark 모델을 사용해 장고가 내부적으로 만든 폼 객체라는 점이다.
즉, CreateView와 UpdateView는 Bookmark 모델에 대한 ModelForm을 스스로 만들고 사용한다는 점을 유의해야 한다.
Bookmark 테이블의 레코드를 변경하기 위해 기존 레코드의 리스트를 보여주는 템플릿이다.
{% extends 'base.html' %}
{% block title %}bookmark_change_list.html{% endblock %}
{% block content %}
<h1>
Bookmark Change - {{ user }}
</h1>
<br>
<table class="table table-bordered table-condensed table-striped">
<thead>
<tr class="table-primary">
<th>Title</th>
<th>Url</th>
<th>Owner</th>
<th>Update</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for item in object_list %}
<tr>
<td>{{ item.title }}</td>
<td>{{ item.url }}</td>
<td>{{ item.owner }}</td>
<td><a href="{% url 'bookmark:update' item.id %}">Update</a></td>
<td><a href="{% url 'bookmark:delete' item.id %}">Delete</a>{</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
레코드를 삭제하기 전 확인하는 화면
{% extends 'base.html' %}
{% block title %}bookmark_confirm_delete.html{% endblock %}
{% block content %}
<h1>
Bookmark Delete
</h1>
<br>
<form action="." method="post">
{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}" ?</p>
<input type="submit" value="Confirm" class="btn btn-danger btn-sm" />
</form>
</div>
{% endblock %}
Post 레코드를 생성 or 수정하기 위한 폼을 보여주는 화면
{% extends 'base.html' %}
{% load widget_tweaks %}
{% block title %}post_form.html{% endblock %}
{% block content %}
<h1>
Post Create/Update - {{ user }}
</h1>
<p class="font-italic">This is a creation or update form for your post.</p>
{% if form.errors %}
<div class="alert alert-danger">
<div class="font-weight-bold">Wrong! Please correct the error(s) below.</div>
{{ form.errors }}
</div>
{% endif %}
<form action="." method="post" class="card pt-3">
{% csrf_token %}
<div class="col-sm-5">
{{ form.title|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
<div class="col-sm-5">
{{ form.title|add_class:"form-control"|attr:"autofocus" }}
</div>
</div>
<div class="form-group row">
{{ form.slug|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
<div class="col-sm-5">
{{ form.slug|add_class:"form-control"|attr:"readonly" }}
</div>
<small class="font-text text-muted">{{ form.slug.help_text }}</small>
</div>
<div class="form-group row">
{{ form.description|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
<div class="col-sm-5">
{{ form.description|add_class:"form-control" }}
</div>
<small class="font-text text-muted">{{ form.description.help_text }}</small>
</div>
<div class="form-group row">
{{ form.content|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
<div class="col-sm-">
{{ form.content|add_class:"form-control" }}
</div>
</div>
<div class="form-group row">
{{ form.tags|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
<div class="col-sm-5">
{{ form.tags|add_class:"form-control" }}
</div>
<small class="font-text text-muted">{{ form.tags.help_text }}</small>
</div>
<div class="form-group">
<div class="offset-sm-2 col-sm-5">
<input type="submit" value="Submit" class="btn btn-info"/>
</div>
</div>
</form>
{% endblock %}
{% extends 'base.html' %}
{% block title %}post_change_list.html{% endblock %}
{% block content %}
<h1>
Post Change - {{ user }}
</h1>
<br>
<table class="table table-bordered table-sm table-striped">
<thead>
<tr class="table-primary">
<th>Title</th>
<th>Description</th>
<th>Owner</th>
<th>Update</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for item in object_list %}
<tr>
<td>{{ item.title }}</td>
<td>{{ item.description }}</td>
<td>{{ item.owner }}</td>
<td><a href="{% url 'blog:update' item.id %}">Update</a></td>
<td><a href="{% url 'blog:delete' item.id %}">Delete</a>{</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% extends 'base.html' %}
{% block title %}post_confirm_delete.html{% endblock %}
{% block content %}
<h1>
Post Delete
</h1>
<br>
<form action="." method="post">
{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}" ?</p>
<input type="submit" value="Confirm" class="btn btn-danger btn-sm" />
</form>
</div>
{% endblock %}
OwnerOnlyMixin 클래스에서 사용하는 템플릿 파일로 403 익셉션을 발생시키면 장고의 디폴트 핸들러 중 하나인 permission_denied() 함수에서 403.html을 렌더링해서 클라이언트에게 403 응답을 보낸다.
{% extends "base.html" %}
{% block title %}403.html{% endblock %}
{% block content %}
<h1>Permission Denied</h1>
<br>
<div class="alert alert-danger">
<div class="font-weight-bold">
{{ exception }}
</div>
</div>
{% endblock %}
{{ exception }} 컨텍스트 변수는 장고의 permission_denied() 핸들러에서 넘겨주는 템플릿 변수로 우리가 지정한 permission_denied_message 문구에 해당한다.
출처: Django로 배우는 파이썬 웹 프로그래밍(실전편) - 김석훈님