diff --git a/server/bin/create_root b/server/bin/create_root new file mode 100755 index 0000000..e274407 --- /dev/null +++ b/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.") diff --git a/server/bin/db_init b/server/bin/db_init new file mode 100755 index 0000000..c51b010 --- /dev/null +++ b/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() diff --git a/server/config.py.example b/server/config.py.example index 297136a..6dea82c 100644 --- a/server/config.py.example +++ b/server/config.py.example @@ -1,6 +1,9 @@ # Patří do hra/config.py -SQLALCHEMY_DATABASE_URI = "postgresql:///ksp-strathra" +# Druh webu (devel/test/pub), z toho CSS třída elementu
+WEB_FLAVOR = 'devel' + +SQLALCHEMY_DATABASE_URI = f"postgresql:///ksp_strathra_{WEB_FLAVOR}" SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ECHO = False @@ -9,8 +12,6 @@ SECRET_KEY = "8aeffbdf14d441f40359708cbbae9b47926db8b08e4ea9e0cdf78071bee7b788" SESSION_COOKIE_NAME = 'ksp_strathra__session' -# Druh webu (devel/test/pub), z toho CSS třída elementu
-WEB_FLAVOR = 'devel' # Nutné pro registraci CAPTCHA = 'hroch' diff --git a/server/constraints.txt b/server/constraints.txt new file mode 100644 index 0000000..e5bacea --- /dev/null +++ b/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 diff --git a/server/hra/db.py b/server/hra/db.py new file mode 100644 index 0000000..5b8eb32 --- /dev/null +++ b/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 '' % self.username + + diff --git a/server/hra/util.py b/server/hra/util.py new file mode 100644 index 0000000..c578a72 --- /dev/null +++ b/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') + diff --git a/server/hra/web/__init__.py b/server/hra/web/__init__.py index 2eb39f4..f888eea 100644 --- a/server/hra/web/__init__.py +++ b/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.validators import ValidationError from flask_wtf import FlaskForm @@ -16,19 +16,8 @@ from wtforms.fields import EmailField from wtforms.widgets import NumberInput import hra.config as config - - -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 hra.web.html as html +import hra.db as db import logging @@ -40,13 +29,14 @@ app = Flask(__name__, static_folder=static_dir) app.config.from_object(config) Bootstrap(app) -db = SQLAlchemy(app) +db.flask_db = SQLAlchemy(app, metadata=db.metadata) class NeedLoginError(werkzeug.exceptions.Forbidden): description = 'Need to log in' + class MenuItem: url: str name: str @@ -65,14 +55,12 @@ def init_request(): return 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: user = None path = request.path if path.startswith('/org/'): - if not user: - raise werkzeug.exceptions.Forbidden() - if not user.org: + if not user or not user.org: raise werkzeug.exceptions.Forbidden() g.user = user @@ -98,250 +86,6 @@ def 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 '' % 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/", 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 diff --git a/server/hra/web/html.py b/server/hra/web/html.py index 1432b6c..42f7725 100644 --- a/server/hra/web/html.py +++ b/server/hra/web/html.py @@ -1,6 +1,8 @@ from flask import Markup, escape 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): @@ -31,7 +33,7 @@ class Tag: is_paired: bool builder: 'Builder' - def __init__(self, builder, name:str, attributes: List[Tuple[str, str]]): + def __init__(self, builder, name: str, attributes: List[Tuple[str, str]]): self.builder = builder self.name = name self.attributes = attributes @@ -44,7 +46,7 @@ class Tag: self.content.append(x) def add_attribute(k, v): - self.attributes.append((k,v)) + self.attributes.append((k, v)) def __call__(self, *arg, **kvarg): for i in arg: @@ -53,7 +55,7 @@ class Tag: self.add_attribute(remove_leading_underscore(k), v) return self - def add_tag(self, name:str, attributes: List[Tuple[str, str]]): + def add_tag(self, name: str, attributes: List[Tuple[str, str]]): t = Tag(self.builder, name, attributes) self.add(t) return t @@ -62,7 +64,7 @@ class Tag: return " ".join(f'{escape_attribute_name(i[0])}="{escape_attribute(i[1])}"' for i in self.attributes) def serialize_append_to_list(self, out, indent): - indent_str = " " + indent_str = " " if self.is_paired: out.append(indent_str*indent + f"<{escape_tag_name(self.name)} {self.format_attributes()}>\n") indent += 1 @@ -70,10 +72,10 @@ class Tag: if isinstance(i, Tag): i.serialize_append_to_list(out, indent) 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") else: - for j in str(i).split("\n"): + for j in escape(str(i)).split("\n"): out.append(indent_str*indent + j + "\n") indent -= 1 out.append(indent_str*indent + f"\n") @@ -95,6 +97,7 @@ class Tag: raise RuntimeError("Duplicit __enter__") self.before_tag = self.builder._current_tag self.builder._current_tag = self + return self def __exit__(self, exc_type, exc_value, exc_traceback): if self.before_tag is None: @@ -109,12 +112,12 @@ class Builder: self._root_tag = tag self._current_tag = tag - 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) - def __call__(self, x: Element): - self._current_tag.add(x) - return x + def __call__(self, *arg, **kvarg): + self._current_tag(*arg, **kvarg) + return self def _print(self): return self._root_tag.print() @@ -122,14 +125,20 @@ class Builder: def _print_file(self): 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): def __init__(self): 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): if s == "": return s @@ -138,9 +147,31 @@ def remove_leading_underscore(s): return s -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(HtmlBuilder, name, l) - run(name) +class WrapAfterBuilder(Builder): + def __init__(self, f): + super().__init__(Tag(self, "root", [])) + self._wrap_done = False + self._wrap_function = f + + 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 diff --git a/server/hra/web/jinja_mac.py b/server/hra/web/jinja_mac.py new file mode 100644 index 0000000..079830f --- /dev/null +++ b/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 diff --git a/server/hra/web/pages.py b/server/hra/web/pages.py new file mode 100644 index 0000000..5adae33 --- /dev/null +++ b/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/", 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) + + diff --git a/server/setup.py b/server/setup.py index 6b5ade0..e560876 100644 --- a/server/setup.py +++ b/server/setup.py @@ -8,6 +8,8 @@ setuptools.setup( description='Hra na soustředění KSP', packages=['hra', 'hra/web'], scripts=[ + "bin/db_init", + "bin/create_root", ], include_package_data=True, zip_safe=False,