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