【Django3 入門】フォームのつくり方やバリデーションの実装など

Django

今回は、フォームまわりの扱い方をみていきます。

前提

・「my_app」というアプリを作成済
・「manage.py」と同じ階層に「templates」ディレクトリを作成し、読込先のテンプレートディレクトリのパスをこちらに変更済

.
.
.
from pathlib import Path
import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATE_DIR = os.path.join(BASE_DIR, 'templates')
.
.
.
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [TEMPLATE_DIR,],


・プロジェクトディレクトリの「urls.py」に以下のようにパスを登録済

path('my_app/', include('my_app.urls')),

フォームの作成

まず「/templates」に以下を作成
・「form」ディレクトリ
・「form/index.html」
・「form/form.html」

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>HOME</title>
    </head>
    <body>
        <h1>HOME</h1>
        <a href="{% url 'my_app:form' %}">フォーム画面へ</a>
    </body>
</html>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Form</title>
    </head>
    <body>
        <form method="POST">
            {% csrf_token %}
                <table class="table">
                    {{ form.as_table }}
                </table>
                <input type="submit" value="送信">
        </form>
    </body>
</html>


「my_app」に「forms.py」を作成

from django import forms

class UserInfo(forms.Form):
    name = forms.CharField()
    age = forms.IntegerField()
    mail = forms.EmailField()


「views.py」を下記のように変更

from django.shortcuts import render
from . import forms

# Create your views here.

def index(request):
    return render(request, 'form/index.html')

def form(request):
    form = forms.UserInfo()

    return render(
        request, 'form/form.html', context={
            'form': form
        }
    )


「my_app」に「urls.py」を作成

from django.urls import path
from . import views

app_name = 'my_app'

urlpatterns = [
    path('', views.index, name='index'),
    path('form', views.form, name='form')
]



「/my_app」にアクセスすると、以下のように表示されます。

送信内容の受け取り

「views.py」を以下のように変更

from django.shortcuts import render
from . import forms

# Create your views here.

def index(request):
    return render(request, 'form/index.html')

def form(request):
    form = forms.UserInfo()
    if request.method == 'POST':
        form = forms.UserInfo(request.POST)
        if form.is_valid():
            print(f"name: {form.cleaned_data['name']}, age: {form.cleaned_data['age']}, mail: {form.cleaned_data['mail']}")

    return render(
        request, 'form/form.html', context={
            'form': form
        }
    )


ブラウザのフォーム画面で値を入力して送信を押すと、コマンドラインに出力されます。

フィールドの種類

下記URLを参照
https://docs.djangoproject.com/ja/3.1/ref/forms/fields/


「forms.py」を修正

from django import forms

class UserInfo(forms.Form):
    name = forms.CharField()
    age = forms.IntegerField()
    mail = forms.EmailField()
    is_married = forms.BooleanField()
    birthday = forms.DateField()
    salary = forms.DecimalField()
    job = forms.ChoiceField(choices=(
        (1, '正社員'),
        (2, '自営業'),
        (3, '学生'),
        (4, '無色')
    ))
    hobby = forms.MultipleChoiceField(choices=(
        (1, 'スポーツ'),
        (2, '読書'),
        (3, '映画鑑賞'),
        (4, '楽器'),
        (5, 'その他')
    ))
    homepage = forms.URLField()


ブラウザの表示↓

フォームのカスタマイズ

ラベルのカスタマイズ

name = forms.CharField(label='名前')

入力必須を無効

homepage = forms.URLField(label='ホームページURL', required=False)

ウィジェットを変更

mail = forms.EmailField(
    label='メールアドレス',
    widget=forms.TextInput(attrs={'placeholder': 'taro@sample.com'})
)
is_married = forms.BooleanField(label='既婚')
birthday = forms.DateField(label='生年月日', initial='1900-01-01')
salary = forms.DecimalField(label='給与')
job = forms.ChoiceField(label='仕事', choices=(
    (1, '正社員'),
    (2, '自営業'),
    (3, '学生'),
    (4, '無色')
), widget=forms.RadioSelect)
hobby = forms.MultipleChoiceField(label='趣味', choices=(
    (1, 'スポーツ'),
    (2, '読書'),
    (3, '映画鑑賞'),
    (4, '楽器'),
    (5, 'その他')
), widget=forms.CheckboxSelectMultiple)
homepage = forms.URLField(label='ホームページURL', required=False)
note = forms.CharField(label='備考', widget=forms.Textarea)

初期値を設定

birthday = forms.DateField(label='生年月日', initial='1900-01-01')

文字数の最小・最大値の設定

name = forms.CharField(label='名前', min_length=1, max_length=20)

id, class の設定

今回は、挙動を確認するために Bootstrap4 を読み込みます。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Form</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    </head>
    <body>
        <div class="container">
            <form method="POST">
                {% csrf_token %}
                    <table class="table">
                        {{ form.as_table }}
                    </table>
                    <input type="submit" value="送信">
            </form>
        </div>
    </body>
</html>


id や class を設定するには2通りのやり方があります。

widget の属性として設定

name = forms.CharField(
    label='名前',
    min_length=1, 
    max_length=20,
    widget=forms.TextInput(attrs={'class': 'form-control'})
)

クラスの初期化処理時に設定

以下の関数を追加します。

def __init__(self, *args, **kwargs):
    super(UserInfo, self).__init__(*args, **kwargs)
    self.fields['age'].widget.attrs['class'] = 'form-control'

フォームのバリデーション

フォームを送信する際に、フォームが正しく入力されているかチェックする機能をバリデーションと言います。

バリデーションの実装方法をいくつかみていきます。

「clean_フィールド名」メソッドの使用

「clean_フィールド名」というメソッドを使用します。

def clean_homepage(self):
    homepage = self.cleaned_data['homepage']
    if not homepage.startswith('https'):
        raise forms.ValidationError('httpsから始めてください')


ホームページURL入力内容が「https」から始まっていない場合、以下のエラーが表示されます。

validators の使用

.
.
.
from django.core import validators
.
.
.
    age = forms.IntegerField(
        label='年齢',
        validators=[validators.MinValueValidator(20, message='20歳以上が対象です')]
    )


年齢の入力内容が20未満だとエラーが表示されます。


validators の種類は以下を参照
https://docs.djangoproject.com/ja/3.1/ref/validators/#built-in-validators

バリデートメソッドの自作

自作したバリデート用のメソッドをフィールドの validators に設定することで実装できます。

.
.
.
from django.core import validators
.
.
.
    def check_name(value):
        if value == 'あいうえお':
            raise validators.ValidationError('その名前は登録できません')

    name = forms.CharField(
        label='名前',
        min_length=1, 
        max_length=20,
        widget=forms.TextInput(attrs={'class': 'form-control'}),
        validators=[check_name]
    )

cleanメソッドの使用

cleanメソッドと、メールアドレス再入力フィールドを追加します。

def clean(self):
    cleaned_data = super().clean()
    mail = cleaned_data['mail']
    verify_mail = cleaned_data['verify_mail']
    if mail != verify_mail:
        raise forms.ValidationError('メールアドレスが一致しません')
・
・
・
verify_mail = forms.EmailField(
    label='メールアドレス再入力',
    widget=forms.TextInput(attrs={'placeholder': 'taro@sample.com'})
)

モデルフォーム

データの挿入

モデルを作成

from django.db import models

# Create your models here.

class Post(models.Model):
    name = models.CharField(max_length=50)
    title = models.CharField(max_length=255)
    memo = models.CharField(max_length=255)


「forms.py」を修正

.
.
.
from .models import Post
・
・
・
class PostModelForm(forms.ModelForm):
    memo = forms.CharField(
        widget=forms.Textarea(attrs={'rows': 5})
    )
    class Meta:
        model = Post
        fields = '__all__'
        # 必要な項目のみ指定
        # fields = ['name', 'title']

        # 除外する項目を指定
        # exclude = ['memo']


その他ファイルを修正

.
.
.
def form_post(request):
    form = forms.PostModelForm()
    if request.method == 'POST':
        form = forms.PostModelForm(request.POST)
        if form.is_valid():
            form.save()
    return render(
        request, 'form/form_post.html', context={'form': form}
    )
path('form_post', views.form_post, name='form_post')
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Form</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    </head>
    <body>
        <div class="container">
            <form method="POST">
                {% csrf_token %}
                    <table class="table">
                        {{ form.as_table }}
                    </table>
                    <input type="submit" value="送信">
            </form>
        </div>
    </body>
</html>



マイグレーションを実行

manage.py makemigrations my_app
python manage.py migrate


「/my_app/form_post」にアクセスすると、以下のように表示されます。


任意の値を入力して送信すると、データベースにも反映されます。

saveメソッドのカスタマイズ

saveメソッドを上書きすることでカスタマイズできます。

class PostModelForm(forms.ModelForm):
    memo = forms.CharField(
        widget=forms.Textarea(attrs={'rows': 5})
    )
    class Meta:
        model = Post
        fields = '__all__'
        
    def save(self, *args, **kwargs):
        obj = super(PostModelForm, self).save(commit=False, *args, **kwargs)
        obj.save()
        return obj

モデルフォームクラスの継承

class BaseForm(forms.ModelForm):
    def save(self, *args, **kwargs):
        print(f'form: {self.__class__.__name__}')
        return super(BaseForm, self).save(*args, **kwargs)


class PostModelForm(BaseForm):
.
.
.

各フィールドのカスタマイズ

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Form</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    </head>
    <body>
        <div class="container py-4">
            <form method="POST">
                {% csrf_token %}
                    {{ form.name.label }}: {{ form.name }}
                    {% if form.name.errors %}
                        {{ form.name.errors.as_text }}
                    {% endif %}

                    {{ form.title.label }}: {{ form.title }}
                    {% if form.title.errors %}
                        {{ form.title.errors.as_text }}
                    {% endif %}

                    {{ form.memo.label }}: {{ form.memo }}
                    {% if form.memo.errors %}
                        {{ form.memo.errors.as_text }}
                    {% endif %}
                    <input type="submit" value="送信">
            </form>
        </div>
    </body>
</html>
class PostModelForm(BaseForm):
    def clean_name(self):
        name = self.cleaned_data.get('name')
        if name == 'あいうえお':
            raise validators.ValidationError('その名前は登録できません')
        return name

    def clean_title(self):
        title = self.cleaned_data.get('title')
        if title == 'あいうえお':
            raise validators.ValidationError('そのタイトルは登録できません')
        return title

    name = forms.CharField(label='名前')
    title = forms.CharField(label='タイトル')
    memo = forms.CharField(
        label='メモ',
        widget=forms.Textarea(attrs={'rows': 5})
    )
.
.
.

ブラウザには以下のように表示されます。



また、バリデーションも正常に実行できています。




複数のエラーを一ヵ所に表示させたい場合は以下のとおりです。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Form</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    </head>
    <body>
        <div class="container py-4">
            <form method="POST">
                {% csrf_token %}
                    {% if form.errors %}
                        {% for key, value in form.errors.items %}
                            <p>{{key}}: {{value.as_text}}</p>
                        {% endfor %}
                    {% endif %}

                    {{ form.name.label }}: {{ form.name }}
                    {{ form.title.label }}: {{ form.title }}
                    {{ form.memo.label }}: {{ form.memo }}
                    <input type="submit" value="送信">
            </form>
        </div>
    </body>
</html>


エラーを発生させると、以下のように一ヵ所にまとめて表示されます。

フォームを外部ファイルに定義

フォームのテンプレートファイルをつくります。

<form method="POST">
    {% csrf_token %}
        {% if form.errors %}
            {% for key, value in form.errors.items %}
                <p>{{key}}: {{value.as_text}}</p>
            {% endfor %}
        {% endif %}
        
        {{ form.name.label }}: {{ form.name }}
        {% if form.name.errors %}
            {{ form.name.errors.as_text }}
        {% endif %}

        {{ form.title.label }}: {{ form.title }}
        {% if form.title.errors %}
            {{ form.title.errors.as_text }}
        {% endif %}

        {{ form.memo.label }}: {{ form.memo }}
        {% if form.memo.errors %}
            {{ form.memo.errors.as_text }}
        {% endif %}
        <input type="submit" value="送信">
</form>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Form</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    </head>
    <body>
        <div class="container py-4">
            {% include "form/form_template.html" %}
        </div>
    </body>
</html>


ブラウザにはこれまで通り表示されます。




もうひとつテンプレートファイルをつくります。

<form method="POST">
    {% csrf_token %}
    {% if as_table %}
        <table>
            {{ form.as_table }}
        </table>
    {% elif as_ul %}
        {{ form.as_ul }}
    {% else %}
        {{ form.as_p }}
    {% endif %}
    <input type="submit" value="送信">
</form>


with でプロパティを渡します。

{% include "form/form_template2.html" with as_ul=True %}


するとフォームがulタグで表示されます。

ファイルのアップロード

基本的な方法

「settings.py」を修正

from pathlib import Path
import os

BASE_DIR = Path(__file__).resolve().parent.parent
.
.
.
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')


「manage.py」と同階層に「media」ディレクトリを作成
(「/media」)


「views.py」を修正

.
.
.
from django.core.files.storage import FileSystemStorage
import os
.
.
.
def upload_sample(request):
    if request.method == 'POST' and request.FILES['upload_file']:
        upload_file = request.FILES['upload_file']
        fs = FileSystemStorage()
        file_path = os.path.join('upload', upload_file.name)
        file = fs.save(file_path, upload_file)
        uploaded_file_url = fs.url(file)
        return render(request, 'form/upload_file.html', context={
            'uploaded_file_url': uploaded_file_url
        })
    return render(request, 'form/upload_file.html')


HTMLファイルを作成

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Form Upload</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    </head>
    <body>
        <div class="container py-4">
            <form method="POST" enctype="multipart/form-data">
                {% csrf_token %}
                <input type="file" name="upload_file"><br>
                <input type="submit" value="保存">
            </form>

            {% if uploaded_file_url %}
                <p>
                    <a href="{{ uploaded_file_url }}">{{ uploaded_file_url }}</a>
                </p>
            {% endif %}
        </div> 
    </body>
</html>


パスの追加

path('upload_sample', views.upload_sample, name='upload_sample')


「/my_app/upload_sample」にアクセスし、適当な画像を選択して保存


すると、「/media/upload」に画像が保存されます。

モデルを使った方法

プロジェクトディレクトリの「urls.py」を修正

.
.
.
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
.
.
.
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


モデルの作成

class User(models.Model):
    name = models.CharField(max_length=50)
    age = IntegerField()
    picture = models.FileField(upload_to='picture/')


マイグレーションの実行

python manage.py makemigrations my_app
python manage.py migrate my_app



「forms.py」の修正

.
.
.
from .models import Post, User
.
.
.
class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = '__all__'


「views.py」の修正

.
.
.
def upload_model_form(request):
    user = None
    if request.method == 'POST':
        form = forms.UserForm(request.POST, request.FILES)
        if form.is_valid():
            user = form.save()
    else:
        form = forms.UserForm()
    return render(request, 'form/upload_model_form.html', context={
        'form': form,
        'user': user
    })


HTMLファイルの作成

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Form Upload</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    </head>
    <body>
        <div class="container py-4">
            <form method="POST" enctype="multipart/form-data">
                {% csrf_token %}
                <input type="file" name="upload_file"><br>
                <input type="submit" value="保存">
            </form>

            {% if uploaded_file_url %}
                <p>
                    <a href="{{ uploaded_file_url }}">{{ uploaded_file_url }}</a>
                </p>
            {% endif %}
        </div> 
    </body>
</html>


パスの追加

path('upload_model_form', views.upload_model_form, name='upload_model_form')




「/my_app/upload_model_form」にアクセスして、値の入力と画像の選択後に保存すると下に表示されます。


画像は「/media/picture」は以下に保存されます。


また、「upload_to=」を以下のように指定すると、
画像を保存したときの日付のディレクトリが自動的に作られます。

class User(models.Model):
    name = models.CharField(max_length=50)
    age = IntegerField()
    picture = models.FileField(upload_to='picture/%Y-%m-%d')





今回は以上になります。
ご覧いただきありがとうございました!

続きはこちら↓

コメント

コンタクトフォーム

    タイトルとURLをコピーしました