TIL & Todo List/Coding for Entrepreneures

Day-4(Form Validation, Dynamic value For ForeignKey, PositionField, prefetch_related, Model manager, RedirectView, 주저리..)

navill 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: Video3에 대한 Video 리스트

 

  • Lecture의 인스턴스가 없을 경우(새로 등록할 Lecture), 아직 등록되어 있지 않은 video(Video0)를 video 리스트에 담는다.

아직 사용되지 않은 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']

위의 세 개는 Main, 아래의 세 개는 Secondary

  • 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

해당 view에서 코드를 구성하지 않고 간단하게 등록한 과정에 대해서만 이벤트를 줄 수 있다.

 

RedirectView

RedirectView를 이용해 구매 과정에 필요한 처리를 진행하고 다른 페이지로 이동시킨다.

  1. CourseDetailView(DetailView): 구매할 페이지 접속
  2. CoursePurchaseView(RedirectView): 구매 진행
  3. 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는 공부를 더 해서 제대로 이해하고 정리 해둬야 할 것 같다.