Python Django入門 (5)


子モデルのCRUD
-------------

### 子の読み方

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

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

```python
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 は書きません。

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

### 子の一覧のビュー

今回は、`汎用ビュー` の ListView を使って書いてみます。  
これを使うと、ページングなどが簡単に実現できます。

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

```python
from django.views.generic.list import ListView
  :

class ImpressionList(ListView):
    """感想の一覧"""
    context_object_name='impressions'
    template_name='cms/impression_list.html'
    paginate_by = 2  # １ページは最大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](https://qiita-image-store.s3.amazonaws.com/0/48136/1d2f0dad-4c85-1fed-b71a-cc65ca672dba.jpeg "django10.jpg")



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

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

```python
  :
from cms.models import Book, Impression
  :

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

### 子の追加、修正のビュー

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

```python
  :
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](https://qiita-image-store.s3.amazonaws.com/0/48136/263e6f49-1437-fd94-c328-f9b66aa11ca7.jpeg "django11.jpg")


### 子の削除のビュー

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

今回は、いきなり消さずに、Bootstrap の モーダルダイアログを出して、確認メッセージを出しています。
ただ、ビューの中身は、親の書籍の時と変わりません。

```python
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](https://qiita-image-store.s3.amazonaws.com/0/48136/9638a581-2190-fb9f-334c-604eecacd3cd.jpeg "django12.jpg")



### 子のURLスキーム

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

```python
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](https://qiita-image-store.s3.amazonaws.com/0/48136/f8456a2c-3d5a-4edf-3f63-3b7a1565da97.jpeg "django13.jpg")

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

```bash
http://127.0.0.1:8000/cms/book/
```

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

[Python Django入門 (6)](http://qiita.com/kaki_k/items/b76acaeab8a9d935c35c) に続きます。