Edited at

Python Django入門 (5)

More than 1 year has passed since last update.


子モデルのCRUD


子の読み方

親の書籍に対して、子の感想の一覧を表示してみます。

cms/models.py で、感想は書籍を外部キーとして持つよう定義しました。

class Impression(models.Model):

"""感想"""
book = models.ForeignKey(Book, verbose_name='書籍', related_name='impressions', on_delete=models.CASCADE)
comment = models.TextField('コメント', blank=True)

よって、書籍に紐づく子供の感想は、related_nameを使って、以下のように読み出すことができます。


ここでも SQL は書きません。

impressions = book.impressions.all().order_by('id')   # 書籍の子供の、感想を読む


子の一覧のビュー

今回は、汎用ビュー の ListView を使って書いてみます。


これを使うと、ページングなどが簡単に実現できます。

cms/views.py に以下の記述を追加します。

from django.views.generic.list import ListView

:

class ImpressionList(ListView):
"""感想の一覧"""
context_object_name='impressions'
template_name='cms/impression_list.html'
paginate_by = 2 # 1ページは最大2件ずつでページングする

def get(self, request, *args, **kwargs):
book = get_object_or_404(Book, pk=kwargs['book_id']) # 親の書籍を読む
impressions = book.impressions.all().order_by('id') # 書籍の子供の、感想を読む
self.object_list = impressions

context = self.get_context_data(object_list=self.object_list, book=book)
return self.render_to_response(context)


子の一覧のテンプレート

BootStrapを応用して、


  • ページングありの時、「前へ、1、2、次へ」などのページ番号を表すリンクを置く。

  • 削除する時、いきなり消すのではなく、確認のモーダルダイアログを出す。

ということをしています。ちょっと長いです。

mybook/cms/templates/cms/base_html を継承して mybook/cms/templates/cms/impression_list.html を作成します。

{% extends "cms/base.html" %}

{% block title %}感想の一覧{% endblock title %}

{% block content %}
<h4 class="mt-4 border-bottom">感想の一覧 <small class="text-muted ml-3">{{ book.name }}</small></h4>
<a href="{% url 'cms:impression_add' book_id=book.id %}" class="btn btn-primary btn-sm my-3">追加</a>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>ID</th>
<th>コメント</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for impression in impressions %}
<tr>
<td>{{ impression.id }}</td>
<td>{{ impression.comment|linebreaksbr }}</td>
<td>
<a href="{% url 'cms:impression_mod' book_id=book.id impression_id=impression.id %}" class="btn btn-outline-primary btn-sm">修正</a>
<button class="btn btn-outline-danger btn-sm del_confirm" data-toggle="modal" data-target="#deleteModal" data-pk="{{ impression.id }}" data-url="{% url 'cms:impression_del' book_id=book.id impression_id=impression.id %}">削除</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>

{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous"><span aria-hidden="true">&laquo;</span><span class="sr-only">Previous</span></a></li>
{% else %}
<li class="page-item disabled"><a class="page-link" href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</span><span class="sr-only">Previous</span></a></li>
{% endif %}
{% for linkpage in page_obj.paginator.page_range %}
{% ifequal linkpage page_obj.number %}
<li class="page-item active"><a class="page-link" href="#">{{ linkpage }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="?page={{ linkpage }}">{{ linkpage }}</a></li>
{% endifequal %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next"><span aria-hidden="true">&raquo;</span><span class="sr-only">Next</span></a></li>
{% else %}
<li class="page-item disabled"><a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&raquo;</span><span class="sr-only">Next</span></a></li>
{% endif %}
</ul>
</nav>
{% endif %}

<a href="{% url 'cms:book_list' %}" class="btn btn-secondary btn-sm">戻る</a>

{# 削除を確認するモーダル ダイアログ #}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">確認</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<p>ID: <span id="del_pk"></span> を削除しますか?</p>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-primary" id="del_url">OK</a>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
{% endblock content %}

{% block extra_js %}
<script>
$(function() {
$('.del_confirm').on('click', function () {
$("#del_pk").text($(this).data("pk"));
$('#del_url').attr('href', $(this).data("url"));
});
});
</script>
{% endblock %}

出来上がる一覧ページは、以下のようなものとなります。(まだ、この先のことをしないと動かないので、こういうイメージになると考えて下さい)

django10.jpg


子の追加、修正のフォーム

cms/forms.py に以下のように追加します。


ここでは、cms/models.py の Impression モデルを追加、修正するための元となるフォームを作成します。

  :

from cms.models import Book, Impression
:

class ImpressionForm(ModelForm):
"""感想のフォーム"""
class Meta:
model = Impression
fields = ('comment', )


子の追加、修正のビュー

cms/views.py に以下のように追加します。

  :

from cms.models import Book, Impression
from cms.forms import BookForm, ImpressionForm
:

def impression_edit(request, book_id, impression_id=None):
"""感想の編集"""
book = get_object_or_404(Book, pk=book_id) # 親の書籍を読む
if impression_id: # impression_id が指定されている (修正時)
impression = get_object_or_404(Impression, pk=impression_id)
else: # impression_id が指定されていない (追加時)
impression = Impression()

if request.method == 'POST':
form = ImpressionForm(request.POST, instance=impression) # POST された request データからフォームを作成
if form.is_valid(): # フォームのバリデーション
impression = form.save(commit=False)
impression.book = book # この感想の、親の書籍をセット
impression.save()
return redirect('cms:impression_list', book_id=book_id)
else: # GET の時
form = ImpressionForm(instance=impression) # impression インスタンスからフォームを作成

return render(request,
'cms/impression_edit.html',
dict(form=form, book_id=book_id, impression_id=impression_id))


子の追加、修正のテンプレート

mybook/templates/base_html を継承して mybook/cms/templates/cms/impression_edit.html を作成します。

{% extends "cms/base.html" %}

{% load bootstrap4 %}

{% block title %}感想の編集{% endblock title %}

{% block content %}
<h4 class="mt-4 mb-5 border-bottom">感想の編集</h4>
{% if impression_id %}
<form action="{% url 'cms:impression_mod' book_id=book_id impression_id=impression_id %}" method="post">
{% else %}
<form action="{% url 'cms:impression_add' book_id=book_id %}" method="post">
{% endif %}
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
<div class="form-group row">
<div class="offset-md-3 col-md-9">
<button type="submit" class="btn btn-primary">送信</button>
</div>
</div>
</form>
<a href="{% url 'cms:impression_list' book_id=book_id %}" class="btn btn-secondary btn-sm">戻る</a>
{% endblock content %}

追加、修正のページは、以下のようになります。(まだ、この先のことをしないと動かないので、こういうイメージになると考えて下さい)

django11.jpg


子の削除のビュー

cms/views.py に以下のように追加します。

今回は、いきなり消さずに、Bootstrap の モーダルダイアログを出して、確認メッセージを出しています。

ただ、ビューの中身は、親の書籍の時と変わりません。

def impression_del(request, book_id, impression_id):

"""感想の削除"""
impression = get_object_or_404(Impression, pk=impression_id)
impression.delete()
return redirect('cms:impression_list', book_id=book_id)

django12.jpg


子のURLスキーム

cms/urls.py に以下のように追加します。

urlpatterns = [

:

# 感想
path('impression/<int:book_id>/', views.ImpressionList.as_view(), name='impression_list'), # 一覧
path('impression/add/<int:book_id>/', views.impression_edit, name='impression_add'), # 登録
path('impression/mod/<int:book_id>/<int:impression_id>/', views.impression_edit, name='impression_mod'), # 修正
path('impression/del/<int:book_id>/<int:impression_id>/', views.impression_del, name='impression_del'), # 削除
]


親の一覧の修正

親の書籍の一覧から、該当する書籍の「感想一覧」が出せるよう、リンクを追加します。

mybook/cms/templates/cms/book_list.html に1行追加します。

          <td>

<a href="{% url 'cms:book_mod' book_id=book.id %}" class="btn btn-outline-primary btn-sm">修正</a>
<a href="{% url 'cms:book_del' book_id=book.id %}" class="btn btn-outline-danger btn-sm">削除</a>
<a href="{% url 'cms:impression_list' book_id=book.id %}" class="btn btn-outline-info btn-sm">感想の一覧</a>
</td>

django13.jpg

それでは、ローカルサーバを起動し、「書籍の一覧」から「感想の一覧」をたどって、感想の登録/修正/削除を行ってみましょう。

http://127.0.0.1:8000/cms/book/

ここまでの説明のように親子関係のあるモデルのCRUDができれば、後はモデルをどう設計するかということなので、応用して色々なものが作れると思います。

Python Django入門 (6) に続きます。