タスク管理アプリを作る その1

長らく Google App Engine 用のネタに困っていたが、タスク管理用のアプリを作る事にした。Gmailのタスク管理と連携できればと思ってちょっと調べたが、どうやらまだAPIが無さそうだ。もっとしっかり探せば見つかるかもしれないが、とりあえず先に進む事にした。

http://code.google.com/intl/ja/appengine/docs/python/gettingstarted/http://code.google.com/intl/ja/appengine/articles/djangoforms.htmlに掲載されているコードをベースにいろいろ改造。改良する点は多々あるがとりあえず動く物が出来上がった。

現在で改良が必要なのは

  • エラー処理
  • 利用をGoogleユーザーのみに制限
  • カレンダーを利用した日付入力
  • データエクスポート機能
  • Memcache サービスの使用
  • メッセージの国際化

それにしてもGoogle App Engineに公開したアプリを今のところ削除できないみたいだが、ちょっと困ってしまうなぁ。10個も作る予定は無いが、公開してしまうとアプリケーションIDやアプリケーション名を変更できないのか?

今回初めていろいろ Python を使って書いてみたが、最初は気持ち悪かったコードも慣れてくると見やすく感じてきた。しかしコードのコピペやエディタのインデント調整機能を使ってインデントが狂うと意図しない動作をするからちょっと怖いな。

app.yml

application: anonymouse
version: 1
runtime: python
api_version: 1

handlers:
- url: /stylesheets
  static_dir: stylesheets

- url: .*
  script: main.py

main.py

#!/usr/bin/env python

import cgi
import os
import wsgiref.handlers

from google.appengine.api import users
from google.appengine.ext import db
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from google.appengine.ext.db import djangoforms


class Item(db.Model):
  title      = db.StringProperty(required = True)
  note       = db.StringProperty(multiline = True)
  due_date   = db.DateProperty()
  priority   = db.IntegerProperty(default = 2,
				  choices = [1, 2, 3])
  done       = db.BooleanProperty(default = False)
  entry_time = db.DateTimeProperty(auto_now_add = True)
  author   = db.UserProperty()


class ItemForm(djangoforms.ModelForm):
  class Meta:
    model = Item
    exclude = ['author']


class CreatePage(webapp.RequestHandler):
  def get(self):
    template_values = {
	'destination': '/new',
	'itemform': ItemForm(),
	'id': None
      }
    path = os.path.join(os.path.dirname(__file__), 'views', 'index.html')
    self.response.out.write(template.render(path, template_values))

  def post(self):
    data = ItemForm(data=self.request.POST)
    if data.is_valid():
      entity = data.save(commit=False)
      entity.author = users.get_current_user()
      entity.put()
      self.redirect('/')
    else:
      template_values = {
        'destination': '/new',
        'itemform': data,
        'id': None
        }
      path = os.path.join(os.path.dirname(__file__), 'views', 'index.html')
      self.response.out.write(template.render(path, template_values))


class ItemPage(webapp.RequestHandler):
  def get(self):
    query = db.GqlQuery("SELECT * FROM Item ORDER BY priority ASC LIMIT 10")
    template_values = {
	'items': query,
	}
    path = os.path.join(os.path.dirname(__file__), 'views', 'list.html')
    self.response.out.write(template.render(path, template_values))


class EditPage(webapp.RequestHandler):
  def get(self):
    id = int(self.request.get('id'))
    item = Item.get(db.Key.from_path('Item', id))
    template_values = {
	'destination': '/edit',
	'itemform': ItemForm(instance=item),
	'id': id
      }
    path = os.path.join(os.path.dirname(__file__), 'views', 'index.html')
    self.response.out.write(template.render(path, template_values))

  def post(self):
    id = int(self.request.get('_id'))
    item = Item.get(db.Key.from_path('Item', id))
    data = ItemForm(data=self.request.POST, instance=item)
    if data.is_valid():
      entity = data.save(commit=False)
      entity.put()
      self.redirect('/')
    else:
      template_values = {
        'destination': '/edit',
        'itemform': data,
        'id': id
        }
      path = os.path.join(os.path.dirname(__file__), 'views', 'index.html')
      self.response.out.write(template.render(path, template_values))


class DeletePage(webapp.RequestHandler):
  def get(self):
    id = int(self.request.get('id'))
    item = Item.get_by_id(id)
    item.delete()
    self.redirect('/')


def main():
  application = webapp.WSGIApplication(
                                       [('/', ItemPage),
                                        ('/new', CreatePage),
                                        ('/edit', EditPage),
                                        ('/delete', DeletePage),
                                        ],
                                       debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__=="__main__":
  main()

views/index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <title>My Top 10 Tasks</title>
    <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" rel="stylesheet" />
  </head>
  <body>
    <form method="post" action="{{ destination }}">
      <table>
	{{ itemform }}
      </table>
      {% if id %}
        <input type="hidden" name="_id" value="{{ id }}">
      {% endif %}
      <input type="submit" />
    </form>
  </body>
</html>


views/list.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <title>My Top 10 Tasks</title>
    <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" rel="stylesheet" />
  </head>
  <body>
    <table>
      <thead>
	<tr>
	  <th>Title</th>
	  <th>Note</th>
	  <th>Priority</th>
	  <th>Due Date</th>
	  <th>Done</th>
	  <th></th>
	  <th></th>
	</tr>
      </thead>
      <tbody>
	{% for item in items %}
	<tr>
	  <td>{{ item.title|escape }}</td>
	  <td>{{ item.note|escape }}</td>
	  <td>{{ item.priority|escape }}</td>
	  <td>{{ item.due_date|escape }}</td>
	  <td>{{ item.done|escape }}</td>
	  <td><a href="/edit?id={{ item.key.id }}">Edit</a></td>
	  <td><a href="/delete?id={{ item.key.id }}">Delete</a></td>
	</tr>
	{% endfor %}
      </tbody>
    </table>
    <a href="/new">New Task</a>
  </body>
</html>