[Django3.0]金額を3桁のカンマ区切りで表示させたい

カンマ区切りを追加するには、「humanize」を利用します。

settings.py

INSTALLED_APPS = [
    // 省略
    'sales',
    'widget_tweaks',
    'django.contrib.humanize', // 追加
]

NUMBER_GROUPING = 3

### view
template側では、humanizeを読み込んで、intcommaを追記します。

{% load humanize %}

<input type="text" disabled id="total" class="form-control col-md-4 text-right" value="{{total.total__sum | intcomma}}円" placeholder="" >

お、凄い良いです。
次はeditとdeleteを実装していきます。

因みに、今日は竹芝ポートシティのタリーズに来てます。なんか竹芝ポートシティの紹介ビデオ見てたらAR勉強したくなってきたなー

[Django3.0]ForeignKeyを使ったデータの取得・表示方法

見積一覧ページで、sales_estimatesテーブルに入っている各見積りデータを表示させたい。

▼ワイヤーフレーム

機能要件
– 見積の会社名は、hasManyのリレーション関係にあるsales_clientsのnameから引っ張って表示させたい
– 最新の登録順に表示させる
– 見積件数が増えた場合を想定して、ページネーションを実装する
– 見積の合計金額を表示させる

### views.py
– ${model}.object.all()でデータを取得する
– order_by(‘-id’)でidをdescで取得する
– *.count()で対象のレコード件数を取得できる
– *.aggregate(Sum(‘${columnName}’))で対象カラムの合計値を取得できる

from django.db.models import Sum

def estimate(request, num=1):
	data = Estimates.objects.all().order_by('-id')
	page = Paginator(data, 3)
	count = data.count()
	total = data.aggregate(Sum('total'))
	params = {
		'data' : page.get_page(num),
		'count' : count,
		'total' : total,
	}
	return render(request, 'sales/estimate.html', params)

### estimate.html
– sumの値は、view側でも{{total.total__sum}}と書いてあげる必要がある

<div class="row col-md-12">
				<label class="col-form-label col-md-3">検索結果 {{ count }}件</label>
				<label for="total" class="col-md-2 col-form-label">合計見積金額(円)</label>
				<input type="text" disabled id="total" class="form-control col-md-4 text-right" value="{{total.total__sum}}円" placeholder="" >
			</div>

– 親テーブルの値である会社名を表示させるには、item.client.nameというように、foreignKeyとそのテーブルのカラム名を繋げてあげれば良い

{% for item in data %}
						<tr>
							<td>{{item.id}}</td>
							<td class="text-nowrap">{{item.estimate_date}}</td>
							<td class="text-nowrap">{{item.client.name}}</td>
							<td class="text-nowrap">{{item.title}}</td>
							<td class="text-right">{{item.total}}円</td>
							<td class="text-nowrap"><button class="btn btn-light">詳細</button> <button class="btn btn-light">編集</button> <button class="btn btn-light" onclick="location.href='#modal'">削除</button></td>
							<td class="text-nowrap"><button class="btn btn-light">ダウンロード</button></td>
						</tr>
						{% endfor %}

Djangoの場合、エラー時に参考にする記事の英語の割合が増えるので、やってて面白い。
さて、次は、見積金額を表示させる際に、3桁以上の場合はカンマをつけたいですね。

[Django3.0]ForeignKeyを使ったモデルとviews.pyの保存処理の書き方

見積登録ページを作成するため、hasManyのモデルをmigrationした後にModelFormを作っていきます。

models.py
-> ForeignKeyでClients ModelのhasManyを設定しています。

class Estimates(models.Model):
	client = models.ForeignKey(Clients, null=True, blank=True, on_delete=models.PROTECT)
	estimate_date = models.DateField()
	position = models.CharField(max_length=50, null=True, blank=True)
	name = models.CharField(max_length=255, null=True, blank=True)
	title = models.CharField(max_length=255)
	classification1 = models.CharField(max_length=255, null=True, blank=True)
	// 省略
	total = models.IntegerField(null=True, blank=True)
	remark = models.TextField(max_length=300, null=True, blank=True)
	created_at = models.DateTimeField(auto_now_add=True)
	updated_at = models.DateTimeField(auto_now=True)

	def __str__(self):
		return self.title

forms.py

class EstimatesForm(forms.ModelForm):
	class Meta:
		model = Estimates
		fields = ['client', 'estimate_date', 'position', 'name', 'title', 'classification1', 'classification2', 'classification3', 'classification4', 'classification5', 'classification6', 'classification7', 'classification8', 'classification9', 'classification10', 'type1', 'type2', 'type3', 'type4', 'type5', 'type6', 'type7', 'type8', 'type9', 'type10', 'name1', 'name2', 'name3', 'name4', 'name5', 'name6', 'name7', 'name8', 'name9', 'name10', 'size1', 'size2', 'size3', 'size4', 'size5', 'size6', 'size7', 'size8', 'size9', 'size10', 'unit1', 'unit2', 'unit3', 'unit4', 'unit5', 'unit6', 'unit7', 'unit8', 'unit9', 'unit10', 'price1', 'price2', 'price3', 'price4', 'price5', 'price6', 'price7', 'price8', 'price9', 'price10', 'total', 'remark']

続いて、views.pyでestimate_input関数を書いていきます。

views.py

from .models import Estimates
from .forms import EstimatesForm

def estimate_input(request):
	params = {
		'form': EstimatesForm()
	}
	return render(request, 'sales/estimate_input.html')

## view
### Select文でリレーションの呼び出し
– viewはwidget_tweaksを使っています。
– form.${親テーブル} とするだけでselect文を作ってくれます。

estimate_input.html

<div class="form-group">
						<label for="client">得意先(選択)</label>
						{% render_field form.client class="form-control" %}
					</div>

– 見積の合計金額は、vue.jsでcomputedします。

var app = new Vue({
			el: '#app',
			data: {
				tax:10,
				price_1:'',
				// 省略
			},
			computed: {
				add1: function(){
					return (this.price_1 * this.unit_1 + this.price_2 * this.unit_2 + this.price_3 * this.unit_3 + this.price_4 * this.unit_4 + this.price_5 * this.unit_5 + this.price_6 * this.unit_6 + this.price_7 * this.unit_7 + this.price_8 * this.unit_8 + this.price_9 * this.unit_9 + this.price_10 * this.unit_10 ) * (100 + this.tax) / 100;

				}
			}

### views.py
formで飛ばして、最後に保存します

def estimate_complete(request):
	if(request.method == 'POST'):
		data = EstimatesForm(request.POST)
		if data.is_valid():
			client = request.POST['client']
			estimate_date = request.POST['estimate_date']
			position = request.POST['position']
			name = request.POST['name']
			title = request.POST['title']
			classification1 = request.POST['classification1']
			// 省略
			total = request.POST['total']
			remark = request.POST['remark']
			estimates = Estimates(client_id=client, estimate_date=estimate_date, position=position, name=name, title=title, classification1=classification1, classification2=classification2, classification3=classification3, classification4=classification4, classification5=classification5, classification6=classification6, classification7=classification7, 
				classification8=classification8, classification9=classification9, classification10=classification10, type1=type1, type2=type2, type3=type3, type4=type4, type5=type5, type6=type6, type7=type7, type8=type8, type9=type9, type10=type10, name1=name1, name2=name2, name3=name3, name4=name4, name5=name5, name6=name6, name7=name7, name8=name8, 
				name9=name9, name10=name10, size1=size1, size2=size2, size3=size3, size4=size4, size5=size5, size6=size6, size7=size7, size8=size8, size9=size9, size10=size10, unit1=unit1, unit2=unit2, unit3=unit3, unit4=unit4, unit5=unit5, unit6=unit6, unit7=unit7, unit8=unit8, unit9=unit9, unit10=unit10, price1=price1, price2=price2, price3=price3,
				price4=price4, price5=price5, price6=price6, price7=price7, price8=price8, price9=price9, price10=price10, total=total, remark=remark)
			estimates.save()
			return render(request, 'sales/estimate_complete.html')
		else:
			params = {
				'form': EstimatesForm(request.POST),
			}
			return render(request, 'sales/estimate_input.html', params)
	return render(request, 'sales/estimate_complete.html')

### 結合テスト
修正しまくって、上手く動くようになるまで数日かかりました。

mysql側も確認し、ちゃんと入っています。
client_idには親テーブルのidが入っています。

mysql> select * from sales_estimates;
+—-+—————+———-+————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+——————+——–+——–+——-+——-+——-+——-+——-+——-+——-+——–+———————–+—————–+——-+——-+——-+——-+——-+——-+——-+——–+———–+——-+——-+——-+——-+——-+——-+——-+——-+——–+——-+——-+——-+——-+——-+——-+——-+——-+——-+——–+——–+——–+——–+——–+——–+——–+——–+——–+——–+———+——–+——–+—————————-+—————————-+———–+
| id | estimate_date | position | name | title | classification1 | classification2 | classification3 | classification4 | classification5 | classification6 | classification7 | classification8 | classification9 | classification10 | type1 | type2 | type3 | type4 | type5 | type6 | type7 | type8 | type9 | type10 | name1 | name2 | name3 | name4 | name5 | name6 | name7 | name8 | name9 | name10 | size1 | size2 | size3 | size4 | size5 | size6 | size7 | size8 | size9 | size10 | unit1 | unit2 | unit3 | unit4 | unit5 | unit6 | unit7 | unit8 | unit9 | unit10 | price1 | price2 | price3 | price4 | price5 | price6 | price7 | price8 | price9 | price10 | total | remark | created_at | updated_at | client_id |
+—-+—————+———-+————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+——————+——–+——–+——-+——-+——-+——-+——-+——-+——-+——–+———————–+—————–+——-+——-+——-+——-+——-+——-+——-+——–+———–+——-+——-+——-+——-+——-+——-+——-+——-+——–+——-+——-+——-+——-+——-+——-+——-+——-+——-+——–+——–+——–+——–+——–+——–+——–+——–+——–+——–+———+——–+——–+—————————-+—————————-+———–+
| 2 | 2020-09-19 | | 山田太郎 | AWS構築費用 | サーバ | サーバ | | | | | | | | | 設計 | 構築 | | | | | | | | | 基本設計書作成 | EC2初期構築 | | | | | | | | | 設計書 | | | | | | | | | | 1 | 1 | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | 50000 | 300000 | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | 385000 | | 2020-09-19 13:00:45.342104 | 2020-09-19 13:00:45.342128 | 19 |
+—-+—————+———-+————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+—————–+——————+——–+——–+——-+——-+——-+——-+——-+——-+——-+——–+———————–+—————–+——-+——-+——-+——-+——-+——-+——-+——–+———–+——-+——-+——-+——-+——-+——-+——-+——-+——–+——-+——-+——-+——-+——-+——-+——-+——-+——-+——–+——–+——–+——–+——–+——–+——–+——–+——–+——–+———+——–+——–+—————————-+—————————-+———–+
1 row in set (0.00 sec)

これ実装するのにすごい時間かかったけど、ここまで来たら、大分山を超えた感があります。後は基本的なところはauthとメールなど細かな機能でしょう。デバッグする際に、form.errors.itemsで画面にエラーメッセージを表示させてからスピードが上がりました。

[Django3.0] 1対多(hasMany)のモデルを作成

顧客が多数の見積を取る事を考え、sales_clientsテーブルから1対多(hasMany)の関係にあるテーブルを作成したい。

見積登録画面

見積画面を元にテーブルのデータ型を作成していきます。
mysqlのdateはDateField、integerはIntegerFieldにします。
clientはhasManyのFoerignKeyとなるものです。

データ型が一通り出来たら、ER図を修正します。

さて、いよいよデータ型を元にmodels.pyを作っていきます。
リレーションは、models.ForeignKey(${modelName})で書きます。
顧客レコードが削除されたら、顧客レコードに紐づいた見積も一緒に削除されると困るので、on_delete=models.PROTECTと設定してあげます。

models.py

class Estimates(models.Model):
	client = models.ForeignKey(Clients, on_delete=models.PROTECT)
	estimate_date = models.DateField(auto_now=True)
	position = models.CharField(max_length=50, null=True, blank=True)
	name = models.CharField(max_length=255, null=True, blank=True)
	title = models.CharField(max_length=255)
	classification1 = models.CharField(max_length=255, null=True, blank=True)
	// classification2~classification10は省略
	type1 = models.CharField(max_length=255, null=True, blank=True)
	// type2〜type10は省略
	name1 = models.CharField(max_length=255, null=True, blank=True)
	// name2〜name10は省略
	size1 = models.CharField(max_length=255, null=True, blank=True)
	// size2〜size10は省略
	unit1 = models.IntegerField(null=True, blank=True)
	// unit2〜unit10は省略
	price1 = models.IntegerField(null=True, blank=True)
	// price2〜price10は省略
	total = models.IntegerField(null=True, blank=True)
	remark = models.TextField(max_length=300, null=True, blank=True)
	created_at = models.DateTimeField(auto_now_add=True)
	updated_at = models.DateTimeField(auto_now=True)

	def __str__(self):
		return self.title

### migration
$ python manage.py makemigrations sales
-> 0004_auto_20200913_1714.pyが作成されたので中身を見てみます。fieldsの末尾にmodels.ForeignKeyが作られている事がわかります。

fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('estimate_date', models.DateField(auto_now=True)),
                ('position', models.CharField(blank=True, max_length=50, null=True)),
                // 省略
                ('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sales.Clients')),
            ],

$ python manage.py migrate

mysql> show tables;
mysql> describe sales_estimates;
+——————+————–+——+—–+———+—————-+
| Field | Type | Null | Key | Default | Extra |
+——————+————–+——+—–+———+—————-+
| id | int(11) | NO | PRI | NULL | auto_increment |
// 省略
| client_id | int(11) | NO | MUL | NULL | |
+——————+————–+——+—–+———+—————-+
70 rows in set (0.00 sec)

ぎゃああああああああああああああああああああああああああああああ
client_idが出来てるううううううううううううううううううう

[Django3.0]あいまい検索の検索フォーム実装

顧客一覧ページで、会社名の検索機能を設けたい。

template

<form action="/client/1" method="post">
				{% csrf_token %}
				<div class="form-group row">
					<div class="col-md-10">
						<input type="text" class="form-control" name="name" placeholder="検索する会社名を入力してください">
					</div>
					<div class="col-md-2">
						<button class="btn search-btn" type="submit">検索</button>
					</div>
				</div>
			</form>

<span>{{ query }}</span>

– if(request.method==’POST’)でgetとpostを分岐させる
– 検索は *.objects.filter(column=value)と書く
– あいまい検索には(column__contains=value)と書く
– nullで検索された場合は、メッセージを表示させたくないので、最初の分岐で、and request.POST[‘name’]も追加する

views.py

def client(request, num=1):
	if(request.method=='POST' and request.POST['name']):
		name= request.POST['name']
		data = Clients.objects.filter(name__contains=name)
		page = Paginator(data, 3)
		params = {
			'data' : page.get_page(num),
			'query' : "会社名に「" + name + "」を含む検索結果"
		}
	else:
		data = Clients.objects.all()
		page = Paginator(data, 3)
		params = {
			'data' : page.get_page(num)
		}
	return render(request, 'sales/client.html', params)

うん、GoodJob! 意外と簡単にできました。
得意先一覧ができたので、続いて見積管理を作っていきます。
得意先が複数の見積を持つというhasManyのモデルをどう作るか考えなければなりません。

[Django3.0]リダイレクトの書き方

views.pyでrenderではなくredirectと書く

urls.py

urlpatterns = [
	//
	path('client/', views.client_redirect, name='client_redirect'),
        // 
]

views.py

from django.shortcuts import render, redirect
def client_redirect(request):
	return redirect('/client/1')

次は検索フィルターの実装です。

[Django3.0]ページネーションを実装する

urls.py

urlpatterns = [
	// 省略
	path('client/<int:num>', views.client, name='client'),
        // 省略
]

views.py

from django.core.paginator import Paginator

def client(request, num=1):
	data = Clients.objects.all()
	page = Paginator(data, 3)
	params = {
		'data' : page.get_page(num)
	}
	return render(request, 'sales/client.html', params)

templateにページネーションを実装します。
*.html

<div class="list">
				<ul class="pagination justify-content-end">
					{% if data.has_previous %}
					<li class="page-item">
						<a class="page-link" href="/client/1">&laquo; first</a>
					</li>
					<li class="page-item">
						<a class="page-link" href="/client/{{data.previous_page_number}}">&laquo; prev</a>
					</li>
					{% else %}
					<li class="page-item">
						<a class="page-link">&laquo; first</a>
					</li>
					<li class="page-item">
						<a class="page-link">&laquo; prev</a>
					</li>
					{% endif %}
					<li class="page-item">
						<a class="page-link">{{data.number}}/{{data.paginator.num_pages}}</a>
					</li>
					{% if data.has_next %}
					<li class="page-item">
						<a class="page-link" href="/client/{{data.next_page_number}}">next &raquo;</a>
					</li>
					<li class="page-item">
						<a class="page-link" href="/client/{{data.paginator.num_pages}}">last &raquo;</a>
					</li>
					{% else %}
					<li class="page-item">
						<a class="page-link">next &raquo;</a>
					</li>
					<li class="page-item">
						<a class="page-link">last &raquo;</a>
					</li>
					{% endif %}
				</ul>
			</div>

あとは、client/にリクエストがあった時に、Page not foundとレスポンスを返してしまうので、urls patternでclient/ではなく、client/で来た時に、client/1にリダイレクトさせたいな。

[Django3.0]widget_tweaksで値を表示したい時

顧客情報編集ページで、formのactionにidを使いたい時

### views.py

def client_edit(request, id):
	data = Clients.objects.get(id=id)
	params = {
		'form': ClientsForm(instance=data),
		'id':id
	}
	return render(request, 'sales/client_edit.html', params)

### template

<form action="/client/{{id}}/complete" method="post">

widget_tweaksで値を表示したい場合は、paramsで値を送る必要がある
action=”/client/{{form.id}}/complete”とすると、何も表示されない

Djangoは日本語のトラブルシューティングのページが殆どないので、laravelなどに比べて調査に時間がかかります。

[Django3.0]idを引数にしたurlの設定

顧客IDをurlのパスに設定して、顧客詳細ページや顧客編集ページを表示させたい。

e.g. http:*/client/detail/${clientId}

### urls.py、views.pyを修正する

/sales/urls.py

urlpatterns = [
	// 省略
	path('client/detail/<int:id>', views.client_detail, name='client_detail'),
	// 省略
]

/sales/views.py

def client_detail(request, id):
	client = Clients.objects.get(id=id)
	params = {
		'client' : client
	}
	return render(request, 'sales/client_detail.html', params)

/templates/sales/client_detail.html

// 省略
{% block title %}{{client.name}} | hanbai - 得意先詳細:{{client.name}}の詳細を表示しています{% endblock %}
// 省略

<table class="table">
					<tr>
						<td class="bg">会社名</td>
						<td>{{client.name}}</td>
						<td class="bg">会社名カナ</td>
						<td>{{client.name_kana}}</td>
					</tr>
					<tr>
						<td class="bg">事業所名</td>
						<td>{{client.office}}</td>
						<td class="bg">部署</td>
						<td>{{client.department}}</td>
					</tr>
					<tr>
						<td class="bg">担当者名</td>
						<td>{{client.charge}}</td>
						<td class="bg">役職</td>
						<td>{{client.position}}</td>
					</tr>
					<tr>
						<td class="bg">メールアドレス</td>
						<td colspan="3">{{client.charge_mail}}</td>
					</tr>
					<tr>
						<td class="bg">住所</td>
						<td colspan="3">{{client.zipcode}} {{client.prefecture}}{{client.address}}</td>
					</tr>
					<tr>
						<td class="bg">電話</td>
						<td>{{client.tel}}</td>
						<td class="bg">FAX</td>
						<td>{{client.fax}}</td>
					</tr>
					<tr>
						<td class="bg">代表者</td>
						<td>{{client.name_top}}</td>
						<td class="bg">役職</td>
						<td>{{client.position_top}}</td>
					</tr>
					<tr>
						<td class="bg">備考</td>
						<td colspan="3">{{client.remark}}</td>
					</tr>
				</table>
// 省略

idを引数に上手く表示されています。

ただし、引数のidがレコードにないと、”Clients matching query does not exist.”と表示されてしまう。

### try & except
以下のように書けば、レコードがない場合、一覧ページに表示されます。

def client_detail(request, id):
	try:
		client = Clients.objects.get(id=id)
		params = {
			'client' : client
		}
		return render(request, 'sales/client_detail.html', params)
	except Clients.DoesNotExist:
		data = Clients.objects.all()
		params = {
			'data' : data
		}
		return render(request, 'sales/client.html', params)

クライアント一覧ページのリンクを以下のように修正して結合テストを行えばOK。

<button class="btn btn-light" onclick="location.href='/client/detail/{{item.id}}'">詳細</button>

ページング以外でGetパラメータを使用するのは抵抗がありますね。

[Django3.0]remodalからの削除処理

顧客一覧ページから、対象の削除ボタンを押下するとremodalで確認ボタンが表示され、OKを押すとレコードが削除されるようにする。

### mysql
mysql> select * from sales_clients;
-> 2つレコードが入っています。

### view
– remodalをformにして、client_idをinput hiddenで削除完了ページにpostします。

remodal

{% block script %}
	<div class="remodal" data-remodal-id="modal">
		<form method="post" action="/client/delete" id="form_id">
			{% csrf_token %}
			<button data-remodal-action="close" class="remodal-close"></button>
				<input type="hidden" id="client_id" name="client_id">
			    <h1>得意先削除</h1>
			    <p>
			    <span id="company_name"></span>を削除して宜しいでしょうか?
			    </p>
			    <br>
			<button data-remodal-action="cancel" class="remodal-cancel">Cancel</button>
			<button data-remodal-action="confirm" class="remodal-confirm">OK</button>
		</form>
	</div>
	<script src="{% static 'sales/js/remodal.min.js' %}"></script>
	<script>
		var remodal = $('[data-remodal-id=modal]').remodal();

		$('.del').on('click', function(){
			var id = $(this).attr("id");
			var name = $(this).attr("value");
			document.getElementById("company_name").innerHTML = name;
			document.getElementById("client_id").setAttribute("value", id);
		})

		$(document).on('confirmation', remodal, function(){
			$('#form_id').submit();
		});
	</script>
{% endblock %}

### views.py
views.pyでは、*.objects.getで対象のレコードを取得して削除します。

/sales/views.py

def client_delete(request):
	if(request.method == 'POST'):
		client_id = request.POST['client_id']
		client = Clients.objects.get(id=client_id)
		client.delete()
		return render(request, 'sales/client_delete.html')
	return render(request, 'sales/client_delete.html')

### 削除実行
mysql> select * from sales_clients;
+—-+——————————————–+——————————–+——–+————+—————–+————–+————————-+———-+————+——————————-+————–+————–+————–+—————–+——–+—————————-+—————————-+
| id | name | name_kana | office | department | position | charge | charge_mail | zipcode | prefecture | address | tel | fax | name_top | position_top | remark | created_at | updated_at |
+—-+——————————————–+——————————–+——–+————+—————–+————–+————————-+———-+————+——————————-+————–+————–+————–+—————–+——–+—————————-+—————————-+
| 1 | ジャパンソフトウェア株式会社 | ジャパンソフトウェア | 本社 | 営業部 | 代表取締役 | 佐藤太郎 | staro@japansoftware.com | 100-0002 | 東京都 | 千代田区皇居外苑1-2-3 | 03-1234-5678 | 03-1234-5679 | 山本五郎 | 代表取締役 | | 2020-08-30 02:09:35.555187 | 2020-08-30 02:09:35.555259 |
+—-+——————————————–+——————————–+——–+————+—————–+————–+————————-+———-+————+——————————-+————–+————–+————–+—————–+——–+—————————-+—————————-+
1 row in set (0.00 sec)

deleteはviews.pyで*.delete()で削除できる。
DjangoのCRUDは一通りマスターしました。
ところでDjangoは論理削除の機能はあるんでしょうか? is_activeのカラムを作って、削除時にupdateするのかな。