[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するのかな。

[Django3.0]viewのループ処理でremodalにidを渡して各レコードを削除したい

得意先リスト一覧ページで、各クライアントの行で削除ボタンを押下した時に、操作ミスによる削除を防ぐため、直ぐに削除するのではなく、remodalで一度確認画面をかまして削除するようにしたい。

### document.getElementsByNameで値を渡そうとした場合
渡す値が一つであれば、getElementsByName、getElementByIdなどで取得が出来る
ただし、forループで回している場合、同じ値を渡してしまい上手くいかない
*.html

{% for item in data %}
						<tr>
							<td id="hoge">{{item.id}}</td>
							<td class="text-nowrap">{{item.name}}</td>
							<td class="text-nowrap">〒{{item.zipcode}} {{item.prefecture}}{{item.address|truncatechars:20}}</td>
							<td>4</td>
							<td>6</td>
							<td class="text-nowrap"><button class="btn btn-light" onclick="location.href='/client/detail#{{item.id}}'">詳細</button> <button class="btn btn-light" onclick="location.href='/client/edit#{{item.id}}'">編集</button> <button class="btn btn-light" onclick="location.href='#modal'" name="target" value="{{item.id}}">削除</button></td>
						</tr>
						{% endfor %}

remodal & javascript

<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" value="00">
			    <h1>得意先削除</h1>
			    <p>
			    東京商事株式会社を削除して宜しいでしょうか?
			    </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();

		$(document).on('opened', remodal, function () {
		     var target = document.getElementsByName('target').value;
		     console.log(target);
		});

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

### buttonをaタグで囲って、idを渡す
*.html

<a href="#modal"><button class="btn btn-light del" id="{{item.id}}">削除</button></a>

remodal & javascript

// remodal
<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">
			    <h1>得意先削除</h1>
			    <p>
			    東京商事株式会社を削除して宜しいでしょうか?
			    </p>
			    <br>
			<button data-remodal-action="cancel" class="remodal-cancel">Cancel</button>
			<button data-remodal-action="confirm" class="remodal-confirm">OK</button>
		</form>
	</div>

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

js側では、on clickでidの値を取得して、getElementById(“client_id”).setAttribute(“value”, id);でformのhiddenのinputにセットすると上手くいきます。idをセットする場所をinputではなくactionのリンク先につけてgetでidを取得しても良いでしょう。

laravelで同じような仕組みを作っていたので、githubでソースコードを確認して実装しました。ここはDjangoってよりもループ処理の時のremodalとJavaScriptの実装の仕方ですね。

[Django3.0]DBのリストをviewに表示

sales_clientsテーブルのレコードを得意先一覧ページで表示させたい。
レコード全てを取得する際には、views.pyで${modelName}.objects.all()として取得する。

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 |
| 16 | 六本木ソフトウェア株式会社 | ロッポンギソフトウェア | 本社 | 営業部 | 課長 | 田中太郎 | ttanaka@roppongisoft.com | 106-6240 | 東京都 | 港区六本木住友不動産六本木グランドタワー40階 | 03-1234-5678 | 03-1234-5679 | 山田一郎 | 代表取締役 | | 2020-09-05 03:18:49.981333 | 2020-09-05 03:18:49.981377 |
+—-+——————————————–+———————————–+——–+————+—————–+————–+————————–+———-+————+———————————————————————–+————–+————–+————–+—————–+——–+—————————-+—————————-+
2 rows in set (0.00 sec)

sales/views.py

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

/templates/sales/client.html

<table class="table table-striped">
					<thead >
						<tr>
							<th scope="col" class="font-weight-normal">ID</th>
							<th scope="col" class="font-weight-normal">会社名</th>
							<th scope="col" class="font-weight-normal">住所</th>
							<th scope="col" class="font-weight-normal">見積</th>
							<th scope="col" class="font-weight-normal">受注</th>
							<th scope="col" class="font-weight-normal">アクション</th>
						</tr>
					</thead>
					<tbody>
						{% for item in data %}
						<tr>
							<td>{{item.id}}</td>
							<td class="text-nowrap">{{item.name}}</td>
							<td class="text-nowrap">〒{{item.zipcode}} {{item.prefecture}}{{item.address|truncatechars:20}}</td>
							<td>4</td>
							<td>6</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>
						</tr>
						{% endfor %}
					</tbody>	
				</table>

住所の文字数を制限するため、item.address|truncatechars:20とすると、20文字以上は自動的に三点リーダで表示されます。

続いて、詳細、編集、削除ボタンのリンクを作成していきます。

[Django3.0]エラーメッセージで辞書型のvalueを表示する書き方

forms.pyで、全角カナ以外の入力があった場合にname_kanaにエラーメッセージを割り当てています。

forms.py

	def clean_name_kana(self):
		name_kana = self.cleaned_data['name_kana']
		p = re.compile('[\u30A1-\u30F4]+')
		if not(p.fullmatch(name_kana)):
			raise forms.ValidationError("全角カナで入力してください。")

### keyが表示される駄目な例
view側で、form.errorsは辞書型のデータを取得するので、form.name_kana.errorsとすれば辞書型のvalueが表示されますが、errorだけだと、辞書型のkeyを表示します。
以下のように書くとエラーメッセージはname_kana となります。

client_input.html

{% for error in form.errors %}
{{ error }}
{% endfor %}

### 辞書型のvalueが表示される例
このように書くと、辞書型のkeyとvalueを取得できるので、指定したバリデーションメッセージを表示させる事ができます。

client_input.html

{% for key, value in form.errors.items %}
{{ value }}
{% endfor %}

これ修正するのに凄い時間かかった。もー

[Django3.0]Viewでのエラーメッセージの書き方

エラーメッセージがあった場合にはforループで回すのが一般的?

モデルで正規表現でカタカナのみ入力可としてる所で、漢字で入力してエラーメッセージを表示させる。

/sales/models.py


class Clients(models.Model):
	name = models.CharField(max_length=255)
	name_kana = models.CharField(max_length=255, null=True, blank=True, validators=[RegexValidator(r"\u30A1-\u30F4")])
	office = models.CharField(max_length=255, null=True, blank=True)
	department = models.CharField(max_length=255, null=True, blank=True)
	position = models.CharField(max_length=255, null=True, blank=True)
	charge = models.CharField(max_length=255)
	charge_mail = models.EmailField(max_length=255)
	zipcode = models.CharField(max_length=8, validators=[RegexValidator(r"\d{3}-\d{4}")])
	prefecture = models.CharField(max_length=20)
	address = models.CharField(max_length=255)
	tel = models.CharField(max_length=15)
	fax = models.CharField(max_length=15, null=True, blank=True)
	name_top = models.CharField(max_length=255)
	position_top = models.CharField(max_length=100, 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)

/templates/sales/client_input.html

{% for error in form.errors %}
<div class="text-danger">
    {{ error }}
</div>
{% endfor %}

エラーメッセージを定義していないから、kana_nameだけ表示される。

class ClientsForm(forms.ModelForm):
	class Meta:
		model = Clients
		fields = ['name', 'name_kana', 'office', 'department', 'position', 'charge', 'charge_mail', 'zipcode', 'prefecture', 'address', 'tel', 'fax', 'name_top', 'position_top', 'remark']

	def clean_name_kana(self);
		name_kana = self.cleaned_data['name_kana']
		p = u'^[\u30A1-\u30F4]+$'
		if(re.match(p, name_kana))
			raise forms.ValidationError("全角カナで入力してください")
		return name_kana

上手くいかない。正規表現を変更する。

	def clean_name_kana(self):
		name_kana = self.cleaned_data['name_kana']
		p = re.compile('[\u30A1-\u30F4]+')
		if not(p.fullmatch(name_kana)):
			raise forms.ValidationError("全角カナで入力してください")
{% for error in form.errors %}
				<div class="text-danger">
				{{ form.name_kana.errors }}
			   </div>
				{% endfor %}

上手く行ったが、なんかモヤモヤするな。