公開しているウェブサイトの掲示板のスパムが酷くしばらく書込み禁止にしている。そのため、ソースコードを表示できるシンプルな簡易BBSが必要となったので、Google App Engineで作ってみた。チュートリアルのゲストブックに毛が生えたようなものだが、自分の目的には取り敢えず適っているし、これからいろいろと肉付けしていけばいいかな。
チュートリアルのゲストブックではまず長い文章が入れられないし、名前が強制的にGoogleアカウントになるし、日時の表示がないし、なによりもソースコードやリンクの表示ができない。それに、一つのデータベースで複数の掲示板を使いたいが、このままでは無理。
で、やったこと。
・ソースコードのインデントや空白などを崩さずに表示。
・リンクのサポート。
・StringPropertyからTextPropertyにして長文をサポート。
・Googleアカウントでログインするが名前は自由に変更でき、再ログイン時には前回使った名前を表示。
・匿名での投稿ができる。ついでに、禁止もできる。
・URLにIDをつけて複数の掲示板が使える。
・日時の表示。
などなど。あとは、投稿のプレビュー、削除、検索、それに例外処理などを実装すればよいかな。検索はちょっと面倒そうだけど、ほかは簡単だろう。ところで、日時の表示では、日本標準時にするために webapp.template.create_template_register と webapp.template.register_template_library でフィルターを作ってみたんだけど、何故かうまく行かなかった。何か間違っていたかな?
と云うわけで、ここのブログのサイドメニューにメッセージボードを入れておく。不具合の報告、意見や要望などに、是非ご利用を。
以下、構成とソースコード。
アプリケーション名をmessagesとすると、まず、messagesディレクトリにmessages.py, index.html, app.yamlを置く。messages/static/stylesheets に main.css を置く。あとは、appcfg.py update messages を実行すればよい。また、messages.pyの先頭に記述されている、TITLE, LIMIT, ANONYMOUS変数を変更することで、掲示板の追加、最大表示件数、匿名の書き込みの許可・不許可を設定できる。
app.yaml
application: messages version: 1 runtime: python api_version: 1 handlers: - url: /stylesheets static_dir: static/stylesheets - url: /.* script: messages.py
messages.py
#!/usr/bin/env python # -*- coding: utf-8 -*- """ 簡易メッセージボード(掲示板) version 0.02 α版 Copyright (C) 2008 nox """ # 許可するメッセージボードおよびそのタイトル TITLE = { "test": "テスト用メッセージボード" } # 最大表示件数 LIMIT = 50 # 匿名の書き込みを許可 ANONYMOUS = False import os, cgi, re, urlparse, datetime import wsgiref.handlers from google.appengine.api import urlfetch from google.appengine.api import users from google.appengine.ext import webapp from google.appengine.ext import db from google.appengine.ext.webapp import template class MainPage(webapp.RequestHandler): def get(self): self.response.out.write(u""" <html> <head> <title>メッセージボード一覧</title> </head> <body> <div><a href="message?id=test">テスト用メッセージボード</a></div> </body> </html>""") # メッセージボード class Message(db.Model): id = db.StringProperty(multiline=False) author = db.UserProperty() username = db.StringProperty(multiline=False) content = db.TextProperty() date = db.DateTimeProperty(auto_now_add=True) admin = db.BooleanProperty() num = db.IntegerProperty() class MessageBoard(webapp.RequestHandler): def get(self): global TITLE, LIMIT, ANONYMOUS id = self.request.get("id") if id not in TITLE.keys(): return messages = Message.all().order("-date").filter("id =", id).fetch(LIMIT) user = users.get_current_user() messages_current_user = Message.all().order("-date").filter("id =", id).filter("author =", user).fetch(LIMIT) if user: if messages_current_user and messages_current_user[0].username: current_username = messages_current_user[0].username if re.search("<span style=\"color: blue;\">(.*)</span>", current_username): username = re.sub("<span style=\"color: blue;\">(.*)</span>", "\g<1>", current_username) else: username = current_username else: username = user.nickname().replace("@", ".") url = users.create_logout_url(self.request.uri) url_linktext = "Logout" else: if ANONYMOUS: username = "匿名" else: username = "" url = users.create_login_url(self.request.uri) url_linktext = "Login" template_values = { "id": id, "title": TITLE[id], "username": username, "messages": messages, "url": url, "url_linktext": url_linktext, } path = os.path.join(os.path.dirname(__file__), "index.html") self.response.out.write(template.render(path, template_values)) class PostMessage(webapp.RequestHandler): def insert_whitespace(self, matchobj): return re.sub("(?<!<br) ", " ", matchobj.group(0).replace("\t", " ")) def post(self): message = Message() message.id = self.request.get("id") if not self.request.get("content").strip(): self.redirect("/message?id=%s" % message.id) return if users.get_current_user(): message.author = users.get_current_user() if self.request.get("username").strip(): message.username = self.request.get("username").strip() else: message.username = message.author.nickname().replace("@", ".") message.content = cgi.escape(self.request.get("content").strip()) message.content = re.sub("\r\n|\r|\n", "<br />", message.content) message.content = re.sub("\[link\](.*)\[/link\]", "<a href=\"\g<1>\">\g<1></a>", message.content) message.content = re.sub("\[code\](.*)\[/code\]", self.insert_whitespace, message.content) message.content = re.sub("\[code\](.*)\[/code\]", "<span style=\"font-family:courier new,monospace;font-size:85%;white-space:pre;color:#0000ff;\">\g<1></span>", message.content) message.admin = users.is_current_user_admin() message.date = message.date + datetime.timedelta(hours=9) max_num_mes = Message.all().order("-date").filter("id =", message.id).fetch(1) if max_num_mes: message.num = max_num_mes[0].num + 1 else: message.num = 1 message.put() self.redirect("/message?id=%s" % message.id) def main(): application = webapp.WSGIApplication([("/", MainPage), ("/message", MessageBoard), ("/message/post", PostMessage)], debug=False) wsgiref.handlers.CGIHandler().run(application) if __name__ == "__main__": main()
index.html
<html> <head> <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" /> <title>{{ title }}</title> </head> <body> <div> <div style="font-weight: bold; font-size: large; padding-bottom: 20px;">{{ title }}</div> {% if username %} <b>{{ username }}</b> さん、こんにちは。 {% else %} こんにちは。書き込みする場合は<a href="https://www.google.com/accounts/">Googleアカウント</a>でログインしてください。 {% endif %} [<a href="{{ url }}">{{ url_linktext }}</a>] </div> {% if username %} <form action="/message/post" method="post"> <div><textarea name="content" rows="5" cols="60"></textarea></div> <div><span style="font-size:70%;color:#666666;">リンク: [link]~[/link], ソースコード: [code]~[/code]</span><br />名前: <input type="text" name="username" size="31" maxlength="255" value="{{ username }}"></input> <input type="submit" value="書き込み"><br /><span style="font-size:70%;color:#666666;">名前を省略した場合はGoogleアカウントのニックネームが表示されます。</span> <input type="hidden" name="id" value="{{ id }}"></div> </form> {% else %} <br /> {% endif %} <div> {% for message in messages %} {% if message.author %} {{ message.num }}. <b>{% if message.admin %}<span style="color: blue;">{{ message.username }}</span>{% else %}{{ message.username }}{% endif %}</b> さんは{{ message.date.year }}年{{ message.date.month }}月{{ message.date.day }}日{{ message.date.hour }}時{{ message.date.minute }}分{{ message.date.second }}秒に書きました: {% else %} {{ message.num }}. 匿名さんは{{ message.date.year }}年{{ message.date.month }}月{{ message.date.day }}日{{ message.date.hour }}時{{ message.date.minute }}分{{ message.date.second }}秒に書きました: {% endif %} <blockquote>{{ message.content }}</blockquote> {% endfor %} </div> <!-- ここより下は変更しないでください。不具合やご意見などがありましたら、下記のnoxのリンク先で報告していただけると嬉しいです。 --> <div><img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif" alt="Powered by Google App Engine" /><br /><span style="font-size:70%;color:#666666;">Copyright © 2008 <a href="http://handasse.blogspot.com/">nox</a>. All rights reserved.</span></div> </body> </html>
main.css
body { color: rgb(20%, 20%, 20%); background-color: rgb(95%, 95%, 95%); font-family: Verdana, Helvetica, sans-serif; font-size: small; margin-left: 10px; margin-right: 10px; } div { line-height: 1.4em; }
チュートリアルのゲストブックではまず長い文章が入れられないし、名前が強制的にGoogleアカウントになるし、日時の表示がないし、なによりもソースコードやリンクの表示ができない。それに、一つのデータベースで複数の掲示板を使いたいが、このままでは無理。
で、やったこと。
・ソースコードのインデントや空白などを崩さずに表示。
・リンクのサポート。
・StringPropertyからTextPropertyにして長文をサポート。
・Googleアカウントでログインするが名前は自由に変更でき、再ログイン時には前回使った名前を表示。
・匿名での投稿ができる。ついでに、禁止もできる。
・URLにIDをつけて複数の掲示板が使える。
・日時の表示。
などなど。あとは、投稿のプレビュー、削除、検索、それに例外処理などを実装すればよいかな。検索はちょっと面倒そうだけど、ほかは簡単だろう。ところで、日時の表示では、日本標準時にするために webapp.template.create_template_register と webapp.template.register_template_library でフィルターを作ってみたんだけど、何故かうまく行かなかった。何か間違っていたかな?
と云うわけで、ここのブログのサイドメニューにメッセージボードを入れておく。不具合の報告、意見や要望などに、是非ご利用を。
以下、構成とソースコード。
アプリケーション名をmessagesとすると、まず、messagesディレクトリにmessages.py, index.html, app.yamlを置く。messages/static/stylesheets に main.css を置く。あとは、appcfg.py update messages を実行すればよい。また、messages.pyの先頭に記述されている、TITLE, LIMIT, ANONYMOUS変数を変更することで、掲示板の追加、最大表示件数、匿名の書き込みの許可・不許可を設定できる。
app.yaml
application: messages version: 1 runtime: python api_version: 1 handlers: - url: /stylesheets static_dir: static/stylesheets - url: /.* script: messages.py
messages.py
#!/usr/bin/env python # -*- coding: utf-8 -*- """ 簡易メッセージボード(掲示板) version 0.02 α版 Copyright (C) 2008 nox """ # 許可するメッセージボードおよびそのタイトル TITLE = { "test": "テスト用メッセージボード" } # 最大表示件数 LIMIT = 50 # 匿名の書き込みを許可 ANONYMOUS = False import os, cgi, re, urlparse, datetime import wsgiref.handlers from google.appengine.api import urlfetch from google.appengine.api import users from google.appengine.ext import webapp from google.appengine.ext import db from google.appengine.ext.webapp import template class MainPage(webapp.RequestHandler): def get(self): self.response.out.write(u""" <html> <head> <title>メッセージボード一覧</title> </head> <body> <div><a href="message?id=test">テスト用メッセージボード</a></div> </body> </html>""") # メッセージボード class Message(db.Model): id = db.StringProperty(multiline=False) author = db.UserProperty() username = db.StringProperty(multiline=False) content = db.TextProperty() date = db.DateTimeProperty(auto_now_add=True) admin = db.BooleanProperty() num = db.IntegerProperty() class MessageBoard(webapp.RequestHandler): def get(self): global TITLE, LIMIT, ANONYMOUS id = self.request.get("id") if id not in TITLE.keys(): return messages = Message.all().order("-date").filter("id =", id).fetch(LIMIT) user = users.get_current_user() messages_current_user = Message.all().order("-date").filter("id =", id).filter("author =", user).fetch(LIMIT) if user: if messages_current_user and messages_current_user[0].username: current_username = messages_current_user[0].username if re.search("<span style=\"color: blue;\">(.*)</span>", current_username): username = re.sub("<span style=\"color: blue;\">(.*)</span>", "\g<1>", current_username) else: username = current_username else: username = user.nickname().replace("@", ".") url = users.create_logout_url(self.request.uri) url_linktext = "Logout" else: if ANONYMOUS: username = "匿名" else: username = "" url = users.create_login_url(self.request.uri) url_linktext = "Login" template_values = { "id": id, "title": TITLE[id], "username": username, "messages": messages, "url": url, "url_linktext": url_linktext, } path = os.path.join(os.path.dirname(__file__), "index.html") self.response.out.write(template.render(path, template_values)) class PostMessage(webapp.RequestHandler): def insert_whitespace(self, matchobj): return re.sub("(?<!<br) ", " ", matchobj.group(0).replace("\t", " ")) def post(self): message = Message() message.id = self.request.get("id") if not self.request.get("content").strip(): self.redirect("/message?id=%s" % message.id) return if users.get_current_user(): message.author = users.get_current_user() if self.request.get("username").strip(): message.username = self.request.get("username").strip() else: message.username = message.author.nickname().replace("@", ".") message.content = cgi.escape(self.request.get("content").strip()) message.content = re.sub("\r\n|\r|\n", "<br />", message.content) message.content = re.sub("\[link\](.*)\[/link\]", "<a href=\"\g<1>\">\g<1></a>", message.content) message.content = re.sub("\[code\](.*)\[/code\]", self.insert_whitespace, message.content) message.content = re.sub("\[code\](.*)\[/code\]", "<span style=\"font-family:courier new,monospace;font-size:85%;white-space:pre;color:#0000ff;\">\g<1></span>", message.content) message.admin = users.is_current_user_admin() message.date = message.date + datetime.timedelta(hours=9) max_num_mes = Message.all().order("-date").filter("id =", message.id).fetch(1) if max_num_mes: message.num = max_num_mes[0].num + 1 else: message.num = 1 message.put() self.redirect("/message?id=%s" % message.id) def main(): application = webapp.WSGIApplication([("/", MainPage), ("/message", MessageBoard), ("/message/post", PostMessage)], debug=False) wsgiref.handlers.CGIHandler().run(application) if __name__ == "__main__": main()
index.html
<html> <head> <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" /> <title>{{ title }}</title> </head> <body> <div> <div style="font-weight: bold; font-size: large; padding-bottom: 20px;">{{ title }}</div> {% if username %} <b>{{ username }}</b> さん、こんにちは。 {% else %} こんにちは。書き込みする場合は<a href="https://www.google.com/accounts/">Googleアカウント</a>でログインしてください。 {% endif %} [<a href="{{ url }}">{{ url_linktext }}</a>] </div> {% if username %} <form action="/message/post" method="post"> <div><textarea name="content" rows="5" cols="60"></textarea></div> <div><span style="font-size:70%;color:#666666;">リンク: [link]~[/link], ソースコード: [code]~[/code]</span><br />名前: <input type="text" name="username" size="31" maxlength="255" value="{{ username }}"></input> <input type="submit" value="書き込み"><br /><span style="font-size:70%;color:#666666;">名前を省略した場合はGoogleアカウントのニックネームが表示されます。</span> <input type="hidden" name="id" value="{{ id }}"></div> </form> {% else %} <br /> {% endif %} <div> {% for message in messages %} {% if message.author %} {{ message.num }}. <b>{% if message.admin %}<span style="color: blue;">{{ message.username }}</span>{% else %}{{ message.username }}{% endif %}</b> さんは{{ message.date.year }}年{{ message.date.month }}月{{ message.date.day }}日{{ message.date.hour }}時{{ message.date.minute }}分{{ message.date.second }}秒に書きました: {% else %} {{ message.num }}. 匿名さんは{{ message.date.year }}年{{ message.date.month }}月{{ message.date.day }}日{{ message.date.hour }}時{{ message.date.minute }}分{{ message.date.second }}秒に書きました: {% endif %} <blockquote>{{ message.content }}</blockquote> {% endfor %} </div> <!-- ここより下は変更しないでください。不具合やご意見などがありましたら、下記のnoxのリンク先で報告していただけると嬉しいです。 --> <div><img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif" alt="Powered by Google App Engine" /><br /><span style="font-size:70%;color:#666666;">Copyright © 2008 <a href="http://handasse.blogspot.com/">nox</a>. All rights reserved.</span></div> </body> </html>
main.css
body { color: rgb(20%, 20%, 20%); background-color: rgb(95%, 95%, 95%); font-family: Verdana, Helvetica, sans-serif; font-size: small; margin-left: 10px; margin-right: 10px; } div { line-height: 1.4em; }
コメント