LoginSignup
52
68

More than 5 years have passed since last update.

Djangoで会員登録やログイン|Userモデル拡張のアプリを作りました

Last updated at Posted at 2019-03-24

概要

Djangoには会員管理機能が内包されており、これを利用することである程度サクッとWebサービスを開発することができます。しかし、テンプレートの自作や会員登録部分の実装は必要なので、毎回同じ作業をしなくて済むように雛形を作成してGithubにアップしました。

機能

  • 会員登録
  • ログイン
  • パスワード変更
  • パスワードを忘れた時のメール送信からの再発行
  • 退会
  • マイページ

デモ

AwesomeScreenshot-2019-03-23T23-12-19-262Z.gif

こんな人向け

  • 簡単な開発をする時に会員管理の実装が面倒
  • 見た目は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という会員管理のためのパッケージが内包されているため、基本的にはこれを最大限利用しつつ、少しのコードで求めている形を実現できるようにしていきます。
全体像としては下記の感じです。

無題のプレゼンテーション(1).jpg

青い部分が今回作成したアプリ、緑の部分はDjangoが標準で提供している機能です。オレンジの矢印は処理の流れを、オレンジの数字はこの記事での説明の順番と対応しています。
ざっくりの流れは下記です。

  1. UserモデルをAbstractBaseUserクラスを継承して新たに作成
  2. url振り分け設定を追記。元々提供されている機能はそのまま使いつつ、それ以外は新しく作成するViewを利用するようにする。
  3. 元々提供されている機能を少し見てみる
  4. 会員登録やプロフィール部分は自分で機能を作成
  5. Templateは全く用意されていないので、全て作成。元々提供されている機能(View)もここで作成したTemplateを利用することになる

1. UserモデルをAbstractBaseUserクラスを継承して新たに作成

DjangoのデフォルトのUserクラスはAbstractUserというクラスを継承していて、このAbstractUserというクラスはusernamee-mailの両方を利用する作りになっています。
そして、UserManagerというクラスの_create_userというユーザーを作成するための関数内でこの両方を求める作りになっているため、python manage.py createsuperuserコマンドでスーパーユーザーを作成すると、usernamee-mailの両方が聞かれます。

今回はusernameは使わずe-mailとパスワードのみで認証を行う作りにしたいので、UserクラスとUserManagerクラスを自作します。

なお、会員管理機能のカスタマイズはAbstractUserAbstractBaseUserのどちらが良いのか、という議論があります。この説明はこちらの記事に譲ります。

自作するmodels.pyファイルは下記のようになります。

account/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と比較すると、今回は使わないusernamefirst_namelast_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.pymanage.pyが含まれるアプリをconfigという名前で作成し、ユーザー管理用のアプリをaccountという名前で作成し、設定系の情報とそれ以外の機能を分けています。
なので、このプロジェクトを基本としてWebサービスを作成する場合はpython manage.py startapp [アプリ名]コマンドで新しくアプリを作成して利用することを推奨します。

Djangoではurl振り分けは各アプリ内に記述する形になりますが、一番最初に参照されるurls.pyはこの基底アプリ(今回だとconfig)です。

config/urls.py
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つあります

  1. 元々提供されている機能へ振り分け
    Djangoではログイン、パスワード変更、パスワード再発行といった機能がdjango.contrib.auth内で提供されてるため、こちらへの振り分けをincludeで記載します。
  2. 自作の機能へ振り分け
    会員登録やプロフィール部分は元々提供されていないので、accountアプリ内で実装する必要があります。そこへの振り分けを記載します。

3. 元々提供されている機能を少し見てみる

会員管理に関してどのあたりの機能が用意されているか、少し見てみます。

django/contrib/auth/urls
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が呼ばれます。ちょっと見てみましょう。

django/contrib/auth/views.py
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から作成します。

account/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ファイルを作成します。

account/views.py
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は下記です。

account/forms.py
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.pyINSTALLED_APPSbootstrap4を追加
  • 利用するテンプレート内に{% load bootstrap4 %}を追加

を忘れないことです。

これを使うとTemplateの<form>タグがとてもすっきりします。例えばログインのTemplateを見てみると

account/templates/registration/login.html
{% 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

図にするとこんな感じです。

無題のプレゼンテーション(2).jpg

会員登録やログインの画面は中央寄せのよくあるデザインで統一しているため、base_centered.htmlというファイルにそのフレームだけ記載し共通化して、前述のlogin.html等中身を埋め込むイメージです。

大体の構造は以上です。
これで個人開発の時にいつもひいひい言っていた会員管理周りが爆速になると思うとテンション爆上がり。

最後に、今回下記ブログやサイトには大変お世話になりました。ありがとうございました。

Django2 でユーザー認証(ログイン認証)を実装するチュートリアル -2- サインアップとログイン・ログアウト
Django における認証処理実装パターン
Djangoで、会員登録機能を自作する

52
68
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
68