カンバンボード アプリケーション開発(1)
この連載ではサンプルアプリケーションとして、カンバンボードを作成します。カンバンはアジャイル開発の現場でよく使われているタスク管理手法の1つです。完成イメージは以下の図のようになります。
各チケットは以下のデータを持っています。
- チケット名
- 説明
- ステータス(ToDo、InProgress、Done)
- 担当者
- 開始日
- 終了日
アプリケーションの作成
以下のコマンドでboardアプリケーションの初期構築を行います。このプロジェクトとアプリケーションの設計思想や構成については、前回記事で詳しく解説しています。
(env) $ mkdir kanban (env) $ cd kanban (env) $ django-admin startapp board
以下のようなファイル構成になったことを確認してください。
modern-django
└──kanban
└── board
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
生成された各ファイルは次のような役割を持っています。
| ファイル | 役割 |
|---|---|
| admin.py | 管理サイトの設定を記述するファイル |
| apps.py | アプリケーションの構成情報を記述するファイル |
| migrations | マイグレーションファイルを格納するディレクトリ |
| models.py | データベースのスキーマを定義するファイル |
| tests.py | テストを記述するファイル |
| views.py | view関数を記述するファイル |
アプリケーションの登録
作成したアプリケーションをプロジェクトに登録して、プロジェクト内で使えるようにします。このためには、config/settings/base.py内のINSTALLED_APPSを更新します。
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'kanban.board.apps.BoardConfig', # BoardConfigを追加
]
またここで追加したBoardConfigを、現在のプロジェクト構成に沿うように編集しておく必要があります。
from django.apps import AppConfig
class BoardConfig(AppConfig):
name = 'kanban.board' # 初期値の'board'から変更
以上でアプリケーションの作成および初期設定が完了しました。
モデルの定義
カンバンボードのチケットに必要となるデータについては、先ほど確認しました。それをもとにしたER図は次のようになります。
このテーブル定義をもとに、kanban/board/models.pyの実装を進めていきます。まずは以下のように編集しましょう。
from django.conf import settings
from django.db import models
class Ticket(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True, default='')
status = models.SmallIntegerField()
assignee = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL)
start = models.DateField(null=True, blank=True)
end = models.DateField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
def __str__(self):
return f'{self.name}'
nameやdescriptionなどのモデルのフィールドは、テーブルのカラムに対応します。フィールドはテーブルのカラムがどのような型で値を保持するかを決定しています。またフィールドには、null、blank、defaultなどのオプション引数を指定できます。それぞれのオプションの意味は以下のとおりです。
-
null=True:データベースにNULLを保存することを許可します。デフォルトはFalse。 -
blank=True:フィールドの値が空であることを許容します。デフォルトはFalse。 -
default=...:デフォルト値を指定します。
null=Trueとblank=Trueは、似ていて紛らわしいので注意してください。nullはデータベースについての設定であるのに対し、blankはあくまでフォームなどでのバリデーションについての設定になります。
これらに加えて、フィールド固有のオプション引数を指定できます。たとえばCharFieldの場合には、max_length=100のようにデータベースに格納する文字数の最大値を指定できます。
フィールドには基本的な型だけではなく、テーブル間の関連付け(リレーション)を表現する型も用意されています。このフィールドを使うことで、テーブル間のリレーションを定義できます。
| フィールド名 | リレーションの種類 |
|---|---|
| ForeignKey | 多対1のリレーション |
| ManyToManyField | 多対多のリレーション |
| OneToOneField | 1対1のリレーション |
Django 2.0からForeignKeyとOneToOneFieldにはon_deleteオプションを指定することが必須となりました。on_deleteは、レコードを削除する際のリレーション先への振る舞いを定義します。
| on_delete値 | 振る舞い |
|---|---|
| CASCADE | リレーション先もすべて削除される |
| PROTECT | リレーションがある場合は削除できなくする |
| SET_NULL | NULLがセットされる(null=Trueの設定が必要) |
| SET_DEFAULT | デフォルト値がセットされる(default=...の設定が必要) |
| SET() | 引数として渡した値がセットされる |
| DO_NOTHING | 何もしない |
Django 1系におけるデフォルト値はCASCADEです。Django 2.0からはこのうちのどれかを自分で指定する必要があります。
モデルフィールドについてのさらに詳しく知りたい方は、公式ドキュメント Model field referenceを参照してください。
Djangoのモデル定義について理解を深めたうえで、Ticketモデルを振り返りましょう。
-
name:最大文字数が100のCharFieldとして定義。 -
description:入力任意、デフォルト値は空文字。文字数制限をせず、TextFieldとして定義。 -
status:ToDo、InProgress、Doneのステータスを数字で扱うので、SmallIntegerFieldとして定義。 -
assignee:Userモデルを参照に持つForeignKeyとして定義。入力任意、NULL許可。担当者だったユーザーが削除された場合にチケットも消えてしまうことは避けたいので、on_delete=models.SET_NULLを設定。 -
start:Pythonコードではdatetime.dateインスタンスとして表されるDateFieldとして定義。入力任意、NULL許可。 -
end:startと同様にDateFieldとして定義。入力任意、NULL許可。 -
created:Pythonコードではdatetime.datetimeインスタンスとして表されるDateTimeFieldとして定義。作成された時刻が自動でセットされるauto_now_add=Trueを指定。 -
modified:createdと同様に、更新日時をDateTimeFieldとして定義。更新された時刻が自動でセットされるauto_now=Trueを指定。
enum
statusフィールドは、ToDo、InProgress、Doneのステータスを扱うSmallIntegerFieldとして定義しました。値はそれぞれ以下の通りとします。
| ステータス | 値 |
|---|---|
| ToDo | 1 |
| InProgress | 2 |
| Done | 3 |
statusはこの3つの値以外にはしたくないので、choicesオプションを使って値を制限するようにしましょう。choicesを設定しておくと、バリデーションやフィルタリングの際に役立ちます。
choicesオプションには、(値, 表示用の文字列)のタプルで構成されるiterable(反復可能なオブジェクト)を渡します。以下のようにそれぞれの値とchoices用のiterableを定数として定義するやり方が、Djangoでは一般的です。
class Ticket(models.Model):
ToDo = 1
InProgress = 2
Done = 3
STATUS_CHOICES = (
(ToDo, 'ToDo'),
(InProgress, 'InProgress'),
(Done, 'Done'),
)
...
status = models.SmallIntegerField(choices= STATUS_CHOICES, default=ToDo)
...
ただこのやり方だと、choicesの選択肢が増えていったり、複数のchoicesが必要になったりすると、コードが煩雑になっていきます。Modelクラス内がchoicesのための定数で溢れてしまい、本来集中したいフィールドの定義がぼやけてしまいます。
Python3.4から導入されたenum(列挙型)を使ってchoicesを定義することをお勧めします。enumは単純に定数として使うだけでなくメソッドを実装できるため、再利用の幅が広がります。
このenumを使って、TicketStatusクラスを実装しましょう。kanban/board/constants.pyを作成して、以下のように編集します。
import enum
class TicketStatus(enum.IntEnum):
ToDo = 1
InProgress = 2
Done = 3
@classmethod
def get_choices(cls):
return tuple((x.value, x.name) for x in cls)
このTicketStatusを用いて、kanban/board/models.pyのstatusフィールドを以下のように書き直します。
from django.conf import settings
from django.db import models
from .constants import TicketStatus
class Ticket(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True, default='')
status = models.SmallIntegerField(choices=TicketStatus.get_choices(), default=TicketStatus.ToDo.value)
...
enumを導入することで、保守性が高まり再利用がしやすいコードが書けるのでぜひ試してみてください。
django-model-utils
Ticketモデルに作成日時(created)、更新日時(modified)のフィールドを定義しましたが、これらはほかのモデルでも汎用的に使えそうです。django-model-utilsというパッケージは、このニーズに応えるTimeStampedModelという抽象ベースクラスを用意してくれています。
django-model-utilsをインストールして、Ticketモデルに適用してみましょう。
(env) $ pip install django-model-utils (env) $ vi requirements/base.txt (env) $ cat requirements/base.txt Django==2.0.2 django-model-utils==3.1.1
from django.conf import settings
from django.db import models
from model_utils.models import TimeStampedModel
from .constants import TicketStatus
class Ticket(TimeStampedModel):
name = models.CharField(max_length=100)
description = models.TextField(blank=True, default='')
status = models.SmallIntegerField(choices=TicketStatus.get_choices(), default=TicketStatus.ToDo.value)
assignee = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL)
start = models.DateField(null=True, blank=True)
end = models.DateField(null=True, blank=True)
def __str__(self):
return f'{self.name}'
Ticketクラスの継承元をdjango.db.models.Modelからmodel_utils.models.TimeStampedModelに変更しています。このおかげで、Ticketクラスからcreatedとmodifiedフィールドの定義を省略できました。django-model-utilsには、TimeStampedModel以外にも便利な抽象ベースクラスやフィールドが用意されています。ぜひ一度チェックしてみてください。
