16
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

django-allauthで、CustomUserを使って複数種類のユーザを管理する(multi type user)

Last updated at Posted at 2020-10-30

概要

  • Djangoで、複数のユーザ種別を作れるシステムが欲しい
    • 「サプライユーザ」「バイヤーユーザ」の2種類のユーザを作れるようにする
    • ユーザ種別ごとに、異なる属性を持たせる
      • サプライユーザは会社名、バイヤーユーザは最寄り駅を登録できるようにする
  • 環境は、
    • Python 3.6.9
    • Django 3.1.1
    • django-allauth 0.42.0 (ソーシャルログイン機能)

Python + Django の制限

  • ユーザ認証(ログインやサインアップ)に使用できるモデルクラスは1つだけ
    • settings.pyの AUTH_USER_MODEL で指定する

実装の概要

  • CustomUserモデルを1つ作り、userTypeをもたせる
  • ユーザ種別ごとの情報は別テーブルに持ち、カスタムユーザクラスとOneToOneFieldで紐付ける
    • UserDetailSupplier
    • UserDetailBuyer
  • ユーザの保存にはアダプタを使う。
    • AccountAdapterをallauth.account.adapter.DefaultAccountAdapter を継承して作成する
    • save_userメソッドを実装し、その中でuserTypeごとに保存処理を分ける
    • settings.py の ACCOUNT_ADAPTER で上記クラスを指定する
  • サインアップ用のテンプレートは種別ごとに分けるが、送信先は1つ、送信先も2つに分ける。
    • signupsignup_supplierを作成
  • ログイン用のテンプレートは1つ

django-allauth と自作クラスの関係

  • django-allauthはaccountというアプリ名を使っているが、それとは別にmemberというアプリを作り、そこで各種管理を行う
    • ググると、複数形のaccountsという名前で作る例が多いが、allauth側と区別がつけづらいのと、ここだけ複数形にしたくなかったので、別な名前をつけた
      • Pythonのメンバ関数と似ているが、アプリ名だから混同しないだろうという考え

問題点

  • サインアップ時にアカウント重複などでエラーとなった場合に、追加したテンプレートではなく元からあるテンプレートに戻ってしまう
    • signup_supplierでサインアップしようとして失敗すると、signupに遷移してしまう
      • フォームの送信先を2つに分けて解決。
  • 追加した項目をフォームから取得する際に request.POST を使っているが、どうにかならないか
    • form.cleaned_data.get から取得しようとしたが、空で取得できなかった
  • ユーザ情報のテーブルが分かれるので、admin画面が分かりづらい。管理画面を自作したほうがよさそう

実装の具体例

django-allauthのインストール

  • pipでインストールし、各種設定を行う

設定

  • 設定ファイルに以下を追加する

# settings.py

# 認証に使うモデルを指定
AUTH_USER_MODEL = 'member.CustomUser'
# signupformからの情報をcustomusermodelに保存するためのアダプタを指定
ACCOUNT_ADAPTER = 'member.adapter.AccountAdapter'

モデルクラスの作成


# member/models.py

from django.contrib.auth.models import AbstractUser
from django.db import models
from django.contrib.auth.models import PermissionsMixin, UserManager

class UserType(models.Model):
    """ ユーザ種別 """
    typename = models.CharField(verbose_name='ユーザ種別',
                                max_length=150)

    def __str__(self):
        return f'{self.id} - {self.typename}'

USERTYPE_SUPPLIER = 100
USERTYPE_BUYER = 200
USERTYPE_DEFAULT = USERTYPE_BUYER

class CustomUserManager(UserManager):
    """ 拡張ユーザーモデル向けのマネージャー """

    def _create_user(self, email, password, **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.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 CustomUser(AbstractUser):
    """ 拡張ユーザーモデル """

    class Meta(object):
        db_table = 'custom_user'

    #作成したマネージャークラスを使用
    objects = CustomUserManager()

    # モデル内にユーザ種別を持つ
    userType = models.ForeignKey(UserType,
                                verbose_name='ユーザ種別',
                                null=True,
                                blank=True,
                                on_delete=models.PROTECT)
    def __str__(self):
        return self.username

class UserDetailSupplier(models.Model):
    user = models.OneToOneField(CustomUser,
                                unique=True,
                                db_index=True,
                                related_name='detail_supplier',
                                on_delete=models.CASCADE)
    # サプライヤーユーザ向けの項目
    companyName = models.CharField(
                                   max_length=100,
                                   null=True,
                                   blank=True,
                                )
    def __str__(self):
        user = CustomUser.objects.get(pk=self.user_id)
        return f'{user.id} - {user.username} - {user.email} - {self.id} - {self.companyName}'

class UserDetailBuyer(models.Model):
    user = models.OneToOneField(CustomUser,
                                unique=True,
                                db_index=True,
                                related_name='detail_buyer',
                                on_delete=models.CASCADE)
    # バイヤーユーザ向けの項目
    nearestStation = models.CharField(
                                   max_length=100,
                                   null=True,
                                   blank=True,
                                )
    def __str__(self):
        user = CustomUser.objects.get(pk=self.user_id)
        return f'{user.id} - {user.username} - {user.email} - {self.id} - {self.nearestStation}'

アダプタの作成


# member/adapter.py

from allauth.account.adapter import DefaultAccountAdapter
from .models import *

class AccountAdapter(DefaultAccountAdapter):

    def save_user(self, request, user, form, commit=True):
        """
        This is called when saving user via allauth registration.
        We override this to set additional data on user object.
        """
        # Do not persist the user yet so we pass commit=False
        # (last argument)
        user = super(AccountAdapter, self).save_user(request, user, form, commit=False)
        #user.userType = form.cleaned_data.get('userType')
        user.userType = UserType(request.POST['userType'])

        if not user.userType:
            user.userType = UserType(USERTYPE_DEFAULT) # デフォルトのユーザ種別を設定

        # ユーザIDを取得するために一旦保存する
        user.save()

        if int(user.userType.id) == USERTYPE_SUPPLIER:
            # サプライヤーユーザ
            supplier = UserDetailSupplier()
            supplier.user_id = user.id
            supplier.companyName = request.POST['companyName']
            supplier.save()
        else:
            # それ以外は一般ユーザ
            user.userType = UserType(USERTYPE_BUYER)
            buyer = UserDetailBuyer()
            buyer.user_id = user.id
            buyer.nearestStation = request.POST.get('nearestStation', False)
            buyer.save()

テンプレートの作成

  • django-allauthのテンプレートを上書き
    • templates/account/signup.html
    • templates/account/signup_supplier.html
      • formにhiddenでユーザ種別を入れる。 <input type="hidden" name="userType" value="1" />
      • POST先は、どちらも {% url 'account_signup' %} とする

urls.pyの設定

  • 以下を追加

    path('member/signup_supplier/', TemplateView.as_view(template_name = 'account/signup_supplier.html'), name='signup_supplier'),

その他

  • admin.pyに、各モデルクラスを追加しておく

参考

16
18
2

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
16
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?