現勤務先では使っていないものの、配属先移動などによっては今後業務としても使いうることもあり、家で Flask さわって物体検知アプリを作って機械学習を囓ったり、最近は Django や Poetry をさわったりして、Pythonを中心にプログラミングを勉強している情シスとメカニックの狭間にいる33歳独身です。
上記の 2つのPythonのフレームワーク を学習する中で
テンプレートの.htmlファイル内のコメントアウト内にPythonの制御構文を書いて埋め込むテンプレートタグ{% ~ %}を中途半端に記述すると、結構な頻度でエラーになる
ことを知ったので今回備忘録として残しました。
今現在純粋なITエンジニアではないため、初歩的な内容かもしれませんが、今後これらのフレームワークや同様のフレームワークを使われる方のTipsとなれば幸いです。
本題に関するDjango、Flaskの概要と特徴
いずれもWebアプリを作るためのフレームワークで、ざっくりいうと
HTML&CSSにPythonの処理を組み込ませる
フレームワークです。例えば、Djangoの場合以下のような感じの構成になります。
フォルダ構成
※「・・・」としたところには実際にはもっと諸々ファイルを作って粒度を高めます
プロジェクト格納フォルダ(名前は何でも良い)
|
|__Main_App(フォルダ)👈プロジェクト作成段階で作成されます
|
|--Main_App(フォルダ)👈プロジェクト作成段階で雛形が作成されます
| |--__init__.py👈プロジェクト作成段階で雛形が作成されます
| |--asgi.py(ASGIという非同期Webアプリケーションのためのプログラム)👈プロジェクト作成段階で雛形が作成されます
| |--settings.py(プロジェクト全体の設定情報を記述するファイル)👈プロジェクト作成段階で雛形が作成されます
| |--urls.py(プロジェクトで使うURLを全体的に管理するファイル)👈プロジェクト作成段階で雛形が作成されます
| |--wsgi.py(WSGIというWebアプリケーションのプログラム)👈プロジェクト作成段階で雛形が作成されます
|
|--manage.py(プロジェクト実行コマンドなどが記述されている)👈プロジェクト作成段階で雛形が作成されます
|
|--Sub_App1(フォルダ)👈各機能毎に分けて個別に作成(Flaskだと各Blueprint(≒アプリを機能毎に分割する機能)に相当)
| |--__init__.py
| |--forms.py👈テンプレートで使う「フォーム」の大枠の設計を書くファイル
| |--views.py👈MVCモデルでいうところのVとCを兼ねるようなファイル(ファイル名はこれでなくてもよい)
| |--templates(フォルダ)👈この名前じゃないと上手く動かない
| | |
| | Sub_App1(フォルダ)👈ツリー構造を見たときに親(ここだと「Sub_App1」)の名前に合わせる
| | |
| | |--sample1.html
| | |-- ・・・
| | ・・・
| ・・・
|
|--Sub_App2(フォルダ)
| |--views.py
| |--templates(フォルダ)👈この名前じゃないと上手く動かない
| | |
| | Sub_App2(フォルダ)👈ツリー構造を見たときに親(ここだと「Sub_App2」)の名前に合わせる
| | |--sample2.html
| ・・・
・・・
※若干異なりますが、Flaskも似たようなフォルダ構成です。主な参考資料①、②
そして、MVCモデルでいうところのV(View)とC(Controller)のような役割をするviews.pyには以下のような記述をします。
(※renderという関数が、Flaskでは「render_template」他には例えばLaravelだと「view()」に相当する、Djangoにおける所定のHTMLファイルをレンダリング(≒ブラウザ表示)するための関数です。)
(上記のフォルダ構成にある)Sub_App1の
from django.shortcuts import render
from django.http import HttpResponse
from django.views.generic import TemplateView
from Sub_App1.forms import Sampleform
def sample_hello(request):
return HttpResponse('Hello World!')
class Sampleform_CR(TemplateView): 👈Laravelでいうリソースコントローラー的な役割をするクラス
def __init__(self):
self.params = {
'title':'Hello',
'message':'your data:',
'form':BSForm(),
'result':None
}
def get(self, request):
return render(request, 'Sub_App1/sample1.html', self.params)
def post(self, request):
msg = 'あなたは、<b>' + request.POST['name'] + '(' + request.POST['age'] + \
')</b>さんです。<br>メールアドレスは <b>' + request.POST['mail'] + '</b>ですね。'
if ('check' in request.POST):
self.params['result'] = 'Checked!!'
else:
self.params['result'] = 'not checked...'
self.params['form'] = BSForm(request.POST)
self.params['message'] = msg
return render(request, 'Sub_App1/sample1.html', self.params)
なおルーティングを定めるurls.py・Sub_App1の forms.py はこんな感じです。
(これも本題とは直接関係ないため折りたたんであります)
forms.py:フォームのパーツ(inputタグの種類など)の大枠を決めるファイル
urls.py:「Django」の各エンドポイント(≒URL)に対して、実行する処理を記述したファイル
ルーティングを定めるurls.py・Sub_App1の forms.py
Main_Appフォルダの
#どのアドレスにアクセスしたら実行するように、このファイルに追記
"""django_portfolio URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path,include
#pathは、path( アクセスするアドレス, 呼び出す処理 )
urlpatterns = [
path('admin/', admin.site.urls),
path('sub_app1/', include('Sub_App1.urls')),
#includeという関数は、引数に指定したモジュールを読み込む
#これで、Sub_App1内のアドレス割り当ては、すべてSub_App1フォルダ内にあるurls.pyに任せることができる
#/sub_app1/というのがprefixのようになっている状態
#例えば/Sub_App1/index/というエンドポイントを作成するときは、Sub_App1フォルダ内にあるurls.pyに 'index/'というエンドポイントを設定する
path('sub_app2/', include('Sub_App2.urls'))
]
Sub_App1フォルダの
from django.urls import path
from Sub_App1 import views
from Sub_App1.views import Sampleform_CR
urlpatterns = [
path('sample/', views.sample_hello, name='hello'),
path('sample_form/', Sampleform_CR.as_view(), name='sample_form')
]
Sub_App1フォルダの
#formの大枠の内容を定義しておくファイル
from django import forms
#form-controlはBootStrpのform-control
class Sampleform(forms.Form):
name = forms.CharField(label='name', widget=forms.TextInput(attrs={'class':'form-control'}))
mail = forms.CharField(label='mail', widget=forms.TextInput(attrs={'class':'form-control'}))
age = forms.IntegerField(label='age', widget=forms.NumberInput(attrs={'class':'form-control'}))
check = forms.BooleanField(label='Checkbox', required=False)
#required=Trueだと、チェックをOFFにしたまま(未入力扱いになる)では送信ができない
そして、レンダリング(=ブラウザ表示)するhtmlファイル例はこんな感じです。
{% load static %} 👈※CSSやJavascriptの読み込みに必要なDjango特有の記述
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }}</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
type="text/css"
href="{% static 'Sub_App1/css/style.css' %}"
/>
</head>
<body class="container">
<h1 class="display-4 text-primary">{{ title }}</h1>
<form action="{% url 'sample_form' %}" method="post"> 👈urlの引数のsample_formは上記のurls.pyによるルーティング設定時に指定できる「name」の値です。
{% csrf_token %}
<table>
{{ form.as_table }}
<!--forms.pyで生成するフォームのフィールドとラベルのタグを tr タグと td タグでくくって書き出す-->
<tr>
<td></td><td>
<input type="submit" value="click" class="btn btn-primary" />
</td>
</tr>
</table>
</form>
<p class="h5 mt-4">{{ message|safe }}</p>
{% if result is None %}
{% else %}
<p class="h5 mt-4">{{ result|safe }}</p>
{% endif %}
<!-- |safeをつけると、エスケープ処理を行わず、HTMLタグはそのままタグとして書き出されるようになる -->
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"
></script>
</body>
</html>
ざっくり処理の流れを説明すると、
①上記のviews.pyのSampleform_CRの処理を実行するエンドポイントにアクセス
⇒②Sampleform_CRクラスの get 関数により、上記のsample1.htmlが表示される
⇒③フォームにそれぞれ入力して「click」と書かれたボタンを押す
⇒④views.pyの post関数が動いて、paramsに入力した内容が再代入されて、sample1.htmlに入力した内容が反映されて表示される。
(いわゆる簡単な入力フォームです。)
※sample1.htmlに渡された変数 params のうち、result は最初 value が None のため、htmlに埋め込まれたテンプレートタグ{% if result is None %}~{% endif %}により表示されていません
やっと本題
上記のsample1.htmlにおいて、実は以下のようなコメントアウトを書くと以下のようなエラーが起こります。
---前略---
<p class="h5 mt-4">{{ message|safe }}</p>
<!-- {% if result == None %}は非推奨です -->
{% if result is None %}
{% else %}
<p class="h5 mt-4">{{ result|safe }}</p>
{% endif %}
---後略---
理由を見ると「Unclosed tag」とあります。どうも「コメントアウト内の{% if result == None %}もDjangoテンプレートエンジンに読み込まれ、これに対応する {% endif %} がない」とみなされるようです。
実際、{% endif %} をコメントアウトしたものを以下のように付け足すと正常に動きます(何でや(´д`))
---前略---
<p class="h5 mt-4">{{ message|safe }}</p>
<!-- {% if result == None %}は非推奨です -->
<!-- {% endif %} -->
{% if result is None %}
{% else %}
<p class="h5 mt-4">{{ result|safe }}</p>
{% endif %}
---後略---
なお、この現象はFlaskでも起こり、if文以外も同様に大概エラーになります。
<!-- {% for user in users %}でループ処理 -->
{% for user in users %}
<tr>
<td>
<a href="{{ url_for('user_crud.edit_user', user_id=user.id) }}"
>{{ user.id }}</a
>
</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
</tr>
{% endfor %}
※これはFlaskでわざとエラーをおこさせた画面です。なおjinja2はFlaskにおけるテンプレートエンジンで同様の内容が読み取れます。(※画像内の黄土色(猫)はネッコサーフィンというChromeのただ猫に邪魔されるという拡張機能で本Qiitaとは全く関係ありません)
テンプレートエンジンにPythonの制御構文だと認識させているものはどうやら「%で挟まれている」構造らしい
実際、いずれもエラーになるコメントアウトで以下のようにいずれかの「%」をとって記述すると問題なく正常に動きます。
<!-- { for user in users %}でループ処理 -->
{% for user in users %}
<tr>
<td>
<a href="{{ url_for('user_crud.edit_user', user_id=user.id) }}"
>{{ user.id }}</a
>
</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
</tr>
{% endfor %}
Django、Flaskにも、RubyのRuby on Railsなどテンプレートタグを用いる多言語のフレームワークでも同様の現象が起きうるのではないかと考えます。もし、起きるようであればコメント追加していただけると幸いです。
※転職時にスクールで学習したLaravelのディレクティブ(@~)ではこの現象は私が調べた限り起きませんでした。主な参考資料
①【Pythonで多分人気2位のWebアプリケーションフレームワーク】Flaskの基本をわかりやすくまとめる ( @gold-kou in 株式会社ZOZO さん Qiita)
https://qiita.com/gold-kou/items/00e265aadc2112b0f56a#%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E6%A7%8B%E6%88%90
②【Flask】中規模な開発のディレクトリ構成を考える ( @Koichi73 さん Qiita)
https://qiita.com/Koichi73/items/9d73f062f0ad56d6f953
③図解Django超入門ハンズオン ( @myasuda220780 in 株式会社スカイウイル さん Qiita)
https://qiita.com/myasuda220780/items/c0c80742e62939a6eede
④『Python Django 4 超入門』掌田津耶乃 (著)秀和システム
⑤『Python FlaskによるWebアプリ開発入門 物体検知アプリ&機械学習APIの作り方』佐藤 昌基・平田 哲也 (著)、寺田 学 (監修) 翔泳社