PyramidでToDo管理サービスを作る 【ToDo登録画面作成】

前回は、ToDoを管理するモデルを作成したので、登録するための画面を作成する。
# 前回からだいぶ時間が経ってしまった…

neoinal.hatenablog.com

実行環境

セッションの設定

pyramidでセッションを使うには、 config に使用するセッションを登録すれば良いらしい。

todomanager/__init__.py にセッションを使うための設定をする。
セッションのシークレットキーは外部ファイルや環境変数辺りに設定するのが正しいのだろうけど、まだ作り途中であるため development.ini から読み取るようにする。

import hashlib
from pyramid.session import SignedCookieSessionFactory

def main(global_config, **settings):
    secret = hashlib.sha256(str(settings['pyramid.session.secret']).encode('utf-8')).hexdigest()
    session_factory = SignedCookieSessionFactory(secret=secret)
    # 省略
    config.set_session_factory(session_factory)

development.ini にシークレットキーを設定する。
デプロイするときはランダム生成させた文字列にした方が良いと思われる。

pyramid.session.secret = development

テンプレートの設定

pyramidのデフォルトで設定されているテンプレートエンジンは Chameleon となっている。
Djangoでは標準のテンプレートを使っていたことも有り、 jinja2 に変更する。

テンプレートエンジン用のモジュールをインストール

次のコマンドを入力してモジュールをインストールする。

(pyramid_python)> pip install pyramid_jinja2

レンダラーを変更

Jinja2 を利用して作成したテンプレートを表示するための準備を行う。
ToDoManager を起動させる上で必要となるモジュール郡の中に、テンプレートエンジン用のモジュールが設定されているので、その部分を Jinja2 用に変える。

setup.py に書かれている部分を次のように修正する。

requires = [
    ...
    'pyramid_chameleon',
    ...
    ]
↓
requires = [
    ...
    'pyramid_jinja2',
    ...
    ]

MANIFEST.in に書かれている以下の部分に *.jinja2 を追記する。

recursive-include todomanager *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
                                                                                                      ^^^^^^^^

todomanager/__init__.py に書かれている部分をつぎのように修正する。

def main(global_config, **settings):
    ...
    config.include('pyramid_chameleon')
    ...
↓
def main(global_config, **settings):
    ...
    config.include('pyramid_jinja2')
    ...

テンプレートの作成

テンプレートエンジンとして jinja2 を使用するが、タグや記述方法については公式を参照。

Welcome to Jinja2 — Jinja2 Documentation (2.8-dev)

Bootstrapの用意

こういうサービスを作るときに便利なのが Bootstrap である。

Bootstrap · The world's most popular mobile-first and responsive front-end framework.

「Bootstrap CDN」を利用すると、必要なファイル一式をダウンロードしなくてもBootstrapを使うことができるらしい。
今回は、静的ファイルの使い方を練習する意味も兼ねてBootstrap一式をダウンロードすることにした。

ToDoManager/todomanager/staticbootstrap ディレクトリを作成して、展開したファイル一式をコピーする。

./ToDoManager/todomanager/static
└─bootstrap
    ├─css
    │      bootstrap-theme.css
    │      bootstrap-theme.css.map
    │      bootstrap-theme.min.css
    │      bootstrap.css
    │      bootstrap.css.map
    │      bootstrap.min.css
    │
    ├─fonts
    │      glyphicons-halflings-regular.eot
    │      glyphicons-halflings-regular.svg
    │      glyphicons-halflings-regular.ttf
    │      glyphicons-halflings-regular.woff
    │      glyphicons-halflings-regular.woff2
    │
    └─js
            bootstrap.js
            bootstrap.min.js
            npm.js

テンプレートの共通部分

それぞれのページで共通となる部分を base.jinja2 と定義して以下の様に記述する。

{%- set navigation_bar = [
    (request.route_url('home'), 'home', 'Home'),
    (request.route_url('create'), 'create', 'Create'),
] -%}
{%- set active_page = active_page|default('home') -%}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE|default('ja') }}">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ page_title|default('ToDo Manager') }}</title>
    <link href="./static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="./static/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
    <nav class="navbar navbar-inverse navbar-static-top">
      <div class="container-fluid">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="{{ request.route_url('home') }}">ToDo Manager</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse">
          <ul class="nav navbar-nav">
          {%- for here, id, caption in navigation_bar %}
              <li{% if id == active_page %} class="active"{% endif %}><a href="{{ here|e }}">{{ caption|e }}</a></li>
          {%- endfor %}
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </nav>
    <div class="container-fluid">
    {%- block container %}{%- endblock %}
    </div><!-- /.container-fluid -->
</body>
</html>

ToDoを登録するページを create.jinja2 として以下のように記述する。

{% extends 'base.jinja2' %}

{%- block container %}
<div class="row">
    <form name="create" action="{{ request.route_url('create') }}" method="post" class="form-horizontal col-md-8 col-md-offset-2">
        <input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}" />
    {%- if result != null %}
        <div class="alert{% if result['status'] == 'success' %} alert-success{% else %} alert-danger{% endif %}" id="result-alert" role="alert">
            <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden>&times;</span></button>
            {{ result['message']|e }}
        </div>
    {%- endif %}
        <div class="form-group">
            <label class="control-label" for="id_summary">ToDo 内容</label>
            <textarea id="id_summary" name="summary" class="form-control" rows="10" cols="40" required="required"></textarea>
        </div>
        <div class="form-group">
            <button class="btn btn-success" type="submit">登録</button>
        </div>
    </form>
</div>
{%- endblock %}

ビューの設定

ビューの処理を作成

scaffoldで作成された view.py は、クラスは無く関数にURLのマッピングレンダリング用のテンプレートが指定されている。

from pyramid.response import Response
from pyramid.view import view_config

@view_config(route_name='home', renderer='templates/mytemplate.pt')
def my_view(request):
    return Response("This page does not created yet.", content_type='text/plain', status_int=500)

機能毎にビューを分けられるようクラスを使用して、マッピングするURLとレンダリング用のテンプレートの設定などを行う。
ホームの処理については、画面デザインを決めあぐねているため仮置きとする。

from pyramid.response import Response
from pyramid.session import check_csrf_token
from pyramid.view import view_config
from todomanager.models import Task, DBSession


class TaskView(object):

    def __init__(self, request):
        self.request = request

    @view_config(route_name='home', renderer='templates/base.jinja2')
    def home(self):
        return Response(
            "This page does not created yet.",
            content_type='text/plain',
            status_int=500
        )

    @view_config(route_name='create', renderer='templates/create.jinja2')
    def create(self):
        parameter = {
            "page_title": "ToDo Manager - タスクの作成",
            "active_page": "create",
        }

        if self.request.method == 'POST':
            if check_csrf_token(self.request):
                summary = self.request.POST.get("summary")
                if summary is not None:
                    task = Task(summary=summary)
                    DBSession.add(task)
                    result = {
                        "status": "success",
                        "message": "ToDoの登録に成功しました。",
                    }
                else:
                    result = {
                        "status": "error",
                        "message": "内容が入力されていません。",
                    }

                parameter["result"] = result
            else:
                raise ValueError("CSRF token did not match.")
        return parameter

ビューを表示するところに、ToDoをDBに登録する処理があるのはイケてない感じがするものの、ひとまずはこれで登録できるようになった。

URLとマッピング

作成したビューの処理を呼び出すために todomanager/__init__.py にルーティングを設定する。
今回新たに紐付けたURLは以下の1つ。

  • ToDo作成画面(/create)

もともと設定されていた home はそのまま使用する。

def main(global_config, **settings):
    ...
    config.add_route('home', '/')
    config.add_route('create', '/create')
    ...