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
사이트에 영상을 올리거나 삭제할 때 필요한 권한을 부여하기 위해 사용된다. 해당 페이지에 접속하려는 유저가 관리자일 경우(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)
- Course.objects.filter가 아닌 Klass.objects.filter를 쓰는 이유
- utils.py(slug 생성)에 Course를 import 할 경우, models.py와 utils.py가 서로를 import 하게 되면서 엉뚱한 에러를 일으킨다.
- 이러한 이유 때문에 instance.__class__를 통해 course 인스턴스에서 class로 접근해야 한다.