Django アプリを Docker イメージに入れる

勉強がてら、Django を Docker イメージに入れてみます。

Django は最低限の部分のみで、ロケットの画面が表示できれば OK とします。

  1. Python 仮想環境に Django をインストール
  2. Django プロジェクトを作る
  3. requirements.txt を作る
  4. Dockerfile を作る
  5. .dockerignore を作る
  6. docker build でイメージ作成
  7. docker run でイメージ実行

Python 仮想環境に Django をインストール

% python3 -m venv venv
% source venv/bin/activate
(venv) % pip install django

Django プロジェクトを作る

とりあえず最低限、あのロケットの画面の表示だけする様にします。

(venv) % django-admin startproject core
(venv) % python core/manage.py runserver
(venv) % ls                                                          
core                    venv

一旦 Django はここまで。

requirements.txt を作る

(venv) % pip freeze > requirements.txt
(venv) % ls                                                          
core                    requirements.txt        venv
# requirements.txt
asgiref==3.6.0
backports.zoneinfo==0.2.1
Django==4.2.1
sqlparse==0.4.4

Django と requirements.txt を作ったところで Docker の作業へ移っていきます。

Dockerfile を作る

今回は「python:3.8.3-slim-buster」のイメージをベースにして Dockerfile を作成します。

FROM python:3.8.3-slim-buster

WORKDIR /app

COPY requirements.txt .
RUN pip3 install -r requirements.txt

COPY . .

# コンテナ外からのアクセスを可能にするため 0.0.0.0 番で runserver を実行
CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]

「COPY . .」の部分で、ローカルの作業フォルダ配下を全てコンテナの作業フォルダ(/app)にコピーします。この部分で Django の関連ファイルも全てコピーされます。

(venv) % ls                                                          
Dockerfile              core                    requirements.txt        venv

/venv 配下のみ、次の .dockerignore で除外設定をします。

.dockerignore を作る

「venv」配下をイメージに含めない様「.dockerignore」に追加します。

# .dockerignore
venv

docker build でイメージ作成

(venv) % docker build -t dockerdjango:1.0 .
(venv) % docker images
REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
dockerdjango   1.0       9c4fe787bc1d   45 seconds ago   205MB

docker run でイメージ実行

「docker run」を実行します。

(venv) % docker run --name dj_dk -p 8000:8000 dockerdjango:1.0

個人的なメモ

  • 「-p 8000:8000」と明示的にポートフォワーディングを設定しないとコンテナが孤立する。
  • たとえホスト側とコンテナ側で使うポート番号が一致していても、明示的に設定しないとホスト 8000 番へのアクセスがコンテナ 8000 番へ転送されない。

下記を実行してブラウザで「127.0.0.1:8000」へアクセスします。

とりあえずロケットの画面は表示されました。

「docker exec」でコンテナの中身を確認します。

(venv) % docker exec -it dj_dk bash
root@40ac730cceba:/app# ls
Dockerfile  core  requirements.txt

「.dockerignore」で指定した venv はきちんと除外されています。

Fetch API と Django のクロスオリジンで気をつける事

個人的に開発する中で、localhost:8080 でホストしている javascript ファイルから localhost:8000 でホストしてる Django に対し HTTP リクエストをかけたところ、CORS (Cross Origin Resource Sharing) 関連のエラーが色々出ました。

色々調べながら解決しましたが、自分なりに分かりやすく整理したかったのでこの記事にまとめました。

Javascript からの HTTP リクエスト には fetch API を使っています。

  1. CORS (Cross Origin Resource Sharing) とは
  2. Step 1: GET
  3. Step 2: request に Cookie を含める
  4. Step 3: POST や DELETE

CORS (Cross Origin Resource Sharing) とは

下記こちらからの抜粋です。

アプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンからの場合は正しい CORS ヘッダーを含んでいることが必要です。

オリジン間リソース共有の仕様は、ウェブブラウザーから情報を読み取ることを許可されているオリジンをサーバーが記述することができる、新たな HTTP ヘッダーを追加することで作用します。

Step 1: GET

まずは一番単純な GET から。Cookie も送らない想定です。

何もクロスオリジン対応をしていないと恐らく「No 'Access-Control-Allow-Origin' header is present on the requested resource.」という様なエラーが発生します。

クロスオリジンの request に対してサーバー側から Access-Control-Allow-Origin ヘッダーを適切に返す必要があります。

Fetch API での対応

Fetch API は普通に fetch() で request URL を叩けば大丈夫です。

let fetchResponse = await fetch(
    'http://localhost:8000/api/something'
)

メインはサーバー側(Django)での対応です。

Django での対応

django-cors-headers を pip install し、settings.py を下記のように追記して http://localhost:8080 からのリクエストを許可します。

pip install django-cors-headers
# settings.py

INSTALLED_APPS = [
   ...
   'corsheaders',
]

MIDDLEWARE = [
  ...
  'corsheaders.middleware.CorsMiddleware',
]

CORS_ALLOWED_ORIGINS = ['http://localhost:8080']

これでクロスオリジンの request を送れる様になります。(localhost:8080 → localhost:8000)

次に、request に cookie を含める設定を行います。

クロスオリジンでの fetch API は「デフォルトでは cookies を送信しない」仕様になっています。

これを解決するには fetch API の credentials パラメータを 'include' に設定し、サーバー側は Access-Control-Allow-Credentials ヘッダーを true で返す必要があります。

Fetch API での対応

fetch API のオプションに credentials: 'include' を含めます。

let fetchResponse = await fetch(
    'http://localhost:8000/api/something',
    {credentials: 'include'} // 追記分
)

Django での対応

settings.py に「CORS_ALLOW_CREDENTIALS = True」を追記します。

# settings.py
CORS_ALLOW_CREDENTIALS = True

これで Fetch API で Cookie が送れる様になり、Django 側もそれを受けられる様になりました。

Django でログイン済みのはずが AnonymousUser となる件

ちなみにログインユーザーを対象にした処理を Django 側で行う場合、上記の設定をしていないと、例えブラウザの cookie と Django の django_session テーブルで sessionid が一致していても 500 Internal Server Error や 403 Forbidden が発生すると思います。

理由は、クライアント側からの sessionid を request.COOKIES として受け取れていないため、request.user が非ログインユーザーを意味する AnonymousUser となるためです。

Step 3: POST や DELETE

さらに、クロスオリジンで POST や DELETE など unsafe な request を送る場合、クライアント側の fetch() に X-CSRFToken ヘッダーを設定し、Django 側では settings.py の CSRF_TRUSTED_ORIGINS のリストに request を許可するオリジンを記述する必要がります。

Fetch API での対応

let fetchResponse = await fetch(
    'http://localhost:8000/api/something',
    {credentials: 'include'},
    // ここから追記分
    method: 'POST',
    headers: {
        'X-CSRFToken': csrftoken, // Cookie から取得
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        'someKey1': someData1,
        'someKey2': someData2
    }
)

GET メソッドは fetch のデフォルト値でしたが、POST や他のメソッドを使用する場合は method を指定する必要があります。

また、headers に X-CSRFToken として cookie から取得した csrftoken をアサインし、Content-Type も指定する必要があります。

Django での対応

settings.py の CSRF_TRUSTED_ORIGINS のリストに request を許可するオリジンを記述します。

# settings.py
CSRF_TRUSTED_ORIGINS = ['http://localhost:8080']

【Django】Apple MusicKit JS で Apple Music サインインの問題発生

事象

まず、MusicKit JS でプレイヤーを設置し曲をフル再生しようとすると Apple Music へのサインインが必要となります。

その際の正常なフローとしては:

  1. 自動ポップアップでサインイン画面が表示される
  2. ユーザーID & パスワード、そして6 桁のワンタイムコードを入力してサインイン
    • Mac の Safari では指紋認証のみでサインイン可
  3. アプリケーション(オリジン毎)による Apple Music へのアクセス許可を求められる
  4. 「許可」ボタンのクリックするとポップアップ画面が閉じる
  5. アプリケーションで曲のフルバージョンが再生できるようになる

今回、上記 4 の「許可」ボタンをクリックしてもボタンがグレーアウトされるだけで何も変わらない事象が起きた。

ポップアップウィンドウは閉じず、曲も再生されない。

もう一度曲を再生しようとしても再度別のポップアップが開いてサインインを要求されるという状況でした。

ちなみにサインインされてない状態だとフルバージョンではなく 30 秒バージョンの再生になります。

問題 1:「許可」ボタンクリックから進まない

まず、上記ステップで「許可」ボタンをクリックしてもボタンがグレーアウトするだけでその先に進まなかった問題から。

原因:Referrer-Policy の設定漏れ

諸々調べた結果、原因としてボタンクリック時の HTTP request に Referer 情報が含まれていなかった事が挙げられます。

Django ではデフォルトの Referrer-Policy が same-origin となりますが、same-origin の挙動は「同一オリジンのリクエストではオリジン、パス、クエリー文字列を送信します。オリジン間リクエストでは Referer ヘッダーを送信しません。」となっています。

つまり Apple の認証システムへの request 時に Referer ヘッダーが送信されず、その後の処理が行われなかったと思われます。

Django での Referrer-Policy 変更方法

Django で Referrer-Policy を変更するには settings.py で SECURE_REFERRER_POLICY を設定する必要があります。

おそらく「SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"」で良いと思います。

残る問題

これで「許可」ボタンをクリックすると、ポップアップウィンドウの中でアプリが表示されました。

仕方なくその中で曲を再生してみようとするとまた別のポップアップウィンドウが開きサインインを求められるループに入りました。

一旦グレーアウトで止まる問題は解消されましたが、別の問題が発生しただけです。

問題 2:曲のフル再生ができない

「許可」ボタンクリック後ポップアップウィンドウが閉じない問題はさておき、曲が再生できないのはなぜなのかを調べました。

原因 1/2:musicUserToken が付与されない

デベロッパーツールで request/response の内容を確認した結果、「許可」ボタンをクリックした際の Apple 側からの response で musicUserToken を受け取っているにもかかわらず、それがこちら側の musicKitInstance に渡っていないことが判明。

正常時はブラウザの Local Storage に保存され、無事認証が完了した musicKitInstance をデベロッパーツールで覗くと下記のように musicUserToken が入っているはずですが、今回はこれが「undefined」となっていました。

ちなみにコード上で半ば無理やり下記のようにアサインすると曲の再生ができていました。(music は musicKitInstance)

music.musicUserToken = "AoSKv/b0ED6YZzVuAEIvki4eOgFgeQYrCPaU+KSFV7fFdEozGUawOuKXxrzGyISRlHPfJlOzkclA+Nk4I0SbLI/f0tiZ++a+QYOG3EP+d935PvL+udndhJjfG/xe+ctry69X/rTtqgdr2VRCbqMgt/xzocg7gg2w/QPuTcA7YSpevglys3/2AsC69ofZKl8fHKkp04dyLuhxVZOC2h4PGXc+6chmnSHIxo7tp/VTv+IWr8+fhQ=="

原因 2/2:ポップアップ起動直前の URL が「#」で終わっている

正直かなり特殊な例かもしれませんが、自分のアプリ上、曲のタブを表示するボタンに a タグを使用し href="#" としていました。

つまり、曲を再生する前にこれをクリックするとメインウィンドウの URL が「http://localhost:8000/#」という形になりますが、これが良くなかったようです。

サインイン用のポップアップが開いて「許可」ボタンをクリックした後、ポップアップウィンドウの URL が更新されますが、その際メインウィンドウの URL と musicUserToken が「#」で繋がったものが入ります。

「http://localhost:8000/#ユーザートークン」の形で「#」につづけて musicUserToken が入ってくるんですが、上記で先に「#」が一つ入るため「http://localhost:8000/##ユーザートークン」という風に「#」が重複していました。

これが原因なのではと思い「#」を一つ消してブラウザ更新すると musicKitInstance に musicUserToken がアサインされ、URL からトークンの部分が消えました。(http://localhost:8000/ になる)

解決:URLに「#」を含まない様変更

アプリのコードを変更して「#」が付かないようにしたところ、「許可」ボタンクリックで musicUserToken は付与されるようになりました。

残る問題

ただ、メインウィンドウに戻らずポップアップ上にそのままアプリが表示される状況です。

その状態で曲はフル再生できますが、ポップアップが閉じてくれないのは問題です。

正直これに関しては解決できませんでした。

Django のテンプレートをそのまま Django の外に置いて python の http.server の Web サーバーで表示したところ無事にサインインできました。

なぜ内容が同じなのに Django で render されたものだとポップアップの挙動がおかしくなるのかは謎です。

と言うわけで、結局 html/css/js の部分は Django を通さず、API 部分だけ Django で書くことにしました。

それによってクロスオリジンのエラーを解決する必要がありましたが今のところ大丈夫そうです。

その他メモ書き

  • musicUserToken はオリジン毎の割り当て。
    • 127.0.0.1:8000 で取得した musicUserToken は localhost:8000 には共有されない。
    • 同じ http://127.0.0.1:8000 であれば http.server を使った html/js でも Django の runserver を使った Django テンプレートでも同じ musicUserToken で動く。

Javascript から Django REST Framework への POST/DELETE で 415 Unsupported Media Type

問題

Django REST Framework を使って API を作成中、Javascript から POST または DELETE リクエストを送ろうとした結果「POST(リクエスト先 URL) 415 (Unsupported Media Type)」のエラー表示。

*DELETE の場合も同じエラーが出ます。

原因

リクエストヘッダーに Content-Type がなかったのが原因。

解決方法

元々↓だったヘッダー部分に…

fetch('http://example.com/exampleapi',
    {
        method: 'POST',
        headers:{
            "X-CSRFToken": getCookie('csrftoken')
        body: JSON.stringify({ 'username': 'example'})
    }
)

↓の様に Content-Type を追加したら解決しました。

fetch('http://example.com/exampleapi',
    {
        method: 'POST',
        headers:{
            "X-CSRFToken": getCookie('csrftoken'),
            'Content-Type': 'application/json'} // 追記分
        body: JSON.stringify({ 'username': 'example'})
    }
)

DELETE の場合も同じ方法で解決しました。

Javascript から Django REST Framework への POST/DELETE で 403 Forbidden

問題

Django REST Framework を使って API を作成中、Javascript から POST または DELETE リクエストを送ろうとした結果「POST(リクエスト先 URL) 403 (Forbidden)」のエラー表示。

*DELETE の場合も同じエラーが出ます。

原因

CRSF (Cross Site Request Forgery) に対するセキュリティが働いて、リクエストが拒否されている様子。

解決法

ブラウザのクッキーから CSRF Token を取って、POST リクエストのヘッダーに含めたら解決しました。

元々↓だったところに…

const fetchOptions = {
    method: 'POST'
}
fetch('http://example.com/exampleapi',fetchOptions)

クッキーから csfrtoken を抽出して headers に含めて POST リクエストをする様にしました。

function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
                const cookie = cookies[i].trim();
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

const fetchOptions = {
    method: 'POST',
    headers:{"X-CSRFToken": getCookie('csrftoken')}
}
fetch('http://example.com/exampleapi',fetchOptions)

これで一応 POST リクエストが通る様になりました。(HTTP Status 200 が返ってきました。)

DELETE の場合も同じ方法で解決しました。

参考にした記事

【Django】カスタム User モデルの作成方法

Django プロジェクトを作る際、もしデフォルトの User モデルで機能が足りている場合でも、将来の保守性や変更の必要性が出た場合を考慮して、カスタムの User モデルを使用することが強く推奨されています。

また、この作業 Django プロジェクトを作成して最初の migrate 実行前に行う必要があります。

  1. ユーザー管理用アプリケーションの作成
  2. カスタム User モデルの作成
  3. カスタム User モデルの登録
  4. マイグレーション

参考資料

ユーザー管理用アプリケーションの作成

account という名前でアプリケーションを作ります。

% python manage.py startapp account
# settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'account.apps.AccountConfig', # 追記
]

カスタム User モデルの作成

# account/models.py

from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    pass

カスタム User モデルの登録

# settings.py

AUTH_USER_MODEL = 'account.User'

尚、この設定をした時の注意点として、django.contrib.auth.models の User を直接参照すると AUTH_USER_MODEL で設定した User モデルを参照できないそうです。

なので User モデルを参照する必要がある場合は django.contrib.auth.get_user_model() を使用する必要があります。

# account/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User # models.py のカスタム User をインポート

admin.site.register(User, UserAdmin)

マイグレーション

まず、新たに作成した「account」アプリケーションのマイグレーションを作成します。

% python manage.py makemigrations account
Migrations for 'account':
  account/migrations/0001_initial.py
    - Create model User

マイグレーションを実行します。

% python manage.py migrate
Operations to perform:
  Apply all migrations: account, admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying account.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying sessions.0001_initial... OK

無事マイグレーション完了です。

【Mac】Django アプリ作成 & MySQL 初回 migrate まで

  1. Python 仮想環境の作成
  2. Django プロジェクト&アプリケーションの作成
  3. MySQL へ初回 migrate
  4. 管理ユーザーの作成

Python 仮想環境の作成

任意の場所に Python 仮想環境を作成、起動します。

% python3 -m venv justjam
% cd justjam
% source bin/activate

Django プロジェクト&アプリケーションの作成

仮想環境内で django をインストール、そしてプロジェクトとアプリケーションを作成します。

% pip install --upgrade pip
% pip install django
% django-admin startproject justjam_proj
% cd justjam_proj
% python manage.py startapp api
# settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'api.apps.ApiConfig', # 追記分
]

runserver を実行し、ブラウザで動作確認します。

% python manage.py runserver

MySQL へ初回 migrate

注)カスタムユーザーモデルを使用する場合は、以下のステップに進む前に設定を完了しておく必要があります。

settings.py の DATABASES を変更し MySQL 対応にします。

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'justjam',
    }
}

MySQL でデータベースを作成し、migrate を実行します。

% mysql
mysql> create database justjam;
mysql> exit
% pip install mysql
% python manage.py migrate

Django のテーブルが作成されていることを確認します。

% mysql
mysql> show tables
    -> ;
+----------------------------+
| Tables_in_justjam          |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
+----------------------------+
10 rows in set (0.00 sec)

mysql> exit;

管理ユーザーの作成

% python manage.py createsuperuser
Username (leave blank to use 'username'): admin
Email address: admin@justjam.com
Password: 
Password (again): 
Superuser created successfully.

取り急ぎ Django でログイン機能を作った方法(クラスベースビュー)

とりあえず Django のクラスベースビューでログイン機能を追加した時の内容をまとめます。

  1. views.py で LoginRequiredMixin の継承
  2. forms.py でログインフォームを作成
  3. views.py で ログインビューの作成
  4. urls.py でパスと view の紐付け
  5. ログイン用テンプレートの作成
  6. ログインの動きのまとめ
  7. ログアウトページの作成

views.py で LoginRequiredMixin の継承

非ログインユーザーに表示させたくない View 全てで LoginRequiredMixin を継承します。

複数のクラスを継承している場合、下記の例の様に必ず一番目に記述します。

# views.py
class ClassList(LoginRequiredMixin, generic.ListView):
    model = ClassMaster
    template_name = 'class_list.html'

class ClubList(LoginRequiredMixin, generic.ListView):
    model = ClubMaster
    template_name = 'club_list.html'

非ログインユーザーが LoginRequiredMixin を継承したビューに当たった場合、デフォルトで accounts/login/ にリダイレクトされますが、下記の様に settings.py に明示的に LOGIN_URL としてリダイレクト先を設定することも可能です。

# settings.py
LOGIN_URL = 'http://ドメイン名/login' # 一例です。

上記いずれかのパスでログインビューを表示する様、後ほど設定します。

また、リダイレクトされる際、元々アクセスしようとしていたパスを next パラメータとして URL に保持した状態で飛んでいきます。これも後ほどログイン後のリダイレクトに利用します。

forms.py でログインフォームを作成

AuthenticationForm クラスを継承してフォームを作成します。とりあえず username と password のみ認証対象とします。

# forms.py

from django.contrib.auth.models import User
from django.contrib.auth.forms import AuthenticationForm

class LoginForm(AuthenticationForm):
    class Meta:
        model = User
        fields = ['username', 'password']

views.py で ログインビューの作成

ログイン用のビューは LoginView クラスを継承して作成します。

ログインフォームの表示と、ユーザーから submit されたログイン情報の認証、そしてユーザーが元々アクセスしようとしていたページへのリダイレクト等を担ってくれます。

# views.py

from django.contrib.auth.views import LoginView

class MyLoginView(LoginView):
    form_class = LoginForm

template_name を指定しなくてもデフォルトで registration/login.html になっているので上記の例では明記していませんが、他の view 同様 「template_name = 'login.html'」の様に設定することも可能です。

また、LoginView クラスは POST 時に next URL を渡すとログイン後その URL にリダイレクトしてくれます

"If called via POST with user submitted credentials, it tries to log the user in. If login is successful, the view redirects to the URL specified in next. If next isn’t provided, it redirects to settings.LOGIN_REDIRECT_URL (which defaults to /accounts/profile/). If login isn’t successful, it redisplays the login form."

後ほどテンプレートを作成する際に form からの POST 時に next 渡す様設定します。

urls.py でパスと view の紐付け

アプリケーションレベルの urls.py にログインページのパスとログイン用 view の紐付けを記載します。

# urls.py

from django.urls import path
from . import views

app_name = 'sample_app'
urlpatterns = [
    #〜省略〜
    path('accounts/login/', views.MyLoginView.as_view(), name="login"),
    #〜省略〜
]

ログイン用テンプレートの作成

作成したログインフォームを表示するテンプレートを作ります。

Form 部分の記述は下記の様な感じ。

<!--login.html-->
 
<form action="{% url 'sample_app:login' %}" method="post">
    {% csrf_token %}
        {{ form.as_p }}
        <p><input type="hidden" name="next" value="{{next}}"></p>
        <p><input type="submit" value="ログイン"></p>
    </form>

いくつかポイントがあります。

  • ログインフォーム自身に対して POST する。
  • input の一つとして、リダイレクトされた時に持ってきた next URL を裏で取っておく(type="hidden" で非表示にしています)。

こうしておくことで、ログイン成功時に LoginView がリダイレクトしてくれます。

ログインの動きのまとめ

というわけで、ログイン部分を一通り作りました。

非ログインユーザーがアクセスした場合、下記の様な流れになります。

  1. 非ログインユーザーがページへアクセスしようとする。
  2. LoginRequiredMixin によって、アクセスしようとしていたページのパスを next に保持したままログインページ(LoginView)へリダイレクトされる。
  3. リダイレクト先でのログインフォームでユーザー名とパスワードを Submit する際、next も一緒に LoginView に対して POST する。
  4. 認証に問題がなければ、ログインされ、next のパスにリダイレクトされる。

ログアウトページの作成

一応ログアウト部分もやっつけで書いておきます。

サイトのヘッダー部分など、どこかしらにログアウト用リンクを作って、ログアウトビューとつなげる感じです。

views.py

LogoutView クラスを継承したビューを作成し、任意の template_name を指定します。

# views.py

class MyLogoutView(LogoutView):
    template_name = 'テンプレート名.html'

urls.py

ログアウト用のパスとログアウト用の view を繋げるだけです。

# urls.py

from django.urls import path
from . import views

app_name = 'sample_app'
urlpatterns = [
    #〜省略〜
    path('accounts/login/', views.MyLoginView.as_view(), name="login"),
    path('accounts/logout/', views.MyLogoutView.as_view(), name="logout"),
    #〜省略〜
]

テンプレート

テンプレートでは、任意の場所にハイパーリンクを置いてログアウト用 URL に飛ぶ様にすれば大丈夫です。

Django の ForeignKey や ManyToManyField で取った情報をテンプレートに表示したい

Django の models.py で ForeignKey や ManyToManyField で他のモデルを参照する形になるとき、テンプレートでその他モデルのデータを引っ張って表示したい事も多いと思います。

と言うかそのために他テーブルを参照する事がほとんどかと思います。

いわゆる Join した形をイメージしますが、実は Join するために models.py で特別なことをする必要はほとんどありません。テンプレート上の記述を少し変更すれば完了します。

  1. 芸人、事務所、番組の例
  2. ForeignKey(外部キー)の場合
  3. ManyToManyField の場合

1. 芸人、事務所、番組の例

芸人、事務所、番組の例で説明します。

#models.py

class JimushoMaster(models.Model): # 芸人の所属事務所
    jimusho_name = models.CharField(max_length=20)

class BangumiMaster(models.Model): # 芸人の出演番組 
    bangumi_name = models.CharField(max_length=60, null=False, unique=True)

class GeininMaster(models.Model):
    name = models.CharField(max_length=20, null=False) # 芸人の名前
    jimusho = models.ForeignKey(JimushoMaster, on_delete=models.PROTECT) # 事務所との紐付き(一対多)
    bangumi = models.ManyToManyField(BangumiMaster) # 番組との紐付き(多対多)

芸人と事務所は一体多の関係、芸人と番組は多対多の関係になります。たぶん。

2. ForeignKey(外部キー)

まずは ForeignKey のケースから。

GeininMaster を渡しているテンプレート上で、JimushoMaster の jimusho_name の値を表出したいとします。

この場合 Template 上では「object名.外部キー項目.参照先テーブルの項目」まで入れるとテンプレート上に値を表示させられます。

{{ object.jimusho.jimusho_name }}

この様な感じです。

models.py で def __str__(self): を記述した場合の注意点

ForeignKey の参照先のモデル(この例では JimushoMaster)のクラス定義で「def __str__(self):」を記述している場合、テンプレートで「object.jimusho」までを指定すると、
def __str__(self):で参照している値がそのまま表出されます。

逆に「object.jimusho.jimusho_name」まで記述しても何も表出されないので気をつけてください。

3. ManyToManyField の場合

続いて ManyToManyField で参照している値をテンプレートに表出したい場合。

これまた GeininMaster を渡しているテンプレート上で、BangumiMaster の bangumi_name の値を表出したいとします。

ListView の例になりますが、View から受け取るオブジェクトの一つ一つに対して複数の ManyRelatedManager オブジェクトが返ってくるので一つずつ表出します。

下記の様な感じです。

{% for geinin in object_list %} # 各芸人に対して
    {% for i in geinin.bangumi.all %} # 複数のオブジェクトが返ってくるので
        {{ i.bangumi_name }} # オブジェクトごとに番組名を表出
    {% endfor %}
{% endfor %}

DjangoのおすすめVPS(仮想専用サーバー)はOSから選ぶべし

*本ページはプロモーションが含まれています

唐突ですが結論から先にお伝えしておくと、自分のおすすめはConoHa VPSさくらのVPSになります。

その理由もこの記事で説明しますので是非参考にしてみてください。

共用サーバーと VPS

おすすめ VPS の前に念の為、レンタルサーバーでよく提供される「共用サーバー」と 「VPS」の違いについてお伝えします。「もうわかってるよ」という方は飛ばしてください。

共用サーバーVPS
root 権限なしあり
価格帯無料~数千円程度数百円~数千円程度
自由度×
OS×
保守サービス提供元ユーザー
構築サービス提供元ユーザー
スペック
外部影響受ける受けにくい

共用サーバー

共用サーバーは1台のサーバーを複数のユーザーで共有する「シェアハウス」の様な形態です。「レンタルサーバー」というとこちらの共用サーバーを指すことが多いです。

自由度が少ないですが最もリーズナブルで、更に様々な管理を提供者側がやってくれます。WordPress の運用など簡単にできるようになっているので初心者にも人気です。

反面、root 権限が付与されずカスタマイズ性が低いのがネックで、Django 開発もここが障壁となります。

VPS(仮想専用サーバー)

VPS はサーバーの中に自分の専用領域を持てるサービスで、OS やライブラリのインストールなど、ほとんどの事を自分で行います。

Django に限らずかなり自由に開発ができます。

自分の目的に合わせて設定を行う必要がある分、より多くの知識が必要となりますが、Django アプリを公開する上では必要なこととなります。

Django や Python なら VPS の方が楽

前述の通り WordPress やメールの運用では共用サーバーの方が簡単ですが、Django 開発となると難易度が逆転します。

共用サーバーは Django を想定していない

そもそもの話になりますが、共用サーバーは Django など Python で動くプログラミングをほとんど想定していない様に見受けられます。

使用欄に Python と書かれているのに必要なライブラリが入っていない場合が多く、追加でインストールしようにもroot 権限がないため困難で八方塞がりになることもあります。

▶︎レンタル共用サーバーで Python は茨の道!VPS が楽得でおすすめ

共用サーバーで Django を動かす意味もない

公平にするために一応触れておきますが、ネットで検索すれば共用サーバーで Django を動かす方法も一応あるにはあります。

ただ、共用サーバーでの Django サービス公開をやっとの思いで実現させたとしても、実際に業務でそのノウハウを使う機会はほぼありません。企業での開発やフリーランスとして第三者に提供するサービスでは、わざわざ共用サーバーで Django を動かす必要が無いからです。

より環境構築が簡単で、より実用的な知識が身に付く VPS が圧倒的におすすめです。

さて、一応ここまで「なぜ Django には VPS なのか」という点で書きましたが、その上で「じゃあどの VPS が良いのか」という疑問に答えていきたいと思います。

「おすすめ VPS」の前に「おすすめ OS」

実は Django 開発を行う上で、実は「どの会社の VPS か」で違いが出る部分は少ないです。

その理由ですが、VPS を契約して初めに OS をインストールしますが、その後はサーバーの OS にログインしてひたすらコマンドを打っていく作業になります。

つまり VPS のコントロールパネルを操作することはほとんどなくなるため、どの会社の VPS が使いやすいか、は関係なくなります。

パソコンの設定をする際も Windows なのか Mac なのかは大事ですが、どこの電器屋で買ったかは関係ないですよね。

ただ、VPS によって楽にインストールできる OS があるので、 まず「どの OS か」を考えてその上で「どの VPS なら OS インストールが楽か」を考える方が適切です。

Django 関連情報の多い OS がおすすめ

では「どの OS か」についてですが、基本的に Django は Python3 が使えるメジャーな OS なら動きます。ただ、本当にどの OS でもいいかというとそうは言えません。

ご存知の通り開発にはエラーが付き物なので、エラー発生時に参考になる既存情報が多い方が作業をスムーズに行うことができます

結論から言うと既存情報が多いのは Ubuntu、Debian、CentOS の 3 つ。

Google 上で Django の関連記事が多い OS を調べてみましたので見てみましょう。

Django インストール時を想定して基本的な検索クエリ「"Django" "インストール"」に Django プロジェクト作成時に必ず実行する「"startproject"」コマンドを組み合わせて検索します。

そして各 OS の検索結果が何件返ってくるかをみてみました。

OS 名検索結果数
Ubuntu16,200
Debian15,300
CentOS1,940
Fedora522
Windows Server514
その他(FreeBSD, Arch Linux, openSUSE, NetBSD 等)数件 〜 300 件程度
*数字は 2021 年 12 月時点のものです

OS によってかなり差がありますね。あくまで目安ですが、Ubuntu、Debian、CentOS あたりの情報はそれなりに多いのでおすすめです。

ただ、CentOS に関しては CentOS 8 のサポートが 2021 年 12 月に終了し、CentOS Stream への移行が推奨されています。CentOS 7 のサポート期間は 2024 年 6 月まで続きますのでこれから使うなら CentOS 7 か、新しいもので試してみたい方は CentOS Stream で試すのもありです。

Ubuntu、Debian、CentOS のインストールが簡単な VPS

Django 関連情報が多い OS が絞れたところで、「どの VPS か」に移ります。

先述したとおり VPS によって特定の OS インストールをほぼ自動でやってくれるものがあります。

自動インストールの手順は各社様々で、例えばさくらの VPS であればコントロールパネルで選択した OS を自動でインストールしてくれますし、GMO のクラウド VPS であれば契約時に選択した OS をインストールされた状態でサーバーが用意されます。

さらに ConoHa VPS に関しては Django のセットアップまである程度自動化されています。

各社の OS 自動インストールの対象は下記の通りになっています。

自動 OSConoHa VPSさくらの VPSお名前.com VPSGMO クラウド VPSKagoya Cloud VPS
Ubuntu
Debian
CentOS
その他Django テンプレ無料お試し

ではそれぞれの VPS をみていきましょう。

1. ConoHa VPS

ConoHa VPS
自動インストール対象 OSCentOS 7.1 〜 7.9
CentOS Stream 8
CentOS Stream 9
Ubuntu 18.04
Ubuntu 20.04
Debian 9.13
Debian 10.10
Debian 11.0
自動インストール方法サーバー追加時にテンプレートを選択することでサーバー作成と同時にOSのインストールやアプリケーションサーバーの構築が完了。

VPS を追加する
おすすめプラン1G(初期費用無料 + 月額 723 円〜)

解説

今回紹介する中で対応 OS の種類が最も豊富なのがこちらの ConoHa VPS。

「VPSをもっと速く、かんたんに」というキャッチコピーだけあって、OS だけでなく Django のテンプレートもあり、簡単にセットアップが可能です。「Django のインストールは自分でやりたい」という方はもちろんそれも可能です。

自動インストール対象の OS の種類も多いですし、下の記事の様に公式が親切に色々な情報を載せているのもポイントです。最も Django におすすめの VPS の一つです。

3 ステップで簡単に始められるとの事で、公式サイトに「たったの25秒で〜サーバーが作れます。」とありますので試してみてください。笑

公式サイトConoHa VPS

2. さくらの VPS

さくらのVPS
自動インストール対象 OSUbuntu 18.04
Ubuntu 20.04
CentOS 7
CentOS Stream 8
自動インストール方法VPSコントロールパネルからワンクリックで再インストールが可能。

さくらの VPS 新規追加(コントロールパネル)
おすすめプラン1G(初期費用無料 + 月額 880 円〜)

解説

「Django VPS」で検索すると最も日本語記事が出てくるのが「さくらの VPS」。

かなり気軽に使える VPS で、Django を動かすのには私自身もさくらの VPS を使っています。ポートの開閉をコントロールパネル上のクリックで行えるパケットフィルター機能が地味に楽です。

ユーザーへのサポートをかなり意識していて、公式サイトでも VPS のセットアップの手順を詳しく説明してくれているのもありがたいです。こちらも 最も Django におすすめの VPS の一つです。

ネコでもわかる!さくらのVPS講座 ~第二回「サーバーをさわってみよう!」

公式サイトさくらのVPS

3. お名前.com VPS

お名前.com VPS
自動インストール対象 OSUbuntu 18.04
Ubuntu 20.04
CentOS 7.5 ~ 7.9
CentOS Stream 8
Debian 9.13
Debian 10.10
自動インストール方法VPSコントロールパネルのサーバーセットアップで OS とバージョンを選択可能。

サーバーを初期セットアップする
おすすめプラン1G(初期費用無料 + 月額 873 円)

解説

オンラインでのお申込みから最短10分で利用開始できるというお名前.com の VPS。

KVM という仮想化方式を採用していて、ネットワークやディスクI/Oのパフォーマンスを良くする Virtio ドライバにも対応するなどハイパフォーマンスを追求している VPS

OS の初期インストールの方法は下記の公式ページに記載があります。

公式サイトお名前.com VPS

4. クラウドVPS by GMO

GMOクラウドのVPS 詳細はこちら
自動インストール対象 OSCentOS 7.9
CentOS 8.2
Ubuntu 20.04
Debian 10.0
自動インストール方法契約時に選択した OS をインストールした状態でサーバーが用意される。

新規サーバー申し込み方法
おすすめプランV1(初期費用無料 + 月額 968 円〜)
お得な情報14 日間無料お試し

解説

コンテナ技術を採用していて、一般的な仮想マシンに比べて軽量で vCPU やメモリなどの負荷が小さく、リソースを効率的に利用できる VPS。どちらかというと法人ユーザーを想定しているようですが、14 日間の無料お試し期間があるのは他と違う点です 。

契約時の OS 選択について下記の公式ページで解説が載っています。

公式サイトGMOクラウドのVPS 詳細はこちら

5. KAGOYA Cloud VPS

カゴヤ・ジャパン
自動インストール対象 OSCentOS 7
CentOS 8
CentOS Stream 8
Ubuntu 18.04
Ubuntu 20.04
自動インストール方法専用コントロールパネルからテンプレートとアプリケーションを選択して環境構築可能。

インスタンス作成
おすすめプラン1コア/1GB(初期費用無料 + 月額 〜550 円)

解説

コントロールパネルから即座にスペック変更することができます

こちらもコントロールパネルからインスタンスを作成する際に任意の OS を指定することができます。

公式サイトカゴヤ・ジャパン

個人的には ConoHa VPS かさくらの VPS が Django におすすめ

OS インストールの手軽さを基準に Django のおすすめ VPS を 5 つ紹介しましたが、いかがだったでしょうか?

個人的なおすすめは ConoHa VPSさくらの VPS です。

ConoHa VPS は申し込み時の手軽さと、さくらの VPS は既に Django に利用している人がかなりいる & 私自身も利用していて全く不満がないです。

多くのユーザーがアクセスする大規模なアプリケーションを作成するのであれば GMO のクラウド VPSお名前.com の VPS(KVM)もいいかと思いますが、個人で小さい Django アプリを作っていくような場合はやはり前述の 2 つがおすすめだと思います。