본문 바로가기

개인 스터디/정리

[Django] Do it 장고+부트스트랩 9~12주차 정리

다대일과 다대다 관계를 배우기 시작했다.

원래 알고 있는 개념이긴 하지만...막상 생각하면 어렵다.


#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 등록 및 설정을 완료하면 끝!

 

id랑 비밀번호는 가림

 

 

#/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>&nbsp&nbsp 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>&nbsp&nbsp 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>&nbsp&nbsp Sign up aith E-mail</a>
                </div>
            </div>
        </div>

 

django-allauth는 외부 회원가입 뿐만 아니라 이메일을 이용한 회원가입, 로그인 모두 지원하므로 html에서 적절한 href 설정을 통해 유저 기능을 이용할 수 있다.

 

 


이번에 배운 기능들은 다 이전에 알 던 것들이지만 이번 기회에 확실하게 배우거나, 더 많이 알 수 있었다.

특히 구글을 이용한 유저 회원가입 및 로그인 기능을 성공시켜 뿌듯했음. 

전에 했던 프로젝트에서 구글은 비용이 추가되는 걸로 알아서 카카오로 시도했던 것 같은데...이 스터디 마무리 하고 카카오 로그인도 여기다가 시도해봐야겠다.

 

또...오류 나서 못한 외부 라이브러리도 고쳐야지..

 

 


참고사이트

- google 외부 API 사이트

https://console.cloud.google.com/

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com