Djangoカスタムユーザーモデル作成＆管理者サイトログイン(編集中)

# はじめに
こちらの記事の続きになります。

https://qiita.com/Nats72/items/95c5b2e903261cfbec3c

### Django管理サイトとは
Djangoには標準で管理サイトが用意されていて、接続しているDBを参照・編集ができたりします。
私的には、この管理サイトが標準で用意されているところがDjangoの良いところだと思ってます。

# ルーティング設定ファイル
`urls.py`はブラウザから送られるHTTPリクエストに対して、どのページを表示するかというルーティングを記載するファイル。
設定ディレクトリや、アプリケーションディレクトリに作成します。
※設定ディレクトリのurls.pyは自動で作成されますが、アプリケーションディレクトリのurls.pyは手動で作成してあげる必要があります。

### (参考)最初にアクセスされるurls.py
`settings.py`に書かれています。
以下の例の場合ですと、設定ディレクトリ(config)内のurls.pyを最初にアクセスするという記述になります。
つまり、このurls.pyに記載されているルーティングが優先されます。
```python:settings.py
ROOT_URLCONF = 'config.urls'
```

### ルーティング設定ファイルの確認
設定ディレクトリに作られるルーティングファイル`urls.py`は以下のようになっています。（コメントアウトは省いてます）
注目すべきは、`urlpatterns`の部分。
`http://{FQDN}:{port}/admin`にアクセスがあったときに、`admin.site.urls(Django管理サイト)`を参照するようなルーティングが書かれています。
※初期状態だとDjango管理サイトの設定をしてないのでブラウザでURLを叩いても何も表示されません。
```python:config/urls.py
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]
```

# アプリケーション作成
Djangoで利用するアプリケーションを作成する。
以下のコマンドでアプリ用のテンプレ構成が作成される。（作られたばかりだと中身スカスカ）
`app`は任意のアプリケーション名を設定。
アプリケーションの中にユーザーの設定を書いていく。
```console:コンテナ
# python manage.py startapp app
```
`settings.py`に作成したアプリケーションを追記する。
`AUTH_USER_MODEL`の設定も追加。認証用のユーザーモデルには、これから作成する`CustomUser`を利用するよ、という記述をしておきます。
```diff:settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
+     'app',
+ 
+ AUTH_USER_MODEL = 'app.CustomUser'
```

# カスタムユーザーモデルを作成
Djangoのユーザーモデルには3種類ある。
今回は、AbstractBaseUserを使って作成する。

| 種類 | カラム操作 | 特徴 |
|:-|:-|:-|
| User | 不可 | Django標準モデル。Model作成後にカラム追加ができないため、標準の状態で使う。 |
| AbstractUser | 追加・変更のみ | カラム追加や変更は可能だが、削除ができないので、使う予定のないカラムが入ってしまう。 |
| AbstractBaseUser | 追加・変更・削除 | オリジナルのテーブルが作成可能。好きにカスタマイズできるのはこれ。 |

:::note info
**参考**
- AbstractUserモデルは、AbstractBaseUserモデルとPermissionsMixinを継承している。
- Userモデルは、AbstractUserモデルを継承している。
:::

どのように継承されているかは、こちらを参照。

https://github.com/django/django/blob/main/django/contrib/auth/models.py

https://github.com/django/django/blob/stable/4.2.x/django/contrib/auth/base_user.py

## models.py
```python:./app/models.py

```

#### from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager, PermissionsMixin

| 設定 | 説明 |
|:-|:-|
| AbstractBaseUser | AbstractBaseUserを継承してカスタマイズ可能なユーザーモデルを作成する。 |
| BaseUserManager | BaseUserManagerを継承してCustomUserManagerクラスを作っている。 |
| PermissionsMixin | AbstractBaseUserはパーミッション(権限)関連の機能を持っていないため、パーミッションの機能を利用したい場合、PermissionsMixinを同時に継承しておく必要がある。 |

継承元のコードはこちらから参照できる。

https://github.com/django/django/blob/stable/4.2.x/django/contrib/auth/base_user.py

#### create_user,create_superuser
`create_user`や、`create_superuser`の作り方は、UserManagerクラスの`create_user`、`create_superuser`メソッドを参考にしている。

https://github.com/django/django/blob/stable/4.2.x/django/contrib/auth/models.py

#### from django.utils.translation import gettext_lazy as _
多言語対応(翻訳)させるときに使う。させない場合は不要。
`as _`としており、`_('ユーザー名は必須です。')`ここで使われている。
なぜアンダーバーなのかというと、Pythonの標準ジュールである`gettext`の慣習的なものなのだそう。
これを使う場合は、`settings.py`に`USE_I18N = True`の設定が必要。
```python:models.py(抜粋)
if not username:
  raise ValueError(_('ユーザー名は必須です。'))
```
`django.utils.translation`には、`gettext`と`gettext_lazy`があるのだが翻訳が実行されるタイミングが違うらしい。

継承元のコードはこちら。

https://github.com/django/django/blob/stable/4.2.x/django/utils/translation/__init__.py


:::note warn
こちらのAbstractUserを参考にすると、Metaクラスは以下のようになります。
https://github.com/django/django/blob/main/django/contrib/auth/models.py
```python
class Meta:
      verbose_name = 'user'
      verbose_name_plural = 'users'
      abstract = True    # これは削除する
```
`abstract = True`は削除してください。`python manage.py make migrations`したときにエラーになります。
以下のようなエラーがでます。
**django.core.exceptions.ImproperlyConfigured: AUTH_USER_MODEL refers to model 'account.CustomUser' that has not been installed**
抽象モデル（クラス）とみなされて実体が作成されず、エラーになるようです。

(参考)こちらのサイトを参考にさせていただきました
https://daeudaeu.com/django-abstractbaseuser/
:::

# 管理者ページを作成する
`admin.py`に以下の記述をします。
このファイルはDjango管理サイトに表示させる内容を記述するファイルになります。
fieldsetsおよびfieldsは、リスト型もしくはタプル型で記述します。
```python:./account/admin.py
from django.contrib.auth.admin import UserAdmin
from django.contrib import admin
from .models import CustomUser

@admin.register(CustomUser)
class CustomUserAdmin(admin.ModelAdmin):

    fieldsets = (
        (None, {"fields": ("username", "password")}),
        ("Personal info", {"fields": ("email",)}),
        ("Permissions", {"fields": ("is_active", "is_staff", "is_admin", "groups", "user_permissions")}),
        ("Important dates", {"fields": ("last_login", "date_joined")}),
    )

    list_display = ("username", 'email', "last_login", 'is_staff')
    search_fields = ("username", "email")
    filter_horizontal = ("groups", "user_permissions")
```

# DBに反映させる
DBに反映させる内容を作成します。
```console
# python manage.py makemigrations
```
`makemigrations`の最後の**s**をつけ忘れやすいので注意しましょう。
**No changes detected**とか、エラーが出てなさそうな表示がされればOK。

DBに反映させます。
```console
# python manage.py migrate
```
エラーがでなければOK。


:::note warn
すでにUserモデルを作成している場合、カスタムユーザー作成しようとするとエラーになります。
最初から作り直した方が楽と言われるくらい面倒くさいようなので、DBの中身を作り込む前に、カスタムユーザーモデルだけ作成しておきましょう。

Userモデルを作り直す場合、面倒くさい手順を踏みます。
1. DBの削除(**db.sqlite3**の場合はファイルごと削除)
2. DB新規作成
(参考)sqlite3のDB作成方法はこちらにまとめてます。
https://qiita.com/Nats72/items/4a420d7a54a0f67aa0cd

3. アプリケーションディレクトリにある`migrations`ディレクトリの中の`__init__.py`以外を削除
※誤ってmigrationsディレクトリをまるごと削除してしまったら、`migrations`ディレクトリを作成し、その中に、`__init__.py`を空ファイルで作成します。
4. 再度マイグレーションします。
※このとき、`./config/settings.py`や、`./config/urls.py`のadminに関する部分をコメントアウトしておきます。
```python:./config/settings.py
INSTALLED_APPS = [
    # 'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'account',
]
```
```python:./config/urls.py
# from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    # path('admin/', admin.site.urls),
    path('', include('account.urls')),
]
```
5. コメントアウトを外して、再度`python manage.py migrate`します。
:::

# 管理者ユーザーの作成
`python manage.py createsuperuser`で作成します。
ユーザーを作成しておかないと、後述するDjango管理サイトにログインできないためです。
```console
# python manage.py createsuperuser
ユーザー名: admin
Eメールアドレス: hogehoge@mail.com
Password: 
Password (again): 
Superuser created successfully.
```
# Django管理サイトにアクセスしてみる
http://localhost:8000/admin/
にアクセスしてみます。※localhostで8000番ポートで公開させている場合

下記のURLにリダイレクトされて、ログインページが表示されると思います。
http://localhost:8000/admin/login/?next=/admin/

先程作成した管理者ユーザーのアカウントでログインします。
ログインできたら成功です。

# CSSが適用されていない場合
adminの静的ファイルを集めてきましょう。
```console
# python manage.py collectstatic
```
参考ページはこちら

https://qiita.com/Nats72/items/0b7baa5fe5282df2790a




## ライブラリを追加
後述するルーティングの記載には、`path`、`include`、`re_path`といった関数を使います。
※インポートしないと使えないので、必ずインポートしておきましょう。
```diff:urls.py
from django.contrib import admin
from django.urls import path
+ from django.urls import path, include, re_path
```

## アプリケーションディレクトリのurls.pyを参照させる
設定ディレクトリのurls.pyから、アプリケーションディレクトリのurls.pyを参照させる記述になります。
urlpatternsはリスト型なので、複数記述する場合はカンマで区切ります。
```python:config/urls.py
urlpatterns = [
    path('', include('app.urls')),
    path('sub/', include('app.sub.urls'))
]
```
`path('', include('app.urls'))`は、appディレクトリのurls.pyに、
`path('sub/', include('app.sub.urls'))`は、appディレクトリの中にあるsubディレクトリのurls.pyに渡します。
※(参考)アプリケーションディレクトリの中に、子ディレクトリを作ることも可能です

## views.pyを参照する
アプリケーションのurls.pyから、views.pyを参照するときに記述します。
設定ディレクトリのurls.pyにも書くことはできると思いますが、アプリケーションディレクトリのurls.pyに書くほうが、どのviews.pyを参照しているか分かりやすいので、アプリケーション側に記述するほうが一般的かなと思います。
```python:app/urls.py
urlpatterns = [
    path('{パス名}', views.{関数名}, name='{名前}'),
    path('{パス名}', views.{クラス名}.as_view(), name='{名前}')
]
```
nameという引数を書いてますが、これはそのpath()に対する名前を書いています。この引数(name)は記述しなくても動きます。関数名と同じ名前を入れてやればいいんじゃないかなと思います。

(例)`views.py`のindex()関数を呼び出す書き方
```python:app/urls.py
urlpatterns = [
    path('', views.index, name='index'),
    path('blog/', views.blog, name='blog'),
]
```
- `path('', views.index, name='index'),`は、http://{サイト名}/のパスにアクセスがあった際に、`views.py`のindex()関数を呼び出します。
- `path('blog/', views.blog, name='blog'),`は、http://{サイト名}/blog/のパスにアクセスがあった際に、`views.py`のblog()関数を呼び出します。

## URLパターン
パスコンバータを使った記述方法と、re_path()関数を使った記述方法があります。

### パスコンバータを使った書き方
int,str,path,slugといったパスコンバータを使えます。
以下に使用例を記載します。
```python:app/urls.py
urlpatterns = [
    path('', views.index, name='index'),
    path('<int>/', views.integer, name='integer'),
    path('blog/<int:page>/', views.blogpage, name='blogpage'),
]
```
- `path('<int>/', views.integer, name='integer')`は、http://{サイト名}/**整数**/のパスにアクセスがあった際に、`views.py`のinteger()関数を呼び出します。
- `path('blog/<int:page>/', views.blogpage, name='blogpage'),`は、http://{サイト名}/blog/**整数**/のパスにアクセスがあった際に、`views.py`のblogpage()関数を呼び出します。その際の整数は、**page**という名前の引数で渡されます。

### re_path()を使った書き方
正規表現を使って記述できます。
```
re_path('[0-9]', views.integer, name='integer'),
```

## 静的ファイルを扱う
```python:app/urls.py
urlpatterns = [
    re_path(r'^media/(?P<path>.*)$', serve,{'document_root': settings.MEDIA_ROOT}), 
    re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT}), 
]
```
こうも書ける
```python:app/urls.py
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 
```
デバッグモード時のみ扱う場合の書き方
```python:app/urls.py
if settings.DEBUG:
    from django.conf.urls.static import static
    from django.contrib.staticfiles.urls import staticfiles_urlpatterns

    urlpatterns += staticfiles_urlpatterns()
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```