LoginSignup
1
0

More than 3 years have passed since last update.

japronto備忘録(1) テンプレート

Posted at

Python+japrontoを試したときに作成したプロジェクトテンプレートを個人的な備忘録として投稿しておきます。

※長くなるので、ポイントとなる部分のみです。

1. 構成

※htmlはbootstap4を使うように設定しています。

(1) プロジェクトには、以下のフォルダで構成されます。

フォルダ名 説明
├─api    プロジェクトの規定アプリを格納しているフォルダ
├─apps    各業務アプリの処理をこのフォルダ内に実装する
├─config  データベース、ロガーの設定ファイル
├─static  css, jsなどの静的ファイル
├─templates jinja2テンプレート用フォルダ

(2) 具体的な構成は以下の通り

│  main.py
├─api
│  │  app.py
│  │  db.py
│  │  initlog.py
│  │  settings.py
├─apps
│  ├─admin
│  │  │  views.py
│  ├─auth
│  │  │  views.py
│  ├─index
│  │  │  views.py
│  ├─todos
│  │  │  service.py
│  │  │  views.py
│  ├─userrole
│  │  │  service.py
│  │  │  views.py
│  └─users
│      │  service.py
│      │  views.py
├─config
│  │  database.conf
│  │  logging.conf
├─static
│  ├─css
│  │      bootstrap.min.css
│  │      main.css
│  └─js
│          bootstrap.min.js
│          jquery-3.2.1.min.js
└─templates
    │  base.html
    ├─admin
    │  │  index.html
    │  ├─userrole
    │  │      create.html
    │  │      delete.html
    │  │      index.html
    │  │      update.html
    │  └─users
    │          create.html
    │          delete.html
    │          index.html
    │          update.html
    ├─auth
    │      login.html
    ├─index
    │      index.html
    └─todos
            create.html
            delete.html
            index.html
            update.html

2. api

  • api/app.py

japrontoの初期化、staticファイルのルーティングなどを行います

from japronto import Application
from api.db import connection
import api.initlog
import os.path
import os
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, Template

# If necessary you can add the content type 
CONTENTTYPES = {
 'css':'text/css',
 'js':'text/javascript',
 'pdf':'application/pdf',
 'xlsx':'application/vnd.ms-excel',
}

#extension
def get_extension(path):
    root, ext = os.path.splitext(path)
    return ext

#mimetype
def get_mimetype(path):
    ext = get_extension(path)
    if (ext[1:] in CONTENTTYPES):
        return CONTENTTYPES[ext[1:]]
    return "application/octet-stream"

#static files route
def static(request):
    mimetype = get_mimetype(request.path[1:])
    with open(request.path[1:], 'rb') as static_file:
        return request.Response(body=static_file.read(), mime_type=mimetype)

#template engine
template_path=os.path.join(Path(__file__).resolve().parents[1], 'templates')
env = Environment(loader=FileSystemLoader(template_path))

# get jinja2 template
def get_tpl(filename):
    return env.get_template(filename)

#decorator
def controller(func):
    def inner(*args, **kwargs):
        request = args[0]
        url=request.path
        loginuser = request.is_loggedin
        if not loginuser:
            return request.Response(headers={'Location': '/login'}, code=302)
        elif (not loginuser['userrole']==0) and url.startswith("/admin"):
            return request.Response(code=403, text="管理者のみ利用可能です")
        else:
            return func(*args, **kwargs)
    return inner    

#create japronto
def createApp():
    app = Application()
    # setup static files
    # https://github.com/squeaky-pl/japronto/issues/43
    app.router.add_route('/static/{1}', static)
    app.router.add_route('/static/{1}/{2}', static)
    app.router.add_route('/static/{1}/{2}/{3}', static)

    # database connection extend
    app.extend_request(connection, property=True)

    return app

app=createApp()
  • api/db.py

connection poolの生成などを行います。

import psycopg2
import psycopg2.extras
import psycopg2.pool

from api.settings import config_section

dbconf = config_section('DB')

host=dbconf.get('host')
port=dbconf.get('port')
dbname=dbconf.get('dbname')
user=dbconf.get('user')
password=dbconf.get('password')
minconn=dbconf.get('minconn')
maxconn=dbconf.get('maxconn')

# connection pool
connection_pool = psycopg2.pool.SimpleConnectionPool(
    minconn=minconn, maxconn=maxconn, 
    host=host, port=port, 
    dbname=dbname, user=user, password=password)

# get cursor
def connection(request):
    def do_callback(request):
        connection_pool.putconn(request.extra['conn'])

    if 'conn' not in request.extra:
        request.extra['conn'] = connection_pool.getconn()
        request.add_done_callback(do_callback)

    return request.extra['conn']

  • api/initlog.py

ログの設定

import yaml
import logging
from logging import config, getLogger

# logging setup
config.dictConfig(
    yaml.load(open('config/logging.conf', encoding='UTF-8').read(), Loader=yaml.FullLoader))

  • api/settings.py

config管理

import os
import configparser
from pathlib import Path

config = configparser.ConfigParser()
config.read('config/database.conf')

def config_section(key):
    return config[key]

3. apps

  • apps/todos/service.py

サービス層は、crudのapi的な役割を定義する。例として、todoアプリであれば以下のようにする。

def find_all(cur):
    sql = "select * from todos"
    dict_result = []
    cur.execute(sql)
    rows = cur.fetchall()
    for row in rows:
        dict_result.append(dict(row))
    return dict_result

def find_by_id(cur, query):
    sql = "select * from todos  where id = %s "
    cur.execute(sql, (query['id'],))
    row = cur.fetchone()
    return dict(row)

def insertdata(cur, query):
    print(query)
    sql = "insert into todos (todo) values (%s)"
    cnt = cur.execute(sql, (query['todo'], ))
    return cnt

def updatedata(cur, query):
    print(query)
    sql = "update todos set todo = %s  where id = %s"
    cnt = cur.execute(sql, (query['todo'], query['id'], ))
    return cnt

def deletedata(cur, query):
    print(query)
    sql = "delete from todos  where id = %s"
    cnt = cur.execute(sql, (query['id'], ))
    return cnt
  • apps/todos/views.py

Django的にしています。controllerデコレータで、認証チェック、権限チェックが行われ、チェックOKなら、各種コントローラが実行されることになる。

from api.app import app, controller, get_tpl
from psycopg2.extras import DictCursor
from apps.todos.service import find_all, find_by_id, insertdata, updatedata, deletedata

@controller
def index(request):
    loginuser = request.is_loggedin
    html=get_tpl('/todos/index.html')
    result={}
    conn = request.connection
    with conn.cursor(cursor_factory=DictCursor) as cur:
        result=find_all(cur)
    return request.Response(text=html.render(data=result, user=loginuser),mime_type='text/html') 

@controller
def create(request):
    loginuser = request.is_loggedin
    html=get_tpl('/todos/create.html')
    result={}
    if request.method.lower()=="post":
        conn = request.connection
        with conn.cursor(cursor_factory=DictCursor) as cur:
            result=insertdata(cur, request.form)
        conn.commit()
    return request.Response(text=html.render(data=result, user=loginuser),mime_type='text/html') 

@controller
def update(request):
    loginuser = request.is_loggedin
    html=get_tpl('/todos/update.html')
    result={}
    conn = request.connection
    with conn.cursor(cursor_factory=DictCursor) as cur:
        if not request.method.lower()=="post":
            result=find_by_id(cur, request.query)
        else:
            result=updatedata(cur, request.form)
        conn.commit()
    return request.Response(text=html.render(data=result, user=loginuser),mime_type='text/html') 

@controller
def delete(request):
    loginuser = request.is_loggedin
    html=get_tpl('/todos/delete.html')
    result={}
    conn = request.connection
    with conn.cursor(cursor_factory=DictCursor) as cur:
        if not request.method.lower()=="post":
            result=find_by_id(cur, request.query)
        else:
            result=deletedata(cur, request.form)
        conn.commit()
    return request.Response(text=html.render(data=result, user=loginuser),mime_type='text/html') 

app.router.add_route('/todos', index)
app.router.add_route('/todos/create', create)
app.router.add_route('/todos/update', update)
app.router.add_route('/todos/delete', delete)

以下は、プロジェクトに最低限必要なアプリケーション

  • apps/admin/views.py

管理者用のトップページ用コントローラ

from api.app import app, controller, get_tpl

@controller
def index(request):
    loginuser = request.is_loggedin
    html=get_tpl('/admin/index.html')
    return request.Response(text=html.render(user=loginuser),mime_type='text/html') 

app.router.add_route('/admin', index)

  • apps/auth/views.py

ログイン認証用のコントローラ

from api.app import app, controller, get_tpl
from http.cookies import SimpleCookie
from psycopg2.extras import DictCursor
import hashlib
import datetime 
import time

sessions = {}

# todo session timeout 
timeout_minute=30

def is_user(request, username, password):
    conn = request.connection
    with conn.cursor(cursor_factory=DictCursor) as cur:
        sql = "select a.* from users a  where a.username = %s and a.password=%s "
        cur.execute(sql, (username, password))
        row = cur.fetchone()
        if row:
            return dict(row)
    return None

def create_sessionid(user):
    # sessionを生成
    session_id = hashlib.sha256(user['username'].encode()).hexdigest()
    # sessionは、辞書内に管理する。
    sessions[session_id] = user
    return session_id

def redirect(request, url, cookies=None):
    return request.Response(headers={'Location': url}, code=302, cookies=cookies)

def get_cookie_sessionid(request):
    if 'Cookie' in request.headers:
        cookies = SimpleCookie()
        cookies.load(request.headers['Cookie'])
        if 'SessionID' in cookies:
            return cookies['SessionID'].value
    return None  

# login controller
def login(request):
    html=get_tpl('/auth/login.html')
    if request.method.lower()=="post":
        form = request.form
        username=form['username']
        password=form['password']
        user = is_user(request, username, password)
        if user:
            session_id = create_sessionid(user)
            cookies = SimpleCookie()
            cookies['SessionID'] = session_id
            return redirect(request, '/', cookies)
        else:
            errmsg = 'ユーザIDまたはパスワードが違います'
    return request.Response(text=html.render(), mime_type='text/html')

def is_loggedin(request):
    request.extra['authenticateduser']=None
    session_id = get_cookie_sessionid(request)
    if session_id and (session_id in sessions):
        request.extra['authenticateduser']=sessions[session_id]
    # 未ログインの状態
    return request.extra['authenticateduser']

# logout controller
def logout(request):
    session_id = get_cookie_sessionid(request)
    sessions.pop(session_id)
    return redirect(request, "/login")

app.router.add_route('/login', login)
app.router.add_route('/logout', logout)

# authentication extend
app.extend_request(is_loggedin, property=True)

  • apps/index/views.py

ログイン後の初期表示画面用コントローラ

from api.app import app, get_tpl, controller

@controller
def index(request):
    loginuser = request.is_loggedin
    html=get_tpl('index/index.html')
    return request.Response(text=html.render(user=loginuser),mime_type='text/html') 

app.router.add_route('/', index)

以下、長くなるので割愛

apps/userrole
apps/users

4. config

  • config/databaase.conf

コネクションプールの接続用プロパティ

[DB]
minconn=10
maxconn=50 
host=localhost
port=5432 
dbname=japrontodb
user=testuser
password=***********************
  • config/logging.conf

logger用のプロパティファイル

version: 1

formatters:
  custmoFormatter:
    format: '%(asctime)s %(levelname)s - %(filename)s %(funcName)s %(lineno)d: %(message)s'
    ; datefmt: '%Y/%m/%d %H:%M:%S'

loggers:
  test:
    handlers: [fileRotatingHandler,consoleHandler]
    level: DEBUG
    qualname: test
    propagate: no

  file:
    handlers: [fileRotatingHandler]
    level: DEBUG
    qualname: file
    propagate: no

  console:
    handlers: [consoleHandler]
    level: DEBUG
    qualname: console
    propagate: no

handlers:
  fileRotatingHandler:
    formatter: custmoFormatter
    class: logging.handlers.TimedRotatingFileHandler
    level: DEBUG
    filename: /mnt/c/linux_home/japronto/log/application.log
    encoding: utf8
    when: 'D'
    interval: 1
    backupCount: 14

  consoleHandler:
    class: logging.StreamHandler
    level: DEBUG
    formatter: custmoFormatter
    stream: ext://sys.stdout

root:
  level: DEBUG
  handlers: [fileRotatingHandler,consoleHandler]

5. static

割愛

6. templates

  • templates/index/index.html

トップページ

{% extends 'base.html' %}
{% block content %}

<div class="jumbotron" style="min-height: 500px;">
    <h2>Hi, {{ user.username }}, your role is {{ user.userrole }}</h2>
    <h1 class="display-4">Welcome to My Project!!</h1>
</div>
{% endblock %}
  • templates/auth/login.html

ログイン画面

{% extends 'base.html' %}
{% block content %}
<h1>Log in</h1>
<form method='post' action='/login' enctype='application/x-www-form-urlencoded'>
    <div class='form-group' >
        <label class='form-label'>username</label>
        <input class='form-control' type='text' name='username'/>
    </div>
    <div class='form-group' >
        <label class='form-label'>password</label>
        <input class='form-control' type='password' name='password'/>
    </div>
    <button class='btn btn-primary' type='submit'>submit</button>
</form>
{% endblock %}
  • templates/base.html

ページ全体のレイアウト

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Cache-Control" content="no-cache">
    <link rel="stylesheet" href="/static/css/bootstrap.min.css">
    <link rel="stylesheet" href="/static/css/main.css">
    <script type="text/javascript" src="/static/js/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="/static/js/bootstrap.min.js"></script>
    <style>
      .navi {display: flex; align-items: flex-end; font-size: 1.2em;}
      .navi >div> a {padding: 5px;}
      .left {display: flex; justify-content: flex-start;}
      .right{display: flex; justify-content: flex-end;}
      .title {font-family: Meiryo UI; font-size: 1.4em !important; color: white !important; min-width: 200px;}
    </style>
</head>
<body>
<div class="container">
  <div class="col-md-10 col-md-offset-1">
    {% if user  %}
      <div class="right">logged in: {{ user.username }} </div>
    {% endif %}
    <header style="background: #263238;padding-left:5px;padding-right:5px;">
        <div class="navi">
          <div class="navi left" style="width:70%; ">
            <div class="title">My Project</div>
            {% if user  %}
            <div>
              <a href="/todos">todo</a>
            </div>
            {% endif %}
        </div>
          <div class="navi right" style="width:29%; ">
              {% if user and user.userrole==0  %}
              <div>
                  <a href="/admin">admin</a>
              </div>
              {% endif %}
              {% if user  %}
              <div>
                <a href="/logout">logout</a>
              </div>
              {% endif %}
          </div>
        </div>
    </header>    
    {% block content %}{% endblock %}
  </div>
</div>
</body>
</html>

7. main.py

  • main.py
from api.app import app
import apps.auth.views
import apps.index.views
import apps.admin.views
import apps.users.views
import apps.userrole.views
import apps.todos.views

if __name__ == '__main__':
    app.run(host='127.0.0.1',port=7777)

8. table

create table "public".userrole (
  roleid integer not null
  , rolename character varying(32) not null
  , primary key (roleid)
);
create table "public".users (
  id serial primary key
  , username character varying(32) not null
  , password character varying(128) not null
  , email character varying(70) not null
  , userrole integer
  , active character(1) not null
  , created_at timestamp(6) without time zone default CURRENT_TIMESTAMP not null
);
create table "public".todos (
    id bigserial primary key
  , todo character varying(255) not null
);

9 その他

japronto備忘録(2) CRUDアプリケーションを自動的に生成するに続きます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0