Browse Source

Strategická: Reforma webu

master
Jiří Kalvoda 2 years ago
parent
commit
e2d353d48a
  1. 23
      server/bin/create_root
  2. 34
      server/bin/db_init
  3. 7
      server/config.py.example
  4. 47
      server/constraints.txt
  5. 54
      server/hra/db.py
  6. 7
      server/hra/util.py
  7. 272
      server/hra/web/__init__.py
  8. 61
      server/hra/web/html.py
  9. 3
      server/hra/web/jinja_mac.py
  10. 248
      server/hra/web/pages.py
  11. 2
      server/setup.py

23
server/bin/create_root

@ -0,0 +1,23 @@
#!/usr/bin/env python3
import argparse
from hra.util import hash_passwd
from sqlalchemy import exc, update
import sys
import hra.db as db
parser = argparse.ArgumentParser()
parser.add_argument("username", help="Username")
parser.add_argument("passwd", help="Password")
args = parser.parse_args()
u = db.User(org=True, username=args.username, passwd=hash_passwd(args.passwd))
try:
db.get_session().add(u)
db.get_session().commit()
except exc.IntegrityError:
print("Uživatelské jméno již existuje")
sys.exit(1)
print("Přidán nový uživatel.")

34
server/bin/db_init

@ -0,0 +1,34 @@
#!/usr/bin/env python3
import hra.db as db
from sqlalchemy import exc, update
import sys
import hra.config as config
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--drop", help="Drop all existing tables", action="store_true")
args = parser.parse_args()
print(config.SQLALCHEMY_DATABASE_URI)
if args.drop:
if config.WEB_FLAVOR != "devel":
if input('Write "DROP ALL": ') != "DROP ALL":
print("Wrong, nothing to do.")
sys.exit(1)
rs = db.get_session().execute("""
SELECT
'DROP TABLE IF EXISTS "' || tablename || '" CASCADE;'
from
pg_tables WHERE schemaname = 'public';""")
for row in rs:
x = db.get_session().execute(row[0])
print(row, x)
db.get_session().commit()
db.metadata.bind = db.get_engine()
db.metadata.create_all()

7
server/config.py.example

@ -1,6 +1,9 @@
# Patří do hra/config.py # Patří do hra/config.py
SQLALCHEMY_DATABASE_URI = "postgresql:///ksp-strathra" # Druh webu (devel/test/pub), z toho CSS třída elementu <header>
WEB_FLAVOR = 'devel'
SQLALCHEMY_DATABASE_URI = f"postgresql:///ksp_strathra_{WEB_FLAVOR}"
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = False SQLALCHEMY_ECHO = False
@ -9,8 +12,6 @@ SECRET_KEY = "8aeffbdf14d441f40359708cbbae9b47926db8b08e4ea9e0cdf78071bee7b788"
SESSION_COOKIE_NAME = 'ksp_strathra__session' SESSION_COOKIE_NAME = 'ksp_strathra__session'
# Druh webu (devel/test/pub), z toho CSS třída elementu <header>
WEB_FLAVOR = 'devel'
# Nutné pro registraci # Nutné pro registraci
CAPTCHA = 'hroch' CAPTCHA = 'hroch'

47
server/constraints.txt

@ -0,0 +1,47 @@
bcrypt==4.0.0
bleach==5.0.1
blinker==1.5
click==8.1.3
dateutils==0.6.12
deprecation==2.1.0
dominate==2.7.0
Flask==2.2.2
Flask-Bootstrap==3.3.7.1
Flask-SQLAlchemy==2.5.1
Flask-WTF==1.0.1
greenlet==1.1.3
itsdangerous==2.1.2
Jinja2==3.1.2
lxml==4.9.1
MarkupSafe==2.1.1
mypy==0.971
mypy-extensions==0.4.3
packaging==21.3
pikepdf==5.6.1
Pillow==9.2.0
psycopg2==2.9.3
pyparsing==3.0.9
python-dateutil==2.8.2
pytz==2022.2.1
pyzbar==0.1.9
six==1.16.0
SQLAlchemy==1.4.41
sqlalchemy-stubs==0.4
sqlalchemy2-stubs==0.0.2a27
tomli==2.0.1
types-bleach==5.0.3
types-Flask-SQLAlchemy==2.5.9
types-Markdown==3.4.1
types-Pillow==9.2.1
types-python-dateutil==2.8.19
types-requests==2.28.10
types-setuptools==65.3.0
types-SQLAlchemy==1.4.51
types-urllib3==1.26.24
typing_extensions==4.3.0
uwsgidecorators==1.1.0
visitor==0.1.3
webencodings==0.5.1
Werkzeug==2.2.2
WTForms==3.0.1
wtforms-html5==0.6.1

54
server/hra/db.py

@ -0,0 +1,54 @@
from sqlalchemy import \
Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, \
text, func, \
create_engine, inspect, select
from sqlalchemy.engine import Engine
from sqlalchemy.orm import relationship, sessionmaker, Session, class_mapper, joinedload, aliased
from sqlalchemy.orm.attributes import get_history
from sqlalchemy.orm.query import Query
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql.expression import CTE
from sqlalchemy.sql.functions import ReturnTypeFromArgs
from sqlalchemy.sql.sqltypes import Numeric
from typing import Any, Optional, List, Tuple
import hra.config as config
Base = declarative_base()
metadata = Base.metadata
_engine: Optional[Engine] = None
_session: Optional[Session] = None
flask_db: Any = None
def get_engine() -> Engine:
global _engine
if _engine is None:
_engine = create_engine(config.SQLALCHEMY_DATABASE_URI, echo=config.SQLALCHEMY_ECHO)
return _engine
def get_session() -> Session:
global _session
if flask_db:
return flask_db.session
if _session is None:
MOSession = sessionmaker(bind=get_engine())
_session = MOSession()
return _session
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
org = Column(Boolean)
username = Column(String(80), unique=True, nullable=False)
passwd = Column(String(80), nullable=False)
def __repr__(self):
return '<User %r>' % self.username

7
server/hra/util.py

@ -0,0 +1,7 @@
import bcrypt
def hash_passwd(a):
salt = b'$2b$12$V2aIKSJC/uozaodwYnQX3e'
hashed = bcrypt.hashpw(a.encode('utf-8'), salt)
return hashed.decode('us-ascii')

272
server/hra/web/__init__.py

@ -1,4 +1,4 @@
from flask import Flask, redirect, flash, render_template, session, g, request from flask import Flask, redirect, flash, render_template, session, g, request, get_flashed_messages
from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
@ -16,19 +16,8 @@ from wtforms.fields import EmailField
from wtforms.widgets import NumberInput from wtforms.widgets import NumberInput
import hra.config as config import hra.config as config
import hra.web.html as html
import hra.db as db
class OptionalIntField(wtforms.IntegerField):
widget = NumberInput()
def process_formdata(self, valuelist):
self.data = None
if valuelist:
if valuelist[0]:
try:
self.data = int(valuelist[0])
except ValueError:
raise wtforms.ValidationError('Nejedná se o číslo.')
import logging import logging
@ -40,13 +29,14 @@ app = Flask(__name__, static_folder=static_dir)
app.config.from_object(config) app.config.from_object(config)
Bootstrap(app) Bootstrap(app)
db = SQLAlchemy(app)
db.flask_db = SQLAlchemy(app, metadata=db.metadata)
class NeedLoginError(werkzeug.exceptions.Forbidden): class NeedLoginError(werkzeug.exceptions.Forbidden):
description = 'Need to log in' description = 'Need to log in'
class MenuItem: class MenuItem:
url: str url: str
name: str name: str
@ -65,14 +55,12 @@ def init_request():
return return
if 'uid' in session: if 'uid' in session:
user = db.session.query(Users).filter_by(id=session['uid']).first() user = db.get_session().query(db.User).filter_by(id=session['uid']).first()
else: else:
user = None user = None
path = request.path path = request.path
if path.startswith('/org/'): if path.startswith('/org/'):
if not user: if not user or not user.org:
raise werkzeug.exceptions.Forbidden()
if not user.org:
raise werkzeug.exceptions.Forbidden() raise werkzeug.exceptions.Forbidden()
g.user = user g.user = user
@ -98,250 +86,6 @@ def init_request():
app.before_request(init_request) app.before_request(init_request)
class Users(db.Model):
id = db.Column(db.Integer, primary_key=True)
org = db.Column(db.Boolean)
username = db.Column(db.String(80), unique=True, nullable=False)
passwd = db.Column(db.String(80), nullable=False)
def __repr__(self):
return '<User %r>' % self.username
def code_points(code, time):
r = 1
if code in bonus:
for b in bonus[code]:
if not b.is_out(time):
r = max(r, b.eval(time))
return r
class Findings(db.Model):
id = db.Column(db.Integer, primary_key=True)
user = db.Column(db.Integer, db.ForeignKey('users.id'))
round = db.Column(db.Integer)
code = db.Column(db.String(10))
time = db.Column(db.Integer)
delete = db.Column(db.Boolean, nullable=False, default=False)
def points(self):
return code_points(self.code, self.time)
class ActualRound(db.Model):
id = db.Column(db.Integer, primary_key=True)
class Round(db.Model):
id = db.Column(db.Integer, primary_key=True)
start_time = db.Column(db.DateTime)
# db.create_all()
# if db.session.query(ActualRound).one_or_none() is None:
# db.session.add(ActualRound(id=0))
# db.session.commit()
class RegistrationForm(FlaskForm):
username = StringField('Jméno týmu', [validators.Length(min=2, max=25), validators.DataRequired()])
captcha = StringField('Captcha', validators=[validators.DataRequired()])
passwd = PasswordField('Heslo', [
validators.DataRequired(),
validators.EqualTo('confirm', message='Passwords must match')
])
confirm = PasswordField('Heslo znovu', validators=[validators.DataRequired()])
submit = SubmitField("Založit")
def validate_captcha(form, field):
if field.data != config.CAPTCHA:
raise ValidationError("Chyba!")
class LoginForm(FlaskForm):
username = StringField('Jméno týmu', [validators.DataRequired()])
passwd = PasswordField('Heslo', [validators.DataRequired()])
submit = SubmitField("Přihlásit")
def hash_passwd(a):
salt = b'$2b$12$V2aIKSJC/uozaodwYnQX3e'
hashed = bcrypt.hashpw(a.encode('utf-8'), salt)
return hashed.decode('us-ascii')
@app.route("/registration", methods=['GET', 'POST'])
def registration():
f = RegistrationForm()
if f.validate_on_submit():
u = Users(org=False, username=f.username.data, passwd=hash_passwd(f.passwd.data))
try:
db.session.add(u)
db.session.commit()
except exc.IntegrityError:
flash("Uživatelské jméno již existuje")
return render_template('registration.html', form=f)
flash("Přidán nový uživatel.", 'success')
return redirect("login")
return render_template('registration.html', form=f)
@app.route("/login", methods=['GET', 'POST'])
def login():
f = LoginForm()
if f.validate_on_submit():
p_hash=hash_passwd(f.passwd.data)
user = db.session.query(Users).filter_by(username=f.username.data).one_or_none()
print(user, p_hash)
if user and user.passwd == p_hash:
session.clear()
session['uid'] = user.id
flash("Přihlášení hotovo.", 'success')
return redirect("/")
flash("Chybné jméno nebo heslo.", 'danger')
return render_template('login.html', form=f)
@app.route("/logout")
def logout():
session.clear()
return redirect('/')
@app.template_filter()
def none_as_minus(x):
return x if x is not None else '-'
@app.template_filter()
def round_points(x):
if x is None:
return None
return round(x,3)
@app.template_filter()
def print_time(t):
if t == None:
return "-"
return ("-" if t<0 else "") + str(abs(t)//1000//60) + ":" + ("00{0:d}".format(abs(t)//1000%60))[-2:]
@app.route("/", methods=['GET', 'POST'])
def web_index():
return render_template('index.html')
@app.route("/org/users")
def web_users():
users = db.session.query(Users).all()
return render_template("org_users.html", users=users)
@app.route("/org/user/<int:user_id>", methods=['GET', 'POST'])
def web_org_user(user_id):
user = db.session.query(Users).filter_by(id=user_id).one_or_none()
f = FindingForm()
del f.user
if f.validate_on_submit():
f.fill_empty()
find = Findings(user=user.id, code=f.f_code, time=f.f_time, round=get_round_id())
db.session.add(find)
db.session.commit()
flash(f"Kód {find.code}… přijat", 'success')
f.code.data = ""
return redirect(f"/org/user/{user_id}")
if not user:
raise werkzeug.exceptions.NotFound()
calc_point(user)
findings = db.session.query(Findings).filter_by(user=user.id, round=get_round_id()).order_by(Findings.time).all()
return render_template("org_user.html", user=user, findings=findings, form=f)
@app.route("/org/admin", methods=['GET', 'POST'])
def web_admin():
obj_round = get_round()
f_round = FormRound(obj=obj_round, prefix="r")
if f_round.validate_on_submit():
f_round.populate_obj(obj_round)
db.session.commit()
return render_template("org_admin.html", f_round=f_round)
@app.route("/org/act", methods=['GET', 'POST'])
def web_act():
obj_act_round = db.session.query(ActualRound).one()
f_act_round = FormActRound(obj=obj_act_round, prefix="act")
if f_act_round.validate_on_submit():
f_act_round.populate_obj(obj_act_round)
db.session.commit()
return render_template("org_act.html", f_act_round=f_act_round)
class SuForm(FlaskForm):
username = StringField('Uživatel')
time_move = OptionalIntField('Časový posun')
round = OptionalIntField('Kolo')
submit = SubmitField("Změnit")
def validate_username(form, field):
form.f_user = db.session.query(Users).filter_by(username=field.data).one_or_none()
if form.f_user == None and field.data:
raise ValidationError("Uživatel neexistuje.")
@app.route("/org/su", methods=['GET', 'POST'])
def web_su():
f = SuForm()
if not f.is_submitted():
f.username.data = g.user.username
if f.validate_on_submit():
if not f.f_user or session['uid'] != f.f_user.id:
session['uid'] = f.f_user.id if f.f_user else None
flash("Uživatel vtělen!")
if f.time_move.data is not None:
session['time_move'] = f.time_move.data * 1000
flash("Čas vtělen!")
if f.round.data is not None:
session['round'] = f.round.data
flash("Kolo vtěleno!")
return redirect('/')
return render_template("org_su.html", f=f)
from hra.web.html import *
@app.route("/test", methods=['GET', 'POST'])
def test():
b = HtmlBuilder()
with b.head():
b.title()("KSP hra")
b.meta(name="viewport", content="width=device-width, initial-scale=1.0")
b.link(rel="stylesheet", href=app.url_for('static', filename='bootstrap.min.css'), type='text/css', media="all")
b.link(rel="stylesheet", href=app.url_for('static', filename='ksp-mhd.css'), type='text/css', media="all")
# b.link(rel="stylesheet", href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css", integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm", crossorigin="anonymous")
b.link(rel="icon", type="image/png", sizes="16x16", href="static/favicon.ico")
b.link(rel="shortcut icon", href=app.url_for('static', filename='img/favicon.ico'))
# b.link(rel="stylesheet", href="https://mo.mff.cuni.cz/osmo/assets/a373a8f2/mo.css", type='text/css', media="all")
with b.body():
with b.header(_class=f"flavor-{config.WEB_FLAVOR}"):
with b.div(_class="content"):
with b.a(href="/", title="Na hlavní stránku"):
b.img(src=app.url_for('static', filename='hippo.png'), style="width: 60px;height: auto;", alt="KSP")
b.h1()("Hra na soustředění KSP")
with b.div(id="nav-wrapper"):
with b.nav(id="main-menu", _class="content"):
for item in g.menu:
b.a()(item.name)
if g.user:
b.a(_class="right", href="/")(f"Přihlášen: {{ g.user.username }}")
b.a(_class="right", href="/logout")(f"Odhlásit")
else:
b.a(_class="right", href="/login")(f"Přihlásit")
b.a(_class="right", href="/registration")(f"Registrovat")
return b._print_file()
import hra.web.pages

61
server/hra/web/html.py

@ -1,6 +1,8 @@
from flask import Markup, escape from flask import Markup, escape
from typing import List, Optional, Union, Tuple from typing import List, Optional, Union, Tuple
import threading
tag_names = ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "head", "header", "hgroup", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "link", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]
class EscapeError(RuntimeError): class EscapeError(RuntimeError):
@ -70,10 +72,10 @@ class Tag:
if isinstance(i, Tag): if isinstance(i, Tag):
i.serialize_append_to_list(out, indent) i.serialize_append_to_list(out, indent)
elif isinstance(i, Markup): elif isinstance(i, Markup):
for j in i.__html__.split("\n"): for j in i.__html__().split("\n"):
out.append(indent_str*indent + j + "\n") out.append(indent_str*indent + j + "\n")
else: else:
for j in str(i).split("\n"): for j in escape(str(i)).split("\n"):
out.append(indent_str*indent + j + "\n") out.append(indent_str*indent + j + "\n")
indent -= 1 indent -= 1
out.append(indent_str*indent + f"</{escape_tag_name(self.name)}>\n") out.append(indent_str*indent + f"</{escape_tag_name(self.name)}>\n")
@ -95,6 +97,7 @@ class Tag:
raise RuntimeError("Duplicit __enter__") raise RuntimeError("Duplicit __enter__")
self.before_tag = self.builder._current_tag self.before_tag = self.builder._current_tag
self.builder._current_tag = self self.builder._current_tag = self
return self
def __exit__(self, exc_type, exc_value, exc_traceback): def __exit__(self, exc_type, exc_value, exc_traceback):
if self.before_tag is None: if self.before_tag is None:
@ -112,9 +115,9 @@ class Builder:
def _tag(self, name: str, attributes: List[Tuple[str, str]] = []): def _tag(self, name: str, attributes: List[Tuple[str, str]] = []):
return self._current_tag.add_tag(name, attributes) return self._current_tag.add_tag(name, attributes)
def __call__(self, x: Element): def __call__(self, *arg, **kvarg):
self._current_tag.add(x) self._current_tag(*arg, **kvarg)
return x return self
def _print(self): def _print(self):
return self._root_tag.print() return self._root_tag.print()
@ -122,14 +125,20 @@ class Builder:
def _print_file(self): def _print_file(self):
return self._root_tag.print_file() return self._root_tag.print_file()
for name in tag_names:
def run(name):
def l(self, **kvarg):
return self._tag(name, [(remove_leading_underscore(k), v) for k, v in kvarg.items()])
setattr(Builder, name, l)
run(name)
class HtmlBuilder(Builder): class HtmlBuilder(Builder):
def __init__(self): def __init__(self):
super().__init__(Tag(self, "html", [])) super().__init__(Tag(self, "html", []))
tag_names = ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "head", "header", "hgroup", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "link", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]
def remove_leading_underscore(s): def remove_leading_underscore(s):
if s == "": if s == "":
return s return s
@ -138,9 +147,31 @@ def remove_leading_underscore(s):
return s return s
for name in tag_names: class WrapAfterBuilder(Builder):
def run(name): def __init__(self, f):
def l(self, **kvarg): super().__init__(Tag(self, "root", []))
return self._tag(name, [(remove_leading_underscore(k), v) for k, v in kvarg.items()]) self._wrap_done = False
setattr(HtmlBuilder, name, l) self._wrap_function = f
run(name)
def _wrap(self, *arg, **kvarg):
if self._wrap_done:
return
self._wrap_done = True
content = self._root_tag.content
self._root_tag = None
self._current_tag = None
self._root_tag = self._wrap_function(self, content, *arg, **kvarg) or self._root_tag
def _print(self, *arg, **kvarg):
self._wrap()
return super()._print(*arg, **kvarg)
def _print_file(self, *arg, **kvarg):
self._wrap()
return super()._print_file(*arg, **kvarg)
def WrapAfterBuilder_decorator(f):
def l():
return WrapAfterBuilder(f)
return l

3
server/hra/web/jinja_mac.py

@ -0,0 +1,3 @@
from hra.web import app
quick_form = app.jinja_env.get_template("bootstrap/wtf.html").module.quick_form

248
server/hra/web/pages.py

@ -0,0 +1,248 @@
from flask import Flask, redirect, flash, render_template, session, g, request, get_flashed_messages
from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField
from wtforms.validators import ValidationError
from flask_wtf import FlaskForm
from flask_bootstrap import Bootstrap
import time
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import exc, update
import hashlib
import bcrypt
import os
import werkzeug.exceptions
import wtforms
from wtforms.fields import EmailField
from wtforms.widgets import NumberInput
import hra.config as config
import hra.web.html as html
import hra.db as db
from hra.web import app
import hra.web.jinja_mac as jinja_mac
from hra.util import hash_passwd
@html.WrapAfterBuilder_decorator
def BasePage(b, content):
b._current_tag = html.Tag(b, "html", [])
b._root_tag = b._current_tag
with b.head():
b.title()("KSP hra")
b.meta(name="viewport", content="width=device-width, initial-scale=1.0")
b.link(rel="stylesheet", href=app.url_for('static', filename='bootstrap.min.css'), type='text/css', media="all")
b.link(rel="stylesheet", href=app.url_for('static', filename='ksp-mhd.css'), type='text/css', media="all")
b.link(rel="icon", type="image/png", sizes="16x16", href="static/favicon.ico")
b.link(rel="shortcut icon", href=app.url_for('static', filename='img/favicon.ico'))
with b.body() as body:
with b.header(_class=f"flavor-{config.WEB_FLAVOR}"):
with b.div(_class="content"):
with b.a(href="/", title="Na hlavní stránku"):
b.img(src=app.url_for('static', filename='hippo.png'), style="width: 60px;height: auto;", alt="KSP")
b.h1()("Hra na soustředění KSP")
with b.div(id="nav-wrapper"):
with b.nav(id="main-menu", _class="content"):
for item in g.menu:
b.a(href=item.url)(item.name)
if g.user:
b.a(_class="right", href="/")(f"Přihlášen: {g.user.username}")
b.a(_class="right", href="/logout")(f"Odhlásit")
else:
b.a(_class="right", href="/login")(f"Přihlásit")
b.a(_class="right", href="/registration")(f"Registrovat")
with b.main():
messages = get_flashed_messages(with_categories=True)
if messages:
for category, message in messages:
if category == "message":
category = warning
b.div(_class=f"alert alert-{category}", role="alert")(message)
b(*content)
class OptionalIntField(wtforms.IntegerField):
widget = NumberInput()
def process_formdata(self, valuelist):
self.data = None
if valuelist:
if valuelist[0]:
try:
self.data = int(valuelist[0])
except ValueError:
raise wtforms.ValidationError('Nejedná se o číslo.')
class RegistrationForm(FlaskForm):
username = StringField('Jméno týmu', [validators.Length(min=2, max=25), validators.DataRequired()])
captcha = StringField('Captcha', validators=[validators.DataRequired()])
passwd = PasswordField('Heslo', [
validators.DataRequired(),
validators.EqualTo('confirm', message='Passwords must match')
])
confirm = PasswordField('Heslo znovu', validators=[validators.DataRequired()])
submit = SubmitField("Založit")
def validate_captcha(form, field):
if field.data != config.CAPTCHA:
raise ValidationError("Chyba!")
class LoginForm(FlaskForm):
username = StringField('Jméno týmu', [validators.DataRequired()])
passwd = PasswordField('Heslo', [validators.DataRequired()])
submit = SubmitField("Přihlásit")
@app.route("/registration", methods=['GET', 'POST'])
def registration():
f = RegistrationForm()
if f.validate_on_submit():
u = db.User(org=False, username=f.username.data, passwd=hash_passwd(f.passwd.data))
try:
db.get_session().add(u)
db.get_session().commit()
except exc.IntegrityError:
flash("Uživatelské jméno již existuje")
return render_template('registration.html', form=f)
flash("Přidán nový uživatel.", 'success')
return redirect("login")
b = BasePage()
b(jinja_mac.quick_form(f, form_type='horizontal'))
return b._print_file()
@app.route("/login", methods=['GET', 'POST'])
def login():
f = LoginForm()
if f.validate_on_submit():
p_hash=hash_passwd(f.passwd.data)
user = db.get_session().query(db.User).filter_by(username=f.username.data).one_or_none()
print(user, p_hash)
if user and user.passwd == p_hash:
session.clear()
session['uid'] = user.id
flash("Přihlášení hotovo.", 'success')
return redirect("/")
flash("Chybné jméno nebo heslo.", 'danger')
b = BasePage()
b(jinja_mac.quick_form(f, form_type='horizontal'))
return b._print_file()
@app.route("/logout")
def logout():
session.clear()
return redirect('/')
@app.template_filter()
def none_as_minus(x):
return x if x is not None else '-'
@app.template_filter()
def round_points(x):
if x is None:
return None
return round(x,3)
@app.template_filter()
def print_time(t):
if t == None:
return "-"
return ("-" if t<0 else "") + str(abs(t)//1000//60) + ":" + ("00{0:d}".format(abs(t)//1000%60))[-2:]
@app.route("/", methods=['GET', 'POST'])
def web_index():
b = BasePage()
return b._print_file()
@app.route("/org/users")
def web_users():
users = db.get_session().query(db.User).all()
return render_template("org_users.html", users=users)
@app.route("/org/user/<int:user_id>", methods=['GET', 'POST'])
def web_orgf_user(user_id):
user = db.get_session().query(db.User).filter_by(id=user_id).one_or_none()
f = FindingForm()
del f.user
if f.validate_on_submit():
f.fill_empty()
find = Findings(user=user.id, code=f.f_code, time=f.f_time, round=get_round_id())
db.get_session().add(find)
db.get_session().commit()
flash(f"Kód {find.code}… přijat", 'success')
f.code.data = ""
return redirect(f"/org/user/{user_id}")
if not user:
raise werkzeug.exceptions.NotFound()
calc_point(user)
findings = db.get_session().query(Findings).filter_by(user=user.id, round=get_round_id()).order_by(Findings.time).all()
return render_template("org_user.html", user=user, findings=findings, form=f)
@app.route("/org/admin", methods=['GET', 'POST'])
def web_admin():
obj_round = get_round()
f_round = FormRound(obj=obj_round, prefix="r")
if f_round.validate_on_submit():
f_round.populate_obj(obj_round)
db.get_session().commit()
return render_template("org_admin.html", f_round=f_round)
@app.route("/org/act", methods=['GET', 'POST'])
def web_act():
obj_act_round = db.get_session().query(ActualRound).one()
f_act_round = FormActRound(obj=obj_act_round, prefix="act")
if f_act_round.validate_on_submit():
f_act_round.populate_obj(obj_act_round)
db.get_session().commit()
return render_template("org_act.html", f_act_round=f_act_round)
class SuForm(FlaskForm):
username = StringField('Uživatel')
time_move = OptionalIntField('Časový posun')
round = OptionalIntField('Kolo')
submit = SubmitField("Změnit")
def validate_username(form, field):
form.f_user = db.get_session().query(db.User).filter_by(username=field.data).one_or_none()
if form.f_user == None and field.data:
raise ValidationError("Uživatel neexistuje.")
@app.route("/org/su", methods=['GET', 'POST'])
def web_su():
f = SuForm()
if not f.is_submitted():
f.username.data = g.user.username
if f.validate_on_submit():
if not f.f_user or session['uid'] != f.f_user.id:
session['uid'] = f.f_user.id if f.f_user else None
flash("Uživatel vtělen!")
if f.time_move.data is not None:
session['time_move'] = f.time_move.data * 1000
flash("Čas vtělen!")
if f.round.data is not None:
session['round'] = f.round.data
flash("Kolo vtěleno!")
return redirect('/')
return render_template("org_su.html", f=f)

2
server/setup.py

@ -8,6 +8,8 @@ setuptools.setup(
description='Hra na soustředění KSP', description='Hra na soustředění KSP',
packages=['hra', 'hra/web'], packages=['hra', 'hra/web'],
scripts=[ scripts=[
"bin/db_init",
"bin/create_root",
], ],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,

Loading…
Cancel
Save