CakePHPでデータ操作〜担当者アサイン機能の実装 - AWS上で構築するRESTfulアプリ勉強会～Web開発ワークショップ～【第6回】マニュアル



:warning: 第5回でusersテーブルを追加しましたが、前回不参加の方に、最初伝え漏れていましたので追記いたしました(事前準備のところ)。すいませんでした!

## :large_blue_circle: はじめに

本投稿は、2015/6/26に行われた、[CakePHPでデータ操作〜担当者アサイン機能の実装 - connpass](http://aws-rest-todo.connpass.com/event/12734/)の内容についてまとめた資料です。

:warning:今後の予定
[AWS上で構築するRESTfulアプリ勉強会～Web開発ワークショップ～ - connpass](http://aws-rest-todo.connpass.com/)

今回は、TODOに担当者を割り当てる機能を追加します。
前回ログイン機能を付けたので、複数ユーザで使用する感じにしてみます！

CakePHPの「アソシエーション」について学びます。


## :large_blue_circle: CakePHPのアソシエーションとは？

前回まで、

- users
- todo_lists

の2テーブルを扱っていました。

しかし、テーブル1つの単純なSelectしかしていませんでした。
今回作成するアプリケーションでは、usersとtodo_listsを結合してSelectしたりする必要がありますが、CakePHPではこういった場合にSQLを直接書くわけではなく、「アソシエーション」という仕組みを使用します。
テーブルを結合したりするのを、SQLではなくコードで表現します。

### 今回学ぶアソシエーション

今回は下記3つのアソシエーションを学びます。

- belongsTo
- hasOne
- hasMany

:warning: 「hasAndBelongsToMany」というのもありますが、今回は割愛します。

1つずつ説明します。

### belongsTo

A belongsTo B、つまり、AはBに属している、というテーブル間の関係がある場合、belongsToアソシエーションを使用します。
例えば、「ユーザ(users)」テーブルに、「国籍」というフィールドがあり、「国籍マスタ(countries)」テーブルがあるとします。

- countries(id, name)

とします。

userは一つの国籍をもちますので、

- users(id, username, country_id)

という感じです。
users.country_id と countries.id で joinしてSelectすれば取得できますね。
SQLで書くとこんな感じです。

```sql
select users.id, users.username, countries.name as country_name
from users inner join countries on users.country_id = countries.id
```

こういう場合に使用するのが「belongsTo」アソシエーションです。
コードは後ほど見ていきます！

### hasOne

hasOneは、相手側テーブルが1件、と言う意味でbelongsToの親戚みたいなものです。
belongsToの例では、users側にcountriesのレコードを示す`country_id`を持っていました。
例えば、ユーザのプロフィールを保持するテーブル(profiles)があるとします。

- profiles(id, user_id, 自己紹介文, 好きな食べ物, etc...)

この場合、users側ではなく、profiles側にuser_idを持っていますので、
users.id と profiles.user_id で joinしてSelectすれば取得できますね。
こういう場合に使用するのが「hasOne」アソシエーションです。

:warning: users側にprofile_idをもつ、という実装も出来ますが、「プロフィールを未登録」の場合に

- users.profile_idをnullにする実装にする
- users.id = profies.user_idとなるprofilesのレコードがないだけ
のどちらにするかと言う設計思想の違いですね。

SQLで書くとこうです。

```sql
select users.id, profiles.自己紹介文, profiles.好きな食べ物
from users inner join profiles on users.id = profiles.user_id
```

コードは後ほど見ていきます！


### hasMany

hasManyは1:nの関係を表す場合に使用します。
例えば、ユーザの職歴を保持するテーブル(careers)があるとします。

- careers(id, user_id, 入社年度, 会社名, etc...)

この場合、usersは複数の職歴(career)レコードを持てるので、
users.id と careers.user_id で joinしてSelectすれば取得できますね。
hasOneとの違いは、この時複数件のレコードがとれてくる、と言う部分のみです。

SQLで書くと、通常はこうです。

```sql
select users.id, careers.入社年度, careers.会社名
from users inner join careers on users.id = careers.user_id
```

:warning:実際にはCakePHPの場合、usersのselectとcareersのselectがそれぞれ一回、計2回のselectが発行される実装になっています。

コードは後ほど見ていきます！


## :large_blue_circle: 今回の内容

今回は、以下の機能を実現します。

### 機能リスト

- Todoの追加
	- TODOの内容に加え「そのTODOを誰が担当するか」を全ユーザのリストから選択して指定する
	- ユーザのリストは、ログインユーザがデフォルトで選択状態とする。
- TODO一覧の表示
	- TODO追加を実行したユーザを「オーナ」、TODOを割り当てたユーザを「担当」として表示します。
	- 下記の操作制限を実装します。
		- 「削除」はオーナのみ可能。ログインユーザがオーナでないTODOの行の「削除」リンクは表示しない。
		- 「詳細」の表示はオーナまたは担当のみ可能。それ以外の場合は「詳細」リンクは表示しない。
- TODO詳細
	- 担当者の変更を可能とする。


### 画面

- TODO一覧画面

![todo_lists.png](https://qiita-image-store.s3.amazonaws.com/2006/65988/8040158b-824b-0de4-2bf5-bc97bec4c0ec.png "todo_lists.png")

- TODO詳細画面

![todo_detail.png](https://qiita-image-store.s3.amazonaws.com/2006/65988/1545828c-0ff0-cf4e-8df8-26cd3751dd7a.png "todo_detail.png")


## :large_blue_circle: 事前準備

事前準備は毎回同じなので、別エントリにまとめています。
[全12回の勉強会でやっているGitの使い方 - AWS上で構築するRESTfulアプリ勉強会～Web開発ワークショップ～ - Qiita](http://qiita.com/k_shimoji/items/6a66dbb93b0553fa7c7f)を参照してください。
やることは、

- gitのブランチを整えて今回用ブランチ`vol/06`を作成する

です。まずこれをやりましょう。
***それと、前回usersテーブルを追加しましたが、前回不参加の方はそっちもやっておく必要があります。[こちら](http://qiita.com/k_shimoji/items/ae5a02bd0c7e03feae52#%E3%83%A6%E3%83%BC%E3%82%B6%E7%99%BB%E9%8C%B2%E7%94%A8%E3%81%AE%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E4%BD%9C%E6%88%90)を参照下さい。***

今回はさらに、
todo_listsテーブルに、

- オーナのユーザID(owner)
- 担当のユーザID(assignee)

を追加します。

### TODO一覧テーブルへの列追加


まず、phpMyAdminにログインしましょう。

URLは、`http://(PublicIP)/phpMyAdmin/`です。
変更していなければ、ユーザ名`study`、パスワード`study`でログインできます。


##### 各列の設定

|名前|データ型|長さ/値|デフォルト値|照合順序|属性|NULL|インデックス|A_I(AutoIncrement)|コメント|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--
| owner |INT|10|なし|　|UNSIGNED|　|　| 　|オーナ
| assignee |INT|10|なし|　|UNSIGNED|　|　|　|担当

#### 準備

では、準備を始めましょう！

- :white_check_mark: Gitのブランチを整えて、`vol/06`ブランチを作成。
- :white_check_mark: TODO一覧(todo_lists)テーブルに列を追加。

準備ができたら、Lesson1です！


## :large_blue_circle: Lesson1 サーバサイド

Lesson1では、サーバサイドのプログラムを実装します。
プログラムのテストには、第1回でインストールした「POSTMAN」を使用してAPIの動作確認を行います。

### ポイント

今回のテーマは「アソシエーション」ですが、前述の3つのうち、実は「belongsTo」だけで今回の機能は実現できてしまいます。
それだけでは面白く無いので、「hasOne」と「hasMany」も試してみます。
まずは、belognsToを使用して、アプリケーションの実装をします。

### 編集するファイル一覧

|編集|file|編集概要|
|---|:----------------------------------|:----------------------|
|修正| app/Controller/TodoListsController.php |findのパラメータの設定
|修正| app/Model/TodoList.php |アソシエーションの設定
|修正| app/Controller/UsersController.php   |ユーザ一覧の取得


#### TodoListsController.php

TodoListモデルのfind関数実行じのパラメータを設定します。

```diff:TodoListsController.php
〜略〜
 class TodoListsController extends AppController {
 
 	public function index() {
-		$res = $this->TodoList->find('all');
+		$query = array (
+			'fields' => array (
+				'TodoList.id',
+				'TodoList.todo',
+				'TodoList.status',
+				'Owner.id',
+				'Owner.name',
+				'Assignee.id',
+				'Assignee.name'
+			),
+			'order' => "TodoList.id"
+		);
+		$res = $this->TodoList->find('all', $query);
+		// 整形
+		if (count($res) > 0) {
+		    $loginUserId = $this->Auth->user()['id'];
+			foreach ( $res as $key => $row ) {
+			    //「ログインユーザがオーナである」フラグ
+				$res[$key]['TodoList']['owned'] = $row['Owner']['id'] === $loginUserId;
+			    //「ログインユーザが担当である」フラグ
+				$res[$key]['TodoList']['assigned'] = $row['Assignee']['id'] === $loginUserId;
+			}
+		}
 		$this->set(compact('res'));
 		$this->set('_serialize', 'res');
 	}
 
 	public function view($id = null) {
 		$res = $this->TodoList->findById($id);
 		$this->set(compact('res'));
 		$this->set('_serialize', 'res');
 	}
 
 	public function add() {
 		$data = $this->request->data;
+		$data['owner'] = $this->Auth->user()['id'];
 		$res = $this->TodoList->save($data);
 		$this->set(compact('res'));
 		$this->set('_serialize', 'res');
 	}

〜以下略〜
```

#### `find`関数に引数追加

- 第二引数`$query`を追加しています。`$query`は配列で、下記を設定しています。
	- `fields`
		- 取得するテーブルの列。これを指定しないと、ユーザのパスワード等もとれてしまうため、必要な物のみ設定しています。
	- `order`
		- ついでにデータの並び順も指定しました。todo_listsテーブルのidの昇順です。

#### 取得したデータの整形

- 取得した1件のレコードについて、
	- ログインユーザがオーナである場合、`owned = true`に設定、そうでない場合`owned = false`に設定。
	- ログインユーザが担当である場合、`assigned = true`に設定、そうでない場合`assigned = false`に設定。

#### TodoList.php

アソシエーションの設定

```diff:TodoList.php
 class TodoList extends AppModel {
+	public $belongsTo = array (
+		'Owner' => array (
+			'className' => 'User',
+			'foreignKey' => 'owner',
+		),
+		'Assignee' => array (
+			'className' => 'User',
+			'foreignKey' => 'assignee'
+		)
+	);
 }
```

#### belongsToアソシエーション

- TODO一件に対する、
	- オーナのユーザ情報
	- 担当のユーザ情報

を設定しています。詳細は下記の通り。

- モデルの変数に`$belongsTo`という変数名で配列を設定します。
	- `Owner`(オーナ), `Assignee`(担当)という名前で2つのusersテーブルを紐つけています。
	- その下のパラメータ詳細(`Owner`)
		- `className` -> モデル名(User)
		- usersに対する外部キー(`todo_lists.owner`を指定)
	- その下のパラメータ詳細(`Assignee`)
		- `className` -> モデル名(User)
		- usersに対する外部キー(`todo_lists.assignee`を指定)

#### addメソッド

- `owner`にログインユーザのIDを設定しています。
	- これで、TODOの追加操作をした際のログインユーザがオーナとして設定されます。

### UsersController.php

ログイン、ログイン済みチェック、ログアウト、ユーザ登録の実装です。

```diff:UsersController.php
 class UsersController extends AppController {
 
+	public function index() {
+		$res = $this->User->find('all', array (
+			'fields' => array (
+				'User.id',
+				'User.username',
+				'User.name'
+			)
+		));
+		$this->set(compact('res'));
+		$this->set('_serialize', 'res');
+	}
+
 	public function login() {
 		$user = $this->Auth->user();
 		$res = array();

〜以下略〜
```

#### `index`関数

- `TodoListController.php`と同じ容量で、`find`関数で取得するフィールドを絞っています。

###　動作確認

POSTMANで、TODO一覧取得API(`http://(PublicIP)/rest-study/todo_lists.json`)をテストします。
下記のように取得できればOKです。
belongsToが効いています。

```json
[
    {
        "TodoList": {
            "id": "62",
            "todo": "牛乳を買う",
            "status": "0",
            "owned": true,
            "assigned": true
        },
        "Owner": {
            "id": "67",
            "name": "山田太郎"
        },
        "Assignee": {
            "id": "67",
            "name": "山田太郎"
        }
    },
    {
        "TodoList": {
            "id": "78",
            "todo": "犬の散歩",
            "status": "0",
            "owned": true,
            "assigned": false
        },
        "Owner": {
            "id": "67",
            "name": "山田太郎"
        },
        "Assignee": {
            "id": "68",
            "name": "田中花子"
        }
    },
    {
        "TodoList": {
            "id": "83",
            "todo": "家に電話",
            "status": "1",
            "owned": false,
            "assigned": false
        },
        "Owner": {
            "id": "68",
            "name": "田中花子"
        },
        "Assignee": {
            "id": "68",
            "name": "田中花子"
        }
    }
]
```


### 実装

では、実際に修正して上記のテストをしてみましょう。

- :white_check_mark: `app/Controller/TodoListsController.php` を上記の通り修正。
- :white_check_mark: `app/Model/TodoList.php` を上記の通り修正。
- :white_check_mark: `app/Controller/UsersController.php` を上記の通り修正。
- :white_check_mark: 動作確認!
- :white_check_mark: Gitにコミット


**GitHubでのdiff表示へのリンク**
[第6回 Lesson1 サーバサイド · suzukishouten-study/rest-study@d4fe1a4](https://github.com/suzukishouten-study/rest-study/commit/d4fe1a4c8428b6cf9ecf6b29256537e2741210ca)

Lesson1.5へ!

## :large_blue_circle: Lesson1.5 他のアソシエーションを試す。

今回実装するアプリケーションとしてはLesson1の実装でOKです。
Lesson2のクライアントの実装に入る前に、`hasOne`と`hasMany`のアソシエーションも試してみましょう。

ソースを修正して動作確認後、元に戻してLesson2へ行きましょう。


テストでは、下記のデータの状態で実行した結果を載せています。

![testdata.png](https://qiita-image-store.s3.amazonaws.com/2006/65988/53a69baa-57cb-fe9e-47c0-529e6d32fb9d.png "testdata.png")

:warning:ユーザは、

- 山田太郎
- 田中花子
- 佐藤一郎

が登録されていますが、佐藤一郎さんはまだTODOが1件もない(自分がオーナのものも、担当としてアサインされたものも）状態です。

### hasOneをテストする

あるユーザがオーナとなっているTODOを取得するケースを考えます。
:warning:`user : todo` は `1 : n`になるので本来は`hasMany`を使用する場面です。
ちょっと無理矢理ですが、「田中花子さん」がオーナのTODOは1件なので、田中花子さんのTODOがちゃんと取れるか確認してみましょう。

#### ソース修正箇所

#####`app/Controller/UsersController.php`の`index`関数を修正。

```php:app/Controller/UsersController.php
	public function index() {
	    $res = $this->User->find('all');	//fieldsの指定を外す。下記はコメントアウト
// 	    $res = $this->User->find('all', array (
// 			'fields' => array (
// 				'User.id',
// 				'User.username',
// 				'User.name'
// 			)
// 		));
		$this->set(compact('res'));
		$this->set('_serialize', 'res');
	}
```

- 


`app/Model/User.php `にアソシエーションを設定。

```php:app/Model/User.php
class User extends AppModel {
	public $hasOne = array (
		'TodoList' => array (
			'className' => 'TodoList',
			'foreignKey' => 'owner'
		)
	);
}
```

さて、ユーザ取得API(`http://(PublicIP)/rest-study/users.json`)を叩いてみましょう！

```json:実行結果
[
    {
        "User": {
            "id": "67",
            "username": "yamada",
            "password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
            "name": "山田太郎"
        },
        "TodoList": {
            "id": "62",
            "todo": "牛乳を買う",
            "status": "0",
            "owner": "67",
            "assignee": "67"
        }
    },
    {
        "User": {
            "id": "67",
            "username": "yamada",
            "password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
            "name": "山田太郎"
        },
        "TodoList": {
            "id": "78",
            "todo": "犬の散歩",
            "status": "0",
            "owner": "67",
            "assignee": "68"
        }
    },
    {
        "User": {
            "id": "68",
            "username": "tanaka",
            "password": "$2a$10$ig3ul1GQlZPbQDODLt6.SuYqJVv94wNlZJwMqSPvK5H/p2qEBih6e",
            "name": "田中花子"
        },
        "TodoList": {
            "id": "83",
            "todo": "家に電話",
            "status": "1",
            "owner": "68",
            "assignee": "68"
        }
    },
    {
        "User": {
            "id": "69",
            "username": "sato",
            "password": "$2a$10$rBdmyhZHwlv4h.EvacI34OSRuE9Q3t7pYx1HysS1zxw99NOGLUVBi",
            "name": "佐藤一郎"
        },
        "TodoList": {
            "id": null,
            "todo": null,
            "status": null,
            "owner": null,
            "assignee": null
        }
    }
]
```

ユーザに対して1件のTODOが紐ついていますね！
1件もTODOを持っていない場合は、各項目がnullの配列が一個出来るようです。

この時実行されたSQLは下記です。hasManyでは一回の実行でSQLが2回実行されます。

```sql
SELECT
	`User`.`id`,
	`User`.`username`, 
	`User`.`password`, 
	`User`.`name`, 
	`TodoList`.`id`, 
	`TodoList`.`todo`, 
	`TodoList`.`status`, 
	`TodoList`.`owner`, 
	`TodoList`.`assignee` 
FROM `study`.`users` AS `User` 
		LEFT JOIN `study`.`todo_lists` AS `TodoList` ON (`TodoList`.`owner` = `User`.`id`)  
WHERE 1 = 1
```

謎の`WHERE 1 = 1`がついてますがこういうSQLです。普通にJOINしてるだけですね。


### hasManyをテストする

`user : todo` は `1 : n`になるので本来の`hasMany`の使いドコロです。

### ソーズ修正箇所

`app/Controller/UsersController.php`はhasOneで修正した状態で置いておきましょう。

`app/Model/User.php `にアソシエーションを設定。

```php:app/Model/User.php
class User extends AppModel {
	public $hasMany = array (
		'TodoList' => array (
			'className' => 'TodoList',
			'foreignKey' => 'owner'
		)
	);
}
```

hasOneをhasManyに変えただけです！
では実験。

```json:実行結果
[
    {
        "User": {
            "id": "67",
            "username": "yamada",
            "password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
            "name": "山田太郎"
        },
        "TodoList": [
            {
                "id": "62",
                "todo": "牛乳を買う",
                "status": "0",
                "owner": "67",
                "assignee": "67"
            },
            {
                "id": "78",
                "todo": "犬の散歩",
                "status": "0",
                "owner": "67",
                "assignee": "68"
            }
        ]
    },
    {
        "User": {
            "id": "68",
            "username": "tanaka",
            "password": "$2a$10$ig3ul1GQlZPbQDODLt6.SuYqJVv94wNlZJwMqSPvK5H/p2qEBih6e",
            "name": "田中花子"
        },
        "TodoList": [
            {
                "id": "83",
                "todo": "家に電話",
                "status": "1",
                "owner": "68",
                "assignee": "68"
            }
        ]
    },
    {
        "User": {
            "id": "69",
            "username": "sato",
            "password": "$2a$10$rBdmyhZHwlv4h.EvacI34OSRuE9Q3t7pYx1HysS1zxw99NOGLUVBi",
            "name": "佐藤一郎"
        },
        "TodoList": []
    }
]
```

こうなりました！ちゃんとユーザに対してTODOが複数件紐ついています。
これがhasManyですね。
この時実行されたSQLは下記2つです。hasManyでは一回の実行でSQLが2回実行されます。

```sql:1回目
SELECT `User`.`id`, `User`.`username`, `User`.`password`, `User`.`name` FROM `study`.`users` AS `User`   WHERE 1 = 1
```

```sql:2回目
SELECT `TodoList`.`id`, `TodoList`.`todo`, `TodoList`.`status`, `TodoList`.`owner`, `TodoList`.`assignee` FROM `study`.`todo_lists` AS `TodoList`   WHERE `TodoList`.`owner` IN (67, 68, 69)
```

2回目は、1回目で取得したuserのidをownerに持つTodoListをselectしています。
こういう実装になってるんですね。

では、修正したところを元に戻して、lesson2、 画面を作りましょう。

## :large_blue_circle: Lesson2 クライアントサイド

Lesson2では、クライアントサイドのプログラムを実装します。

### ポイント

前回までの知識でほぼ開発できますが、今回、下記の「ユーザ一覧を取得して表示する」部分だけ少し解説します。

- TODO一覧画面のユーザ一覧表示

![userlist_todo_list.png](https://qiita-image-store.s3.amazonaws.com/2006/65988/431e3918-7146-0264-5695-7e26900b639e.png "userlist_todo_list.png")

- TODO詳細画面のユーザ一覧表示

![userlist_todo_detail.png](https://qiita-image-store.s3.amazonaws.com/2006/65988/894b5c3f-77e2-2e75-228b-9f6fefb9971b.png "userlist_todo_detail.png")


#### 今回の実装

Backbone、Marionetteをフル活用するなら、本来はユーザ一覧表示部分をViewにし、「Behavior」機能を使うなどして共通化するといい感じなのですが、
今回のメインは「CakePHPのアソシエーション」なので、ちょっと簡単（手抜き？）の実装になっています。
ご了承下さい。

具体的には、

- ユーザ取得API(`http://(PublicIP)/rest-study/users.json`)を叩いてcollectionにユーザ一覧を取得
- そのcollectonをループで処理し、テンプレートで用意している`<select></select>`タグの中に、`<option value="ユーザのid">ユーザの氏名</option>`というタグをJQueryを使ってはめ込んでいく。

というやり方です。

これを、TODO一覧とTODO詳細の両画面で「コピペ」しています。
(DRYではないですが今回はご勘弁！)

では、ソース解説です。


### 編集するファイル一覧

|編集|file|編集概要|
|---|:------------------------------------------------|:----------------------|
|修正|app/View/Layouts/default.ctp                    |テンプレートの修正
|新規|app/webroot/js/collections/user-collection.js   |ユーザ一覧用collection
|修正|app/webroot/js/models/todo-model.js             |追加項目のparse
|修正|app/webroot/js/views/todo-layout-view.js        |ユーザ一覧読み込み処理
|修正|app/webroot/js/views/todo-composite-view.js     |ユーザ一覧表示
|修正|app/webroot/js/views/todo-item-view.js          |削除・詳細ボタンの表示制御
|修正|app/webroot/js/views/todo-detail-layout-view.js |ユーザ一覧読み込み処理
|修正|app/webroot/js/views/todo-detail-item-view.js   |ユーザ一覧表示


### default.ctp

テンプレートを修正。

```diff:default.ctp

〜　略　〜
  <title>シンプルTODOアプリ</title>
 </head>
 <body>
 	<!-- ヘッダ -->
 	<div id="header"></div>
 	<!-- コンテンツ -->
 	<div id="main"></div>

〜　略　〜
 
 	<!-- TODO一覧表示のテンプレート -->
 	<script type="text/template" id="todo-composite-template">
 	<textarea style="width:300px;height:50px"id="new-todo" placeholder="Todo?" autofocus></textarea>
+	<select name="assignee" id="user-list">
 	<input type="button" id="addTodo" value="追加">
 	<hr>
 	<div>
 		<table border="1" width="350px">
+			<thead>
+				<tr>
+					<td>完了</td>
+					<td>ToDo</td>
+					<td>オーナ</td>
+					<td>担当</td>
+					<td colspan="2"></td>
+				</tr>
+			</thead>
 			<tbody></tbody>
 		</table>
 	</div>
 	</script>
 
 	<!-- TODO一行分のテンプレート（上のtbody部分に挿入される） -->
 	<script type="text/template" id="todo-item-template">
 	<td><input type="checkbox" class="toggle" <%- status === '1' ? 'checked' : '' %>></td>
 	<td style="margin:0px">
 		<span class="todo-edit" style="margin:0px"><%- todo %></span>
 	</td>
 	<td>
+		<span><%- Owner.name %></span>
+	</td>
+	<td>
+		<span><%- Assignee.name %></span>
+	</td>
+	<td>
 		<a class="remove-link" href="#">削除</a>
+	</td>
+	<td>
 		<a class="detail-link" href="#todo-lists/<%- id %>">詳細</a>
 	</td>
 	</script>
 
 	<!-- 詳細画面のレイアウトテンプレート -->
 	<script type="text/template" id="todo-detail-layout-template">
 	<div id="todo-item"></div>
 	</script>
 
 	<!-- 詳細画面の表示内容テンプレート -->
 	<script type="text/template" id="todo-detail-item-template">
 	<h2>Todo #<%- id %></h2>
 	<div>
 	<textarea style="width:300px;height:50px" id="edit-todo" autofocus placeholder="Todo?"><%- todo %></textarea>
+	<select name="assignee" id="user-list">
+	</select>
 	<input type="button" id="updateTodo" value="更新"></input>
 	<input type="button" id="updateCancel" value="キャンセル"></input>
 	</div>
 	</script>
 
〜　略　〜
```

下記の通り修正。

- TODO一覧画面
	- ユーザ一覧表示用`<select></select>`を追加
	- tableに`<thead></thead>`を追加(項目が増えてきたので)
	- [削除]と[詳細]を別の`<td></td>`に分割
- TODO詳細画面
	- ユーザ一覧表示用`<select></select>`を追加

### user-collection.js

```js:user-collection.js
//User一覧表示用コレクション
define(function(require) {
	var UserModel = require('models/user-model');

	var UserCollection = Backbone.Collection.extend({
		url : '/rest-study/users.json',
		model : UserModel,

		parse : function(response) {
			return response;
		}
	});
	
	return UserCollection;
});
```

必要最低限のcollectionの実装です。
これはもう説明不要ですね！


### todo-model.js

追加項目の`Owner`と`Assignee`をパースしています。

```diff:todo-model.js
 //Todoデータ１件を表すモデル
 define(function() {
 	var TodoModel = Backbone.Model.extend({
 		urlRoot : '/rest-study/todo_lists',
 		parse : function(response) {
 			//モデルをパース
 			console.log("モデルをパース");
 			console.log(response);
-			return response.TodoList;
+			var parsed = response.TodoList;
+			if (response.Owner) {
+				parsed.Owner = response.Owner;
+				parsed.Assignee = response.Assignee;
+			}
+			return parsed;
 		},
 		toggle : function() {
 			this.set('status', this.get("status") === '1' ? '0' : '1');
 			this.save();
 		}
 	});
 	return TodoModel;
 });
```

サーバからはCakePHP任せの下記フォーマットでTODO1件のJSONが来るので、

```json:サーバから来た生の状態
{
    "TodoList": {
        "id": "62",
        "todo": "牛乳を買う",
        "status": "0",
        "owned": false,
        "assigned": false
    },
    "Owner": {
        "id": "67",
        "name": "山田太郎"
    },
    "Assignee": {
        "id": "67",
        "name": "山田太郎"
    }
}
```

こんなフォーマットに変更しています。

```json:クライアントで処理しやすいよう整形
{
    "id": "62",
    "todo": "牛乳を買う",
    "status": "0",
    "owned": false,
    "assigned": false,
    "Owner": {
        "id": "67",
        "name": "山田太郎"
    },
    "Assignee": {
        "id": "67",
        "name": "山田太郎"
    }
}
```

### todo-layout-view.js

ユーザ一覧読み込み処理を追加しています。

```diff:todo-layout-view.js
 define(function(require){
 	var TodoCompositeView = require('views/todo-composite-view');
 	var TodoCollection = require('collections/todo-collection');
+	var UserCollection = require('collections/user-collection');
 
 	var TodoLayoutView = Marionette.LayoutView.extend({
 		//テンプレート
 		template: '#todo-layout-template',
 
 		regions : {
 			listRegion : '#todo-lists',
 		},
 
-		onRender : function(){
+		onRender : function() {
+			this.userCollection = new UserCollection();
+			this.listenTo(this.userCollection, 'reset', this.onLoadUsers, this);
+			this.userCollection.fetch({reset : true});
+		},
+
+		onLoadUsers : function(userCollection) {
 			var todoCollection = new TodoCollection();
 			this.listenTo(todoCollection , 'reset', this.showTodoList, this);
 			todoCollection.fetch({reset : true});
 		},
 
 		showTodoList : function(todoCollection){
 			this.listRegion.show( new TodoCompositeView({
-				collection : todoCollection
+				collection : todoCollection,
+				userList : this.userCollection.models
 			}));
 		},
 
 	});
 	return TodoLayoutView;
 });
```

- `collections/user-collection`を`require`で読み込む
- `onRender`関数内で、`this.userCollection.fetch()`でサーバからユーザ一覧取得。
- fetchが終わると(ListenToが効いて)`onLoadUsers`関数を実行、続いて`showTodoList`が実行される
- `showTodoList`関数内で、`TodoCompositeView`の初期化時に、取得したユーザ一覧のcollectionを渡すようにしている
	- このユーザ一覧collectionを`TodoCompositeView`の処理で表示するわけです。




### `todo-composite-view.js`

ユーザ一覧を表示します。

```diff:todo-composite-view.js
 //Todo一覧表示用ビュー
 define(function(require) {
 	var TodoItemView = require('views/todo-item-view');
 
 	var TodoCompositeView = Marionette.CompositeView.extend({
 		template: '#todo-composite-template',
 
 		childView : TodoItemView,
 
 		childViewContainer : 'tbody',
 
 		ui : {
 			addTodo : '#addTodo',
-			newTodo : '#new-todo'
+			newTodo : '#new-todo',
+			userList : '#user-list'
 		},
 
 		events : {
 			'click @ui.addTodo' : 'onCreateTodo',
 		},
 
-		initialize: function(){
+		initialize: function(options){
 			_.bindAll( this, 'onCreatedSuccess' );
+			this.userList = options.userList;
 		},
 
+		onRender : function() {
+			//ユーザ一覧を表示
+			this.showUserList(this.ui.userList, this.userList);
+			//ログインユーザをデフォルトで選択状態にする
+			this.ui.userList.val(window.application.loginUser.id);
+		},
+					
+		//ユーザ一覧を表示
+		showUserList : function($list, userList){
+			$.each(userList, function(index, userModel) {
+				$list.append(
+					"<option value='" 
+					+ userModel.attributes.id + "'>"
+					+ userModel.attributes.name + "</option>");
+			});
+		},
+			
 		onCreateTodo : function() {
 			this.collection.create(this.newAttributes(), {
 		          silent:  true ,
 		          success: this.onCreatedSuccess
 			});
 			this.ui.newTodo.val('');
 		},
 
 		newAttributes : function() {
 			return {
 				todo : this.ui.newTodo.val().trim(),
-				status : 0
+				status : 0,
+				assignee : this.ui.userList.val()
 			};
 		},
 
 		onCreatedSuccess : function(){
 			this.collection.fetch({ reset : true });
 		},
 
 	});
 	return TodoCompositeView;
 });
```


- ユーザ一覧のセレクトボックスを処理するため`ui`変数に`userList`を追加。
- `initialize`関数内で、todo-layout-viewから渡されたユーザ一覧collection(`options.userList`に入ってくる)を自身の中(this.userListに保持)
- `onRender`関数内で、`showUserList`関数を実行してユーザ一覧の表示を行う
- ユーザ一覧表示後、ログイン時に細んしている`window.application.loginUser`のユーザ情報からログインユーザのidを取得し、ユーザ一覧表示で自分（ログインユーザ）をデフォルトで選択された状態に設定。
- `showUserList`関数では、jqueryを使用し、`ui`変数で定義済みの`<select></select>`タグの中に`<option></option>`タグを埋め込んでいます。
- todoの新規追加時(`newAttributes`関数)は、ユーザ一覧で選択したユーザを担当者(`assignee`)をして設定します。

### todo-item-view.js

削除・詳細ボタンの表示制御を行っています。

```diff:todo-item-view.js
 //Todo一覧の1件表示用ビュー
 define(function(){
 	var TodoItemView = Marionette.ItemView.extend({
 		//DOMに要素追加のタグ名
 		tagName : 'tr',
 
 		//テンプレート
 		template : '#todo-item-template',
 
 		ui : {
 			checkBox : '.toggle',
-			removeLink : '.remove-link'
+			removeLink : '.remove-link',
+			detailLink : '.detail-link'
 		},
 
 		//DOMイベントハンドラ設定
 		events : {
 			//チェックボックスクリック時
 			'click @ui.checkBox' : 'onStatusToggleClick',
 			//削除ボタンクリック時
 			'click @ui.removeLink' : 'onRemoveClick',
 		},
 
 		onStatusToggleClick : function() {
 			this.model.toggle();
 		},
 
 		onRemoveClick : function() {
 			this.model.destroy({
 				wait : true
 			});
 		},
 
+		onRender : function() {
+			if (!this.model.attributes.owned) {
+				//オーナでない場合は削除リンク非表示
+				this.ui.removeLink.css({
+					display : 'none'
+				});
+				if (!this.model.attributes.assigned) {
+					//担当者でもない場合は、
+					//詳細リンクも非表示
+					this.ui.detailLink.css({
+						display : 'none'
+					});
+					//チェックボックスも変更不可
+					this.ui.checkBox.prop('disabled', true);
+				}
+			}
+		}
 	});
 	return TodoItemView;
 }); 
```

- `ui`変数に削除リンク用の変数`detailLink`を追加
- `onRender`関数で、削除、詳細の各リンクの表示制御を下記の仕様で行っています。
	- オーナでない場合は削除リンクを非表示にする
		- さらに、担当者でもない場合は、
			- 詳細リンクも表示にする
			- チェックボックスをdisableにして変更不可とする

※表示制御の実装

- リンクの非表示
	- jqueryのcss関数で、`display : 'none'`のスタイルを設定
- チェックボックスの変更不可
	- jqueryのprop関数で、`disabled`を`true`に設定

### todo-detail-layout-view.js

ユーザ一覧読み込み処理を追加しています。
やっていることは、`todo-layout-view.js`とほぼ同じです。

```diff:todo-detail-layout-view.js
 define(function(require) {
     var TodoDetailItemView = require('views/todo-detail-item-view');
     var TodoModel = require('models/todo-model');
+    var UserCollection = require('collections/user-collection');
 
 	var TodoDetailLayoutView = Marionette.LayoutView.extend({
 		//テンプレート
 		template : '#todo-detail-layout-template',
 
 		regions : {
 			itemRegion : '#todo-item',
 		},
 
 		onRender : function() {
-			var todoModel = new TodoModel({
-				id : this.options.modelId
-			});
-			//モデルのサーバからのデータ取得完了時、描画を行う
-			this.listenTo(todoModel, 'sync', this.showItem, this);
-			//サーバからデータ取得
-			todoModel.fetch({
-				wait : true
+			this.userCollection = new UserCollection();
+			this.listenToOnce(this.userCollection, 'reset', this.onLoadUsers, this);
+			this.userCollection.fetch({
+				reset : true
 			});
 		},
 
-		showItem : function(todoModel) {
-			this.itemRegion.show( new TodoDetailItemView({
-				model : todoModel
-			}));
-		},
+		onLoadUsers : function(userCollection){
+ 			var todoModel = new TodoModel({
+ 				id : this.options.modelId
+ 			});
+ 			//モデルのサーバからのデータ取得完了時、描画を行う
+			this.listenToOnce(todoModel, 'sync', this.showItem, this);
+ 			//サーバからデータ取得
+ 			todoModel.fetch({
+ 				wait : true
+ 			});
+ 		},
+
+ 		showItem : function(todoModel) {
+ 			this.itemRegion.show(new TodoDetailItemView({
+ 				model : todoModel,
+ 				userList : this.userCollection.models
+ 			}));
+ 		},
 
 	});
 	return TodoDetailLayoutView;
 });
```

- `collections/user-collection`を`require`で読み込む
- `onRender`関数内で、`this.userCollection.fetch()`でサーバからユーザ一覧取得。
	- 元々`onRender`内で行っていた、model1件のサーバからの取得処理は、`onLoadUsers`内に移動しています。
-　fetchが終わると(ListenToが効いて)`onLoadUsers`関数を実行、`todoModel.fetch()`でTODO1件の取得を行う。
- `showItem`関数内で、`TodoDetailItemView`の初期化時に、取得したユーザ一覧のcollectionを渡すようにしている
	- このユーザ一覧collectionを`TodoDetailItemView`の処理で表示します。


### `todo-detail-item-view.js`

ユーザ一覧を表示します。

```diff:todo-detail-item-view.js
 //詳細ビュー
 define(function() {
 	var TodoDetailItemView = Marionette.ItemView.extend({
 
 		//テンプレート
 		template: "#todo-detail-item-template",
 
 		ui : {
 			todoStatus   : '#edit-todo',
 			updateButton : '#updateTodo',
-			cancelButton : '#updateCancel'
+			cancelButton : '#updateCancel',
+			userList     : '#user-list'
 		},
 
 		//DOMイベントハンドラ設定
 		events : {
 			//更新ボタンクリック時
 			'click @ui.updateButton' : 'onUpdateClick',
 			//キャンセルボタンクリック時
 			'click @ui.cancelButton' : 'onCancelClick',
 		},
 
 		//初期化
-		initialize: function(){
+		initialize: function(options){
 			_.bindAll( this, 'onSaveSuccess' );
+			this.userList = options.userList;
+		},
+
+		onRender : function() {
+			//ユーザ一覧を表示
+			this.showUserList(this.ui.userList, this.userList);
+			//担当者を選択状態にする
+			this.ui.userList.val(this.model.attributes.assignee);
+		},
+
+		//ユーザ一覧を表示
+		showUserList : function($list, userList){
+			$.each(userList, function(index, userModel) {
+			$list.append(
+				"<option value='" 
+				+ userModel.attributes.id + "'>"
+				+ userModel.attributes.name + "</option>");
+			});
 		},
 
 		//更新ボタンクリックのイベントハンドラ
 		onUpdateClick : function() {
 			//テキストボックスから文字を取得
-			var todoString = this.ui.todoStatus.val();
+			var todoString = this.ui.todoStatus.val();  // Todo
+			var assigneeId = this.ui.userList.val();	// 担当者
 			this.model.save({
-				todo : todoString
+				todo : todoString,
+				assignee : assigneeId
 			}, {
 				silent : true,
 				success : this.onSaveSuccess,
 			});
 		},
 
 		//キャンセルボタンクリックのイベントハンドラ
 		onCancelClick : function() {
 			this.backTodoLists();
 		},
 
 		//更新成功
 		onSaveSuccess : function() {
 			this.backTodoLists();
 		},
 
 		//TODOリスト画面に戻る
 		backTodoLists : function() {
 			Backbone.history.navigate('#todo-lists', true);
 		}
 
 	});
 	return TodoDetailItemView;
 });
```

ユーザ一覧の表示に関しては、`todo-composite-view.js`でやったことと同様です。


- ユーザ一覧のセレクトボックスを処理するため`ui`変数に`userList`を追加。
- `initialize`関数内で、todo-layout-viewから渡されたユーザ一覧collection(`options.userList`に入ってくる)を自身の中(this.userListに保持)
- `onRender`関数内で、`showUserList`関数を実行してユーザ一覧の表示を行う
- ユーザ一覧表示後、表示するTODOの担当者のユーザID(modelの`assignee`から取得)を選択された状態に設定。
- `showUserList`関数は、`todo-composite-view.js`内の同関数のコピペです！（今回はこれでご了承を！）
- 更新ボタンクリック時の処理(`onUpdateClick`関数)では、ユーザ一覧で選択したユーザを担当者(`assignee`)をして設定しています。



### 実装

では、実際に修正して画面の動作確認をしてみましょう。


- :white_check_mark: `app/View/Layouts/default.ctp`を上記の通り修正。
- :white_check_mark: `app/webroot/js/collections/user-collection.js`を上記の通り修正。
- :white_check_mark: `app/webroot/js/models/todo-model.js`を上記の通り修正。
- :white_check_mark: `app/webroot/js/views/todo-layout-view.js`を上記の通り修正。
- :white_check_mark: `app/webroot/js/views/todo-composite-view.js`を上記の通り修正。
- :white_check_mark: `app/webroot/js/views/todo-item-view.js`を上記の通り新規作成。
- :white_check_mark: `app/webroot/js/views/todo-detail-layout-view.js`を上記の通り新規作成。
- :white_check_mark: `app/webroot/js/views/todo-detail-item-view.js`を上記の通り新規作成。
- :white_check_mark: 動作確認!
- :white_check_mark: Gitにコミット


**GitHubでのdiff表示へのリンク**

[第6回 Lesson1 クライアントサイド · suzukishouten-study/rest-study@f1623dc](https://github.com/suzukishouten-study/rest-study/commit/f1623dce75cbd2d38a4b971ffcb5b48ca6d4b91b)

以上です！

## 次回予告

次回テーマは、[サーバー・クライアントの両面からかけるバリデーション - connpass](http://aws-rest-todo.connpass.com/event/12735/)です。
バリデーション機能を付けて、「ちゃんとした」アプリケーションに成長させていきます。
またぜひご参加ください！

## コメント/フィードバックお待ちしております。

参加者の方も、そうでない方もお気づきの点があればお願い致します。