Closed6

Django アプリ実装

biwakonbubiwakonbu

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
biwakonbubiwakonbu

tailwindcss を導入する。

$ npm init # 適当に設定する
$ npm install tailwindcss postcss autoprefixer

インストールしたら下記を設定する。

tailwind.config.js
module.exports = {
    purge: [
        './backend/**/*.tmpl',
        './frontend/js/**/*.js'
    ],
    darkMode: false, // or 'media' or 'class'
    theme: {
        extend: {},
    },
    variants: {
        extend: {},
    },
    plugins: [],
}

.postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}
package.json
...
    "scripts": {
        "build:css": "npx tailwindcss-cli@latest build ./frontend/css/style.css -o ./backend/assets/css/style.css"
    },
...
frontend/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 用の設定も済ませる。

backend/config/settings.py
import os

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'assets')
]

これで collectstaticstatic/css/style.css に配置されるようになる。

$ python manage.py collectstatic
biwakonbubiwakonbu

共通系のテンプレート (templates) は manage.py と同じ階層に配置する。

ディレクトリ構造は下記で運用する。

.
├── apps
│   └── app1
├── assets
├── config
├── static
├── templates
└── manage.py

アプリケーションは数が増えていく事を想定して、apps に予め分けておく。
他の基本的な配置は django_root (manage.py の階層) に配置する。

biwakonbubiwakonbu

認証用のカスタムユーザを作成する。

$ mkdir -p apps/account
$ python manage.py startapp account ./apps/account # dir を選択して app を作成する場合は先に dir を作成する必要がある

account app を作成したら auth user を作成する。
メールアドレスでの認証をやりたい事が多い (デフォルトはユーザー名) ので、メールアドレスでの認証に変更。

backend/apps/account/models.py
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)
backend/apps/account/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User

admin.site.register(User, UserAdmin)
backend/config/settings.py
# 追記
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 のログインページを開き、メールアドレスログインが出来る事を確認する。

biwakonbubiwakonbu

ログイン機能

ログイン機能はデフォルトのものにフォームを自分で用意して使う。

$ mkdir -p apps/session
$ python manage.py startapp session ./apps/session

デフォルトの LogoutView は GET リクエストでもログアウトできてしまうので、POST のみでログアウト可能に変更しておく。
元実装を使って一部書き換えで上書きとしている。

backend/apps/session/view.py
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)
backend/config/urls.py
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')),
]
backend/apps/session/urls.py
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_FIELDemail を設定しているからっぽい (面倒なので調べてはいないけど。)。

backend/apps/session/templates/session/login.tmpl
{% 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 呼んでるし、割と雑という印象になったんだけど、そういうものか?

biwakonbubiwakonbu

Makefile を設定する

Django (だけじゃないけど) 使ってるといちいち collectstatic するのが面倒だったり、npm run ~ とか合わせて入力が面倒だったりするので、Makefile を書いちゃって作業をまとめておくのをやりがち。

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 だけして予約はしてある状態。

このスクラップは18時間前にクローズされました