SMTL

Die Stadtmeistersteilerliste und das Anmeldeformular - SK Langen e.V.
git clone git://git.oshgnacknak.de/SMTL.git
Log | Files | Refs | README

commit c0203bcfacfaae40d215598ae25a7b5258b55150
parent 68349116ee4b1d9ee73bf06475a0ea0292554d19
Author: Oshgnacknak <osh@oshgnacknak.de>
Date:   Sat,  3 Aug 2019 17:58:58 +0100

Caching + api routes

Diffstat:
Mconfig.py.dist | 6++++++
Mrequirements.txt | 3++-
Mrun.py | 9++++++---
Msmtl/app.py | 6++++--
Dsmtl/meta.py | 5-----
Msmtl/models/player.py | 38+++++++++++++++++++++++++++++++++++++-
Dsmtl/routes.py | 57---------------------------------------------------------
Asmtl/routes/__init__.py | 2++
Asmtl/routes/api.py | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asmtl/routes/front_end.py | 33+++++++++++++++++++++++++++++++++
Msmtl/signup_form.py | 27+++++++++++++++++++++------
Asmtl/templates/base.html | 22++++++++++++++++++++++
Dsmtl/templates/home.html | 17-----------------
Msmtl/templates/player_table.html | 51++++++++++++++++++++++++++++++++++++++-------------
Msmtl/templates/signup_form.html | 59++++++++++++++++++++++++++++++++++++++++++++---------------
Asmtl/utils.py | 4++++
16 files changed, 267 insertions(+), 120 deletions(-)

diff --git a/config.py.dist b/config.py.dist @@ -3,6 +3,12 @@ # Things in 'app_config' must all be set. ($ head -c 32 /dev/random | sha1sum) # the 'run_config' is optional but has to be a dict. +meta = { + "description": "Teilnehmerliste und Anmeldeformular der Langener Stadtmeisterschaft 2019", + "author": "SK Langen e.V." +} + + app_config = { 'SECRET_KEY': 'some_key', 'SQLALCHEMY_DATABASE_URI': 'sqlite:///../database.db', diff --git a/requirements.txt b/requirements.txt @@ -1,4 +1,5 @@ -flask +flask>=1.1.1 wtforms flask_wtf flask-sqlalchemy +flask_caching diff --git a/run.py b/run.py @@ -1,16 +1,19 @@ from smtl.app import app, db -from smtl.routes import routes +from smtl.routes import front_end_blue_print, api_blue_print from config import run_config import os def main(): db.create_all() - app.register_blueprint(routes) - + + app.register_blueprint(front_end_blue_print) + app.register_blueprint(api_blue_print, url_prefix='/api') + host = run_config.get('HOST', '127.0.0.1') port = run_config.get('PORT', 5000) debug = run_config.get('DEBUG', False) + app.run(host=host, port=port, debug=debug) diff --git a/smtl/app.py b/smtl/app.py @@ -1,9 +1,10 @@ from flask import Flask +from flask_caching import Cache from flask_wtf.csrf import CSRFProtect from flask_sqlalchemy import SQLAlchemy -from config import app_config - +from config import app_config, run_config +cache = Cache(config={'CACHE_TYPE': 'null' if run_config.get('DEBUG', False) else 'simple'}) csrf = CSRFProtect() app = Flask(__name__) @@ -12,4 +13,5 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config.update(app_config) csrf.init_app(app) +cache.init_app(app) db = SQLAlchemy(app) diff --git a/smtl/meta.py b/smtl/meta.py @@ -1,5 +0,0 @@ - -meta = { - "description": "Teilnehmerliste und Anmeldeformular der Langener Stadtmeisterschaft 2019", - "author": "SK Langen e.V." -} diff --git a/smtl/models/player.py b/smtl/models/player.py @@ -1,4 +1,12 @@ from smtl.app import db +from smtl.utils import current_year +from enum import Enum + + +class Gender(Enum): + MALE = 'M' + FEMALE = 'W' + DIVERSE = 'D' class Player(db.Model): @@ -13,6 +21,16 @@ class Player(db.Model): nullable=False ) + gender = db.Column( + db.Enum(Gender), + default=Gender.DIVERSE + ) + + birth_year = db.Column( + db.Integer(), + nullable=False + ) + club = db.Column( db.String(120) ) @@ -26,11 +44,29 @@ class Player(db.Model): db.Integer(), default=0 ) - + approved = db.Column( db.Boolean(), default=False ) + def to_dict(self): + return { + 'name': self.name, + 'dwz': self.dwz, + 'club': self.club, + 'attr': self.get_attr() + } + + def get_attr(self): + attr = '' + if self.gender != Gender.MALE: + attr += self.gender.values + if self.birth_year >= 2001: + attr += 'J' + elif self.birth_year < 1960: + attr += 'S' + return attr + def __str__(self): return f'Player({self.name})' diff --git a/smtl/routes.py b/smtl/routes.py @@ -1,57 +0,0 @@ -from flask import Blueprint, request, render_template, flash, redirect -from sqlalchemy.exc import SQLAlchemyError -from smtl.signup_form import SignupForm -from smtl.meta import meta -from smtl.app import db -from smtl.models.player import Player -from smtl.logging import logger - - -routes = Blueprint('routes', __name__) - - -@routes.route('/signup', methods=['POST']) -def signup(): - form = SignupForm(request.form) - if form.validate(): - try: - p = add_player(form) - logger.info(request.remote_addr + ' added ' + str(p)) - except SQLAlchemyError as e: - logger.error('Database Error: ' + e) - return 'Database Error!', 500 - else: - show_errors(form) - return redirect('/', code=302) - - -def add_player(form): - p = Player( - name=form.data['name'], - club=form.data['club'], - email=form.data['email'], - dwz=form.data['dwz'] - ) - db.session.add(p) - db.session.commit() - flash(f'{p.name} wurde hinzugefügt.') - return p - - -def show_errors(form): - for messages in form.errors.values(): - for message in messages: - flash(message, 'error') - - -@routes.route('/') -@routes.route('/home') -def home(): - form = SignupForm(request.form) - return render_template( - 'home.html', - title='Stadtmeisterschaft', - form=form, - meta=meta, - players=Player.query.filter_by(approved=True).all() - ) diff --git a/smtl/routes/__init__.py b/smtl/routes/__init__.py @@ -0,0 +1,2 @@ +from .api import blue_print as api_blue_print +from .front_end import blue_print as front_end_blue_print diff --git a/smtl/routes/api.py b/smtl/routes/api.py @@ -0,0 +1,48 @@ +from flask import Blueprint, request, jsonify +from sqlalchemy.exc import SQLAlchemyError +from smtl.signup_form import SignupForm +from smtl.app import db, cache +from smtl.models.player import Player, Gender +from smtl.logging import logger + + +blue_print = Blueprint('api', __name__) + + +@blue_print.route('/add_player', methods=['POST']) +def add_player(): + form = SignupForm(request.form) + if not form.validate(): + return jsonify( + status='error', + message='Formulardaten falsch oder fehlend.', + form_errors=form.errors + ) + try: + p = add_player(form) + logger.info(request.remote_addr + ' added ' + str(p)) + return jsonify(status='success', message=f'{p.name} wurde hinzugefügt.') + except SQLAlchemyError as e: + logger.error('Database Error: ' + str(e)) + return jsonify(status='error', message='Database Error!'), 500 + + +def add_player(form): + p = Player( + name=form.data['name'], + gender=Gender[form.data['gender']], + birth_year=form.data['birth_year'], + club=form.data['club'], + email=form.data['email'], + dwz=form.data['dwz'] + ) + db.session.add(p) + db.session.commit() + return p + + +@blue_print.route('/get_players') +@cache.cached(timeout=60*10) +def get_players(): + players = Player.query.filter_by(approved=True).all() + return jsonify([p.to_dict() for p in players]) diff --git a/smtl/routes/front_end.py b/smtl/routes/front_end.py @@ -0,0 +1,33 @@ +from flask import Blueprint, request, render_template, flash, redirect +from sqlalchemy.exc import SQLAlchemyError +from config import meta +from smtl.signup_form import SignupForm +from smtl.app import db, cache +from smtl.models.player import Player +from smtl.logging import logger + + +blue_print = Blueprint('front_end', __name__) +cache_time = 60*60*3 + + +@blue_print.route('/signup') +def signup(): + form = SignupForm() + return render_template( + 'signup_form.html', + title='Anmeldeformular', + meta=meta, + form=form + ) + + +@blue_print.route('/') +@blue_print.route('/player_table') +@cache.cached(timeout=cache_time) +def player_table(): + return render_template( + 'player_table.html', + title='Teilnehmerliste', + meta=meta, + ) diff --git a/smtl/signup_form.py b/smtl/signup_form.py @@ -1,6 +1,7 @@ -from wtforms import Form, TextField, IntegerField +from wtforms import Form, TextField, IntegerField, SelectField from wtforms.validators import DataRequired, Regexp, Email, Length, NumberRange -import re +from smtl.models.player import Gender, Player +from smtl.utils import current_year class SignupForm(Form): @@ -8,14 +9,28 @@ class SignupForm(Form): label='Name:', validators=[ DataRequired('Der Name darf nicht leer sein.'), - Length(max=60, message='Der Vorname ist zu lang.') + Length(max=Player.name.type.length, message='Der Vorname ist zu lang.') + ] + ) + + gender = SelectField( + label='Geschlecht:', + default=Gender.MALE.name, + choices=[(g.name, g.value) for g in Gender] + ) + + birth_year = IntegerField( + label='Geburtsjahr:', + validators=[ + DataRequired('Das Geburtsjahr darf nicht leer sein.'), + NumberRange(max=current_year, message='In der Zukumpft geboren ergibt keinen Sinn.') ] ) club = TextField( label='Verein:', validators=[ - Length(max=120, message='Der Vereinsname ist zu lang.') + Length(max=Player.club.type.length, message='Der Vereinsname ist zu lang.') ] ) @@ -24,7 +39,7 @@ class SignupForm(Form): validators=[ DataRequired('Die EMail darf nicht leer sein.'), Email('Die Email enstpricht nicht dem gewünschten Format.'), - Length(max=120, message='Die Email ist zu lang.') + Length(max=Player.email.type.length, message='Die Email ist zu lang.') ] ) @@ -32,6 +47,6 @@ class SignupForm(Form): label='DWZ:', default=0, validators=[ - NumberRange(min=0, message='Die DWZ ist zu klein.') + NumberRange(min=0, message='Die DWZ muss positiv sein.') ] ) diff --git a/smtl/templates/base.html b/smtl/templates/base.html @@ -0,0 +1,22 @@ +<!doctype html> + +<html lang="en"> + <head> + <meta charset="utf-8"> + {% if title: %} + <title>Stadtmeisterschaft - {{ title }}</title> + {% else: %} + <title>Stadtmeisterschaft</title> + {% endif %} + + {% include 'meta.html' %} + <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> + </head> + <body> + {% if title: %} + <h2 id="title">{{ title }}</h2> + {% endif %} + {% block content %} + {% endblock %} + </body> +</html> diff --git a/smtl/templates/home.html b/smtl/templates/home.html @@ -1,17 +0,0 @@ -<!doctype html> - -<html lang="en"> - <head> - <meta charset="utf-8"> - <title>{{ title }}</title> - {% include 'meta.html' %} - <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> - </head> - <body> - <h2 id="title">{{ title }}</h2> - {% include 'flashes.html' %} - {% include 'signup_form.html' %} - {% include 'player_table.html' %} - <script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script> - </body> -</html> diff --git a/smtl/templates/player_table.html b/smtl/templates/player_table.html @@ -1,20 +1,45 @@ -{% if players %} +{% extends 'base.html' %} +{% block content %} <table id="player_table"> <tr> <th>Name</th> <th>Verein</th> <th>DWZ</th> + <th>Attr.</th> </tr> - {% for player in players: %} - <tr> - <td>{{ player.name }}</td> - <td>{{ player.club or '-'}}</td> - <td>{{ player.dwz or 0 }}</td> - </tr> - {% endfor %} </table> -{% else %} - <div id="player_table" class="no_players"> - <b>Noch keine Spieler...</b> - </div> -{% endif %} + + <script> + const sorter = { + dwz: (a, b) => a.dwz - b.dwz, + name: (a, b) => a.name < b.name, + club: (a, b) => a.club < b.club, + attr: (a, b) => a.attr < b.attr + } + + const table = getElementById('player_table'); + let players; + + function render() { + Array.from(table.querySelectorAll(':scope .data')) + .forEach(e => e.remove()); + players.sort(sorter.dwz); + players.forEach(p => { + const tr = document.createElement('tr'); + tr.classList.add("data"); + ['name', 'club', 'dwz', 'attr'].forEach(k => + td.innerHTML += `<td>${p[k]}</td>\n`); + table.appendChild(tr); + }); + } + + window.addEventListener('DOMContentLoaded', () => { + fetch('/api/get_players') + .then(r => r.json()) + .then(p => { + players = p; + render(); + }); + }); + </script> +{% endblock %} diff --git a/smtl/templates/signup_form.html b/smtl/templates/signup_form.html @@ -1,17 +1,46 @@ -<form action="/signup" id="signup_form" method="POST"> - <table> - {% for field in [form.name, form.club, form.dwz, form.email] %} +{% extends 'base.html' %} +{% block content %} + <form id="signup_form" method="POST"> + <table> + {% for field in [form.name, form.gender, form.birth_year, form.club, form.dwz, form.email] %} + <tr> + <td>{{ field.label }}</td> + <td>{{ field }}</td> + </tr> + {% endfor %} <tr> - <td>{{ field.label }}</td> - <td>{{ field }}</td> + <td> + <input type="submit" value="Teilnehmen"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> + </td> + <td><input type="reset" value="Löschen"></td> </tr> - {% endfor %} - <tr> - <td> - <input type="submit" value="Teilnehmen"> - <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> - </td> - <td><input type="reset" value="Löschen"></td> - </tr> - </table> -</form> + </table> + </form> + <script> + const form = document.getElementById('signup_form'); + form.onsubmit = e => { + e.preventDefault(); + fetch('/api/add_player', { + method: 'POST', + body: new FormData(form) + }) + .then(r => r.json()) + .then(r => { + if (r.status == 'success') { + alert(r.message); + form.reset(); + } else if (r.form_errors) { + Array.from(form.elements) + .filter(e => r.form_errors[e.name]) + .forEach(e => { + e.setCustomValidity(r.form_errors[e.name].join('\n')) + e.checkValidity(); + }); + } else { + throw new Error(r.message); + } + }); + } + </script> +{% endblock %} diff --git a/smtl/utils.py b/smtl/utils.py @@ -0,0 +1,4 @@ +from datetime import date + + +current_year = date.today().year