ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Day-3(Mixin, method_decorator, staticfiles, form_valid, slugify)
    TIL & Todo List/Coding for Entrepreneures 2020. 1. 14. 16:19

    Login, Member and Staff required Mixin

    Member required mixin

    • 특정 페이지에 접근 권한을 부여하기 위해 사용될 mixin 구현
    • 동영상은 무료로 제공되거나 가입된 멤버에게만 제공된다.
    • Video 모델에 free & member_required 필드를 추가하고 해당 조건에 따라 영상에 대한 접근을 허용한다.
    # mixins.py
    class MemberRequiredMixin(object):
        def dispatch(self, request, *args, **kwargs):
            obj = self.get_object()
            if request.user.is_staff or obj.free:
                return super().dispatch(request, *args, **kwargs)
            return HttpResponse('무료로 제공되는 동영상이 아닙니다.')
    
    # views.py
    class VideoDetailView(MemberRequiredMixin, DetailView):
        queryset = Video.objects.all()
    
        def get_context_data(self, *, object_list=None, **kwargs):
            context = super(VideoDetailView, self).get_context_data(**kwargs)
            return context
    • dispatch: 요청 메서드(get, post 등..)를 실행하기 전에 실행되는 메서드. 요청된 메서드가 'get'일 경우 get method를 실행, 'post'일 경우 post method를 실행하도록 한다. (View-dispatch)
      - 해당 페이지에 접근할 때 mixin을 실행하고 접근을 허용할지 하지 않을지 결정한다.

     

    • Mixin은 python의 최상위 클래스(object)의 자식 클래스로 구성하고, 사용하고자 하는 View class에서 상속할 때 반드시 Generic View보다 왼쪽에 위치시켜야 한다(Post: Python MRO). 그렇지 않을 경우 MRO 원칙에 의해 mxin이 가지고 있는 dispatch는 실행되지 않는다.
    # 참고 예제
    class A(object):  # DetailView
       def func(self):  # dispatch()
          print("func A")
    
    class B(object):  # MemberRequiredMixin
       def func(self):  # dispatch()
          print("func B")    
          super().func()  # B의 상위 MRO가 가지고 있는 func() 호출
    
    # 만일 C(A, B)로 구성할 경우 'func B' 문구는 실행되지 않는다.
    class C(B, A):  # VideoDetailView
       pass   
    
    c = C()
    c.func()
    
    # output
    func B
    func A
    • 위 예제는 C(최하위) -> B -> A -> object(최상위) 순서로 MRO가 형성
    • c.func() -> B.func 호출 -> 'func B' 출력 -> 실행 중 super().func() 호출 -> A.func()이 실행되면서 'func A' 출력

     

    Staff member required mixin 

    Django - Staff member required decorator

    Django - Decorating the class

    사이트에 영상을 올리거나 삭제할 때 필요한 권한을 부여하기 위해 사용된다. 해당 페이지에 접속하려는 유저가 관리자일 경우(is_staff=True) 접근을 허용한다.

    class StaffMemberRequiredMixin(object):
        @method_decorator(staff_member_required)
        def dispatch(self, request, *args, **kwargs):
            return super().dispatch(request, *args, **kwargs)
    • 함수형 view를 사용할 경우 @staff_member_required 데코레이터를 이용할 수 있다. 하지만 함수형 view가 아닌 클래스형 뷰의 메서드에서 사용할 경우, method_decorator를 이용해 staff_member_required를 감싸야한다.
    • 감싸지 않을 경우 "'VideoCreateView' object has no attribute 'user'" 에러가 발생한다.

    Static Files 

    STATICFILES_DIRS: Live 서버에서 사용될 static directory

    STATIC_URL: 최상위 static URL 경로

    STATIC_ROOT: 명령어 collectstatic 실행 시 저장될 static 파일의 디렉터리

    # settings.py
    STATIC_URL = '/static/'
    MEDIA_URL = '/media/'
    STATICFILES_DIRS = [
        os.path.join(BASE_DIR, 'static_in_env'),
    ]
    
    VENV_PATH = os.path.dirname(BASE_DIR)
    # => /Users/jh/Desktop/django-advanced/srvup-2_jihoon
    
    # # collectstatic 실행 시 아래의 STATIC_ROOT에 지정된 폴더에 static 파일들이 저장된다.
    STATIC_ROOT = os.path.join(VENV_PATH, 'static_root')
    MEDIA_ROOT = os.path.join(VENV_PATH, 'media_root')
    
    # urls.py
    ...
    
    # code의 마지막줄에 작성
    if settings.DEBUG:
        urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
        urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    Django Documentation
    https://docs.djangoproject.com/en/3.0/ref/settings/#static-files

    Django Staticfile에 대해 자세히 정리된 블로그 https://blog.hannal.com/2015/04/start_with_django_webframework_06/

     

     

    form_valid

    새로운 app(courses) 생성

    # models.py
    class Course(models.Model):
        user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
        title = models.CharField(max_length=120)
        slug = models.SlugField(blank=True, null=True)
        description = models.TextField()
        price = models.DecimalField(decimal_places=2, max_digits=20)
        timestamp = models.DateTimeField(auto_now_add=True)
        updated = models.DateTimeField(auto_now=True)
    
        def __str__(self):
            return self.title
    
    # views.py
    class CourseCreateView(StaffMemberRequiredMixin, CreateView):
        # queryset = Video.objects.all()  # ImproperlyConfigured 에러를 일으킨다.
        model = Course
        form_class = CourseForm
        success_url = '/success/'
    
        def form_valid(self, form):
            obj = form.save(commit=False)
            obj.user = self.request.user
            obj.save()
            return super(CourseCreateView, self).form_valid(form)
     
     # forms.py
     class CourseForm(forms.ModelForm):
        class Meta:
            model = Course
            fields = [
                'title',
                'description',
                'slug',
                'price'
            ]
    
    • form_valid: 사용자가 입력한 form에 대한 유효성을 검사하는 과정
      • form.save(commit=False): 사용자가 입력한 값을 메모리에 올리지만, 그 값을 database에 저장하지 않는다.
      • obj에 CourseForm에 담긴 값(course 객체 포함)을 할당한다.
      • course.user 필드에 request.user 객체를 저장한다.

    현재 구성된 코드의 문제점 - slugify

    기존의 pre_save를 이용해 slug를 생성하면 동일한 slug가 여러 개 생성될 수 있다. 이는 url에 slug를 사용할 때 문제가 된다.

    # 기존의 pre_save
    def pre_save_course_receiver(sender, instance, *args, **kwargs):
        instance.slug = slugify(instance.title)
    
    pre_save.connect(pre_save_course_receiver, sender=Course)

     

    위 문제를 해결하기 위해 아래와 같이 생각해볼 수 있다.

    • 동일한 slug를 가진 course 객체가 있는지 확인
    • 있을 경우 특정 값을 slug에 추가
    • 만일의 경우 특정 값이 중복될 수 있기 때문에 반복적으로 확인
    # utils.py
    # slug에 추가할 문자열 생성
    def unique_string_generator(size=5, chars=string.ascii_lowercase + string.digits):
        return "".join(random.choice(chars) for _ in range(size))
    
    # slug 생성
    def create_slug(instance, new_slug=None):
        if not new_slug:
            slug = slugify(instance.title)
        else:
            slug = new_slug
        Klass = instance.__class__  # == Course
        qs = Klass.objects.filter(slug=slug)  # 동일한 slug가 있는지 확인
        if qs.exists():
            unique_string = unique_string_generator()
            new_created_slug = slug + f'-{unique_string}'
            # 만일 unique_string_generator()에서 생성된 문자가 동일할 경우 
            # qs.exists()가 실행될 수 있기 때문에 재귀를 이용해 해결 
            # -> 동일하지 않는 slug가 생성될 때 까지 반복
            return create_slug(instance, new_slug=new_created_slug)  
        return slug
    
    
    # models.py
    # pre_save를 이용해 중복없는 slug 생성
    def pre_save_course_receiver(sender, instance, *args, **kwargs):
        instance.slug = create_slug(instance)
    
    pre_save.connect(pre_save_course_receiver, sender=Course)

    중복없는 slug를 생성할 수 있다.

    • Course.objects.filter가 아닌 Klass.objects.filter를 쓰는 이유
      • utils.py(slug 생성)에 Course를 import 할 경우, models.py와 utils.py가 서로를 import 하게 되면서 엉뚱한 에러를 일으킨다.
      • 이러한 이유 때문에 instance.__class__를 통해 course 인스턴스에서 class로 접근해야 한다.

    댓글

Designed by Tistory.