이전 챕터에선 콘텐츠 편집 권한을 Photo앱에도 부여하려한다.
비슷한 내용이 많지만, Album과 Photo의 1:N 관계때문에 설계시 어떻게 작동되는지 구조를 정확히 이해하고 넘어가야한다.
photo/models.py
owner = models.ForeignKey('auth.User',on_delete=models.CASCADE,verbose_name='OWNER',blank=True,null = True)
모델링 파일에서 앨범 클래스와 포토클래스 각각 입력하면된다.
URL conf
포토와 앨범클래스 각각에 대해서 add/update/change/delete를 작성하면된다.
views.py
forms.py
from django.forms import inlineformset_factory
from photo.models import Album, Photo
PhotoInlineFormSet = inlineformset_factory(Album, Photo,
fields = ['image', 'title', 'description'],
extra = 2)
인라인 폼셋을 만든느 inlineformset_factory()를 임포트.
Photo에 있는 필드들을 폼셋에 사용한닫. 빈 폼셋의 개수는 2개
#--- Create/Change-list/Update/Delete for Photo
class PhotoCV(LoginRequiredMixin, CreateView):
model = Photo
fields = ('album', 'title', 'image', 'description')
success_url = reverse_lazy('photo:index')
def form_valid(self, form):
form.instance.owner = self.request.user
return super().form_valid(form)
#PhotoCV(createView)는 add할때 쓰이는 view다.
class PhotoChangeLV(LoginRequiredMixin, ListView):
model = Photo
template_name = 'photo/photo_change_list.html'
def get_queryset(self):
return Photo.objects.filter(owner=self.request.user)
#ChangeLV는 변경하려고할때 리스트를 보여준느 view다
class PhotoUV(OwnerOnlyMixin, UpdateView):
model = Photo
fields = ('album', 'title', 'image', 'description')
success_url = reverse_lazy('photo:index')
#PhotoUV는 photoCV와 같은곳으로 이동한다.OwnerOnlyMixin을 상속받아 권한 허가자만 수정할수있도록 함.
class PhotoDelV(OwnerOnlyMixin, DeleteView):
model = Photo
success_url = reverse_lazy('photo:index')
#PhotoDelV는 리스트가 주어진 상황에서 삭제하도록 도와준다.
Change-list/Delete for Album
#---
class AlbumChangeLV(LoginRequiredMixin, ListView):
model = Album
template_name = 'photo/album_change_list.html'
def get_queryset(self):
return Album.objects.filter(owner=self.request.user)
#앨범 변경시 list를 보여준다.
class AlbumDelV(OwnerOnlyMixin, DeleteView):
model = Album
success_url = reverse_lazy('photo:index')
#앨범 삭제시 실행
#--- (InlineFormSet) Create/Update for Album
class AlbumPhotoCV(LoginRequiredMixin, CreateView):
model = Album
fields = ('name', 'description')
success_url = reverse_lazy('photo:index')
#앨범을 add하는 경우에 해당 view가 실행된다.
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.POST:
context['formset'] = PhotoInlineFormSet(self.request.POST, self.request.FILES)
else:
#GET요청인경우
context['formset'] = PhotoInlineFormSet()
return context
#컨텍스트 변수에 formset 을 추가한다.
def form_valid(self, form):
form.instance.owner = self.request.user
#현재 유저에 권한 할당
context = self.get_context_data()
formset = context['formset']
#여기까지 폼셋은 유효성검사X 폼은 유효성검사 O
for photoform in formset:
photoform.instance.owner = self.request.user
#폼셋에 있는 각 폼의 owner필드에 로그인된 사용자의 User객체를 할당한다.
if formset.is_valid():
self.object = form.save()
#폼의 데이터를 테이블에 저장한다.
formset.instance = self.object
formset.save()
#폼셋 저장 앨범 레코드에 1:N관계로 연결된 여러 레코드를 테이블에 저장.
return redirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
#유효하지 않으면, 메인 폼 및 인라인 폼셋을 출력한다. 이전에 사용자가 입력한 데이터를 다시 보여준다.
updateView(UV)는 CV와 모두 같다고 보면된다.
class AlbumPhotoUV(OwnerOnlyMixin, UpdateView):
model = Album
fields = ('name', 'description')
success_url = reverse_lazy('photo:index')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.POST:
context['formset'] = PhotoInlineFormSet(self.request.POST, self.request.FILES, instance=self.object)
else:
context['formset'] = PhotoInlineFormSet(instance=self.object)
return context
def form_valid(self, form):
context = self.get_context_data()
formset = context['formset']
if formset.is_valid():
self.object = form.save()
formset.instance = self.object
formset.save()
return redirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
AlbumPhotoCV,AlbumPhotoUV 은 다른 뷰들과 차별을 두어야하는게, Photo들을 inline한 상태로 같이 입력받아야 하기 때문이다. 그렇기때문에 클래스 뷰 내에 get_context_data와 form_valid를 두고 넘어오는 데이터를 검증한다.
Template 작성
photo_form.html
{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %}photo_form.html{% endblock %}
{% block content %}
<h1>Photo Create/Update - {{user}}</h1>
<p class="font-italic">This is a creation or update form for your photo.</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 %}
{% if form.is_multipart %}
#is_multipart()메서든느 폼이나 폼셋을 미리 체크해 인코딩이 필요한지 알려준다.
<form enctype="multipart/form-data" action="" method="post" class="card pt-3">
{% else %}
<form action="." method="post" class="card pt-3">
{% endif %}
{% csrf_token %}
<div class="form-group row">
{{ form.album|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
<div class="col-sm-2">
{{ form.album|add_class:"form-control" }}
</div>
#앨범을 선택하는 드롭다운 박스 위젯 출력 Photo테이블이지만, 1:N관계의 Album 레코드도 정할수있다.
<div class="col-sm-2 my-auto">
<a href="{% url 'photo:album_add' %}" class="btn btn-outline-primary btn-sm">
Add Album</a>
</div>
#사진 레코드 생성 폼이지만, 사진이 소속될 앨범 또한 add할수있다.
</div>
<div class="form-group row">
{{ 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.image|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
<div class="col-sm-5">
{{ form.image|add_class:"form-control-file" }}
</div>
</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-8">
{{ form.description|add_class:"form-control"|attr:"rows:3" }}
</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 %}
이후 photo_delete,change_list도 이전 챕터와 구조적으로 비슷하고,
Album_delete,change,list,form또한 유사하다.