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:
Jimmy Vargo 2024-05-07 18:59:57 +09:00 committed by james
commit dae67ce653
8 changed files with 128 additions and 53 deletions

1
.gitignore vendored
View file

@ -2,6 +2,7 @@ node_modules
public
package-lock.json
events/.venv
data.json
*.log
*.lock

View file

@ -1,74 +1,61 @@
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__':

View file

@ -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
View 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"]

View file

@ -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.&NewLine;都合の時間を一つ以上を選択してください。'
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.&NewLine;興味あるテーマを一つ以上を選択してください。'
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.&NewLine;履歴・業種を一つ以上を選択してください。'
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',
}

View file

@ -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?&NewLine;履歴・業種を教えてください。</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?&NewLine;どちらの言語がご希望ですか?</p>
<select id="language">

View file

@ -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
View 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