Webアプリ開発実践：Djangoでシフト作成ページを作ろう！（データベースモデルの設計）

**[前回](http://qiita.com/juniskw/items/530a94dce4fed5d4277f)の続きです（2014/4/4修正しました）。**

### 1. 必要なデータの選定

まずは、アプリにどんなデータが必要になりそうかを考えてみました。
シフトを考える際に必要な情報は、ざっくり分けると二種類になりそうです。

- **スタッフに関するデータ**
- **利用者に関するデータ**

いずれも名前などの基本情報から、スタッフであれば経験や技量・利用者であれば過去の経歴や性格などのちょっとデータとしては表現しづらい個人情報までありますが、 **現時点ではひとまず前者の基本情報だけで充分でしょう。** 後者のような個人情報は施設の職員ならみんな嫌でも知っているはずなので、わざわざ無理をしてデータ化する必要もないと思います。

ということで、とりあえず最初は極力シンプルにしてみました。

#### スタッフ
- **名前**
- **シフト希望**

#### 利用者
- **名前**

あとは、実際に作ったスケジュールのデータも保存しないといけませんね。

#### スケジュール

- **スタッフのシフト**
- **利用者の利用予定**

加えて、シフト表の管理者（編集者）やスタッフ（閲覧者）も定義します。アクセスしてきた人が誰でも閲覧・編集できるのでは困ってしまいますので

#### 所有者

- **管理者のアカウント情報**
- **スタッフのアカウント情報（複数）**

以上、こんな感じでやっていきます。

### 2. モデル定義

 ```startproject``` でdjangoプロジェクトを作成した後、 ```startapp``` でアプリを作ります。

アプリは一つで充分そうな気がしましたが、後でごちゃごちゃしそうな気がして嫌だったので先に分割しておくことにしました（上で考えたのと同じで、'staff','guest','schedule','owner'の四つ）。それぞれ忘れないうちにsettings.pyのINSTALLED_APPSに追加しておき、models.pyを書いていきます。

まずはstaffから見ていきましょう。

###### staff/models.py

```py
from django.db import models
from django.contrib.auth.models import User
from owner.models import GroupSchedule


class Staff(models.Model):
	groupschedule = models.ForeignKey(GroupSchedule)

	name = models.CharField(max_length=40)

	user = models.OneToOneField(User,null=True,blank=True)

	class Meta:
		unique_together = ( ('name','groupschedule',), )

	def __unicode__(self):
		return self.name
```

現時点でのモデルクラスはStaffのみ。
##### 1.groupschedule:
そのスタッフがどのシフト表に所属しているかを決めるために（複数のシフト表のスタッフがごちゃ混ぜにならないように）ownerアプリのGroupScheduleというモデルと結びつけています。これの中身についてはひとまず後で。
##### 2.name:
分かると思いますが、スタッフの名前です。
##### 3.user:
そのスタッフがアカウントを作ってログイン閲覧することを想定して、組み込みモデルクラスUser（名前、パスワードなどを内包）と関連づけできるようにしています。が、職場にはパソコンを触らない人も居るので必須項目にはしていません（blank=Trueは要らないかも？ここでいうnullとblankの区別が今いち分かっていない・・・・）。
##### 4.Meta:
ここに詳細設定みたいなものを追加できる。unique_togetherというのは重複させたくないテーブルの組み合わせを設定（この組み合わせは複数にもできるみたい）。


次にguest。

###### guest/models.py

```py
from django.db import models
from owner.models import GroupSchedule


class Guest(models.Model):
	groupschedule = models.ForeignKey(GroupSchedule)

	name = models.CharField(max_length=40)

	class Meta:
		unique_together = ( ('name','groupschedule',), )

	def __unicode__(self):
		return self.name
```

staffとほとんど同じですね。
これらはプロジェクトに他の実用アプリを追加したくなったとき都度都度呼び出されるかもしれません。


**シフト制作ページ固有と思われるテーブルはscheduleアプリのモデルに定義しました（なので今のところここが圧倒的に盛りだくさんです）。**

###### schedule/models.py

```py
from django.db import models
from django.core.validators import MaxValueValidator,MinValueValidator
from owner.models import GroupSchedule
from staff.models import Staff
from guest.models import Guest


#### base classes ####

class Date(models.Model):

	date = models.DateField()

	def strfdate(self):
		return self.date.strftime('%Y/%m/%d,%a')

	class Meta:
		abstract = True	# This class is not make table


class TimeTable(models.Model):
	start = models.TimeField(default='00:00')
	end = models.TimeField(default='00:00')

	def strftimetable(self):
		timef = '%H:%M'
		start,end = self.start,self.end
		return "%s ~ %s" % ( start.strftime(timef),end.strftime(timef) )

	class Meta:
		abstract = True	# This class is not make table


#### main classes ####

###### staff ######
class MonthShift(models.Model):
	year = models.PositiveIntegerField(validators=[MinValueValidator(1),])

	month = models.PositiveIntegerField(validators=[MaxValueValidator(12),MinValueValidator(1),])

	groupschedule = models.ForeignKey(GroupSchedule)

	completed = models.BooleanField(default=False)

	class Meta:
		unique_together = ( ('year','month','groupschedule',), )


class WorkTime(TimeTable):
	groupschedule = models.ForeignKey(GroupSchedule)

	title = models.CharField(max_length=50,unique=True)

	simbol = models.CharField(max_length=5,unique=True)

	def save(self,*args,**kwargs):
		from datetime import time
		if self.start >= self.end:
			WorkTime.objects.create(title=self.title+'2',simbol='-',start=time(0,0),end=self.end)

			self.end = time(23,59)
		super(WorkTime,self).save(*args,**kwargs)

	class Meta:
		unique_together = ( ('groupschedule','title',),('groupschedule','simbol',), )

	def __unicode__(self):
		return self.title


class StaffSchedule(Date):
	staff = models.ForeignKey(Staff,unique_for_date='date')

	worktime = models.ForeignKey(WorkTime)

	leader = models.BooleanField(default=False)

	phoner = models.BooleanField(default=False)

	def __unicode__(self):
		return self.strfdate()


class NgShift(Date):
	staff = models.ForeignKey(Staff,unique_for_date='date')

	ng_shift = models.ManyToManyField(WorkTime)

	def ng_values(self):
		values = self.ng_shift.values_list('title')
		
		return ",".join( reduce( lambda x,y:x + y ,values ) )
	
	def __unicode__(self):
		return self.staff.name


###### guest ######
class GuestSchedule(Date,TimeTable):
	guest = models.ForeignKey(Guest,unique_for_date='date')

	def __unicode__(self):
		return self.strfdate()
```

'base classes'としてあるのは親クラスで、 **syncdbしてもテーブルを作りません** 。'main classes'というのが実際にテーブルを作るクラスです。

#### 1.MonthShift:
それがどのグループの何年の何月分のシフトなのか、それは完成しているのかを定義（例えばシフト編集ページで、「完了」ボタンを押したら勤務時間が空白の箇所は全て休日に変換、という風にしたいと考えている）。year、monthともにvalidatorsで最大値や最小値を設定しています。
#### 2.WorkTime:
「早番」や「遅番」など、勤務時間のパターンを定義。自分の職場ではそれらが「○」や「△」など一文字で表現されているため、simbolという項目も用意しています。django組み込みのメソッドsave()を上書きし、勤務時間が二日にまたがっている場合は一日単位に分割する処理を追加しています（夜勤は「★」、明けは「ー」みたく表現したいので別のパターンとして登録してしまう）。
#### 3.StaffSchedule:
日付ごとのスタッフの予定を意味しています。通常シフトを配る際には、これを一ヶ月単位でまとめたりしますね。ForeignKeyで先ほどのWorkTimeデータを一つだけ選べるようにしておきます。日直や宿直であるかどうかの項目も今の表にあったので作りました。
#### 4.NgShift:
ManyToManyFieldで複数のWorkTimeデータを選べます。スタッフが「この日この時間帯は無理です」っていうのがあれば登録できるような感じ（全選択が実質の休日希望ということになりますね）。ng_valuesは登録されているng_shiftをカンマ区切りの文字列で返す手作り関数ですが、これはadminページ用です。
#### 5.GuestSchedule:
StaffScheduleと似ていますが、お客さんの利用時間は勤務時間と違いパターン化しづらそうなので手入力の形式にしています。

最後に管理者用の情報をownerにまとめます。

###### owner/models.py

```py
from django.db import models
from django.contrib.auth.models import User,Group
from django.core.validators import MaxValueValidator,MinValueValidator


class GroupSchedule(models.Model):
	group = models.OneToOneField(Group)

	owner = models.OneToOneField(User)

	start_point = models.PositiveIntegerField(default=1,validators=[MaxValueValidator(31),MinValueValidator(1),])

	def get_calendar(self,year,month):
		from calendar import Calendar

		from datetime import date
		cal_start = date( year,month,self.start_point )
		cal_end = cal_start.replace(month=cal_start.month+1)
		
		this_month = list( Calendar().itermonthdates( year,month ) )
		next_month = list( Calendar().itermonthdates( year,month+1 ) )
		
		wcal = this_month + next_month
		wcal_list = wcal[wcal.index(cal_start):wcal.index(cal_end)]

		return sorted( set(wcal_list),key=wcal_list.index )

	def __unicode__(self):
		return self.group.name
```

#### 1.GroupSchedule:
それがどのグループの予定で、所有者は誰なのかを決め、start_pointでは毎月何日を始点としているかを登録できます（自分の職場では毎月１５日から一ヶ月間で少し変わっていたため自由に設定できる形にしました）。get_calendarは始点から一ヶ月分の日付をセットで返す関数ですが、おそらくviewの方で呼び出すことになるでしょう。

### さて、これで基本的なデータベースは構築できたと思います。

## 次回は『adminページで実験』をしてみることにしましょう。