naritoブログ

このブログはPython/DjangoとBootstrap4で作成されました
Githubのソースコード

Django、インラインフォームセットを使う

約224日前 2018年1月4日9:43
プログラミング関連
Django Python

概要


このまえDjango、モデルフォームセットを紹介したので、今回はインラインフォームセットを紹介します。
インラインフォームセットはどういう状況に使うのかといいますと...

まず、以下のようなモデルがあったとしましょう。
日記の一日と、それに紐づくカテゴリです。
from django.db import models
from django.utils import timezone


class Category(models.Model):
    """日記のカテゴリ"""
    name = models.CharField('タイトル', max_length=30)

    def __str__(self):
        return self.name


class Day(models.Model):
    """日記の一日となるモデル"""
    title = models.CharField('タイトル', max_length=200)
    text = models.TextField('本文')
    date = models.DateTimeField('日付', default=timezone.now)
    category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)

    def __str__(self):
        return self.title




通常であればまずCategoryを作成し、その後Dayを作成する際に既に作成したCategoryを指定していきます。
しかし場合によっては、あるカテゴリを作成すると同時にDayも作成したくなるかもしれません。別々に作成させりゃいいじゃんと思っても、そうはいかない状況も出てきます。
Categoryの作成画面でDayも一緒に作成できるのが、インラインフォームです。
実際に使ってみます。

自分で作ったページで表示させる



forms.py


inlineformset_factory関数を使います。第一引数と第二引数は必須です。
fieldsは、excludeやform引数を指定すれば不要です。
modelformset_factoryと違い、extraはデフォルトで3、can_deleteがデフォルトでTrueです。とりあえず、今回はcan_deleteをFalseにします。
Trueにしても以降のコードは何も変更箇所はありませんので、削除用のチェックボックスが欲しい方はTrueにしましょう。
DayInlineFormSet = forms.inlineformset_factory(
    Category, Day, fields=('title', 'text'), can_delete=False
)



views.py


モデルフォームセットとは違い、カテゴリの追加処理がまず必要になります。
なのでCreateViewでカテゴリ作成ができるようにして、それに記事のフォームセットをくっつけるアプローチにします。
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.views import generic
from .forms import DayInlineFormSet
from .models import Category
...
...
class CategoryCreateView(generic.CreateView):
    model = Category
    fields = '__all__'
    success_url = reverse_lazy('diary:index')

    def form_valid(self, form):
        # カテゴリはまだ保存せず、オブジェクトだけ取得する
        self.object = form.save(commit=False)

        # 取得したカテゴリを、instance引数に渡す
        formset = DayInlineFormSet(self.request.POST, instance=self.object)

        # 記事達が問題なければ、カテゴリを保存し、記事達も保存する
        if formset.is_valid():
            self.object.save()
            formset.save()
            return HttpResponseRedirect(self.get_success_url())
        else:
            return self.render_to_response(self.get_context_data(form=form, formset=formset))

    def get_context_data(self, **kwargs):
        if 'formset' not in kwargs:
            kwargs['formset'] = DayInlineFormSet(self.request.POST or None)
        return super().get_context_data(**kwargs)



category_form.html


カテゴリ(と記事達)を追加する、category_form.htmlは以下のようになりました。
{% extends 'diary/base.html' %}

{% block content %}
<form action="" method="POST">
  <!-- ここから、カテゴリ({{ form }}) の部分 -->
  <h2>カテゴリの追加</h2>
  {{ form.non_field_errors }}
  <table class="table table-bordered">
      {% for field in form %}
          <tr>
              <td>{{ field.label_tag }}</td>
              <td>{{ field }} {{ field.errors }}</td>
          </tr>
      {% endfor %}
  </table><!-- ここまで、カテゴリの追加 -->
  
  <!-- ここから、記事達({{ formset }})の部分 -->
  <h2>記事達の追加</h2>
  {{ formset.management_form }} 
  {% for form in formset %}
    {{ form.id }}
    {{ form.non_field_errors }}
    <table class="table table-bordered">
        {% for field in form.visible_fields %}
            <tr>
                <td>{{ field.label_tag }}</td>
                <td>{{ field }} {{ field.errors }}</td>
            </tr>
        {% endfor %}
    </table>
  {% endfor %}<!-- ここまで、記事達の追加 -->

  <button type="submit" class="btn btn-primary">送信</button>
  {% csrf_token %}
</form>
{% endblock %}




今回は、forループを上手く使ってフォーム部分を作成しています。こちらがカテゴリ部分
  <!-- ここから、カテゴリ({{ form }}) の部分 -->
  <h2>カテゴリの追加</h2>
  {{ form.non_field_errors }}
  <table class="table table-bordered">
      {% for field in form %}
          <tr>
              <td>{{ field.label_tag }}</td>
              <td>{{ field }} {{ field.errors }}</td>
          </tr>
      {% endfor %}
  </table><!-- ここまで、カテゴリの追加 -->



そして、記事の部分。モデルフォームセットと扱い方は同じです。
  <!-- ここから、記事達({{ formset }})の部分 -->
  <h2>記事達の追加</h2>
  {{ formset.management_form }} 
  {% for form in formset %}
    {{ form.id }}
    {{ form.non_field_errors }}
    <table class="table table-bordered">
        {% for field in form.visible_fields %}
            <tr>
                <td>{{ field.label_tag }}</td>
                <td>{{ field }} {{ field.errors }}</td>
            </tr>
        {% endfor %}
    </table>
  {% endfor %}<!-- ここまで、記事達の追加 -->



通常ページでの見た目


ブラウザでアクセスしてみると、中々良い感じですね。
通常のページでインライン表示させた


UpdateViewで使う


カテゴリの更新でも同様にインラインフォームを使うならば、先程とほとんど同じように書けます。
class CategoryUpdateView(generic.UpdateView):
    model = Category
    fields = '__all__'
    success_url = reverse_lazy('diary:index')

    def form_valid(self, form):
        # カテゴリはまだ保存せず、オブジェクトだけ取得する
        self.object = form.save(commit=False)

        # 取得したカテゴリを、instance引数に渡す
        formset = DayInlineFormSet(self.request.POST, instance=self.object)

        # 記事達が問題なければ、カテゴリを保存し、記事達も保存する
        if formset.is_valid():
            self.object.save()
            formset.save()
            return HttpResponseRedirect(self.get_success_url())
        else:
            return self.render_to_response(self.get_context_data(form=form, formset=formset))

    def get_context_data(self, **kwargs):
        if 'formset' not in kwargs:
            kwargs['formset'] = DayInlineFormSet(self.request.POST or None, instance=self.object)
        return super().get_context_data(**kwargs)




違いはUpdateViewになったことと、get_context_dataメソッド内のDayInlineFormSetにinstance引数が追加されたことです。
重複コードが多いので、上手いこと共通化できそうですね。フォームセット用のViewやMixinを書くのも面白いでしょう。
テンプレートは、作成で使ったものがそのまま使えます。
class CategoryUpdateView(generic.UpdateView):
...
...
    def get_context_data(self, **kwargs):
        if 'formset' not in kwargs:
            kwargs['formset'] = DayInlineFormSet(self.request.POST or None, instance=self.object)



admin管理サイトでインライン表示させる


人によっては、管理画面でもこのインラインフォームセットを使いたいと思うかもしれません。
これは非常に簡単に作ることができます!

admin.py


from django.contrib import admin
from .models import Day, Category


class DayInline(admin.StackedInline):
    model = Day
    extra = 3


class CategoryAdmin(admin.ModelAdmin):
    inlines = [DayInline]


admin.site.register(Category, CategoryAdmin)
admin.site.register(Day)



admin管理サイトでの見た目


管理画面でインライン表示させた


admin.StackedInlineをadmin.TabularInlineに変えると...
class DayInline(admin.TabularInline):
    model = Day
    extra = 3



各フィールドが横一列に並びます。こちらも見た目にいいですね。
管理画面でインライン表示。1データを1列に
名無し 約20日前 2018年7月27日13:44 返信する
添付ファイルダウンロード(source.zip)
とりなさん、いつも拝見させていただいています。

本エントリー(Django、インラインフォームセットを使う)を参考に、自分でも簡単にインラインフォームを作成してソースコードを描いて見ましたが、うまくデータベースに情報が格納されません。
色々といじって見たのですが、その度にエラーの内容が異なってしまい、まるでデスマーチの世界に入ったようです。

お忙しいとは思いますが、本エントリーの範囲を超えないような簡単なソースコードになりますので、目を通していただきどこが悪いのかを教えていただけると幸いです。

なお、環境と目的は下記のようになります。

環境 : python3, Django2, mac high sierra
目的 : カスタムユーザーにPersonalityモデルをOneToOneで紐付けをし、Personalityモデルに則ったフォームを作成。Personalityモデルはnicknameフィールドしか持っていないため、ニックネームをフォームから登録すると、自動でカスタムユーザーテーブルに紐づいた形でPersonalityテーブルにnicknameが追加される仕様です。
カスタムユーザー : registerアプリのmodels.pyに独自に作成
Personalityモデル : testAppのmodels.pyに作成

ソースコードはファイルにまとめてアップいたします。

それではなにとぞ宜しくお願いいたします。
なりと 約20日前 2018年7月27日19:57
添付ファイルダウンロード(viewszip.zip)
インラインフォームセットでは、どのデータに紐づくか?を何処かで指定する必要があります。
ニックネームを入力しPersonalityデータが作成されますが、どのUserに紐づくかを指定しなければ上手く動作しません。

いただいたソースコードではpersonality.user = userという部分がありますが、
user_form = UserForm(self.request.POST)
が上手く動作しないため、if user_form.is_valid():は必ずFalseになってしまいます。
self.request.POSTにはニックネーム等のPersonalityモデルに関するデータしかなく、UserFormが求めている入力内容がないためです。

もしユーザー作成と同時にPersonalityを作成したいならば、記事内のビューを
Category→User
Day→Personality
DayInlineFormSet→PersonalitySetForm
と置換する感じで作っていただけると、動作すると思います。

もっと手軽な例として、Personalityを作成すると、現在ログイン中のユーザーに紐づく例のviews.pyを添付しています(zip解凍後に出現します)。ログインしていないとエラーが表示されますが、ログインしていれば問題なく動作します。
紐づく対象のデータが事前に分かっている場合、非常に単純に書けます。
get_formメソッドはフォームを返すためのメソッドで、フォームオブジェクトが欲しいときに内部で呼ばれるメソッドです。
get_success_urlメソッドはリダイレクト先のURLを返すメソッドです。なぜ上書きしているかというと、クラス属性のsuccess_urlの指定だけでは大人の事情でエラーになるためです。

難しいと思うので、関数ベースのviews.pyも添付しています。(views2.py,zip解凍後に出現します)
まずはこちらを試していただくほうが、わかりやすいかもしれません。
名無し 約19日前 2018年7月28日9:11 返信する
ありがとうございます!いただいたソースコードを研究したいと思います。
詳しい解説まで本当にありがとうございます!
名無し 約19日前 2018年7月28日9:45 返信する
とりなさん、できました!!すごいです!
ありがとうございます!!
名無し 約19日前 2018年7月28日11:49 返信する
とりなさん、すみません、2点だけ追加で質問をさせてください。
1点目、ときどき、

PersonalInfoFormSet(self.request.POST or None, instance = self.request.user)

のように、

instance = self.request.user

の部分がありあますが、この時点でフォームとログイン中のユーザー情報をひも付けしているのでしょうか?

2点目、初歩的ですが、、

forms.pyで指定した

PersonalInfoFormSet = inlineformset_factory(
parent_model = get_user_model(),
model = PersonalInfo,
fields = ['height', 'weight'],
extra = 1,
can_delete = False,
max_num = 1,
)

ですが、フォームのフィールドにform-controlクラスをつけるにはどうすればいいでしょうか。inlineformset_factoryの定義元の/site-packages/django/forms/models.pyで変数が用意されているかなどを探してみましたが、見つからず、、。

申し訳ございませんが、よろしくお願いします。。。
なりと 約19日前 2018年7月28日13:08
>instance = self.request.user
>の部分がありあますが、この時点でフォームとログイン中のユーザー情報をひも付けしているのでしょうか?

はい、request.userにはログイン中のユーザー、UserモデルやカスタムUserモデルのインスタンスが自動的に格納されます。
ログインしていない場合は、AnonymusUserという特殊なオブジェクトが入っています。

補足ですが、今回はOneToOneでPersonalInfoと紐付けていますので、
personall = PersonalInfo.objects.get...
personall.user
のようにPersonalInfoからUserにアクセスできるだけでなく、

user = User.objects.get...
user.personallinfo
のようにUserからPersonalInfoにもアクセスできます。

そのため、Userが格納されているrequest.userからも
request.user.personallinfo
としてPersonalInfoにアクセスできます。

>フォームのフィールドにform-controlクラスをつけるにはどうすればいいでしょうか。
fields = ['height', 'weight'],のかわりに、form = PersonallForm のようにしてフォームを指定できます。そのフォーム内で、form-controlをつけてください。PersonallFormは通常のModelFormとして定義できます。
名無し 約19日前 2018年7月28日21:19 返信する
なりとさん

諸々ありがとうございます。フォームについても無事思い通りにいきました!
大変勉強になります!!
名無し 約18日前 2018年7月29日20:39 返信する
添付ファイルダウンロード(source.zip)
とりなさん、お世話になっています。

昨日インラインフォームについて質問をさせていただいたものです。
さっそく自分で新しいテーマを考えて、別のアプリを作成してみました。
テーマは単純で、会員機能付きのアンケートフォームです。

しかし、いざ自分で実際に実装してみるとうまくいかないことが結構あり、再度ご迷惑をおかけしてしまいますが、ご質問をさせていただければと思います。

【仕様】
・カスタムユーザーのidに紐づける形で、アンケートテーブルを作成
・アンケートを入力したら、確認ページ経由でDBに登録をする

1点目
https://torina.top/detail/284/
を参考に、入力した情報をセッションを利用して確認ページ経由でDBに登録をしたいのですが、確認ページで入力された情報だけ出力されれば良いものが、修正可能なフォームの状態で出力されたり、確認ページにおける「送信」ボタンを押すと「TemplateResponseMixin requires either a definition of 'template_name' or an implementation of 'get_template_names()’」というエラーが表示されてしまいます。get_form()などの各メソッドの中をいじってみたりしたのですが、どうも思うように行きません、、。

2点目
チュートリアルの範囲を超えてしまい恐縮なのですが、今後アンケートフォームを3つまで増やし、3つ入力した後に3つまとめた確認ページの作成も考えています。この場合のテーブル設計は現状のようにカスタムユーザーに紐づける形で増やしていってもいいのでしょうか。

3点目
よくCreateViewなどを継承したクラスにmodel = Userなどユーザーをひも付けしているサンプルがありますが、たまに省略しているときもあります。これは必ず記述してモデルとひも付けをするものと思っていましたが、どういったときに省略をしてもよく、またいけないという事になるのでしょうか。


長くなり本当に恐れ入りますが、教えていただけると幸いです。
現状まで作成したソースも用意しておりますので、合わせてご確認いただければと思います。

それではどうぞよろしくお願いいたします。m(__)m
なりと 約17日前 2018年7月30日15:39
1店目
ProcedureConfirmのget_context_data内、
context['form'] = Procedure1FormSet(form_data)

context['form'] = Procedure1FormSet(form_data, instance=self.request.user)

としてください。
また、フォームセットをテンプレートへ渡す場合は、
{% for fm in form %}
{% for field in fm %}

{% endfor %}
{% endfor %}
のようにします。フォームセットは複数のフォームを扱うための機能で、[form1, form2, form3]のようなリストとしてテンプレートへ渡されます。フォームセット内のフォームが実際には1つでも、[form1]のようになっているため、{% for %}で取り出す必要があります。

>修正可能なフォームの状態で出力されたり
テンプレートの上側で{{ form }}としていたためです。
下側にかかれていた{% for field in form %}が動作していなかったため、入力欄のあるフォーム(上側の{{ form }})だけが表示されていました。

>送信」ボタンを押すと「TemplateResponseMixin requires either a definition of 'template_name' or an implementation of 'get_template_names()’」というエラー

ProcedureCompleteのget_form内
return self.form_class(form_data)

return self.form_class(form_data, instance=self.request.user)
としてください。
内部のform.is_valid()が失敗し、エラーのためテンプレートを再表示させようとしましたが、template_nameやモデルに対応するテンプレート名が見つからないためそのエラーが出ました。
instance=..とするとform.is_valid()に失敗することは基本的になくなるので、form.is_validがTrueになりリダイレクト処理で遷移できます。

今回のようにOneToOneで紐づく関係で、紐づく対象がrequest.userのようにフォームを作らずともすぐに取得できるならば、無理にインラインフォームセットを使わないほうがシンプルかもしれません。
なりと 約17日前 2018年7月30日16:10
3点目
基本的にはmodel属性を指定しておけば間違いありません。
CreateViewならばmodelを指定せずにform_classとtemplate_name属性を指定すればエラーにはならない、UpdateViewやDeleteViewならばmodel属性は必須、しかしいくつかのメソッドを上書きしていれば必ずしもそうではない、のように複雑な話になってきます。

form_classの有無になどに関わらず、CreateView, UpdtaeView, DeleteView等ではmodel属性をつけておくのが一番確実です。
なりと 約17日前 2018年7月30日16:25
2点目

大丈夫だと思います。
確認画面ですが、今回のように作成済みのユーザーと紐付けれるならばDBをに一度保存してしまう方法もあります。Userモデルのis_activeのような感じです。

アンケートのモデルにis_active = models.BooleanField(default=False)のようなフィールドを作っておき、
Procedure1Viewでis_active=Falseの状態で作成
ProcedureConfirmでis_active=False、紐付いたユーザーが自分のものを取得し、表示します。
ProcedureCompleteでis_active=False、紐付いたユーザーが自分のものを取得し、is_active=Trueでsaveし直します。

解答されたアンケートの一覧は、is_active=Trueなモデルを取得することで表示できます。
つばさ 約16日前 2018年7月31日13:03 返信する
なりとさん

udemyの方でもお世話になり、こちらのブログも参考にさせていただいております。
今回こちらの記事を参考にインラインフォームを作成しており、
extraを指定して表示させた新規の行に初期値を編集したいと考えているのですがうまく行かず、
可能であればサンプルコード等ご紹介いただけないでしょうか。

以上、お忙しいところ申し訳ありませんが、何卒よろしくお願い致します。
なりと 約15日前 2018年8月1日14:23
通常のフォームと同様に、initialという引数に指定できます。
DayInlineFormSet(self.request.POST or None, initial=[{'title': 'hello'}])
これはフォームセットの、最初のフォームのタイトルにhelloと入力されます。

DayInlineFormSet(self.request.POST or None, initial=[{'title': 'hello'}, {' title': 'world'}])
これは最初のフォームのタイトルにhelloが、2番目のフォームのタイトルにworldが入ります。
つばさ 約14日前 2018年8月2日8:40
なりとさん

迅速な回答いただきありがとうございました。
無事に実装することができました
ありがとうございました。
つばさ 約14日前 2018年8月2日19:24
なりとさん

申し訳ありませんが追加でもう一点教えてください。
get_context_data内で、inline_formのextraを動的に変更したいのですが、
form_setの引数としてどのように渡せば良いのでしょうか。

お忙しいところ申し訳ありませんが、よろしくお願いいたします。
なりと 約14日前 2018年8月2日20:11
get_context_data内で、formset = forms.inlineformset_factory...から定義してください。forms.pyに書いている記述を、get_context_data内に移動します。
つばさ 約13日前 2018年8月3日8:08
なりとさん

回答ありがとうございました。
無事に実装することができました。
いつもわかりやすい回答をありがとうございます。
名無し 約15日前 2018年8月1日12:10 返信する
とりなさん、お世話になっております。2018年7月29日20:39のものです。

お忙しい中、ご丁寧にありがとうございます。
いただいた修正点でうまく動作しました。大変助かりました。

また、いただいた回答についても何点か疑問点が出て来てしまいました、、。
他の方にも回答しないといけないと思いますので、お時間がある際にご回答いただければと思います。

>CreateViewならばmodelを指定せずにform_classとtemplate_name属性を指定すればエラーにはならない、UpdateViewやDeleteViewならばmodel属性は必須、しかしいくつかのメソッドを上書きしていれば必ずしもそうではない、のように複雑な話になってきます。

こちらはやはりそうでしたか。。いろいろといじって動作確認をしていたのですが、どうも挙動がオーバーライドするメソッドによっても変わってくるので、調べれば調べるほどわからなくなってしまっていました。


>instance=..とするとform.is_valid()に失敗することは基本的になくなるので、

CreateViewなどのクラスビューに対してmodel属性を設定する、またinstance=..について、自分の中で情報の整理がうまくできないのですが、自分の中では、例えばCreateViewの中のform_class=..でforms.pyで定義されたフォームクラスで、Metaクラスの中でmodelを設定しているのに、なぜCreateViewやinstance=..でまたモデルと紐付けをする必要があるのだろうと思っています。特にMetaクラスと(例えば)CreateViewのmodelsで異なるモデルを紐付けしてもエラーが出なかったと思います。その場合にどちらが優先されるとかあるのでしょうか。

>確認画面ですが、今回のように作成済みのユーザーと紐付けれるならばDBをに一度保存してしまう方法もあります。Userモデルのis_activeのような感じです。

すみません、ご提案までいただいた上に大変厚かましくて恐縮ですが、こちらに関してももう少し詳細に情報をいただけますでしょうか、、?
なりと 約15日前 2018年8月1日15:43
>特にMetaクラスと(例えば)CreateViewのmodelsで異なるモデルを紐付けしてもエラーが出なかったと思います。その場合にどちらが優先されるとかあるのでしょうか。

CreateViewでは、form_class引数があればそれが優先されます。
フォームを作成するために、form_classがあればそれを、なければmodel属性を見ながら自動でフォームを生成する、という処理をしています。

instance引数ですが、これは主に2つ役割があります。
1つはsaveメソッド呼び出し時(つまりデータの保存時)に、どのモデルと紐付いているかを指定するのに使います。イメージとしては
obj = form.save(commit=False)
obj.instance = self.instance # 紐付ける
obj.save()
といった処理です。今回でいえば、紐づくユーザーを指定して保存する際に使われます。

もう1つが、フォームセットの作成時です。既に何件かデータが保存されている場合、それらを更新したい場合もあるはずです。今回のアプリケーションで言うと、
/procedure/procedure1/
に訪れると、既に作成していた場合は入力済みになっているはずです。このように、作成済みのデータがある場合にそれの編集用フォームも作成します(UpdateViewのような感じです)。

さきほど気づいたのですが、Procedure1Viewのform_validでsuoer().form_validとしているのでこの時点でデータが作成されていました。正しくは、以下のURLのような感じです。
https://paiza.io/projects/0GCN_zRHHRYf8cKl3QDImA

今回ですが、セッションを利用しつつ、インラインフォームセットを使っています。また、インラインフォームセットをOneToOneで紐づくフィールドに使うケースも少しめずらしい(ForeignKeyで多数のデータを紐付けたい場合に使うのが最も多いです)なので、ちょっと複雑になっています。

>>確認画面ですが、今回のように作成済みのユーザーと紐付けれるならばDBをに一度保存してしまう方法も
こちらの方法を紹介しますので、簡単そうであればこちらを採用してみてください。
なりと 約15日前 2018年8月1日16:43
添付ファイルダウンロード(project_bcp.zip)
DBに一度保存する作りにしたプロジェクトを添付しています。修正箇所はpdfアプリケーション内の、views.pyやforms.py、models.py、templates以下です。

ある程度コメントをつけてはいますが、今までとかなり内容が違うため、質問などあればおっしゃってください。
名無し 約13日前 2018年8月3日12:00 返信する
なりとさん、ご回答いただきありがとうございます。

modelの優先順位、またinstance引数についてよくわかりました。ありがとうございます。

整理すると、
1 : forms.pyのMetaクラス内のmodel(form_class)
2 : CreateViewなどの汎用クラスビュー内のmodel
3 : instance引数

の3カ所でモデルと紐付けをする箇所があるというイメージでよろしいでしょうか?
また、その中でmodelの優先順位は、
form_class > CreateView内のmodelのことですが、
instanceでモデルを指定した場合の優先順位はどのようになりますか?

>さきほど気づいたのですが、Procedure1Viewのform_validでsuoer().form_validとしているのでこの時点でデータが作成されていました。正しくは、以下のURLのような感じです。
https://paiza.io/projects/0GCN_zRHHRYf8cKl3QDImA

上記のソースを貼り付けたところ、確認画面に飛ばなくなってしまいました、、。(笑)
また、すでにDB内に情報がある場合は、CreateViewはうまく実行されますが、新規の場合はCreateViewのところでは【TemplateResponseMixin requires either a definition of 'template_name' or an implementation of 'get_template_names()’】というエラーが出てしまうことに気づきました、、。
なりと 約13日前 2018年8月3日13:58
添付ファイルダウンロード(views.py)
> の3カ所でモデルと紐付けをする箇所があるというイメージでよろしいでしょうか?

一般的なCreateViewならば1と2の2箇所で、インラインフォームセットを使う場合は3が必要になります。
CreateViewのmodel属性とModelForm.Metaのmodel属性は同じものを指定し、
インラインフォームセットのinstance引数はinlineformset_factory関数のparent_modelと同じもの(正確にはparent_modelにはモデルクラスを、instanceには実際のインスタンス)を指定することになります。

>instanceでモデルを指定した場合の優先順位はどのようになりますか?
優先順は特に変わりません。instance引数は言い換えると「DB内のどのデータに紐づくか?」という具体的な指定で、インラインフォームセットの内部で
obj.instance = self.instance # 保存時

filter(instance=self.instance) # 更新用フォームを作る時
といった具体的な処理の際に使われている引数です。

>上記のソースを貼り付けたところ、確認画面に飛ばなくなってしまいました、、。(笑)
修正したviews.pyを添付しているので、こちらでお試しください。
名無し 約13日前 2018年8月3日12:00 返信する
>DBに一度保存する作りにしたプロジェクトを添付しています。修正箇所はpdfアプリケーション内の、views.pyやforms.py、models.py、templates以下です。

わざわざソースコードを用意していただきありがとうございます。
無理にクラスを利用せずにケースバイケースで関数を選択するとスッキリした感じになりますね。

拝見したところ、いただいたソースコードはis_activeを利用して【入力】→【確認】→【登録(処理のみ)】を実装したもので、アンケート数に応じてテーブル数を増えてしまうという今の設計を改善している訳ではないですよね、、?


あと、細かいご質問になるのですが、
クラスビューでよく使うオーバーライドされる下記関数についてです。

def get_form(self, form_class = None):
def get_success_url(self):
def form_valid(self, form):

1 : これらの実行順序などの決まりはありますか。
2 : 引数(例えばform_validのform)には自動で、各種データが入るという認識で大丈夫でしょうか。
3 : これらの返り値は、定義元の返り値の型に合わせる必要がありますか。
例えば、get_form()の定義元では、下記のようにフォームオブジェクトを返すようになっていますが、オーバーライドした場合は、同じフォームオブジェクトを返すことが必要でしょうか。

return form_class(**self.get_form_kwargs())

かなり長くなってしまいましたが、
大方いままで勉強してきたところで不明な点はほとんどなくなってきたと思いますので、
最後まで長文でご迷惑をおかけしますが、ご教授いただければと思います。

それではどうぞ宜しくお願い致します。
なりと 約13日前 2018年8月3日14:17
def get_form(self, form_class = None):
def get_success_url(self):
def form_valid(self, form):

1
GETメソッドでのリクエスト時は、この中だとget_formだけ呼ばれます。
POSTメソッドでのリクエスト時は、get_form→form_valid→get_success_urlです。ちなみにですがform_validで使われるform引数は、get_formで返ってきたフォームです。

2
その認識で問題ございません。1点注意があるとすれば、GETメソッド時のパラメータはフォームに紐付かないので、<form method="GET">内にフォームを置いて送信した場合は、ちょっと上書きする必要があります。

3
返す必要があります。
get_object → モデルインスタンス(Model.objects.get..のようにして取得したもの)
get_queryset → クエリーセット(Mode.obects.all()やfilter()のようにして取得したもの)

get_form → フォームオブジェクト
get_form_class → フォームクラス。get_formと違い、クラスそのもの
get_form_kwargs → 辞書
get_initial → 辞書

get_success_url → 文字列(URLを表す)

form_valid → HTTPResponseオブジェクト(renderやredirectなどが返すオブジェクト)
form_invalid → 上に同じ
get → 上に同じ
post → 上に同じ

get_context_data → 辞書

よく使うものだとこんな感じだと思います。

>アンケート数に応じてテーブル数を増えてしまうという今の設計を改善している訳ではないですよね、、?
改善していないです。テーブル数が増えるのがちょっと気になるならば、別の方法がありますので、そちらも紹介します。
名無し 約13日前 2018年8月3日18:28 返信する
なりとさん、迅速なご回答誠にありがとうございます。

>一般的なCreateViewならば1と2の2箇所で、インラインフォームセットを使う場合は3が必要になります。

こちらなのですが、instance属性はインラインフォームにしか使われないという認識でよろしいでしょうか。というのも、最近出版されたDjangoの本には、普通のModelFormを継承したクラスをインスタンス化する際にinstance属性を設定していたのですが、その書籍ではinstance属性が使われている理由が書いておらず、コメントアウトしても動作が変わらなかったので、instance属性を使う理由がいまいち分からなかったというのがあります。

>> >上記のソースを貼り付けたところ、確認画面に飛ばなくなってしまいました、、。(笑)
修正したviews.pyを添付しているので、こちらでお試しください。

こちら無事動作しました。誠にありがとうございます。

> >アンケート数に応じてテーブル数を増えてしまうという今の設計を改善している訳ではないですよね、、?
改善していないです。テーブル数が増えるのがちょっと気になるならば、別の方法がありますので、そちらも紹介します。

どのみちお聞きすると思いますので、大変恐縮ですが教えていただいてよろしいでしょうか、、?

いつもご丁寧に回答いただいきありがとうございます。
大変勉強になっております。(>_<)
なりと 約12日前 2018年8月4日15:26
添付ファイルダウンロード(project_bcp.zip)
通常のモデルフォームで使う際は、instance引数を使うと更新用のフォームになり、UpdateViewの内部でもintance引数を使ってフォームが作成されています。

新しいアンケートプロジェクトを添付しています。
「Questionnaire」というモデルは、いくつかの質問を集めた1アンケートを表すモデルです。「食べ物に関するアンケート」のように名前をつけて、ManyToManyで質問を紐付けます。
「Question」が質問です。「カレーは好きですか」のように入力し、質問を作ります。
この2モデルは、管理画面などからあらかじめ定義します。

「UserQuestionnaire」はQuestionnaireにに紐づくもので、ユーザーがどのアンケートに解答したかを表すのに役立つモデルです。
「Answer」はUserQuestionnaireと紐づくもので、ユーザーの解答1つを表すモデルです。どの解答に紐づいているかを知る必要があるため、Questionと紐付きます。

少し汎用的なアンケートをDjangoで作ろうとすると、方向性としてはこんな感じになると思います。これはシンプルな方法をとっていますがそれでも複雑になっているので、場合によっては元々のシンプルな作りのアンケートのほうが使いやすいかもしれません。
名無し 約10日前 2018年8月6日21:51 返信する
なりとさん、返信遅くなりました。

ソースコードを用意していただき誠にありがとうございます。
時間を作って研究したいと思います!

ありがとうございます!
T 約7日前 2018年8月9日12:29 返信する
なりとさん、お世話になっております。

先日教えていただいた、
1 : is_active()を利用する方法
2 : テーブル数が増えないよう対応した方法

の2つのソースコードを研究したところ、いくつか不明点が出て来ましたので
お忙しいとは思いますが、ご教授いただけると幸いです。

【1(is_activae)を利用した場合】
・新規pdf作成時には問題なく動作しますが、更新の際にはUNIQUE constraint failed: pdf_procedure1.user_idというエラーが表示されてしまいます。
恐らくユーザー情報との紐付けがうまく行っていないことから発生するエラーだとは思いますが、ソースコードを読んだ感じ、上手く行っている気もします。どこに原因があるのか教えていただけますででしょうか。
・get_object_or_404()はどのような動きをするのでしょうか。

【2を利用した場合】
 ・現状質問に対しての回答がテキストフィールドのみとなっていますが、これにチェックボックスなどを追加することは可能でしょうか?また、bootstrap4を使いたいのですが、フィールドにcssクラスを追加することは可能でしょうか。
 ・views.pyで下記コードが見受けられましたが、アンダーバー(_)はどういった使用方法になるのでしょうか。
 ・1の場合と異なり、is_activeの動きがいまいち分かりませんでした。例えば1の場合だと、明示的にis_active=Falseのオブジェクトを取得し、それに対して処理をしていましたが、2の場合ですと、最後の本登録時のみis_active=Trueにしているかと思います。なので、どこでis_active=Falseのオブジェクトのみを取得しているのかと思いました。
 
 以上となります。
 再度お手数をおかけすることになりますが、何卒よろしくお願いいたします。
なりと 約7日前 2018年8月9日21:40
>> ・新規pdf作成時には問題なく動作しますが、更新の際にはUNIQUE constraint failed: pdf_procedure1.user_idというエラーが表示されてしまいます。

procedure1_createビューでは
・仮登録状態のアンケートがなければ、新規作成
・仮登録状態のアンケートがあれば、それの更新
の2つに対応していますが、本登録状態のアンケートがあった場合の処理は考えていません。
もし本登録状態のアンケートがあっても、
Procedure1.objects.get(user=request.user, is_active=False) # 仮登録データしか取得しない
では取得がされないため、
form = Procedure1Form(request.POST or None)
での新規登録用フォームが作成されます。

Procedure1モデルは
user = models.OneToOneField(User, on_delete = models.CASCADE)
としているため、同じユーザーで複数のアンケートを作ることはできません。今回出ているエラーはこれが原因で、2個目のアンケートを作ろうしたのが原因です。

簡単に解決するならば、本登録のアンケートが既にある場合に「本登録したアンケートの更新用ビュー」(例えばprocedure1_update)にリダイレクトさせる、などができます。

本登録のアンケートが既にあるか?という判断は
Procedure1.objects.get(user=request.user, is_active=True) のように判断ができますので、procedure1_createの最初あたりにこのコードを挿入し、既に存在していればprocedure1_updateにリダイレクト、といった形になると思います。

procedure1_create内に全ての処理を書くこともできなくはないと思いますが、別のビューで定義するほうが見やすく、管理しやすいと思います。

>> ・get_object_or_404()はどのような動きをするのでしょうか。
get_object_or_404(Procedure1, user=self.request.user, is_active=False)
user=self.request.user, is_active=FalseなProcedure1を取得しようと試みて、見つからなかったら404ページを見せるという処理です。
今回の例ではあまり親切ではないし適切ではない気がしてきましたが、非常によく使う関数です。

Hoge.objects.get...は、データが見つからなかった場合に500エラーになります。状況にもよりますが、多くの場合は404エラーとして返すほうがHTTPのルール的にも適切です。

このブログでいうと、/detail/100000 のようにしたとき、Post.objects.get(pk=100000)というコードを書いてしまうと、その記事が既に消されていたり存在しないpkの値の際は500エラーが出ます。しかし、これは404のほうが適切です。
try〜exceptで、データが見つからなかったら404ページへという処理をいちいち書くのは面倒なので、それを1行で行うための処理がget_object_or_404です。
なりと 約7日前 2018年8月9日22:14
>  ・現状質問に対しての回答がテキストフィールドのみとなっていますが、これにチェックボックスなどを追加することは可能でしょうか?また、bootstrap4を使いたいのですが、フィールドにcssクラスを追加することは可能でしょうか。

可能ですが、ちょっと辛い作業になります。

> また、bootstrap4を使いたいのですが、フィールドにcssクラスを追加することは可能でしょうか。
forms.inlineformset_factory(UserQuestionnaire, Answer, fields=('text',)

forms.inlineformset_factory(UserQuestionnaire, Answer, form=AnswerForm
のように、fields引数のかわりにform引数を使って自作のフォームが指定できますので、自作のフォーム内でcssクラスを追加できます。


> ・views.pyで下記コードが見受けられましたが、アンダーバー(_)はどういった使用方法になるのでしょうか。

get_or_createメソッドは、
(取得 or 作成したモデルインスタンス, 作成したかどうかのフラグ)
というタプルを結果として返します。
answer, created = Answer.objects.get_or_create...
のように書いてもいいのですが、今回の処理ではcreatedは使いませんでした。こういった場合、その変数を使わないことを示すのに「_」という変数名が使われます。

他に代表的な例だと、こんなコードがあります。
max_num = 10
[None for _ in range(max_num)]
max_numの大きさのリストを、初期値Noneで作りたいというケースです。rangeから取り出した値(通常はiなどの変数名)は使わないので、「_」としています。

> どこでis_active=Falseのオブジェクトのみを取得しているのかと思いました。
失礼しました、is_active=Falseの処理を付け忘れていました。
Answer.objects.get_or_create(user_questionnaire=user_questionnaire, question=question, is_active=False)としてください。
本登録のアンケートがあった場合の処理は、上でお伝えした内容と同様です。
T 約3日前 2018年8月13日22:09 返信する
なりとさん

お世話になっております。

いろいろと細かい部分までご丁寧に対応いただき誠にありがとうございます。
またわからないことが出てきましたらお伺いするかと思いますが、
何卒よろしくお願いいたします。m(_ _)m