Documentation

You are viewing the documentation for the 2.1.5 release in the 2.1.x series of releases. The latest stable release series is 2.4.x.

§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.javaProjectsTest から始めましょう:

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 にコミットしてください。

次章 に進みましょう