DullCodes’s diary

programming,c++,python,MachineLearning,Math,Django,Competitive

Django のユーザ 小まとめ CustomUser

☆作りかけ☆

ユーザモデルを作りたい

Django にビルトインされている User モデルは、必ずしもプロジェクトが必要とする認証のモデルと合致するわけではありません。 たとえば、サイトによってはユーザー名の代わりにメールアドレスを識別トークンとして使用する方が適した場合があります。

docs.djangoproject.com

新しくプロジェクトを始める場合は、デフォルトの User で十分である場合でも、カスタムユーザーモデルを作成することを強く推奨します。

ふーん、というわけでカスタムユーザモデルの作り方をまとめる

ユーザモデルの種類

  • AbstractBaseUserを継承
  • AbstractUserを継承
  • OneToOneFieldを既存のユーザモデルに追加

の3種類があるらしい
OneToOneFieldはなんだかよくわからなかったので前2つを調べていく
カスタムユーザモデルの作成前に行っておくべきことがいくつかあるのでまとめる

カスタムユーザ作成フロー

ユーザにカスタムモデルを使うときに必ずやらなければいけないことがある

データベースにUserのテーブルが生成される前に、つまりマイグレーションを行う前に
カスタムユーザの作成を終えておく必要がある
そうしないとマイグレーション時に変なエラーが大量に出るらしい

例として以下の適当なアプリを作ったとする

# django-admin startproject config . で作ったやつ

$ tree

hogehoge
├── config 
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

このプロジェクトにカスタムユーザモデルを追加していく

1. ユーザ用のアプリを作成する

github を見てみるとカスタムモデルのアプリ名は
accounts, User, users, custom_user という名前が一般的っぽい

あとは Email と password で認証する系のアプリを作りたい場合は
email_user という名前を付けているのを見つけた

基本的には必ずカスタムユーザモデルを作ることになるので
パット見でカスタムユーザなんだなって分かるヤツの方が適していると思う

まぁ今回はどうでもいいので accounts にする

├── accounts # 作成 もちろんどんなアプリ名でもOK
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── readme.md
│   ├── tests.py
│   └── views.py
├── config
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

2. ユーザ用のアプリの models.py にカスタムユーザを作る

これは後述

例としてAbstractUserを使うときには

from django.contrib.auth.models import AbstractUser

# こんな感じで使う
# クラス名は勿論どんなものでもいいけど見てわかりやすいほうがいい
# CustomUser や User が一般的っぽい
class CustomUser(AbstractUser):
    pass

3. settings.py の AUTH_USER_MODEL に 作成したカスタムユーザを登録する

これ忘れそうだ
設定し忘れるとわけのわからんエラーが大量に出るので要注意
場所はどこでも良いらしい

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

...(中略) ...

# これがカスタムユーザモデルを作る際に必要不可欠な設定
# アプリ名.モデル名 で設定する
# ここで AUTH_USER_MODEL を typo していると
# カスタムユーザが認識されずに変なエラーが大量に出るので慎重に !
AUTH_USER_MODEL = "accounts.CustomUser"

4. settings.py の INSTALLED_APPS に 作成したアプリを登録

INSTALLED_APPS = [
    'django.contrib.admin',
    ...

    'accounts.apps.AccountsConfig'
]

そりゃそうですよね
これも中々忘れそうなので要注意
以上が作成フロー 多そうに見えて結構簡単

  1. ユーザ用のアプリを作成する
  2. ユーザ用のアプリの models.py にカスタムユーザを作る
  3. settings.py の AUTH_USER_MODEL に 作成したカスタムユーザを登録する
  4. settings.py の INSTALLED_APPS に 作成したアプリを登録

カスタムユーザの作成

AbstractUser, AbstractBaseUser の2種類を作ってみる

AbstractUser は元のUserモデルをちょっと手を加える時に使う
AbstractBaseUser は元のUserモデルからだいぶ変更をする時に使う

AbstractUser

最も簡単な指定の仕方がこれ

from django.contrib.auth.models import AbstractUser


class CustomUser(AbstractUser):
    pass

CustomUser にはなんの属性も設定していないけど
これでカスタムユーザが使えるようになる



継承元の AbstractUser について
元のコードをざっと見てみる

"""
このコードは django の django/contrib/auth/models.py をそのままもってきてる
なんと AbstractUser は AbstractBaseUser を継承していることが判明
"""
class AbstractUser(AbstractBaseUser, PermissionsMixin):
    # Username に Unicode characters を許可する
    # @ . + - _ の5種類の文字(記号)
    username_validator = UnicodeUsernameValidator()

    # そのまま username
    username = models.CharField(
        _('username'),
        max_length=150,
        unique=True,
        help_text=_('Required. 150 chars 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=150, blank=True)
    # 名
    last_name = models.CharField(_('last name'), max_length=150, blank=True)
    # メールアドレス
    email = models.EmailField(_('email address'), blank=True)
    # is_staff が True であれば admin ページにアクセスできる権限をもつ
    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)

    # UserManager というのはUserモデルに色々なヘルパーメソッドを提供するクラスらしい
    # 例えば create_user, create_superuser とか
    # User モデルにはこういったメソッドは付けずにヘルパーとして Managerのちからをかりて
    # これらの機能を実行するということ
    # AbstractBaseUser を使う際には UserManager も独自で実装するのが普通らしい
    # ただ UserManager もやっていることは結構単純なのであとで説明できたらする
    objects = UserManager()

    # email を設定しているフィールド名を指定する
    # email のフィールド名を my_super_email_with_sendgrid とする場合は
    # EMAIL_FIELD = 'my_super_email_with_sendgrid'
    # とするって感じ
    EMAIL_FIELD = 'email'

    # どのフィールドを一意な値として = unique なフィールドとして扱うかってのを決定するフィールド
    # なので USERNAME_FIELD って書いてあるけど username とは何ら関係はない
    # よって USERNAME_FIELD  = 'email' とかにすると
    # email が一意でなければいけないものとして扱われる
    # USERNAME_FIELD が email って意味が分かりづらすぎる
    USERNAME_FIELD = 'username'

    # create_superuser を実行してアカウントを作成するときに
    # 要求してほしいフィールドをここで指定するらしい
    REQUIRED_FIELDS = ['email']

    # モデルのときには Meta クラス
    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        # この値が True だと makemigrations 時にテーブルが作成されないという属性
        # 継承したときにこいつが発動して勝手にuserテーブルできちゃうと困るので
        # 普段は True になっている
        # よって自分でCustomUserを使うときには必ずFalseを入れる or 削除しないといけない
        abstract = True

    # これより下は見たら分かるはず
    # わからないなら使わない, 別に使わなくていいし
    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)

ところどころ不可解なフィールド名があったけど、全体的にはすっきりして見やすい
こんな感じのAbstractUser を使えるようになる
もちろん各フィールドをオーバーライドしたり、新しいフィールドを付け加えたりして
新しいCustomUserを作ることができる

from django.contrib.auth.models import AbstractUser
# 他の import は省略


class CustomUser(AbstractUser):
    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        'ゆーざめいだよ',
        max_length=50,
        unique=True,
        help_text='50もじいないだよ',
        validators=[username_validator],
        error_messages={
            'unique': "もういるだよ",
        },
    )
    # 新しい フィールド
    nickname = models.CharField(
        'NickName',
        max_length=30,
    )
    # 名前の変更
    send_grid_email = models.EmailField('use sendgrid', blank=True)
    is_staff = models.BooleanField(
        'staff status',
        default=False,
        help_text='adminにはいれるだよ',
    )

    is_active = models.BooleanField(
        'いきてる',
        default=True,
    )
    date_joined = models.DateTimeField('アカウント作成日時', default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = 'send_grid_email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['send_grid_email']

    class Meta:
        verbose_name = 'user'
        verbose_name_plural = 'users'
        # abstract = True は必ず削除すること !

    def clean(self):
        super().clean()
        self.send_grid_email = self.__class__.objects.normalize_email(self.send_grid_email)

面倒だからこれくらいにするけど
こういう感じで修正や追加を行うことができます
例えば username の代わりに email を使いたいときには username を消して emailに変えればいいだけ
AbstractBaseUser使う必要あるのかな こっちだけで十分いける気がするけども

UserManager

"""
UserManager をカスタムするときに最低限必要なことは以下の3つ

1. _create_user
2. create_user
3. create_superuser
"""
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):
        # admin の権限はないよ
        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=None, password=None, **extra_fields):
        # admin に入れるよ
        extra_fields.setdefault('is_staff', True)
        # 全権限もってるよ
        extra_fields.setdefault('is_superuser', True)

        # superuser は権限をもってないといけないので 簡単に Model Validation するよ
        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)

    # この with_perm ってやつはなにやってんだかよくわからないので無視
    def with_perm(self, perm, is_active=True, include_superusers=True, backend=None, obj=None):
        ...

これだけ
これをそのままぱくってちょっと修正すればいいだけなのでとても簡単

use_in_migratoins

docs.djangoproject.com

オプションとして、マネージャをマイグレーションシリアライズして、RunPython の中で使えるようにすることができます。


docs.djangoproject.com

マイグレーション履歴を反映して独自の Python コードを実行します。

???

というわけで user_in_migrations はCustomUserManagerを作る時には必須のフィールドとして定義しておきましょう

AbstractBaseUser

流石に長すぎるのでコードを折りたたむ

BaseUserManager

UserManager の元

Django のフォーム 小まとめ validation 編

Django のフォームには validation という役割がある
validation とは 簡単に言うとForm に入力されたデータが
こちらで指定した条件を満たしているかどうかを検証すること
正式な訳は妥当性検証というらしい

よくわからないのでForm の validation をざっとまとめる
Django公式に全部書いてあるはず

docs.djangoproject.com

準備

project

適当に django-admin でアプリを作る
アプリ名は validatorにする
何かのメソッドにかぶってないか不安


FormValidation/
├── config
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── templates
│   └── validator
│       └── index.html
└── validator
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── forms.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    ├── urls.py
    └── views.py

models.py

簡単なモデルを作成

from django.db import models


class Product(models.Model):
    class Meta:
        db_table = 'product'

    name = models.CharField(max_length=50)
    price = models.IntegerField()
    quantity = models.IntegerField()
    comment = models.TextField()

    def __str__(self):
        return self.name

templates

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FormValidation Training</title>
</head>

<body>
  {{ form }}
</body>

</html>

views.py

from django.shortcuts import render, redirect
from django.views import View

from validator.models import Product
from validator.forms import ProductForm


class IndexView(View):
    def get(self, request, *args, **kwargs):
        ctx = {
            'form': ProductForm()
        }
        return render(request, 'validator/index.html', ctx)

    def post(self, request, *args, **kwargs):
        form = ProductForm(request.POST)
        if not form.is_valid():
            return render(request, 'validator/index.html', {'form': form})

        # ここでモデルに保存したりする
        return redirect('validator:index')

urls.py, settings.py はよしなに

Form の作成

一番簡単な forms.ModelFormを使う

from django import forms
from validator.models import Product


class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-class'

validation の基礎

Form での validation の流れは以下の通り

  1. view で form.is_valid() が実行され
  2. form で clean_<field_name>() が実行され
  3. form で clean() が実行される

という感じ

validation 全体では Model クラスや、モデルに定義されている各フィールド内
で行われるものがあるので、実際はもっと複雑だけど
今回はForm が行うvalidation 限定でいく

name = models.CharField(max_length=100, unique=True) とかこんなの
※ Field Validation と呼ばれている

詳しく

# 何かのView があり
class IndexView(View):
    def post(self, request, *args, **kwargs):
        # form から 何かが POST され
        form = ProductForm(request.POST)

        # form.is_valid() が実行されたらvalidation 開始!
        if not form.is_valid():
            return render(request, 'validator/index.html', {'form': form})

        ...  (何かの処理が続く) ...

View で form.is_valid() が実行されると

from django import forms
from validator.models import Product


class ProductForm(forms.ModelForm):
    """
    name = models.CharField(max_length=50)
    price = models.IntegerField()
    quantity = models.IntegerField()
    comment = models.TextField(blank=True)
    """
    class Meta:
        model = Product
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-class'

    # こいつらが次々と実行され
    def clean_name(self):
        ...

    def clean_price(self):
        ...

    def clean_quantity(self):
        ...

    def clean_comment(self):
        ...

    # 最後にこいつが動く
    def clean(self):
        ...

clean_

使い方は簡単

from django import forms
from validator.models import Product


class ProductForm(forms.ModelForm):
    ...

    def clean_name(self):
        value = self.cleaned_data['name']
        if len(value) < 10:
            raise forms.ValidationError('10文字以上で入力してください')
        return value

まずは cleaned_data から必要なデータをもらってくる
cleaned_data は form に設定したモデルに応じてデータをその型に適したものに変換してくれる
DateField だったら 文字列を datetime.date 型にしてくれたり
Email だったら、明らかにmailとして不適切な文字列を消してくれたり

公式にとってもわかりやすく書いてあら

docs.djangoproject.com

で、value = self.cleaned_data['name']としてデータを取得できたら
あとは iftry except などで、validation したい条件を書いていく
引っかかったら、raise forms.ValidationError('error message') とする
エラーが無かったら 普通に return して終了

特に説明する箇所がない
実際にフォームを作成して使ってみる

from django import forms
from validator.models import Product


class ProductForm(forms.ModelForm):

    class Meta:
        model = Product
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-class'

    def clean_name(self):
        value = self.cleaned_data['name']
        if len(value) < 10:
            raise forms.ValidationError('10文字以上で入力してください')
        return value

本番

普通に form を出すとこうなる

<tr>
  <th><label for="id_name">Name:</label></th>
  <td><input type="text" name="name" maxlength="50" class="form-class" required id="id_name"></td>
</tr>

で、このnameに10文字以内のデータを入力してvalidationしてみると

<tr>
  <th><label for="id_name">Name:</label></th>
  <td>
    <ul class="errorlist">
      <li>10文字以上で入力してください</li>
    </ul><input type="text" name="name" value="over10" maxlength="50" class="form-class" required id="id_name">
  </td>
</tr>

class="errorlist" というクラスが新たに追加されており
中に入っている文字列は raise forms>ValidationError( ... ) で入力したものそのまんま

このエラーリストの中身は templates でこういう感じで出力する

<body>
  <div class="">
    <a href="{% url 'validator:index' %}">IndexView</a>
  </div>

  <form action="{% url 'validator:index' %}" method="POST">
    {% csrf_token %}

    {% for field in form %}

    {% if field.errors %}
    <div class="errorlist">
      {% for error in field.errors %}
      {{ error }}
      {% endfor %}
    </div>
    {% endif %}

    <div class="form-class">
      {{ field.label }}{{ field }}
    </div>
    {% endfor %}

    <input type="submit" value="submit" class="btn">
  </form>
</body>

errorlist を個別に表示

実際の表示はこんな感じ

f:id:DullCodes:20200531004533p:plain
django form error list

全部の validation を付けるとこんな感じ

from django import forms
from validator.models import Product


class ProductForm(forms.ModelForm):

    class Meta:
        model = Product
        fields = '__all__'
        # fields = 'name',

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-class'

    def clean_name(self):
        value = self.cleaned_data['name']
        if len(value) < 10:
            raise forms.ValidationError('10文字以上で入力してください')
        return value

    def clean_price(self):
        value = self.cleaned_data['price']
        if value <= 0:
            raise forms.ValidationError('0円以下の値段など無い')
        return value

    def clean_quantity(self):
        value = self.cleaned_data['quantity']
        if value < 0:
            raise forms.ValidationError('商品に0個は無い')
        return value

    def clean_comment(self):
        value = self.cleaned_data['comment']
        if len(value) <= 0:
            raise forms.ValidationError('必ずコメントは無いとダメ')
        return value

表示はこんな感じ

f:id:DullCodes:20200531005450p:plain
django form all error lists

errorlist を一緒に表示

<body>
    {% if form.errors %}
    <div class="errorlist">
      {% for errors in form.errors.values %}
      {% for error in errors %}
      <div class="error-field">
        {{ error }}
      </div>
      {% endfor %}
      {% endfor %}
    </div>
    {% endif %}

    <form action="{% url 'validator:index' %}" method="POST">
      {% csrf_token %}

      {% for field in form %}
      <div class="form-class">
        {{ field.label }}{{ field }}
      </div>
      {% endfor %}

      <input type="submit" value="submit" class="btn">
    </form>
</body>

表示はこんな感じ

f:id:DullCodes:20200531011935p:plain
django form list all at same place

clean

各clean_ で validation できたやつ同士の整合性を取るためのもの
たとえば Confirm Password などのマッチングなどを行う

from django import forms
from accounts.models import User


class RegisterForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('username', 'email', 'password', )

    confirm_password = forms.CharField(
        label='confirm password',
        required=True,
        strip=False,
    )

    def clean_password(self):
        value = self.cleaned_data['password']
        # 長さのバリデーションだとか、使える文字の記号が云々だとか
        if value ... 
            raise forms.ValidationError( ... )
        return value
    
    def clean(self):
        password = self.cleaned_data['password']
        confirm = self.cleaned_data['confirm_password']
        if password != confirm:
            raise forms.ValidationError('not matching')
        super().clean()

という感じで使うのがclean の役割
これ以外にまともな例が思いつかない

form.non_field_errors

clean(self)raise をすると特殊なerrorlistにはいる
それが non_field_errors という属性

    ...

    def clean(self):
        raise forms.ValidationError('クリーンでのバリデーションエラー')


<div class="container">
  <div class="non-field">
    {{ form.non_field_errors }}
  </div>

  {% if form.errors %}
  <div class="errorlist">
    {% for errors in form.errors.values %}
    {% for error in errors %}
    <div class="error-field">
      {{ error }}
    </div>
    {% endfor %}
    {% endfor %}
  </div>
  {% endif %}

  <form action="{% url 'validator:index' %}" method="POST">
    {% csrf_token %}

    {% for field in form %}
    <div class="form-class">
      {{ field.label }}{{ field }}
    </div>
    {% endfor %}

    <input type="submit" value="submit" class="btn">
  </form>
</div>

実際の表示

f:id:DullCodes:20200531013648p:plain
django form list non_field_errors

validation まとめ

  • forms.py

    • def clean_
    • def clean
  • template

    • form.non_field_errors : 全体エラー
    • form.errors : 個別エラー
      • form.errors.values

というわけでform のvalidationもだいたいOKになったはず

Django のフォーム 小まとめ

Form の機能をざっとまとめる

本家 これから書くことが全て書いてある

docs.djangoproject.com

準備

適当にプロジェクトを作成

$ tree
.
├── FormTraining
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── static
│   └── css
│       └── base.css
├── templates
│   ├── base.html
│   └── train1
│       └── index.html
└── train1
    ├── apps.py
    ├── forms.py
    ├── models.py
    ├── urls.py
    └── views.py

で、こういうモデルがあるだけのプロジェクト

from django.db import models


class Product(models.Model):
    class Meta:
        db_table = 'product'

    name = models.CharField(max_length=50)
    price = models.IntegerField()
    quantity = models.IntegerField()
    comment = models.TextField()

    def __str__(self):
        return self.name

    def is_in_stock(self):
        return self.quantity > 0

templates , settings.py を適当に指定する
で、forms.pyを作ってフォームを定義していく

from django import forms


class ProductForm(forms.Form):
    name = forms.CharField(
        label='name',
        max_length=100
    )

    price = forms.IntegerField(
        label='price'
    )

    quantity = forms.IntegerField(
        label='quantity'
    )

    comment = forms.CharField(
        label='comment',
        widget=forms.Textarea
    )

urls.pyを適当に設定して 以下のようなhtmlを表示されるようにする
views.pyでForm インスタンスが何をもっているのかを表示してみる

class FormView(View):
    def get(self, request, *args, **kwargs):
        ctx = {
            'form': ProductForm()
        }
        print(ctx['form'])
        return render(request, 'train1/index.html', ctx)

すると以下のようにレンダリングされる

<tr>
  <th><label for="id_name">name:</label></th>
  <td><input type="text" name="name" maxlength="100" required id="id_name"></td>
</tr>
<tr>
  <th><label for="id_price">price:</label></th>
  <td><input type="number" name="price" required id="id_price"></td>
</tr>
<tr>
  <th><label for="id_quantity">quantity:</label></th>
  <td><input type="number" name="quantity" required id="id_quantity"></td>
</tr>
<tr>
  <th><label for="id_comment">comment:</label></th>
  <td><textarea name="comment" cols="40" rows="10" required id="id_comment">
</textarea></td>
</tr>

実際にHTMLに出力してみると

{% extends 'base.html' %}

{% block main %}
<h1>Train1</h1>

<div class="sep">
  <h2>form</h2>
  {{form }}
</div>
{% endblock main %}

f:id:DullCodes:20200514115152p:plain
django train1 forms.Form

おおー
というわけでフォームをHTMLでぺたぺた書いていくのではなくて
MTVの精神から画面に表示するテンプレートと、そのもととなるFormを分離することで
関心の分離がなんとか、機能性が云々とか色々といいことがあるんじゃないすかね

というわけでフォームの作り方まとめ

forms.py

ここから本題

form の作り方は大きく分けて2つ

forms.Form は手作業でフィールドを定義する必要がある

それともう一つは、フォームのフィールドの定義とフォームを作りたいモデルが
だいたい同じときにはforms.ModelFormというものが使える

forms.Forms

上に書いたとおり
必要なフィールドを一つ一つ定義していく
見た目通りとてもわかり易い

from django import forms


class ProductForm(forms.Form):
    name = forms.CharField(
        label='name',
        max_length=100
    )

    price = forms.IntegerField(
        label='price'
    )

    quantity = forms.IntegerField(
        label='quantity'
    )

    comment = forms.CharField(
        label='comment',
        widget=forms.Textarea
    )

定義できる項目もなかなかに種類が豊富

Form fields | Django ドキュメント | Django

- ビルトインの Field クラス
BooleanField, CharField, ChoiceField, TypedChoiceField,
DateField, DateTimeField, DecimalField, DurationField, EmailField,
FileField, FilePathField, FloatField, ImageField, IntegerField,
GenericIPAddressField, MultipleChoiceField, TypedMultipleChoiceField,
NullBooleanField, RegexField, SlugField, TimeField, URLField, UUIDField

この他にもあんまり使わないフィールドや
そもそも自作の custom field も作れるから、つまり何でも作れる

そして、それらのフィールドの中には
ラベルだったり、help_text だったりと色々属性が設定できる

Form fields | Django ドキュメント | Django

- field のコアとなる引数

required
label
label_suffix
initial
widget
help_text
error_messages
validators
localize
disabled

この他に、各フィールドが特別にもっている引数があるみたいなのでもうちょい多い
よっぽどマニアックなことしない限り、custom field は必要なさそう

不必要に引数を付けまくったFormがこんな感じ

from django import forms
from .models import Product


class ProductForm(forms.Form):
    name = forms.CharField(
        label='name',
        help_text='これはname',
        max_length=100,
        required=True,
        initial='BOOOOOOOOOOOOM',
        error_messages={'required': '入力してないぞ'}
    )

    price = forms.IntegerField(
        label='price',
        help_text='これは値段',
        min_value=0,
        max_value=100000
    )

    quantity = forms.IntegerField(
        label='quantity',
        help_text='これは品数',
        min_value=1,
        max_value=100
    )

    comment = forms.CharField(
        label='comment',
        help_text='This is a comment',
        widget=forms.Textarea
    )

出力はこんな感じ

<tr>
  <th><label for="id_name">name:</label></th>
  <td><input type="text" name="name" value="BOOOOOOOOOOOOM" maxlength="100" required id="id_name"><br><span class="helptext">これはname</span></td>
</tr>
<tr>
  <th><label for="id_price">price:</label></th>
  <td><input type="number" name="price" min="0" max="100000" required id="id_price"><br><span class="helptext">これは値段</span></td>
</tr>
<tr>
  <th><label for="id_quantity">quantity:</label></th>
  <td><input type="number" name="quantity" min="1" max="100" required id="id_quantity"><br><span class="helptext">これは品数</span></td>
</tr>
<tr>
  <th><label for="id_comment">comment:</label></th>
  <td><textarea name="comment" cols="40" rows="10" required id="id_comment">
</tr>

で、見た目はこんな感じ

f:id:DullCodes:20200514122935p:plain
django train1 forms.Form with useless args

widget

ここで注目スべきはこれ

class ProductForm(forms.Form):
    comment = forms.CharField(
        label='comment',
        help_text='This is a comment',
        widget=forms.Textarea
    )

CharFieldのフィールドを定義しており、その引数にwidgetというものがあり
Textareaを設定しており、結果としてHTMLにはtextarea が表示されている

docs.djangoproject.com

このwidgetでより細かいフォームの属性が設定できる

from django import forms
from .models import Product


class ProductForm(forms.Form):
    comment = forms.CharField(
        label='comment',
        help_text='This is a comment',
        widget=forms.Textarea(
            attrs={
                'rows': 10,
                'cols': 20,
                'title': 'Pls leave your comment',
                'required': True,
                'something': 'It works!'
            }
        )
    )

中身は

<tr>
  <th><label for="id_comment">comment:</label></th>
  <td><textarea name="comment" cols="20" rows="10" title="Pls leave your comment" required something="It works!" id="id_comment">
</textarea><br><span class="helptext">This is a comment</span></td>
</tr>

素晴らしい というわけで細かな設定をするときにはこのwidgetという
引数にごちゃごちゃ書いていくことになる

ちなみにwidgetで設定したフィールド(ここではforms.Textarea)に
フィールドごとそのまま上書きされる
本当にそのまま上書きされちゃうので、例えばこんなことが可能になる

from django import forms
from .models import Product


class ProductForm(forms.Form):
    # CharField を
    comment = forms.CharField(
        label='comment',
        help_text='This is a comment',
        # checkbox に上書き
        widget=forms.CheckboxInput
    )

レンダリングした後を見てみると

<tr>
  <th><label for="id_comment">comment:</label></th>
  <td><input type="checkbox" name="comment" required id="id_comment"><br><span class="helptext">This is a comment</span></td>
</tr>

はい、チェックボックス
CharFieldは跡形もなく消え去ってしまってる
この使い方に意味はないけど勘違いしてたり、間違った上書きで
想定外のHTMLが出力されることがあるので注意

forms.ModelForm

forms.ModelFormを継承することによって
Metaクラスに指定したモデルを自動的にフォームにしてくれるという機能をもつ
以下のようなクラスがあるとする

from django.db import models


class Product(models.Model):
    class Meta:
        db_table = 'product'

    name = models.CharField(max_length=50)
    price = models.IntegerField()
    quantity = models.IntegerField()
    comment = models.TextField()

    def __str__(self):
        return self.name

    def is_in_stock(self):
        return self.quantity > 0

ModelFormを使う
Metaクラスの中でなんのモデルを使うのか
その中でなんのフィールドを使うのかを指定する

from django import forms
from .models import Product


class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ('name', 'price', 'quantity', 'comment')

全部使いたい時は

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = '__all__'

とだけ書けば良い

とすると、HTMLは

<tr>
  <th><label for="id_name">Name:</label></th>
  <td><input type="text" name="name" maxlength="50" required id="id_name"></td>
</tr>
<tr>
  <th><label for="id_price">Price:</label></th>
  <td><input type="number" name="price" required id="id_price"></td>
</tr>
<tr>
  <th><label for="id_quantity">Quantity:</label></th>
  <td><input type="number" name="quantity" required id="id_quantity"></td>
</tr>
<tr>
  <th><label for="id_comment">Comment:</label></th>
  <td><textarea name="comment" cols="40" rows="10" required id="id_comment">
</textarea></td>
</tr>

できてる! というわけで必要なフォームがモデルに全部設定されていれば
これだけで簡単にフォームが作成できる

しかも、ModelFormでも widget での変更が可能
Metaクラスにwidgetを指定すればよいだけ

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = 'comment',

        widgets = {
            'comment': forms.Textarea(
                attrs={
                    'rows': 10,
                    'cols': 20,
                    'title': 'Pls leave your comment',
                    'required': True,
                    'something': 'It works!'
                }
            )
        }

できたHTMLは

<tr>
  <th><label for="id_comment">Comment:</label></th>
  <td><textarea name="comment" cols="20" rows="10" title="Pls leave your comment" required something="It works!" id="id_comment">
</textarea></td>
</tr>

はい簡単
というわけで、forms.Form と forms.ModelFormで全く同じフォームが作成
出来るようになりました

使い分け

モデルとフォームが同じような感じだったらModelForm
そうじゃなければforms.Form で良いはず
これ以外の方法は知らない見てない

init 内部での変更

おまけとして widget の指定の方法3つを見ていく
フォームの作り方とは直接関係ない

widget の指定の仕方は以下の2つがあった

# forms.Form
class ProductForm(forms.Form):
    comment = forms.CharField(
        label='comment',
        help_text='This is a comment',
        widget=forms.Textarea
    )

これと

# forms.ModelForm
class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = 'comment',

        widgets = {
            'comment': forms.Textarea(
                attrs={
                    'rows': 10,
                    'cols': 20,
                    'title': 'Pls leave your comment',
                    'required': True,
                    'something': 'It works!'
                }
            )
        }

2つの違いはそもそものFormクラスの作り方の違いにあったのですが
widgetの指定がもう一つあり、それが init での変更

from django import forms
from .models import Product


class ProductForm(forms.Form):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # name
        self.fields['name'].required = True
        self.fields['name'].label = 'THIS IS A LABEL FOR NAME'
        self.fields['name'].widget.attrs = {
            'placeholder': 'YOU MUST BE THERE',
            'class': 'ui form form-class tektiouni'
        }

        # price
        self.fields['price'].widget = forms.HiddenInput()

        # quantity
        self.fields['quantity'].label = '数量よ'

        # comment
        self.fields['comment'].widget = forms.Textarea()
        self.fields['comment'].widget.attrs = {
            'rows': 10,
            'cols': 20,
            'required': True,
            'something': 'It Also Works!',
            'message': 'Django is fun'
        }

    name = forms.CharField()
    price = forms.IntegerField()
    quantity = forms.IntegerField()
    comment = forms.CharField()

という具合に設定をしていくやり方 なんなら一番わかりやすくない?
self.fieldsってところに設定したfieldが全部入っているので、例えば

for field in self.fields.value():
    field.widget.attrs["class"] = "ui form form-class"

みたいな感じで全部のフォームに表示したいクラスをこうやって適用できる
べんりだねー

しかもModelFormでも使える方法なのでなかなか便利
最後に中身をみて終了

<tr>
  <th><label for="id_name">THIS IS A LABEL FOR NAME:</label></th>
  <td><input type="text" name="name" placeholder="YOU MUST BE THERE" class="ui form form-class tektiouni" required id="id_name"></td>
</tr>
<tr>
  <th><label for="id_quantity">数量よ:</label></th>
  <td><input type="number" name="quantity" required id="id_quantity"></td>
</tr>
<tr>
  <th><label for="id_comment">Comment:</label></th>
  <td><textarea name="comment" rows="10" cols="20" required something="It Also Works!" message="Django is fun" id="id_comment">
</textarea><input type="hidden" name="price" id="id_price"></td>
</tr>

f:id:DullCodes:20200514131907p:plain
django form self.fields forms

css は設定してないので見た目はアレですけど

まとめ

forms.Form

from django import forms
from .models import Product


class ProductForm(forms.Form):
    name = forms.CharField(
        label='name',
        help_text='これはname',
        max_length=100,
        required=True,
        initial='BOOOOOOOOOOOOM',
        error_messages={'required': '入力してないぞ'}
    )

    price = forms.IntegerField(
        label='price',
        help_text='これは値段',
        min_value=0,
        max_value=100000
    )

    quantity = forms.IntegerField(
        label='quantity',
        help_text='これは品数',
        min_value=1,
        max_value=100
    )

    comment = forms.CharField(
        label='comment',
        help_text='This is a comment',
        widget=forms.Textarea
    )

ModelForm

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        # fields = '__all__'
        # fields = ('name', 'email', 'nickname')
        fields = 'comment',

        widgets = {
            'comment': forms.Textarea(
                attrs={
                    'rows': 10,
                    'cols': 20,
                    'title': 'Pls leave your comment',
                    'required': True,
                    'something': 'It works!'
                }
            )
        }

self widget

from django import forms
from .models import Product


class ProductForm(forms.Form):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.fields['comment'].widget = forms.Textarea()
        self.fields['comment'].widget.attrs = {
            'rows': 10,
            'cols': 20,
            'required': True,
            'something': 'It Also Works!',
            'message': 'Django is fun'
        }

    comment = forms.CharField()

一言

これだけ分かればもうフォームはどうにかできる

はず

コンゴ 2020年5月

まとめ方もわからない

Python

Django

  • テストの方法をまとめる
    • urls, views, models, forms
    • Selenium で機能テスト

pytest, unittest, django.test のどれか
pytest が楽でいいからそれ使う どれも大して変わらない
mixer でfixture を使う方法もまとめる 多分簡単
Test Driven Development ってやつをやってみる

  • logger

    • 全く使ったこと無いから使ってみる
  • アプリを作る

    • たくさん作る snippet検索するやつとか色々
  • Heroku

    • 使い方わからんので使う
  • python_anywhere

    • 引き続き作成

Rest API

もやる

FrontEnd

  • React

    • 楽しい
    • Component の import , props を入れる方法まではいった
  • HTML and CSS

    • Flexbox, Grid layout, Sass, などなど

  • Docker, FIrebase
    • なにがなんだかわからない
    • Dockerはすぐにでもやらないとダメな技術っぽいから優先して

TypeScript 超初め

TypeScript

  • 公式

www.typescriptlang.org

  • わかりやすい

www.typescriptlang.org

  • TypeScript DeepDive 日本語版
    • 素人向きじゃないけど、多分最高にわかりやすい

typescript-jp.gitbook.io

環境構築

node をいれて npm を使えるようにして typescript をインストール

$ npm --version
6.13.4

$ node --version
v12.14.1

$ npm view typescript version
3.8.3

まずは設定ファイルを作る

とにかくミニマルなやつでがんばる
必要なのは以下の2つ

# make tsconfig.json
tsc --init

# make package.json
npm init -y

tsconfig.json

適当に書き換える 200行くらい出力されるので
コメントアウトされているやつは全部削除する
こんなことしていいのかどうかは知らん

{
  "compilerOptions": {
    // es6 にしたほうが何かと便利
    "target": "es6",

    // この3つはよく知りません
    "module": "commonjs",
    "strict": true,
    "forceConsistentCasingInFileNames": true,

    // いい感じの名前に変えよう
    "outDir": "./dist/",
    "rootDir": "./src/"
  }
}

package.json

{
  "name": "hello",
  "version": "1.0.0",
  "description": "This is a TypeScript practice.",

  // あとで使うのでこのコマンドだけ設定する
  "scripts": {
    "watch": "tsc -w"
  },
 
  "author": "YOURNAME",
  "license": "ISC"
}

ファイル構成

.
├── dist
│   └── main.js
├── src
│   └── main.ts
├── package.json
└── tsconfig.json

こんな感じになるので一番上の階層でこんなコマンドを撃つ

npm start watch

とするとこんな画面になる

[20:40:16] File change detected. Starting incremental compilation...

[20:40:16] Found 0 errors. Watching for file changes.

あとはsrc の中に有る main.ts ファイルをいじっていくと
その場でどんどんと js ファイルに変換してくれる すげぇ

main.ts を適当にごちゃごちゃ書く

let hello: string = "helloworld";

interface User {
    id: number,
    name: string,
}

const user: User = {
    id: 0,
    name: 'michael'
}

function getUser(id: number, name: string): User {
    return { id, name };
}

const admin: User = getUser(101, 'Macho');
console.log(admin);

すると変換される

"use strict";
let hello = "helloworld";
const user = {
    id: 0,
    name: 'michael'
};
function getUser(id, name) {
    return { id, name };
}
const admin = getUser(101, 'Macho');
console.log(admin);

はい簡単
素敵なTypeScript ライフを

React ion の 学び

React

ja.reactjs.org

環境構築

一番簡単バージョン

開発中のhtmlに下のscriptを追加すればおわり

  <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

<script src="YOUR_SCRIPT.js"></script>

JSXを使うには

let message = <h1>Hello World!</h1>;

という不思議な記法
これを使うには下記のscriptを読み込めばOK

<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

JSX もっと速くする

上の方法みたいにbabel.min.js を読み込んでそいつにJSXを変換させるのは
激重なので npm のパッケージにそれを頼む

npm init -y
npm install babel-cli@6 babel-preset-react-app@3
mkdir src
npx babel --watch src --out-dir . --presets react-app/prod

src の中に YOUR_SCRIPT.js を作って入れる
npx babel がトランスパイルをしてくれて随時script.jsに変換

# --watch src : src ディレクトリを watch
# --out-dir . : 出力ディレクトリは .(現在のディレクトリ)
# --presets react-app/prod : ?
npx babel --watch src --out-dir . --presets react-app/prod
project
 |--- npmで入った有象無象のファイル群
 |--- ...
 |---
 |--- index.html
 |--- converted.js # こいつになる
 |--- src/
       |---YOUR_SCRIPT.js # こいつが

一番オススメの環境構築

以上の環境は既存のシステムにReactを作る時にやる感じ
新規プロジェクトを作るのであればnpm create-react-appを利用するのがおすすめらしい

ja.reactjs.org

使い方は超かんたん

npx create-react-app my-app
cd my-app
npm start

これだけ デプロイ用には

npm run build

とすれば、build内に必要なやつが全部できる

続き

以上