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を変更します。

 

python 変数の型を確認する

isinstance関数を使う。

isinstance(変数, 型) の戻り値がTrueなら型があっている、Falseなら型が違うということになる。

主な型一覧
型名 説明
bool 真偽値型
int 整数型
float 小数型
complex 複素数型
list list型
tuple タプル型
range range型
str 文字列型
bytes バイト型
set 集合型
frozenset イミュータブルな集合型
dict 辞書型
使用例

 

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に以下のように追記します。

 

参考サイト)

 

Django Migrate時に「The row in table ‘app_fuga’ with primary key ‘*’ has an invalid foreign key: app_fuga.hoge_id contains a value ‘*’ that does not have a corresponding value in app_hoge.id.」の解消

エラーの内容

makemigrationsまでは通るのですが、その後migrateしようとすると、以下のようなエラーに遭遇するときがあります。

このエラーは、開発環境でモデルを追加したり変更したりmigrateしたりを繰り返していると出ます。

直訳すると、

となり、つまり「子テーブル(app_fuga)の中に、親テーブル(app_hoge)の外部キーらしきIDの値があるけど、そんなIDを持つレコードはapp_hogeにはねーよ!」ということらしいです。

ここでは、app_hoge が親テーブル、app_fuga が子テーブルなので、app_fuga の hoge_id というカラムが、親テーブルのIDを参照する外部キーです。

このカラムに例えば「1」という値が入っていたとすると、app_hogeの中に、IDが「1」のレコードは存在しないと言っていることになります。

DBの中身を見てみる

一応確認します。

?結果がありません。どゆこと?

つまり、hoge_idが「1」となっているレコードが無いのにもかかわらず、「あるし!」と言われているので、訳が分かりません。

結論から言うと、おそらく「app/migrations」に、過去のモデル修正の繰り返しで作成されたマイグレーションファイル(000*_auto_YYYYMMDD_TT.pyとか)が蓄積されている状況で、これらが悪さをしているようです。

新しいマイグレーションファイルは、さらに過去のマイグレーションファイルを参照し、変更の履歴を見てコンフリクトを解消しようとします。

つまりその流れの中で、過去のマイグレーションで行ってきた操作ができなかった時、例えば前提とされているテーブル構造が実際には変わっていたり、あるはずのレコードが削除されてたりすると、途中経過でエラーとなる場合があるみたいです。

今回はおそらく、モデルに新しくデフォルト値などを張った都合で整合性が取れなくなったのかと思います。

解決方法

開発環境なので、手順なんていちいち記録していないことも多いでしょうから、どこで狂ったのかなんてわかりません。

そういう時はマイグレーションファイルを削除するのが手っ取り早いのですが、この場合、全てのアプリのマイグレーションファイルを削除してはいけません。全て削除すると、また同じ結果になります。

おそらくmakemigrationsの順番が関係しているのでしょう。

ポイントは、エラーが出ているテーブルのモデルを定義しているマイグレーションファイルをだけを削除します。

今回は「app_fuga」でエラーが出ているので、「app/migrations」の中の、__init__.py以外を全て削除します。

こうすることで、再度makemigrationsしたときに、どうやら変更履歴を無視して、現在のモデル構造だけを反映したマイグレーションファイルを作ってくれるようです。

無事にmigrateが通りました。