Jiří Kalvoda
2 years ago
11 changed files with 481 additions and 287 deletions
@ -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.") |
@ -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() |
@ -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 |
@ -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 |
||||
|
|
||||
|
|
@ -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') |
||||
|
|
@ -0,0 +1,3 @@ |
|||||
|
from hra.web import app |
||||
|
|
||||
|
quick_form = app.jinja_env.get_template("bootstrap/wtf.html").module.quick_form |
@ -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) |
||||
|
|
||||
|
|
Loading…
Reference in new issue