15장에서 CreateView
와 UpdateView
를 사용하여 포스트 작성 페이지와 포스트 수정 페이지를 만들었다. 하지만 아래 사진처럼 입력 폼이 한쪽으로 치우쳐 있어서 모양이 예쁘진 않았다.
이러한 문제는 django-crispy-forms
을 사용하여 아주 쉽게 해결할 수 있다.
공식문서는 아래 링크를 통해 볼 수 있다. 해당 포스트에 나오지 않은 더 자세한 내용이 궁금하면 들어가보자.
https://django-crispy-forms.readthedocs.io/en/latest/
우선 django-crispy-forms
을 다음 명령어를 통해서 설치하자.
pip install django-crispy-forms
설치가 완료됐으면 settings.py
의 INSTALLED_APPS
에 등록을 하자. 다음과 같이 crispy_forms
와 crispy_bootstrap4
를 추가하자. 그리고 settings.py
의 맨 아래에 CRISPY_TEMPLATE_PACK = 'bootstrap4'
를 추가하자.
# 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',
'crispy_bootstrap4',
'blog',
'single_pages',
(..생략..)
CRISPY_TEMPLATE_PACK = 'bootstrap4'
(..생략..)
crispy_forms를 적용하기 위해 post_form.html
의 맨 위에 {% load crispy_forms_tags %}
를 추가한다. 그리고 {{ form }}
으로 되어 있는 것을 {{ form | crispy }}
로 바꿔준다.
{% extends 'blog/base_full_width.html' %}
{% load crispy_forms_tags %}
{% block head_title %}Create Post - Blog{% endblock %}
{% block main_area %}
<h1>Create New Post</h1>
<hr/>
<form method="post" enctype="multipart/form-data">{% csrf_token %}
<table>
{{ form | crispy }}
<tr>
<label for="id_tags_str">Tags:</label>
<input type="text" name="tags_str" id="id_tags_str">
<tr>
<table>
<button type="submit" class="btn btn-primary float-right">Submit</button>
</form>
{% endblock %}
그러면 웹 브라우저를 열어 포스트 작성 페이지에 들어가보면 다음과 같이 폼 입력란이 바뀐 것을 볼 수 있다.
하지만 아래 Tags는 바뀌지 않았다. 페이지 소스를 통해 form 태그가 어떻게 구성되어있는지 봐보자.
crispy-form이 적용된 field 들은 div로 묶여있는 것을 볼 수 있다. 이처럼 Tags 입력란도 crispy-form처럼 바꾸기 위해서는 div로 똑같이 묶어주고, id도 똑같은 형식으로 바꿔주면 된다. 또한 input의 class도 바꿔준다.
{% extends 'blog/base_full_width.html' %}
{% load crispy_forms_tags %}
{% block head_title %}Create Post - Blog{% endblock %}
{% block main_area %}
<h1>Create New Post</h1>
<hr/>
<form method="post" enctype="multipart/form-data">{% csrf_token %}
{{ form | crispy }}
<div id="div_id_tags_str">
<label for="id_tags_str">Tags:</label>
<input type="text" name="tags_str" id="id_tags_str" class="textinput textInput form-control">
</div>
<br/>
<button type="submit" class="btn btn-primary float-right">Submit</button>
</form>
{% endblock %}
그러면 다음과 같이 Tag 입력란도 crispy-form이 적용된다.
django-markdownx
를 설치하여 마크다운 문법을 적용해보자.
django markdownx에 대한 자세한 내용은 공식 웹사이트를 참고하자. https://neutronx.github.io/django-markdownx/
터미널에 다음과 같이 입력하여 django markdownx를 설치하자.
pip install django-markdownx
이후 settings.py
를 열어 INSTALLED_APPS
에 markdownx를 추가한다.
# settingspy
(..생략..)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'crispy_forms',
'crispy_bootstrap4',
'markdownx',
'blog',
'single_pages',
]
(..생략..)
django markdownx는 urls.py
에 경로를 추가해야 원활하게 작동한다. 다음과 같이 프로젝트 폴더의 urls.py
를 열어 경로를 추가하자. 여기서 주의할 점은 app이름/urls.py
가 아니라 장고 프로젝트 폴더/urls.py
에 추가해야한다.
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')),
path('', include('single_pages.urls')),
path('markdownx/', include('markdownx.urls')), # 추가!!
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
이제 Post
모델의 content
필드를 MarkdownxField
로 바꿔준다.
from django.db import models
from django.contrib.auth.models import User
from markdownx.models import MarkdownxField
import os
(..생략..)
class Post(models.Model): # models 모듈의 Model 클래스를 확장하여 만든 클래스
"""
포스트의 형태를 정의하는 Post 모델
제목(title), 내용(content), 작성일(created_at), 작성자 정보(author) 등등
"""
title = models.CharField(max_length=30) # CharField : 문자를 담는 필드
hook_text = models.CharField(max_length=100, blank=True)
content = MarkdownxField() # <<<
(..생략..)
모델의 필드 형식이 변경되었으니 마이그레이션을 하여 데이터베이스에 등록해준다.
이후 post_form.html
과 post_update_form.html
에 {{ form.media }}
를 추가한다. 이는 장고가 form에 필요한 css 및 JS 파일을 포함하는 HTML 태그를 자동으로 생성하게끔 해준다. 즉, form field에 마크다운 편집기를 제공하는 Markdownx를 사용하는 경우, 편집기가 작동하기 위한 미디어 파일이 필요한데, form.media
는 필요한 미디어 파일을 자동으로 생성 및 제공한다.
<!-- post_form.html -->
{% extends 'blog/base_full_width.html' %}
{% load crispy_forms_tags %}
{% block head_title %}Create Post - Blog{% endblock %}
{% block main_area %}
<h1>Create New Post</h1>
<hr/>
<form method="post" enctype="multipart/form-data">{% csrf_token %}
{{ form | crispy }}
<div id="div_id_tags_str">
<label for="id_tags_str">Tags:</label>
<input type="text" name="tags_str" id="id_tags_str" class="textinput textInput form-control">
</div>
<br/>
<button type="submit" class="btn btn-primary float-right">Submit</button>
</form>
{{ form.media }}
{% endblock %}
포스트 작성 페이지에서 내용을 입력해보면 form.media
에 의해 실시간을 마크다운이 적용되어 렌더링된 모습이 아래에 나타난다.
하지만 아직 위 화면에서만 마크다운 내용이 보이는 것이지, 제출시에는 마크다운 문법으로 적용된 모습이 보이지 않는다.
이를 해결하기 위해선 화면에 포스트를 렌더링할 때 마크다운 문법으로 작성된 content 필드 값을 HTML로 변환하는 작업이 필요하다. 해당 기능 역시 markdownx에서 제공한다.
Post
클래스에 새로운 메서드를 다음과 같이 만들자.
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from markdownx.models import MarkdownxField
from markdownx.utils import markdown # 추가
import os
(..생략..)
class Post(models.Model): # models 모듈의 Model 클래스를 확장하여 만든 클래스
"""
포스트의 형태를 정의하는 Post 모델
제목(title), 내용(content), 작성일(created_at), 작성자 정보(author) 등등
"""
title = models.CharField(max_length=30) # CharField : 문자를 담는 필드
hook_text = models.CharField(max_length=100, blank=True)
content = MarkdownxField() # <<<
(..생략..)
def get_content_markdown(self): # 추가
return markdown(self.content)
새로운 메서드인 get_content_markdown()
은 Post 레코드의 content 필드에 저장되어 있는 텍스트를 마크다운 문법을 적용하여 HTML로 만들어주는 역할을 한다.
이후 이를 템플릿에 반영해주자. post_detail.html
에서 content를 그대로 가져오던 부분 post.content
와 post_list.html
에서 p.content
로 되어있는 부분을 다음과 같이 수정한다.
이렇게 해야 content 필드에 마크다운 문법에 따라 저장되어 있는 텍스트를 HTML로 변환한 후 가져온다. | safe
를 넣는 이유는 HTML 이스케이핑을 방지하기 위함이다.
<!-- post_detail.html -->
(..생략..)
<!-- Post Content -->
<!-- 수정 -->
<p>{{ post.get_content_markdown | safe }}</p>
<!-- -->
{% if post.tags.exists %}
<i class="fas fa-tags"></i>
{% for tag in post.tags.iterator %}
<a href="{{ tag.get_absolute_url }}"><span class="badge badge-pill badge-light">{{ tag }}</span></a>
{% endfor %}
<br/>
<br/>
{% endif %}
(..생략..)
<!-- post_list.html -->
(..생략..)
<div class="card-body">
{% if p.category %}
<span class="badge badge-secondary float-right">{{ p.category }}</span>
{% else %}
<span class="badge badge-secondary float-right">미분류</span>
{% endif %}
<h2 class="card-title h4">{{ p.title }}</h2>
{% if p.hook_text %}
<h5 class="text-muted">{{ p.hook_text }}</h5>
{% endif %}
<!-- 수정 -->
<p class="card-text">{{ p.get_content_markdown | truncatewords_html:45 | safe }}</p>
<!-- -->
{% if p.tags.exists %}
<i class="fas fa-tags"></i>
{% for tag in p.tags.iterator %}
<a href="{{ tag.get_absolute_url }}"><span class="badge badge-pill badge-light">{{ tag }}</span></a>
{% endfor %}
<br/>
<br/>
{% endif %}
<a class="btn btn-primary" href="{{ p.get_absolute_url }}">Read more →</a>
</div>
(..생략..)
post_list.html
에서는 조금 더 추가적으로 수정해야한다. 이전과 달리 정보가 HTML로 넘어오기 때문에 truncateworkds
를 truncateworkds_html
로 추가 수정한다.
그러면 포스트 상세페이지와 포스트 목록 페이지에서 content 내용이 마크다운이 적용된 모습을 볼 수 있다.
django-allauth를 통해서 이메일을 통한 회원가입 및 로그인, 구글, 카카오 로그인을 쉽게 구현할 수 있다.
공식 웹 사이트에서 더 자세한 내용과 추가적인 템플릿을 제공하니 참고하자.
https://django-allauth.readthedocs.io/en/latest/
pip install django-allauth
를 통해 설치하자.
이후 마찬가지로 settings.py
를 열어 INSTALLED_APPS
에 내용을 다음과 같이 추가한다.
필자는 구글 로그인을 사용할거라서 allauth.socialaccount.providers.google
를 추가적으로 넣었다.
또한 AUTHENTICATION_BACKENDS
설정과 SITE_ID
설정 등을 다음과 같이 맨 아래에 추가하자.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'crispy_forms',
'crispy_bootstrap4',
'markdownx',
# 추가
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google', # 구글 로그인을 사용.
'blog',
'single_pages',
]
(..생략..)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
) # 추가
SITE_ID = 1 # 추가
ACCOUNT_EMAIL_REQUIRED = True # 회원가입시 이메일 필요 여부, 추가
ACCOUNT_EMAIL_VERIFICATION = 'none' # 이메일 검증 여부, 추가
SOCIALACCOUNT_LOGIN_ON_GET = True # 추가
LOGIN_REDIRECT_URL = '/blog/' # 로그인 후 리다이렉트될 경로
이후 프로젝트 폴더의 urls.py
를 열어 django-allauth가 사용할 수 있는 URL 경로를 추가한다.
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
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')), # 추가
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
django-allauth를 사용하려면 데이터베이스에도 반영을 해줘야한다. 간단히 터미널에서 python manage.py migrate
만 입력하자.
구글 개발자 콘솔에 접속한 후 새 프로젝트를 만들자. console.cloud.google.com
프로젝트 이름은 원하는 이름으로 하면 된다. 이후 OAuth 동의 화면에서 User Type을 "외부"로 선택하고 만들기를 누른다.
그러면 "앱 등록 수정" 페이지가 나오는데, "앱 이름", "사용자 지원 이메일", "개발자 연락처 정보"를 채우고 "저장 후 계속" 버튼을 누른다.
이후 왼쪽 메뉴에서 "사용자 인증 정보"를 선택한 다음 오른쪽 상단에서 "+ 사용자 인증 정보 만들기" -> "OAuth 클라이언트 ID"를 클릭한다.
애프리케이션 유형으로 "웹 애플리케이션"을 선택하고, 이름과 URI를 지정해야한다. 아직 서버도 없고 도메인도 없으니 로컬서버 http://127.0.0.1:8000
를 "승인된 자바스크립트 출처 URI"에 입력한다. 그리고 "승인된 리디렉션 URI"에는 http://127.0.0.1:8000/accounts/google/login/callback/
을 입력하고 만들기 버튼을 클릭한다.
그러면 클라이언트 ID와 클라이언트 보안 비밀번호를 알려줄텐데, 이를 기억해두자.
이제 로컬 서버의 관리자 페이지를 열어보면 다음과 같이 "SITES"와 "SOCIAL ACCOUNTS" 메뉴가 추가되어있다.
"SITES -> Sites"로 들어가서 "example.com"을 수정하자. 현재는 SITE_ID=1
에 해당하는 도메인이 "example.com"이라는 뜻이므로, 이를 로컬 서버 경로인 127.0.0.1:8000
으로 수정한다.
navbar.html
을 열어 {% load socialaccount %}
를 추가한다.
이후 구글 로그인 버튼에 해당하는 HTML 코드를 찾아 다음과 같이 수정한다. <button>
태그를 <a>
태그로 바꾸고, href
에 링크 주소를 명시한다.
{% load socialaccount %}
(..생략..)
<div class="col-md-6">
<a role="button" class="btn btn-outline-dark btn-block btn-sm" href="{% provider_login_url 'google' %}"><i class="fab fa-google"></i>   Log in with Google</a>
이후 관리자 페이지에서 "SOCIAL ACCOUNTS" -> "Social applications"로 들어가 "ADD SOCIAL APPLICATION" 버튼을 클릭하여 새로운 social application을 추가해야한다.
Clined id와 Secret Key는 앞서 구글이 부여한 클라이언트 ID와 보안 비밀번호를 입력한다.
그럼 끝!
현재 상태는 다음 그림과 같다. 로그인을 했지만, 로그인 했는지 모르는 상태... (로그아웃 버튼이 없음)
로그인되어 있다면 내비게이션 바에 Log In 버튼이 아니라 로그인한 계정의 username이 나오게 하고, Dropdown link로 Log Out 버튼을 구현해보자.
navbar.html
에 있는 Dropdown link를 잘라내어 Log In 버튼에 대한 코드 바로 위에 붙여넣는다. 버튼에 들어갈 텍스트는 {{ user.username }}
으로 수정하여 로그인한 user의 username이 출력되도록 한다.
드롭다운 메뉴로 Log Out 버튼 하나만 필요하므로 기존 메뉴 3개중 2개는 삭제한다. 남은 하나의 텍스트는 "Log Out"으로 바꾸고, href
경로도 /accounts/logout/
으로 바꾸자. 그러면 django-allauth에 의해 해당 경로로 가게되면 자동으로 로그아웃이 된다.
마지막으로 로그인 상태에 따라 다른 버튼을 보여줘야한다. if
문을 사용하자.
위와 같이 수정하고 웹 브라우저에 가서 구글 아이디로 로그인 하면 다음과 같이 로그인 한 상태의 내비게이션 바가 바뀌게 된다.
구글 로그인이 아닌, "이메일로 회원가입"과 "이메일로 로그인" 기능을 구현해보자. 해당 기능들 또한 django-allauth
에서 제공한다.
마찬가지로 navbar.html
을 수정해야 한다.
아래 사진과 같이 "Log in with E-mail" 버튼은 <a>
태그로 수정하고, href="/accounts/login/"
로 로그인 페이지 링크를 추가한다.
"Sign Up with E-mail" 역시 <a>
태그로 수정하고 href="/accounts/signup/"
으로 회원가입 페이지 경로를 추가한다.
그러면 이제 로그인 모달의 어떤 버튼을 클릭해도 정상적으로 작동한다.
위 사진을 보면 로그인과 회원가입, 그리고 사진엔 없지만 로그인 모달과 로그아웃 페이지의 양식이 배포를 하여 서비스 하기엔 너무 구리다(?).....
Django-allauth 커스텀 탬플릿을 적용하면 해결할 수 있다.
해당 내용은 차차 포스트 하도록 하겠다...
장고의 외부 라이브러리 활용방법
django-crispy-forms
: 장고 폼 페이지 꾸미기
django-markdownx
: 장고에 마크다운 적용
django-allauth
: 장고 로그인 기능 구현