GWTで共通処理を使いまわす方法

表題の件について結構悩んだので、自分の中のまとめの意味を込めて記録を残しておきます。

まず、ひとつのモジュールを単純に別のページで使いまわすには、そのモジュールがコンパイルされたjsファイルをscriptタグで取り込むだけで大丈夫です。たとえば、ログインユーザの名前を画面のヘッダに表示する処理を実装したモジュールがcom.example.common.LoginStatusだったとすると、別ページでもログインユーザの名前をヘッダに出すには、以下のようにscriptタグを書けばいい。もちろん、src属性の指定は、場所に合わせるか絶対指定で。

<script type="text/javascript" language="javascript" src="../com.example.common.LoginStatus/com.example.common.LoginStatus.nocache.js"></script>

ただ、これだと、サーバとのやり取り部分だけを別モジュールで使うといったことができません。そこで、共有するサーバサイド処理とサーバ間通信用DTOオブジェクトを、ひとつの共通モジュールにまとめておきます。

たとえば、ログインユーザ情報を格納するUserDtoと、ログインユーザを取得するサーバサイドのメソッドを定義したCommonServiceインタフェースがあるとすると、以下のようなフォルダ構造にしておきます。

src/com/example/common/
 ./
    Common.gwt.xml
 ./client/
    CommonService.java
    CommonServiceAsync.java
    UserDto.java
 ./server/
    CommonServiceImpl.java

そして、この共通処理を使うモジュールは、Xxx.gwt.xml内にてcom.example.common.Commonをinheritsに指定すれば、コンパイルが通るようになります。このとき、共通モジュールCommonに定義されているEntryPointも一緒に動いてしまうので、この共通モジュールのEntryPointは中身のない処理にしておいたほうがいいでしょう。

<inherits name="com.example.common.Common" />

しかし、実際にこの方法で共通処理を利用したページを作ると、サーバサイドにアクセスしようとしたところでエラーが出ます。どうも、サーバサイドへの問い合わせが相対パス指定になってしまうからのようです。ちょっといい方法が思いつかなかったので、以下のように指定することで問題を回避しました。

まず、CommonService.javaの@RemoteServiceRelativePathアノテーションでひとつ上の相対パスを指定します。

/**
 * The client side stub for the RPC service.
 */
@RemoteServiceRelativePath("../common")
public interface CommonService extends RemoteService {

次に、web.xmlの記述を変更し、/commonでこのサーバサイド処理を受け取るようにします。

  <servlet>
    <servlet-name>commonServlet</servlet-name>
    <servlet-class>com.example.common.server.CommonServiceImpl</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>commonServlet</servlet-name>
    <url-pattern>/common</url-pattern>
  </servlet-mapping>

こうすることで、どのモジュールからでも、共有モジュールで定義/実装したサーバサイド処理を利用できるようになりました。

Google App Engine for Javaにも手を出してみる

気がつけば、もう4ヶ月も放置してしまっていた。仕事上でいろいろ忙しかったりすると、こういうサンデープログラマ的活動は止まってしまうという。

で、ちょっと気分を変えて、Google App Engine for Javaにも手を出してみることにしました。ただ、それだけだと、仕事でよく使うサーブレットに毛の生えたもの(まぁ、StrutsとかSpringとかいったフレームワークなしというのは珍しいものの)という感じで個人的に全く面白みがないので、Google Web Toolkitも合わせて挑戦してみることにします。

というわけで、EclipseGoogle App Engine for Java用のPluginを入れて、Google Web ToolkitのTutrialのGetting Startedをそのままやってみる。
うん、何となく流れがわかった気がする。

  1. モジュールを作る
  2. HTMLを作り、JavaScriptで動的に変更したい部分にid属性を付ける。
  3. RemoteServiceを継承したインターフェースをモジュールのclientフォルダ内に作る。
  4. RemoteServiceServletを継承し、さっき作ったインターフェースを実装したクラスを作る。
    • サーバとクライアントとのデータのやり取りには、シリアライズ可能なDTOクラスをclientフォルダ内に用意してそれを使う。
  5. clientフォルダ内に、さっきつくったインターフェースの名前の後ろにAsyncという文字がついたインターフェースを作る。
    • クライアント用インターフェース
    • これは、返り値がvoidで最後の引数がCallbackになったもの
  6. EntryPointを継承したクラスを作る。
  7. web.xmlに記述を追加。


Google Web Toolkitは使ったことがなかったが、割と面白そうだ。

で、動的に変わるひとつのページを作るのはいいとして、複数のページに共通するような処理、たとえば、ログインユーザの名前を画面のヘッダ部分に常に出すとかいったことは、どうやって実現すればいいのかというところで詰まりました。

たぶん、モジュールとか、その呼びだし方(というよりどういう機序で動いているのか)がわかってないっぽい。

というわけで、そこら辺をちゃんと把握して、次回まとめようと思います。
今日はここまで。

TODOタスクの状態表示と状態変更機能

前回で、TODOタスクに状態を持たせることができました。今度は、その状態の表示と、状態変化のロジックを実装することにします。

まず、TODOタスクの状態変化処理用のメソッドを用意します。

/todo/views.py

def update_task_status(request):
    if not request.user.is_authenticated():
        return render_to_response(request, "login.html")
    
    #ログインユーザのタスクかどうかチェック
    task = Task.get(request.POST["key"])
    if (task.user != request.user):
        return HttpResponseRedirect(reverse('todo.views.list_tasks'))
    
    task.status = int(request.POST["status"])
    task.put()
    
    return HttpResponseRedirect(reverse('todo.views.list_tasks'))

次に、/todo/update_statusにアクセスされたらこのメソッドが呼ばれるようにします。

/todo/urls.py

urlpatterns = patterns('todo.views',
    ...
    (r'^update_status$', 'update_task_status'),
    ...
)

最後に、TODOタスク一覧画面を変更し、個々のTODOタスクの状態と、状態を変更するボタンを追加します。

/todo/templates/task_list.html

<table class="list">
  <tr>
    <th width="400">タイトル</th>
    <th width="80">状況</th>
    <th width="250">締切</th>
    <th width="100">締切お知らせメール通知</th>
    <th width="200">変更</th>
    <th width="80">削除</th></tr>
  {% for task in object_list %}
  <tr><td><a href="update?key={{ task.key }}">{{ task.title }}</a></td>
  <td>{{ task.status_name }}</td>
  <td>{{ task.limit_time|date:"Y-m-d H:i" }}</td>
  <td>{% if task.is_notified %}する{% else %}しない{% endif %}</td>
  <form action="update_status" method="post">
  <input type="hidden" name="key" value="{{ task.key }}">
  <td>
    <select name="status">
      {% ifnotequal task.status 1 %}<option value="1">未着手</option>{% endifnotequal %}
      {% ifnotequal task.status 2 %}<option value="2">開始</option>{% endifnotequal %}
      {% ifnotequal task.status 3 %}<option value="3">完了</option>{% endifnotequal %}
      {% ifnotequal task.status 4 %}<option value="4">取消</option>{% endifnotequal %}
    </select>
    <input type="submit" value="変更">
  </td>
  </form>
  <form action="delete" method="post"><td><input type="hidden" name="key" value="{{ task.key }}"><input type="submit" value="削除"></td></form>
  {% endfor %}
</table>

これで状態を変更できるようになったので、いつものようにmanage.py updateコマンドを実行して金星翻車魚にアップロードしました。

既存データの一括更新

これまでTODOタスクは追加と削除だけができるものでしたが、実際にTODO管理をしようとすると、それがどういう状況なのかを記録したくなると思います。といっても、あまり複雑にするとややこしくなるので、以下の4つの状態をとれるということにします。

  • 未着手: 登録しただけ
  • 開始: 着手したもの
  • 完了: 完了したもの
  • 取消: 実行を取りやめたもの

これに合わせて、モデルを変更します。また、ステータス変更時の時刻を記録するプロパティも合わせて用意します。

/todo/models.py

class Task(db.Model):
    STATUS_NEW = 1
    STATUS_START = 2
    STATUS_FINISH = 3
    STATUS_CANCEL = 4

    user = db.ReferenceProperty(User)
    title = db.StringProperty()
    create_time = db.DateTimeProperty(auto_now_add=True)
    limit_time = db.DateTimeProperty()
    is_notified = db.BooleanProperty(default=False, required=False)
    mail_send_time = db.DateTimeProperty()
    update_time = db.DateTimeProperty(auto_now=True)
    status = db.IntegerProperty(default=STATUS_NEW)

次に、既存のTODOタスクについて、statusを全て未着手(STATUS_NEW)に更新することにします。まず、更新用のメソッドを用意します。

/todo/views.py

def batchjob(request):
    for task in Task.all():
        task.status = Task.STATUS_NEW
        task.put()

次に、/todo/batchというURLでアクセスされたらこのメソッドが呼ばれるようにします。

/todo/urls.py

urlpatterns = patterns('todo.views',
    ...
    (r'^batch$', 'batchjob'),
)

最後に、管理者ユーザだけがこのURLにアクセスできるようにします。

/app.yaml

- url: /todo/batch
  script: common/appenginepatch/main.py
  login: admin

ここまでやった上で金星翻車魚のサイトにアップし、/todo/batchにアクセスします。画面表示をしていないのでちょっとエラーになりますが、処理はこれで終わります。これで、用がなくなったので、念の為、上記のurls.pyとapp.yamlを元に戻しておきます。

というわけで、TODOタスクに状態を持たせることができたので、次から、これをクリックひとつで変更していけるようにすることを考えます。

定期的に処理を走らせる

それでは、メールによる締切通知部分を作成しようと思います。

まず、締切時にメールを送信する設定になっていて、かつ、締切を過ぎているのにまだメールを送信していないTODOタスクを抜き出します。

    now = datetime.datetime.today()
    now += datetime.timedelta(hours=9)
    queryset = Task.gql(
        "WHERE limit_time != :none "
        + "AND limit_time <= :now "
        + "AND is_notified = True "
        + "AND mail_send_time = :none "
        + "LIMIT 10 ",
        now=now, none=None)

ここで、Nullの判定をするのにわざわざ :none とバインド変数を定義して none=None とNoneバインドしているのは、GQLの仕様でIS NULLといった構文が用意されていないからです。また、LIMIT 10と限定しているのは、大量のTODOが引っかかって処理がタイムアウトすることを防ぐためです。ちなみに、現在日時に9時間を足しているのは、日本時間に合わせるためです。もちろん、ちゃんと作るにはタイムゾーンを指定するべきですが、今回は日本専用で自分用という位置づけなので目をつぶります。

次に、メール送信部分のメソッドを用意します。

def send_notifier_mail(task, now):
    message = mail.EmailMessage(sender="...")  # 管理者のメールアドレス
    message.to = task.user.email
    message.subject = u"金星翻車魚:締切のお知らせ"
    message.body = u'''
こんにちは、%sさん

ご登録いただいた以下のTODOが
指定された締切日時に到達したことをお知らせします。

タイトル: %s
締切 : %s
''' % (task.user.username, task.title, task.limit_time.strftime("%Y-%m-%d %H:%M"),)
    message.send()

    logging.info(message.body)
    task.mail_send_time = now
    task.save()
    return

これら二つの処理を組み合わせたメソッドをcronjobという名前でviews.pyに定義し、それを、/todo/cronというURLに紐づけることにします。

/todo/urls.py

urlpatterns = patterns('todo.views',
    (r'^$', 'list_tasks'),
    (r'^create$', 'add_task'),
    (r'^delete$', 'delete_task'),
    (r'^update$', 'update_task'),
    (r'^cron$', 'cronjob'),
)

これで/todo/cronというURLにアクセスすると締切お知らせメールが送信されるようになりました。このURLはGoogle App Engineのcron処理で利用し、一般のアクセスは受け取らないつもりですので、app.yamlに以下の行を付け足します。

/app.yaml

- url: /todo/cron
  script: common/appenginepatch/main.py
  login: admin

login: adminと指定することで、一般のアクセスを受け取らないようにしています。なお、この設定は、以下の一般アクセス用の設定の前に入れておきます。

/app.yaml

- url: /.*
  script: common/appenginepatch/main.py

最後に、Google App Engineのcron(定期実行処理)の設定を書けば完了です。app.yamlと同じトップフォルダにcron.yamlというファイルを作成し、以下の設定を書き入れます。

/cron.yaml

cron:
- description: todo cron job
  url: /todo/cron
  schedule: every 15 minutes

これで、15分おきに定期実行されるようになりました。いつものようにmanage.py updateコマンドを発行し、金星翻車魚のサイトを更新しました。さて、次から何をしようかな。

既存のモデルを拡張する

締め切りがきたらメールを送る機能を実装するにあたり、これまでのTaskモデルクラスだと情報が足りないので、締め切りをメールで通知するか/いつメールを送ったかを格納するプロパティを加えることにします。

/todo/models.py

class Task(db.Model):
    user = db.ReferenceProperty(User)
    title = db.StringProperty()
    create_time = db.DateTimeProperty(auto_now_add=True)
    limit_time = db.DateTimeProperty()
    is_notified = db.BooleanProperty(default=False)  # 締切をメールで知らせるか
    mail_send_time = db.DateTimeProperty()           # 締切通知メールの送信日時

また、これに合わせて、TODO情報の入力フォームも変更します。
/todo/forms.py

class TaskForm(forms.ModelForm):
    user = forms.CharField(widget=forms.HiddenInput, required=False)
    title = forms.CharField(max_length=100, required=True, label=u"タイトル", help_text=u"(必須)")
    limit_time = forms.DateTimeField(required=False, label=u"締切", help_text=u"(任意)yyyy-mm-dd HH:MMの形式で入力してください")
    key = forms.CharField(widget=forms.HiddenInput, required=False)
    is_notified = forms.BooleanField(label=u"締め切り時のメール通知", help_text=u"ここにチェックを入れると、締め切りがきたときにメールでお知らせします")
    class Meta:
        model = Task
        exclude = ('create_time', 'mail_send_time')

最後に、TODO情報一覧のテンプレートで、締切通知メールの送信日時を表示するようにします。

/todo/templates/task_list.html

<table class="contents">
  <tr><th width="320">タイトル</th><th width="200">締切</th><th width="200">締切お知らせメール送信日時</th><th width="80">削除</th></tr>
  {% for task in object_list %}
  <tr><td><a href="update?key={{ task.key }}">{{ task.title }}</a></td>
  <td>{{ task.limit_time|date:"Y-m-d H:i" }}</td>
  <td>{{ task.mail_send_time|date:"Y-m-d H:i" }}</td>
  <form action="delete" method="post"><td><input type="hidden" name="key" value="{{ task.key }}"><input type="submit" value="削除"></td></form>
  {% endfor %}
</table>

いったんここまでで、manage.py update コマンドを発行して金星翻車魚のサイトを更新しました。

ところで、このようにデータモデルを変更した場合、データストアに登録されている既存のデータオブジェクトはどうなるかというと、どうもなりません。古いデータモデルから作成されたオブジェクトが登録されたままになっています。したがって、データストアに登録されている同じ種類のオブジェクトであっても、保持しているプロパティの種類が異なるということが起こりえます。

今回は、既存のデータオブジェクトに対し、追加したプロパティに何らかの値を投入する必要はありませんが、もし、データ移行のためにそのような作業が必要な場合は、既存のデータオブジェクトに対して新しく追加したプロパティの値をセットしていくようなメソッドを用意し、それを呼び出す必要があります。詳細については、http://code.google.com/intl/ja/appengine/articles/update_schema.htmlの記事を参考にしてください。

というわけで、もろもろの準備が整いましたので、次から、メールによる締切通知部分を作っていこうと思います。

アプリケーション専用のメールアドレスからメールを送信する

あっという間に3週間ほど放置状態となってしまいました。プライベートで何か忙しくなると、とたんに滞ってしまったりするのが趣味プログラミングの辛いところだったりします。まぁ、あせらず気長に続けていこうと思います。

ちょっと間があきましたが、前回まででTODO情報の登録/編集/削除/閲覧といったところまではできるようになりました。これからどうしていくかですが、せっかく締切という情報を登録しているので、締め切りを過ぎたらメールで知らせるという機能を作っていきたいと思います。

Google App Engineには、一定の間隔で定期的に処理を行うというCron機能が用意されていますので、これを使うことにします。処理の流れとしては、存在するTODO情報のうち締め切りを過ぎていてかつまだ締切メールが送られていないTODO情報を取り出し、そのTODO情報を登録したユーザあてにメールを送るというものです。と、ここまで考えてふと思ったのですが、Google App Engineからメールを送る場合、その送信元のメールアドレスは以下のどちらかのメールアドレスでなければなりません。

  • アプリケーションの管理者のメールアドレス
  • ログインユーザのメールアドレス

今回は定期起動の処理中なので管理者のメールアドレスを使うことになりますが、管理者のメールアドレスというのは私の個人利用のメールアドレスなので、個人で使っているメールアドレスでシステムからのメールを送るのはちょっと恥ずかしい気がします。ここはやはり、金星翻車魚(キンボシマンボウ)専用のメールアドレスを用意して、それを送信元アドレスとしてメールを送りたいところです。

というわけで、アプリケーション専用のメールアドレスからメールを送りたいと思ったらどうしたらいいんだろうかと調べてみると、ドキュメントにちゃんと書いてありました。

If you want to send email on behalf of the application but do not want to use a single administrator's personal Google Account as the sender, you can create a new Google Account for the application using any valid email address, then add the new account as an administrator for the application. To add an account as an administrator, see the "Developers" section of the Admin Console.

http://code.google.com/intl/en/appengine/docs/python/mail/overview.html

言われてみると当たり前の話で、使いたい専用のメールアドレスを用意し、そのメールアドレスでGoogleアカウントを取得して、その新しく作ったGoogleアカウントを該当するアプリケーションの管理者に追加すればいいということです。というわけで、さっそく専用メールアドレスのGoogleアカウントを作ってアプリケーションの管理者に追加しました。なお、アプリケーションの管理者に追加するには、Google App Engineの管理コンソールから、該当のアプリケーションの設定を開いてDevelopersを選択し、追加したい管理者のメールアドレスを入力してInviteボタンを押せばいいです。

これで、専用メールアドレスからメールが送れるようになりました。メールを送信するコードは、以下のようになります。

from google.appengine.api import mail
def cronjob(request):
    message = mail.EmailMessage(sender="...") // 用意した専用の送信元メールアドレス
    message.to = "..." //送信先メールアドレス
    message.subject = u'テストメール'
    message.body = u'''
こんにちは。

これは、金星翻車魚からのお知らせです。
'''
    message.send()
    ...

準備が整いましたので、次から、締切を過ぎたTODOに関してお知らせメールを送る機能を作っていこうと思います。