LoginSignup
0
0

Flask - 個人的NOTE / 公式DOC>Tutorial (Templates~BlogBluePrint) 2/3

Posted at

Templates

render_template()でhtmlファイルを呼び出している。
このhtmlファイルはflaskrパッケージの中のtemplatesディレクトリに保存する必要がある。

Jinja template

Templatesフォルダには、静的また動的なデータファイルがありtemplateはその中のデータと共に最終的なドキュメントを作成する。FlaskではJinjaというtemplate ライブラリを用いている

・Jinjaの詳細ページ

今回のアプリケーションの中では、ブラウザ向けのHTMLファイルにレンダリングするためにtemplatesフォルダを用いる。

自動エスケープ機能
Flaskではユーザが入力した値で の様にHTMLを混乱させる様なものがあっても安全な値に置換してくれる

Jinjaの {{ ~ }} {% ~ %}

  • {{ ~ }} :
    • 最終的なHTMLファイルの出力結果を示しています。(Flaskのview functionの結果の引数など)
  • {% ~ %} :
    • for や ifを示すブロック
    • Pythonとは違い、インデントではなくブロックでstart,endを示す({% for i in list %} {% endfor %})
base.html
<!DOCTYPE html>
<title>{% block title %}{% endblock %} - Flaskr</title>
<link rel="stylesheet" href="{{ url_for('static',filename=style.css) }}">
<nav>
    <h1>Flaskr</h1>
    <ul>
        {% if g.user %}
        <li><span>{{ g.user['username'] }}</span></li>
        <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
        {% else %}
        <li><a href="{{ url_for(auth.register) }}">Register</a></li>
        <li><a href="{{ url_for(auth.login) }}">Log in</a></li>
        {% endif %}
    </ul>
</nav>
<section class="content">
    <header>
        {% block header %}{% endblock %}
    </header>
    {% for message in get_flashed_messages() %}
    <div class="flash">{{ message }}</div>
    {% endfor %}
    {% block content %}{% endblock %}
</section>

navタグ :
image.png
現在の文書内の他の部分や他の文書へのナビゲーションリンクを提供するためのセクションを表します。ナビゲーションセクションの一般的な例としてメニュー、目次、索引などがあります。

spanタグ :
インラインレベルのスタイル付けに利用(でもここだと単にユーザー名をコード上で強調したかった?)
image.png

gオブジェクト :
gオブジェクトはg.userがセットされているかどうかに応じて、usernameとログアウトリンクが表示されるか、新規登録かログインの画面が表示されるかを決める。

url_for() :
手打ちでURLを書いてview functionに紐づける代わりに自動でURLを生成させるのに使っている

flashの内容をHTMLに反映させる :
get_flashed_messages()を使って、view functionで使ったflash()処理に入れたエラー情報を表示させる

    {% for message in get_flashed_messages() %}
    <div class="flash">{{ message }}</div>
    {% endfor %}

他のtemplateに上書きされて使われる箇所

  • {% block title %}
    • ブラウザーのタブと、ウィンドウのタイトルに表示されるタイトルを司る
  • {% block header %}
    • titleに似ているが、ページ上のタイトルを司る
  • {% block content %}
    • 各ページにおけるコンテントを司る(ログインフォームやブログ投稿など)

・blueprintとtemplates
Blueprint用のtemplateは、templatesフォルダの中のblueprintと同じ名前を持ったフォルダの中で管理すれば、統一感を維持できる。

Register

templates/auth/register.html
{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Register{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method='post'>
    <label for="username">Username</label>
    <input name="username" id="username" required>
    <label for="password">Password</label>
    <input type="password" name="password" id="password" required>
    <input type="submit" value="Register">
</form>
{% endblock %}

{% extends 'base.html' %} :
この表記は当template(register)が、base.htmlの各ブロックの表記を置換することをJinjaに伝えている。

{% block header %}
{% block title %} Register {% endblock %}
{% endblock %}
:
title blockに入れた値がheaderにも出力されて同じ処理を2回も描かなくて良くなるという手法(header : window, title : page)

・titleタグ :
ブラウザーの題名バーやページのタブに表示される文書の題名を定義します。テキストのみを含めることができます。要素内のタグはすべて無視されます。

・headerタグ :
導入的なコンテンツ、ふつうは導入部やナビゲーション補助のグループを表します。見出し要素だけでなく、ロゴ、検索フォーム、著者名、その他の要素を含むこともできます。
image.png

inputタグのrequired :
必須項目事項の指定ができる、が、ブラウザのバージョンやユーザーの入力環境によってはrequestされる可能性もあるので、常にユーザからの入力された値は検証しなければならない

Log In

auth/login.html
{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}Login{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method="post">
    <label for="username">Username</label>
    <input type="text" name="username" id="username" required>
    <label for="password">Password</label>
    <input type="password" name="password">
    <input type="submit" value="Log in">
</form>
{% endblock %}

Static Files

Authentication templateに用いられるデザインは単調なのでCSSファイルを適用させる。このデザインは静的なのでtemplatesではなくstaticフォルダで管理する

Flaskはflaskr/staticディレクトリからの相対PATHでstatic viewを追加する、base.htmlはすでにstyle.cssへのリンクを持っている

{{ url_for('static',filename=・・・) }}
staticファイルにはcssの他Javascriptや画像などのファイルも含まれる。それらのファイルはすべてflaskr/staticフォルダに格納されて、url_forを用いてアクセスされる。

style.cssを適用後の画面 :
image.png

Blog Blueprint

Blockはすべての投稿を一覧化し、ログインしたユーザには投稿作成・作成者には投稿した内容の編集と削除ができる様にする

The Blueprint

blog.py
from flask import(
    Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort
from flaskr.auth import login_required
from flaskr.db import get_db

bp = Blueprint('blog', __name__)

appインスタンスにBlog Blueprintを譲渡する

__init__.py
import os
from flask import Flask

def create_app(test_config=None):
    # appインスタンスの生成と設定
    app = Flask(__name__, instance_relative_config=True) # check, instance_relative_config
    app.config.from_mapping(
        SECRET_KEY = 'dev',
        DATABASE = os.path.join(app.instance_path, 'flaskr.sqlite')
    )
    print('app.instance_path : ', app.instance_path)
    
    if test_config is None:
        # test状態ではなく、configインスタンスが存在する場合はconfigインスタンスを読み込む
        app.config.from_pyfile('config.py', silent=True) # check, from_pyfile
    else:
        # test_configが渡されていたら読み込み
        app.config.from_mapping(test_config)

    # フォルダの存在を確認する
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # hello出力
    @app.route('/hello')
    def hello():
        return 'Hello, World!!'
    # db情報のimportと登録    
    from . import db
    db.init_app(app)

    # blueprintのimportと登録
    from . import auth
    app.register_blueprint(auth.bp)
#---------------------------------------------
#追加部分 : 
    from . import blog
    app.register_blueprint(blog.bp)
    app.add_url_rule('/', endpoint='index')
#---------------------------------------------
    return app

url_prefixがない

auth系(loginなど)のURLへはurl_prefixを入れていた
bp = Blueprint('auth', __name__,url_prefix='/auth')

blog系のbpはこのサービスのメインコンテンツなのでメインの目次として扱うためにurl_prefixを実装していない。

app.add_url_rule('/',endpoint='index')について

url_for('index')url_for('blog.index')が共に /というURLを生み出せる様に URLルールを変更している

blog blueprintの中のエンドポイント index は何もしないとblog.indexとなる.。

エンドポイント"Index"

以下のindexへの処理は全投稿内容を、投稿日時で降順で並べる。Userテーブルから投稿者の情報を利用できる様にするためにJOINが使われている。

blog/index
@bp.route('/')
def index():
    db = get_db()
    posts = db.execute(
        'SELECT p.id, title, body, created, author_id, username'
        'FROM post p JOIN user u ON p.author_id=u.id'
        'ORDER BY created DESC'
    ).fetchall()
    return render_template('blog/index.html',posts=posts)
index.html
{% extends 'base.html' %}
{% block header %}
    <h1>{% block title %}Posts{% endblock %}</h1>
{% if g.user %}
    <a href="{{url_for('blog.create')}}" class="action">New</a>
{% endif %}
{% endblock %}

{% block content %}
{% for post in posts %}
<article class="post">
    <header>
        <div>
            <h1>{{ post['title'] }}</h1>
            <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
        </div>
        {% if g.user['id'] == post['author_id'] %}
        <a href="{{ 'blog.update', id=post['id'] }}" class="action">Edit</a>
        {% endif %}
    </header>
    <p class="body">{{ post['body'] }}</p>
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{% endblock %}

articleタグ :
文書、ページ、アプリケーション、サイトなどの中で自己完結しており、(集合したものの中で)個別に配信や再利用を行うことを意図した構成物を表します。例えば、フォーラムの投稿、雑誌や新聞の記事、ブログの記事、商品カード、ユーザーが投稿したコメント、対話型のウィジェットやガジェット、その他の独立したコンテンツの項目が含まれます。
image.png

{% if not loop.last %} :
Jinjaの特殊な記法で、loop.lastはその時点のループが最後の処理かどうかを判定します。この場合、最後ではないループ処理の場合は区切り線<hr>を入れるという処理になっています。

Create

register viewと同じ様な動きをするviewです。
・フォームを表示
・投稿された情報を検証し、データベースに追加するかもしくはエラーを出すかをします

blog.py
@bp.route('/create', methods=('GET','POST'))
@login_required
def create():
    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'
        
        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'INSERT INTO post (title, body, author_id)'
                ' VALUES (?,?,?)',
                (title, body, g.user['id'])
                )
            db.commit()
            return redirect(url_for('blog.index'))
    return render_template('blog/create.html')

blog.indexを使っている理由 :
恐らくコードの可読性を高めるためにブループリント名を明示していると考えられる。

TODO :
このurl_forのblog.indexをindexに変換して動作に違いが出るかを検証

blog/create.html
{% extends 'base.html' %}

{% block header %}
<h1>{% block title %}New Post{endblock}</h1>
{% endblock %}

{% block content %}
<form method="post">
    <label for="title">Title</label>
    <input type="text" name="title" id="title" value="{{request.form['title']}}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body" >{{ request.form['body'] }}</textarea>
    <input type="submit" value="Save">
</form>
{% endblock %}

各HTMLのタグの補足 :

labelタグ

  • for: 以下のinputやtextareaタグのidと紐づけると次のメリットがある
    • ブラウザの読み上げ機能が連動する
    • labelをクリックするとinputがアクティブになる(スマホなど)

inputタグ

  • name:
    • 入力欄コントロールの名前。名前/値の組の部分としてフォームと一緒に送信される
  • id :
    • labelタグのforと連動させる値
  • type="submit"のvalue :
    • ボタンのラベルとして表示される文字列を示します。ボタンはその他の真の値を持ちません。
    • 指定しなかったらブラウザのデフォルト値が表示される(送信など)

textareaタグ

  • name:
    • フォームが送信されたときにデータポイントに関連付けられた名前を設定している
  • id:
    • labelタグのforと連動させる値

Update

updateとdeleteのviewは投稿情報をユーザIDを使って取得する必要がある。そして、投稿者がログインしているユーザーと同じかどうかを判定する

重複した内容のコードを作る事を避けるため、投稿情報を取得して返すget_post 関数を作成する

blog.py
def get_post(id, check_author=True):
    post = get_db().execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = user_id'
        ' WHERE p.id=?',
        (id,)
    ).fetchone()

    if post is None:
        abort(404, f"Post id {id} doesn't exist.")
    
    if check_author and post['author_id'] != g.user['id']:
        abort(403)
    
    return post

abort()関数
HTTPstatusコードを指定して吐き出す関数。第二引数の文字列は任意の文字列となり、指定しなければ各のstatusのデフォルトの値が表示される

デフォルト値 :
404 -- Not Found
403 -- Forbidden
401 -- => 今回はlogin画面への自動遷移

check_author引数
このキーワード引数は、投稿者とログインしているユーザーが一致しているかの照合検証を行うかどうかを指定している。

blog.pyにてcheck_authotがTrueの時
if check_author and post['author_id'] != g.user['id']:
    abort(403)

get_post({id}, check_author=False)で呼び出す:
=> 投稿を編集しない一般のユーザー向けに投稿情報を表示する場合に使用

get_post({id})で呼び出す:
=> 編集機能を意図する時の呼び出し方。投稿者とログインユーザーの照合を投稿情報を返す前に実行する

blog.py
@bp.route('/<int:id>/update',methods=('GET','POST'))
@login_required
def update(id):
    post = get_post(id) # ユーザの照合を行う

    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required'
        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'UPDATE post SET title=?, body=?'
                ' WHERE id = ?',(title, body,id)
            )
            db.commit()
            return redirect(url_for('blog.index'))
    return render_template('blog/update.html', post=post)

URL内の<int:id>
@bp.route('/<int:id>/update' ~ :
idは元々データベーススキーマではINTEGERとして定義されている
image.png

FlaskはURLに含まれた「1」という数字がINTEGER型かどうかを確かめてから関数に対して INTEGER型のidとして引き渡している。

もし、文字列が入っていれば Errorコード404を返す

もし、<int:id>を指定しなかったら、idは123の様な数字であってもSTRING型"123"として引き渡される。

update.html
{% expands 'base.html' %}

{% block header %}
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}

{% block content %}
<form method="post">
    <label for="title">Title</label>
    <input type="text" name="title" id="title" value="{{ request.form['title'] or post['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
    <input type="submit" value="Save">
</form>
<hr>
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
    <input type="submit" class="danger" value="Delete" onclick="return confirm('Are you sure?');">
</form>
{% endblock %}

{{ request.form['title'] or post['title'] }} :
フォームが送信される前は元々の投稿情報を表示し、もし正しくない情報が入力された場合はその投稿した時の値を表示します。

Delete

Delete viewはtemplateを持っていない、update.htmlのボタンの一部としてdeleteボタンが存在しており、このボタンから /\<id\>/deleteへRequestされる

templateがないのでPOSTメソッドだけを扱い、実行されたらindex viewへ遷移させるという機能だけを持つ

blog.py
@bp.route('/<int:id>/delete', method=('POST',))
@login_required
def delete(id):
    get_post(id)
    db = get_db()
    db.execute('DELETE FROM post WHERE id = ?', (id,))
    db.commit()
    return redirect(url_for('blog.index'))

=> ようやく最終セクション ・プロジェクトをインストール可能にする

0
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
0
0