Update Events (#10)
This update restructures the events page code and provides additional functionality. - Overwrite old entries on re-submission of previously used emails - Build data and stats caches on launch for use in submission checks and stats regeneration - Update to form content - Move stats function to separate utils file Reviewed-on: ayo/website#10 Co-authored-by: Jimmy Vargo <james@ayo.tokyo> Co-committed-by: Jimmy Vargo <james@ayo.tokyo>
This commit is contained in:
parent
b2bf426820
commit
dae67ce653
8 changed files with 128 additions and 53 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,6 +2,7 @@ node_modules
|
|||
public
|
||||
package-lock.json
|
||||
events/.venv
|
||||
data.json
|
||||
|
||||
*.log
|
||||
*.lock
|
||||
|
|
|
|||
|
|
@ -1,75 +1,62 @@
|
|||
from flask import Flask, request, render_template
|
||||
from datetime import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, request, render_template
|
||||
|
||||
from form_content import FormContent
|
||||
from utils import get_data_stats
|
||||
|
||||
app = Flask(__name__)
|
||||
save_path = Path('data.txt')
|
||||
all_keys = set(FormContent.times | FormContent.topics | FormContent.languages) | { 'name', 'email', 'topic-other' }
|
||||
save_path = Path('data.json')
|
||||
data_cache = { entry['email']: entry for entry in json.loads(save_path.read_text()) }\
|
||||
if save_path.exists() else {}
|
||||
stats_cache = get_data_stats(data_cache)
|
||||
|
||||
def get_data_stats() -> dict:
|
||||
data = {}
|
||||
summed = {
|
||||
key: {
|
||||
'sum': 0,
|
||||
'total': 0,
|
||||
'percent': 0
|
||||
} for key in all_keys
|
||||
}
|
||||
summed['topic-other'] = set()
|
||||
MAX_FILE_SIZE = 1_000_000
|
||||
MAX_REQUEST_LENGTH = 1_000
|
||||
|
||||
if save_path.exists():
|
||||
for raw_line in save_path.read_text().split('\n'):
|
||||
if raw_line:
|
||||
line = json.loads(raw_line)
|
||||
data[line['email']] = line
|
||||
for line in data.values():
|
||||
for key, value in dict.items(line):
|
||||
if key == 'topic-other' and value is not None:
|
||||
summed['topic-other'] = summed['topic-other'] | { value }
|
||||
if type(value) is bool:
|
||||
sum = summed[key]['sum'] if key in summed else 0
|
||||
sum = sum + 1 if value is True else sum
|
||||
total = summed[key]['total'] + 1 if key in summed else 1
|
||||
summed[key] = {
|
||||
'sum': sum,
|
||||
'total': total,
|
||||
'percent': f'{((sum / total) * 100):.1f}'
|
||||
}
|
||||
summed['topic-other'] = list(summed['topic-other'])
|
||||
return summed
|
||||
|
||||
stats_cache = (0, get_data_stats())
|
||||
|
||||
@app.route('/')
|
||||
def form():
|
||||
return render_template('form.html', times=FormContent.times, topics=FormContent.topics, languages=FormContent.languages)
|
||||
return render_template('form.html',
|
||||
times=FormContent.times,
|
||||
topics=FormContent.topics,
|
||||
background=FormContent.background,
|
||||
languages=FormContent.languages)
|
||||
|
||||
|
||||
@app.route('/submit', methods=['POST'])
|
||||
def submit():
|
||||
if save_path.exists() and save_path.stat().st_size > 1_000_000:
|
||||
if save_path.exists() and save_path.stat().st_size > MAX_FILE_SIZE:
|
||||
return { 'error': 'Database full.' }, 500
|
||||
if len(request.data) > 1000:
|
||||
if len(request.data) > MAX_REQUEST_LENGTH:
|
||||
return { 'error': 'Request too long.' }, 400
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
if set(data) != all_keys:
|
||||
if set(data) != FormContent.all_keys:
|
||||
return { 'error': 'Invalid request format.' }, 400
|
||||
|
||||
with save_path.open(mode='a') as file:
|
||||
file.write(f'{json.dumps(data)}\n')
|
||||
data['date_submitted'] = datetime.strftime(datetime.now(), '%Y/%m/%d, %H:%M:%S')
|
||||
|
||||
data_cache[data['email']] = data
|
||||
save_path.write_text(json.dumps(list(data_cache.values()), indent='\t'))
|
||||
|
||||
global stats_cache
|
||||
stats_cache = get_data_stats(data_cache)
|
||||
return {}, 200
|
||||
|
||||
|
||||
@app.route('/stats')
|
||||
def stats():
|
||||
global stats_cache
|
||||
if save_path.exists() and save_path.stat().st_mtime > stats_cache[0]:
|
||||
stats_cache = (save_path.stat().st_mtime, get_data_stats())
|
||||
return render_template('stats.html', times=FormContent.times, topics=FormContent.topics,
|
||||
languages=FormContent.languages, data=stats_cache[1])
|
||||
return render_template('stats.html',
|
||||
times=FormContent.times,
|
||||
topics=FormContent.topics,
|
||||
background=FormContent.background,
|
||||
languages=FormContent.languages,
|
||||
data=stats_cache)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=9700, debug=False)
|
||||
app.run(host='0.0.0.0', port=9700, debug=False)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,20 @@ class FormContent:
|
|||
'topic-ai': 'AI / Deep Learning libraries ディープラーニングのライブラリー (Pytorch, Tensorflow, etc.) ',
|
||||
'topic-web-dev': 'Web Development ウェブ開発'
|
||||
}
|
||||
background = {
|
||||
'background-business': 'Business / Sales 営業',
|
||||
'background-manager': 'Manager マネージャー',
|
||||
'background-engineer': 'Engineer エンジニア',
|
||||
'background-other': 'Other その他'
|
||||
}
|
||||
languages = {
|
||||
'language-english': 'English 英語',
|
||||
'language-japanese': 'Japanese 日本語',
|
||||
'language-japanese': 'Japanese 日本語'
|
||||
}
|
||||
|
||||
all_keys = set(times | topics | background | languages) | {
|
||||
'name',
|
||||
'email',
|
||||
'topic-other',
|
||||
'presentation-interest'
|
||||
}
|
||||
|
|
|
|||
8
events/pyproject.toml
Normal file
8
events/pyproject.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[tool.ruff]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff.lint]
|
||||
preview = true
|
||||
select = ["A", "ARG", "B", "C", "E", "F", "FURB", "G", "I","ICN", "ISC", "PERF", "PIE", "PL", "PLE", "PTH",
|
||||
"Q", "RET", "RSE", "RUF", "SLF", "SIM", "T20", "TCH", "UP", "W"]
|
||||
ignore = ["E201", "E202", "FURB140", "I001", "PERF203", "PLW0603", "Q000", "RET502", "RET503", "RUF012"]
|
||||
|
|
@ -35,7 +35,7 @@ const send = () => {
|
|||
const times = [...document.querySelectorAll('[id^=time-]')]?.reduce((entries, checkbox) => {
|
||||
return { ...entries, [checkbox.id]: checkbox.checked }
|
||||
}, {})
|
||||
if (Object.values(times).every(time => time == false)) {
|
||||
if (Object.values(times).every(item => item == false)) {
|
||||
error.style.display = 'block'
|
||||
error.innerHTML = 'Please select at least one convenient time.
都合の時間を一つ以上を選択してください。'
|
||||
return
|
||||
|
|
@ -44,12 +44,21 @@ const send = () => {
|
|||
const topics = [...document.querySelectorAll('[id^=topic-]')]?.reduce((entries, checkbox) => {
|
||||
return { ...entries, [checkbox.id]: checkbox.checked }
|
||||
}, {})
|
||||
if (Object.values(topics).every(time => time == false)) {
|
||||
if (Object.values(topics).every(item => item == false)) {
|
||||
error.style.display = 'block'
|
||||
error.innerHTML = 'Please select at least one topic of interest.
興味あるテーマを一つ以上を選択してください。'
|
||||
return
|
||||
}
|
||||
|
||||
const background = [...document.querySelectorAll('[id^=background-]')]?.reduce((entries, checkbox) => {
|
||||
return { ...entries, [checkbox.id]: checkbox.checked }
|
||||
}, {})
|
||||
if (Object.values(background).every(item => item == false)) {
|
||||
error.style.display = 'block'
|
||||
error.innerHTML = 'Please select at least one background.
履歴・業種を一つ以上を選択してください。'
|
||||
return
|
||||
}
|
||||
|
||||
const otherText = document.getElementById('other-text')
|
||||
if (topics['topic-other'] && !otherText.value.trim()) {
|
||||
error.style.display = 'block'
|
||||
|
|
@ -58,12 +67,15 @@ const send = () => {
|
|||
}
|
||||
|
||||
const language = document.getElementById('language')
|
||||
const presentationInterest = document.getElementById('presentation-interest')
|
||||
const data = {
|
||||
name: name.value,
|
||||
email: email.value,
|
||||
...times,
|
||||
...topics,
|
||||
...background,
|
||||
'topic-other': topics['topic-other'] ? otherText.value : null,
|
||||
'presentation-interest': presentationInterest.value,
|
||||
'language-english': language.value == 'english' || language.value == 'either',
|
||||
'language-japanese': language.value == 'japanese' || language.value == 'either',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,21 @@
|
|||
<input id="other-text" type="text" placeholder="Other その他" />
|
||||
</div>
|
||||
|
||||
<div class="question">
|
||||
<input id="presentation-interest" type="checkbox" />
|
||||
<label for="presentation-interest">I'm interested in giving a talk at an event. イベントでプレゼンテーションをすることに興味があります。</label>
|
||||
</div>
|
||||
|
||||
<div class="question">
|
||||
<p class="required">What is your professional background?
履歴・業種を教えてください。</p>
|
||||
{% for id, name in background.items() %}
|
||||
<div class="row">
|
||||
<input id="{{ id }}" type="checkbox" />
|
||||
<label for="{{ id }}">{{ name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="question">
|
||||
<p class="required">What language would you prefer?
どちらの言語がご希望ですか?</p>
|
||||
<select id="language">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<h1>AYO Events</h1>
|
||||
|
||||
<p>Thank you for your feedback submission!</p>
|
||||
<p>アンケートのご協力いただき、ありがとうございました!</p>
|
||||
<p>アンケートにご協力いただき、ありがとうございました!</p>
|
||||
|
||||
<br>
|
||||
<p class="instructions">Here are some insights into what others have said.</p>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<br>
|
||||
|
||||
<div class="stats-table">
|
||||
<span class="stats-header">Times ご都合の良いの時間</span>
|
||||
<span class="stats-header">Times ご都合の良い時間</span>
|
||||
{% for id, label in times.items() %}
|
||||
<div class="stat-item">
|
||||
<div class="stat-chart">
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<span class="stats-header">Topics of Interest 興味あるテーマ</span>
|
||||
<span class="stats-header">Topics of Interest 興味のあるテーマ</span>
|
||||
{% for id, label in topics.items() %}
|
||||
<div class="stat-item">
|
||||
<div class="stat-chart">
|
||||
|
|
@ -50,6 +50,17 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<span class="stats-header">Backgrounds 履歴・業種</span>
|
||||
{% for id, label in background.items() %}
|
||||
<div class="stat-item">
|
||||
<div class="stat-chart">
|
||||
<div class="pie-chart" style="background: conic-gradient(var(--color) 0%, var(--color) {{ data[id]['percent'] }}%, var(--bg) {{ data[id]['percent'] }}%, var(--bg) 100%);"></div>
|
||||
<span class="stat-percent">{{ data[id]['percent'] }}%</span>
|
||||
</div>
|
||||
<div><p class="stat-label">{{ label }}</p></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<span class="stats-header">Languages 言語</span>
|
||||
{% for id, label in languages.items() %}
|
||||
<div class="stat-item">
|
||||
|
|
|
|||
28
events/utils.py
Normal file
28
events/utils.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from form_content import FormContent
|
||||
|
||||
|
||||
def get_data_stats(data: dict) -> dict:
|
||||
summed = {
|
||||
key: {
|
||||
'sum': 0,
|
||||
'total': 0,
|
||||
'percent': 0
|
||||
} for key in FormContent.all_keys
|
||||
}
|
||||
topic_other = set()
|
||||
|
||||
for entry in data.values():
|
||||
for key, value in dict.items(entry):
|
||||
if key == 'topic-other' and value is not None:
|
||||
topic_other |= { value }
|
||||
if isinstance(value, bool):
|
||||
_sum = summed[key]['sum'] if key in summed else 0
|
||||
_sum = _sum + 1 if value is True else _sum
|
||||
total = summed[key]['total'] + 1 if key in summed else 1
|
||||
summed[key] = {
|
||||
'sum': _sum,
|
||||
'total': total,
|
||||
'percent': f'{((_sum / total) * 100):.1f}'
|
||||
}
|
||||
summed['topic-other'] = list(topic_other)
|
||||
return summed
|
||||
Loading…
Add table
Add a link
Reference in a new issue