python

Django入門その4(ToDoアプリ)

ToDoアプリの作成の続き

前回の内容をやっていない方はまずこちらから進めてください。
ToDoアプリ作成

CRUD操作

ここからは、htmlにボタンなどを表示させた上で、そこからDBに書き込みができるようにしていきます。
CRUDは、
C:Create(作成する)
R:Read(情報を読み込む)
U:Update(更新する)
D:Delete(削除する)
の頭文字を取ったのもので、サービス作成に必須な機能になっています。

まずは、Readから実装していきます。
ただ、ここでも楽をしたいので、djangoが提供しているListViewというデータの一覧をリストとして表示することに適したテンプレートを継承して作成していきます。
それでは、todoディレクトリのurls.pyを変更して、一覧データを表示するためのURL設定を行います。

from django.urls import path
from .views import TodoList

urlpatterns = [
    path('list/', TodoList.as_view()),
]

urls.pyの記載したTodoListのclassをviews.pyに書いていきます。
一旦中身を全て削除して、書き換えていきます。

from django.shortcuts import render
from django.views.generic import ListView

class TodoList(ListView):
  template_name = 'list.html'

template_nameに記載したhtmlファイルをtemplatesの中にlist.htmlという名前で作成していきます。

その際に、DBの内容をhtml側に表示する時に使う記法が {% %}{{ }} です。
Railsなどを触ったことがある方は分かると思いますが、上記の記法を用いることで、pythonで書かれた変数などを表示することができます。
具体的な挙動は次のコードで確認していきます。list.htmlに書き加えていきます。

<!-- object_listはListViewを指定した際に使用できるDBの中にあるデータをリストで保存しているオブジェクト -->
{% for item in object_list %} 
<ul>
  <li>{{ item.title }}</li>
  <li>{{ item.memo }}</li>
</ul>
{% endfor %}

これで必要最低限の表示に関する部分ができました。
ただ、これだと今後の実装が不便になるので、views.pyを修正します。
何を修正するかというと、DBのどのテーブルを使用するかを明示的に示します。
今はテーブルが一つしかないので困りませんが、テーブルを増やしていくとどのテーブルを使用しているか分からなくなることがあるので、忘れないようにしましょう

from django.shortcuts import render
from django.views.generic import ListView
from .models import TodoModel

class TodoList(ListView):
  template_name = 'list.html'
  model = TodoModel

最後に、サーバーを立ち上げて、http://localhost:8000/list/ にアクセスをして、DBに保存した内容が表示されるか確認してみましょう。
これでREADは一旦完了です。

まだ足りない、READの機能としてはamazonのサイトをイメージしてあげれば分かりますが、今回作成したのは一覧ページになります。なので、詳細な内容に関しては別のディレクリを用意して飛ばしてあげる必要があります。
次に詳細ページを作る方法について紹介します。

ListViewと同様に、詳細ページを作るのにも適したテンプレートがDetailViewという名前で提供されています。基本的にはListViewと一緒なのですが、特徴としてデータの中身を表示することに適したテンプレートであることが挙げられます。
使い方についてコードを書いて確認していきます。
まずは、一覧ページの時と同様にurls.pyを編集します。ここで注意して欲しいのが、詳細ページは1つのデータ毎にページが用意されるので、データのidをベースにディレクトリを作成してくれる<int:pk>というものを用います。これ以外にもslugを用いる方法もあります。

from django.urls import path
from .views import TodoList, TodoDetail

urlpatterns = [
    path('list/', TodoList.as_view()),
    path('detail/<int:pk>', TodoDetail.as_view())
]

続いて、views.pyを編集します。

from django.shortcuts import render
from django.views.generic import ListView, DetailView
from .models import TodoModel

class TodoList(ListView):
  template_name = 'list.html'
  model = TodoModel

class TodoDetail(DetailView):
  template_name = 'detail.html'
  model = TodoModel

detail.htmlを作成します。ここで、ListViewとの差異が出てきます。
object_listが今回は使えない代わりに、1つのデータを直接取ってくることができます。

ここまで出来たら、サーバーを立ち上げて、http://localhost:8000/detail/1 にアクセスしてみてください。detail.htmlの内容が表示されているはずです。
ここまでで、READについてはお終いです。
次の内容に進む前に、Bootstrapを使用して、少し見た目を整えておきます。

https://getbootstrap.com/docs/4.4/getting-started/introduction/#starter-template
からテンプレートのソースコードをコピーしてきます。
そうしたら、list.htmlを編集していきます。

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

    <title>Hello, world!</title>
  </head>
  <body>
    {% for item in object_list %} 
    <ul class="list-group">
      <li class="list-group-item">{{ item.title }}</li>
      <li class="list-group-item">{{ item.memo }}</li>
    </ul>
    {% endfor %}

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

続いて同様にdetail.htmlも編集します。

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

    <title>Hello, world!</title>
  </head>
  <body>
    <div class="alert alert-primary" role="alert">
      <h2>{{ object.title }}</h2>
    </div>

    <div class="alert alert-primary" role="alert">
      <p>{{ object.memo }}</p>
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

かなり雑ですが、少し見た目が変化しました。
bootstarp以外にもmaterial UIなどといったものもありますので、適宜使用してみてください。
使用する場合には、全て覚える必要はなく、公式ドキュメントを参照しながら自分のイメージに合ったものを選択してくのが便利です。

2つのhtmlファイルを編集してきたのですが、大部分が共通していますね。今後もhtmlファイルが増えてきて、共通部分を修正する時に全ファイルを修正するのは面倒なので、共通部分はまとめて一つのファイルにしてしまいます。

そのために、共通部分を担うファイルをbase.htmlという名前で作成し、他のファイルで使い回すようにしていきます。
それでは、base.htmlを作成して、list.htmlとdetail.htmlを修正していきます。

はじめに、base.htmlにbootstrapの初期情報を入れていきましょう。
そして、別ファイル(list.htmlなど)から処理を書き込まれるように、下記のプログラムを書き込みます
{% block header %}
{% endblock header %}

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

    <title>Hello, world!</title>
  </head>
  <body>
    <!-- 別ファイルの内容を入れる位置を明示的に指定 -->
    <!-- headerという名前は任意 -->
    {% block header %}
    {% endblock header %}

    <!-- もう一つ書き込める場所を用意 -->
    {% block content %}
    {% endblock content %}

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

続いて、list.htmlを編集していきます。
まずはbase.htmlにほとんどの内容を移したので、全て削除してしまいます。
その上で下記のコードを書き込みます。完全に上書きしてしまっても良いです。

{% extends 'base.html' %}

{% block header %}
<h1>リストページです</h1>
{% endblock header %}

{% block content %}
{% for item in object_list %}
<ul class="list-group">
  <li class="list-group-item">{{ item.title }}</li>
  <li class="list-group-item">{{ item.memo }}</li>
</ul>
{% endfor %}
{% endblock content %}

この状態で、サーバーを立ち上げて確認すると前回と比較して「リストページです。」が加わった以外全く同じものが表示されます。

コードの説明を行うと、{% extends ‘base.html’ %} はbase.htmlを持ってきてこのファイルをベースにしますという処理になります。
そして、base.htmlで明示的に示した書き込める場所を、{% block header %}{% endblock header %}というプログラムを書くことで、その中に表示したい内容を書けば表示されます。{% block content %}も同様です。

bootstrapの使い方も含めて少し見た目を整えましたが、もう少し見栄えを調整していきます。list.htmlを書き換えます。CSSの部分なので詳しくは説明しないのですが、興味があれば公式ドキュメントを見て色々とデザイン調整をしてみてください。

{% extends 'base.html' %}

{% block header %}
<div class="jumbotron jumbotron-fluid">
  <div class="container">
    <h1 class="display-4">ToDoリスト</h1>
    <p class="lead">ToDoリストを管理することで生産性をアップしていきます。</p>
  </div>
</div>
<h1></h1>
{% endblock header %}

{% block content %}
<div class='container'>
{% for item in object_list %}
<div class="alert alert-primary" role="alert">
  <p>{{ item.title }}<br> ー {{ item.memo }}</p>
  <a class="btn btn-primary" href="#" role="button">詳細</a>
  <a class="btn btn-success" href="#" role="button">編集</a>
  <a class="btn btn-danger" href="#" role="button">削除</a>
</div>
{% endfor %}
</div>
{% endblock content %}

ToDoリストには優先順位の表示できる機能もつけたいので、DBに優先順位を格納するためのカラムを追加していきましょう。
DBを操作するのはmodel.pyが担うのでしたね。そうしたら、models.pyを変更していきます。

from django.db import models

PRIORITY = (('danger','high'),('warning','middle'),('info', 'normal'))
class TodoModel(models.Model):
  "項目を作成していきます"
  title = models.CharField(max_length=100) # 文字列のフィールド
  memo = models.TextField()
  priority = models.CharField(
    max_length = 50,
    choices = PRIORITY # 選択肢(selectboxのようなもの)
  )
  duedate = models.DateField() # 期限
  def __str__(self):
    return self.title

今回は優先順位と期限を管理するカラムを追加しました。
PRIORITY = ((‘danger’,’high’),(‘warning’,’middle’),(‘info’, ‘normal’)) の部分が少し分かりにくいかもしれませんが、dangerなどの1つ目の値が、item.titileなどで呼び出した時に呼び出される値で、’high’などの2つ目の値は管理画面で表示される値になります。

ここまで出来たら再び、migrateしていきます。
やり方を忘れてしまった方は、https://techpr.info/python/django-3/ を見直して実行してみてください。

migrateすると、下記のような文言が流れてくるはずです。

You are trying to add a non-nullable field ‘duedate’ to todomodel without a default; we can’t do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit, and let me add a default in models.py

これは、既に作られているデータに対して、duedateカラムがnullになってしまいます。という警告になります。そして、現在の設定ではnullを許容していないということが言われています。
そこで、duedateカラムをnullを下記のように許容するように変更することもできます。

from django.db import models

PRIORITY = (('danger','high'),('warning','middle'),('info', 'normal'))
class TodoModel(models.Model):
  "項目を作成していきます"
  title = models.CharField(max_length=100) # 文字列のフィールド
  memo = models.TextField()
  priority = models.CharField(
    max_length = 50,
    choices = PRIORITY # 選択肢(selectboxのようなもの)
  )
  duedate = models.DateField(null=True) # 期限
  def __str__(self):
    return self.title

話を戻しますが、ターミナルで聞かれていることは、
1)が何かしらの値を入れて対応
2)がmigrateを止めて、models.pyを自身で修正する
になります。
今回は1を選択します。
そうすると、次に何の値を入れるかを聞かれます。

Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type ‘exit’ to exit this prompt

今回はdjangoが提案してくれている、timezone.nowを設定します。

>>> timezone.now

続いて同じようにpriorityについても聞かれるので、1を選択してdangerと入力しまししょう。

>>> ‘danger’

成功するとmaigrationsフォルダにファイルが作成されているのでDBに書き込みます。

ここまでできたら、サーバーを立ち上げて管理画面にアクセスして確認してみてください。
新しい項目が追加されていることが確認できるはずです。

最後に優先度をデザインの色で表現するために、list.htmlのclass名をpriorityの値を用いて動的に変更させます。

{% extends 'base.html' %}

{% block header %}
<div class="jumbotron jumbotron-fluid">
  <div class="container">
    <h1 class="display-4">ToDoリスト</h1>
    <p class="lead">ToDoリストを管理することで生産性をアップしていきます。</p>
  </div>
</div>
<h1></h1>
{% endblock header %}

{% block content %}
<div class='container'>
{% for item in object_list %}
<div class="alert alert-{{ item.priority }}" role="alert">
  <p>{{ item.title }}<br> ー {{ item.memo }}</p>
  <a class="btn btn-primary" href="#" role="button">詳細</a>
  <a class="btn btn-success" href="#" role="button">編集</a>
  <a class="btn btn-danger" href="#" role="button">削除</a>
</div>
{% endfor %}
</div>
{% endblock content %}

新しくデータを追加して、優先度に応じてデザインが変わるかを確認してみてください。

次回で、更新機能と削除機能を追加してToDoアプリを完成させていきたいと思います。