Django アプリ実装
Django でアプリ作る時にやるセットアップとかカスタマイズとか。
$ mkdir -p django-pj/backend
$ cd django-pj
$ python -m venv venv
$ . ./venv/bin/activate.fish # fish user なので
$ pip install --upgrade pip
$ pip install django
$ django-admin startproject config ./django-pj
プロジェクト開始時に設定 dir は config ディレクトリに置きたいのでそのつもりで作成。
backend dir に作成してるのは、front 類を分けておくため。
$ cd backend
$ python manage.py migrate
$ python manage.py runserver
tailwindcss を導入する。
$ npm init # 適当に設定する
$ npm install tailwindcss postcss autoprefixer
インストールしたら下記を設定する。
module.exports = {
purge: [
'./backend/**/*.tmpl',
'./frontend/js/**/*.js'
],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
...
"scripts": {
"build:css": "npx tailwindcss-cli@latest build ./frontend/css/style.css -o ./backend/assets/css/style.css"
},
...
@tailwind base;
@tailwind components;
@tailwind utilities;
これで、npm run build:css
でビルド、backend/assets/css/style.css
に配置される。
css はとりあえず django dir と分離して管理する。
また、ビルド後ファイルは assets を経由して static に移動するので、一旦 assets に置く (直で static でも良いとは思うけど)。
ついでに django で collectstatic
用の設定も済ませる。
import os
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'assets')
]
これで collectstatic
で static/css/style.css
に配置されるようになる。
$ python manage.py collectstatic
共通系のテンプレート (templates) は manage.py と同じ階層に配置する。
ディレクトリ構造は下記で運用する。
.
├── apps
│ └── app1
├── assets
├── config
├── static
├── templates
└── manage.py
アプリケーションは数が増えていく事を想定して、apps に予め分けておく。
他の基本的な配置は django_root (manage.py の階層) に配置する。
認証用のカスタムユーザを作成する。
$ mkdir -p apps/account
$ python manage.py startapp account ./apps/account # dir を選択して app を作成する場合は先に dir を作成する必要がある
account app を作成したら auth user を作成する。
メールアドレスでの認証をやりたい事が多い (デフォルトはユーザー名) ので、メールアドレスでの認証に変更。
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.core.mail import send_mail
from django.contrib.auth.base_user import BaseUserManager
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, username, email, password, **extra_fields):
"""
Create and save a user with the given username, email, and password.
"""
if not username:
raise ValueError('The given username must be set')
email = self.normalize_email(email)
username = self.model.normalize_username(username)
user = self.model(username=username, email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(username, email, password, **extra_fields)
def create_superuser(self, username, 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(username, email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
"""
An abstract base class implementing a fully featured User model with
admin-compliant permissions.
Username and password are required. Other fields are optional.
"""
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_('username'),
max_length=150,
unique=True,
help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
validators=[username_validator],
error_messages={
'unique': _("A user with that username already exists."),
},
)
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=150, blank=True)
email = models.EmailField(_('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.'
),
)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
objects = UserManager()
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def get_full_name(self):
"""
Return the first_name plus the last_name, with a space in between.
"""
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
"""Return the short name for the user."""
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
admin.site.register(User, UserAdmin)
# 追記
AUTH_USER_MODEL = 'account.User'
既に migrate を実行している場合は、database を drop & create するなり、migrate zero するなりして初期状態に戻しておく事。
makemigrations で作り直しが必要。
$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py createsuperuser
管理者を作成したら、admin site のログインページを開き、メールアドレスログインが出来る事を確認する。
ログイン機能
ログイン機能はデフォルトのものにフォームを自分で用意して使う。
$ mkdir -p apps/session
$ python manage.py startapp session ./apps/session
デフォルトの LogoutView は GET リクエストでもログアウトできてしまうので、POST のみでログアウト可能に変更しておく。
元実装を使って一部書き換えで上書きとしている。
from django.shortcuts import render
from django.http import HttpResponseNotAllowed
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.views import LoginView, LogoutView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
class Login(LoginView):
template_name = 'session/login.tmpl'
class Logout(LogoutView, LoginRequiredMixin):
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if request.method != 'POST':
return HttpResponseNotAllowed('HTTP Method Not Allowed.')
auth_logout(request)
next_page = self.get_next_page()
if next_page:
# Redirect to this page until the session has been cleared.
return HttpResponseRedirect(next_page)
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""Logout may be done via POST."""
return self.get(request, *args, **kwargs)
from django.contrib import admin
from django.conf import settings
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('session/', include('apps.session.urls')),
]
from django.urls import path
from . import views
app_name = 'session'
urlpatterns = [
path('login/', views.Login.as_view(), name='login'),
path('logout/', views.Logout.as_view(), name='logout'),
]
ログイン認証でメールアドレスを使う設定にしているが、name 属性は username のままになる (おそらく AbstractBaseUser
の都合)。
USERNAME_FIELD
で email
を設定しているからっぽい (面倒なので調べてはいないけど。)。
{% extends "base.tmpl" %}
{% block body %}
<form action="{% url 'session:login' %}" method="POST">
{% csrf_token %}
<div class="container mx-auto">
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 flex flex-col mt-32 w-1/2">
<div class="mb-4">
<label class="block text-grey-darker text-sm font-bold mb-2" for="username">
Email
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker" id="username" type="text" placeholder="sample@example.com" name="username">
</div>
<div class="mb-6">
<label class="block text-grey-darker text-sm font-bold mb-2" for="password">
Password
</label>
<input class="shadow appearance-none border border-red rounded w-full py-2 px-3 text-grey-darker mb-3" id="password" type="password" placeholder="******************" name="password">
<p class="text-red text-xs italic">Please choose a password.</p>
</div>
<div class="flex items-center justify-between">
<button class="bg-blue-500 hover:bg-blue-800 text-white font-bold py-2 px-4 rounded" type="submit">
Sign In
</button>
{% comment %}
<a class="inline-block align-baseline font-bold text-sm text-blue hover:text-blue-darker" href="#">
Forgot Password?
</a>
{% endcomment %}
</div>
</div>
</div>
</form>
{% endblock %}
LogoutView の内部実装が dispatch で無差別に auth_logout してたので割とゲンナリした。
post の中でも self.get で get 呼んでるし、割と雑という印象になったんだけど、そういうものか?
Makefile を設定する
Django (だけじゃないけど) 使ってるといちいち collectstatic
するのが面倒だったり、npm run ~
とか合わせて入力が面倒だったりするので、Makefile を書いちゃって作業をまとめておくのをやりがち。
build: build-css build-js publish
build-css:
npm run build:css
build-js:
npm run build:js
publish:
echo 'yes' | python ./backend/manage.py collectstatic
今はこれだけを書いてビルドをまわしてる。
まだ build:js の中身は無いけど、echo だけして予約はしてある状態。