ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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: 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는 공부를 더 해서 제대로 이해하고 정리 해둬야 할 것 같다.

    댓글

Designed by Tistory.