Skip to content

Commit 8f51429

Browse files
Refactor results loader for standardization and multi-row benchmark table
- Unified JSON parsing and confidentiality checks - Added affiliation-based filtering - Standardized row/column extraction - Benchmark columns now use multi-row headers in table
1 parent b19beef commit 8f51429

9 files changed

Lines changed: 337 additions & 167 deletions

File tree

result_server/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
# Create the Flask app and specify the templates folder
1919
app = Flask(__name__, template_folder="templates")
2020

21-
2221
# Set a secret key for session management (required for flash and OTP sessions)
2322
# In production, use a secure random key, e.g., os.urandom(24)
2423
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev_secret_key")
@@ -37,5 +36,6 @@ def hard_env(sys):
3736
if __name__ == "__main__":
3837
# Ensure the directory to store received files exists
3938
os.makedirs("received", exist_ok=True)
39+
os.makedirs("estimated_results", exist_ok=True)
4040
# Start the server, listening on all interfaces at port 8800
4141
app.run(host="0.0.0.0", port=8800)

result_server/routes/results.py

Lines changed: 91 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,42 @@
11
import os
22
from flask import (
33
Blueprint, render_template, request, session,
4-
redirect, url_for, flash, abort
4+
redirect, url_for, flash, abort, send_from_directory
55
)
6-
from utils.results_loader import load_results_table
6+
from utils.results_loader import load_results_table, load_estimated_results_table
77
from utils.otp_manager import send_otp, verify_otp, get_affiliations
88
from utils.result_file import load_result_file, get_file_confidential_tags
99

1010
results_bp = Blueprint("results", __name__)
1111
SAVE_DIR = "received"
12+
ESTIMATE_DIR = "estimated_results"
1213

1314

15+
# ==========================================
16+
# 共通関数: ファイルアクセス権限確認
17+
# ==========================================
18+
def check_file_permission(filename, session_key_authenticated, session_key_email, dir_path):
19+
tags = get_file_confidential_tags(filename, dir_path)
20+
if not tags:
21+
return # 公開ファイル
22+
23+
authenticated = session.get(session_key_authenticated, False)
24+
email = session.get(session_key_email)
25+
affs = get_affiliations(email) if email else []
26+
if not authenticated or not (set(tags) & set(affs)):
27+
abort(403, "このファイルにアクセスする権限がありません")
28+
29+
30+
def serve_confidential_file(filename, dir_path, session_key_authenticated, session_key_email):
31+
"""ファイルアクセス権限確認して送信"""
32+
check_file_permission(filename, session_key_authenticated, session_key_email, dir_path)
33+
#return send_from_directory(dir_path, filename, as_attachment=True)
34+
return load_result_file(filename, dir_path)
35+
1436
# ==========================================
1537
# 公開用の結果一覧ページ
1638
# ==========================================
17-
@results_bp.route("/results")
39+
@results_bp.route("/results", strict_slashes=False)
1840
def results():
1941
rows, columns = load_results_table(public_only=True)
2042
return render_template("results.html", rows=rows, columns=columns)
@@ -23,79 +45,93 @@ def results():
2345
# ==========================================
2446
# 機密データ付きの結果ページ(OTP認証付き)
2547
# ==========================================
26-
@results_bp.route("/results_confidential", methods=["GET", "POST"])
27-
def results_confidential():
28-
# フォーム送信処理
29-
if request.method == "POST":
30-
email = request.form.get("email")
31-
otp = request.form.get("otp")
32-
33-
if email and not otp:
34-
# STEP1: メール送信
35-
success, msg = send_otp(email)
36-
if success:
37-
flash("OTPをメールに送信しました")
38-
session["otp_email"] = email
39-
session["otp_stage"] = "otp"
40-
else:
41-
flash(msg)
42-
session.pop("otp_email", None)
43-
session["otp_stage"] = "email"
44-
return redirect(url_for("results.results_confidential"))
45-
46-
elif otp:
47-
# STEP2: OTP検証
48-
otp_email = session.get("otp_email")
49-
if otp_email and verify_otp(otp_email, otp):
50-
session["authenticated_confidential"] = True
51-
flash("認証成功")
52-
session.pop("otp_stage", None) # 認証済みなので削除
53-
else:
54-
flash("OTP認証失敗")
55-
session.pop("otp_email", None)
56-
session.pop("authenticated_confidential", None)
57-
session["otp_stage"] = "email"
58-
return redirect(url_for("results.results_confidential"))
59-
60-
# OTPステージ判定
61-
authenticated = session.get("authenticated_confidential", False)
62-
otp_email = session.get("otp_email")
48+
def handle_otp_post(session_key_authenticated, session_key_email, route_name):
49+
email = request.form.get("email")
50+
otp = request.form.get("otp")
51+
52+
if email and not otp:
53+
success, msg = send_otp(email)
54+
if success:
55+
flash("OTPをメールに送信しました")
56+
session[session_key_email] = email
57+
session["otp_stage"] = "otp"
58+
else:
59+
flash(msg)
60+
session.pop(session_key_email, None)
61+
session["otp_stage"] = "email"
62+
return redirect(url_for(route_name))
63+
64+
elif otp:
65+
otp_email = session.get(session_key_email)
66+
if otp_email and verify_otp(otp_email, otp):
67+
session[session_key_authenticated] = True
68+
flash("認証成功")
69+
session.pop("otp_stage", None)
70+
else:
71+
flash("OTP認証失敗")
72+
session.pop(session_key_email, None)
73+
session.pop(session_key_authenticated, None)
74+
session["otp_stage"] = "email"
75+
return redirect(url_for(route_name))
76+
77+
78+
def render_confidential_table(template_name, public_only, session_key_authenticated, session_key_email, loader_func=None):
79+
authenticated = session.get(session_key_authenticated, False)
80+
otp_email = session.get(session_key_email)
6381

6482
if authenticated:
65-
otp_stage = None # 認証済みなのでフォームは出さない
83+
otp_stage = None
6684
elif otp_email:
67-
otp_stage = "otp" # OTP入力待ち
85+
otp_stage = "otp"
6886
else:
69-
otp_stage = "email" # メール入力待ち
87+
otp_stage = "email"
7088

89+
# ローダー関数を使い分ける
90+
if loader_func is None:
91+
# デフォルトは通常の results
92+
loader_func = load_results_table
7193

72-
# 結果テーブル読み込み(confidential制御は utils 内で処理)
73-
rows, columns = load_results_table(
74-
public_only=False,
94+
rows, columns = loader_func(
95+
public_only=public_only,
7596
session_email=otp_email,
7697
authenticated=authenticated
7798
)
7899

79100
return render_template(
80-
"results_confidential.html",
101+
template_name,
81102
rows=rows,
82103
columns=columns,
83104
authenticated=authenticated,
84105
otp_stage=otp_stage
85106
)
86107

108+
109+
@results_bp.route("/results_confidential", methods=["GET", "POST"], strict_slashes=False)
110+
def results_confidential():
111+
if request.method == "POST":
112+
return handle_otp_post("authenticated_confidential", "otp_email", "results.results_confidential")
113+
return render_confidential_table("results_confidential.html", public_only=False,
114+
session_key_authenticated="authenticated_confidential",
115+
session_key_email="otp_email")
116+
117+
118+
@results_bp.route("/estimated_results", methods=["GET", "POST"], strict_slashes=False)
119+
def estimated_results():
120+
if request.method == "POST":
121+
return handle_otp_post("authenticated_estimated", "otp_email_estimated", "results.estimated_results")
122+
return render_confidential_table("estimated_results.html", public_only=False,
123+
session_key_authenticated="authenticated_estimated",
124+
session_key_email="otp_email_estimated", loader_func=load_estimated_results_table)
125+
126+
87127
# ==========================================
88128
# 個別結果ファイルの表示/ダウンロード
89129
# ==========================================
90130
@results_bp.route("/results/<filename>")
91131
def show_result(filename):
92-
tags = get_file_confidential_tags(filename)
132+
return serve_confidential_file(filename, SAVE_DIR, "authenticated_confidential", "otp_email")
93133

94-
if tags:
95-
authenticated = session.get("authenticated_confidential", False)
96-
email = session.get("otp_email")
97-
affs = get_affiliations(email) if email else []
98-
if not authenticated or not (set(tags) & set(affs)):
99-
abort(403, "このファイルにアクセスする権限がありません")
100134

101-
return load_result_file(filename)
135+
@results_bp.route("/estimated_results/<filename>")
136+
def show_estimated_result(filename):
137+
return serve_confidential_file(filename, ESTIMATE_DIR, "authenticated_estimated", "otp_email_estimated")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% if not authenticated %}
2+
<div class="overlay">
3+
<div class="modal">
4+
<h3>データの閲覧には認証が必要です</h3>
5+
6+
{% if otp_stage == "email" %}
7+
<form method="POST">
8+
<input type="email" name="email" placeholder="メールアドレス" required>
9+
<button type="submit">OTPを送信</button>
10+
</form>
11+
{% elif otp_stage == "otp" %}
12+
<form method="POST">
13+
<input type="text" name="otp" maxlength="6" placeholder="OTP (6桁)" required>
14+
<button type="submit">認証する</button>
15+
</form>
16+
{% endif %}
17+
</div>
18+
</div>
19+
{% endif %}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<style>
2+
/* 共通テーブルスタイル */
3+
table { border-collapse: collapse; width: 100%; }
4+
th, td { border: 1px solid #ddd; padding: 8px; }
5+
th { background-color: #f2f2f2; }
6+
tr:hover { background-color: #f5f5f5; }
7+
.underline-link { text-decoration: underline; color: darkblue; cursor: pointer; }
8+
9+
/* OTPモーダル */
10+
.overlay {
11+
position: fixed;
12+
top: 0; left: 0; width: 100%; height: 100%;
13+
background: rgba(0,0,0,0.5);
14+
display: flex; justify-content: center; align-items: center;
15+
z-index: 9999;
16+
}
17+
.modal {
18+
background: white; padding: 20px; border-radius: 8px; text-align: center;
19+
}
20+
</style>
21+
22+
<script>
23+
function filterTable() {
24+
const input = document.getElementById("filterInput").value.toLowerCase();
25+
const rows = document.querySelectorAll("#resultsTable tbody tr");
26+
27+
rows.forEach(row => {
28+
const rowText = row.innerText.toLowerCase();
29+
const terms = input.split(/\s+/).filter(t => t.length > 0);
30+
const match = terms.every(term => rowText.includes(term));
31+
row.style.display = match ? "" : "none";
32+
});
33+
}
34+
</script>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Estimated Results</title>
6+
{% include "_table_base.html" %}
7+
</head>
8+
<body>
9+
10+
<h1>Estimated Results</h1>
11+
12+
{% include "_otp_modal.html" %}
13+
14+
<input type="text" id="filterInput" onkeyup="filterTable()" placeholder="Search by multiple keywords...">
15+
<br><br>
16+
17+
18+
19+
<table border="1">
20+
<thead>
21+
<tr>
22+
<th rowspan="2">Code</th>
23+
<th rowspan="2">Exp</th>
24+
<th colspan="3">System A</th>
25+
<th colspan="3">System B</th>
26+
<th colspan="2">Benchmark</th>
27+
<th rowspan="2">JSON</th>
28+
<th rowspan="2">Ratio</th>
29+
</tr>
30+
<tr>
31+
<th>FOM</th>
32+
<th>Method</th>
33+
<th>Nodes</th>
34+
<th>FOM</th>
35+
<th>Method</th>
36+
<th>Nodes</th>
37+
<th>FOM</th>
38+
<th>System</th>
39+
</tr>
40+
</thead>
41+
<tbody>
42+
{% for r in rows %}
43+
<tr>
44+
<td>{{ r.code }}</td>
45+
<td>{{ r.exp }}</td>
46+
<td>{{ r.systemA_fom }}</td>
47+
<td>{{ r.systemA_method }}</td>
48+
<td>{{ r.systemA_nodes }}</td>
49+
<td>{{ r.systemB_fom }}</td>
50+
<td>{{ r.systemB_method }}</td>
51+
<td>{{ r.systemB_nodes }}</td>
52+
<td>{{ r.benchmark_fom }}</td>
53+
<td>{{ r.benchmark_system }}</td>
54+
<td>
55+
<a href="{{ url_for('results.show_estimated_result', filename=r.json_link) }}">json</a>
56+
</td>
57+
<td>{{ r.performance_ratio }}</td>
58+
</tr>
59+
{% endfor %}
60+
</tbody>
61+
</table>
62+
63+
64+
</body>
65+
</html>

result_server/templates/results.html

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,12 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5-
<title>Uploaded Results</title>
6-
<style>
7-
table { border-collapse: collapse; width: 100%; }
8-
th, td { border: 1px solid #ddd; padding: 8px; }
9-
th { background-color: #f2f2f2; }
10-
tr:hover { background-color: #f5f5f5; }
11-
.underline-link { text-decoration: underline; color: darkblue; cursor: pointer; }
12-
</style>
13-
<script>
14-
function filterTable() {
15-
const input = document.getElementById("filterInput").value.toLowerCase();
16-
const rows = document.querySelectorAll("#resultsTable tbody tr");
17-
18-
rows.forEach(row => {
19-
const rowText = row.innerText.toLowerCase();
20-
const terms = input.split(/\s+/).filter(t => t.length > 0);
21-
const match = terms.every(term => rowText.includes(term));
22-
row.style.display = match ? "" : "none";
23-
});
24-
}
25-
</script>
5+
<title>Results</title>
6+
{% include "_table_base.html" %}
267
</head>
278
<body>
289

29-
<h1>Uploaded Results</h1>
30-
10+
<h1>Results</h1>
3111
<input type="text" id="filterInput" onkeyup="filterTable()" placeholder="Search by multiple keywords...">
3212
<br><br>
3313

0 commit comments

Comments
 (0)