TIL & Todo List/Coding for Entrepreneures

Day-3(Mixin, method_decorator, staticfiles, form_valid, slugify)

navill 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로 접근해야 한다.