概要
Djangoには会員管理機能が内包されており、これを利用することである程度サクッとWebサービスを開発することができます。しかし、テンプレートの自作や会員登録部分の実装は必要なので、毎回同じ作業をしなくて済むように雛形を作成してGithubにアップしました。
機能
- 会員登録
- ログイン
- パスワード変更
- パスワードを忘れた時のメール送信からの再発行
- 退会
- マイページ
デモ
こんな人向け
- 簡単な開発をする時に会員管理の実装が面倒
- 見た目はBootstrap4ベースでOK
- 会員認証はE-mailとパスワードで行いたい
動かし方
Githubから取得
- Githubからダウンロードするか、下記コマンドでクローンしてください。
git clone https://github.com/motoitanigaki/django-account-ja.git
動かす
cd django-account-ja
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver
python manage.py createsuperuser
runserver
コマンドが成功すると、http://127.0.0.1:8000/から動作を確認できるようになります。
中身の解説
全体像
Djangoにはdjango/contrib/auth
という会員管理のためのパッケージが内包されているため、基本的にはこれを最大限利用しつつ、少しのコードで求めている形を実現できるようにしていきます。
全体像としては下記の感じです。
青い部分が今回作成したアプリ、緑の部分はDjangoが標準で提供している機能です。オレンジの矢印は処理の流れを、オレンジの数字はこの記事での説明の順番と対応しています。
ざっくりの流れは下記です。
- Userモデルを
AbstractBaseUser
クラスを継承して新たに作成 - url振り分け設定を追記。元々提供されている機能はそのまま使いつつ、それ以外は新しく作成するViewを利用するようにする。
- 元々提供されている機能を少し見てみる
- 会員登録やプロフィール部分は自分で機能を作成
- Templateは全く用意されていないので、全て作成。元々提供されている機能(View)もここで作成したTemplateを利用することになる
1. UserモデルをAbstractBaseUser
クラスを継承して新たに作成
DjangoのデフォルトのUser
クラスはAbstractUser
というクラスを継承していて、このAbstractUser
というクラスはusername
とe-mail
の両方を利用する作りになっています。
そして、UserManager
というクラスの_create_user
というユーザーを作成するための関数内でこの両方を求める作りになっているため、python manage.py createsuperuser
コマンドでスーパーユーザーを作成すると、username
とe-mail
の両方が聞かれます。
今回はusername
は使わずe-mail
とパスワードのみで認証を行う作りにしたいので、User
クラスとUserManager
クラスを自作します。
なお、会員管理機能のカスタマイズはAbstractUser
とAbstractBaseUser
のどちらが良いのか、という議論があります。この説明はこちらの記事に譲ります。
自作するmodels.py
ファイルは下記のようになります。
from django.contrib.auth.models import AbstractBaseUser, UserManager, PermissionsMixin
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.mail import send_mail
from django.utils import timezone
class UserManager(UserManager):
def _create_user(self, email, password, **extra_fields):
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(_('email address'), unique=True)
is_staff = models.BooleanField(
_('staff status'),
default=False,
help_text=_('Designates whether the user can log into this admin site.'),
)
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
objects = UserManager()
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
AbstractBaseUser
と比較すると、今回は使わないusername
、first_name
、last_name
が削除されています。
ここで注意なのがUSERNAME_FIELD = 'email'
という部分です。
Djangoではログインの認証やメール送信など様々なところでUSERNAME_FIELD
で定義しているフィールドを利用します。通常username
フィールドを利用するところを代わりにemail
フィールドを利用するようにリプレイスするような書き方になっています。
本来は恐らくUSERNAME_FIELD
経由で参照するのではなくEMAIL_FIELD
を参照する形で色々な処理を行うべきだと思いますが、USERNAME_FIELD
は
django/auth/contrib/auth/backends.py
django/auth/contrib/forms.py
django/auth/contrib/checks.py
- などなど
で利用されているため、その深淵へ入り込むことは断念しました。
2. url振り分け設定を追記
ちなみに今回作成したプロジェクトはTwo Scoops of Djangoに乗っ取り、settings.py
やmanage.py
が含まれるアプリをconfig
という名前で作成し、ユーザー管理用のアプリをaccount
という名前で作成し、設定系の情報とそれ以外の機能を分けています。
なので、このプロジェクトを基本としてWebサービスを作成する場合はpython manage.py startapp [アプリ名]
コマンドで新しくアプリを作成して利用することを推奨します。
Djangoではurl振り分けは各アプリ内に記述する形になりますが、一番最初に参照されるurls.py
はこの基底アプリ(今回だとconfig
)です。
from django.contrib import admin
from django.urls import path, include
from account.views import Top
urlpatterns = [
path('', Top.as_view(), name='top'),
path('admin/', admin.site.urls),
path('accounts/', include('account.urls')), # 2. 自作の機能へ振り分け
path('accounts/', include('django.contrib.auth.urls')), # 1. 元々提供されている機能へ振り分け
]
ポイントは2つあります
- 元々提供されている機能へ振り分け
Djangoではログイン、パスワード変更、パスワード再発行といった機能がdjango.contrib.auth
内で提供されてるため、こちらへの振り分けをinclude
で記載します。 - 自作の機能へ振り分け
会員登録やプロフィール部分は元々提供されていないので、account
アプリ内で実装する必要があります。そこへの振り分けを記載します。
3. 元々提供されている機能を少し見てみる
会員管理に関してどのあたりの機能が用意されているか、少し見てみます。
urlpatterns = [
path('login/', views.LoginView.as_view(), name='login'),
path('logout/', views.LogoutView.as_view(), name='logout'),
path('password_change/', views.PasswordChangeView.as_view(), name='password_change'),
path('password_change/done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
ログイン、ログアウト、パスワードの変更、パスワード再発行といった機能がそれぞれ用意されていることがわかります。ここで宣言されているurlに則れば、対象の機能が利用できそうです。
例えば~/accounts/login
というurlが踏まれると、ログインのためのLoginView
が呼ばれます。ちょっと見てみましょう。
class LoginView(SuccessURLAllowedHostsMixin, FormView):
"""
Display the login form and handle the login action.
"""
form_class = AuthenticationForm
authentication_form = None
redirect_field_name = REDIRECT_FIELD_NAME
template_name = 'registration/login.html'
redirect_authenticated_user = False
extra_context = None
...
フロントにレスポンスする時に用いるTemplateがtemplate_name = 'registration/login.html'
とすでに宣言されています。このhtmlファイルの実態はまだ存在しないので、後ほどこのディレクトリ構造に従ってファイルを作成していきます。
4. 会員登録やプロフィール部分は自分で機能を作成
Djangoが元々提供していない会員登録やプロフィール画面などは自分で実装していきます。
まずurls.pyから作成します。
from django.urls import path
from django.views.generic import TemplateView
from . import views
app_name = 'accounts'
urlpatterns = [
path('profile/', views.ProfileView.as_view(), name='profile'),
path('signup/', views.SignUpView.as_view(), name='signup'),
path('delete_confirm', TemplateView.as_view(template_name='registration/delete_confirm.html'), name='delete-confirmation'),
path('delete_complete', views.DeleteView.as_view(), name='delete-complete'),
]
続いて対応するViewファイルを作成します。
from django.urls import reverse_lazy
from django.views import generic
from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import (
get_user_model, logout as auth_logout,
)
from .forms import UserCreateForm
User = get_user_model()
class Top(generic.TemplateView):
template_name = 'top.html'
class SignUpView(generic.CreateView):
form_class = UserCreateForm
success_url = reverse_lazy('login')
template_name = 'registration/signup.html'
class ProfileView(LoginRequiredMixin, generic.View):
def get(self, *args, **kwargs):
return render(self.request,'registration/profile.html')
class DeleteView(LoginRequiredMixin, generic.View):
def get(self, *args, **kwargs):
user = User.objects.get(email=self.request.user.email)
user.is_active = False
user.save()
auth_logout(self.request)
return render(self.request,'registration/delete_complete.html')
User = get_user_model()
の記述が無いと、自作したUser
モデルを利用しないので注意が必要です。
また、新しいユーザを作成するためのUserCreationForm
というクラスが提供されていますが、username
を利用しないため一部変更するためにUserCreateForm
というクラスを作成し、それをSignUpView
で利用しています。
UserCreateForm
は下記です。
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
User = get_user_model()
class UserCreateForm(UserCreationForm):
class Meta:
model = User
if User.USERNAME_FIELD == 'email':
fields = ('email',)
else:
fields = ('username', 'email')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
5. Templateは全く用意されていないので、全て作成
あとは元々提供されている機能、新しく作成した機能ともに利用するためのTemplateファイルをガシガシ作っていきます。
見た目は(めちゃくちゃこだわるというわけではないので)Bootstrap4を利用しています。
また、Formに対して自動的にBootstrap4のクラスをあてがってくれるdjango-bootstrap4というライブラリがあるので、今回はこれを利用します。
pipでインストールした後の利用上の注意としては
-
settings.py
のINSTALLED_APPS
にbootstrap4
を追加 - 利用するテンプレート内に
{% load bootstrap4 %}
を追加
を忘れないことです。
これを使うとTemplateの<form>
タグがとてもすっきりします。例えばログインのTemplateを見てみると
{% extends "base_centered.html" %}
{% load bootstrap4 %}
{% block content %}
<h5 class="card-title text-center">ログイン</h5>
<form action="{% url 'login' %}" method="POST">
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary btn-block">ログイン</button>
<input type="hidden" name="next" value="{{ next }}"/>
{% csrf_token %}
</form>
<a href="{% url 'password_reset' %}">パスワードをお忘れですか?</a>
{% endblock %}
という感じになります。
ちなみにTemplateが結構多いので、いくつか分割・部品化をしています。
ディレクトリ構造としては下記の感じで、
├── templates
│ ├── base.html
│ ├── base_centered.html
│ ├── registration
│ │ ├── base.html
│ │ ├── delete_complete.html
│ │ ├── delete_confirm.html
│ │ ├── logged_out.html
│ │ ├── login.html
│ │ ├── password_change_done.html
│ │ ├── password_change_form.html
│ │ ├── password_reset_complete.html
│ │ ├── password_reset_confirm.html
│ │ ├── password_reset_done.html
│ │ ├── password_reset_email.html
│ │ ├── password_reset_form.html
│ │ ├── password_reset_subject.txt
│ │ ├── profile.html
│ │ ├── signup.html
│ │ └── subnav.html
│ └── top.html
図にするとこんな感じです。
会員登録やログインの画面は中央寄せのよくあるデザインで統一しているため、base_centered.html
というファイルにそのフレームだけ記載し共通化して、前述のlogin.html
等中身を埋め込むイメージです。
大体の構造は以上です。
これで個人開発の時にいつもひいひい言っていた会員管理周りが爆速になると思うとテンション爆上がり。
最後に、今回下記ブログやサイトには大変お世話になりました。ありがとうございました。
Django2 でユーザー認証(ログイン認証)を実装するチュートリアル -2- サインアップとログイン・ログアウト
Django における認証処理実装パターン
Djangoで、会員登録機能を自作する