自分用のDjangoプロジェクトの雛形を作ってみた

# 更新履歴

- 2020年12月29日
 - 素のDjangoではなくWagtailをベースにしたプロジェクト構成に変更
 - Django REST Frameworkの基本的な初期設定を追加
 - BootstrapVueをReact Bootstrapに変更


# はじめに

Djangoのプロジェクトを作成するときにとりあえず最初にすることをまとめて雛形にしておけば便利だろうとふと思いついて作ってみた．といっても，[DjangoX](https://github.com/wsvincent/djangox)や[cookiecutter](https://github.com/audreyr/cookiecutter)みたいなフォーマルなものじゃなくて，基本的に自分用のインフォーマルな手作りの雛形だ．備忘録を兼ねてここに概要をまとめておこうと思う．おおまかな方針は下記の通り．

- 素のDjangoではなく[Wagtail](https://wagtail.io/)をベースにしたプロジェクト構成
- AbstractUserを継承してカスタムユーザの雛形作成
- [django-environ](https://github.com/joke2k/django-environ)を導入して環境毎に設定を切替え可能に
- [django-allauth](https://github.com/pennersr/django-allauth)を導入してユーザ認証まわりをまとめて実装
- [Django REST Framework](https://www.django-rest-framework.org/)の基本的な初期設定を追加
- テンプレートに[React Bootstrap](https://react-bootstrap.github.io/)を導入

なお，以下の作業は，macOS Big Sur搭載のマシンで，Pipenvで作成したPython 3.8の仮想環境を使用して行った．

# 必要なライブラリのインストールからプロジェクトの構成まで

プロジェクト用のディレクトリ（名前は何でもいいが以下ではmyCMSとする）を作成し，そのディレクトリで仮想環境を構築して必要なライブラリをいくつかインストールする．

```console
$ mkdir myCMS
$ cd myCMS
$ pipenv --python 3
$ pipenv install wagtail  # これでDjangoもインストールされる
$ pipenv install djangorestframework
$ pipenv install django-environ
$ pipenv install django-allauth
```

作成した仮想環境のシェルを起動して，以降はその中で作業を進めていく．

```console
$ pipenv shell
```

最初に，アプリケーションを格納していくためのプロジェクトを作成しよう．本稿では，これを素のDjangoではなく，Wagtailのプロジェクトとして構成する．そして，下記のように，カスタムユーザを定義するためのアプリケーションuserを追加しておく．

```console
$ wagtail start config .
$ python manage.py startapp users
```

# カスタムユーザの雛形作成

上の準備によって，myCMSの中にusersというディレクトリが作成されているはずである．users/models.pyのファイルを開いて，カスタムユーザのモデルを定義しよう．

```python
from django.db import models
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    pass
```
AbstractUserを継承してCustomUserというモデルを定義しているのがわかる．ただし，単なる雛形なので，中身はpassのみで具体的にはまだ何も変更していない．実際には，対象プロジェクト（の中の各アプリケーション）のニーズに応じてフィールドを追加するなどの拡張を加えていくことになる．なお，最初にカスタムユーザを定義しておくことが推奨されている理由は，プロジェクトを運用し始めてから途中でユーザモデルに拡張を加えたくなったとしても，デフォルトのユーザモデルのままではそれが難しいからということらしい．

ここで，users/admin.pyを開いて，このカスタムユーザのモデルをAdminサイトにも登録しておこう．

```python
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser

admin.site.register(CustomUser, UserAdmin)
```

# settingsの修正

プロジェクトの枠組みをWagtailで作成したので，myCMSの中のconfig/settingsは，下記のように，モジュール（ファイル）ではなくパッケージ（ディレクトリ）になっているはずである．

```
config/
  settings/
    __init__.py
    base.py
    dev.py
    production.py
```

config/settingsの中のdev.pyにローカルの開発環境の設定を，production.pyにサーバ上の運用環境の設定をそれぞれ格納すると考える．なお，base.pyには共通的な設定を記述しておき，双方からimportするようになっている．dev.pyとproduction.pyのどちらの設定ファイルを使用するかは，環境変数DJANGO_SETTINGS_MODULEで指定することになるが，manage.py（やconfig/wsgi.py）をみると，この環境変数が明示されていない場合は，dev.pyがデフォルトになることがわかる．

```python
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev")
```

django-environの助けを借りてこれらの設定ファイルを少し修正する．ポイントは，`SECRET_KEY`などのセキュリティ上重要な設定値を環境変数に登録しておいてそこから読み込むようにすることと，開発と運用など，環境ごとに異なる設定値（データベースやEメールバックエンドなど）をあらかじめ.envファイルに書いておいてそこから読み込むようにすることである．また，django-environには，ディレクトリの指定を簡単に記述できる機能も備わっているので，それも活用するようにしよう．

ここでは，`SECRET_KEY`，`DEBUG`，`ALLOWED_HOSTS`，`DATABASE_URL`の4つの設定値を.envファイルから読み込むと仮定して話を進めよう（ただし，特に運用環境では，セキュリティ上重要な設定値は.envファイルから読み込むよりも環境変数に登録しておいた方がいいように思う）．

myCMSのディレクトリに.envというファイルを作成し次の内容を書き込んでおく（実際の設定値は環境に応じて書き換えてください）．

```
SECRET_KEY=<< replace this with your secret key >>
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
DATABASE_URL=sqlite:///db.sqlite3
```
また，シェルに環境変数を1つ追加しておく（これはシェルの設定ファイルに記述しておくと便利だろう）．

```console
$ export DJANGO_READ_DOT_ENV_FILE=True
```
その上で，base.pyの出だしの部分を以下のように書き換える．

```python
import environ # django-environのインポート（これでosモジュールは不要になる）

# ディレクトリの設定がこのように簡単にできる
BASE_DIR = environ.Path(__file__) - 3 # myCMS/
CONFIG_DIR = environ.Path(__file__) - 2 # myCMS/config/

env = environ.Env() # 環境変数の読み込み

# .envファイルを読み込むかどうかを指定する環境変数
READ_DOT_ENV_FILE = env.bool('DJANGO_READ_DOT_ENV_FILE', default=False)

if READ_DOT_ENV_FILE:
    env.read_env(str(BASE_DIR.path('.env'))) # myCMS/.envの読み込み

#.envから読み込んだ値をもとに環境変数を設定
SECRET_KEY = env('SECRET_KEY')
DEBUG = env.bool('DEBUG', False)
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
DATABASES = {
    'default':env.db(),
}
```

これで`SECRET_KEY`，`DEBUG`，`ALLOWED_HOSTS`，`DATABASES`は設定できたので，これらに該当するデフォルトの記述は削除しておこう．開発環境のEメールバックエンドの設定は，dev.pyに記述されているものをそのまま残しておけばOKだろう．一方，運用環境のEメールバックエンドの設定は，production.pyに（セキュリティに配慮しながら）新たに記述する必要があるが，ここでは省略する． 

この段階で，他にもいくつか変更を加えておく．まず，`INSTALLED_APPS`に6項目を追加する．

```python
INSTALLED_APPS = [
    ...,

    'django.contrib.sites', # django-allauthのために追加

    'allauth', # django-allauthのために追加
    'allauth.account', # django-allauthのために追加
    'allauth.socialaccount', # django-allauthのために追加

    'rest_framework',  # Django REST frameworkのために追加

    'users.apps.UsersConfig', # カスタムユーザモデルのアプリケーション
]
```
次に，configディレクトリの中にプロジェクト全体に関わるテンプレートとstaticファイルを格納するようにする．

```python
# myCMS/config/templatesにもテンプレートを置く
TEMPLATES = [
    {
        ...,
        'DIRS': [CONFIG_DIR('templates'),],
        ...,

    },
]

# myCMS/config/staticにもstaticファイルを置く
STATICFILES_DIRS = [
    CONFIG_DIR('static'),
]

# collectstaticでのコピー先の記述もenvironに合わせて修正
STATIC_ROOT = BASE_DIR('static')
MEDIA_ROOT = BASE_DIR('media')
```

ここで，時刻の設定も変更しておこう．

```python
TIME_ZONE = 'Asia/Tokyo'  # 日本時間（JST）
```

あわせて，Django REST frameworkの基本的な設定も追加しておく．

```python
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
}
```

# ユーザ認証機能の導入

続いて，django-allauthを用いてユーザ認証の機能を盛り込んでいく．当初，django-allauthはソーシャル認証の機能を提供してくれる（だけの）モジュールだと思っていたんだけど，よく調べるとそうではなかった．自分のサイト上での通常のユーザ登録，ログイン，ログアウト，パスワード再設定など，ユーザ認証まわりの大抵の機能は用意されていて，djangoデフォルトのものよりも便利そうだった（ので，全部まとめて利用しちゃおう）．

まず，auth関連の設定として，設定ファイルbase.pyに下記の内容を書き加える．

```python
# デフォルトではなく上で作成したカスタムユーザモデルを使う
AUTH_USER_MODEL = 'users.CustomUser'

# ログイン後とログアウト後のリダイレクト先を変更
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'

# django-allauthを使うための設定
AUTHENTICATION_BACKENDS = (
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
)

# django-allauthに必要となるsitesフレームワークのための設定
SITE_ID = 1

# django-allauthのemailに関する設定
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
```

これで必要な設定は盛り込めたので，後はconfig/urls.pyを少し編集すればOKだ．下では，Django REST frameworkのユーザ認証機能なども合わせて組み込んでいる．

```python
...
from django.views.generic.base import TemplateView

urlpatterns = [
    ...,

    # ホームページ用の設定（後述）
    path('', TemplateView.as_view(template_name="home.html"), name="home"),

    # django-allauthのユーザ認証機能のためのルーティング
    path('accounts/', include('allauth.urls')),

    # Django REST frameworkのユーザ認証機能のためのルーティング
    path('api-auth/', include('rest_framework.urls')),
]
```

これで，django-allauthのユーザ登録（`/accounts/signup/`），ログイン（`/accounts/login/`），ログアウト（`/accounts/logout/`），パスワード再設定（`/accounts/password/reset/`）などのページが利用できるようになる．なお，[ソーシャル認証の導入方法](https://django-allauth.readthedocs.io/en/stable/index.html)はここでは省略する．

# テンプレートの構造化と見栄えの調整

上で，config/templatesの中にプロジェクト全体に関わるテンプレートを格納できるようにした．そこで，ここに起点となる共通的なテンプレート（root.html）を置いておき，必要に応じてそれを継承してレンダリングに利用するようにしよう．config/urls.pyに追加したTemplateViewで利用するホームページのテンプレート（home.html）もここに置いておけばよい．また，django-allauthのテンプレートもこのroot.htmlを継承するようにして，少しだけ見栄えよくしてみたい．

見栄えの調整には[React Bootstrap](https://react-bootstrap.github.io/)を利用する．javascriptやcssなどのstaticファイルのフォルダ構成もテンプレートに準じて構造化しておくとわかりやすいと思う．将来的にアプリケーションを追加していった際には，アプリケーション固有のテンプレートやstaticファイルはそのアプリケーションのディレクトリ内に置くとして，プロジェクト全体に関わるものについては以下のような構造にまとめた．

```
config/
  static/
    css/
      config.css
      root.css
    js/
      config.js
      home.min.js
      navbar.min.js
  templates/
    account/
      base.html
    404.html
    500.html
    base.html
    home.html
    root.html
```

これらのうち，templates/404.html，templates/500.html，templates/base.htmlの3つはWagtail用に自動作成されたテンプレートであり，今後，WagtailのCMSアプリケーションを作成した際に利用することになるので，そのまま放置しておく．また，css/config.cssとjs/config.jsは，templates/base.htmlから読み込まれるstaticファイルとして自動作成された空ファイルである（ので，これも放置しておく）．

ここでは，それら以外のファイルを新たに作成していく．まず，config/templates/root.htmlから見ていこう．

```jinja
{% load static %}

<!DOCTYPE html>
<html class="no-js" lang="en">
    <head>
        <meta charset="utf-8" />
        <title>{% block head_title %}myCMS{% endblock %}</title>
        <meta name="description" content="" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />

        {# stylesheets #}
        <link
        rel="stylesheet"
        href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
        integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
        crossorigin="anonymous"
        />

        {% block css %}
            <link rel="stylesheet" type="text/css" href="{% static 'css/root.css' %}">
        {% endblock %}
    </head>

    <body>
        {% block body_divs %}
            <div id="header">{% block header %}{% endblock %}</div>
            <div id="main">{% block main %}{% endblock %}</div>
            <div id="footer">
                {% block footer %}
                    <nav class="navbar navbar-dark bg-info fixed-bottom">
                        <span class="navbar-text mx-auto py-0">
                            {% block footer_text %}
                                Say something at the end.
                            {% endblock %}
                        </span>
                    </nav>
                {% endblock %}
            </div>
        {% endblock %}

    {# javascript #}
        <script src="https://unpkg.com/react@17/umd/react.production.min.js" crossorigin></script>
        <script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js" crossorigin></script>
        <script
        src="https://unpkg.com/react-bootstrap@next/dist/react-bootstrap.min.js"
        crossorigin></script>

    {% block js %}
        <script type="text/javascript" src="{% static 'js/navbar.min.js' %}"></script>
        <script>
            const header_props = {
                color: "info",
                variant: "dark",
                brandlabel: "myCMS: {{ user.username }}",
                brandhref: "{% url 'home' %}",
                navs: [
                {% if user.is_authenticated %}
                    {label: "Change E-mail", href: "{% url 'account_email' %}" },
                    {label: "Sign Out", href: "{% url 'account_logout' %}" },
                {% else %}
                    {label: "Sign In", href: "{% url 'account_login' %}" },
                    {label: "Sign Up", href: "{% url 'account_signup' %}" },
                {% endif %}
                ],
            };

            ReactDOM.render(
                React.createElement(MyNavbar, header_props),
                document.getElementById('header')
                );
        </script>
        {% endblock %}
    </body>
</html>
```
`{# stylesheets #}`と`{# javascript #}`の箇所でReact Bootstrapに必要なcssとスクリプトをCDNから読み込んでいる．また，`{% block css %}`でconfig/static/css/にあるroot.cssを読み込んでいる（必要に応じてオーバライドすることもできる）．`<body>`の中はheader，main，footerの3つに構造化してあり，これらのdivにReactで要素を追加していくこともできるようになっている．`{% block js %}`の箇所にヘッダー用のReactコードが記載されていることもわかる．

なお，この箇所で読み込んでいるjs/navbar.min.jsは，JSXを用いて作成した下記のReactコードをコンパイル・圧縮したものである．

```javascript
var Navbar = ReactBootstrap.Navbar;
var Nav = ReactBootstrap.Nav;

function MyNavbar (props) {
  return (
    <Navbar bg={props.color} variant={props.variant} fixed="top" expand="sm">
      <Navbar.Brand href={props.brandhref}>{props.brandlabel}</Navbar.Brand>
      <Navbar.Toggle/>
      <Navbar.Collapse>
      <Nav className="ml-auto">
        {props.navs.map((nav) => (
          <Nav.Link href={nav.href}>{nav.label}</Nav.Link>
        ))}
      </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
};
```

仮ホームページのテンプレート（config/templates/home.html）はroot.htmlを継承して次のように作成した．

```jinja
{% extends 'root.html' %}
{% load static %}
{% block js %}
    {{ block.super }}
    <script type="text/javascript" src="{% static 'js/home.min.js' %}"></script>
    <script>
        const main_props = {
            {% if user.is_authenticated %}
                greeting: "Hello, {{ user.username }}!",
                message: "This is your home page.",
                navs: [
                    {label: "Sign Out", href: "{% url 'account_logout' %}" },
                ],
            {% else %}
                greeting: "Welcome!",
                message: "Please log in or sign up to continue.",
                navs: [
                    {label: "Sign In", href: "{% url 'account_login' %}" },
                    {label: "Sign Up", href: "{% url 'account_signup' %}" },
                    {label: "Forgot password?", href: "{% url 'account_reset_password' %}" },
                ],
            {% endif %}
        };
    
        ReactDOM.render(
            React.createElement(HomeMain, main_props),
            document.getElementById('main')
        );
    </script>
{% endblock %}
```

テンプレートの中で読み込んでいるjs/home.min.jsは，JSXを用いて作成した下記のReactコードをコンパイル・圧縮したものである．

```javascript
var Container = ReactBootstrap.Container;
var Row = ReactBootstrap.Row;
var Col = ReactBootstrap.Col;
var Button = ReactBootstrap.Button;

function HomeMain(props)  {
  return (
    <Container>
      <Row>
        <Col>
          <h3>{props.title}<br/>{props.message}</h3>
          <p>
            {props.navs.map((nav) => (
              <Button href={nav.href} variant="outline-primary">{nav.label}</Button>
            ))}
          </p>
        </Col>
      </Row>
    </Container>
  );
};
```

特に説明すべきほどのものでもないが，ボタンのhrefを見るとdjango-allauthのユーザ認証機能のurlの指定方法がわかると思う．

最後にconfig/templates/account/base.htmlを見ておこう．django-allauthが利用するテンプレートはaccountディレクトリの中から探す設定になっており，そのいずれもがbase.htmlを継承したaccount/base.htmlを継承したものになっている．上記の構成では，wagtailの起点テンプレートconfig/templates/base.htmlがdjango-allauthのテンプレートツリーの起点となるbase.htmlを上書きしてしまうので，ユーザ認証関連のページはすべてこれを継承することになってしまう．

ここで追加したconfig/templates/account/base.htmlは，django-allauthのテンプレートツリーのaccount/base.htmlを上書きすることによって，ユーザ認証関連のページがすべてconfig/templates/root.htmlを継承するように変更する機能を果たす．

```jinja
{% extends 'root.html' %}
{% block main %}
    <div class="container">
        {% block body %}
            {% if messages %}
                <div>
                    <strong>Messages:</strong>
                    <ul>
                        {% for message in messages %}
                            <li>{{message}}</li>
                        {% endfor %}
                    </ul>
                </div>
            {% endif %}
            {% block content %}{% endblock %}
        {% endblock %}
        {% block extra_body %}{% endblock %}
    </div>
{% endblock %}
```

# おわりに

自分用に作成したWagtailプロジェクトの雛形について述べてきた．もしCMSが不要なら，素のDjangoでこれと同じようなプロジェクトの雛形を構成することができるだろう．基本的に，素人の手作りにすぎないものだけれど，もし何かの参考になれば幸いだ．今後も何か思いついたら随時更新していく予定なので，改善のアドバイスもいただければうれしい．