はじめに
私は競馬予想プログラムソフトの制作過程を動画で投稿している者です。
ここでは、モデル分析用のWEBアプリの開発手順を話していきます。
現在作成している競馬予想プログラムソフトの概要は以下を参照ください。
本プログラムの前提
本プログラムでは以下の前提を置いています。
- Windows 10
- Python3.10.5
- Django ver5.0.4
- 競馬予想プログラムで作成したモデルを分析する目的で使います。
- Bookersで公開中のモデル分析管理クラスと連携してモデル分析を行います。
とりわけ、最後の2つに関しては有料記事でソースを公開しているため、まったく同じ環境で競馬予想プログラムの作成とモデルの分析を行いたい場合は以下のBookers記事一覧から記事を購入ください。
また、競馬予想プログラムの制作過程については大まかな概要は以下の再生リストから参照ください。
詳細な解説は以下の記事一覧を参照ください。
モデル分析画面:モデル選択機能と分析画面ページ作成
今回からはモデル分析画面の開発に入っていきます。モデル分析画面では、選択したモデルに対して各種分析画面で分析結果を確認できる機能を目指します。
分析結果では、ベースラインとして指定したモデルと選択したモデルとの比較結果を確認できるようにします。
まずは、今回のパートではモデルを選択する機能と各種分析画面の遷移先ページの作成を行います。
今回の完成イメージを以下のGIFで見せます。
「モデル選択」ボタンを押すとサイドバーが出現して「Target Models」と「Base Model」を選択できるようになっています。
「Target Models」では分析対象のモデルを指しており、複数モデル選択できるようにしています。
「Base Model」ではベースラインとしてTarget Modelsと比較対象となるモデルを1個選択できるようにしています。だたし、必ず選択しなくてもよく「Target Models」だけ選択しても問題ないようにしています。
モデル選択後「Start Analyze」を押下すると「分析モード」のボタンが活性状態になります。
「分析モード」のボタンを押下すると、再度サイドバーが出現し各種分析画面のリンク先アコーディオンが選択できるようになっています。
上記のようなモデル分析画面を今回作っていきます。
フォルダ構成
今回の実装でもフォルダ構成をかなり変えています。
前回記事のフォルダ構成をベースに変更箇所を赤字にしています。注意して変更してください。
<any-dir>
┣ <app_keiba>
┃ ┣ <app_keiba>
┃ ┃ ┣ __init__.py
┃ ┃ ┣ settings.py
┃ ┃ ┣ urls.py
┃ ┃ ┣ asgi.py
┃ ┃ ┗ wsgi.py
┃ ┣ <model_analyzer>
┃ ┃ ┣ <migrations>
┃ ┃ ┃ ┗ __init__.py
┃ ┃ ┣ <tools>
┃ ┃ ┃ ┣ <form_control>
┃ ┃ ┃ ┃ ┗ model_manage_forms.py
┃ ┃ ┃ ┗ <model_control>
┃ ┃ ┃ ┣ model_analyze_models.py
┃ ┃ ┃ ┗ model_manage_models.py
┃ ┃ ┣ <main_viewer>
┃ ┃ ┃ ┣ <analyze_viewr> ※モデル分析画面用のViewをここで管理
┃ ┃ ┃ ┃ ┣ <base_analyze_page> ※基礎分析用のView関数
┃ ┃ ┃ ┃ ┃ ┗ views.py
┃ ┃ ┃ ┃ ┣ <model_info_list_page> ※モデル情報用のView関数
┃ ┃ ┃ ┃ ┃ ┗ views.py
┃ ┃ ┃ ┃ ┗ <ogs_page> ※オッズグラフ用のView関数
┃ ┃ ┃ ┃ ┗ views.py
┃ ┃ ┃ ┗ views.py ※ ファイルの位置が変わっているので注意
┃ ┃ ┣ __init__.py
┃ ┃ ┣ admin.py
┃ ┃ ┣ apps.py
┃ ┃ ┣ models.py
┃ ┃ ┣ test.py
┃ ┃ ┣ urls.py ※各種分析画面へのURLを登録
┃ ┃ ┗ views.py
┃ ┣ <static>
┃ ┃ ┣ <css>
┃ ┃ ┃ ┣ <model_analyze> ※ モデル分析画面用のCSS
┃ ┃ ┃ ┃ ┗ style.css
┃ ┃ ┃ ┣ <model_manage>
┃ ┃ ┃ ┃ ┗ style.css
┃ ┃ ┃ ┗ base.css
┃ ┃ ┣ <images>
┃ ┃ ┃ ┣ favicon.png
┃ ┃ ┃ ┗ file-bar-graph-fill.svg
┃ ┃ ┗ < js >
┃ ┃ ┣ <base_layout>
┃ ┃ ┃ ┗ fade_popup.js
┃ ┃ ┣ <model_analyze> ※ モデル分析画面用のJavaScript
┃ ┃ ┃ ┣ check_submit_mode.js
┃ ┃ ┃ ┣ sidebar_slide.js
┃ ┃ ┃ ┗ validate_form.js
┃ ┃ ┗ <model_manage>
┃ ┃ ┗ update_form_initial.js
┃ ┣ <templates>
┃ ┃ ┣ <model_analyze> ※ モデル分析画面用のHTML管理フォルダ
┃ ┃ ┃ ┣ <analyze_pages> ※ 各種分析画面のHTML管理フォルダ
┃ ┃ ┃ ┃ ┣ base_analyze_page.html
┃ ┃ ┃ ┃ ┣ model_info_list_page.html
┃ ┃ ┃ ┃ ┗ ogs_page.html
┃ ┃ ┃ ┣ <parts> モデル分析画面で用いるツールのHTML
┃ ┃ ┃ ┃ ┣ <accordion_items> ※ アコーディオンパーツ
┃ ┃ ┃ ┃ ┃ ┣ base_analyze.html
┃ ┃ ┃ ┃ ┃ ┣ display_model_info.html
┃ ┃ ┃ ┃ ┃ ┗ display_OGS.html
┃ ┃ ┃ ┃ ┣ offcanvas_analyze_list.html
┃ ┃ ┃ ┃ ┗ select_models.html
┃ ┃ ┃ ┗ model_manage.html ※ 場所が変わっているので注意
┃ ┃ ┣ <model_manage>
┃ ┃ ┃ ┣ <modal>
┃ ┃ ┃ ┃ ┣ modelDeleteModal.html
┃ ┃ ┃ ┃ ┣ modelImportModal.html
┃ ┃ ┃ ┃ ┗ modelInfoModal.html
┃ ┃ ┃ ┗ model_manage.html
┃ ┃ ┣ base_layout.html ※ jQueryの競合を懸念して一部修正
┃ ┃ ┣ top_page.html
┃ ┃ ┗ model_analyze.html
┃ ┗ manage.py
┗ <src>
モデル選択機能と分析画面ページ作成の開発手順(Django側)
それでは、実際に変更されたソースを一つずつ見せていきます。基本はこの手順通りにソースを変更して頂ければ、完成するはずです。
今回は変更および新規作成されたファイルが非常に多いので注意ください。
(新規) model_analyze_models.pyの作成
モデル分析画面用にDBへ登録されているモデル情報を取得する関数が入っています。新規作成なのでソース丸写しでOKです。
from model_analyzer.models import ModelList
from django.db.models import Max, Min, Count, Q
def get_model_latest_list(model_list: list = None):
# 各model_idについて最新のものを取得
if model_list:
query = ModelList.objects.filter(model_id__in=model_list).values(
"model_id"
).annotate(
import_date=Min("regist_date"),
latest_id=Max("id"),
sort_id=Min("id"),
version=Count("model_id")
)
else:
query = ModelList.objects.values(
"model_id"
).annotate(
import_date=Min("regist_date"),
latest_id=Max("id"),
sort_id=Min("id"),
version=Count("model_id")
)
return [q["latest_id"] for q in query]
def get_import_models(model_list: list = None):
target_models_id = get_model_latest_list(model_list)
return ModelList.objects.filter(id__in=target_models_id)
def get_select_models(nav_params: dict, model_list: list, base_model: str = "") -> dict:
models = get_import_models(model_list)
nav_params["model_query"] = models
if base_model:
model = ModelList.objects.filter(
model_id=base_model).latest("regist_date")
nav_params["base_model"] = model
else:
nav_params["base_model"] = None
return nav_params
(更新) views.pyの編集
もともと「model_analyzer/views.py」にあったviewsファイルを「model_analyzer/main_viewer/views.py」へ移動させています。
新規インポートとModelAnalyzeViewクラスを編集しています。
# 下記3行のインポート文を追加
import urllib
import urllib.parse
from model_analyzer.tools.model_control.model_analyze_models import get_import_models, get_select_models
(省略)
# 以下のモデル分析画面用のViewクラスを編集
class ModelAnalyzeView(View):
nav_params = {
"model_analyze": False,
"model_manage": True
}
def start(self, request: WSGIRequest):
self.nav_params["model_table"] = get_import_models()
self.nav_params["model_num"] = len(self.nav_params["model_table"])
self.nav_params["analyze_list_disable"] = True
if request.GET.get("tm"):
self.nav_params["analyze_list_disable"] = False
self.nav_params = get_select_models(
self.nav_params,
request.GET.getlist("tm"),
request.GET.get("bm")
)
def start_post(self, request: WSGIRequest):
self.start(request)
self.nav_params["model_table"] = get_import_models()
self.nav_params["model_num"] = len(self.nav_params["model_table"])
self.nav_params = get_select_models(
self.nav_params,
request.POST.getlist("target-model"),
request.POST.get("base-model")
)
self.nav_params["analyze_list_disable"] = False
return self.redirect_analyze(request)
def redirect_analyze(self, request: WSGIRequest):
params = "&".join([f"tm={d}" for d in request.POST.getlist(
"target-model")])
params += f"&bm={request.POST.get('base-model')}" if request.POST.get(
'base-model') else ""
for keymode in ["base-analyze-mode", "model-info-list-mode", "ogs-mode"]:
analyze_mode = request.POST.get(keymode)
if analyze_mode is not None and analyze_mode != "":
return f"/model-analyze/{analyze_mode}?{params}"
return f"/model-analyze?{params}"
def get(self, request: WSGIRequest):
self.start(request)
return render(request, 'model_analyze/model_analyze.html', self.nav_params)
def post(self, request: WSGIRequest):
urls = self.start_post(request)
if urls:
return redirect(urls)
return render(request, 'model_analyze/model_analyze.html', self.nav_params)
(省略)
(新規) 各種分析画面用のViewクラス作成
下記3件はクラス名が違うだけでクラスの内容は全く同じなので、まとめてコード見せます。
from django.shortcuts import redirect, render
from django.views import View
from django.core.handlers.wsgi import WSGIRequest
from model_analyzer.main_viewer.views import ModelAnalyzeView
from model_analyzer.tools.model_control.model_analyze_models import get_select_models
from model_analyzer.models import ModelList
from model_analyzer.tools.model_control.model_manage_models import get_table_list
from model_analyzer.tools.form_control.model_manage_forms import generate_model_import_forms, validation_model_import_forms
class BaseAnalyzeView(ModelAnalyzeView):
def get(self, request: WSGIRequest):
self.start(request)
return render(request, 'model_analyze/analyze_pages/base_analyze_page.html', self.nav_params)
def post(self, request: WSGIRequest):
urls = self.start_post(request)
if urls:
return redirect(urls)
return render(request, 'model_analyze/analyze_pages/base_analyze_page.html', self.nav_params)
from django.shortcuts import redirect, render
from django.views import View
from django.core.handlers.wsgi import WSGIRequest
from model_analyzer.main_viewer.views import ModelAnalyzeView
from model_analyzer.tools.model_control.model_analyze_models import get_select_models
from model_analyzer.models import ModelList
from model_analyzer.tools.model_control.model_manage_models import get_table_list
from model_analyzer.tools.form_control.model_manage_forms import generate_model_import_forms, validation_model_import_forms
class ModelInfoListView(ModelAnalyzeView):
def get(self, request: WSGIRequest):
self.start(request)
return render(request, 'model_analyze/analyze_pages/model_info_list_page.html', self.nav_params)
def post(self, request: WSGIRequest):
urls = self.start_post(request)
if urls:
return redirect(urls)
return render(request, 'model_analyze/analyze_pages/model_info_list_page.html', self.nav_params)
from django.shortcuts import redirect, render
from django.views import View
from django.core.handlers.wsgi import WSGIRequest
from model_analyzer.main_viewer.views import ModelAnalyzeView
from model_analyzer.tools.model_control.model_analyze_models import get_select_models
from model_analyzer.models import ModelList
from model_analyzer.tools.model_control.model_manage_models import get_table_list
from model_analyzer.tools.form_control.model_manage_forms import generate_model_import_forms, validation_model_import_forms
class OGSView(ModelAnalyzeView):
def get(self, request: WSGIRequest):
self.start(request)
return render(request, 'model_analyze/analyze_pages/ogs_page.html', self.nav_params)
def post(self, request: WSGIRequest):
urls = self.start_post(request)
if urls:
return redirect(urls)
return render(request, 'model_analyze/analyze_pages/ogs_page.html', self.nav_params)
(更新) urls.pyの編集
今回新しく各種分析画面を作成しますので、表示用のURLのルートを登録しておく必要があります。
以下のファイルを編集してください。
# 以下4行のインポート文を追加
from .main_viewer import views
from model_analyzer.main_viewer.analyze_viewer.base_analyze_page.views import BaseAnalyzeView
from model_analyzer.main_viewer.analyze_viewer.model_info_list_page.views import ModelInfoListView
from model_analyzer.main_viewer.analyze_viewer.ogs_page.views import OGSView
# 以下のインポート文は削除
from . import views
(省略)
path('model-manage/delete/<int:number>',
views.modelDeleteView, name='model-manage-delete'),
# こっから下3行を追加
path('model-analyze/base-analyze',
BaseAnalyzeView.as_view(), name='model-analyze-base-analyze'),
path('model-analyze/models-info-list',
ModelInfoListView.as_view(), name='model-analyze-models-info-list'),
path('model-analyze/ogs',
OGSView.as_view(), name='model-analyze-ogs'),
]
モデル選択機能と分析画面ページ作成の開発手順(WEB画面側)
以下のimageファイル、CSSファイル、JavaScriptファイル、HTMLファイルを新規作成・編集をしてください。
イメージファイルの追加
今回「分析モード」ボタンを用意するために、分析っぽいアイコンが欲しいと思い新しくアイコン用のイメージファイルを追加しています。
同じように表示したい人は、以下のリンクからダウンロードして「file-bar-graph-fill.svg」というファイル名のSVGファイルを「static/images」フォルダ配下に置いてください。
File bar graph fill · Bootstrap Icons (getbootstrap.com)
CSSファイル
#select-models {
position: fixed;
height: auto;
min-width: 70%;
max-width: 70%;
left: 0%;
z-index: 10;
margin-top: -38px;
}
#select-models.SlideNormal {
transform: translateX(-101%);
}
#select-models.SlideOut {
animation: SlideOut 0.25s forwards;
}
@keyframes SlideOut {
0% {
transform: translateX(-1px);
}
100% {
transform: translateX(-101%);
}
}
#select-models.SlideIN {
animation: SlideIN 0.25s forwards;
}
@keyframes SlideIN {
0% {
transform: translateX(-101%);
}
100% {
transform: translateX(-1px);
}
}
.in-shadow {
box-shadow: inset 0 0 3px rgb(120, 120, 120, .75);
}
JavaScriptファイル
document.addEventListener('DOMContentLoaded', () => {
const baseAnalyzeBtn = document.querySelector('.base-analyze-btn');
const modelInfoListBtn = document.querySelector('.model-info-list-btn');
const ogsBtn = document.querySelector('.ogs-btn');
baseAnalyzeBtn.addEventListener("click", (e) => {
const modelInfoListMode = document.querySelector(".model-info-list-mode");
const ogsMode = document.querySelector(".ogs-mode");
modelInfoListMode.value = null;
ogsMode.value = null;
});
modelInfoListBtn.addEventListener("click", (e) => {
const modelAnalyzeMode = document.querySelector(".base-analyze-mode");
const ogsMode = document.querySelector(".ogs-mode");
modelAnalyzeMode.value = null;
ogsMode.value = null;
});
ogsBtn.addEventListener("click", (e) => {
const modelAnalyzeMode = document.querySelector(".base-analyze-mode");
const modelInfoListMode = document.querySelector(".model-info-list-mode");
modelInfoListMode.value = null;
modelAnalyzeMode.value = null;
});
});
$('#select-model-btn').on('click', function () {
SlideSelectModel()
});
const SlideSelectModel = () => {
if ($('#select-models').hasClass("SlideNormal")) {
$('#select-models').removeClass('SlideNormal');
$('#select-models').addClass('SlideIN');
}
else if ($('#select-models').hasClass("SlideOut")) {
$('#select-models').removeClass('SlideOut');
$('#select-models').addClass('SlideIN');
} else {
$('#select-models').removeClass('SlideIN');
$('#select-models').addClass('SlideOut');
}
}
document.addEventListener('DOMContentLoaded', () => {
const validForm = document.querySelector('.select-models-form');
const errorClassName = 'error';
const requiredElements = document.querySelectorAll('.required');
const anyCheckedElements = document.querySelectorAll('.anychecked');
const raiseError = (elem, errorMessage) => {
const errorSpan = document.createElement('span');
errorSpan.classList.add(errorClassName);
errorSpan.setAttribute('aria-live', 'polite');
errorSpan.style.color = "red"
errorSpan.textContent = errorMessage;
elem.parentNode.appendChild(errorSpan);
}
validForm.addEventListener('submit', (e) => {
const errorElems = validForm.querySelectorAll('.' + errorClassName);
errorElems.forEach((elem) => {
elem.remove();
});
requiredElements.forEach((elem) => {
const elemValue = elem.value.trim();
if (elemValue.length === 0) {
raiseError(elem, '*入力は必須です');
e.preventDefault();
}
});
let flag = true;
for (let data of anyCheckedElements) {
if (data.checked) {
flag = false;
}
}
if (flag) {
raiseError(document.querySelector(".anychecked-box"), '*いずれかを選択してください。');
e.preventDefault();
} else {
const analyzeListBtn = document.getElementById("analyze-list-btn");
analyzeListBtn.disabled = false;
}
});
});
HTMLファイル
{% extends "base_layout.html" %}
{% block cssblock %}
<link rel="stylesheet" href="/static/css/model_analyze/style.css">
{% block ext-cssblock %}
{% endblock %}
{% endblock %}
{% block content %}
<div class="d-flex flex-column w-100">
<div class="col">
{% include 'model_analyze/parts/select_models.html' %}
{% include 'model_analyze/parts/offcanvas_analyze_list.html' %}
</div>
{% block analyze-content %}
{% endblock %}
</div>
{% endblock %}
{% block jsblock %}
<script src="/static/js/model_analyze/sidebar_slide.js"></script>
<script src="/static/js/model_analyze/validate_form.js"></script>
<script src="/static/js/model_analyze/check_submit_mode.js"></script>
{% block ext-jsblock %}
{% endblock %}
{% endblock %}
{% extends "model_analyze/model_analyze.html" %}
{% block analyze-content %}
<div class="col border border-dark-subtle rounded p-1 mt-3"></div>
{% endblock %}
{% extends "model_analyze/model_analyze.html" %}
{% block analyze-content %}
<div class="col border border-dark-subtle rounded p-1 mt-3"></div>
{% endblock %}
{% extends "model_analyze/model_analyze.html" %}
{% block analyze-content %}
<div class="col border border-dark-subtle rounded p-1 mt-3"></div>
{% endblock %}
<!-- 分析一覧オフキャンバス -->
<div class="offcanvas offcanvas-start bg-body-secondary" tabindex="-1" id="offcanvasAnalyzeList"
aria-labelledby="offcanvasAnalyzeListLabel">
<div class="offcanvas-header bg-dark shadow">
<div class="d-flex container-fluid justify-content-center">
<h3 class="offcanvas-title text-white" id="offcanvasAnalyzeListLabel">分析一覧</h3>
</div>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<form method="post" class="offcanvas-body mr-2">
<div class="container-fluid d-frex rounded border border-dark-subtle shadow-lg bg-light pb-3 pt-2">
{% csrf_token %}
<div class=" dropend">
<button class="btn dropdown-toggle fw-bold fs-3 border border-0" type="button" data-bs-toggle="dropdown"
style="transform: translateX(-10px);">
選択中モデル
</button>
<ul class="dropdown-menu p-2">
<li class="fw-bold">Target Models</li>
{% for query in model_query %}
<li>
<span class="ml-2" tabindex="0" data-bs-toggle="tooltip" title="modelID: {{query.model_id}}">
<input name="target-model" value="{{query.model_id}}" type="hidden"></input>
{{query.model_name}}
</span>
</li>
{% endfor %}
{% if base_model %}
<li>
<hr class="dropdown-divider">
</li>
<li class="fw-bold">Base Model</li>
<li>
<span class="ml-2" tabindex="0" data-bs-toggle="tooltip" title="modelID: {{base_model.model_id}}">
<input name="base-model" value="{{base_model.model_id}}" type="hidden"></input>
{{ base_model.model_name }}
</span>
</li>
{% endif %}
</ul>
</div>
<div class="mt-3"></div>
<div class="fs-3 fw-bold">分析項目</div>
<div class="m-2"></div>
<!-- 分析一覧のアコーディオン -->
<div class="accordion in-shadow p-2 border border-dark rounded" id=" accordionAnalyzeList">
<div class="inset border border-0">
{% include 'model_analyze/parts/accordion_items/display_model_info.html' %}
{% include 'model_analyze/parts/accordion_items/base_analyze.html' %}
{% include 'model_analyze/parts/accordion_items/display_OGS.html' %}
</div>
</div>
</div>
</form>
</div>
<div class="btn-group-vertical text-nowrap clearfix" role="group" id="model-analyze-btn-group">
<button class="btn btn-outline-primary select-btn" id="select-model-btn">モデル選択</button>
<button type="button" id="analyze-list-btn" {% if analyze_list_disable %} class="btn btn-outline-secondary" disabled
{% else %} class="btn btn-info" {% endif %} data-bs-toggle="offcanvas" data-bs-target="#offcanvasAnalyzeList"
aria-controls="offcanvasAnalyzeList">
<img src="/static/images/file-bar-graph-fill.svg">
分析モード
</button>
</div>
<!-- モデル選択画面 -->
<div class="SlideNormal border border-black bg-primary-subtle shadow p-3 rounded-end text-nowrap" id="select-models">
<div class="d-flex container justify-content-center">
<form class="select-models-form row" action="" method="post" novalidate>
{% csrf_token %}
<div class="col-auto">
<div class="row-nowrap">Target Models (複数選択可)</div>
<div class="row-nowrap">
<div class="container overflow-auto bg-light rounded h-100 p-2 border border-dark-subtle anychecked-box">
{% for model in model_table %}
<div class="form-check form-switch">
<span tabindex="0" data-bs-toggle="tooltip" title="model ID: {{model.model_id}}">
<input class="form-check-input border border-dark required anychecked" type="checkbox"
id="flexSwitchCheckModels" name="target-model" value="{{model.model_id}}">
<label class="form-check-label" for="flexSwitchCheckModels">
{{model.model_name}}
</label>
</span>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="col-auto">
<div class="row-nowrap">Base Model</div>
<div class="row-nowrap">
<select class="form-select border border-dark-subtle" aria-label="single select" name="base-model">
<option value=""></option>
{% for model in model_table %}
<option value="{{model.model_id}}" tabindex="0" data-bs-toggle="tooltip"
title="model ID: {{model.model_id}}">{{model.model_name}}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-auto">
<div class="row-nowrap pt-4">
<button type="submit" class="btn btn-primary mb-3">Start Analyze</button>
</div>
</div>
</form>
</div>
</div>
<div class="accordion-item border border-0">
<h2 class="accordion-header">
<button class="accordion-button collapsed fw-bold fs-4" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
基礎分析
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#accordionAnalyzeList">
<div class="accordion-body">
<p>ここではモデルの基礎分析結果を確認します。分析内容は以下の項目です。</p>
<ul>
<li>モデルの収支グラフ</li>
<li>モデルの回収率と的中率</li>
<li>モデルの人気別ベット回数</li>
</ul>
</div>
<div class="accordion-footer">
<div class="container-fluid d-flex justify-content-end mb-2">
<input type="hidden" value="base-analyze" name="base-analyze-mode" class="base-analyze-mode">
<button type="submit" class="btn btn-primary base-analyze-btn">表示</a>
</div>
</div>
</div>
</div>
<div class="accordion-item border border-0">
<h2 class="accordion-header">
<button class="accordion-button collapsed fw-bold fs-4" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
モデル情報一覧確認
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#accordionAnalyzeList">
<div class="accordion-body">
ここでは、選択中のモデルのモデル情報を一覧化して参照することができます。
</div>
<div class="accordion-footer">
<div class="container-fluid d-flex justify-content-end mb-2">
<input type="hidden" value="models-info-list" name="model-info-list-mode" class="model-info-list-mode">
<button type="submit" class="btn btn-primary model-info-list-btn">表示</a>
</div>
</div>
</div>
</div>
<div class="accordion-item border border-0">
<h2 class="accordion-header">
<button class="accordion-button collapsed fw-bold fs-4" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
オッズグラフスコアの確認
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#accordionAnalyzeList">
<div class="accordion-body">
ここでは、選択中のモデルに対するオッズグラフスコアを表示します。ベースモデルが選択されていれば、ベースモデルとのAonBオッズグラフスコアも算出します。
</div>
<div class="accordion-footer">
<div class="container-fluid d-flex justify-content-end mb-2">
<input type="hidden" value="ogs" name="ogs-mode" class="ogs-mode">
<button type="submit" class="btn btn-primary ogs-btn">表示</a>
</div>
</div>
</div>
</div>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="icon" type="image/x-icon" href="/static/images/favicon.png">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
{% block cssblock %}
{% endblock %}
<title>競馬予想ソフト</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<header>
<nav class="navbar navbar-expand bg-body-secondary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="/index.html">um-AI</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar"
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/index.html">Home</a>
</li>
<li class="nav-item">
{% if model_analyze %}
<a class="nav-link active" href="/model-analyze">モデル分析</a>
{% else %}
<a class="nav-link disabled" href="/model-analyze" tabindex="-1" aria-disabled="true">モデル分析</a>
{% endif %}
</li>
<li class="nav-item">
{% if model_manage %}
<a class="nav-link active" href="/model-manage">モデル管理</a>
{% else %}
<a class="nav-link disabled" href="/model-manage" tabindex="-1" aria-disabled="true">モデル管理</a>
{% endif %}
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="mt-1"></div>
{% if messages %}
<div class="container-fluid">
<div class="notification is-info">
{% for message in messages %}
{% if message.tags != 'error' %}
<div {% if message.tags %}class="alert alert-{{ message.tags }}" {% endif %} id="success-alert"
style="font-size: small;">
{% else %}
<div {% if message.tags %}class="alert alert-danger" {% endif %} id="success-alert" style="font-size: small;">
{% endif %}
{{ message }}
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<main class="d-flex container-fluid h-100">
{% block content %}
<!-- ここに個別のhtmlが入る -->
{% endblock %}
</main>
{% block jsblock %}
{% endblock %}
<script src="/static/js/base_layout/fade_popup.js"></script>
</body>
</html>
サーバの起動と画面の確認
それでは、モデル選択機能と分析画面ページ作成ができたので、サーバを起動しましょう。
manage.pyファイルがあるフォルダがカレントディレクトリになっていることを確認して、コマンドプロンプトで以下のコマンドを実行
python manage.py runserver
http://localhost:8000/model-analyze へアクセスしましょう。
以下の画面が表示されればOKです!
簡単な動作確認
正常な動きは最初にお見せした完成イメージGIF通りです。しかし、今回作成したモデル選択機能には実は入力値チェック機能も備えているので、それが正常に動くか確認しましょう。
コードの詳細は「static/js/model_analyze/validate_form.js
」ファイルと「templates/model_analyze/select_models.html
」ファイルを参照ください。
入力値チェック機能とは?
誤ったデータを送信しないように、フォーム画面上でチェックできるところはチェックしておきましょう。というのが入力値チェック機能です。ものすごいざっくりとした説明なので文句は認めます。
とにかく、入力値が間違っていたらデータを送信せずに「間違っているよ」とお知らせしてあげるのが主な挙動となります。
今回でいうところの「Target Models」を未選択の状態で「Start Analyze」ボタンを押すと「Target Models」を選択してくれというメッセージが表示されるようになります。
動作確認GIF
以下のような動きになっていればOKです。
上記のように「*いずれかを選択してください。」という警告文が表示されればOKです。
あとは、「分析モード」ボタンを押下して表示されるアコーディオンから、各種分析ページに遷移されていれば正常です。
以上で、モデル選択機能と分析画面ページの開発手順完了です。お疲れさまでした。
ソース公開しました!
Bookersでロードマップ4で解説したソースを公開しました!
以下のリンクへ飛んでいただき、BookersとYouTube連携して私のチャンネルを登録すると無料でWEBアプリのソースが手に入ります。
良ければ、実際に触って遊んでみてください!
コメント