CreateViewの使い方をマスターしよう

CreateViewを使いこなすクラスベースビューをマスターしよう

↓↓↓はじめて講座を受ける方はこちら↓↓↓
開発環境を作ろう

ウサギ
ウサギ

前回はDetailVIewを使って日記の詳細表示の方法を勉強したな

今回はCreateViewを使って、日記の新規作成画面を作る方法をやっていくぞ。

完成形の確認

ウサギ
ウサギ

下記が完成した新規作成画面だ

CreateViewを使った新規作成画面

仕様はこんな感じだ。

  • http://127.0.0.1:8000/add/ にアクセスすると新規作成画面が表示される
  • 新規作成画面は
    • ログイン必須
    • タイトル、本文、カテゴリーの入力フォームが表示される
    • タイトル、本文は入力必須
    • タイトルには誹謗中傷の文言「ばか」は入力禁止
    • 作成した投稿は、ログイン中の投稿者に紐つく
    • 作成完了したら、日記一覧画面に遷移する
    • FormのスタイルはBootstrapを使って実装する
    • Formのエラー時は下記のようにエラー内容を表示する

CreateViewを使うとどれだけ分かりやすく、簡単に書けるかやってみるぞ!

ベースアプリの準備

前回の講座で構築済みの方は読み飛ばしてください

リポジトリのクローン

ウサギ
ウサギ

まずは日記アプリのリポジトリをクローンする

GitHub - yheihei/base_diary: ウサでも分かる中級Django講座の教材
ウサでも分かる中級Django講座の教材. Contribute to yheihei/base_diary development by creating an account on GitHub.
$ 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
日記編集画面

完成版ソースだけ見る場合

ウサギ
ウサギ

今回も完成形を書いたブランチも用意しておいた。

答えだけ先に見たいやつは下記コマンドでブランチをチェックアウトしてくれ

GitHub - yheihei/base_diary at feature/#15
ウサでも分かる中級Django講座の教材. Contribute to yheihei/base_diary development by creating an account on GitHub.
# 完成版ブランチを見たい場合はこちら
$ 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を使った簡単な新規作成画面
わいへい
わいへい

おお、新規作成フォームっぽいのができた!

ログイン必須にする

ただ、誰でも日記を作成できてしまうと問題がある。

ログインしているユーザのみこのページを開けるようにするぞ。

ログイン必須にするには、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/にアクセスしてみると。。。

CreateViewをログイン必須にする
わいへい
わいへい

ログインしてないと見られなくなった!

ログインユーザーを投稿者として保存する

保存処理完成まであともう少しだ。

今は投稿者を選択するような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を使った保存処理、完成版
わいへい
わいへい

保存処理ができた〜〜〜

タイトルに特殊なバリデーションを付与する

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してやると、バリデーションエラー扱いになる。

最後に、作ったPostFormCreateView側と紐つける。

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'

この状態でタイトルに誹謗中傷を入れて「投稿」ボタンを押すと。。。

FormのエラーのDOM構造を書き換える
わいへい
わいへい

無事エラー表示がいい感じになったな

今回はviews.py側でほとんどのデザイン指定をした。

もちろん、テンプレート側をif文等でカスタマイズし、うまい感じのエラー表示を出すことも可能だ。

あえてそれをしないのは、テンプレート側にif文等が入り込むと、テンプレートが複雑化することが多い。

また、テンプレート側にFormのロジックが漏れるのも気持ち悪い。

それを許すと、Formを変更したらテンプレート側も変更しなければならない〜と言った場面がいつか出てくる。

そうなった時、やたらコピペでテンプレートを書き直したり、if文を読み解いたりする作業が発生する。これは微妙だ。なぜならバグの温床になるからだ。

ということで、ちょっと厚くForm周りを解説してみた。

Formの変更だけで済むような仕組みをぜひ意識してくれ。

まとめ

ウサギ
ウサギ

CreateViewの使い方をまとめるぞ

  • CreateViewはModelを指定することで、そのModelに紐ついたレコードを作成する画面を作ることができる
  • Formはfieldsにカラム指定する方法でもできるし、Formクラスを継承したクラスを用いることもできる
  • save時に特殊な操作をする場合はform_valid関数が使える
  • Formのデザイン変更、バリデーション等は厄介なのでよく覚えておく
わいへい
わいへい

なんかCreateViewの解説というよりはFormの解説みたいだったな。

ウサギ
ウサギ

新規作成画面のキモはFormだからな

ウサギ
ウサギ

ぶっちゃけ、CreateViewなんてチュートリアル見れば一発でわかるんだよ

ウサギ
ウサギ

それよりは、自動で生成されたFormを、どうやったらカスタマイズできるかの方が難しい

ウサギ
ウサギ

CreateViewのデフォルトのフォームを使う開発なんて絶対無い

ウサギ
ウサギ

きちんと使いこなせるようになっておいてくれ

\ 講座で学んだことを即アウトプットしよう /

次の講座

Djangoの基礎を学びたい方は

Djangoの基礎を固めたい方はこちら(セール時に買うのがおすすめ)

『Djangoパーフェクトマスター』〜インスタ映えを支えるPython超高速開発Webフレームワークを徹底解説!

動画講座で手を動かしながら、ほとんどのことが学べます。

ウサギもここから始めました。

コメント

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