ログインフォームを作成しますが,Django のソース ( C:\Users\lecture\anaconda3\envs\py39\Lib\site-packages\django\contrib\auth\forms.py ) から必要部分(1つの関数 _unicode_ci_compare
,および2つのクラス UsernameField
と AuthenticationForm
)をコピーし,一部書き換えます.
users/forms.py
import unicodedata
from django import forms
from django.contrib.auth import authenticate, get_user_model, password_validation
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ValidationError
from django.core.mail import EmailMultiAlternatives
from django.template import loader
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.text import capfirst
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
UserModel = get_user_model()
def _unicode_ci_compare(s1, s2):
"""
Perform case-insensitive comparison of two identifiers, using the
recommended algorithm from Unicode Technical Report 36, section
2.11.2(B)(2).
"""
return (
unicodedata.normalize("NFKC", s1).casefold()
== unicodedata.normalize("NFKC", s2).casefold()
)
class UsernameField(forms.CharField):
def to_python(self, value):
return unicodedata.normalize("NFKC", super().to_python(value))
def widget_attrs(self, widget):
return {
**super().widget_attrs(widget),
"autocapitalize": "none",
"autocomplete": "username",
'class': 'form-control',
}
class AuthenticationForm(forms.Form):
"""
Base class for authenticating users. Extend this to get a form that accepts
username/password logins.
"""
username = UsernameField(widget=forms.TextInput(attrs={"autofocus": True}))
password = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={"autocomplete": "current-password", 'class': 'form-control'}),
)
error_messages = {
"invalid_login": _(
"Please enter a correct %(username)s and password. Note that both "
"fields may be case-sensitive."
),
"inactive": _("This account is inactive."),
}
def __init__(self, request=None, *args, **kwargs):
"""
The 'request' parameter is set for custom auth use by subclasses.
The form data comes in via the standard 'data' kwarg.
"""
self.request = request
self.user_cache = None
super().__init__(*args, **kwargs)
# Set the max length and label for the "username" field.
self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
username_max_length = self.username_field.max_length or 254
self.fields["username"].max_length = username_max_length
self.fields["username"].widget.attrs["maxlength"] = username_max_length
if self.fields["username"].label is None:
self.fields["username"].label = capfirst(self.username_field.verbose_name)
def clean(self):
username = self.cleaned_data.get("username")
password = self.cleaned_data.get("password")
if username is not None and password:
self.user_cache = authenticate(
self.request, username=username, password=password
)
if self.user_cache is None:
raise self.get_invalid_login_error()
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
def confirm_login_allowed(self, user):
"""
Controls whether the given User may log in. This is a policy setting,
independent of end-user authentication. This default behavior is to
allow login by active users, and reject login by inactive users.
If the given user cannot log in, this method should raise a
``ValidationError``.
If the given user may log in, this method should return None.
"""
if not user.is_active:
raise ValidationError(
self.error_messages["inactive"],
code="inactive",
)
def get_user(self):
return self.user_cache
def get_invalid_login_error(self):
return ValidationError(
self.error_messages["invalid_login"],
code="invalid_login",
params={"username": self.username_field.verbose_name},
)
views.py についても Django のソースから次の3つのクラスをコピーします.
class SuccessURLAllowedHostsMixin:
class LoginView(SuccessURLAllowedHostsMixin, FormView):
class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
users/views.py
from urllib.parse import urlparse, urlunparse
from django.conf import settings
# Avoid shadowing the login() and logout() views below.
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import login_required
# from django.contrib.auth.forms import (
# AuthenticationForm,
# PasswordChangeForm,
# PasswordResetForm,
# SetPasswordForm,
# )
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.http import HttpResponseRedirect, QueryDict
from django.shortcuts import resolve_url
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme, urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from django.shortcuts import render
from django.shortcuts import redirect
from django.urls import reverse
from .forms import AuthenticationForm
UserModel = get_user_model()
# Create your views here.
def users_index(request):
if not request.user.is_authenticated:
return redirect('%s?next=%s' % (reverse('users:login'), request.path))
context = {}
return render(request, 'users/index.html', context)
class SuccessURLAllowedHostsMixin:
success_url_allowed_hosts = set()
def get_success_url_allowed_hosts(self):
return {self.request.get_host(), *self.success_url_allowed_hosts}
class LoginView(SuccessURLAllowedHostsMixin, FormView):
"""
Display the login form and handle the login action.
"""
form_class = AuthenticationForm
authentication_form = None
next_page = None
redirect_field_name = REDIRECT_FIELD_NAME
# template_name = "registration/login.html"
template_name = "users/login.html"
redirect_authenticated_user = False
extra_context = None
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if self.redirect_authenticated_user and self.request.user.is_authenticated:
redirect_to = self.get_success_url()
if redirect_to == self.request.path:
raise ValueError(
"Redirection loop for authenticated user detected. Check that "
"your LOGIN_REDIRECT_URL doesn't point to a login page."
)
return HttpResponseRedirect(redirect_to)
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return self.get_redirect_url() or self.get_default_redirect_url()
def get_redirect_url(self):
"""Return the user-originating redirect URL if it's safe."""
redirect_to = self.request.POST.get(
self.redirect_field_name, self.request.GET.get(self.redirect_field_name, "")
)
url_is_safe = url_has_allowed_host_and_scheme(
url=redirect_to,
allowed_hosts=self.get_success_url_allowed_hosts(),
require_https=self.request.is_secure(),
)
return redirect_to if url_is_safe else ""
def get_default_redirect_url(self):
"""Return the default redirect URL."""
return resolve_url(self.next_page or settings.LOGIN_REDIRECT_URL)
def get_form_class(self):
return self.authentication_form or self.form_class
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["request"] = self.request
return kwargs
def form_valid(self, form):
"""Security check complete. Log the user in."""
auth_login(self.request, form.get_user())
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
current_site = get_current_site(self.request)
context.update(
{
self.redirect_field_name: self.get_redirect_url(),
"site": current_site,
"site_name": current_site.name,
**(self.extra_context or {}),
}
)
return context
class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
"""
Log out the user and display the 'You are logged out' message.
"""
next_page = None
redirect_field_name = REDIRECT_FIELD_NAME
# template_name = "registration/logged_out.html"
template_name = "users/logged_out.html"
extra_context = None
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
auth_logout(request)
next_page = self.get_next_page()
if next_page:
# Redirect to this page until the session has been cleared.
return HttpResponseRedirect(next_page)
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""Logout may be done via POST."""
return self.get(request, *args, **kwargs)
def get_next_page(self):
if self.next_page is not None:
next_page = resolve_url(self.next_page)
elif settings.LOGOUT_REDIRECT_URL:
next_page = resolve_url(settings.LOGOUT_REDIRECT_URL)
else:
next_page = self.next_page
if (
self.redirect_field_name in self.request.POST
or self.redirect_field_name in self.request.GET
):
next_page = self.request.POST.get(
self.redirect_field_name, self.request.GET.get(self.redirect_field_name)
)
url_is_safe = url_has_allowed_host_and_scheme(
url=next_page,
allowed_hosts=self.get_success_url_allowed_hosts(),
require_https=self.request.is_secure(),
)
# Security check -- Ensure the user-originating redirection URL is
# safe.
if not url_is_safe:
next_page = self.request.path
return next_page
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
current_site = get_current_site(self.request)
context.update(
{
"site": current_site,
"site_name": current_site.name,
"title": _("Logged out"),
**(self.extra_context or {}),
}
)
return context
ログインページを作成します.
users/templates/users/login.html
{% extends "base.html" %}
{% block title %}
ログイン
{% endblock %}
{% block content %}
<div class="col-md-12">
<h1 class="my-5">コメントアプリケーション</h1>
<div class="card">
<div class="card-header">Login</div>
<div class="card-body">
<form method="post" action="{% url 'users:login' %}">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary btn-block">
Login
</button>
<input type="hidden" name="next" value="{{ next }}">
</form>
</div>
<div class="card-footer">
<p>
</p>
</div>
</div>
{% endblock %}
ログアウト完了時に表示されるページを作成します.
users/templates/users/logged_out.html
{% extends "base.html" %}
{% block title %}
ログアウトしました
{% endblock %}
{% block content %}
<h1 class="my-5">ログアウトしました</h1>
<ul>
<li>
<a href="/">トップページへ</a>
</li>
</ul>
{% endblock content %}
ユーザ一覧ページにログアウトのリンクを作成します.
users/templates/users/index.html
{% extends "base.html" %}
{% block title %}
ユーザ一覧
{% endblock %}
{% block content %}
<h1 class="my-5">ユーザ一覧</h1>
<ul>
<li>
<a href="{% url 'index' %}">トップへ戻る</a>
</li>
<li>
<a href="{% url 'users:logout' %}">ログアウト</a>
</li>
</ul>
{% endblock content %}
ユーザ一覧ページにアクセスしようとするとログイン画面が表示されるはずです.すでに作成したユーザアカウントを使ってログインとログアウトができることを確認します.