FlaskでmongoDBを使うチュートリアルをやってみた & チュートリアル和訳 (かなり雑ですが)
mongoDBを使ってWEBアプリケーション作ってみたくて、
でもDjango1.4はまだmongoDBに対応してないということで、
別のフレームワーク、flaskを使ってやってみることに。
いい資料無いかなーと探してたらチュートリアルがあったので、
これをやってみることに。
Write a Tumblelog Application with Flask and MongoEngine
http://docs.mongodb.org/manual/tutorial/write-a-tumblelog-application-with-flask-mongoengine/
ちなみにtumblelogとういのは
リンクや文章の引用、写真や動画などを投稿するだけのシンプルなブログ
tumblelogとは
だそうです。
要はTumblrですね。
機能としては2つ。
- みんながポストを見れてコメントも残せる
- An admin site that lets you add and change posts. 管理者がポストを追加したり編集したりできる
では早速やっていきます。
パッケージのインストール
pip install flask # 開発サーバを簡単に使えるパッケージ pip install flask-script # フォームのハンドリングを簡単にするためのパッケージ pip install WTForms pip install mongoengine # MongoEngine, Flask, WTFormsを統合するためのパッケージ pip install flask_mongoengine
ブログの作成
まず骨格になるアプリケーションを作っていきます。
- tumblelogというディレクトリを作る
- その中にinit.py というフィアルを作成し以下を入力
from flask import Flask app = Flask(__name__) if __name__ == '__main__': app.run()
manage.pyファイル作成
Flask-scriptsで開発サーバ立てる用のファイルです。
# Set the path import os, sys sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from flask.ext.script import Manager, Server from tumblelog import app manager = Manager(app) # Turn on debugger by default and reloader manager.add_command("runserver", Server( use_debugger = True, use_reloader = True, host = '0.0.0.0') ) if __name__ == "__main__": manager.run()
python manage.py runserver
ってやればサーバ走りますので、http://0.0.0.0:5000/ にアクセス。
hello world!って表示されます。
MongoDBとFlaskの設定
init.pyに追加します。
from flask import Flask from flask.ext.mongoengine import MongoEngine app = Flask(__name__) app.config["MONGODB_SETTINGS"] = {'DB': "my_tumble_log"} app.config["SECRET_KEY"] = "KeepThisS3cr3t" db = MongoEngine(app) if __name__ == '__main__': app.run()
設定については、こちらも参照のこと。 MongoEngine Settings
DB構造の定義
tumblelogのポストとコメントを定義しましょう。
ポストはコメント群を持つことができます。
models.pyファイルを作ってその中に以下を記述します。
import datetime from flask import url_for from tumblelog import db class Post(db.Document): created_at = db.DateTimeField(default=datetime.datetime.now, required=True) title = db.StringField(max_length=255, required=True) slug = db.StringField(max_length=255, required=True) body = db.StringField(required=True) comments = db.ListField(db.EmbeddedDocumentField('Comment')) def get_absolute_url(self): return url_for('post', kwargs={"slug": self.slug}) def __unicode__(self): return self.title meta = { 'allow_inheritance': True, 'indexes': ['-created_at', 'slug'], 'ordering': ['-created_at'] } class Comment(db.EmbeddedDocument): created_at = db.DateTimeField(default=datetime.datetime.now, required=True) body = db.StringField(verbose_name="Comment", required=True) author = db.StringField(verbose_name="Name", max_length=255, required=True)
MongoEngineのシンタックスはDjangoにクリソツですね。
シェルを使ってデータを追加する
シェル立ちあげます。
python manage.py shell
ポストのデータを追加しましょう。
>>> from tumblelog.models import * >>> post = Post( ... title="Hello World!", ... slug="hello-world", ... body="Welcome to my new shiny Tumble log powered by MongoDB, MongoEngine, and Flask" ... ) >>> post.save()
コメントのデータも追加しましょう。
>>> post.comments [] >>> comment = Comment( ... author="Joe Bloggs", ... body="Great post! I'm looking forward to reading your blog!" ... ) >>> post.comments.append(comment) >>> post.save()
ポストとコメントが追加されているか調べましょう。
>>> post = Post.objects.get()
>>> post
<Post: Hello World!>
>>> post.comments
[<Comment: Comment object>]
Viewを追加
Using Flask’s class-based views system allows you to produce List and Detail views for tumblelog posts. Add views.py and create a posts blueprint:
Flaskのviewシステムを使えば、ポストごとのリストビュー、詳細ビュー(個別ページ)を簡単につくれます。
view.pyファイルを作って以下を記述。
from flask import Blueprint, request, redirect, render_template, url_for from flask.views import MethodView from tumblelog.models import Post, Comment posts = Blueprint('posts', __name__, template_folder='templates') class ListView(MethodView): def get(self): posts = Post.objects.all() return render_template('posts/list.html', posts=posts) class DetailView(MethodView): def get(self, slug): post = Post.objects.get_or_404(slug=slug) return render_template('posts/detail.html', post=post) # Register the urls posts.add_url_rule('/', view_func=ListView.as_view('list')) posts.add_url_rule('/<slug>/', view_func=DetailView.as_view('detail'))
以下をinit.pyに追加。
def register_blueprints(app): # Prevents circular imports from tumblelog.views import posts app.register_blueprint(posts) register_blueprints(app)
テンプレートの追加
templateディレクトリと、template/postsディレクトリを作成。
mkdir -p templates/posts
そしたら、templates/base.html を作成して以下を記述。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>My Tumblelog</title> <link href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.css" rel="stylesheet"> <style>.content {padding-top: 80px;}</style> </head> <body> {%- block topbar -%} <div class="topbar"> <div class="fill"> <div class="container"> <h2> <a href="/" class="brand">My Tumblelog</a> <small>Starring Flask, MongoDB and MongoEngine</small> </h2> </div> </div> </div> {%- endblock -%} <div class="container"> <div class="content"> {% block page_header %}{% endblock %} {% block content %}{% endblock %} </div> </div> {% block js_footer %}{% endblock %} </body> </html>
次に、全ポストを表示するブログのランディングページを作ります。
templates/posts/list.html を作って以下を記述。
{% extends "base.html" %} {% block content %} {% for post in posts %} <h2><a href="{{ url_for('posts.detail', slug=post.slug) }}">{{ post.title }}</a></h2> <p>{{ post.body|truncate(100) }}</p> <p> {{ post.created_at.strftime('%H:%M %Y-%m-%d') }} | {% with total=post.comments|length %} {{ total }} comment {%- if total > 1 %}s{%- endif -%} {% endwith %} </p> {% endfor %} {% endblock %}
最後に個別ページ用テンプレートの作成。
templates/posts/detail.html を作って以下を記述。
{% extends "base.html" %} {% block page_header %} <div class="page-header"> <h1>{{ post.title }}</h1> </div> {% endblock %} {% block content %} <p>{{ post.body }}<p> <p>{{ post.created_at.strftime('%H:%M %Y-%m-%d') }}</p> <hr> <h2>Comments</h2> {% if post.comments %} {% for comment in post.comments %} <p>{{ comment.body }}</p> <p><strong>{{ comment.author }}</strong> <small>on {{ comment.created_at.strftime('%H:%M %Y-%m-%d') }}</small></p> {{ comment.text }} {% endfor %} {% endif %} {% endblock %}
一度確認してみよう
ここで一度
python manage.py runserver
してhttp://localhost:5000にアクセスしてみましょう。
以下のような画面が表示されるはずです。
コメントフォームの追加
ユーザがポストに対してコメントを残せるように、
WTFormを使ってフォームを用意しましょう。
まずはviews.pyファイルを修正します。
from flask.ext.mongoengine.wtf import model_form ... class DetailView(MethodView): form = model_form(Comment, exclude=['created_at']) def get_context(self, slug): post = Post.objects.get_or_404(slug=slug) form = self.form(request.form) context = { "post": post, "form": form } return context def get(self, slug): context = self.get_context(slug) return render_template('posts/detail.html', **context) def post(self, slug): context = self.get_context(slug) form = context.get('form') if form.validate(): comment = Comment() form.populate_obj(comment) post = context.get('post') post.comments.append(comment) post.save() return redirect(url_for('posts.detail', slug=slug)) return render_template('posts/detail.html', **context)
テンプレートにコメントを追加する
最後にテンプレートにフォームを追加します。
templates/_forms.htmlを作成し、以下を記述しましょう。
{% macro render(form) -%} <fieldset> {% for field in form %} {% if field.type in ['CSRFTokenField', 'HiddenField'] %} {{ field() }} {% else %} <div class="clearfix {% if field.errors %}error{% endif %}"> {{ field.label }} <div class="input"> {% if field.name == "body" %} {{ field(rows=10, cols=40) }} {% else %} {{ field() }} {% endif %} {% if field.errors or field.help_text %} <span class="help-inline"> {% if field.errors %} {{ field.errors|join(' ') }} {% else %} {{ field.help_text }} {% endif %} </span> {% endif %} </div> </div> {% endif %} {% endfor %} </fieldset> {% endmacro %}
これを、templates/posts/detail.htmlに追加します。
#ファイルの先頭に記述 {% import "_forms.html" as forms %} ... # {% endfor %} の後に記述 <hr> <h2>Add a comment</h2> <form action="." method="post"> {{ forms.render(form) }} <div class="actions"> <input type="submit" class="btn primary" value="comment"> </div> </form>
python manage.py runserverで確認してみましょう。
こんな感じでコメントフォームが追加されています。
サイト管理ページを作成
毎度shellから投稿というのも不便ですので管理画面ももちろん作りましょう!
auth snippet にならってベーシックな管理画面を作ります。
auth.pyを作成し、以下を記述します。
from functools import wraps from flask import request, Response def check_auth(username, password): """This function is called to check if a username / password combination is valid. """ return username == 'admin' and password == 'secret' def authenticate(): """Sends a 401 response that enables basic auth""" return Response( 'Could not verify your access level for that URL.\n' 'You have to login with proper credentials', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'}) def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): return authenticate() return f(*args, **kwargs) return decorated
管理ページ用のViewの作成
admin.pyを作成し、以下を記述。
from flask import Blueprint, request, redirect, render_template, url_for from flask.views import MethodView from flask.ext.mongoengine.wtf import model_form from tumblelog.auth import requires_auth from tumblelog.models import Post, Comment admin = Blueprint('admin', __name__, template_folder='templates') class List(MethodView): decorators = [requires_auth] cls = Post def get(self): posts = self.cls.objects.all() return render_template('admin/list.html', posts=posts) class Detail(MethodView): decorators = [requires_auth] def get_context(self, slug=None): form_cls = model_form(Post, exclude=('created_at', 'comments')) if slug: post = Post.objects.get_or_404(slug=slug) if request.method == 'POST': form = form_cls(request.form, inital=post._data) else: form = form_cls(obj=post) else: post = Post() form = form_cls(request.form) context = { "post": post, "form": form, "create": slug is None } return context def get(self, slug): context = self.get_context(slug) return render_template('admin/detail.html', **context) def post(self, slug): context = self.get_context(slug) form = context.get('form') if form.validate(): post = context.get('post') form.populate_obj(post) post.save() return redirect(url_for('admin.index')) return render_template('admin/detail.html', **context) # Register the urls admin.add_url_rule('/admin/', view_func=List.as_view('index')) admin.add_url_rule('/admin/create/', defaults={'slug': None}, view_func=Detail.as_view('create')) admin.add_url_rule('/admin/<slug>/', view_func=Detail.as_view('edit'))
次にinit.pyを修正します。
def register_blueprints(app): # Prevents circular imports from tumblelog.views import posts from tumblelog.admin import admin app.register_blueprint(posts) app.register_blueprint(admin)
管理ページ用のtemplate追加
templates/admin/base.htmlを作成し、以下を記述。
{% extends "base.html" %} {%- block topbar -%} <div class="topbar" data-dropdown="dropdown"> <div class="fill"> <div class="container"> <h2> <a href="{{ url_for('admin.index') }}" class="brand">My Tumblelog Admin</a> </h2> <ul class="nav secondary-nav"> <li class="menu"> <a href="{{ url_for("admin.create") }}" class="btn primary">Create new post</a> </li> </ul> </div> </div> </div> {%- endblock -%}
templates/admin/list.htmlを作成し、以下を記述。
{% extends "admin/base.html" %} {% block content %} <table class="condensed-table zebra-striped"> <thead> <th>Title</th> <th>Created</th> <th>Actions</th> </thead> <tbody> {% for post in posts %} <tr> <th><a href="{{ url_for('admin.edit', slug=post.slug) }}">{{ post.title }}</a></th> <td>{{ post.created_at.strftime('%Y-%m-%d') }}</td> <td><a href="{{ url_for("admin.edit", slug=post.slug) }}" class="btn primary">Edit</a></td> </tr> {% endfor %} </tbody> </table> {% endblock %}
templates/admin/detail.htmlを作成し、以下を記述。
{% extends "admin/base.html" %} {% import "_forms.html" as forms %} {% block content %} <h2> {% if create %} Add new Post {% else %} Edit Post {% endif %} </h2> <form action="?{{ request.query_string }}" method="post"> {{ forms.render(form) }} <div class="actions"> <input type="submit" class="btn primary" value="save"> <a href="{{ url_for("admin.index") }}" class="btn secondary">Cancel</a> </div> </form> {% endblock %}
確認してみよう
http://localhost:5000/admin/ にアクセスすると、こんな感じになってるはずです。
BlogをTumblelogにしよう
さあ、ここまでで"ブログ"は完成しました。
動画、画像、引用にも対応させましょう。
これらを追加するにあたって、あまり大きな修正は必要ありません。
MongoEngineはドキュメントの継承をサポートしているからです。
models.pyを以下の用に修正しましょう。
class Post(db.DynamicDocument): created_at = db.DateTimeField(default=datetime.datetime.now, required=True) title = db.StringField(max_length=255, required=True) slug = db.StringField(max_length=255, required=True) comments = db.ListField(db.EmbeddedDocumentField('Comment')) def get_absolute_url(self): return url_for('post', kwargs={"slug": self.slug}) def __unicode__(self): return self.title @property def post_type(self): return self.__class__.__name__ meta = { 'allow_inheritance': True, 'indexes': ['-created_at', 'slug'], 'ordering': ['-created_at'] } class BlogPost(Post): body = db.StringField(required=True) class Video(Post): embed_code = db.StringField(required=True) class Image(Post): image_url = db.StringField(required=True, max_length=255) class Quote(Post): body = db.StringField(required=True) author = db.StringField(verbose_name="Author Name", required=True, max_length=255)
viewのロジックはmongoEngineが勝手にやってくれます。
が、templateは自分で変更する必要があります。
templates/posts/list.htmlを修正。
{% if post.body %} {% if post.post_type == 'Quote' %} <blockquote>{{ post.body|truncate(100) }}</blockquote> <p>{{ post.author }}</p> {% else %} <p>{{ post.body|truncate(100) }}</p> {% endif %} {% endif %} {% if post.embed_code %} {{ post.embed_code|safe() }} {% endif %} {% if post.image_url %} <p><img src="{{ post.image_url }}" /><p> {% endif %}
templates/posts/detail.html を修正。
{% if post.body %} {% if post.post_type == 'Quote' %} <blockquote>{{ post.body }}</blockquote> <p>{{ post.author }}</p> {% else %} <p>{{ post.body }}</p> {% endif %} {% endif %} {% if post.embed_code %} {{ post.embed_code|safe() }} {% endif %} {% if post.image_url %} <p><img src="{{ post.image_url }}" /><p> {% endif %}
admin.pyを修正。
from tumblelog.models import Post, BlogPost, Video, Image, Quote, Comment # ... class Detail(MethodView): decorators = [requires_auth] # Map post types to models class_map = { 'post': BlogPost, 'video': Video, 'image': Image, 'quote': Quote, } def get_context(self, slug=None): if slug: post = Post.objects.get_or_404(slug=slug) # Handle old posts types as well cls = post.__class__ if post.__class__ != Post else BlogPost form_cls = model_form(cls, exclude=('created_at', 'comments')) if request.method == 'POST': form = form_cls(request.form, inital=post._data) else: form = form_cls(obj=post) else: # Determine which post type we need cls = self.class_map.get(request.args.get('type', 'post')) post = cls() form_cls = model_form(cls, exclude=('created_at', 'comments')) form = form_cls(request.form) context = { "post": post, "form": form, "create": slug is None } return context
template/admin/base.htmlを修正して、ポストのタイプをドロップダウンメニューで選べるようにします。
{% extends "base.html" %} {%- block topbar -%} <div class="topbar" data-dropdown="dropdown"> <div class="fill"> <div class="container"> <h2> <a href="{{ url_for('admin.index') }}" class="brand">My Tumblelog Admin</a> </h2> <ul class="nav secondary-nav"> <li class="menu"> <a href="#" class="menu">Create new</a> <ul class="menu-dropdown"> {% for type in ('post', 'video', 'image', 'quote') %} <li><a href="{{ url_for("admin.create", type=type) }}">{{ type|title }}</a></li> {% endfor %} </ul> </li> </ul> </div> </div> </div> {%- endblock -%} {% block js_footer %} <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script> <script src="http://twitter.github.com/bootstrap/1.4.0/bootstrap-dropdown.js"></script> {% endblock %}
これで完成です! 長かった。。。
Twitter Bootstrapのおかげで見た目もスッキリしてます。
こういう時特に、Bootstrapは大活躍ですね。