Google App Engine でRSSリーダーを作った

昔のホームページには大抵リンク集があったが、
今はあまり見なくなってしまった。
無断リンク禁止とか、相互リンクお断りとか面白かったのに。



まぁ、やろうとすれば
2chニュー速クオリティ http://news4vip.livedoor.biz/
とか簡単にできるけど、
みんなで追加できるリンク集のようなものがあればいいのにと思い、
Google App Engineチュートリアルビデオ
を少しいじれば作れそうだったので、作ってみることにした。



・・・・最終的にはリンク集じゃなくて、RSSリーダーができた。
feedparser.py というモジュールに感動したからだと思う。



feedparser.py
ダウンロード
http://code.google.com/p/feedparser/downloads/list
参考サイト
http://d.hatena.ne.jp/souta-bot/20090204/1233764053
http://itpro.nikkeibp.co.jp/search/index.html?q=feedparser.py(1週間前にあったリンクが切れた・・)



目標管理的にどうかと思うし、
できのいいモジュールを使っているとバカになってしまいそうな気がするが・・
これも Joel Spolsky がいうところの Fire and Motion の一種なのかもしれない。
Fire and Motion
http://www.joelonsoftware.com/articles/fog0000000339.html



できたRSSリーダー
PublicRSS
http://ippaipub.appspot.com/publicrss



以下詳細



開発前に目標とした点
1.2つの画面を作る
2.複数のレコードの1つを指定して消せるようにする


チュートリアルビデオと同じ事をするのはさすがに気が引けたので。



関係ないけど、このビデオはいい。
なんか見ていて楽しくなってくる。
このビデオの人がコンピューターサイエンスの博士号を持っている宇宙飛行士であれ、
そこら辺を歩いていた暇人であれ、とても尊敬できる。


で、目標とした点=苦労した点だった。
でも、やる前からそこでひっかかることは予想できたので問題ではない・・はず。
かかった時間もほぼ予想通りだった。全部で2,30時間ぐらい。
目標を満たすまで10時間ぐらいかかって、あとは何回か作り直した。


1.2つの画面を作る
Web アプリでは画面と画面の間は別の世界で、簡単には値を受け渡せない。そこで
(1)別ページに移動する前に、cookie に値を格納する。
publicrss.py

cookie = 'publicrss_siteurl=%s;' % self.request.get('get_item')
self.response.headers.add_header('Set-Cookie', cookie )

(2)別ページで、cookie から値を取得する。
publicrss.py

feedsiteurl= self.request.cookies.get('publicrss_siteurl', '')

2.複数のレコードの1つを指定して消せるようにする
レコードを識別する必要がある。そこで、
(1)checkbox にレコードID をセットする(name=レコードID)
publicrss.html

<input type="checkbox" name={{feed.siteurl}}>

(2)チェックしたレコードID のレコードを削除する。
publicrss.py

if self.request.get(str(feedsite.siteurl))  !='':
    feedsite.delete( )

作っている間は、熱狂、狂乱的な何かがあった。
ゲームにはまっているときと同じで、早く家に帰ってすぐいじりたい感じ。
・・ほんとは簿記2級の勉強をしなくてはいけないのだけど・・
そういう時ほど集中できる。
人間は適当なダミー目標を持っているといいのかもしれない。
MBD (Management By Dummy objectives)
ただ、波に乗っているかと聞かれれば、波に巻かれている気がする。


上のJoel の記事から。
It doesn't matter if your code is lame and buggy and nobody wants it.
If you are moving forward, writing code and fixing bugs constantly, time is on your side.
・・だってさ。




ソースコード全部は以下の通り


app.yaml

application: ippaipub
version: 1
runtime: python
api_version: 1

handlers:
- url: /favicon\.ico
  static_files: favicon.ico
  upload: favicon\.ico

- url: /css
  static_dir: css

- url: /publicrss
  script: publicrss.py
- url: /publicrss/item
  script: publicrss.py

- url: .*
  script: main.py

publicrss.html

<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../css/publicrss.css" type="text/css">
<title>PublicRSS</title>
</head>


<body>
<p>
PublicRSS
</p>
<form action="#" method="post">
<div><input type="text" name="t_sitename" placeholder="Slashdot" required>
<input type="text" name="t_siteurl" size="50" placeholder="http://slashdot.jp/slashdotjp.rss" required></div>
<div><input type="submit" name="add_site" value="Add RSS Site" id="addbutton"></div>
</form>
<p>
</p>
<form action="#" method="post">
<input type="submit" name="del_site" value="Delete checked RSS site" id="delbutton">
{% for feed in tp_feed %}
<div>
<input type="checkbox" name={{feed.siteurl}}>
<a href="{{feed.siteurl}}" target="_blank">{{feed.sitename}}</a>
<input type="submit" name="get_item" value={{feed.siteurl}}  class="urlbutton">
</div>
{% endfor %}
</form>
</body>
</html>

publicrssitem.html

<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PublicRSS Item</title>
</head>


<body>
<p>
<A href="../publicrss">PublicRSS</a>
</p>

<p>
{{tp_feed.publisher}}
<br>
{{tp_feed.title}}
</p>

{% for feeditem in tp_feeditem %}
<div>
<a href="{{feeditem.link}}" target="_blank">{{feeditem.title}}</a>
</div>
{% endfor %}
</body>
</html>

publicrss.css

#addbutton	{ -webkit-border-radius: 3px;
                      -moz-border-radius: 3px;
                      background: #ccccff;
                      color: #000000; }

#delbutton	{ -webkit-border-radius: 3px;
                      -moz-border-radius: 3px;
                      background: #ffcccc;
                      color: #000000; }

.urlbutton	{ width: 100px; 
                        color: #666666; }

publicrss.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import datetime
import logging
from google.appengine.dist import use_library
use_library('django', '1.2')
from google.appengine.ext.webapp import template
from google.appengine.ext import db

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util


class Feedsite(db.Model):
    sitename = db.StringProperty(required=True)
    siteurl     = db.StringProperty(required=True)
    sitetime  = db.DateTimeProperty(auto_now_add=True)


def parse_feed(feed_url):
    import feedparser
    from google.appengine.api import urlfetch
    """
    call feedpaser module
    """
    result = urlfetch.fetch(feed_url)
    if result.status_code == 200:
        d = feedparser.parse(result.content)
    else:
        raise Exception('Can not retrieve giben URL.')
    if d.bozo == 1:
        raise Exception('Can not parse giben URL.')
    return d


class HeadHandler(webapp.RequestHandler):
    def get(self):
# display feedsite information
        feedsite  = db.GqlQuery(
            'SELECT * FROM Feedsite '
            'ORDER BY sitetime')
        template_values = {
            'tp_feed': feedsite
            }
        path = os.path.join(os.path.dirname(__file__), 
                                       'template/publicrss.html')
        self.response.out.write(template.render(path, template_values))

    def post(self):
# add feedsite information
        if self.request.get('add_site') !='':
            if (self.request.get('t_sitename') !='' and 
                self.request.get('t_siteurl') !='' ):
                feedsite = Feedsite(
                    sitename = self.request.get('t_sitename'),
                    siteurl = self.request.get('t_siteurl'))
                feedsite.put( )
            self.redirect('/publicrss')
# delete feedsite information
        if self.request.get('del_site') !='':
            for feedsite in Feedsite.all( ):
                if self.request.get(str(feedsite.siteurl))  !='':
                    feedsite.delete( )
            self.redirect('/publicrss')
# set feedsite URL to Cookie
        if self.request.get('get_item') !='':
            cookie = 'publicrss_siteurl=%s;' % self.request.get('get_item')
            self.response.headers.add_header('Set-Cookie', cookie )
            self.redirect('/publicrss/item')


class ItemHandler(webapp.RequestHandler):
    def get(self):
# get feedsite URL from Cookie
        feedsiteurl= self.request.cookies.get('publicrss_siteurl', '')
# get feed
        try:
            d = parse_feed(feedsiteurl)
        except Exception, e:
            logging.info(Exception)
            return self.redirect('/publicrss?error=Invalid%20URL')
# display feed
        template_values = {
            'tp_feed':d.feed ,
            'tp_feeditem':d.entries
            }
        path = os.path.join(os.path.dirname(__file__), 
                                       'template/publicrssitem.html')
        self.response.out.write(template.render(path, template_values))


def main():
    application = webapp.WSGIApplication(
                                         [('/publicrss', HeadHandler),
                                          ('/publicrss/item', ItemHandler)],
                                         debug=True)
    util.run_wsgi_app(application)

if __name__ == '__main__':
    main()