Strategická: Reforma webu
This commit is contained in:
parent
5f3da84f71
commit
e2d353d48a
11 changed files with 481 additions and 287 deletions
23
server/bin/create_root
Executable file
23
server/bin/create_root
Executable file
|
@ -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
Executable file
34
server/bin/db_init
Executable file
|
@ -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()
|
|
@ -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 <header>
|
||||
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 <header>
|
||||
WEB_FLAVOR = 'devel'
|
||||
|
||||
# Nutné pro registraci
|
||||
CAPTCHA = 'hroch'
|
||||
|
|
47
server/constraints.txt
Normal file
47
server/constraints.txt
Normal file
|
@ -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
Normal file
54
server/hra/db.py
Normal file
|
@ -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
Normal file
7
server/hra/util.py
Normal file
|
@ -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')
|
||||
|
|
@ -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 '<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
|
||||
|
|
|
@ -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"</{escape_tag_name(self.name)}>\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
|
||||
|
|
3
server/hra/web/jinja_mac.py
Normal file
3
server/hra/web/jinja_mac.py
Normal file
|
@ -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
Normal file
248
server/hra/web/pages.py
Normal file
|
@ -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)
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue