前回はDetailVIewを使って日記の詳細表示の方法を勉強したな
今回はCreateViewを使って、日記の新規作成画面を作る方法をやっていくぞ。
完成形の確認
下記が完成した新規作成画面だ
仕様はこんな感じだ。
- http://127.0.0.1:8000/add/ にアクセスすると新規作成画面が表示される
- 新規作成画面は
- ログイン必須
- タイトル、本文、カテゴリーの入力フォームが表示される
- タイトル、本文は入力必須
- タイトルには誹謗中傷の文言「ばか」は入力禁止
- 作成した投稿は、ログイン中の投稿者に紐つく
- 作成完了したら、日記一覧画面に遷移する
- FormのスタイルはBootstrapを使って実装する
- Formのエラー時は下記のようにエラー内容を表示する
CreateViewを使うとどれだけ分かりやすく、簡単に書けるかやってみるぞ!
ベースアプリの準備
リポジトリのクローン
まずは日記アプリのリポジトリをクローンする
$ git clone https://github.com/yheihei/base_diary.git
$ cd base_diary
マイグレーションと初期データ投入
次にマイグレーションとloaddataによる初期データの投入だ
$ python manage.py migrate
$ python manage.py loaddata initial.json
この状態でhttp://127.0.0.1:8000/
を開くと。。。
よし、日記一覧が表示されたね
日記の記事を追加したり、編集したりする場合はhttp://127.0.0.1:8000/admin/diary/post/
に入って行う。
ログインできるユーザー名とパスワードは下記だ。
UserName: yhei
Password: password
完成版ソースだけ見る場合
今回も完成形を書いたブランチも用意しておいた。
答えだけ先に見たいやつは下記コマンドでブランチをチェックアウトしてくれ
# 完成版ブランチを見たい場合はこちら
$ git fetch && git checkout feature/#15
CreateViewで機能を作ってみる
基本のフォームを表示する
まずCreateViewの処理をdiary/views.pyに記述する。
from .models import Post
from django.views.generic import CreateView
class PostCreateView(CreateView):
model = Post
fields = ['user', 'title', 'body', 'categories']
CreateViewを継承したモデルを作成、保存するモデル(Post)をmodel
変数に入力する。
また、表示したいフォームをfields
変数に入力する。下記のようにだ。
fields = ['user', 'title', 'body', 'categories']
ここで指定するフォーム名は、Postモデルのカラム名だ。Postモデルを見てみよう。
class Post(models.Model):
...
user = models.ForeignKey(User, on_delete=CASCADE)
title = models.CharField(max_length=2048)
body = models.TextField()
...
categories = models.ManyToManyField(
...
)
投稿者のカラムのuser、日記のタイトルと本文を表すtitleとbody、日記のカテゴリを多対多で紐つけるcategoriesを指定すれば、日記が作成できるな。
次はルーティングの設定だ。
http://127.0.0.1:8000/add/のアクセスを、CreateView
にルーティングさせる。
diary/urls.py
に下記を入力する。
from django.urls import path
from . import views
app_name = "diary"
urlpatterns = [
path("", views.index, name="index"),
# CreateViewにルーティングさせる
path('add/', views.PostCreateView.as_view(), name='post-add'),
]
最後に、CreateViewで表示させるテンプレートを作成する。
CreateViewのテンプレート名は他のクラスベースビューと同じく自動でデフォルトが決まる。
(指定も可能)
デフォルトは/templates/アプリ名/<modelに指定したクラス名の小文字>_form.html
だ。
ということで、diary/templates/diary/post_form.html
に下記を記述する。
<html>
<head>
<title>とあるエンジニアの日記帳</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
</head>
<body>
{% load static %}
<header class="container pt-3">
<h1>とあるエンジニアの日記帳</h1>
</header>
<main role="main" class="container pt-3 pb-3">
<article>
<h2>記事の追加</h2>
<div>
<form action="{% url 'diary:post-add' %}" method="POST">
{% csrf_token %}
{% for field in form.visible_fields %}
<div>
<label>{{ field.label }}</label>
{{ field }}
{{ field.errors }}
{{ field.help_text }}
</div>
{% endfor %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<input type="submit" value="投稿" />
</form>
</div>
</article>
</main>
</body>
</html>
これでCreateViewの基本形は完成した。
http://127.0.0.1:8000/add/にアクセスしてみると。。。
おお、新規作成フォームっぽいのができた!
ログイン必須にする
ただ、誰でも日記を作成できてしまうと問題がある。
ログインしているユーザのみこのページを開けるようにするぞ。
ログイン必須にするには、CreateViewを継承しているクラスに、LoginRequiredMixin
クラスを継承してやると良い。
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
class PostCreateView(LoginRequiredMixin, CreateView):
# ログインURLも指定する
login_url = reverse_lazy('admin:login')
...
login_url変数に、ログインURLを指定するのも忘れずに。
一旦ログアウトして、http://127.0.0.1:8000/add/にアクセスしてみると。。。
ログインしてないと見られなくなった!
ログインユーザーを投稿者として保存する
保存処理完成まであともう少しだ。
今は投稿者を選択するようなUIになっているが、
普通ログインユーザーが投稿者になるべきだよな?
ということで、
- FormからUserを消し
- Formの保存時にログインユーザーを紐つけて登録する処理
をやってみる。
こんな感じだ。
class PostCreateView(LoginRequiredMixin, CreateView):
...
# fields = ['user', 'title', 'body', 'categories']
fields = ['title', 'body', 'categories']
def form_valid(self, form):
'''
save時に特殊な操作をする
'''
# ユーザーを投稿者として保存できるようにする
object = form.save(commit=False)
object.user = self.request.user
object.save()
return super().form_valid(form)
form_valid関数はレコードが保存される直前で呼ばれる関数だ。
form_valid
関数をオーバーライドし、保存処理のついでにobject.user
にログインユーザー(self.request.user
)をセットしてやる。
これでuserがきちんと保存される。
保存が完了したら日記一覧に遷移させる
最後に保存が完了したら、日記一覧に遷移させる。
下記を追加する。
class PostCreateView(LoginRequiredMixin, CreateView):
...
# 保存したら一覧に遷移するようにする
success_url = reverse_lazy('diary:index')
これで保存処理が完成だ。やってみるぞ。
保存処理ができた〜〜〜
タイトルに特殊なバリデーションを付与する
CreateViewとは直接関係ないが、Formのフィールドに個別のバリデーションをつける方法もやっておく。
例えば、タイトルには誹謗中傷の単語を入れられないようにしてみる。
やり方は簡単。
Postに紐ついたModelFormを用意し、clean_(フィールド名)関数をオーバーライドする。
from django import forms
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'body', 'categories']
def clean_title(self):
'''
特殊なバリデーション
'''
title = self.cleaned_data['title']
if 'ばか' in title:
raise forms.ValidationError('誹謗中傷ワードはタイトルに設定できません')
return title
clean_(フィールド名)
関数はFormのバリデーションが実行された際に、呼ばれるものだ。
ここでValidationError
をraiseしてやると、バリデーションエラー扱いになる。
最後に、作ったPostForm
をCreateView
側と紐つける。
class PostCreateView(LoginRequiredMixin, CreateView):
...
# fields = ['title', 'body', 'categories'] # これはもういらない
form_class = PostForm
これでタイトルに誹謗中傷ワードを入れて「投稿」ボタンを押すと。。。
バリデーションができてる!!
フォームのラベル表示を変える
またまたCreateViewからは話がそれるが。
気になるので語らせてくれ。
ModelForm
が自動で生成している、下記のラベル部分。
今はModelのカラム名が自動で表示されている。
これを任意の名前に変えるのも簡単にできる。
diary/model.py
のModel定義にverbose_name
で指定してやればいい。
class Post(models.Model):
...
title = models.CharField(max_length=2048, verbose_name='タイトル',)
body = models.TextField(verbose_name='本文',)
...
categories = models.ManyToManyField(
'Category',
blank=True,
related_name="posts",
verbose_name='カテゴリー',
)
これで再度表示すると。。。
きちんとラベルが更新されたな!
フォームにCSSをあてる
そして。
本当に申し訳ないのだが。
またしてもクラスベースビューとは関係のない話をする。
なぜなら気になるからだ。
何回か別のレッスンでも言っているが、Form周りの細かいデザインのカスタマイズ方法があまり情報として出回っていない。なのでここでまとめておく。
今回は、CreateViewを使って作ったFormに、BootstrapのCSSを当てていくぞ。
下記のようなDOM構造になれば完成だ。
<div class="form-group">
<label>タイトル</label>
<input type="text" name="title" maxlength="2048" class="form-control" required id="id_title">
</div>
<div class="form-group">
<label>本文</label>
<textarea name="body" cols="40" rows="10" class="form-control" required id="id_body">
</textarea>
</div>
<div class="form-group">
<label>カテゴリー</label>
<select name="categories" class="form-control" id="id_categories" multiple>
<option value="1">日記</option>
<option value="2">お知らせ</option>
</select>
</div>
まずはテンプレート側にBootstrapのクラスを当てていくぞ。
...
<article>
<h2>記事の追加</h2>
<div class="mt-3">
<form action="{% url 'diary:post-add' %}" method="POST">
{% csrf_token %}
{% for field in form.visible_fields %}
<div class="form-group">
<label>{{ field.label }}</label>
{{ field }}
{{ field.errors }}
{{ field.help_text }}
</div>
{% endfor %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<input class="btn btn-primary" type="submit" value="投稿" />
</form>
</div>
</article>
...
formのfieldを<div class="form-group">
で囲っておく。
その上で、中のinputにclass=”form-control”をつけてやればいいが。
テンプレート側では設定できない。
そこで、PostForm側で下記のようにしてCSSを設定する。
class PostForm(forms.ModelForm):
...
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# formにCSSをあてる
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
これで完成。
http://127.0.0.1:8000/add/ にアクセスしてみると。。。
かなり綺麗になった!!!
フォームのエラー文のデザインをカスタマイズする
最後だ。
あとは、フォームのエラー文のデザインをきちんと表示させるぞ。
今フォームのエラーを表示させると、下記のDOM構造になる。
<div class="form-group">
<label>タイトル</label>
<input type="text" name="title" value="ばかのタイトル" maxlength="2048" class="form-control" required id="id_title">
<ul class="errorlist"><li>誹謗中傷ワードはタイトルに設定できません</li></ul>
</div>
エラーの内容がul → liでリスト表示されている。
このままではBootstrapのエラー表示が使えないので。
下記のようなDOMに書き換えてみる。
<div class="form-group">
<label>タイトル</label>
<input type="text" name="title" value="ばかのタイトル" maxlength="2048" class="form-control is-invalid" required id="id_title">
<div class="invalid-feedback mt-2">誹謗中傷ワードはタイトルに設定できません</div>
</div>
formのinputフィールドに、is-invalid
クラスをつけ、その下にinvalid-feedback
クラスをつけたdivでエラー表示をする。
こんな感じになる。
色々やり方はあるが、今回はテンプレート側を変えずに実装していく。
まずは、エラー文をulではなく、特定のclassをつけたdivで表示させるようにカスタマイズする。
ErrorListクラスを継承したBootstrapErrorListクラスを作成し、as_ul
関数でをオーバーライドする。
その中でエラーで表示したいDOMを記載してreturnしてやる。
from django.forms.utils import ErrorList
from django.utils.html import format_html_join
class BootstrapErrorList(ErrorList):
'''
エラー文のDOM構造をカスタマイズする
'''
def as_ul(self):
if not self.data:
return ''
return format_html_join(
'\n',
'<div class="invalid-feedback mt-2">{}</div>',
((e,) for e in self)
)
このBootstrapErrorList
クラスを、Form側で指定すると、as_ul
関数が呼び出されて、任意のDOM構造でエラー表示できるって寸法だ。
Form側のerror_classをBootstrapErrorList
に変更するのは下記。
class PostForm(forms.ModelForm):
...
def __init__(self, *args, **kwargs):
# デフォルトのエラー表示を変えるカスタムクラスを指定
if 'error_class' not in kwargs.keys():
kwargs['error_class'] = BootstrapErrorList
さらに、input fieldについても、エラー時はis-invalid
クラスをつける必要がある。
それも__init__
関数内でやってしまうと良い。
class PostForm(forms.ModelForm):
...
def __init__(self, *args, **kwargs):
...
# エラー時はエラーが起きているfieldにis-invalidのclass付与
for error_field_name in self.errors:
self.fields[error_field_name].widget.attrs['class'] += ' is-invalid'
この状態でタイトルに誹謗中傷を入れて「投稿」ボタンを押すと。。。
無事エラー表示がいい感じになったな
まとめ
CreateViewの使い方をまとめるぞ
- CreateViewはModelを指定することで、そのModelに紐ついたレコードを作成する画面を作ることができる
- Formはfieldsにカラム指定する方法でもできるし、Formクラスを継承したクラスを用いることもできる
- save時に特殊な操作をする場合は
form_valid
関数が使える - Formのデザイン変更、バリデーション等は厄介なのでよく覚えておく
なんかCreateViewの解説というよりはFormの解説みたいだったな。
新規作成画面のキモはFormだからな
ぶっちゃけ、CreateViewなんてチュートリアル見れば一発でわかるんだよ
それよりは、自動で生成されたFormを、どうやったらカスタマイズできるかの方が難しい
CreateViewのデフォルトのフォームを使う開発なんて絶対無い
きちんと使いこなせるようになっておいてくれ
\ 講座で学んだことを即アウトプットしよう /
次の講座
Djangoの基礎を学びたい方は
Djangoの基礎を固めたい方はこちら(セール時に買うのがおすすめ)
『Djangoパーフェクトマスター』〜インスタ映えを支えるPython超高速開発Webフレームワークを徹底解説!
動画講座で手を動かしながら、ほとんどのことが学べます。
ウサギもここから始めました。
コメント