1)
@login_required
def item_new(request):
if request.method == "POST":
form = ItemForm(request.POST, request.FILES)
if form.is_valid():
is_valid()는 full_clean()을 호출하기 전에 데이터의 바인드 여부와 함께 에러 여부를 확인하게 된다. 이 때 각각의 필드는 clean()로 검사를 받게 된다. 필드.clean() 뿐만아니라 폼.clean() 역시 가능하다.
validator 함수와 다르게 clean 함수는 값을 반환한다.
2)
폼에 필드를 추가하는 방법은 모델폼을 통해서, 폼에서 직접, 모델폼에 추가해서 이렇게 세 가지 방법이 있다.
3)
{% if form.instance %}
<a href="{{ form.instance.get_absolute_url }}" class="btn btn-secondary"> 내용으로 </a>
{% endif %}
<a href="{% url "gallery:item_list" %}" class="btn btn-primary"> 목록 </a>
form.instance가 있는 경우는 edit 상황이므로 내용으로 돌아갈 수 있도록 링크를 걸어준다. views.py에서 form에 넘겨지는 item을 instance로 정의했음을 확인할 수 있다.
4)
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message.message }}
</div>
{% endfor %}
</div>
{% endif %}
views.py에 messaegs를 정의하는 것과 별개로 이를 '소비'하는 건 보통 layout.html에서 {{ messages }} 형태로 진행된다. 쌓여있기 때문에 순회하도록 설정했다.
render 함수를 통해서 message 객체를 넘겨주지 않아도 layout.html에서 사용 가능한 이유는 django의 context_processors기능 덕분이다. contrib 및 templates에 구현돼있고 'user', 'request'와 같은 변수들도 같은 기능을 갖는다.
하지만 이 부분은 {% load bootstrap %}과 {% bootstrap_messages %}를 통해서 내부 기능으로 처리할 수도 있다.
5)
from django.contrib.messages import constants
MESSAGE_TAGS = {
constants.DEBUG: 'secondary',
constants.ERROR: 'danger',
}
django와 bootstrap에서 맞지 않는 태그가 있어서 settings.py에 추가로 지정해주었다.
6)
{% load bootstrap4 %}
...
{% bootstrap_form form %}
table 안에서 form을 보여주기만 했던 부분은 bootstrap을 로드해서 장식해주었다.
7)
path('<int:pk>/delete/', views.item_delete, name='item_delete'),
+
@login_required
def item_delete(request, pk):
item = get_object_or_404(Item, pk=pk)
if request.method == 'POST':
item.delete()
messages.success(request, "포스팅을 삭제했습니다.")
return redirect('gallery:item_list')
return render(request, 'gallery/item_confirm_delete.html', {
'item': item,
})
+
{% extends "gallery/layout.html" %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
<div class="alery alert-danger">
정말 삭제하시겠습니까?
</div>
<a href="{{ item.get_absolute_url }}" class="btn btn-secondary">
취소
</a>
<input type="submit" value="삭제하겠습니다." class="btn btn-danger"/>
</form>
{% endblock content %}
item_delete 함수는 기본적인 뼈대는 앞의 함수들과 같지만, POST 요청이 아닌 경우 item_delete 템플릿만을 렌더링하고 POST 요청일 경우에는 수행 후 pk가 없어지므로(그런데 item_detail 템플릿에서는 아직 item.pk를 보여져서 충돌이 발생하므로) redirect를 수행해준다. 그 다음 urls.py에 views.py에 정의한 item_delete을 보여주는 루트를 구성했다.
item_delete 템플릿은 기입하기 위한 양식은 아니지만, post요청이 있으므로 ^form^ 내부에서 csrf_token 조건 하에서 작성할 수 있도록 한다.
8)
django/conf/global_settings.py를 확인하면 LOGIN_URL의 기본값이 /accounts/login임을 확인할 수 있다. 그러므로 로그인 기능 구현을 accounts 앱에서 수행할 것이다.
9)
from django.contrib.auth.views import LoginView
from django.urls import path
urlpatterns = [
path('login/', LoginView.as_view(template_name='accounts/login_form.html'), name='login'),
]
+
{% extends "accounts/layout.html" %}
{% load bootstrap4 %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
{% bootstrap_form form %}
<input type="submit" value="login"/>
</form>
{% endblock content %}
django.contrib.auth를 include하는 방법으로 login 및 다른 기능들을 한 번에 구현할 수도 있지만, 일단 가볍게 login만 단독으로 구현해보았다. accounts/ 주소는 이미 프로젝트 폴더의 urls.py에서 include 해주었으므로 accounts/login으로 해당 기능에 접근 가능하다.(물론 템플릿 rendering 프로세스까지 마쳐야한다.)
LoginView 소스를 확인해보면 form_class, template_name 등에 기본값이 입력되있는 것을 확인할 수 있는데 as_view 괄호 안은 class가 아닌 instance 값이므로 커스터마이징이 가능하다.(원래 템플릿은 'reigstration/login.html'이다
로그인이 유효하다면 global_settings.py의 LOGIN_REDIRECT_URL 조건에 따라서 accounts/profile로 이동하는데, 이는 ?next=/url/ 요청과 같다.
LOGIN_URL의 또다른 용도는 @login_required 혹은 LoginRequiredMixin 조건에서 조건을 만족하지 못할 경우 주소로 이용되는 것이다.(물론 커스터마이징도 가능하다.)
10)
accounts 앱의 템플릿에서 layout.html을 사용할 것이므로 해당 파일을 프로젝드 폴더의 templates 폴더로 격상시키고 기존 layout.html와 accounts 앱에 만들어주는 layout.html dms 새로운 layout.html을 extends 받는다.
11)
from . import views
urlpatterns = [
...
path('profile/', views.profile, name='profile'),
]
+
from django.contrib.auth.decorators import login_required
@login_required
def profile(request):
return render(request, 'accounts/profile.html')
+
{% extends "accounts/layout.html" %}
{% block content %}
<h2> User: {{ user }} </h2>
{% if user.profile %}
<ul>
<li> {{ user.profile.address }} </li>
</ul>
{% endif %}
<a href="{% url "profile_edit" %}" class="btn btn-primary">
프로필 수정
</a>
{% endblock content %}
로그인 사용자에게만 profile을 보여주기 위해 @login_required 기능을 사용했다. profile이라는 함수라고는 하지만, 사실상 user 정보를 보여주는 템플릿 렌더링용 함수로 봐도 무방하지 않을까 싶다.
해당 {{ user }} 역시 {{ message }}와 마찬가지로 context_processor 기능의 산물이다. 로그인 시에는 User 모델의 인스턴스로서 user지만, 로그아웃 시에는 AnnonymousUser 클래스의 인스턴스로서 user가 사용된다.
12)
urlpatterns = [
...
path('profile/', views.profile, name='profile'),
path('profile/edit/', views.profile_edit, name='profile_edit'),
]
+
@login_required
def profile_edit(request):
try:
profile = request.user.profile
except Profile.DoesNotExist:
profile = None
if request.method == "POST":
form = ProfileForm(request.POST, request.FILES, instance=profile)
if form.is_valid():
profile = form.save(commit=False)
profile.user = request.user
profile.save()
return redirect('profile')
else:
form = ProfileForm(instance=profile)
return render(request, 'accounts/profile_form.html', {
'form': form,
})
+
{% extends "accounts/layout.html" %}
{% load bootstrap4 %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button class="btn btn-primary">프로필 수정</button>
{% endbuttons %}
</form>
{% endblock content %}
item_edit과 전체적인 구조는 유사하지만, redirect에서 객체명이 아닌 view 이름 'profile'을 넘겨주었다.
클래스 객체명을 넘겨주면 구현된 함수 get_absolute_url()을 통해서 url을 뽑아낸 뒤 http response instance를 생성하는 반면, urls.py에 정의한 view 이름을 넘겨준다면 reverse()로 해당 작업을 수행한다.
13)
{% if not user.is_authenticated %}
<li>
<a href="{% url 'signup' %}"> 회원가입</a>
</li>
<li>
<a href="{% url 'login' %}"> 로그인 </a>
</li>
{% else %}
<li>
<a href="{% url 'profile' %}"> 프로필 </a>
</li>
<li>
<a href="{% url 'logout' %}"> 로그아웃 </a>
</li>
{% endif %}
가장 상위 layout.html에서 유저 상태창을 로그인 여부에 따라 구현했다.
14)
from django.views.generic import CreateView
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
from django.conf import settings
...
User = get_user_model()
signup = CreateView.as_view(
model = User,
form_class = UserCreationForm,
success_url = 'settings.LOGIN_URL',
template_name = 'accounts/signup_form.html',
)
+
{% extends "accounts/layout.html" %}
{% load bootstrap4 %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button class="btn btn-primary">회원가입</button>
{% endbuttons %}
</form>
{% endblock content %}
UserCreationForm은 Meta에서 User를 모델로 받는다. User 모델을 가져올 때는 auth의 models가 아닌 get_user_model()로 활성화된 모델을 가져오도록 한다.
회원가입 이후에는 자동으로 login 화면으로 넘어가도록 LOGIN_URL을 success_url로 지정해준다.
패스워드 저장 시에는 user.password = 방법이 아닌 set_password() 함수를 통해서 알고리즘을 거친다. salt, iteration, algorithm, hash 등이 사용된다.
ModelForm을 활용한 ProfileForm에서는 함수뷰로 'form을 명시적으로 넘겨줬지만, 클래스 뷰인 CreateView를 활용한 작업에서는 'form'을 명시적으로 넘겨주지는 않았다.
15)
signup = CreateView.as_view(
model = User,
form_class = UserCreationForm,
success_url = 'settings.LOGIN_REDIRECT_URL',
template_name = 'accounts/signup_form.html',
)
+
from django.contrib.auth import get_user_model, login as auth_login
...
User = get_user_model()
class SignupView(CreateView):
model = User
form_class = UserCreationForm
success_url = 'settings.LOGIN_URL'
template_name = 'accounts/signup_form.html'
def form_valid(self, form):
response = super().form_valid(form)
user = self.object
auth_login(self.request, user)
return response
signup = SignupView.as_view()
django.contrib.auth의 login(auth_login으로 받았다.)에서 form.get_user()를 두 번째 인자로 받기 때문에 user instance를 생성하는 함수가 필요함을 알 수 있다.
django.views.generic.edit.py에서 ModelFormMixin, 즉, 자식의 form_valid는 object를 만들며 FormMixin, 즉, 부모의 form_valid는 http 인스턴스를 생성함을 알 수 있다.
여기서는 해당 object를 이용해서 로그인하는 기교가 추가됐음을 알 수 있다. 그리고 LOGIN_REDIRECT_URL로 succes_url이 변경돼서 로그인 여부를 확인할 수 있다.