-
Day-4(Form Validation, Dynamic value For ForeignKey, PositionField, prefetch_related, Model manager, RedirectView, 주저리..)TIL & Todo List/Coding for Entrepreneures 2020. 1. 15. 23:32
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는 공부를 더 해서 제대로 이해하고 정리 해둬야 할 것 같다.
'TIL & Todo List > Coding for Entrepreneures' 카테고리의 다른 글
Day-5(forloop.counter, custom template tags) (0) 2020.01.17 Day-3(Mixin, method_decorator, staticfiles, form_valid, slugify) (0) 2020.01.14 Day-2(Signal, get_object & get_queryset, form) (0) 2020.01.13 Day-1(Templates, get_context_data) (0) 2020.01.12 -