Django モデルフォームを使わずにPost値を保存する

モデルフォームはとても便利ですが、すこし複雑な処理をするときはどこまでカスタムするかという問題も出てきます。

カスタム沼にはまり込んでしまうより、むしろモデルフォームを使わない方が楽な時があります。

以下は、チェックされた価格が設定されていない本に、まとめて同じ価格を設定するときの例です。

 

同じnameの複数パラメタは、request.POST.getlistで受け取ることができます。

 

参考サイト)

DjangoでGET/POSTから値を取得する方法

 

DjangoでSumの集計結果がNoneだった時に初期値を0にする

テーブルの一部のカラムの値をSumしていると、集計対象のレコードが存在しなかった場合、Sumの結果には通常Noneが返されます。

ここで処理が終わりであればよいのですが、その後そのSumした集計値を使っていろいろ計算を続ける場合、数値として認識されないため、その後の計算の結果もすべてNoneになってしまいます。

なので、もしSumした結果がNone(合計する行が存在しない場合も含め)であれば、0としておくと都合が良いことになります。

Coalesceを使います。

ポイントとしては、annotateが連続する場合、最後のannotateの中でCoalesceを使います。

annotateが連続しない場合、最初のSum時に、

とやってもOKです。

 

参考サイト)

Django “sqlite Invalid connector for timedelta: /”を何とかする

クエリセットで、DurationFieldの値を、掛けたり割ったりしたものを取得したい時があります。たとえばチームの作業で1人当たり何分かっかったのか、とか、1mあたり何分くらいかかっているのか、とか。

しかし、以下のようなクエリセットを構築し、レンダリングしようとすると怒られます。

“sqlite Invalid connector for timedelta: /” と言われてまして、どうやらSQLiteではtimedelta形式の値(Durationフィールド)をそのまま除算できないようで、”/”が不正なコネクタとして認識されてしまいます。

DjangoのDuration型は、SQLite3ではbigint型となります。(Djangoのフィールド型とDBの型の組合せはこちらを参照)

MySQLなどでは大丈夫とのことですが、SQLiteではバグか何かしりませんが、同じintなのに、bigint型を直接計算できない模様。

いろいろ試した結果、Djangoで型を合わせればイケルというこうとがわかったので、今回はDurationを、割る方のintegerに合わせます。

クエリセット作成時に、Castしてoutput_fieldを指定してやればOKです。Durationをintegerに変換すると、100万分の1秒が整数1位で返るので、秒単位にするには1,000,000で割ってやります。

これで、テンプレート側では、以下で取得できます。

 

 

 

 

Django モデルフォーム利用時に独自のバリデーションを追加

例として以下のようなモデルがあったとします。

この場合、モデルフォームを使ったフォームでは、値が空欄の時はエラーとなりますが、0の場合はOKとなります。

0より大きい値でなければ受け付けない場合は、モデルにバリデーションを設定します。

 

Django 1対多(one2many)でリレーションされた子テーブルの集計値を取得する

外部キーによって子テーブルが親テーブルのIDを参照している場合に、親テーブルのオブジェクトから、その子テーブルの任意カラムの合計値を取得するサンプルです。

以下のようなモデルを想定します。

 

なお、子テーブルのデータを単純に表示したいだけであれば、テンプレート変数「子テーブルのモデル名_set」だけで事足ります。

例えば、views.pyで、

としていれば、テンプレートでは、

のようにすることで可能です。

このとき、子テーブルの一部のカラムの値を合計(この場合は”defective_weight”)して取得したい場合は、”子テーブルのモデル名__カラム名”で子テーブルにアクセスできます。

テンプレートは以下のようになります。

 

ただし、複数の子テーブルをリレーションした場合は、計算が合わなくなります。例えば以下のようなモデルです。

先ほどと同じやり方でクエリセットを構成すると、

となりますが、これだと計算が合わなくなります。なぜなら、テーブル結合の方法がLEFT OUTER JOINとなるからです。

不良カテゴリ1と不良カテゴリ2の対象レコード数が異なっていた場合、結合後のテーブルの行数が、レコード数の多い方のテーブルに合わせた行数となります。

したがって、不良カテゴリ1の行数が4, 不良カテゴリ2の行数が2だった場合、不良カテゴリ2をSumしたときに、4行分を合計してしまいます。

これを避けるにはINNER JOINするべきなのですが、ORMではなるべく生のSQLは触りたくありません。

これを手っ取り早く解決するにはサブクエリを使い、子テーブルを個別に参照して計算し、結合します。ただし多少重くなります。

 

いずれにせよ、このようにしておけば、filterで歩留りの大小順に集計したりもできますので、便利です。

 

参考サイト)

 

Django 1画面に種類の違う複数のformsetを共存させる

複数の単体formを共存させるやり方はネット上にいろいろありますが、複数のformsetを共存させる情報がなかったので、書いてみます。

少し冗長ですが、今回もviews.pyの中でやります。汎用性があれば、forms.pyにまとめたり、クラス化したりするのが良いかと思います。

まず、formsetの基となる2種類のフォーム情報を、forms.pyに書きます。ModelFormを使います。(models.pyは割愛します。)

 

views.pyの中で、フォームセットを定義します。処理が複雑になるので、今回もクラスviewは使わず、関数viewで書きます。

ポイントは、保存時にフォームセット数を調整してあげることです。

なぜかというと、複数のフォームセットをテンプレートに出力すると”form-TOTAL_FORMS”というhiddenフィールドがセットの数だけ複数出力されるため、単純にPOSTで受け取っただけだと、いずれかが無効になってしまいます。

その結果、form-TOTAL_FORMSの数と実際にPOSTしたフォームの数が合わずに、バリデーションエラーとなります。(※すべてのフォームセットへの入力数が同じ数であれば、この心配はいりません。)

この対策として、POSTされたデータを一旦コピー&form-TOTAL_FORMSフィールドの値を上書きし、フォームセットオブジェクト生成時に上書きしたPOSTデータを引数として渡します。

 

テンプレートは、ただフォームセットを埋め込むだけです。なお、crispyを使っていると、formタグが勝手に生成されてしまうので、この場合は使わない方が得策です。

 

参考サイト)

Django 1画面に同じフォームを複数表示させる

formsetを使います。

ネット上では、forms.pyの中でフォームセットを作っているパターンの紹介が多いですが、今回はviews.pyの中でやります。

汎用性があるものは、forms.pyにまとめたり、クラス化したりするのが良いかと思います。

まず、forms.pyに普通のフォームを書きます。ModelFormを使います。

 

views.pyの中で、フォームセットを使いテンプレートにレンダリングします。処理が複雑になるので、クラスviewは使わず、関数viewで書きます。

保存時に一括でsave()がうまくいかない時は、transactionを使用し、フォームセットのPOST値をループで回してsave()するとできます。(本当はbulk_createを使いたかったのですがうまくいきませんでした。)

 

テンプレートは、ただフォームセットを埋め込むだけです。

 

参考サイト)

Django 管理画面のアプリ名を任意の名前(日本語)に変更する

自前で管理画面を用意しない場合や、多言語対応を行っていない場合、Djangoの管理画面ではデフォルトでアプリ名がそのまま英字で表示されます。

具体的には、setting.py で利用するアプリを指定しますが、その指定した名前が表示されます。

この部分を変更する(日本語にする)場合は、そのアプリのapps.pyをDjangoに認識させる必要があります。

apps.pyでverbose_nameを指定します。

apps.pyを介してアプリを呼び出すようsettting.pyを変更します。

 

Django 記録時刻を10分単位や15分単位で丸める

出退勤記録をつける際に、例えば8:12に出社したら、出勤時刻は8:15として、15分刻みで実働時間や時給計算するというのはよくあります。

逆に退勤時は、15:12に退勤したら、15:00と記録したいですね。

特に丸めずにそのまま記録して、集計時に丸めてもよいのですが、djangoだと集計時にいろいろ面倒なので、最初から丸めて記録しておくと良いかと思います。

主にformの機能を使ってDBに記録するので、記録する際にviews.pyのform_validをいじります。

以下は出勤時の例です。

 

退勤用のフォームを作る場合は、form_validで以下のようにします。

 

なお実際には、出勤した直後に(15分もたたずに)すぐ退勤した場合、上記の例だと出勤時刻と退勤時刻が逆転してしまいますので、もし丸めた退勤時刻が出勤時刻より早くなってしまったら出勤時刻と同じにするなどのイレギュラー処理が必要となります。

参考サイト)

https://www.it-swarm.dev/ja/python/%E6%97%A5%E6%99%82%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%88%86%E3%82%92%E4%B8%B8%E3%82%81%E3%82%8B%E6%96%B9%E6%B3%95python/969167474/

Django FilterViewを使って作成した検索フォームのラベル名変更

FilterView(filters.py)を使うと、検索フォームに表示される検索項目の名前(ラベル名)は、モデルで定義した名前になります。

例えば、モデルに、

と書いてあれば、verbose_nameの値(出勤日時)になります。verbose_nameが無い場合はモデル名(attendance_at)になります。

このラベル名を変更する場合は、filters.pyに以下のように追記します。

 

参考サイト)