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
|
public
|
||||||
package-lock.json
|
package-lock.json
|
||||||
events/.venv
|
events/.venv
|
||||||
|
data.json
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
*.lock
|
*.lock
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,61 @@
|
||||||
from flask import Flask, request, render_template
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flask import Flask, request, render_template
|
||||||
|
|
||||||
from form_content import FormContent
|
from form_content import FormContent
|
||||||
|
from utils import get_data_stats
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
save_path = Path('data.txt')
|
save_path = Path('data.json')
|
||||||
all_keys = set(FormContent.times | FormContent.topics | FormContent.languages) | { 'name', 'email', 'topic-other' }
|
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:
|
MAX_FILE_SIZE = 1_000_000
|
||||||
data = {}
|
MAX_REQUEST_LENGTH = 1_000
|
||||||
summed = {
|
|
||||||
key: {
|
|
||||||
'sum': 0,
|
|
||||||
'total': 0,
|
|
||||||
'percent': 0
|
|
||||||
} for key in all_keys
|
|
||||||
}
|
|
||||||
summed['topic-other'] = set()
|
|
||||||
|
|
||||||
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('/')
|
@app.route('/')
|
||||||
def form():
|
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'])
|
@app.route('/submit', methods=['POST'])
|
||||||
def submit():
|
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
|
return { 'error': 'Database full.' }, 500
|
||||||
if len(request.data) > 1000:
|
if len(request.data) > MAX_REQUEST_LENGTH:
|
||||||
return { 'error': 'Request too long.' }, 400
|
return { 'error': 'Request too long.' }, 400
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
if set(data) != all_keys:
|
if set(data) != FormContent.all_keys:
|
||||||
return { 'error': 'Invalid request format.' }, 400
|
return { 'error': 'Invalid request format.' }, 400
|
||||||
|
|
||||||
with save_path.open(mode='a') as file:
|
data['date_submitted'] = datetime.strftime(datetime.now(), '%Y/%m/%d, %H:%M:%S')
|
||||||
file.write(f'{json.dumps(data)}\n')
|
|
||||||
|
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
|
return {}, 200
|
||||||
|
|
||||||
|
|
||||||
@app.route('/stats')
|
@app.route('/stats')
|
||||||
def stats():
|
def stats():
|
||||||
global stats_cache
|
return render_template('stats.html',
|
||||||
if save_path.exists() and save_path.stat().st_mtime > stats_cache[0]:
|
times=FormContent.times,
|
||||||
stats_cache = (save_path.stat().st_mtime, get_data_stats())
|
topics=FormContent.topics,
|
||||||
return render_template('stats.html', times=FormContent.times, topics=FormContent.topics,
|
background=FormContent.background,
|
||||||
languages=FormContent.languages, data=stats_cache[1])
|
languages=FormContent.languages,
|
||||||
|
data=stats_cache)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,20 @@ class FormContent:
|
||||||
'topic-ai': 'AI / Deep Learning libraries ディープラーニングのライブラリー (Pytorch, Tensorflow, etc.) ',
|
'topic-ai': 'AI / Deep Learning libraries ディープラーニングのライブラリー (Pytorch, Tensorflow, etc.) ',
|
||||||
'topic-web-dev': 'Web Development ウェブ開発'
|
'topic-web-dev': 'Web Development ウェブ開発'
|
||||||
}
|
}
|
||||||
|
background = {
|
||||||
|
'background-business': 'Business / Sales 営業',
|
||||||
|
'background-manager': 'Manager マネージャー',
|
||||||
|
'background-engineer': 'Engineer エンジニア',
|
||||||
|
'background-other': 'Other その他'
|
||||||
|
}
|
||||||
languages = {
|
languages = {
|
||||||
'language-english': 'English 英語',
|
'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) => {
|
const times = [...document.querySelectorAll('[id^=time-]')]?.reduce((entries, checkbox) => {
|
||||||
return { ...entries, [checkbox.id]: checkbox.checked }
|
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.style.display = 'block'
|
||||||
error.innerHTML = 'Please select at least one convenient time.
都合の時間を一つ以上を選択してください。'
|
error.innerHTML = 'Please select at least one convenient time.
都合の時間を一つ以上を選択してください。'
|
||||||
return
|
return
|
||||||
|
|
@ -44,12 +44,21 @@ const send = () => {
|
||||||
const topics = [...document.querySelectorAll('[id^=topic-]')]?.reduce((entries, checkbox) => {
|
const topics = [...document.querySelectorAll('[id^=topic-]')]?.reduce((entries, checkbox) => {
|
||||||
return { ...entries, [checkbox.id]: checkbox.checked }
|
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.style.display = 'block'
|
||||||
error.innerHTML = 'Please select at least one topic of interest.
興味あるテーマを一つ以上を選択してください。'
|
error.innerHTML = 'Please select at least one topic of interest.
興味あるテーマを一つ以上を選択してください。'
|
||||||
return
|
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')
|
const otherText = document.getElementById('other-text')
|
||||||
if (topics['topic-other'] && !otherText.value.trim()) {
|
if (topics['topic-other'] && !otherText.value.trim()) {
|
||||||
error.style.display = 'block'
|
error.style.display = 'block'
|
||||||
|
|
@ -58,12 +67,15 @@ const send = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const language = document.getElementById('language')
|
const language = document.getElementById('language')
|
||||||
|
const presentationInterest = document.getElementById('presentation-interest')
|
||||||
const data = {
|
const data = {
|
||||||
name: name.value,
|
name: name.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
...times,
|
...times,
|
||||||
...topics,
|
...topics,
|
||||||
|
...background,
|
||||||
'topic-other': topics['topic-other'] ? otherText.value : null,
|
'topic-other': topics['topic-other'] ? otherText.value : null,
|
||||||
|
'presentation-interest': presentationInterest.value,
|
||||||
'language-english': language.value == 'english' || language.value == 'either',
|
'language-english': language.value == 'english' || language.value == 'either',
|
||||||
'language-japanese': language.value == 'japanese' || language.value == 'either',
|
'language-japanese': language.value == 'japanese' || language.value == 'either',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,21 @@
|
||||||
<input id="other-text" type="text" placeholder="Other その他" />
|
<input id="other-text" type="text" placeholder="Other その他" />
|
||||||
</div>
|
</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">
|
<div class="question">
|
||||||
<p class="required">What language would you prefer?
どちらの言語がご希望ですか?</p>
|
<p class="required">What language would you prefer?
どちらの言語がご希望ですか?</p>
|
||||||
<select id="language">
|
<select id="language">
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<h1>AYO Events</h1>
|
<h1>AYO Events</h1>
|
||||||
|
|
||||||
<p>Thank you for your feedback submission!</p>
|
<p>Thank you for your feedback submission!</p>
|
||||||
<p>アンケートのご協力いただき、ありがとうございました!</p>
|
<p>アンケートにご協力いただき、ありがとうございました!</p>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<p class="instructions">Here are some insights into what others have said.</p>
|
<p class="instructions">Here are some insights into what others have said.</p>
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div class="stats-table">
|
<div class="stats-table">
|
||||||
<span class="stats-header">Times ご都合の良いの時間</span>
|
<span class="stats-header">Times ご都合の良い時間</span>
|
||||||
{% for id, label in times.items() %}
|
{% for id, label in times.items() %}
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-chart">
|
<div class="stat-chart">
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<span class="stats-header">Topics of Interest 興味あるテーマ</span>
|
<span class="stats-header">Topics of Interest 興味のあるテーマ</span>
|
||||||
{% for id, label in topics.items() %}
|
{% for id, label in topics.items() %}
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-chart">
|
<div class="stat-chart">
|
||||||
|
|
@ -50,6 +50,17 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</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>
|
<span class="stats-header">Languages 言語</span>
|
||||||
{% for id, label in languages.items() %}
|
{% for id, label in languages.items() %}
|
||||||
<div class="stat-item">
|
<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