###はじめに
前回の投稿では、Djangoの簡易サーバーを起動させて、文字列の表示をするWEBアプリケーションの作り方を紹介しました。内容はDjangoに興味がある方を対象にしていますが、その構造に親しんでもらえたのではないでしょうか。
システムというものは、一度構築してしまえばメンテナンスの大変さは無いと思うのですが、開発環境とか運用環境を作るのは結構大変だと思います。ここでは簡易サーバー環境でDjangoを紹介しておりますが、その構造などを習得するという意味においては特に問題は無いと思います。
さて、今回の投稿ではDjangoの真骨頂とも云えるModelを使ってデータベースの操作を簡単に解説します。
引き続き、前回の投稿(https://qiita.com/cloghjordan/items/1c6e691062528a2235cd)で記述した
ファイルを使用します。
###1.モデルの設計
今回はお店という設定で、簡単に商品マスタと商品の受注明細を作成するアプリを構築します。
モデルは以下の内容です。
関係性としては、一対多の関係です。関係性については面倒くさいところもあるので、それぞれ独立して関係性なしのテーブルにしたとしても問題無いのですが、解説なのでここはキチンと関係性をもたせます。
####ⅰ.モデルを作成する。
【items】フォルダの直下にmodels.pyがあります。モデルの記述は、この中で行いますのでファイルを開いて編集しましょう。
from django.db import models
# Create your models here.
初めはこれだけですのから、自分なりに描くモデルを記述していきます。早速先ほどに設計したモデルを記述します。
from django.db import models
# Create your models here.
#-----------------------------
# 品目モデル
#-----------------------------
class Item(models.Model):
品目CD= models.IntegerField(primary_key= True)
品目名= models.CharField(max_length= 40)
価格= models.FloatField()
def __str__(self):
return self.品目名
#-----------------------------
# 受注明細モデル
#-----------------------------
class Order(models.Model):
class Meta:
db_table = '受注明細' # salesテーブルという名前付けする
日付= models.DateField()
品目CD= models.ForeignKey(Item, related_name='order', on_delete= models.DO_NOTHING)
品目名= models.CharField(max_length= 40)
数量= models.FloatField()
価格= models.FloatField()
金額= models.FloatField()
摘要= models.CharField(max_length=200)
まず、モデルはclassとして記述します。そしてそのclassの中でデータについての項目定義や場合によっては関数(メソッド)を追加して纏まりを持たせます。
モデルとなるクラス名や各項目名を日本語表記することもできるので、ここではそのようにしました。
ところで、今回はデータベースは標準装備のSQLiteを使用しています。DjangoはSQLite以外にもMySQLやPostgreSQLなどもサポートしています。もしデータベースを変更したい場合には、多分特有の「準備」が必要なこともありますが、settings.pyの設定を変えることでそれらのデータベースに変更することができます。下記はsettings.pyに初めから記述されている内容です。小規模なアプリであればSQLiteで十分だと思います。
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
SQLiteについては、便利なツール(DB Browser for SQLite https://sqlitebrowser.org/)がありますので紹介します。デーブルやその内容を確認するときなど便利だと思います。
データ内容を確認する程度であれば後で解説するDjangoの管理画面も行うことが可能ですが、このツール色々と便利ですので、この解説では使っています。
少し横にそれました。記述したモデル(2つのclass)に話を戻しましょう。
####ⅱ.品目モデルから分かること。
モデルとしてのクラス名はItemにしました。そしてこのクラスは、models.Modelを継承しています。このことで色んな恩恵が受けられます。
まず主キーについてなのですが、品目CDを主キーに設定しています。
品目CD= models.IntegerField(primary_key= True)
primary_keyをTrueにすることで「品目CD」が主キーになります。もしprimary_keyの宣言がなければ、djangoが自動で追加する「id」項目がこのモデル(テーブル)の主キーになります。
つぎにこのクラスには関数がついています。
def __str__(self):
return self.品目名
これは、Djangoの管理画面にモデル表示させるときに「わりやすくなる記述」だと理解しています。
管理画面については後ほど見ますのでお忘れなきように。。。
あとの記述は問題ないと思います。品目名を40の文字型に、価格を浮動小数点型にそれぞれ定義しています。
####ⅲ.受注明細モデルから分かること。
モデルとしてのクラス名はOrderにしました。そしてこのクラスもまた、models.Modelを継承しています。
まず主キーについてなのですが、ここでは主キーを記述していません。この場合は品目モデルのところで解説したように、Djangoが勝手に「id」という主キーを追加してくれます。
続いての記述内容ですが、ここではデータベースのテーブル名を設定しているものだとご理解下さい。この2行別に必要ではありませんが、どの様に見えるかは後で見るテーブル内容から明らかになります。
class Meta:
db_table = '受注明細' # salesテーブルという名前付けする
あとの記述は項目の定義です。データ型を宣言していますが「品目CD」項目だけ解説します。
品目CD= models.ForeignKey(Item, related_name='order', on_delete= models.DO_NOTHING)
簡単に言うと、「品目CD」項目をItemモデルの主キー(品目CDのこと)と関係させて外部キーとさせるための宣言です。ForeignKey()の2つ目のパラメータである「related_name」ですが、Itemモデルから逆引き参照するときに使うようにします。3つ目のパラメータである「on_delete」は、関係性を張ったItemテーブルにおいてデータが削除されたときにOrderモデル側のデータをどうするのか、という挙動を定義しています。今回は、品目マスタのレコードが削除されたとしても受注明細は残しておきたいので「models.DO_NOTHING」としておきます。
###2.モデルをデータベースに移行する。
さて、いよいよ定義されたモデルをデータベースへ反映させます。
####ⅰ.モデルをデータベースにする。
久しぶりのコマンド作業です。流れとしては、makeminrationsをしてその後migrateを事項する順序です。
カレントをプロジェクトフォルダに変えてコマンド実行して下さい。
>> cd mysite
mytite>> python manage.py makemigrations
Migrations for 'items':
items\migrations\0001_initial.py
- Create model Item
- Create model Order
コマンドの結果、ItemモデルとSalesモデルをCreateした旨の応答が得られます。
Djangoは「python manage.py makemigrations」の命令でモデルに記述される内容に基づきデータベースに発行する記述などを作成します。したがってこの指示だけでは未だデータベース(ここではSQLite)に影響しません。Djangoが作業した内容は、【items】フォルダの中にある【migrations】フォルダに反映されています。
因みに、設計変更に依ってモデルが変更された場合など、makemigrationsを何度も行うことが可能です。また、アプリが増えるときプロジェクト全体は無くて特定のアプリモデルだけ実行することが可能です。
python manage.py makemigrations items
このことは、このあとのmigrateでも同じことが言えます。
それでは、データベースに定義を反映させましょう。
mysite>> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, items, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying items.0001_initial... OK
Applying sessions.0001_initial... OK
「python manage.py migrate」をした結果、たくさんの応答が帰ってきましたが、itemsアプリに
関係するところは、最後の2行だけです。Djangoに初めてmigrateをすると基本的なテーブルをベータベースへ反映させます。また、この基本的はデータベースが出来れば、Djangoが用意する管理画面に入る準備も整ったと思って下さい。
####ⅱ.データベースを覗いてみる。
Djangoがデータベースにモデルをテーブル化してれましたので、先に紹介した「DB Brouser for SQLite」で確認してみたいと思います。プロジェクトフォルダ【mysite】の直下にdb.sqlite3というファイルがありますのでツールから開いてみて下さい。これまでのことが分かると思います。
2つのテーブル名はDjangoのモデル名と異なりますが、Django内では一貫してその名前はItemでありOrderなのでご留意下さい。
###3.Django管理画面の紹介
ここで、Djangoの管理画面を紹介します。管理画面からデータの確認やユーザの管理と権限設定などを行うことが可能です。こうした機能もDjangoがフルスタックの全部入りWEBフレームワークと言われる所以です。
コマンドラインからDjangoの簡易WEBサーバーを起動してみてください。
mysite>> python manage.py runserver
その後、WEBブラウザのアドレスに「127.0.0.1:8000/admin」を入力してページ表示させると次の画面が表示されるはずです。
どうでしょう、表示されましたでしょうか。環境によっては綺麗に表示されないかもしれませんが、こんな感じで表示されればOKです。
ここでは、ユーザー名とパスワードが求められています。何を入力しましょうか。。。
そうです、まだ初めのユーザーを登録していないのです。したがって初めのユーザー(スーパーユーザー)の登録をしましょう。
簡易サーバーが待機中の場合は、[ctrl+C]でWEBサーバーを停止してから、スーパーユーザー登録の為のコマンド入力をします。
>> python manage.py createsuperuser
ユーザー名 (leave blank to use 'XXXX'):
コマンド入力すると、Djangoはユーザー名を求めてきます。だから初めのユーザー名を返してやします。
mysite>> python manage.py createsuperuser
ユーザー名 (leave blank to use 'XXXX'): admin
メールアドレス: hoge@hoge.co.jp
Password:
Password (again):
Superuser created successfully.
ユーザー名の後で、メールアドレス、パスワードを欲しがりますから、それらを与えます。
ユーザー登録が済んだら、簡易WEBサーバーを起動してみましょう。
mysite>> python manage.py runserver
そしてまた、WEBブラウザのアドレスに「127.0.0.1:8000/admin」を入力してページ表示させます。管理画面が表示されたら、先ほど作成したユーザー情報を入力して「ログイン」しましょう。
無事管理画面に入ることができたらログイン成功です。
ユーザーに関する事が「認証と許可」として表示されています。
管理画面ではモデルの表示もできるので、折角ですから管理画面に作成したモデルを表示させます。
アプリフォルダである【Items】フォルダの直下にadmin.pyが有るので開いてみて下さい。
from django.contrib import admin
# Register your models here.
このファイルを下記の様に追記します。
from django.contrib import admin
from .models import Item, Order #<-追加
# Register your models here.
admin.site.register(Item) #<-追加
admin.site.register(Order) #<-追加
ITEMSアプリのItemモデルとOredrモデルが追加されていると思います。しかも、右の方を見るとモデルにデータの追加が出来ような感じになっています。なので早速Itemのデータを登録しましょう。
「+追加」をクリックしてその処理を実行して下さい。
「保存」ボタンで登録完了となります。
続いて受注明細(Orders)も一件登録してみましょう。Itemのときと同じ様に「+追加」をクリックして下さい。
そうです、Djangoの管理画面はモデルのデータ型を知っているためその方に合わせてインプットのコントロールを変えてきます。そしてそうです、外部キーとした「品目CD」項目は、Itemモデルの中からデータ選択させる様に機能しています。更にそうです、私も初めこの選択する項目はItemモデルの「品目CD」、つまり「ORENGE」では無く「100」を予想していました。でも思い出してください。
Itemモデルに追加した記述を。
def __str__(self):
return self.品目名
その訳はもうおわかりだと思います。そうです、Djangoはちゃんと覚えているのです。
Itemモデルについては、「品目名」がそのデータの名前です。
その違いは、ItemモデルとOrderモデルのデータ一覧からもわかります。
物事は比較して眺めてみるとその違いから、それぞれが持つ意味合いがわかるのだと思います。
それでは、データベースの側から見るとどうなっていますか。Djangoは事情を知っていますが、SQLiteさんはどう見ているのでしょうか?
受注明細(Order)レコードにはキチンと外部キーとして品目マスタ(Item)の品名ではなく、品目CDを保持しています。
このあたりでDjango管理画面については終わりです。
なお、管理画面まわりは、色々カスタマイズもできるらしいです。
###4.データの操作
管理画面からそれぞれのモデルのデータを1件ずつ登録しました。
今度はプログラム側(views.py)からモデルを扱って行こうと思います。
まず、ページ画面に品目マスタの一覧表示をして、品目マスタに対する登録(更新)、削除ができるようにします。続いて、受注明細を使って品目の受注登録ができるようにしたいと思います。ページ展開やデザインなど乱暴はところもありますが、ご勘弁下さい。
####ⅰ.設計ミスへの対応。
突然ですが、ここでお詫びと訂正があります。「1.モデルの設計」のところで受注明細の品目CDを外部キー扱いにしました。
品目CD= models.ForeignKey(Item, related_name='order', on_delete= models.DO_NOTHING)
この行のパラメータ指定「on_delete= models.DO_NOTHING」は品目マスタ側で削除されても受注明細側のレコードはそのままにしておく意図を持って指定しました。しかしSQLiteにおいはこの指定で、品目マスタのレコード削除を行うと「FOREIGN KEY constraint failed」という「外部キーの制約」というエラーで対抗してきます。SQLiteの設定で「外部キーの制約」を無効化する術も有るのかもしれませんが、ここは抗うことはせずに「on_delete」の指定を変更します。品目マスタのレコードを削除するとき、受注明細で参照がある場合は、その品目マスタのレコードの削除はさせない様に変更です。
品目CD= models.ForeignKey(Item, related_name='order', on_delete= models.PROTECT)
ちなみに、「on_delete」で指定する値は幾つかあるようなのでそれらを列挙します。
CASCADE : 参照するオブジェクトも削除する。
PROTECT : 参照があるばあい、削除を許可しない。
SET_NULL : NULLをセットする。
SET_DEFAULT : 初期値を決めてセットする。
SET(..) : ある値をせっとする。
DO_NOTHING: 参照を続けるが、データベースから外部キーの制約を受ける。
実際にDjangoの管理画面からその動きを確認してみても良いかもしれません。
ついでなのでもう一箇所訂正をさせて下さい。受注明細の「摘要」項目なのですが、いまのままだとテキスト入力が必須になってしまうため、下記のように変更させて下さい。
摘要= models.CharField(max_length=200, blank=True, null=True)
「blank=True」にすると入力のときに必須入力である旨のエラーが回避できます。また、「null=True」にすると、データの値が無くてもレコード登録を許可してくれます。
一部変更を加えたところで、再度マイグレートします。再マイグレートをすれば、Djangoはモデルに変更を加えて、データベースの再構築を行ってくれるので助かります。
mytite>> python manage.py makemigrations
items\migrations\0002_auto_20191213_1246.py
- Alter field 品目CD on order
- Alter field 摘要 on order
これでモデルの変更が受け入れまれました。続いてその変更をデータベースに反映させます。
mytite>> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, items, sessions
Running migrations:
Applying items.0002_auto_20191213_1246... OK
これで、設計ミスへの対応が出来たと思います。
改めてmodels.pyを掲載し直します。
from django.db import models
# Create your models here.
#-----------------------------
# 品目モデル
#-----------------------------
class Item(models.Model):
品目CD= models.IntegerField(primary_key= True)
品目名= models.CharField(max_length= 40)
価格= models.FloatField()
def __str__(self):
return self.品目名
#-----------------------------
# 受注明細モデル
#-----------------------------
class Order(models.Model):
class Meta:
db_table = '受注明細' # salesテーブルという名前付けする
日付= models.DateField()
品目CD= models.ForeignKey(Item, related_name='order', on_delete= models.PROTECT)
品目名= models.CharField(max_length= 40)
数量= models.FloatField()
価格= models.FloatField()
金額= models.FloatField()
摘要= models.CharField(max_length=200, blank=True, null=True)
####ⅱ.品目マスタの登録(更新)、削除について。
再びミスが起きないことを願いつつ、気を取り直して続けてゆきたいと思います。
そして、このテーマは少し長くなりそうなのですが、どうぞよろしくお願いいたします。
データ更新に先立ってデータ表示を行いたいのですが、いかがでしょうか。
前回の投稿では、過激だけど憎めない店員さんのご挨拶を表示させるViewを作りました。今度はSQLiteのデータを表示させるViewを作ります。そのために、itemsアプリのurls.py、views.pyに手を加えて専用のテンプレート(html)を追加します。
まずはurls.pyからです。
# -*- coding: utf-8 -*-
from django.urls import path
from . import views
app_name = 'items'
urlpatterns = [
path('', views.index, name='index'),
path('item_list', views.item_list, name='item_list'), #<-追加します
]
三つの要素をlist_viewという名前で統一しました。
次にviews.pyに新しいviewを書き加えます。
#---------------------------------------------------------------
# Itemの一覧表示
#---------------------------------------------------------------
class ItemListView(ListView):
model = Item
item_list = ItemListView.as_view()
この新しいクラスは、ListViewを継承しています。したがってListViewの継承が可能となる様に冒頭の一文も追記しなくてはなりません。
「model=Item」のmodelはListViewから継承しているオブジェクトです。Itemは品目マスタのモデルを意味しています。「このクラスで扱うモデルはItemです」ということを明示します。
継承されたものが隠れているだけだから当たり前なのかもしれませんが、Djangoが謎めいて思えるのはこうした「隠し事」が多いからだと思います。たった3行だけですが、どうやら余白部分には必要な機能がたくさん詰まっている様です。こんなこと言っていると「もっとドキュメント読め!」とキツイひと言を浴びせられそうで、そんなに怒らないで下さいね。
それにしてもこれだけでテンプレート(html)に必要なデータが届くのでしょうか、お楽しみです。
viewではモデルを扱いますから、models.pyに書かれているItemとOrderも参照できるようにviews.pyにそのことを宣言します。
# -*- coding: utf-8 -*-
from django.shortcuts import render
from django.views.generic import TemplateView, ListView #<-ListViewを追加した
from .models import Item, Order #<-モデルを追加した
続いて、テンプレート(html)です。【items】フォルダの中の【template】フォルダ、更にその中の【items】フォルダの中にindex.htmlがあると思いますが、そのファイルと同じ階層に「item_list.html」ファイルを作成して下さい。記述内容は下記のとおりです。
<html>
<head>
<title>ItemList</title>
</head>
<body>
<h3><a href="{% url 'items:index' %}">はじめの画面に戻る</a></h3>
<table border="2" width="800" cellspacing="0" bordercolor="black">
<caption>テーブルの表題</caption>
<tr bgcolor="gray">
<th>品目CD</th>
<th>品目名</th>
<th>価格</th>
<th>操作</th>
</tr>
{% for dt in item_list %}
<tr>
<td align="left">{{dt.品目CD}}</td>
<td align="left">{{dt.品目名}}</td>
<td align="left">{{dt.価格}}</td>
<td align="left"></td>
</tr>
{% endfor %}
</table>
</body>
</html>
私はhtmlに詳しくないのでこの程度ですが、ここでは2つの箇所に注目してほしいです。まずはページの冒頭に表示されることになる「はじめに戻る」のところです。
<h3><a href="{% url 'items:index' %}">はじめの画面に戻る</a></h3>
ダブルクォーテーションの中でurls.pyのなかで記述したよう「逆引き名」を指定しています。「itemsアプリのindexに飛ばすよ。」的な意味合いです。
もう一つは、html上の表にデータをセットする箇所です。
{% for dt in object_list %}
<tr>
<td align="left">{{dt.品目CD}}</td>
<td align="left">{{dt.品目名}}</td>
<td align="left">{{dt.価格}}</td>
<td align="left"></td>
</tr>
{% endfor %}
前回の投稿において、制御系の書き方はビート店員の挨拶のところで解説しました。それは{% %}で制御分を囲うということでした。「for dt in object_list」の「dt」は適当な名前で変数を取りましたが「object_list」はどこから来たのでしょうか。
views.pyを見返してみて下さい。その様な宣言は見当たりません。でもDjangoは知っているのです。ListViewを継承したときから、見えない余白部分にobject_listがあること。そしてobject_listの実体はviewに記述した「model = Item」に原因していることもです。
ちなみに、「object_list 」を「item_list」に変更しても動きます。残念ながら私にはその理由はわかりませんが、itemモデルのことを指していることはなんとなく推察できます。。そしてこの感じはhtmlの名前にも繋がる類いの感覚です。
クラスの記述の中で使用するテンプレート(html)をしていませんでしたよね。ビート店員の挨拶ではキチンと宣言しました。
template_name = "items/index.html"
そうなのです、省略ができるのです。そして省略した場合のネーミング規則は モデル名_list.htmlだと思います。
最後に、index.htmlから新しいviewが表示できるように書き加えます。
<html>
<head>
</head>
<body>
<div>ビート店員のあいさつ:
{% for bb in opinion %}
<p>{{ bb }}</p>
{% endfor %}
</div>
<h3>
<a href="{% url 'items:item_list' %}">品目の一覧表示</a>
</h3>
</body>
</html>
itemsアプリのitem_listでviewを呼び出します。
さあ、ここまでの結果を確認しましょう。簡易WEBサーバーが起動していない場合はそれをきどうしてください。そしてブラウザにアドレス入力します。「127.0.0.1:8000/items」
ページが表示されたら「品目の一覧表示」をクリックして下さい。次のようになると思います。
データが一覧表示されればOKです。ここではデータ1件しかありませんが。。。
しかし、ここまでくればこっちのものです。みなさんも実感が湧いたのではないでしょうか。
あとはこのオレンジの価格を値上げ操作したり、取り扱い品目を増やしたりするだけです。
その前に、Djangoが提供する画面表示用に使用しているクラスなのですが、ここまで2つ使用しました。一つはTemplateでこのクラスは汎用性も高いため、他のクラスをあえて使用しなくても済むかもしれません。
もう一つはListViewクラスです。品目マスタの一覧表示のために使用しました。Templateクラスを使用した場合、一覧表示のためにはデータを取得するための準備が必要となりますが、ListViewクラスの場合、モデルを指定するだけであとはクラスがデータ取得の準備を行ってくれます。
したがって、目的に応じてクラスを使い分けることはそれなりに開発工数の削減に役立っているようです。
今後使用する画面対話用のクラスは次のものです。
TemplateView:汎用性がある、下記の5つを使わない場面で
FormView:汎用性のがある、下記の5つを使わないフォーム利用の場面で
ListView:データー集計や一覧作成に向く
DetailView:レコードの詳細の扱いに向く
CreateView:新規レコード作成に向く
UpdateView:レコード更新に向く
DeleteView:レコード削除に向く
これら7つの力を持ったスーパー戦隊は、目的にあった変数や関数が秘められているために、アプリ作成で活躍してくれるでしょう。もちろんスーパー戦隊の協力がなくても関数だけでもアプル開発することもできます。しかしせっかくのDjangoですから、彼らの力を借りた方が良いと思います。
それでは、さっそく品目マスタを登録する画面から作成しましょう。
このあたりからは、取り扱うファイルは、urls.py, views.py, forms.py, htmlに限られてくるのですが、ここまでのところでforms.pyは登場していませんでした。forms.pyはユーザーと対話するためにhtml上に載せるコントロールを管理するファイルですが、Djangoでitemアプリを作成したときにはurls,pyと同様にforms.pyはそこに存在していませんでした。恐らくはurls.pyもforms.pyもアプリを作る上で必須では無いからだと思います。それらの使用については開発者の好みに合わせてくれているのでしょう。
ここでは使用しますから、【items】フォルダの直下にforms.pyを作成して下さい。今のところ中身は空っぽで良いです。
まずurls.pyを次のように書き加えます。
# -*- coding: utf-8 -*-
from django.urls import path
from . import views
app_name = 'items'
urlpatterns = [
path('', views.index, name='index'),
path('item_list', views.item_list, name='item_list'),
path('add_item', views.add_item, name='add_item'), #<-登録画面用に追加します
]
続いて、空っぽだったforms.pyを以下のように書き加えます。
#-*- coding: utf-8 -*-
from django import forms
from .models import Item
#------------------------------------------------
# 品目登録用のフォーム
#------------------------------------------------
class HinmokuForm(forms.ModelForm):
class Meta:
model= Item
fields= ('品目CD', '品目名', '価格')
widgets= {
'品目CD': forms.TextInput(attrs={'size':60, 'placeholder': '品目コード'}),
'品目名': forms.TextInput(attrs={'size':80, 'placeholder': '品目名'}),
'価格': forms.NumberInput(attrs={'min': '0', 'max': '9999', 'placeholder': '9999'}),
}
#--------------------------------------
# init
#--------------------------------------
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['品目名'].required = False
instance = getattr(self, 'instance', None)
if instance and instance.品目CD:
self.fields['品目CD'].required = False
self.fields['品目CD'].widget.attrs['readonly'] = True
#--------------------------------------
# 単一のフォームのバリデーションチェックをする
# def clean_xxx(self): <- xxx部分はフィールド名
#--------------------------------------
def clean_品目CD(self):
cd = self.cleaned_data['品目CD']
if(cd <= 0):
raise forms.ValidationError("品目CDは1以上です。")
return cd
forms.pyへの記述内容としては冒頭で、Djangoのformsと、Itemモデルを取り込んでいます。
from django import forms
from .models import Item
そして対話画面で使用するフォームは、HinmokuFormという名前にしました。HinmokuformはDjangoのmodelformから派生させましたので、使用する「モデル」と「フィールド」を指定する必要があります。
widgetsには、各フィールドに対するコントロールとしての属性を定義しています。
model= Item
fields= ('品目CD', '品目名', '価格')
widgets= {
'品目CD': forms.TextInput(attrs={'size':60, 'placeholder': '品目コード'}),
'品目名': forms.TextInput(attrs={'size':80, 'placeholder': '品目名'}),
'価格': forms.NumberInput(attrs={'min': '0', 'max': '9999', 'placeholder': '9999'}),
}
もし表示させたくない、つまり登録不要なフィールドがある場合は、fieldsにセットする「項目」名を取り除けばそれで済みます。ただし、その項目がNULLを許さないように、モデル側で定義されている場合にはレコード登録の時に怒られます。
今回は使用しませんが、もし「モデル」の「フィールド」に無い項目を追加情報として得たいときなどは、それらを追加してユーザーに登録させることも可能です。
#------------------------------------------------
# 品目登録用のフォーム
#------------------------------------------------
class HinmokuForm(forms.ModelForm):
temp_id = forms.IntegerField() #<---追加したいフィールドを記述します
class Meta:
このフォームでは使用するされるに先立って__init__において次のことが行われています。
まず「品目名」の必須入力を無効化しています。Itemモデルを作成したときには、デフォルトで必須入力だった(blank=False)項目でしたが、ここで上書きしたことになります。
続いて、getattrでこのクラスのインスタンスを得ようとしています。そしてその下の部分を見ていただければ分かるのですが。品目CDになにか値が入っていた場合は、その品目CDのコントロールは読み取り専用扱いに属性を変更します。
品目マスタを登録するとき品目CDはまだ空であるためこの部分はスルーします。これが効いてくるのは既存の品目をこのformクラスでも再利用して修正ができるようにしたいからです。この場合、品目CDはキー項目なのでユーザーに上書きされると都合良くありません。
つまり、このHinmokuFormは登録と更新のそれぞれで使おうとしているのです。
self.fields['品目名'].required = False
instance = getattr(self, 'instance', None)
if instance and instance.品目CD:
self.fields['品目CD'].required = False
self.fields['品目CD'].widget.attrs['readonly'] = True
そして残りの部分ですが、フィールドの値チェックを行っています。品目CDは1以上という制約を設けて、エラーとなった場合は下記の様に表示させたいためです。
#--------------------------------------
# 単一のフォームのバリデーションチェックをする
# def clean_xxx(self): <- xxx部分はフィールド名
#--------------------------------------
def clean_品目CD(self):
cd = self.cleaned_data['品目CD']
if(cd <= 0):
raise forms.ValidationError("品目CDは1以上です。")
return cd
BootStrapとかCSSを使えばそれなりのエラー表示ができるのでは無いかと思いますが、ここでは行いません。
なんとかformが出来ましたので、ここからはviews.pyに記述していきます。
まず、機能の追加をしますので、必要な部品をimportします。
# -*- coding: utf-8 -*-
from django.shortcuts import render
from django.views.generic import TemplateView, FormView, \
ListView, DetailView, CreateView, UpdateView, DeleteView #<- 追加した
from .models import Item, Order
from .forms import HinmokuForm #<- 追加した
from django.contrib import messages #<- 追加した
追加したものは、スーパー戦隊(各種のview)と先ほど作ったHinmokuForm などです。
続いて品目登録ページを表示するクラスを書き加えます。
#---------------------------------------------------------------
# Itemの一覧表示
#---------------------------------------------------------------
class ItemListView(ListView):
model = Item
item_list = ItemListView.as_view()
#-------------------------------------------------------------
# 新規品目登録
#-------------------------------------------------------------
class AddHinmokuView(CreateView):
model= Item
form_class= HinmokuForm
success_url= '/items/item_list'
def form_valid(self, form):
''' バリデーションを通ったとき '''
mess= "新規に品目を保存しました。"
messages.success(self.request, mess)
return super().form_valid(form)
def form_invalid(self, form):
''' バリデーションに失敗したとき '''
messages.warning(self.request, "品目を保存できませんでした。")
return super().form_invalid(form)
add_item= AddHinmokuView.as_view()
AddHinmokuViewクラスは、CreateViewクラスを継承しています。modelとform_classに値の設定してください。また、success_urlには、ユーザーがPOSTしたあとで、HinmokuFormが返してくる有効データを保存した後に表示する画面(ここでは一覧画面)をセットしています。form_invaild()とform_valid()は、データ保存が無効なときと有効なときの処理が記述できるように呼出ししていますが、別に無くても構いません。もしviews側からformの値を取り出したいときには、有効なデータとして受け取る事ができますのでform_valid()関数の中において次のようにアクセスすることが可能です。
form.cleaned_data["品目名"]
formとviewを見て分かることは、このプログラムにおいてデータの有効性は、forms.pyでチェックしているということです。
urls.py、forms.py、views.pyに記述を加えましたので、最後はhtmlを記述します。
2つのhtmlを記述しますが、一つは既存のitem_list.htmlです。このファイルに一行書き加えます。
<h3><a href="{% url 'items:add_item' %}">品目の登録</a></h3>
これで、views.py内のクラスを呼び出すことが出来ました。
続いて、新規登録画面用にitem_list.htmlと同じ階層に「item_form.html」を作ります。
views.py側ではtemplateの指定をしておりませんので「item_form.html」は「item_list.himl」同様にクラスの初期値だと思われます。
もし、template名(html)を変えたい場合には、template_name変数ページ名を指定すればよいです。
template_name = "items/XXXXX.html"
さて、item_form.htmlを下記の様に記述して下さい。
<div>
<form method="POST">
{% csrf_token %}
<table>
{{ form }}
</table>
<br>
<input type="submit" name="button_1" value="保存する">
</form>
</div>
<br>
ここで書かれている {{ form }} は、HinmokuFormのインスタンスのことです。
また、{% csrf_token %} は安全上の「おまじない」らしいのでが、ここでは省略します。
それでは、簡易WEBサーバーが起動していることを確認して、動かしてみましょう。
いかがでしょうか。皆さんの環境でも無事に登録されていれば嬉しいのですが。。
しかし簡単にWEBでアプリ作成といっても結構大変ですね。こうしてデータの登録ということを思うとEXCELって本当にありがたいです。ノーベル賞と言わないまでも、なにか賞を差し上げることに一票投じたい。
ちなみに、入力される品目CDが既にデータ上にある場合、Djangoはそのことを教えてくれます。
ここまでで、登録の解説は終わりです。
このあとは、登録した品目情報の詳細表示からの更新、削除を予定します。
しかし、その前に登録についでにDjangoのSHELL機能を使って何件か品目データを登録しておこうと思います。
####ⅲ.一寸いっぷく、DjangoのShell機能をつかう。
業界的には当たり前で、私が無知なだけなのかもしれませんが、Djangoって便利ですね。そんなDjnagoも気がつけばバージョン3.0がリリースされていました。ロードマップを見る限り今後も計画がありますので、Djangoを習得する価値も有ると思います。
そんなDjangoさんのShell機能を使って、品目マスタを追加登録したいと思います。
簡易WEBサーバーが起動していたら一旦とめて、下記のコマンドを入力して下さい。そして入力待ちになればOKです。
mysite>> python manage.py shell
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
December 15, 2019 - 17:19:07
Django version 2.2.5, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
(base) PS C:\Users\PC1804-12\mysite_qi> python manage.py shell
Python 3.7.4 (default, Aug 9 2019, 18:34:13) [MSC v.1915 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.10.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]:
DjangoのShell機能を使うと、プロジェクト固有の環境下で試験的にプログラムをどうさせることができます。例えばデータ操作したいときや動作確認やテストをしたいときなどに使えると思います。
ここではデータ操作をしますが、まず【items】フォルダのモデルをインポートします。views.pyの中でも必要なものをインポートしていますがそれと同じです。
In [1]: from items.models import Item
In [2]:
品目マスタにデータを追加しますのでItemをインポートしました。続いてItemクラスを使って品目のインスタンス(オブジェクト)を作成します。ここでは引数として品目CDを指定(102)してPineappleを追加します。
In [2]: Pineapple= Item(品目CD=102)
続いて品目名、価格をセットしてデータ保存します。
In [3]: Pineapple.品目名= 'Pineapple'
In [4]: Pineapple.価格= 780
In [5]: Pineapple.save()
これでデータ登録が出来ました。折角ですから、もう一件データ登録します。
In [6]: Item(品目CD=103, 品目名= 'Banana', 価格=298).save()
登録されたデータを確認します。
In [7]: items= Item.objects.all()
In [8]: print(items)
<QuerySet [<Item: Orange>, <Item: Strawberry>, <Item: Pineapple>, <Item: Banana>]>
Item.objects.all()でItemに登録されている全てのデータを取得することが出来ます。得られるデータはQuesySetという形で返されます。ここでは4件のデータが含まれていることがわかりました。
また、必要なデータを1件だけ欲しい場合には、get()を使いその要求を行います。
In [9]: items= Item.objects.get(品目CD=103)
In [10]: print(items)
Banana
もし絞り込みのためにデータ抽出(複数件)したい場合には、filtter()を使います。
抽出したい項目の後に __gte (アンダーバーは2つです)をつけると、「以上」の意味になります。
In [11]: items= Item.objects.filter(品目CD__gte=102)
In [12]: print(items)
<QuerySet [<Item: Pineapple>, <Item: Banana>]>
ここでも戻り値はQuerySetです。データ操作についてはこちらの掲載がとても参考になりました。
「Django データベース操作 についてのまとめ」
https://qiita.com/okoppe8/items/66a8747cf179a538355b
shellを使ったデータ操作は一旦ここで終わりです。exit()でshellを終了して下さい。
In [13]: exit()
mysite>>
####ⅳ.品目マスタの登録(更新)、削除について。- 続き
Djangoのshell機能からデータを2件追加しましたので、htmlベージからもその確認をしてみたいとと思います。簡易WEBサーバーを起動し、内容確認して下さい。データが4件になっていればOKです。
データ表示されたところで、表示の並び順を変えてみましょう。価格の降順で行いたいと思います。
views.pyを開いて下さい。ItemListViewは、ListViewから派生していますので、get_queryset()関数をオーバーライドすることが出来ます。このget_queryset()で、Itemのデータ抽出方法を決定します。並び替えは、 order()で行いますがマイナス記号で降順させていますが、reverse() でも行うことが出来ます。reverse()のときはマイナス記号はいりません。
#---------------------------------------------------------------
# Itemの一覧表示
#---------------------------------------------------------------
class ItemListView(ListView):
model = Item
def get_queryset(self):
qs= super().get_queryset()
return qs.order_by('-価格')
item_list = ItemListView.as_view()
結果を見てみましょう。価格の降順になっていればOKです。
ItemListViewでは未だ使用していませんが、HTMLページに渡す値(辞書)は、viwes.py側のget_context_data()においてセットします。
get_queryset()は、get_context_data()に先に呼び出されるため、get_context_data()においてobject_listにアクセスしたときには、オーバーライドされたget_queryset()において抽出されたデータに置き換わっていることに注意して下さい。
そのほか、get()も良く使うメソッドだと思いますので、ここで3つを整理しておきます。
get():
TOPレベルのメソッドで、それそれのHTTPページにはget(),post(),patch()などありますが、get()はview表示の前になにか行っておきたいときに呼び出します。このメソッドは(formをロードする前の)はじめに実行される様です。(意味合いが曖昧ですみませんです。)
get_queryset():
ListViewのときに使われるメソッド。このメソッドは、オブジェクトのリストをどの様にするかを決定するときに使用します。ここのメソッドで抽出データをカスタマイズしなければ、全てのオブジェクトがリストされます。
get_context_data():
HTMLに渡す値(template context)に辞書データを詰め込むために使用します。
これら3つのメソッドはHTMLページ表示前にはよく使いますので、それぞれの特性を知っておいたほうが良いと思います。
それでは、商品マスタの詳細表示を行いましょう。流れとしては、①urls.pyにurl&クラス関数を追記する、②views.pyにクラスを作成する、③インターフェイスとなる、HTMLを作成&追記する、の順番で行います。下記のように一気に記述します。
①urls.py
urlpatterns = [
path('', views.index, name='index'),
path('item_list', views.item_list, name='item_list'),
path('add_item', views.add_item, name='add_item'),
path('dtl_item/<int:pk>', views.dtl_item, name='dtl_item'), #<- 追記します
]
②views.py
#-------------------------------------------------------------
# 品目詳細表示
#-------------------------------------------------------------
class DtlHinmokuView(DetailView):
model= Item
dtl_item= DtlHinmokuView.as_view()
③HTML
③-1:詳細表示画面作成
作成するファイル名は、item_detail.html です。
<html>
<head>
<title>商品の詳細表示</title>
</head>
<body>
<h3><a href="{% url 'items:item_list' %}">品目一覧に戻る</a></h3>
<table border="2" width="400" cellspacing="0" bordercolor="black">
<caption>商品の詳細</caption>
<tr bgcolor="gray" align="center">
<th>項目</th>
<th>内容</th>
</tr>
<tr>
<td bgcolor="lightgray">品目CD</td>
<td bgcolor="white"><font color="#381d1d"><b>{{object.品目CD}}</b></font></td>
</tr>
<tr>
<td bgcolor="lightgray">品目名</td>
<td bgcolor="white"><font color="#381d1d"><b>{{object.品目名}}</b></font></td>
</tr>
<tr>
<td bgcolor="lightgray">価格</td>
<td bgcolor="white"><font color="#381d1d"><b>{{object.価格}}</b></font></td>
</tr>
</table>
</body>
</html>
データはobjectにセットされています。
したがってテンプレート側からは{{ object }}としてアクセスします。
③-2:一覧画面修正・・・「詳細表示」の箇所追加
<html>
<head>
<title>ItemList</title>
</head>
<body>
<h3><a href="{% url 'items:index' %}">はじめの画面に戻る</a></h3>
<h3><a href="{% url 'items:add_item' %}">品目の登録</a></h3>
<table border="2" width="800" cellspacing="0" bordercolor="black">
<caption>商品の一覧</caption>
<tr bgcolor="gray">
<th>品目CD</th>
<th>品目名</th>
<th>価格</th>
<th>操作</th>
</tr>
{% for dt in object_list %}
<tr>
<td align="left">{{dt.品目CD}}</td>
<td align="left">{{dt.品目名}}</td>
<td align="left">{{dt.価格}}</td>
<td align="left">
<a href="{% url 'items:dtl_item' dt.品目CD %}">詳細表示</a>
</td>
</tr>
{% endfor %}
</table>
</body>
</html>
プログラムの追加修正が終わったら、早速動かしてみましょう。
追加された、【詳細表示】をクリックします。
いかがでしょうか。詳細表示されましたでしょうか。
もう3つのステップで作業することは問題ないのではないかと思いますが、詳細画面を表示するとき、つまり商品を特定して表示するためにユーザーが「何を」選択したのかを得たうえで処理する必要があります。
urls.pyを見てみましょう。
path('dtl_item/<int:pk>', views.dtl_item, name='dtl_item'), #<- 追記します
はじめの引数に注目して下さい。「dtl_item/」のあとに「int:pk」が追加されました。
これは整数型の付加情報を求めることを意味しています。実際に一覧から詳細表示したとき、ブラウザのurlには”127.0.0.1:8000/items/dtl_item/102”などと表示されていはずです。それでは、この例の「102」はどこから来たのでしょうか?答えはitem_list.htmlで追記した箇所にあります。
<a href="{% url 'items:dtl_item' dt.品目CD %}">詳細表示</a>
詳細表示クラス(DtlHinmokuView)の関数を逆引きしていますが、一覧表示するために使っていた「dt.品目CD」をキーとして付加しています。そしてこの付加情報は詳細表示クラス(DtlHinmokuView)にキチンと伝わっています。とてもわかりにくいのは、DetailViewから橋させた DtlHinmokuViewがたったこれだけの記述であるからかもしれません。
class DtlHinmokuView(DetailView):
model= Item
dtl_item= DtlHinmokuView.as_view()
もしプログラム内で引数(この例では102)を得たいときには下記のように、クラスが引数として得ている内容にアクセスします。
商品CD = self.kwargs['pk']
さて、引き続きデータ「更新」の記述に移ります。今回も3ステップです。①urls.pyにurl&クラス関数を追記する、②views.pyにクラスを作成する、③インターフェイスとなるHTMLを作成&追記する、の順番です。一気に記述します。
①urls.py
urlpatterns = [
path('', views.index, name='index'),
path('item_list', views.item_list, name='item_list'),
path('add_item', views.add_item, name='add_item'),
path('dtl_item/<int:pk>', views.dtl_item, name='dtl_item'),
path('upd_item/<int:pk>', views.upd_item, name='upd_item'), #<- 追記します
]
②views.py
from django.urls import reverse_lazy #<- ファイルの初め部分に追加する。
#-------------------------------------------------------------
# 品目内容更新
#-------------------------------------------------------------
class UpdHinmokuView(UpdateView):
model= Item
form_class= HinmokuForm
def get_success_url(self):
''' 更新が成功したときは、詳細表示画面に戻す '''
return reverse_lazy('items:dtl_item', kwargs={'pk': self.kwargs['pk']})
upd_item= UpdHinmokuView.as_view()
更新用のクラス(UpdHinmokuView)はUpdateViewより派生させています。登録用のクラス(AddHinmokuView)と同様にform_valid()およびform_invalid()を記述出来ますが、ここでは省略しました。代わりにget_success_url()を呼び出しています。このメソッドでは、戻り値に更新が終わった後の表示先を指定しています。success_url は使用せずに表示先を指定しているという感じです。reverse_lazy()を使用してそのことを行っています。reverse()でも動くようなのですが、クラスの場合はreverse_lazy()でということが何処かにかかれていた様な気がしたためそうしました。reverse_lazy()を使用するためには、事前にdjango.urlsからクラスをインポートする必要があります。
③HTML
③-1:詳細表示画面作成
更新画面は、item_form.htmlを流用するため、新規に作成しなくても大丈夫です。しかし、独自に画面を持ちたい場合は、views.pyのUpdHinmokuViewクラスにおいてtemplate_nameに設定するファイルを【Templates】フォルダの【items】フォルダ内にhtmlファイルを作成して下さい。
③-2:詳細画面修正
item_detail.htmlの終わりの部分に下記の一行を付け加えます。
更新用のクラス(UpdHinmokuView)も詳細画面用クラス(DtlHinmokuView)同様に「どの品目」を必要としますので、キーである品目CDを引数としてセットしてあげます。
<h3><a href="{% url 'items:upd_item' object.品目CD %}">内容を変更する</a></h3>
修正して保存すると詳細画面に戻ります。
当然のことながら、Pineappleは価格が1000高くなりましたので、リストの一番はじめに表示されます。
ところで、forms.pyのHinmokuFormクラスの下記の部分を思い出してください。
instance = getattr(self, 'instance', None)
if instance and instance.品目CD:
self.fields['品目CD'].required = False
self.fields['品目CD'].widget.attrs['readonly'] = True
この部分が機能しているため、更新画面では品目CDの上書きが出来ないようになっているはずです。
いよいよこの項最後のとなりました。削除機能を実装してゆきます。ここでも3ステップで作り込んでゆきます。なんとなくDjangoの生産性の高さを実感していただければ幸いです。
①urls.py
urlpatterns = [
path('', views.index, name='index'),
path('item_list', views.item_list, name='item_list'),
path('add_item', views.add_item, name='add_item'),
path('dtl_item/<int:pk>', views.dtl_item, name='dtl_item'),
path('upd_item/<int:pk>', views.upd_item, name='upd_item'),
path('del_item/<int:pk>', views.del_item, name='del_item'), #<- 追記します
]
DeleteViewから派生した、DelHinmokuViewは「何を」削除するのかが必要となりますのでご注意下さい。
②views.py
#-------------------------------------------------------------
# 品目の削除
#-------------------------------------------------------------
class DelHinmokuView(DeleteView):
model= Item
success_url= '/items/item_list'
del_item= DelHinmokuView.as_view()
品目を削除した後に表示する画面指定が要りますので、success_url に一覧表示の場所を指定します。
③HTML
③-1:削除確認画面作成
作成するファイル名は、item_confirm_delete.html です。
<form method="post">
{% csrf_token %}
<p><font color="red">このデータを削除しますか?</font></p>
<table border="2" width="400" cellspacing="0" bordercolor="black">
<tbody>
<tr>
<th align="left" bgcolor="lightgray">品目CD</th>
<td>{{ object.品目CD }}</td>
</tr>
<tr>
<th align="left" bgcolor="lightgray">品目名</th>
<td>{{ object.品目名 }}</td>
</tr>
</tbody>
</table>
<br>
<input type="submit" value="削除する">
</form>
この部分少し手抜きしていますが、動作に影響がなさそうなので記述の省略をします。
③-2:詳細画面修正
「内容を更新する」の下に追記します。
<h3><a href="{% url 'items:del_item' object.品目CD %}">削除する</a></h3>
いかがでしょうか。無事に削除されていますか?
###5.今回のまとめ
ようやく一覧表示、登録(更新)、詳細表示、削除の機能について解説することが出来ました。
基本的なDjangoの開発手法やその構造を理解していただけたのではないかと思います。
それぞれの役割に特化したクラスは使い勝手も良いと思いますが、開発者からは見えにくい機能も多いと思います。そのあたりは徐々に習得すれば良いと思いますが、get()、get_ontext_data() そしてListViewを扱う場合には、get_queryset()などを良く使うことになると思いますのでこれらのメソッドはおさえておきたいところです。
(get()の解説はここではしておりませんでした。。)
次回(https://qiita.com/cloghjordan/items/557d5dc9d4aa9836f297)は、「受注明細」を取り扱って、その後は若干の「データ分析」を行いたいと思います。