Day-4(Form Validation, Dynamic value For ForeignKey, PositionField, prefetch_related, Model manager, RedirectView, 주저리..)
Form Validation
-
사용자가 form에서 직접 slug를 입력할 경우 validation이 필요하다.
-
db에 저장하기 위함이 아닌 page 단계에서의 유효성 검사
-
def clean_<field_name>: 해당 field에 대한 validation이 일어난다.
class CourseForm(forms.ModelForm):
class Meta:
fields = [
'slug',
...
]
def clean_slug(self):
slug = self.cleaned_data.get('slug')
qs = Course.objects.filter(slug=slug)
# if qs.exists(): # -> course를 업데이트할 때 마다 새로운 slug로 변경해야한다.
if qs.count() > 1:
raise forms.ValidationError('동일한 slug가 존재합니다.')
return slug
Dynamic value For ForeignKey(폼 레벨에서 외래키에 대한 동적 처리)
- Course: 과정(Django 중급 과정)
- Lecture: 강좌(제1강, 제2강,...)
- Video: 강좌에 사용될 동영상(1강 동영상, 2강 동영상,...)
- 위와 같이 프로그램을 구현할 때, 동영상(Video)이 특정 강좌에 등록되면 다른 강좌에는 등록할 수 없도록 설계해야 한다.
- 강좌(Lecture)를 생성할 때 이미 등록된 동영상(Video)은 목록에 표시되지 않도록 Form에서 동적으로 ForeignKey를 제한한다.
# models.py
class Lecture(models.Model):
course = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True)
video = models.ForeignKey(Video, on_delete=models.SET_NULL, null=True)
title = models.CharField(max_length=120)
...
class LectureAdminForm(forms.ModelForm):
class Meta:
model = Lecture
fields = [
# 화면에 출력될 필드
'title', 'video', 'description', 'slug'
]
def __init__(self, *args, **kwargs):
super(LectureAdminForm, self).__init__(*args, **kwargs)
obj = kwargs.get('instance') # Lecture
qs = Video.objects.filter(lecture__isnull=True) # video0
if obj:
if obj.video:
this_ = Video.objects.filter(pk=obj.video.pk) # 예) Video3
qs = (qs | this_) # 예) Video3 / Video0 -> video 리스트에 담는다.
self.fields['video'].queryset = qs
else:
# lecture에 등록되지 않은 video(Video0)를 video 리스트에 담는다
self.fields['video'].queryset = qs
- Video.objects.filter(lecture__isnull=True): Lecture 객체에서 사용 중이지 않은(lecture_obj.video == Null) Video 객체들을 필터링
- 만일 Lecture.video가 있을 경우(기존에 저장된 Lecture-video1,2,3), video 리스트를 눌렀을 때 자신이 가지고 있는 video(Video3)와 아직 등록되지 않은 video(Video0)를 모두 나타내야 한다.
- Lecture의 인스턴스가 없을 경우(새로 등록할 Lecture), 아직 등록되어 있지 않은 video(Video0)를 video 리스트에 담는다.
Django PositionField - Github
- Django PositionField는 모델 객체의 순서 및 정렬을 편리하게 처리할 수 있는 오픈소스이다.
- Main & Secondary 두 개의 카테고리가 있다고 가정할 때, Main과 Secondary 부모 객체에 자식 객체를 생성할 경우 순서 번호를 부여하고 정렬시킬 수 있다.
POS_CHOICES = (
('main', 'Main'),
('sec', 'Secondary'),
)
class Course(models.Model):
...
category = models.CharField(max_length=120, choices=POS_CHOICES, default='main')
order = PositionField(collection='category')
...
class Meta:
ordering = ['category', 'order']
- order(순서 번호)를 사용자가 직접 입력할 경우 나머지 아이템의 order가 자동으로 정렬되고 저장된다.
prefetch_related - Django: prefetch_related
-
select_related: OneToOneField, ForeignKey에 사용
-
prefetch_related: ManyToManyField에 사용
[MyCourses에 듣고 싶은 Courses를 등록하고, Courses List 페이지에 내가 등록한 과정(Courses)에 대해 표시]
class MyCourses(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
courses = models.ManyToManyField('Course', related_name='owned', blank=True)
아래의 template 코드는 순환문이 실행되는 동안 user -> mycourses -> courses 순서로 객체에 접근해야 한다.
만약 object_list(Course.objects.all())가 많을 경우 데이터베이스에 많은 부하를 줄 수 있다.
{% for item in object_list%}
<li>
<a href="{{ item.get_absolute_url }}">
{{ item.title}}
</a><small>{{ item.lecture_set.count }} Lectures</small> |
<!-- 문제가 되는 부분 -->
{% if item in request.user.mycourses.courses.all %}
Owend
{% else %}
{{ item.display_price }}
{% endif %}<br><br>
</li>
{% empty %}
<li>No item Found</li>
{% endfor %}
이러한 문제는 prefetch_related를 이용해 course + mycourse(filtering user)를 한꺼번에 가져와 해결할 수 있다.
class CourseListView(ListView):
def get_queryset(self):
qs = Course.objects.all()
user = self.request.user
if user.is_authenticated:
qs = qs.prefetch_related(
# owned: MyCourses가 가리키고있는 Course에 대한 related_name(역참조)
Prefetch('owned', queryset=MyCourses.objects.filter(user=user), to_attr='is_owner'))
return qs
prefetch_related(Prefecth()): prefetch_related를 좀 더 상세하게 컨트롤할 때 사용(Django-Prefetch)
- 왼쪽 그림은 course_list.html 에서 {{ item.owner }}(역참조)를 사용할 경우를 나타낸다.
- {% if item.owned.all %}을 이용해 내가 등록한 course가 있는지 한번 더 확인해야 한다.
- filter를 거친 queryset을 사용할 수 없다. (Prefetch('owned')과 동일 -> Prefetch를 쓰는 의미가 없어진다)
- 오른쪽 그림은 동일한 코드에서 to_attr='is_owner'를 설정하고, template에서 {{ item.is_onwer }}를 사용한 결과이다.
- to_attr를 이용해 'is_owner'라는 속성으로 쿼리 결과를 저장하고 templates에 전달할 수 있다.
Model Manager for prefetch_related
-
모델의 필드를 CRUDL에서 공통적으로 사용할 경우 model manager를 오버 라이딩해서 처리하는 것이 좋다.
-
기존의 prefetch_related를 CourseQuerySet에 정의하여 Course를 사용하는 모든 페이지에서 Course를 참조하는 MyCourse를 필터링할 수 있다.
- 유저가 등록한 course에 대해 조건을 부여하거나 filter 기능을 부여하는 것이 간단해진다.
-
활성화(Course.active=models.Boolean)된 과정만 화면에 표시되어야 하기 때문에, all() 메서드에 filter를 추가
class CourseQuerySet(models.QuerySet):
def active(self):
return self.filter(active=True)
def owned(self, user):
# view에서 처리하던 prefetch_related를 가져옴
return self.prefetch_related(
# owned: MyCourses가 가리키고있는 Course에 대한 related_name
Prefetch('owned', queryset=MyCourses.objects.filter(user=user), to_attr='is_owner'))
class CourseManager(models.Manager):
def get_queryset(self):
return CourseQuerySet(self.model, using=self._db)
def all(self):
# return self.get_queryset.all().active() # -> error
return super().all().active()
class Course(models.Model):
...
objects = CourseManager()
class CourseDetailView(MemberRequiredMixin, DetailView):
def get_object(self, queryset=None):
slug = self.kwargs.get('slug')
# slug가 일치하고, 유저의 mycourse에 등록된 Course 객체를 가져온다
obj = Course.objects.filter(slug=slug).owned(self.request.user)
if obj.exists():
return obj.first()
raise Http404
RedirectView
RedirectView를 이용해 구매 과정에 필요한 처리를 진행하고 다른 페이지로 이동시킨다.
- CourseDetailView(DetailView): 구매할 페이지 접속
- CoursePurchaseView(RedirectView): 구매 진행
- return obj.get_absolute_url(): 처리 후 Course 객체 페이지로 이동
class CoursePurchaseView(LoginRequiredMixin, RedirectView):
def get_redirect_url(self, slug=None):
slug = self.kwargs.get('slug')
qs = Course.objects.filter(slug=slug)
if qs.exists():
user = self.request.user
if user.is_authenticated:
# o2o관계이기 때문에 mycourses_set(x)
my_courses = user.mycourses
# ----거래에 필요한 처리----
my_courses.courses.add(qs.first())
# 거래 완료
return qs.first().get_absolute_url()
# return '/login/' -> 미구현
return '/courses/'
get_absolute_url(self, <slug 또는 pk>): url에 등록된 pk 또는 slug(또는 지정된 변수명)를 인자로 받고 url을 반환한다.
정리를 하면서도 머릿속으로 이해는 되지만 글로 설명하기 어려운 부분들이 많다. 문맥이 맞지 않거나 비유가 적절하지 못한 부분들도 많은 것 같다. 블로그를 작성할 때마다 혼자 머릿속으로 알고 있는 것을 남들에게 설명하는 것이 정말 어렵다는 걸 매번 느낀다.
가볍게 django를 복습하고 정리해보자! 생각하고 시작한 tutorial 따라 하기에서 새롭게 알게 된 부분들이 생각보다 많다. 새롭게 알게 된 Django의 내장 함수, 기능별로 구조를 나누거나 합치는 등 강의를 보면서 '와.. 이걸 이렇게도 구현할 수 있구나.. 이렇게도 접근할 수 있구나..' 하는 생각이 든다. 감탄만 하지 말고 하루빨리 누군가에게 감탄을 줄 수 있는 개발자가 되도록 노력해야겠다.
prefetch_related & select_related는 공부를 더 해서 제대로 이해하고 정리 해둬야 할 것 같다.