다대일과 다대다 관계를 배우기 시작했다.
원래 알고 있는 개념이긴 하지만...막상 생각하면 어렵다.
#09
다대일 관계인 작성자와 카테고리 기능을 추가한다.
장고에서 기본으로 제공하는 User 모델을 외부키로 하여 post에 author 필드를 추가하였다. 또, on_delete=models.CASCADE 조건으로 사용자가 삭제 되면 해당 포스트도 모두 삭제되게 하거나, on_delete=models.SET_NULL조건으로 사용자를 빈 칸으로 두는 두 가지 방법에 대하여 배웠다.
카테고리 필드도 추가하기 위해, Category 모델을 새로 만들었다.
#models.py
class Category(models.Model):
name=models.CharField(max_length=50, unique=True)
slug=models.SlugField(max_length=200, unique=True, allow_unicode=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return f'/blog/category/{self.slug}/' #url 반환
class Meta:
verbose_name_plural='Caregories' #admin category name 설정
SlugField에 대하여 새롭게 배웠다. 주로 사람이 읽을 수 있는 텍스트로 고유 URL을 만들 수 있어, 카테고리와 같이 URL을 통해 뜻을 알 수 있으며, 갯수가 많지 않고 길이가 짧을 때 pk대신 쓰기에 유용한 필드인 것 같다.
지금 하고 있는 다른 프로젝트에 카테고리가 많이 사용되는데, 유용하게 잘 쓸 것 같다. 이렇게 만든 카테고리 모델을 post에서 ForeginField로 연결하면 된다.
#admin.py
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields={'slug':('name', )}
admin.site.register(Category, CategoryAdmin)
name 필드에 값이 입력되면 자동으로 slug가 만들어지도록 한다.
#views.py
class PostList(ListView):
model=Post
ordering='-pk'
def get_context_data(self, **kwargs):
context=super(PostList, self).get_context_data()
context['categories']=Category.objects.all()
context['no_category_post_count']=Post.objects.filter(category=None).count() #대소문자 유의!!
return context
ListView나 DetailView에 내장된 get_context_data 메서드를 오버라이딩 한다. post_list=Post.object.all()을 자동으로 명령할 뿐만 아니라, 딕셔너리 형태로 'categories' 에 모든 카테고리를 가져오고, 'no_category_post_count' 키에 카테고리가 지정되지 않은 포스트의 개수를 저장한다.
#10
다대다 관계에서는 Tag 기능을 만든다.
#models.py
class Tag(models.Model):
name=models.CharField(max_length=50, unique=True)
slug=models.SlugField(max_length=200, unique=True, allow_unicode=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return f'/blog/tag/{self.slug}/'
django에서는 다대다 관계를 나타내기 위한 ManyToManyField가 존재한다. 다대다 관계는 기본적으로 on_delete=models.SET_NULL과 null=True가 설정되어 있어 필요없다. 다대다 관계인 것만 제외하면 category와 방식이 거의 유사하다.
#11
django에서 제공하는 폼 기능을 이용하여 간편하게 포스트 작성과 수정 페이지를 만든다.
#views.py
from django.views.generic import CreateView, UpdateView
class PostCreate(CreateView):
model=Post
fields=['title','hook_text', 'content', 'head_image', 'file_upload', 'category']
class PostUpdate(UpdateView):
model=Post
fields=['title','hook_text', 'content', 'head_image', 'file_upload', 'category']
위와 같이 django에서 제공하는 view를 상속받는다. 이때, PostCreate의 경우 post_form.html을 요구하기 때문에 탬플릿을 따로 만들어줘야 한다.
#blog/post_form.html
<h1>Create New Post</h1>
<hr/>
<form method="post" enctype="multipart/form-data">{% csrf_token %}
<table>
{{ form }}
</table>
<br/>
<button type="submit" class="btn btn-primary float-right">Submit</button>
</form>
post_form.html은 이렇게 작성한다. enctype="mutipart/form-data"는 파일을 전송하기 위해 필요하며,
{% csrf_token %}은 보안을 위해 필요하다. 폼을 이용할 때는 form 태그 안에 넣어주어야 한다.
포스트 작성 시 현재 로그인 된 유저 정보가 자동으로 입력되도록 하고, 즉 자동으로 author 필드가 채워지도록 하고, staff권한이 있는 유저만 작성이 가능하도록 PostCreate 함수를 수정한다.
#views.py
class PostCreate(LoginRequiredMixin, UserPassesTestMixin, CreateView):
model=Post
fields=['title','hook_text', 'content', 'head_image', 'file_upload', 'category']
def test_func(self):
return self.request.user.is_superuser or self.request.user.is_staff
def form_valid(self, form):
current_user=self.request.user
if current_user.is_authenticated and (current_user.is_staff or current_user.is_superuser):
form.instance.author=current_user
response = super(PostCreate, self).form_valid(form)
else:
return redirect('/blog/')
test_func() 함수를 이용해 페이지에 접근 제한을 걸고, form_valid()에서도 로그인한 사용자의 권한에 따라 동작하도록 수정한다. 이에 맞게 html도 {% if user.is_authenticated %}를 사용하여 post 작성 페이지가 권한에 따라 보이도록 수정하였다.
PostUpdate도 PostCreate와 마찬가지로 권한에 따라 수정한다.
#views.py
class PostUpdate(LoginRequiredMixin, UpdateView):
model=Post
fields=['title','hook_text', 'content', 'head_image', 'file_upload', 'category']
template_name='blog/post_update_form.html' #템플릿 네임 지정
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated and request.user==self.get_object().author:
return super(PostUpdate, self).dispatch(request, *args, **kwargs)
else:
raise PermissionDenied
dispatch() 메서드를 이용해 사용자가 GET 혹은 POST 방식 중 어느 방식을 요청했는지 알 수 있다. PostUpdate는 권한에 따라 GET, POST 두 방식 모두 사용이 불가하므로 dispatch()는 실행되는 순간 포스트 작성자가 맞는지 확인한다. 따라서, 방문자(request.user)는 로그인 한 상태이고, author 필드와 동일한 경우이면 disapatch() 메서드를 진짜로 실행한다.
마지막으로, 두 함수에 태그입력란도 추가한다. 본래 form은 존재하는 태그를 선택만 가능했지만, 함수와 모델을 수정하여 태그를 입력할 수 있게 만든다.
#views.py
class PostCreate(LoginRequiredMixin, UserPassesTestMixin, CreateView):
model=Post
fields=['title','hook_text', 'content', 'head_image', 'file_upload', 'category']
def test_func(self):
return self.request.user.is_superuser or self.request.user.is_staff
def form_valid(self, form):
current_user=self.request.user
if current_user.is_authenticated and (current_user.is_staff or current_user.is_superuser):
form.instance.author=current_user
response = super(PostCreate, self).form_valid(form)
tags_str = self.request.POST.get('tags_str')
if tags_str:
tags_str = tags_str.strip()
tags_str = tags_str.replace(',', ';')
tags_list = tags_str.split(';')
for t in tags_list:
t = t.strip()
tag, is_tag_created = Tag.objects.get_or_create(name=t)
if is_tag_created:
tag.slug = slugify(t, allow_unicode=True)
tag.save()
self.object.tags.add(tag)
return response
else:
return redirect('/blog/')
tag는 ;로 구분하며, ',' 또한 ';'로 치환된다. 한글 tag도 인식되는데, 이는 slugify()함수를 이용하여 간편하게 만들 수 있다.
PostUpdate 또한 비슷한 방식으로 만들 수 있다. 단, Update를 할 때 기존의 태그를 불러와야 하므로 html에 value="{{ tags_str_default }}" 를 추가한다.
#views.py
class PostUpdate(LoginRequiredMixin, UpdateView):
model=Post
fields=['title','hook_text', 'content', 'head_image', 'file_upload', 'category']
template_name='blog/post_update_form.html'
def get_context_data(self, **kwargs):
context = super(PostUpdate, self).get_context_data()
if self.object.tags.exists():
tags_str_list = list()
for t in self.object.tags.all():
tags_str_list.append(t.name)
context['tags_str_default'] = '; '.join(tags_str_list)
return context
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated and request.user==self.get_object().author:
return super(PostUpdate, self).dispatch(request, *args, **kwargs)
else:
raise PermissionDenied
def form_valid(self, form):
response = super(PostUpdate, self).form_valid(form)
self.object.tags.clear()
tags_str = self.request.POST.get('tags_str')
if tags_str:
tags_str = tags_str.strip()
tags_str = tags_str.replace(',', ';')
tags_list = tags_str.split(';')
for t in tags_list:
t = t.strip()
tag, is_tag_created = Tag.objects.get_or_create(name=t)
if is_tag_created:
tag.slug = slugify(t, allow_unicode=True)
tag.save()
self.object.tags.add(tag)
return response
#12
다음으로 여러 외부 라이브러리를 django에서 활용해본다.
먼저, form을 꾸미기 위한 django-crispy-forms를 배웠다. 다만, 이상하게 모두 설치하고 setting까지 건드렸는데도 앱을 인식 못하는 지 template를 찾지 못하는 에러가 발생했다. 설치파일을 보면 제대로 설치가 되었는데...일단 중요한 부분은 아니기에 넘어갔다.
django-markdownx를 사용해 웹페이지에서 마크다운 문법을 사용할 수 있도록 만들었다. 마크다운을 설치한 후, url과 model을 수정하면 간단하게 사용할 수 있다. 또, 관리자페이지에서도 마크다운을 간단히 적용할 수 있었다.
마지막으로 가장 중요한 django-allauth를 이용한 회원가입과 로그인 기능이다. 이전에는 카카오로 시도해보긴 했지만, 굉장히 어려워서 애를 먹었던 기억이 있다. 이번에는 google이긴 하지만, 결론적으로 성공했다!!
#settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'crispy_forms',
'markdownx',
'django.contrib.sites', #여기부터
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google', #여기까지
'blog',
'single_pages',
]
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by e-mail
'allauth.account.auth_backends.AuthenticationBackend',
]
SITE_ID = 1
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'none' #이메일 인증은 x
LOGIN_REDIRECT_URL='/blog/'
pip install django-allauth 후 위와같이 setting을 설정한다. provider는 google, kakao, naver...등 많았던 걸로 기억한다.
#urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')),
path('', include('single_pages.urls')),
path('markdownx/', include('markdownx.urls')),
path('accounts/', include('allauth.urls'))
]
당연하지만...지금까지 설치한 외부라이브러리들은 전체 프로젝트 urls.py에다가 설정해준다.
리디렉션 URI에는 뒤에 '/'를 꼭 붙이도록 주의...
다음으로 admin 페이지에서도 sites와 social applications 등록 및 설정을 완료하면 끝!
#/blog/navbar.html
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<a role="button" class="btn btn-outline-dark btn-block btn-sm"
href="{% provider_login_url 'google' %}"><i class="fa-brands fa-google"></i>   Log in with Google</a>
<a role="button" class="btn btn-outline-dark btn-block btn-sm"
href="/accounts/login/"><i class="fa-regular fa-envelope"></i>   Log in with E-mail</a>
</div>
<div class="col-md-6">
<a role="button" class="btn btn-outline-dark btn-block btn-sm"
href="/accounts/signup/"><i class="fa-regular fa-envelope"></i>   Sign up aith E-mail</a>
</div>
</div>
</div>
django-allauth는 외부 회원가입 뿐만 아니라 이메일을 이용한 회원가입, 로그인 모두 지원하므로 html에서 적절한 href 설정을 통해 유저 기능을 이용할 수 있다.
이번에 배운 기능들은 다 이전에 알 던 것들이지만 이번 기회에 확실하게 배우거나, 더 많이 알 수 있었다.
특히 구글을 이용한 유저 회원가입 및 로그인 기능을 성공시켜 뿌듯했음.
전에 했던 프로젝트에서 구글은 비용이 추가되는 걸로 알아서 카카오로 시도했던 것 같은데...이 스터디 마무리 하고 카카오 로그인도 여기다가 시도해봐야겠다.
또...오류 나서 못한 외부 라이브러리도 고쳐야지..
참고사이트
- google 외부 API 사이트
https://console.cloud.google.com/
'개인 스터디 > 정리' 카테고리의 다른 글
Do it 웹사이트 따라 만들기 - 웹사이트 만들 준비하기, 개발 환경 준비하기 (0) | 2024.01.18 |
---|---|
[Django] Do it 장고+부트스트랩 16주차 정리 (0) | 2023.03.13 |
[Django] Do it 장고+부트스트랩 13~15주차 정리 (0) | 2023.02.26 |
[Django] Do it 장고+부트스트랩 5~8주차 정리 (0) | 2023.02.05 |
[Django] Do it 장고+부트스트랩 1~4주차 정리 (0) | 2023.01.30 |