Edited at

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

More than 3 years have passed since last update.

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


:large_blue_circle: はじめに

本投稿は、2015/6/26に行われた、CakePHPでデータ操作〜担当者アサイン機能の実装 - connpassの内容についてまとめた資料です。

:warning:今後の予定

AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - connpass

今回は、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で書くとこんな感じです。

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で書くとこうです。

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で書くと、通常はこうです。

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


  • TODO詳細画面

todo_detail.png


:large_blue_circle: 事前準備

事前準備は毎回同じなので、別エントリにまとめています。

全12回の勉強会でやっているGitの使い方 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - Qiitaを参照してください。

やることは、


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

です。まずこれをやりましょう。

それと、前回usersテーブルを追加しましたが、前回不参加の方はそっちもやっておく必要があります。こちらを参照下さい。

今回はさらに、

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関数実行じのパラメータを設定します。


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

アソシエーションの設定


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

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


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が効いています。

[

{
"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

Lesson1.5へ!


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

今回実装するアプリケーションとしてはLesson1の実装でOKです。

Lesson2のクライアントの実装に入る前に、hasOnehasManyのアソシエーションも試してみましょう。

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

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

testdata.png

:warning:ユーザは、


  • 山田太郎

  • 田中花子

  • 佐藤一郎

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


hasOneをテストする

あるユーザがオーナとなっているTODOを取得するケースを考えます。

:warning:user : todo1 : nになるので本来はhasManyを使用する場面です。

ちょっと無理矢理ですが、「田中花子さん」がオーナのTODOは1件なので、田中花子さんのTODOがちゃんと取れるか確認してみましょう。


ソース修正箇所


app/Controller/UsersController.phpindex関数を修正。


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にアソシエーションを設定。


app/Model/User.php

class User extends AppModel {

public $hasOne = array (
'TodoList' => array (
'className' => 'TodoList',
'foreignKey' => 'owner'
)
);
}

さて、ユーザ取得API(http://(PublicIP)/rest-study/users.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回実行されます。

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 : todo1 : nになるので本来のhasManyの使いドコロです。


ソーズ修正箇所

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

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


app/Model/User.php

class User extends AppModel {

public $hasMany = array (
'TodoList' => array (
'className' => 'TodoList',
'foreignKey' => 'owner'
)
);
}

hasOneをhasManyに変えただけです!

では実験。


実行結果

[

{
"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回実行されます。


1回目

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



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


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

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

テンプレートを修正。


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


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

追加項目のOwnerAssigneeをパースしています。


todo-model.js

 //Todoデータ1件を表すモデル

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が来るので、


サーバから来た生の状態

{

"TodoList": {
"id": "62",
"todo": "牛乳を買う",
"status": "0",
"owned": false,
"assigned": false
},
"Owner": {
"id": "67",
"name": "山田太郎"
},
"Assignee": {
"id": "67",
"name": "山田太郎"
}
}

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


クライアントで処理しやすいよう整形

{

"id": "62",
"todo": "牛乳を買う",
"status": "0",
"owned": false,
"assigned": false,
"Owner": {
"id": "67",
"name": "山田太郎"
},
"Assignee": {
"id": "67",
"name": "山田太郎"
}
}


todo-layout-view.js

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


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-collectionrequireで読み込む


  • onRender関数内で、this.userCollection.fetch()でサーバからユーザ一覧取得。

  • fetchが終わると(ListenToが効いて)onLoadUsers関数を実行、続いてshowTodoListが実行される


  • showTodoList関数内で、TodoCompositeViewの初期化時に、取得したユーザ一覧のcollectionを渡すようにしている


    • このユーザ一覧collectionをTodoCompositeViewの処理で表示するわけです。




todo-composite-view.js

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


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

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


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関数で、disabledtrueに設定




todo-detail-layout-view.js

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

やっていることは、todo-layout-view.jsとほぼ同じです。


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-collectionrequireで読み込む


  • onRender関数内で、this.userCollection.fetch()でサーバからユーザ一覧取得。


    • 元々onRender内で行っていた、model1件のサーバからの取得処理は、onLoadUsers内に移動しています。
      - fetchが終わると(ListenToが効いて)onLoadUsers関数を実行、todoModel.fetch()でTODO1件の取得を行う。




  • showItem関数内で、TodoDetailItemViewの初期化時に、取得したユーザ一覧のcollectionを渡すようにしている


    • このユーザ一覧collectionをTodoDetailItemViewの処理で表示します。




todo-detail-item-view.js

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


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

以上です!


次回予告

次回テーマは、サーバー・クライアントの両面からかけるバリデーション - connpassです。

バリデーション機能を付けて、「ちゃんとした」アプリケーションに成長させていきます。

またぜひご参加ください!


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

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