§AJAX アクションの追加
ログインできるようになったので、アプリケーションの機能を書いていきましょう。ナビゲーションドローワー、つまりプロジェクトのリストを含むサイドバーに、シンプルに動的な機能を追加するところから始めましょう。
この章では、テストを含むバックエンドの処理から実装します。
§Projects コントローラ
具体的に、以下のいくつかのバックエンド処理を追加します。
- プロジェクトを追加する
- プロジェクトをリネームする
- プロジェクトを削除する
- グループを追加する
app/controllers/Projects.java
と呼ばれるコントローラクラスを作ることから始めます:
package controllers;
import play.*;
import play.mvc.*;
import play.data.*;
import static play.data.Form.*;
import java.util.*;
import models.*;
import views.html.*;
import views.html.projects.*;
@Security.Authenticated(Secured.class)
public class Projects extends Controller {
}
ここで確認しておく重要な事項は、クラス全体をセキュリティ認証機能で注釈したことです。ダッシュボードではメソッドだけを注釈しましたが、クラス内のすべてのメソッドがこのアクションを使わなければならないことを伝えるために、これらのアノテーションをクラスレベルで付与することができます。これにより大量のボイラープレートコードを削減することができ、またメソッドをうっかり注釈し忘れることも防止できます。
それでは、新しいプロジェクトを作成するメソッドを追加しましょう:
public static Result add() {
Project newProject = Project.create(
"New project",
form().bindFromRequest().get("group"),
request().username()
);
return ok(item.render(newProject));
}
request().username()
から返却される、現在ログインしているユーザーが所有する新しいプロジェクトを作成するために、Project
モデルに存在する create
メソッドを使いました。
新しいプロジェクトを表示するために、以前に作成した item
テンプレートを再利用していることにも注目してください。これで、なぜ以前に構造的にテンプレートを作成したのかが分かってきたと思います。このメソッドはページの小さな部分しか表示しませんが、この断片は AJAX アクションで使うので、これで問題ありません。
続いてプロジェクトをリネームするメソッドを追加しようと思いますが、その前にこの機能のセキュリティ要件について考えてみましょう。あるユーザーは、自身がメンバーになっているプロジェクトのリネームだけが許可されるべきです。これを確認するユーティリティメソッドを app/controllers/Secured.java
に書いてみましょう:
public static boolean isMemberOf(Long project) {
return Project.isMember(
project,
Context.current().request().username()
);
}
request()
を取得するために Context.current()
を使ったことに気付いたかもしれません。アクションの中にいない場合でも、この便利な方法でリクエストにアクセスすることができます。内部的には、現在のリクエスト、レスポンス、セッションやその他を見つけるために thread local を使用します。
この isMemberOf
メソッドは、Project
モデルにまだ書いていない新しいメソッドを使っています。実際のところ、Project
オブジェクトにいくつかの新しいメソッドが必要なので、ここで app/models/Project.java
を開いてそれらを追加しましょう:
public static boolean isMember(Long project, String user) {
return find.where()
.eq("members.email", user)
.eq("id", project)
.findRowCount() > 0;
}
public static String rename(Long projectId, String newName) {
Project project = find.ref(projectId);
project.name = newName;
project.update();
return newName;
}
Project
モデルに rename
メソッドを追加したことで、app/controllers/Projects.java
にアクションを実装する準備が整いました:
public static Result rename(Long project) {
if (Secured.isMemberOf(project)) {
return ok(
Project.rename(
project,
form().bindFromRequest().get("name")
)
);
} else {
return forbidden();
}
}
これが引数を受け取るアクションを実装する最初の機会です。この場合、引数はプロジェクトを示す Long 値です。この引数は、後ほど routes ファイルにこのアクション用のルートを追加する際に目にする、このアクション用のパスから引き渡されることになります。
最初に、現在のユーザーがプロジェクトのメンバーであることを確認し、そうでない場合は forbidden
レスポンスを返却しているのが分かります。form
メソッドを使っていることにも気付くでしょう。これは、以前にログインフォームを追加し、バリデーションを行ったときにも目にしてきました。しかし、今回はフォームの内容を展開し、バリデーションを行うための bean を渡していません。その代わりに、ダイナミックフォームと呼ばれるものを使っています。ダイナミックフォームは、投稿されたフォームを文字列のキーと値を持つマップに展開するだけのもので、とくにバリデーションを行いたくない、ひとつまたはふたつの値を投稿するシンプルなフォームを扱うときにとても便利です。
プロジェクトを削除するメソッドに移りましょう:
public static Result delete(Long project) {
if(Secured.isMemberOf(project)) {
Project.find.ref(project).delete();
return ok();
} else {
return forbidden();
}
}
そして最後に、グループを作成するメソッドを追加しましょう:
public static Result addGroup() {
return ok(
group.render("New group", new ArrayList<Project>())
);
}
これでコントローラのメソッドを実装し終えたので、これらコントローラへのルートを conf/routes
に追加しましょう:
POST /projects controllers.Projects.add()
POST /projects/groups controllers.Projects.addGroup()
DELETE /projects/:project controllers.Projects.delete(project: Long)
PUT /projects/:project controllers.Projects.rename(project: Long)
プロジェクトの削除とリネームに、引数が渡されるべき場所を宣言しているのが分かります。パス中において動的な箇所を指定するためにコロンを使っており、この値はアクションの第一引数として引き渡されます。このため、/projects/123
に対して PUT リクエストが送信された場合、Projects.rename(123)
が実行されます。
それでは、ブラウザからアプリケーションをさっとリフレッシュし、コンパイルエラーが発生していないことを確認してください。
§アクションのテスト
認証アクションと同様に、いま書いたばかりのアクション用にテストを書いていきます。test/controllers/ProjectsTest.java
の ProjectsTest
から始めましょう:
package controllers;
import org.junit.*;
import static org.junit.Assert.*;
import java.util.*;
import models.*;
import play.mvc.*;
import play.libs.*;
import play.test.*;
import static play.test.Helpers.*;
import com.avaje.ebean.Ebean;
import com.google.common.collect.ImmutableMap;
public class ProjectsTest extends WithApplication {
@Before
public void setUp() {
start(fakeApplication(inMemoryDatabase(), fakeGlobal()));
Ebean.save((List) Yaml.load("test-data.yml"));
}
}
新しいプロジェクトを作成するアクションのテストを書きましょう:
@Test
public void newProject() {
Result result = callAction(
controllers.routes.ref.Projects.add(),
fakeRequest().withSession("email", "[email protected]")
.withFormUrlEncodedBody(ImmutableMap.of("group", "Some Group"))
);
assertEquals(200, status(result));
Project project = Project.find.where()
.eq("folder", "Some Group").findUnique();
assertNotNull(project);
assertEquals("New project", project.name);
assertEquals(1, project.members.size());
assertEquals("[email protected]", project.members.get(0).email);
}
withSession
を使って Bob をログインさせたことが分かります。そしてその後、リクエストを実行し、これが成功したことを確認してから、期待したことが起こっていることを確認するためにデータベースに問い合わせました。
リクエスト結果の副作用について確認する前にリクエストのステータスを確認するのは、常に良い考えです。その理由は、Play framework が非同期であるという特性は、テストアクションですらも異なるスレッドで実行されるかもしれないことを意味するからです。リクエストのステータスをチェックすることで、Play がそのリクエストの処理を完了したことを保証したことになります。
プロジェクトをリネームするアクションのテストを書きましょう:
@Test
public void renameProject() {
long id = Project.find.where()
.eq("members.email", "[email protected]")
.eq("name", "Private").findUnique().id;
Result result = callAction(
controllers.routes.ref.Projects.rename(id),
fakeRequest().withSession("email", "[email protected]")
.withFormUrlEncodedBody(ImmutableMap.of("name", "New name"))
);
assertEquals(200, status(result));
assertEquals("New name", Project.find.byId(id).name);
}
これも重要なことなので、認可機能が動作していることを確認して、プロジェクトのメンバーでない誰かがプロジェクトの名前を変更できないことを確認しましょう:
@Test
public void renameProjectForbidden() {
long id = Project.find.where()
.eq("members.email", "[email protected]")
.eq("name", "Private").findUnique().id;
Result result = callAction(
controllers.routes.ref.Projects.rename(id),
fakeRequest().withSession("email", "[email protected]")
.withFormUrlEncodedBody(ImmutableMap.of("name", "New name"))
);
assertEquals(403, status(result));
assertEquals("Private", Project.find.byId(id).name);
}
これらのテストが動作することを確認するために、テストを実行してください。これまでで、アクションをどのように実装するか、そしてそれらをどのようにテストするのかについて、少しだけ見てきました。ここで更にテストを書くこともできますが、このチュートリアルの目的のために、それらはここに置いていきましょう。練習するために、プロジェクトを削除するメソッド、そして新しいプロエジェクトを作成するメソッドを確認する、さらにいくつかのテストを書くことができます。
作業内容を git にコミットしてください。
次章 に進みましょう