Ruby on the RailsでAjaxを実装して動的なサイトを作成していく場合、partialというhtmlファイル内のブロック要素を部品化して、そのまま非同期処理する便利な方法がありますが、それと同じことをFlaskでもできないかと探ってみたところ、
実は簡単にできてしまった(その割に情報がなかった)ので、その備忘録と方法を置いておきます。
また、FlaskでできるならDjangoでもできるんじゃないかと探ってみたので、それも記録しています。
§Flask編
テンプレートファイルとしてJinja2を使用しています。
手順
結論から言えば、やっていることはRailsと同じで、イベントをjQueryで制御してから、partial(部品化)したテンプレートファイルをレスポンスさせるというものです。
レイアウトを作る
まず、ページのレイアウト部分です。ここに制御用のスクリプトを置きます(どこに置いてもいいですし、jQuery以外の方法(FetchAPI、axiosなど)でも問題ありません。Ajaxがうまくいくと変数resにpartial(部品)が返ってくるようにします。
<head>
<title>{{ title }}</title>
<script
src="https://code.jquery.com/jquery-3.6.0.js"
integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk="
crossorigin="anonymous">
</script>
<script>
$(function(){
$("#city_no").on("change",function(){
let city_no = $(this).val();
$.ajax({
type: 'POST',
url: '/_result', //問い合わせ先
data: {"city_no":city_no}, //選択された都市番号
}).done(function(res){
$('#result').html(res)
}).fail(function(){
console.log("NG")
})
})
})
</script>
</head>
<body>
{% block content %}
<!-- ここにメインコンテンツを書く -->
{% endblock %}
</body>
</html>
ビュー部分
これが選択フォーム部分となります。ここのフォームにactionプロパティに値を設定すると、通常ページのように画面遷移します。今回はこのページを遷移せずに、プルダウンの値を選択することで、#resultへpartial(部品)化したテンプレートを埋め込みます。
{% extends "layout.html" %}
{% block content %}
<!-- ここにメインコンテンツを書く -->
<content class="box">
<article class="box_left">
<form method="post">
<select id="city_no" name="city_no">
<option value=""> --対象の都市を選択-- </option>
{% for option in options %}
<option value="{{option.city_no}}">{{ option.city_nm }}</option>
{% endfor %}
</select>
</form>
<article id="result" class="box_right"></article>
</content>
{% endblock %}
コントローラ部分
さて、一番肝心な部分がコントローラ部分です。sportsというデータベース内にあるmajorというテーブルに問い合わせ、プルダウンメニューを生成したり、照会結果をAjaxに返すようにします。
※SQLAlchemyは使っていませんが、使用したものでも問題なく動かせます。
#Mysqlに接続
from flask import Flask, request, render_template, jsonify
import pymysql.cursors
app = Flask(__name__)
@app.route('/')
def index():
db = Db() #インスタンス作成
db.con() #db接続
sql = 'select city_no,city_nm from major'
rows = db.query(sql) #クエリ取得
return render_template('index.html',options=rows)
class Db:
rows = ''
def con(self):
conn = pymysql.connect(
user='hogehoge',
passwd='fugafuga',
host='127.0.0.1',
db='sports',
charset='utf8',
cursorclass=pymysql.cursors.DictCursor #json化処理
)
c = conn.cursor()
self.c = c
def query(self,sql):
c = self.c
c.execute(sql) #sql実行
rows = c.fetchall()
#self.rows = rows
return rows
#レンダリング(methods=['POST']とすることで、Ajaxへレスポンスを返す)
@app.route('/_result',methods=["POST"])
def result():
city_no = request.form['city_no']
rows = []
db = Db() #インスタンス作成
db.con() #db接続
sql = f'select city_nm,mlb,nba,nfl,nhl,mls from major where city_no = {city_no}'
rows = db.query(sql) #クエリ取得
return render_template('result.html',rows=rows)
if __name__ == "__main__":
app.run(debug=False, host= '0.0.0.0', port=5000)
ですが、この制御を見てもらえばわかると思いますが、Ajaxだからといって別段特別な記述をしているわけではなく、render_templateはこのように、methodsを指定さえしてやれば、Ajaxにレスポンスを返せるみたいです(Pythonに精通した人なら常識なのかも知れませんが…)
パーシャル部分
パーシャル(部品)部分の制御となります。これも特別なことをしているわけではないですが、違いはブロック化せず、素のままブロック要素を記述している点です。
<table border= "1" cellspacing="0">
<tr>
<th>都市名</th>
<th>MLB</th>
<th>NBA</th>
<th>NFL</th>
<th>NHL</th>
<th>MLS</th>
</tr>
<tbody>
{% for row in rows %}
<tr>
<td>{{ row.city_nm }}</td>
<td>{{ row.mlb }}</td>
<td>{{ row.nba }}</td>
<td>{{ row.nfl }}</td>
<td>{{ row.nhl }}</td>
<td>{{ row.mls }}</td>
</tr>
{% endfor %}
</tbody>
</table>
これで準備完了です。このように選択されたプルダウンの値に連動して、テーブル表示も変わります(ステータスもxhrとなっています)。
htmlをそのまま引っ張ってきているのはセキュリティ上問題があるのではと思い、対策を調べてみたところ、公式マニュアルによるとjinjaテンプレートはhtml、xmlなど4つの拡張子に限り自動でサニタイズ処理をしてくれるautoescapeという機能が実装されているそうなので、これで問題はないようです。
§Django編
Djangoでも同様にテンプレートをpartial(部品)として扱い、Ajax制御ができるようです。ただ、Flaskと違い、自動サニタイズ処理がされないのでCSRF対策が必須となります。
※使用バージョンはDjango3、またアプリケーション名はtestという名前でテストしています。
ルート制御
Djangoの場合はAjaxでレスポンスするpartialに対しても、ルート設定が必要です。これを忘れるとレスポンスを受け取れません。
from django.conf.urls import url
from test import views
urlpatterns = [
url(r'^members/$', views.index, name='index'),
url(r'^members/result$', views.result, name='result'), #Ajaxでレスポンスするpartial
]
レイアウト部分
base.htmlがレイアウト部分となり、ここに制御用のAjaxを記述しておきます。また、
'csrfmiddlewaretoken': '{{ csrf_token }}'
この記述が肝となり、これを忘れると403エラーが発生し、データを転送できません。
{% load static %}
<!DOCTYPE html>
<html lang="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>{% block title %}My books{% endblock %}</title>
<!-- Bootstrap -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
{% block content %}
{{ content }}
{% endblock %}
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script
src="https://code.jquery.com/jquery-3.6.0.js"
integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk="
crossorigin="anonymous">
</script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
$(function(){
$("#city_no").on("change",function(){
let city_no = $(this).val();
$.ajax({
type: 'POST',
url: 'result', //問い合わせ先
data: {
"city_no":city_no,
'csrfmiddlewaretoken': '{{ csrf_token }}',
}, //選択された都市番号
}).done(function(res){
$('#result').html(res)
}).fail(function(){
console.log("NG")
})
})
})
</script>
<!-- jsを書く場所 -->
{% block script %}
{% endblock %}
</body>
</html>
テンプレート部分
base.htmlから呼び出すindex.html部分です。ここがメインの制御画面となり、id名resultにpartial(部品)を返すようにします。
{% extends "base.html" %}
{% block content %}
<!-- ここにメインコンテンツを書く -->
<content class="box">
<article class="box_left">
<form method="post">
{% csrf_token %}
<select id="city_no" name="city_no">
<option value=""> --対象の都市を選択-- </option>
{% for member in members %}
<option value="{{member.city_no}}">{{ member.city_nm }}</option>
{% endfor %}
</select>
</form>
<article id="result" class="box_right"><!-- ここに値を返す --></article>
</content>
{% endblock %}
ビュー(制御メソッド)部分
DjangoはMTVなのでコントローラとは言わないようですが、制御はビューにメソッド化して記述します。また、転送はrender_template関数の代わりにrender関数を使い、リンク先をAjax用のpartialと同じにします。
from django.shortcuts import render
from django.http import HttpResponse
from test.models import Major
from icecream import ic #デバッグ用
# プルダウン制御
def index(request):
members = Major.objects.all().order_by('city_no')
#ic(members)
return render(request,'members/index.html',{'members':members})
# Ajax用のpartial
def result(request):
if request.method == 'POST':
city_no = request.POST['city_no']
# ic(city_no) デバッグ用
row = Major.objects.get(city_no=city_no)
return render(request,'members/result.html',{'row':row})
partial(部品)部分
ここの全部品がAjaxとして返されます。
<table border= "1" cellspacing="0">
<tr>
<th>都市名</th>
<th>MLB</th>
<th>NBA</th>
<th>NFL</th>
<th>NHL</th>
<th>MLS</th>
</tr>
<tbody>
<tr>
<td>{{ row.city_nm }}</td>
<td>{{ row.mlb }}</td>
<td>{{ row.nba }}</td>
<td>{{ row.nfl }}</td>
<td>{{ row.nhl }}</td>
<td>{{ row.mls }}</td>
</tr>
</tbody>
</table>
DBのマイグレーションとモデルについて
DjangoはDBテーブルのマイグレーションが簡単にできるのですが、マイグレーション制御が正しくないとDBテーブル呼び出しにエラーが発生することがあります。
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Major',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('city_no', models.CharField(max_length=3)),
('city_nm', models.CharField(max_length=100)),
('mlb', models.CharField(max_length=100)),
('nba', models.CharField(max_length=100)),
('nfl', models.CharField(max_length=100)),
('nhl', models.CharField(max_length=100)),
('mls', models.CharField(max_length=100)),
],
options={
'db_table': 'major',
'managed': False,
},
),
]
from django.db import models
# Register your models here
class Major(models.Model):
city_no = models.CharField(max_length=3)
city_nm = models.CharField(max_length=100)
mlb = models.CharField(max_length=100)
nba = models.CharField(max_length=100)
nfl = models.CharField(max_length=100)
nhl = models.CharField(max_length=100)
mls = models.CharField(max_length=100)
class Meta:
managed = False
db_table = 'major'
これでDjangoでもFlaskと同じような制御が行われます。