|
|
@ -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 |
|
|
|