django-allauthを使って登録済みユーザのみGoogle認証でログインさせる

Django

はじめに

Djnagoとdjango-allauthを使ってGoogleの2段階認証を実装します。
通常、Googleのアカウントを持っていると誰でもログイン(2段階認証)できますが、今回の仕様上、予めDjangoの管理画面でCustomUserに登録したメールアドレスを持つユーザにログインを許可します。

また、管理側でログインを許可するユーザを制御する関係上、ログイン、ログアウトのみテンプレートを用意し、それ以外は404エラーとして処理します。

Googleの2段階認証とDjangoが作るCookieの有効期限は異なるので、Cookieの期限はsettings.pyで制御します。
ログ機能も付けているので、不要なら削除します。

作業手順

Djangoのプロジェクトを作って、

mkdir ProjectName
cd ProjectName
django-admin startproject config .

ログを記録するなら、下記のsettings.pyに設定したようにディレクトリを作成して、

mkdir log

ライブラリをインストールして、

pipenv install django-allauth

CustomUserを作成して、

python manage.py startapp users

今回、掲載した複数のファイルを修正して、makemigrations、migrateを実行して、

python manage.py makemigrations

python manage.py migrate

管理ユーザを作成して、

python manage.py createsuperuser

Djangoの管理画面で、Usersテーブルに新規ユーザ(メールアドレスを登録)を作成してからテストします。

python manage.py runserver 192.168.10.100

構成

実際はpipenvで作った仮想環境で行っています。

Debian GNU Linux(Bookworm)

Python 3.10.13

Django 5.1.2

django-allauth 65.0.2

pipenv 2023.10.3

google-api-core 2.21.0
google-api-python-client 2.149.0
google-auth 2.35.0
google-auth-httplib2 0.2.0
google-auth-oauthlib 1.2.1
googleapis-common-protos 1.65.0

ディレクトリ構成

django-allauthのルーティングをCustomUserのルーティング(urls.py)で上書きして使いますが、ひとつだけ制御できなかったので、ローカルにdjango-allauthのテンプレート構成を再現してファイルを置いています。
ただ、urls.pyで制御できなかったファイルが

  .venv/lib/python3.10/site-packages/allauth/templates/socialaccount/login_cancelled.html

と分かったので、以下のように対処しました。

・ローカルにdjango-allauthのテンプレート用ディレクトリを作成。
・404.htmlをコピーしてlogin_cancelled.htmlに名前変更。
・settings.pyのTEMPLATESにローカルのallauthテンプレートを参照するようにパスを追加。

今回、関係ありそうなファイルは以下の通りです。
Bootstrapはダウンロード・解凍して、設置しています。

project/
  - config/
      settings.py

  - templates/
      - 404.html
      - home.html
      - base.html
      - allauth/
          - socialaccount/
              login_cancelled.html
      - users
          login.html
          logout.html

  - users/
      admin.py
      forms.py
      models.py
      urls.py
      views.py

  - static
      - css
          bootstrap.min.css
          bootstrap.min.css.map
      - js
          bootstrap.min.js
          bootstrap.min.js.map
      - favicon
          favicon.ico

django-allauthのインストール

pipの場合は、

pip install django-allauth

pipenvを使っている場合は、

cd project

pipenv shell

pipenv install django-allauth

settings.py

django-allauthに関連する部分の抜粋です。
デバッグ中はデータベースを削除、作成を繰り返すことが多く、Webで毎回設定するのが面倒なのでGoogleのclient_id、secretは直書きします。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',                     # django-allauth
    'allauth',                                  # django-allauth
    'allauth.account',                          # django-allauth
    'allauth.socialaccount',                    # django-allauth
    'allauth.socialaccount.providers.google',   # django-allauth
    'users.apps.UsersConfig',                   # CustomUser
    省略
]

# カスタムユーザーモデル
AUTH_USER_MODEL = 'users.CustomUser'

SITE_ID = 1

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend', # allauth用
)

ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED        = True
ACCOUNT_USERNAME_REQUIRED     = False

# メール認証無し
ACCOUNT_EMAIL_VERIFICATION  = 'none'

# CustomUserモデルにusernameは不要
ACCOUNT_USER_MODEL_USERNAME_FIELD = None

ACCOUNT_UNIQUE_EMAIL = True

SOCIALACCOUNT_EMAIL_REQUIRED = True
SOCIALACCOUNT_AUTO_SIGNUP    = True

# トークンを保存する
SOCIALACCOUNT_STORE_TOKENS = True

# home.html
LOGIN_URL                   = 'home'
LOGOUT_REDIRECT_URL         = 'home'
ACCOUNT_LOGOUT_REDIRECT_URL = 'home'
LOGIN_REDIRECT_URL          = 'home'

SOCIALACCOUNT_ADAPTER = 'users.views.CustomSocialAccountAdapter'

# Googleのclient_idとsecretを直に記述
SOCIALACCOUNT_PROVIDERS = {
    'google': {
        "APPS": [
            {
                "client_id": 'xxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.apps.googleusercontent.com',
                "secret": 'xxxxxx-xxxxxx-xxxxxx',
                "key": '',
                "sites": '127.0.0.1',
            },
        ],
        'SCOPE': [
            'profile',
            'email',
        ],
        'AUTH_PARAMS': {
            'access_type': 'online',
        },
        # PKCEを有効化
        'OAUTH_PKCE_ENABLED': True,
    }
}

'''
Cookieのセッション管理
優先順位
1. SESSION_EXPIRE_AT_BROWSER_CLOSE
2. SESSION_COOKIE_AGE
3. SESSION_SAVE_EVERY_REQUEST
'''
# Cookie有効期間(秒)
SESSION_COOKIE_AGE = 24 * 60 * 60 * 7
# 設定された期限に関係なく、ブラウザを閉じたらセッションを終了
#SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# 全てのリクエストでセッションを保存する。
#SESSION_SAVE_EVERY_REQUEST = True

'''
infoとerrorで出力ファイルを分別する。

CRITICAL > ERROR > WARNING > INFO > DEBUG > NOTSET
'''
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    # 時刻を自動的に挿入する。
    # 例:
    # 2024-06-25 15:19:48,384 : users.views : INFO : Login emailaddress
    'formatters': {
        'verbose': {
            'format': '%(asctime)s : %(name)s : %(levelname)s : %(message)s'
        },
    },
    'handlers': {
        'info_handlers': {
            #'level': 'INFO',
            'level': 'WARNING',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': BASE_DIR / 'log/info.log',
            'maxBytes': 1024*1024*5,  # 5 MB
            'backupCount': 5,
            'formatter': 'verbose',
        },
        'error_handlers': {
            'level': 'ERROR',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': BASE_DIR / 'log/error.log',
            'maxBytes': 1024*1024*5,  # 5 MB
            'backupCount': 5,
            'formatter': 'verbose',
        },
    },
    'loggers': {
        '': {
            'level': 'INFO',
            #'handlers': ['info_handlers', 'error_handlers'],
            'handlers': ['info_handlers',],
        },
    },
}

login_cancelled.html

「構成」に記載した理由から仕方なく作成。

{% extends 'base.html' %}

{% block title %}
ページが見つかりませんでした
{% endblock title %}

{% block h1_title %}
It is just a 404 Error !
{% endblock h1_title %}

{% block error_404 %}
<p class="text-center">
Sorry, the requested URL was not found on this server.
<br>
<a href="{% url 'account_login' %}" class="btn btn-outline-danger">Login</a>
</p>
{% endblock error_404 %}

models.py

AbstractBaseUserを使ってCustomUserを作ります。
usenameは使わないのでemailで代用します。
また、ユーザデータは今回の仕様上、管理者のみ操作します。

from django.db import models
from django.contrib import auth
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.core.mail import send_mail
from django.contrib.auth.hashers import make_password
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import PermissionsMixin
import uuid

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('The given email must be set')
        email         = self.normalize_email(email)
        user          = self.model(email=email, **extra_fields)
        user.password = make_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=None, password=None, **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.')
        
        if not password:
            raise ValueError('Password is required for super users.')

        return self._create_user(email, password, **extra_fields)

    def with_perm(self, perm, is_active=True, include_superusers=True, backend=None, obj=None):
        if backend is None:
            backends = auth._get_backends(return_tuples=True)
            if len(backends) == 1:
                backend, _ = backends[0]
            else:
                raise ValueError(
                    'You have multiple authentication backends configured and '
                    'therefore must provide the `backend` argument.'
                )
        elif not isinstance(backend, str):
            raise TypeError(
                'backend must be a dotted import path string (got %r).' % backend
            )
        else:
            backend = auth.load_backend(backend)
        if hasattr(backend, 'with_perm'):
            return backend.with_perm(
                perm,
                is_active=is_active,
                include_superusers=include_superusers,
                obj=obj,
            )
        return self.none()

'''
カスタムユーザ定義
'''
class CustomUser(AbstractBaseUser, PermissionsMixin):
    # Primary KeyをUUIDに変更
    id    = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    email = models.EmailField(verbose_name='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."
        ),
    )

    # 追加フィールド
    created_time = models.DateTimeField(verbose_name='作成日時', auto_now_add=True) 
    updated_time = models.DateTimeField(verbose_name='更新日時', auto_now=True)
    name         = models.CharField(verbose_name='氏名', max_length=512)
    section      = models.CharField(verbose_name='所属', max_length=1024)
    remarks      = models.TextField(verbose_name='備考', blank=True, null=True)

    objects = UserManager()

    EMAIL_FIELD     = 'email'
    USERNAME_FIELD  = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name        = 'ユーザー'
        verbose_name_plural = 'ユーザー'

    '''
    ACCOUNT_USER_MODEL_USERNAME_FIELDを設定しているが、django-allauthでusernameが必要なため
    usernameとしてemailを返すように設定。
    '''
    @property
    def username(self):
        return self.email

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

views.py

ログインとログアウトの処理をオーバーライドします。
登録ユーザの有無を検査してからGoogleの2段階認証を行っているので、EmailAddressテーブルにメールアドレスを登録し、CustomUserとリンクを設定するのは自前で行います。

from django.contrib.auth import get_user_model
from django.contrib.auth.signals import user_logged_in
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.dispatch import receiver
from django.conf import settings
from django.shortcuts import render
from django.core.exceptions import ValidationError
from allauth.account.views import LoginView, LogoutView
from allauth.account.models import EmailAddress
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.views import OAuth2LoginView
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter

from .forms import CustomLoginForm
from .models import CustomUser

import logging
logger = logging.getLogger(__name__)

# Googleの2段階認証に伴う処理
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
    def pre_social_login(self, request, sociallogin):
        email = sociallogin.user.email
        try:
            # CustomUserに登録されているか確認
            user = CustomUser.objects.get(email=email)

            # CustomUserモデルのインスタンスを保存する(ルーティングに必要)
            sociallogin.user = user
            sociallogin.user.save()

            # メールアドレスを登録してCustomUserモデルと紐づける。
            email_address, created = EmailAddress.objects.get_or_create(
                user=user,
                email=email,
                defaults={'primary': True, 'verified': True}
            )
            if created:
                logger.info(f'{request.META.get("REMOTE_ADDR")} {email} email successfully created. (EmailAddress table)')
        except CustomUser.DoesNotExist:
            # 未登録の場合はエラーを発生させる
            messages.error(request, 'Your email is not registered. #1')
            raise ValidationError("This email is not registered.")

    # Googleの2段階認証が失敗した場合に、EmailAddressテーブルからメールアドレスを削除する。
    def authentication_failed(self, request, sociallogin):
        email = sociallogin.user.email
        try:
            email_address = EmailAddress.objects.get(email=email)
            email_address.delete()
            logger.info(f'{request.META.get("REMOTE_ADDR")} {email} email removed due to authentication failure.')
        except EmailAddress.DoesNotExist:
            logger.info(f'{request.META.get("REMOTE_ADDR")} {email} No email found to remove on authentication failure.')

# Googleの2段階認証でログイン処理を行う。
class CustomLoginView(LoginView):
    template_name = 'users/login.html'
    form_class    = CustomLoginForm

    def post(self, request, *args, **kwargs):
        form     = self.form_class(request.POST)
        next_url = request.POST.get('next', 'home')

        if form.is_valid():
            email = form.cleaned_data['email']
            try:
                # CustomUserに存在するか確認
                user = CustomUser.objects.get(email=email)

                # Google OAuth2 ログインを開始
                oauth2_login = OAuth2LoginView.adapter_view(GoogleOAuth2Adapter)
                return oauth2_login(request)

            except CustomUser.DoesNotExist:
                messages.error(request, 'Your email is not registered. #2')
                return render(request, self.template_name, {'form': form})

            except Exception as e:
                logger.error(f'{request.META.get("REMOTE_ADDR")} {email} Error during login: {str(e)}')
                messages.error(request, f'An error has occurred: {str(e)}')
                return render(request, self.template_name, {'form': form})

        return render(request, self.template_name, {'form': form})

# ログアウト処理を行う。
class CustomLogoutView(LoginRequiredMixin, LogoutView):
    template_name = 'users/logout.html'

forms.py

from allauth.account.forms import LoginForm
from django import forms

from .models import CustomUser

'''
views.pyで使う。
ユーザのログイン
'''
class CustomLoginForm(LoginForm):
    email = forms.EmailField(
        widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'Enter your email'}),
        label="Email",
        required=True
    )
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        del self.fields['login']
        del self.fields['password']
        
        # Bootstrapクラスを適用
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
    
    def clean(self):
        return self.cleaned_data
    
    def login(self, request, redirect_url=None):
        pass

'''
admin.pyで使う。
パスワードフィールドを除外したカスタムのユーザー作成フォーム
'''
class CustomUserCreationForm(forms.ModelForm):
    class Meta:
        model = CustomUser
        # パスワードフィールドを除外
        fields = ('email', 'name', 'section', 'remarks', 'is_staff', 'is_active')

    def save(self, commit=True):
        user = super().save(commit=False)
        # パスワードを使用不可に設定
        user.set_unusable_password()
        if commit:
            user.save()
        return user

'''
admin.pyで使う。
パスワードフィールドを除外したカスタムのユーザー変更フォーム
'''
class CustomUserChangeForm(forms.ModelForm):
    class Meta:
        model = CustomUser
        # パスワードフィールドを除外
        fields = ('email', 'name', 'section', 'remarks', 'is_staff', 'is_active')

urls.py

ログインとログアウト以外のdjango-allauthのルーティングは、全て404.htmlを表示させます。
ここでlogin_cancelled.htmlのルーティングを制御できれば良かったのですが・・・。

from django.urls import path, include
from django.shortcuts import render
from django.views.generic import TemplateView

from .views import CustomLoginView, CustomLogoutView

def redirect_to_404(request, exception=None):
    return render(request, '404.html', status=404)

urlpatterns = [
    # django-allauthのログインとログアウトを上書き
    path('account/login/', CustomLoginView.as_view(), name='account_login'),
    path('account/logout/', CustomLogoutView.as_view(), name='account_logout'),

    # Googleの2段階認証で使う。
    path('account/social/login/google/', include('allauth.socialaccount.providers.google.urls')),

    # django-allauthのログインとログアウト以外は404エラーにリダイレクト
    # 確認のためnameは同じだが、URLが異なるものも記述。
    path('account/login/cancelled/',         redirect_to_404, name='account_login_cancelled'),
    path('account/login/confirm/',           redirect_to_404, name='account_confirm_login_code'),
    path('account/signup/passkey',           redirect_to_404, name='account_signup_by_passkey'),
    path('account/login/code/',              redirect_to_404, name='account_request_login_code'),
    path('account/signup/',                  redirect_to_404, name='account_signup'),
    path('account/signup/',                  redirect_to_404, name='account_reauthenticate'),
    path('account/password/reset/',          redirect_to_404, name='account_reset_password'),
    path('account/password/reset/done/',     redirect_to_404, name='account_reset_password_done'),
    path('account/password/reset/key/',      redirect_to_404, name='account_reset_password_from_key'),
    path('account/password/reset/key/done/', redirect_to_404, name='account_reset_password_from_key_done'),
    path('account/password/change/',         redirect_to_404, name='account_change_password'),
    path('account/password/set',             redirect_to_404, name='account_set_password'),
    path('account/email/',                   redirect_to_404, name='account_email'),
    path('account/inactive/',                redirect_to_404, name='account_inactive'),
    path('account/confirm-email/',           redirect_to_404, name='account_confirm_email'),
    path('account/email-verification-sent/', redirect_to_404, name='account_email_verification_sent'),
    path('account/social/',                  redirect_to_404, name='socialaccount_connections'),
    path('account/social/signup/',           redirect_to_404, name='socialaccount_signup'),
    path('account/signup/',                  redirect_to_404, name='socialaccount_signup'),
    path('account/social/login/cancelled/',  redirect_to_404, name='socialaccount_login_cancelled'),
    path('account/login/cancelled/',         redirect_to_404, name='socialaccount_login_cancelled'),
    path('account/social/login/redirect/',   redirect_to_404, name='socialaccount_login_redirect'),
    path('account/login/error/',             redirect_to_404, name='socialaccount_login_error'),

    # django-allauthの定義
    path('account/', include('allauth.urls')),
]

handler404 = redirect_to_404

admin.py

from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _

from .models import CustomUser

from .forms import CustomUserCreationForm, CustomUserChangeForm

'''
カスタムユーザー
'''
CustomUser = get_user_model()

class CustomUserAdmin(UserAdmin):
    save_on_top = True
    save_as     = True

    '''
    ユーザー作成フォーム
    forms.pyで定義
    '''
    add_form = CustomUserCreationForm

    '''
    ユーザー変更フォーム
    forms.pyで定義
    '''
    form = CustomUserChangeForm

    # ユーザーの詳細画面、編集画面で表示するフィールド
    fieldsets   = [
        (None, {'fields': ('email',)}),
        (_('Personal info'), {'fields':('name', 'section', 'remarks')}),
        (_('Permissions'), {'fields':('is_active', 'is_staff', 'is_superuser', 'groups','user_permissions')}),
        (_('Important dates'), {'fields':('last_login',)}),
    ]

    # ユーザー追加フォームで入力するフィールド
    add_fieldsets = (
        (None,
        {
            'classes': ('wide',),
            'fields': (
                'email',
                'name',
                'section',
                'remarks',
            ),
        },),
    )

    list_display = (
        'email',
        'name',
        'section',
        'is_staff',
        'is_superuser',
        'is_active',
        'created_time',
        'updated_time',
    )
    list_filter = ('is_staff', 'is_superuser', 'groups')
    search_fields = ('email', 'name', 'section')
    ordering = ('is_active',)
    filter_horizontal = (
        'groups',
        'user_permissions',
    )

admin.site.register(CustomUser, CustomUserAdmin)

login.html

メールアドレスを入力して、Googleマークのボタンをクリックすると、Googleの2段階認証が起動します。

{% extends 'base.html' %}
{% load static %}
{% block title %}
Login
{% endblock title %}
{% block body %}
<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-4">
            <h2 class="text-center">Login</h2>
            <p>Only users who have been pre-registered by the administrator can use it.</p>
            <p>Please enter your email address to continue.</p>
            <form method="POST" action="{% url 'account_login' %}">
                {% csrf_token %}
                <input type="hidden" name="next" value="{% url 'home' %}">
                <div class="form-group mb-3">
                    <input type="email" class="form-control" id="email" name="email" placeholder="Enter your email address" required>
                </div>
                
                <button type="submit" class="btn btn-light w-100 d-flex align-items-center justify-content-center">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="24px" height="24px">
                        <path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/>
                        <path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/>
                        <path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/>
                        <path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/>
                    </svg>
                    <span class="ms-2">Sign in with Google</span>
                </button>
            </form>
            {% if messages %}
                <div class="mt-3">
                    {% for message in messages %}
                        <div class="alert alert-{{ message.tags }}">{{ message }}</div>
                    {% endfor %}
                </div>
            {% endif %}
        </div>
    </div>
</div>
{% endblock body %}

logout.html

{% extends 'base.html' %}
{% load static %}

{% block title %}
Logout
{% endblock title %}

{% block h1_title %}
Logout
{% endblock h1_title %}

{% block body %}

<p>You are logged out.</p>
<p><a href="{% url 'account_login' %}">Login</a></p>

{% endblock body %}

base.html

login.html、logout.htmlの最初で読み込みます。
Bootstrapは、static/cssに置いています。

{% load i18n static %}
<!doctype html>
<html lang="ja" data-bs-theme="dark">
<head>
<meta charset="utf-8">

<link rel="shortcut icon" type="image/png" href="{% static '' %}">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">

<title>
{% block title %}
{% endblock %}
</title>

</head>

<body>

{# container: 表示幅を可変にする。#}
<div class="container-fluid">

{# formのエラー表示 #}
{% if form.errors and request.method == 'POST' %}
    {% for field, errors in form.errors.items %}
        {% for error in errors %}
            <div class="alert alert-danger" role="alert">
                <p class="mb-0">{{ field }} {{ error }}</p>
            </div>
        {% endfor %}
    {% endfor %}
{% endif %}


{# 「form.errors」以外のエラー表示(カスタムバリデーションなど)  #}
{% if non_field_errors %}
    <div class="alert alert-danger" role="alert">
        {% for error in form.non_field_errors %}
            <p{% if forloop.last %} class="mb-0"{% endif %}>{{ error }}</p>
        {% endfor %}
    </div>
{% endif %}

{# SuccessMessageMixinのメッセージ表示 #}
{% if messages %}
<div class="text-center messages alert alert-primary" role="alert">
    {% for message in messages %}
        {{ message }}
    {% endfor %}
</div>
{% endif %}

{% block error_404 %}
{% endblock %}

{% block body %}
{% endblock %}

{# フッタ #}
<footer class="container-fluid">
<small>
&copy;ほげ
</small>
</footer>

</div>

</body>
</html>

404.html

django-allauthでルーティングされているログイン、ログアウト以外は、このページを表示します。

{% extends 'base.html' %}

{% block title %}
ページが見つかりませんでした
{% endblock title %}

{% block h1_title %}
It is just a 404 Error !
{% endblock h1_title %}

{% block brain_404 %}
<p class="text-center">
Sorry, the requested URL was not found on this server.
<br>
<a href="{% url 'account_login' %}" class="btn btn-outline-danger">Login</a>
</p>
{% endblock brain_404 %}

home.html

ログインが成功した場合に表示します。
各種メニューを列挙します。

{% extends "base.html" %}
{% load static %}

{% block title %}
Menu
{% endblock title %}

{% block h1_title %}
Menu
{% endblock h1_title %}

{% block body %}

{% if user.is_authenticated %}
    <div class="row justify-content-center">
        <p>
        <div class="col-auto">
            <a class="btn btn-success" href="{% url 'ほげ' %}">ほげほげ</a> 
        </div>
        </p>
    </div>

    <div class="row justify-content-center">
        <p>
        <div class="col-auto">
            <form method="post" action="{% url 'account_logout' %}">
            {% csrf_token %}
            <button class="btn btn-outline-secondary" type="submit">Logout</button>
            </form>
        </div>
        </p>
    </div>
{% else %}
    <p class="text-center"><a class="btn btn-outline-success" href="{% url 'account_login' %}">Login</a></p>
{% endif %}

{% endblock body %}

Comments