Rewrite data layer using SqlAlchemy Part 1
37
.editorconfig
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
# Matches multiple files with brace expansion notation
|
||||||
|
# Set default charset
|
||||||
|
[*.{js,py}]
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
# 4 space indentation
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Tab indentation (no size specified)
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.{js,ts}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Matches the exact files either package.json or .travis.yml
|
||||||
|
[{package.json,.travis.yml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.html]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
530
app.py
@@ -1,530 +0,0 @@
|
|||||||
import functools
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import Flask, g, render_template, request, redirect, session, url_for
|
|
||||||
from flask_bootstrap import Bootstrap
|
|
||||||
|
|
||||||
import db
|
|
||||||
import forms
|
|
||||||
import models
|
|
||||||
import util
|
|
||||||
from config import Config
|
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(filename=Config.LOG_PATH, level=logging.DEBUG)
|
|
||||||
|
|
||||||
logging.info(f"Starting in working dir: {os.getcwd()}")
|
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
|
||||||
app = Flask(__name__)
|
|
||||||
Bootstrap(app)
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
|
|
||||||
app.config.from_object(Config)
|
|
||||||
|
|
||||||
|
|
||||||
@app.template_filter("format_time")
|
|
||||||
def format_time(value):
|
|
||||||
"""Make the datetime to time string formatting available to jinja2"""
|
|
||||||
if value is None:
|
|
||||||
return ""
|
|
||||||
return util.datetime_time_str(value)
|
|
||||||
|
|
||||||
|
|
||||||
@app.template_filter("format_timedelta")
|
|
||||||
def format_timedelta(value):
|
|
||||||
"""Make formatting for timedeltas available to jinja2"""
|
|
||||||
if value is None:
|
|
||||||
return ""
|
|
||||||
return util.timedelta_to_str(value)
|
|
||||||
|
|
||||||
|
|
||||||
@app.cli.command("initdb")
|
|
||||||
def init_db_command():
|
|
||||||
"""Initializes the database."""
|
|
||||||
db.init_db()
|
|
||||||
|
|
||||||
|
|
||||||
@app.teardown_appcontext
|
|
||||||
def close_connection(exception):
|
|
||||||
db = getattr(g, "_database", None)
|
|
||||||
if db is not None:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
|
|
||||||
def set_user_role(data):
|
|
||||||
"""Set the users role in the flask g object for later usage"""
|
|
||||||
g.is_editor = data == "editor"
|
|
||||||
|
|
||||||
|
|
||||||
def authorize(func):
|
|
||||||
@functools.wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
try:
|
|
||||||
set_user_role(session["user_type"])
|
|
||||||
except KeyError:
|
|
||||||
return redirect("/login")
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_type(password):
|
|
||||||
# TODO password hashing?
|
|
||||||
if password == Config.WRITE_PW:
|
|
||||||
return "editor"
|
|
||||||
if password == Config.READ_PW:
|
|
||||||
return "readonly"
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/login", methods=["GET", "POST"])
|
|
||||||
def login():
|
|
||||||
if request.method == "GET":
|
|
||||||
return render_template("login.html")
|
|
||||||
else:
|
|
||||||
user_type = get_user_type(request.form.get("password"))
|
|
||||||
if not user_type:
|
|
||||||
return redirect("/login")
|
|
||||||
session["user_type"] = user_type
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/logout")
|
|
||||||
def logout():
|
|
||||||
session.pop("role", None)
|
|
||||||
return redirect("login")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
@authorize
|
|
||||||
def landing():
|
|
||||||
return redirect("/season")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season")
|
|
||||||
@authorize
|
|
||||||
def season_list():
|
|
||||||
sql, args = db.load_season()
|
|
||||||
results = db.query_db(sql, args, cls=models.Season)
|
|
||||||
model = {
|
|
||||||
"seasons": results,
|
|
||||||
"columns": [
|
|
||||||
("code", "#"),
|
|
||||||
("game", "Game"),
|
|
||||||
("description", "Season Description"),
|
|
||||||
("start", "Started At"),
|
|
||||||
("end", "Ended At"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
return render_template("season_list.html", model=model)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season/new", methods=["GET"])
|
|
||||||
@authorize
|
|
||||||
def season_new():
|
|
||||||
form = forms.SeasonForm()
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="New Season",
|
|
||||||
form_title="Create New Season",
|
|
||||||
post_url="/season/edit/null",
|
|
||||||
)
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season/edit/<season_id>", methods=["GET", "POST"])
|
|
||||||
@authorize
|
|
||||||
def season_edit(season_id: int):
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="Seasons",
|
|
||||||
form_title="Edit Season",
|
|
||||||
post_url=f"/season/edit/{season_id}",
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
sql, args = db.load_season(season_id)
|
|
||||||
season: models.Season = db.query_db(sql, args, one=True, cls=models.Season)
|
|
||||||
|
|
||||||
form = forms.SeasonForm()
|
|
||||||
form.season_id.data = season.id
|
|
||||||
form.code.data = season.code
|
|
||||||
form.game_name.data = season.game
|
|
||||||
form.description.data = season.description
|
|
||||||
form.start.data = season.start
|
|
||||||
form.end.data = season.end
|
|
||||||
|
|
||||||
model.form_title = f"Edit Season '{season.code}: {season.game}'"
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
else:
|
|
||||||
form = forms.SeasonForm()
|
|
||||||
|
|
||||||
if not form.validate_on_submit():
|
|
||||||
model.errors = form.errors
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
season = models.Season.from_form(form)
|
|
||||||
sql, args = db.save_season_query(season)
|
|
||||||
errors = db.update_db(sql, args)
|
|
||||||
return redirect(url_for("season_list"))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season/<season_id>", methods=["GET"])
|
|
||||||
@authorize
|
|
||||||
def season_overview(season_id: int):
|
|
||||||
sql, args = db.load_season(season_id)
|
|
||||||
season = db.query_db(sql, args, one=True, cls=models.Season)
|
|
||||||
|
|
||||||
sql, args = db.load_episodes(season.id)
|
|
||||||
episodes = db.query_db(sql, args, cls=models.Episode)
|
|
||||||
|
|
||||||
infos = {
|
|
||||||
"Number": season.code,
|
|
||||||
"Game": season.game,
|
|
||||||
"Start Date": season.start,
|
|
||||||
"End Date": season.end if season.end else "Ongoing",
|
|
||||||
}
|
|
||||||
model = {
|
|
||||||
"title": f"{season.code} {season.game}",
|
|
||||||
"season_info": infos,
|
|
||||||
"episodes": episodes,
|
|
||||||
}
|
|
||||||
return render_template("season_overview.html", model=model)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season/<season_id>/episode/<episode_id>")
|
|
||||||
@authorize
|
|
||||||
def episode_detail(season_id: int, episode_id: int):
|
|
||||||
sql, args = db.load_season(season_id)
|
|
||||||
season = db.query_db(sql, args, one=True, cls=models.Season)
|
|
||||||
sql, args = db.load_episode(episode_id)
|
|
||||||
episode = db.query_db(sql, args, one=True, cls=models.Episode)
|
|
||||||
sql, args = db.load_episode_players(episode_id)
|
|
||||||
ep_players = db.query_db(sql, args, cls=models.Player)
|
|
||||||
|
|
||||||
events = {"entries": [], "victory_count": 0, "defeat_count": 0}
|
|
||||||
|
|
||||||
model = {
|
|
||||||
"title": f"{season.code}{episode.code}",
|
|
||||||
"episode": episode,
|
|
||||||
"season": season,
|
|
||||||
"players": ep_players,
|
|
||||||
"events": events,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render_template("episode_details.html", model=model)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season/<season_id>/episode", methods=["GET"])
|
|
||||||
@authorize
|
|
||||||
def episode_list(season_id: int):
|
|
||||||
sql, args = db.load_season(season_id)
|
|
||||||
season = db.query_db(sql, args, one=True, cls=models.Season)
|
|
||||||
sql, args = db.load_episodes(season_id)
|
|
||||||
episodes = db.query_db(sql, args, cls=models.Episode)
|
|
||||||
|
|
||||||
model = {"season_id": season_id, "season_code": season.code}
|
|
||||||
return render_template("episode_list.html", model=model)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season/<season_id>/episode/new", methods=["GET"])
|
|
||||||
@authorize
|
|
||||||
def episode_new(season_id: int):
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="New Episode",
|
|
||||||
form_title="Create New Episode",
|
|
||||||
post_url=f"/season/{season_id}/episode/null/edit",
|
|
||||||
)
|
|
||||||
|
|
||||||
form = forms.EpisodeForm(request.form)
|
|
||||||
form.season_id.data = season_id
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season/<season_id>/episode/<episode_id>/edit", methods=["GET", "POST"])
|
|
||||||
@authorize
|
|
||||||
def episode_edit(season_id: int, episode_id: int):
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="Edit Episode",
|
|
||||||
form_title="Edit Episode",
|
|
||||||
post_url=f"/season/{season_id}/episode/{episode_id}/edit",
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
sql, args = db.load_episode(episode_id)
|
|
||||||
episode: models.Episode = db.query_db(sql, args, one=True, cls=models.Episode)
|
|
||||||
|
|
||||||
sql, args = db.load_episode_players(episode_id)
|
|
||||||
ep_players = db.query_db(sql, args, cls=models.Player)
|
|
||||||
|
|
||||||
form = forms.EpisodeForm()
|
|
||||||
form.season_id.data = episode.season_id
|
|
||||||
form.episode_id.data = episode.id
|
|
||||||
form.code.data = episode.code
|
|
||||||
form.date.data = episode.date
|
|
||||||
form.start.data = episode.start
|
|
||||||
form.end.data = episode.end
|
|
||||||
form.title.data = episode.title
|
|
||||||
form.players.data = [p.id for p in ep_players]
|
|
||||||
|
|
||||||
model.form_title = f"Edit Episode '{episode.code}: {episode.title}'"
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
else:
|
|
||||||
form = forms.EpisodeForm()
|
|
||||||
|
|
||||||
if not form.validate_on_submit():
|
|
||||||
model.errors = form.errors
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
errors = False
|
|
||||||
episode = models.Episode.from_form(form)
|
|
||||||
sql, args = db.save_episode(episode)
|
|
||||||
|
|
||||||
last_key = db.update_db(sql, args, return_key=True)
|
|
||||||
|
|
||||||
episode_id = episode.id if episode.id else last_key
|
|
||||||
|
|
||||||
form_ids = form.players.data
|
|
||||||
|
|
||||||
sql, args = db.load_episode_players(episode_id)
|
|
||||||
ep_players = db.query_db(sql, args, cls=models.Player)
|
|
||||||
pids = [p.id for p in ep_players]
|
|
||||||
|
|
||||||
new_ids = [pid for pid in form_ids if pid not in pids]
|
|
||||||
removed_ids = [pid for pid in pids if pid not in form_ids]
|
|
||||||
|
|
||||||
if removed_ids:
|
|
||||||
sql, args = db.remove_episode_player(episode_id, removed_ids)
|
|
||||||
errors = db.update_db(sql, args)
|
|
||||||
|
|
||||||
if new_ids:
|
|
||||||
sql, args = db.save_episode_players(episode_id, new_ids)
|
|
||||||
errors = db.update_db(sql, args)
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
model.errors = {"Error saving episode": [errors]}
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
return redirect(url_for("season_overview", season_id=season_id))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/season/<s_id>/episode/<ep_id>/event/new", methods=["GET"])
|
|
||||||
@authorize
|
|
||||||
def event_new(s_id: int, ep_id: int):
|
|
||||||
model = {
|
|
||||||
"page_title": "New Event",
|
|
||||||
"form_title": "Create New Event",
|
|
||||||
"post_url": f"/seasons/{s_id}/episodes/{ep_id}/events/null/edit",
|
|
||||||
}
|
|
||||||
|
|
||||||
return render_template("event_editor.html", model=model)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(
|
|
||||||
"/season/<s_id>/episode/<ep_id>/event/<ev_id>/edit", methods=["GET", "POST"]
|
|
||||||
)
|
|
||||||
@authorize
|
|
||||||
def event_edit(s_id: int, ep_id: int, ev_id: int):
|
|
||||||
model = {
|
|
||||||
"page_title": "Edit Event",
|
|
||||||
"form_title": "Edit Event",
|
|
||||||
"post_url": f"/season/{s_id}/episode/{ep_id}/event/{ev_id}/edit",
|
|
||||||
}
|
|
||||||
if request.method == "GET":
|
|
||||||
return render_template("event_editor.html", model=model)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/player/new", methods=["GET"])
|
|
||||||
@authorize
|
|
||||||
def player_new():
|
|
||||||
form = forms.PlayerForm()
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="Players",
|
|
||||||
form_title="Create a new Player",
|
|
||||||
post_url="/player/null/edit",
|
|
||||||
)
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/player/<player_id>/edit", methods=["GET", "POST"])
|
|
||||||
@authorize
|
|
||||||
def player_edit(player_id: int):
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="Players",
|
|
||||||
form_title=f"Edit Player",
|
|
||||||
post_url=f"/player/{player_id}/edit",
|
|
||||||
)
|
|
||||||
# Edit Existing Player
|
|
||||||
if request.method == "GET":
|
|
||||||
sql, args = db.load_players(player_id)
|
|
||||||
player = db.query_db(sql, args, one=True, cls=models.Player)
|
|
||||||
|
|
||||||
form = forms.PlayerForm()
|
|
||||||
form.player_id.data = player.id
|
|
||||||
form.anonymize.data = player.anon
|
|
||||||
form.real_name.data = player.real_name
|
|
||||||
form.alias.data = player.alias
|
|
||||||
form.hex_id.data = player.hex_id
|
|
||||||
|
|
||||||
model.form_title = f'Edit Player "{player.name}"'
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
# Save POSTed data
|
|
||||||
else:
|
|
||||||
form = forms.PlayerForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
player = models.Player.from_form(form)
|
|
||||||
res = db.save_player(player)
|
|
||||||
return redirect("/player")
|
|
||||||
|
|
||||||
model.form_title = "Incorrect Data"
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/player")
|
|
||||||
@authorize
|
|
||||||
def player_list():
|
|
||||||
sql, args = db.load_players()
|
|
||||||
players = db.query_db(sql, args, cls=models.Player)
|
|
||||||
model = {
|
|
||||||
"player_list": players,
|
|
||||||
"columns": [
|
|
||||||
("id", "ID"),
|
|
||||||
("name", "Player Name"),
|
|
||||||
("alias", "Alias"),
|
|
||||||
("hex_id", "Hex ID"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
return render_template("player_list.html", model=model)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/drink")
|
|
||||||
@authorize
|
|
||||||
def drink_list():
|
|
||||||
sql, args = db.load_drinks()
|
|
||||||
drinks = db.query_db(sql, args, cls=models.Drink)
|
|
||||||
model = {
|
|
||||||
"drinks": drinks,
|
|
||||||
"columns": [("id", "ID"), ("name", "Drink Name"), ("vol", "Alcohol %")],
|
|
||||||
"controls": [("edit", "Edit")],
|
|
||||||
}
|
|
||||||
return render_template("drink_list.html", model=model)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/drink/<drink_id>/edit", methods=["GET"])
|
|
||||||
@authorize
|
|
||||||
def drink_edit(drink_id: int):
|
|
||||||
sql, args = db.load_drinks(drink_id)
|
|
||||||
drink = db.query_db(sql, args, one=True, cls=models.Drink)
|
|
||||||
|
|
||||||
form = forms.DrinkForm()
|
|
||||||
form.drink_id.data = drink.id
|
|
||||||
form.name.data = drink.name
|
|
||||||
form.vol.data = drink.vol
|
|
||||||
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="Edit Drink",
|
|
||||||
form_title=f'Edit Drink "{drink.name}"',
|
|
||||||
post_url="/drink/save",
|
|
||||||
)
|
|
||||||
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/drink/new", methods=["GET"])
|
|
||||||
@authorize
|
|
||||||
def new_drink():
|
|
||||||
form = forms.DrinkForm()
|
|
||||||
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="New Drink",
|
|
||||||
form_title=f"Create a new Drink",
|
|
||||||
post_url="/drinks/save",
|
|
||||||
)
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/drink/save", methods=["POST"])
|
|
||||||
@authorize
|
|
||||||
def drink_save():
|
|
||||||
form = forms.DrinkForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
drink = models.Drink.from_form(form)
|
|
||||||
res = db.save_drink(drink)
|
|
||||||
return redirect("/drink")
|
|
||||||
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="Drinks", form_title="Edit Drink", post_url="/drink/save"
|
|
||||||
)
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/enemy")
|
|
||||||
@authorize
|
|
||||||
def enemy_list():
|
|
||||||
sql, args = db.load_enemies()
|
|
||||||
enemies = db.query_db(sql, args, cls=models.Enemy)
|
|
||||||
model = {"enemies": enemies}
|
|
||||||
return render_template("enemies.html", model=model)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/enemy/new", methods=["GET"])
|
|
||||||
@authorize
|
|
||||||
def enemy_new(preselect_season=None):
|
|
||||||
form = forms.EnemyForm()
|
|
||||||
|
|
||||||
if preselect_season:
|
|
||||||
form.season_id.default = preselect_season
|
|
||||||
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="Enemies",
|
|
||||||
form_title="Create a new Enemy",
|
|
||||||
post_url=f"/enemy/null/edit",
|
|
||||||
)
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/enemy/<enemy_id>/edit", methods=["GET", "POST"])
|
|
||||||
@authorize
|
|
||||||
def enemy_edit(enemy_id: int):
|
|
||||||
model = models.GenericFormModel(
|
|
||||||
page_title="Enemies",
|
|
||||||
form_title="Edit Enemy",
|
|
||||||
post_url=f"/enemy/{enemy_id}/edit",
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
sql, args = db.load_enemies(enemy_id)
|
|
||||||
enemy = db.query_db(sql, args, one=True, cls=models.Enemy)
|
|
||||||
|
|
||||||
form = forms.EnemyForm()
|
|
||||||
form.season_id.data = enemy.season_id if enemy.season_id else -1
|
|
||||||
form.name.data = enemy.name
|
|
||||||
form.is_boss.data = enemy.boss
|
|
||||||
form.enemy_id.data = enemy_id
|
|
||||||
|
|
||||||
model.form_title = f'Edit Enemy "{enemy.name}"'
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
else:
|
|
||||||
form = forms.EnemyForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
enemy = models.Enemy.from_form(form)
|
|
||||||
sql, args = db.save_enemy(enemy)
|
|
||||||
errors = db.update_db(sql, args)
|
|
||||||
|
|
||||||
if form.submit_continue_button.data:
|
|
||||||
form.name.data = None
|
|
||||||
return enemy_new(preselect_season=enemy.season_id)
|
|
||||||
return redirect("/enemy")
|
|
||||||
|
|
||||||
model.form_title = "Incorrect Data"
|
|
||||||
return render_template("generic_form.html", model=model, form=form)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run()
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
SECRET_KEY = os.environ.get("ES_SECRET_KEY")
|
|
||||||
WRITE_PW = os.environ.get("ES_WRITE_PW")
|
|
||||||
READ_PW = os.environ.get("ES_READ_PW")
|
|
||||||
DATABASE_PATH = os.environ.get("ES_DATABASE_PATH")
|
|
||||||
LOG_PATH = os.environ.get("ES_LOG_PATH")
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
export ES_READ_PW=""
|
|
||||||
export ES_WRITE_PW=""
|
|
||||||
export ES_SECRET_KEY=""
|
|
||||||
export ES_DATABASE_PATH=""
|
|
||||||
export ES_LOG_PATH=""
|
|
||||||
36
estusshots/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_bootstrap import Bootstrap
|
||||||
|
|
||||||
|
|
||||||
|
from estusshots.config import config
|
||||||
|
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
Bootstrap(app)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
if not config.SECRET_KEY:
|
||||||
|
logging.error(
|
||||||
|
"No secret key provided for app. Are the environment variables set correctly?"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
logging.basicConfig(filename=config.LOG_PATH, level=logging.DEBUG)
|
||||||
|
logging.info(f"Starting in working dir: {os.getcwd()}")
|
||||||
|
app.config.from_object(config)
|
||||||
|
|
||||||
|
import estusshots.views.drinks
|
||||||
|
import estusshots.views.enemies
|
||||||
|
import estusshots.views.episodes
|
||||||
|
import estusshots.views.events
|
||||||
|
import estusshots.views.login
|
||||||
|
import estusshots.views.players
|
||||||
|
import estusshots.views.seasons
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import models
|
from estusshots import orm, models, db
|
||||||
import db
|
from estusshots.orm import Season
|
||||||
|
|
||||||
|
|
||||||
|
def event_choices():
|
||||||
|
return [(member.value, member.name) for member in models.EventType]
|
||||||
|
|
||||||
|
|
||||||
def season_choices():
|
def season_choices():
|
||||||
""" Query the database for available seasons.
|
""" Query the database for available seasons.
|
||||||
This returns a list of tuples with the season ID and a display string.
|
This returns a list of tuples with the season ID and a display string.
|
||||||
"""
|
"""
|
||||||
sql, args = db.load_season()
|
db = orm.new_session()
|
||||||
seasons = db.query_db(sql, args, cls=models.Season)
|
seasons = db.query(Season).order_by(Season.code).all()
|
||||||
choices = [(s.id, f"{s.code}: {s.game}") for s in seasons]
|
choices = [(s.id, f"{s.code}: {s.game}") for s in seasons]
|
||||||
choices.insert(0, (-1, "No Season"))
|
choices.insert(0, (-1, "No Season"))
|
||||||
return choices
|
return choices
|
||||||
@@ -33,6 +37,15 @@ def drink_choice():
|
|||||||
return choices
|
return choices
|
||||||
|
|
||||||
|
|
||||||
|
def enemy_choice_for_season(season_id: int):
|
||||||
|
"""
|
||||||
|
Query database for all available enemies in this season
|
||||||
|
"""
|
||||||
|
sql, args = db.load_enemies_for_season(season_id)
|
||||||
|
enemies = db.query_db(sql, args, cls=models.Enemy)
|
||||||
|
return [(e.id, e.name) for e in enemies]
|
||||||
|
|
||||||
|
|
||||||
class IterableBase:
|
class IterableBase:
|
||||||
"""
|
"""
|
||||||
This is used to declare choices for WTForms SelectFields at class definition time.
|
This is used to declare choices for WTForms SelectFields at class definition time.
|
||||||
@@ -60,3 +73,8 @@ class PlayerChoiceIterable(IterableBase):
|
|||||||
class DrinkChoiceIterable(IterableBase):
|
class DrinkChoiceIterable(IterableBase):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._loader = drink_choice
|
self._loader = drink_choice
|
||||||
|
|
||||||
|
|
||||||
|
class EventChoiceIterable(IterableBase):
|
||||||
|
def __init__(self):
|
||||||
|
self._loader = event_choices
|
||||||
6
estusshots/config.ini
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[Default]
|
||||||
|
ES_READ_PW = 123
|
||||||
|
ES_WRITE_PW = 1234
|
||||||
|
ES_SECRET_KEY = 1234
|
||||||
|
ES_DATABASE_PATH = ../databases/debug.db
|
||||||
|
ES_LOG_PATH = ../logs/debug.log
|
||||||
16
estusshots/config.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self):
|
||||||
|
parser = ConfigParser()
|
||||||
|
parser.read("config.ini")
|
||||||
|
self.SECRET_KEY = parser.get("Default", "ES_SECRET_KEY")
|
||||||
|
self.WRITE_PW = parser.get("Default", "ES_WRITE_PW")
|
||||||
|
self.READ_PW = parser.get("Default", "ES_READ_PW")
|
||||||
|
self.DATABASE_PATH = parser.get("Default", "ES_DATABASE_PATH")
|
||||||
|
self.LOG_PATH = parser.get("Default", "ES_LOG_PATH")
|
||||||
|
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
|
||||||
@@ -4,8 +4,8 @@ from typing import Sequence
|
|||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
|
|
||||||
import models
|
from estusshots import models
|
||||||
from config import Config
|
from estusshots.config import config
|
||||||
|
|
||||||
|
|
||||||
class DataBaseError(Exception):
|
class DataBaseError(Exception):
|
||||||
@@ -16,8 +16,8 @@ def connect_db():
|
|||||||
"""Create a new sqlite3 connection and register it in 'g._database'"""
|
"""Create a new sqlite3 connection and register it in 'g._database'"""
|
||||||
db = getattr(g, "_database", None)
|
db = getattr(g, "_database", None)
|
||||||
if db is None:
|
if db is None:
|
||||||
log.info(f"Connecting {Config.DATABASE_PATH}")
|
log.info(f"Connecting {config.DATABASE_PATH}")
|
||||||
db = g._database = sqlite3.connect(Config.DATABASE_PATH)
|
db = g._database = sqlite3.connect(config.DATABASE_PATH)
|
||||||
|
|
||||||
db.row_factory = sqlite3.Row
|
db.row_factory = sqlite3.Row
|
||||||
return db
|
return db
|
||||||
@@ -119,7 +119,9 @@ def save_drink_query(drink):
|
|||||||
sql = "insert into drink values (?, ?, ?)"
|
sql = "insert into drink values (?, ?, ?)"
|
||||||
args = (None, drink.name, drink.vol)
|
args = (None, drink.name, drink.vol)
|
||||||
else:
|
else:
|
||||||
sql = "update drink " "set name=?, vol=? " "where id==?"
|
sql = "update drink " \
|
||||||
|
"set name=?, vol=? " \
|
||||||
|
"where id==?"
|
||||||
args = (drink.name, drink.vol, drink.id)
|
args = (drink.name, drink.vol, drink.id)
|
||||||
return sql, args
|
return sql, args
|
||||||
|
|
||||||
@@ -139,12 +141,22 @@ def load_enemies(id=None):
|
|||||||
return sql, args
|
return sql, args
|
||||||
|
|
||||||
|
|
||||||
|
def load_enemies_for_season(season_id: int):
|
||||||
|
sql = "select * from enemy " \
|
||||||
|
"where season_id = ? or season_id = 'None'" \
|
||||||
|
"order by enemy.id"
|
||||||
|
args = (season_id, )
|
||||||
|
return sql, args
|
||||||
|
|
||||||
|
|
||||||
def save_enemy(enemy: models.Enemy):
|
def save_enemy(enemy: models.Enemy):
|
||||||
if not enemy.id:
|
if not enemy.id:
|
||||||
sql = "insert into enemy values (?, ?, ?, ?)"
|
sql = "insert into enemy values (?, ?, ?, ?)"
|
||||||
args = (None, enemy.name, enemy.boss, enemy.season_id)
|
args = (None, enemy.name, enemy.boss, enemy.season_id)
|
||||||
else:
|
else:
|
||||||
sql = "update enemy " "set name=?, boss=?, season_id=? " "where id==?"
|
sql = "update enemy " \
|
||||||
|
"set name=?, boss=?, season_id=? " \
|
||||||
|
"where id==?"
|
||||||
args = (enemy.name, enemy.boss, enemy.season_id, enemy.id)
|
args = (enemy.name, enemy.boss, enemy.season_id, enemy.id)
|
||||||
return sql, args
|
return sql, args
|
||||||
|
|
||||||
@@ -214,10 +226,12 @@ def load_episode_player_links(episode_id: int):
|
|||||||
|
|
||||||
|
|
||||||
def load_episode_players(episode_id: int):
|
def load_episode_players(episode_id: int):
|
||||||
sql = "select player.* " \
|
sql = (
|
||||||
"from player " \
|
"select player.* "
|
||||||
"left join episode_player ep on player.id = ep.player_id " \
|
"from player "
|
||||||
"where ep.episode_id = ?"
|
"left join episode_player ep on player.id = ep.player_id "
|
||||||
|
"where ep.episode_id = ?"
|
||||||
|
)
|
||||||
args = (episode_id,)
|
args = (episode_id,)
|
||||||
return sql, args
|
return sql, args
|
||||||
|
|
||||||
@@ -229,8 +243,7 @@ def save_episode_players(episode_id: int, player_ids: Sequence[int]):
|
|||||||
|
|
||||||
|
|
||||||
def remove_episode_player(episode_id: int, player_ids: Sequence[int]):
|
def remove_episode_player(episode_id: int, player_ids: Sequence[int]):
|
||||||
sql = "delete from episode_player " \
|
sql = "delete from episode_player " "where episode_id = ? and player_id = ?"
|
||||||
"where episode_id = ? and player_id = ?"
|
|
||||||
args = tuple((episode_id, pid) for pid in player_ids)
|
args = tuple((episode_id, pid) for pid in player_ids)
|
||||||
return sql, args
|
return sql, args
|
||||||
|
|
||||||
@@ -263,3 +276,31 @@ def save_episode(episode: models.Episode):
|
|||||||
episode.id,
|
episode.id,
|
||||||
)
|
)
|
||||||
return sql, args
|
return sql, args
|
||||||
|
|
||||||
|
|
||||||
|
def save_event(event: models.Event):
|
||||||
|
args = [
|
||||||
|
None,
|
||||||
|
event.episode_id,
|
||||||
|
event.player_id,
|
||||||
|
event.enemy_id,
|
||||||
|
event.type.name,
|
||||||
|
event.time.timestamp(),
|
||||||
|
event.comment,
|
||||||
|
]
|
||||||
|
if not event.event_id:
|
||||||
|
sql = "insert into event values (?, ?, ?, ?, ?, ?, ?)"
|
||||||
|
else:
|
||||||
|
sql = (
|
||||||
|
"where id==? "
|
||||||
|
"update event "
|
||||||
|
"set episode_id=?, player_id=?, enemy_id=?, type=?, time=?, comment=?"
|
||||||
|
)
|
||||||
|
args[0] = event.event_id
|
||||||
|
return sql, args
|
||||||
|
|
||||||
|
|
||||||
|
def load_events(episode_id: int):
|
||||||
|
sql = "select * from event where episode_id = ?"
|
||||||
|
args = (episode_id,)
|
||||||
|
return sql, args
|
||||||
17
estusshots/estus-shots.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class EditorModule{
|
||||||
|
|
||||||
|
setCurrentTime = (elemId: string) => {
|
||||||
|
const elem = document.getElementById(elemId) as HTMLInputElement;
|
||||||
|
if (!elem) return;
|
||||||
|
elem.value = this.currentTimeHHMM()
|
||||||
|
};
|
||||||
|
|
||||||
|
currentTimeHHMM = () => {
|
||||||
|
const d = new Date();
|
||||||
|
const hours = (d.getHours()<10 ? '0' : '') + d.getHours();
|
||||||
|
const minutes = (d.getMinutes()<10 ? '0' : '') + d.getMinutes();
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorModule = new EditorModule();
|
||||||
@@ -8,11 +8,13 @@ from wtforms import (
|
|||||||
DecimalField,
|
DecimalField,
|
||||||
SelectField,
|
SelectField,
|
||||||
SelectMultipleField,
|
SelectMultipleField,
|
||||||
HiddenField
|
HiddenField,
|
||||||
|
FieldList,
|
||||||
|
FormField,
|
||||||
)
|
)
|
||||||
from wtforms.validators import DataRequired, Optional
|
from wtforms.validators import DataRequired, Optional
|
||||||
|
|
||||||
import choices
|
from estusshots import choices
|
||||||
|
|
||||||
|
|
||||||
class SeasonForm(FlaskForm):
|
class SeasonForm(FlaskForm):
|
||||||
@@ -64,3 +66,25 @@ class EnemyForm(FlaskForm):
|
|||||||
is_boss = BooleanField("Is Boss")
|
is_boss = BooleanField("Is Boss")
|
||||||
submit_button = SubmitField("Submit")
|
submit_button = SubmitField("Submit")
|
||||||
submit_continue_button = SubmitField("Submit and Continue")
|
submit_continue_button = SubmitField("Submit and Continue")
|
||||||
|
|
||||||
|
|
||||||
|
class PenaltyFrom(FlaskForm):
|
||||||
|
penalty_id = HiddenField("Penalty ID")
|
||||||
|
player_id = HiddenField("Player ID")
|
||||||
|
player = HiddenField("Player")
|
||||||
|
drink = SelectField("Drink", choices=choices.DrinkChoiceIterable(), coerce=int)
|
||||||
|
|
||||||
|
|
||||||
|
class EventForm(FlaskForm):
|
||||||
|
event_id = HiddenField("Event ID")
|
||||||
|
episode_id = HiddenField("Episode ID")
|
||||||
|
event_type = SelectField(
|
||||||
|
"Type", choices=choices.EventChoiceIterable(), coerce=int,
|
||||||
|
validators=[DataRequired()]
|
||||||
|
)
|
||||||
|
time = TimeField("Time", format="%H:%M", validators=[DataRequired()])
|
||||||
|
player = SelectField("Player", choices=choices.PlayerChoiceIterable(), coerce=int)
|
||||||
|
enemy = SelectField("Enemy", coerce=int)
|
||||||
|
comment = StringField("Comment")
|
||||||
|
penalties = FieldList(FormField(PenaltyFrom))
|
||||||
|
submit_button = SubmitField("Submit")
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import enum
|
||||||
from numbers import Rational
|
from numbers import Rational
|
||||||
from typing import Dict, List, Union
|
from typing import Dict, List, Union, Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import forms
|
from typing import TYPE_CHECKING
|
||||||
import util
|
if TYPE_CHECKING:
|
||||||
|
from estusshots import forms, util
|
||||||
|
|
||||||
|
|
||||||
|
class EventType(enum.Enum):
|
||||||
|
Pause = 0
|
||||||
|
Death = 1
|
||||||
|
Victory = 2
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -28,7 +36,7 @@ class Player:
|
|||||||
return self.real_name if self.real_name and not self.anon else self.alias
|
return self.real_name if self.real_name and not self.anon else self.alias
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_form(cls, form: forms.PlayerForm):
|
def from_form(cls, form: "forms.PlayerForm"):
|
||||||
id = int(form.player_id.data) if form.player_id.data else None
|
id = int(form.player_id.data) if form.player_id.data else None
|
||||||
real_name = str(form.real_name.data) if form.real_name.data else None
|
real_name = str(form.real_name.data) if form.real_name.data else None
|
||||||
alias = str(form.alias.data)
|
alias = str(form.alias.data)
|
||||||
@@ -44,7 +52,7 @@ class Drink:
|
|||||||
vol: float
|
vol: float
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_form(cls, form: forms.DrinkForm):
|
def from_form(cls, form: "forms.DrinkForm"):
|
||||||
id = int(form.drink_id.data) if form.drink_id.data else None
|
id = int(form.drink_id.data) if form.drink_id.data else None
|
||||||
name = str(form.name.data)
|
name = str(form.name.data)
|
||||||
vol = float(form.vol.data)
|
vol = float(form.vol.data)
|
||||||
@@ -61,7 +69,7 @@ class Enemy:
|
|||||||
season_id: int
|
season_id: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_form(cls, form: forms.EnemyForm):
|
def from_form(cls, form: "forms.EnemyForm"):
|
||||||
id = int(form.enemy_id.data) if form.enemy_id.data else None
|
id = int(form.enemy_id.data) if form.enemy_id.data else None
|
||||||
name = str(form.name.data)
|
name = str(form.name.data)
|
||||||
boss = bool(form.is_boss.data)
|
boss = bool(form.is_boss.data)
|
||||||
@@ -91,7 +99,7 @@ class Season:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_form(cls, form: forms.SeasonForm):
|
def from_form(cls, form: "forms.SeasonForm"):
|
||||||
season_id = int(form.season_id.data) if form.season_id.data else None
|
season_id = int(form.season_id.data) if form.season_id.data else None
|
||||||
code = str(form.code.data)
|
code = str(form.code.data)
|
||||||
game = str(form.game_name.data)
|
game = str(form.game_name.data)
|
||||||
@@ -126,7 +134,7 @@ class Episode:
|
|||||||
self.end = datetime.datetime.fromtimestamp(self.end)
|
self.end = datetime.datetime.fromtimestamp(self.end)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_form(cls, form: forms.EpisodeForm):
|
def from_form(cls, form: "forms.EpisodeForm"):
|
||||||
episode_id = int(form.episode_id.data) if form.episode_id.data else None
|
episode_id = int(form.episode_id.data) if form.episode_id.data else None
|
||||||
season_id = int(form.season_id.data)
|
season_id = int(form.season_id.data)
|
||||||
code = str(form.code.data)
|
code = str(form.code.data)
|
||||||
@@ -139,3 +147,43 @@ class Episode:
|
|||||||
end = end + datetime.timedelta(days=1)
|
end = end + datetime.timedelta(days=1)
|
||||||
|
|
||||||
return cls(episode_id, season_id, title, date, start, end, code)
|
return cls(episode_id, season_id, title, date, start, end, code)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Penalty:
|
||||||
|
penalty_id: int
|
||||||
|
player_id: int
|
||||||
|
drink_id: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Event:
|
||||||
|
event_id: int
|
||||||
|
episode_id: int
|
||||||
|
type: EventType
|
||||||
|
time: datetime.datetime
|
||||||
|
comment: str
|
||||||
|
player_id: Optional[int]
|
||||||
|
enemy_id: Optional[int]
|
||||||
|
penalties: List[Penalty]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_form(cls, form: "forms.EventForm"):
|
||||||
|
event_id = int(form.event_id.data) if form.event_id.data else None
|
||||||
|
episode_id = int(form.episode_id.data)
|
||||||
|
event_type = EventType(form.event_type.data)
|
||||||
|
time = util.combine_datetime(datetime.datetime.today(), form.time.data)
|
||||||
|
comment = str(form.comment.data) if form.comment.data else None
|
||||||
|
player_id = int(form.player.data) if form.player.data else None
|
||||||
|
enemy_id = int(form.enemy.data) if form.enemy.data else None
|
||||||
|
|
||||||
|
penalties = []
|
||||||
|
for entry in form.penalties:
|
||||||
|
penalties.append(Penalty(
|
||||||
|
penalty_id=int(entry.penalty_id.data) if entry.penalty_id.data else None,
|
||||||
|
player_id=int(entry.player_id.data),
|
||||||
|
drink_id=int(entry.drink.data)
|
||||||
|
))
|
||||||
|
|
||||||
|
return cls(event_id, episode_id, event_type, time, comment, player_id, enemy_id,
|
||||||
|
penalties)
|
||||||
154
estusshots/orm.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import enum
|
||||||
|
import sqlalchemy
|
||||||
|
from sqlalchemy import create_engine, ForeignKey, Table
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy import Column, Integer, String, Boolean, Float, Enum, Date, Time
|
||||||
|
from sqlalchemy.orm import sessionmaker, relationship
|
||||||
|
|
||||||
|
from estusshots import util, forms
|
||||||
|
|
||||||
|
engine = create_engine('sqlite:///../databases/test.db')
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class EventType(enum.Enum):
|
||||||
|
Pause = 0
|
||||||
|
Death = 1
|
||||||
|
Victory = 2
|
||||||
|
|
||||||
|
|
||||||
|
class Player(Base):
|
||||||
|
__tablename__ = "players"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
real_name = Column(String)
|
||||||
|
alias = Column(String)
|
||||||
|
hex_id = Column(String)
|
||||||
|
anon = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
events = relationship("Event", back_populates="player")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.real_name if self.real_name and not self.anon else self.alias
|
||||||
|
|
||||||
|
def populate_from_form(self, form: "forms.PlayerForm"):
|
||||||
|
self.real_name = str(form.real_name.data) if form.real_name.data else None
|
||||||
|
self.alias = str(form.alias.data)
|
||||||
|
self.hex_id = str(form.hex_id.data) if form.hex_id.data else None
|
||||||
|
self.anon = bool(form.anonymize.data)
|
||||||
|
|
||||||
|
|
||||||
|
class Drink(Base):
|
||||||
|
__tablename__ = "drinks"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
vol = Column(Float)
|
||||||
|
|
||||||
|
def populate_from_form(self, form: "forms.DrinkForm"):
|
||||||
|
self.name = str(form.name.data)
|
||||||
|
self.vol = float(form.vol.data)
|
||||||
|
|
||||||
|
|
||||||
|
class Season(Base):
|
||||||
|
__tablename__ = "seasons"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
code = Column(String, default='SXX')
|
||||||
|
game = Column(String)
|
||||||
|
description = Column(String)
|
||||||
|
start = Column(Date)
|
||||||
|
end = Column(Date)
|
||||||
|
|
||||||
|
episodes = relationship("Episode", back_populates="season")
|
||||||
|
enemies = relationship("Enemy", back_populates="season")
|
||||||
|
|
||||||
|
def populate_from_form(self, form: "forms.SeasonForm"):
|
||||||
|
self.code = str(form.code.data)
|
||||||
|
self.game = str(form.game_name.data)
|
||||||
|
self.description = str(form.description.data) if form.description.data else None
|
||||||
|
self.start = form.start.data
|
||||||
|
self.end = form.end.data
|
||||||
|
|
||||||
|
|
||||||
|
class Enemy(Base):
|
||||||
|
__tablename__ = "enemies"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
boss = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
season_id = Column(Integer, ForeignKey('seasons.id'))
|
||||||
|
season = relationship("Season", back_populates="enemies")
|
||||||
|
|
||||||
|
events = relationship('Event', back_populates="enemy")
|
||||||
|
|
||||||
|
def populate_from_form(self, form: "forms.EnemyForm"):
|
||||||
|
self.name = str(form.name.data)
|
||||||
|
self.boss = bool(form.is_boss.data)
|
||||||
|
self.season_id = int(form.season_id.data)
|
||||||
|
|
||||||
|
|
||||||
|
class Episode(Base):
|
||||||
|
__tablename__ = "episodes"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
code = Column(String, default='EXX')
|
||||||
|
title = Column(String)
|
||||||
|
date = Column(Date)
|
||||||
|
start = Column(Time)
|
||||||
|
end = Column(Time)
|
||||||
|
|
||||||
|
season_id = Column(Integer, ForeignKey('seasons.id'))
|
||||||
|
season = relationship("Season", back_populates="episodes")
|
||||||
|
|
||||||
|
events = relationship('Event', back_populates='episode')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playtime(self):
|
||||||
|
return util.timedelta(self.start, self.end)
|
||||||
|
|
||||||
|
|
||||||
|
class Event(Base):
|
||||||
|
__tablename__ = 'events'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
type = Column(Enum(EventType))
|
||||||
|
time = Column(Time)
|
||||||
|
comment = Column(String)
|
||||||
|
|
||||||
|
episode_id = Column(Integer, ForeignKey('episodes.id'))
|
||||||
|
episode = relationship('Episode', back_populates='events')
|
||||||
|
|
||||||
|
player_id = Column(Integer, ForeignKey('players.id'))
|
||||||
|
player = relationship('Player', back_populates='events')
|
||||||
|
|
||||||
|
enemy_id = Column(Integer, ForeignKey('enemies.id'))
|
||||||
|
enemy = relationship('Enemy', back_populates='events')
|
||||||
|
|
||||||
|
penalties = relationship('Penalty', back_populates='event')
|
||||||
|
|
||||||
|
|
||||||
|
class Penalty(Base):
|
||||||
|
__tablename__ = 'penalties'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
player_id = Column(Integer, ForeignKey('players.id'))
|
||||||
|
player = relationship('Player')
|
||||||
|
|
||||||
|
drink_id = Column(Integer, ForeignKey('drinks.id'))
|
||||||
|
drink = relationship('Drink')
|
||||||
|
|
||||||
|
event_id = Column(Integer, ForeignKey('events.id'))
|
||||||
|
event = relationship('Event', back_populates='penalties')
|
||||||
|
|
||||||
|
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
def new_session() -> sqlalchemy.orm.Session:
|
||||||
|
"""Open up a new session. This function exists for ease of use, as the return type is hinted for the IDE."""
|
||||||
|
return Session()
|
||||||
@@ -87,7 +87,47 @@ create table if not exists episode_player
|
|||||||
references player
|
references player
|
||||||
);
|
);
|
||||||
|
|
||||||
create unique index if not exists episode_player_link_id_uindex
|
create table if not exists penalty
|
||||||
on episode_player (link_id);
|
(
|
||||||
|
id integer not null
|
||||||
|
constraint penalty_pk
|
||||||
|
primary key autoincrement,
|
||||||
|
episode_id integer not null
|
||||||
|
references episode,
|
||||||
|
drink_id integer not null
|
||||||
|
references drink
|
||||||
|
|
||||||
|
);
|
||||||
|
create unique index if not exists penalty_id_uindex
|
||||||
|
on penalty (id);
|
||||||
|
|
||||||
|
create table if not exists event
|
||||||
|
(
|
||||||
|
id integer not null
|
||||||
|
constraint event_pk
|
||||||
|
primary key autoincrement,
|
||||||
|
episode_id integer not null
|
||||||
|
constraint event_episode_id_fk
|
||||||
|
references season,
|
||||||
|
player_id integer not null
|
||||||
|
constraint event_player_id_fk
|
||||||
|
references player,
|
||||||
|
enemy_id integer
|
||||||
|
references enemy,
|
||||||
|
type text not null,
|
||||||
|
time timestamp not null,
|
||||||
|
comment text
|
||||||
|
);
|
||||||
|
create unique index if not exists event_id_uindex
|
||||||
|
on event (id);
|
||||||
|
|
||||||
|
create table if not exists event_penalty
|
||||||
|
(
|
||||||
|
link_id integer not null
|
||||||
|
constraint event_punishment_pk
|
||||||
|
primary key autoincrement,
|
||||||
|
event_id integer not null
|
||||||
|
references event,
|
||||||
|
punishment_id integer not null
|
||||||
|
references punishment
|
||||||
|
);
|
||||||
@@ -68,3 +68,7 @@ a:hover {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: rgb(34, 34, 34);
|
background-color: rgb(34, 34, 34);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
20
estusshots/static/js/estus-shots.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
var EditorModule = (function () {
|
||||||
|
function EditorModule() {
|
||||||
|
var _this = this;
|
||||||
|
this.setCurrentTime = function (elemId) {
|
||||||
|
var elem = document.getElementById(elemId);
|
||||||
|
if (!elem)
|
||||||
|
return;
|
||||||
|
elem.value = _this.currentTimeHHMM();
|
||||||
|
};
|
||||||
|
this.currentTimeHHMM = function () {
|
||||||
|
var d = new Date();
|
||||||
|
var hours = (d.getHours() < 10 ? '0' : '') + d.getHours();
|
||||||
|
var minutes = (d.getMinutes() < 10 ? '0' : '') + d.getMinutes();
|
||||||
|
return hours + ":" + minutes;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return EditorModule;
|
||||||
|
}());
|
||||||
|
var editorModule = new EditorModule();
|
||||||
|
//# sourceMappingURL=estus-shots.js.map
|
||||||
1
estusshots/static/js/estus-shots.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"estus-shots.js","sourceRoot":"","sources":["../../estus-shots.ts"],"names":[],"mappings":"AAAA;IAAA;QAAA,iBAcC;QAZC,mBAAc,GAAG,UAAC,MAAc;YAC9B,IAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAqB,CAAC;YACjE,IAAI,CAAC,IAAI;gBAAE,OAAO;YAClB,IAAI,CAAC,KAAK,GAAG,KAAI,CAAC,eAAe,EAAE,CAAA;QACrC,CAAC,CAAC;QAEF,oBAAe,GAAG;YAChB,IAAM,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,IAAM,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,GAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC1D,IAAM,OAAO,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,GAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC;YAChE,OAAU,KAAK,SAAI,OAAS,CAAC;QAC/B,CAAC,CAAC;IACJ,CAAC;IAAD,mBAAC;AAAD,CAAC,AAdD,IAcC;AAED,IAAM,YAAY,GAAG,IAAI,YAAY,EAAE,CAAC"}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
{% block title %}- Estus Shots{% endblock %}
|
{% block title %}- Estus Shots{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="text/javascript" src="{{ url_for('static', filename='js/estus-shots.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block styles %}
|
{% block styles %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.1/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.1/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}drink_save
|
||||||
{% set active_page = "drinks" %}
|
{% set active_page = "drinks" %}
|
||||||
{% block title %}Drinks {{ super() }}{% endblock %}
|
{% block title %}Drinks {{ super() }}{% endblock %}
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
<table class="table table-hover table-striped table-bordered">
|
<table class="table table-hover table-striped table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="col-sm-auto text-center">ID</th>
|
|
||||||
<th scope="col" class="col-sm-auto text-center">Name</th>
|
<th scope="col" class="col-sm-auto text-center">Name</th>
|
||||||
|
<th scope="col" class="col-sm-auto text-center">Season</th>
|
||||||
<th scope="col" class="col-sm-auto text-center">Boss Enemy</th>
|
<th scope="col" class="col-sm-auto text-center">Boss Enemy</th>
|
||||||
|
|
||||||
{% if g.is_editor %}
|
{% if g.is_editor %}
|
||||||
@@ -26,19 +26,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in model.enemies %}
|
{% for enemy in model.enemies %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="col-sm-auto text-center">{{ item.id }}</td>
|
<td class="col-sm-auto text-center">{{ enemy.name }}</td>
|
||||||
<td class="col-sm-auto text-center">{{ item.name }}</td>
|
<td class="col-sm-auto text-center">{{ enemy.season.game }}</td>
|
||||||
<td class="col-sm-auto text-center">
|
<td class="col-sm-auto text-center">
|
||||||
{% if item.boss %}
|
{% if enemy.boss %}
|
||||||
<span class="fas fa-check"></span>
|
<span class="fas fa-check"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{% if g.is_editor %}
|
{% if g.is_editor %}
|
||||||
<td class="col-sm-auto text-center">
|
<td class="col-sm-auto text-center">
|
||||||
<a class="btn btn-default" href="{{ url_for('enemy_edit', enemy_id = item.id) }}">
|
<a class="btn btn-default" href="{{ url_for('enemy_edit', enemy_id = enemy.id) }}">
|
||||||
<span class="fas fa-pencil-alt"></span>
|
<span class="fas fa-pencil-alt"></span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -88,15 +88,46 @@
|
|||||||
<!--endregion-->
|
<!--endregion-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
{% if model.events %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header text-center">
|
<div class="card-header text-center">
|
||||||
Event List
|
Event List
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="col-sm-auto text-center">Time</th>
|
||||||
|
<th scope="col" class="col-sm-auto text-center">Type</th>
|
||||||
|
<th scope="col" class="col-sm-auto text-center">Player</th>
|
||||||
|
<th scope="col" class="col-sm-auto text-center">Enemy</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in model.events.entries %}
|
||||||
|
<tr>
|
||||||
|
<td class="col-sm-auto text-center">{{ entry.time }}</td>
|
||||||
|
<td class="col-sm-auto text-center">{{ entry.type.name }}</td>
|
||||||
|
<td class="col-sm-auto text-center">{{ entry.type.player_name }}</td>
|
||||||
|
<td class="col-sm-auto text-center">{{ entry.type.enemy_name }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
Event List
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">Nothing did happen yet</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
95
estusshots/templates/event_editor.html
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "bootstrap/wtf.html" as wtf %}
|
||||||
|
{% set active_page = "seasons" %}
|
||||||
|
{% block title %}{{ model.page_title }} {{ super() }}{% endblock %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<div class="text-center">
|
||||||
|
<h1>{{ model.form_title }}</h1>
|
||||||
|
|
||||||
|
{% if model.errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{% for field, errors in model.errors.items() %}
|
||||||
|
<div>
|
||||||
|
<strong class="text-capitalize">{{ field }}</strong>:
|
||||||
|
{{ errors|join(', ') }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="{{ model.post_url }}" method="post">
|
||||||
|
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<div class="form-group row required">
|
||||||
|
<div class="col-lg-2">
|
||||||
|
{{ form.event_type.label(class_="form-control-label") }}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-10">
|
||||||
|
{{ form.event_type(class_="form-control") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row required">
|
||||||
|
<div class="col-lg-2">
|
||||||
|
{{ form.time.label(class_="form-control-label") }}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-10 row">
|
||||||
|
<div class="col">
|
||||||
|
{{ form.time(class_="form-control") }}
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button type="button" class="btn btn-default btn-block"
|
||||||
|
onclick="editorModule.setCurrentTime('time')">Now</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-group row required">
|
||||||
|
<div class="col-lg-2">
|
||||||
|
{{ form.player.label(class_="form-control-label") }}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-10">
|
||||||
|
{{ form.player(class_="form-control") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row required">
|
||||||
|
<div class="col-lg-2">
|
||||||
|
{{ form.enemy.label(class_="form-control-label") }}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-10">
|
||||||
|
{{ form.enemy(class_="form-control") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-lg-2">
|
||||||
|
{{ form.comment.label(class_="form-control-label") }}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-10">
|
||||||
|
{{ form.comment(class_="form-control") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="penalty-container">
|
||||||
|
{% for penalty in form.penalties %}
|
||||||
|
{{ penalty.hidden_tag() }}
|
||||||
|
<div class="penalty-item">
|
||||||
|
{{ penalty.penalty_id }}
|
||||||
|
{{ penalty.player_id }}
|
||||||
|
{{ penalty.player }}
|
||||||
|
<label class="form-control-label">{{ penalty.player.data }}</label>
|
||||||
|
{{ penalty.drink }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="offset-lg-2 col-lg-10">
|
||||||
|
{{ form.submit_button(class_="btn btn-primary") }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
120
estusshots/util.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import functools
|
||||||
|
from datetime import datetime, time, date, timedelta
|
||||||
|
|
||||||
|
from flask import g, session, redirect
|
||||||
|
|
||||||
|
from estusshots import config, app, db
|
||||||
|
|
||||||
|
TIME_FMT = "%H:%M"
|
||||||
|
DATE_FMT = "%Y-%m-%d"
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_datetime(data: str) -> datetime:
|
||||||
|
"""
|
||||||
|
Convert %H:%M formatted string into a python datetime object
|
||||||
|
"""
|
||||||
|
data = ":".join(data.split(":")[:2])
|
||||||
|
return datetime.strptime(data, TIME_FMT)
|
||||||
|
|
||||||
|
|
||||||
|
def datetime_time_str(data: datetime) -> str:
|
||||||
|
"""
|
||||||
|
Convert a datetime object into a formatted string for display
|
||||||
|
:param data: datetime
|
||||||
|
:return: str
|
||||||
|
"""
|
||||||
|
return data.strftime(TIME_FMT)
|
||||||
|
|
||||||
|
|
||||||
|
def timedelta_to_str(data: timedelta) -> str:
|
||||||
|
"""
|
||||||
|
Remove second and microsecond portion from timedeltas for display
|
||||||
|
:param data: datetime.timedelta
|
||||||
|
:return: str
|
||||||
|
"""
|
||||||
|
return str(
|
||||||
|
data - timedelta(seconds=data.seconds, microseconds=data.microseconds)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def timedelta(start: time, end: time) -> float:
|
||||||
|
startDateTime = datetime.combine(date.today(), start)
|
||||||
|
# Check if the the end is still on the same day
|
||||||
|
if start.hour > end.hour:
|
||||||
|
base = date.today() + timedelta(days=1)
|
||||||
|
else:
|
||||||
|
base = date.today()
|
||||||
|
endDateTime = datetime.combine(base, end)
|
||||||
|
difference = startDateTime - endDateTime
|
||||||
|
difference_hours = difference.total_seconds() / 3600
|
||||||
|
return difference_hours
|
||||||
|
|
||||||
|
|
||||||
|
def combine_datetime(date: datetime.date, time: datetime.time):
|
||||||
|
"""
|
||||||
|
Combine a date and time object into a datetime object
|
||||||
|
"""
|
||||||
|
return datetime(
|
||||||
|
date.year,
|
||||||
|
date.month,
|
||||||
|
date.day,
|
||||||
|
time.hour,
|
||||||
|
time.minute,
|
||||||
|
time.second,
|
||||||
|
time.microsecond,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_type(password):
|
||||||
|
# TODO password hashing?
|
||||||
|
if password == config.WRITE_PW:
|
||||||
|
return "editor"
|
||||||
|
if password == config.READ_PW:
|
||||||
|
return "readonly"
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_role(data):
|
||||||
|
"""Set the users role in the flask g object for later usage"""
|
||||||
|
g.is_editor = data == "editor"
|
||||||
|
|
||||||
|
|
||||||
|
def authorize(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
set_user_role(session["user_type"])
|
||||||
|
except KeyError:
|
||||||
|
return redirect("/login")
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter("format_time")
|
||||||
|
def format_time(value):
|
||||||
|
"""Make the datetime to time string formatting available to jinja2"""
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
return datetime_time_str(value)
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter("format_timedelta")
|
||||||
|
def format_timedelta(value):
|
||||||
|
"""Make formatting for timedeltas available to jinja2"""
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
return timedelta_to_str(value)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cli.command("initdb")
|
||||||
|
def init_db_command():
|
||||||
|
"""Initializes the database."""
|
||||||
|
db.init_db()
|
||||||
|
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close_connection(exception):
|
||||||
|
db = getattr(g, "_database", None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
72
estusshots/views/drinks.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from flask import render_template, redirect
|
||||||
|
|
||||||
|
from estusshots import app
|
||||||
|
from estusshots import forms, models, orm
|
||||||
|
from estusshots.util import authorize
|
||||||
|
from estusshots.orm import Drink
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/drink")
|
||||||
|
@authorize
|
||||||
|
def drink_list():
|
||||||
|
db = orm.new_session()
|
||||||
|
drinks = db.query(Drink).order_by(Drink.name).all()
|
||||||
|
model = {
|
||||||
|
"drinks": drinks,
|
||||||
|
"columns": [("name", "Drink Name"), ("vol", "Alcohol %")],
|
||||||
|
"controls": [("edit", "Edit")],
|
||||||
|
}
|
||||||
|
return render_template("drink_list.html", model=model)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/drink/<drink_id>/edit", methods=["GET"])
|
||||||
|
@authorize
|
||||||
|
def drink_edit(drink_id: int):
|
||||||
|
db = orm.new_session()
|
||||||
|
drink = db.query(Drink).filter(Drink.id == drink_id).first()
|
||||||
|
form = forms.DrinkForm()
|
||||||
|
form.drink_id.data = drink.id
|
||||||
|
form.name.data = drink.name
|
||||||
|
form.vol.data = drink.vol
|
||||||
|
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="Edit Drink",
|
||||||
|
form_title=f'Edit Drink "{drink.name}"',
|
||||||
|
post_url="/drink/save",
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/drink/new", methods=["GET"])
|
||||||
|
@authorize
|
||||||
|
def new_drink():
|
||||||
|
form = forms.DrinkForm()
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="New Drink",
|
||||||
|
form_title=f"Create a new Drink",
|
||||||
|
post_url="/drink/save",
|
||||||
|
)
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/drink/save", methods=["POST"])
|
||||||
|
@authorize
|
||||||
|
def drink_save():
|
||||||
|
form = forms.DrinkForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
drink_id = int(form.drink_id.data) if form.drink_id.data else None
|
||||||
|
db = orm.new_session()
|
||||||
|
if drink_id:
|
||||||
|
drink = db.query(Drink).filter(Drink.id == drink_id).first()
|
||||||
|
else:
|
||||||
|
drink = Drink()
|
||||||
|
db.add(drink)
|
||||||
|
drink.populate_from_form(form)
|
||||||
|
err = db.commit()
|
||||||
|
return redirect("/drink")
|
||||||
|
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="Drinks", form_title="Edit Drink", post_url="/drink/save"
|
||||||
|
)
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
72
estusshots/views/enemies.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from flask import render_template, request, redirect
|
||||||
|
|
||||||
|
from estusshots import app
|
||||||
|
from estusshots import forms, models, orm
|
||||||
|
from estusshots.util import authorize
|
||||||
|
from estusshots.orm import Enemy
|
||||||
|
from sqlalchemy.orm import subqueryload
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/enemy")
|
||||||
|
@authorize
|
||||||
|
def enemy_list():
|
||||||
|
db = orm.new_session()
|
||||||
|
enemies = db.query(Enemy).options(subqueryload(Enemy.season)).order_by(Enemy.name).all()
|
||||||
|
model = {"enemies": enemies}
|
||||||
|
return render_template("enemies.html", model=model)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/enemy/new", methods=["GET"])
|
||||||
|
@authorize
|
||||||
|
def enemy_new(preselect_season=None):
|
||||||
|
form = forms.EnemyForm()
|
||||||
|
|
||||||
|
if preselect_season:
|
||||||
|
form.season_id.default = preselect_season
|
||||||
|
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="Enemies",
|
||||||
|
form_title="Create a new Enemy",
|
||||||
|
post_url=f"/enemy/null/edit",
|
||||||
|
)
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/enemy/<enemy_id>/edit", methods=["GET", "POST"])
|
||||||
|
@authorize
|
||||||
|
def enemy_edit(enemy_id: int):
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="Enemies",
|
||||||
|
form_title="Edit Enemy",
|
||||||
|
post_url=f"/enemy/{enemy_id}/edit",
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
db = orm.new_session()
|
||||||
|
enemy = db.query(Enemy).filter(Enemy.id == enemy_id).first()
|
||||||
|
|
||||||
|
form = forms.EnemyForm()
|
||||||
|
form.season_id.data = enemy.season_id if enemy.season_id else -1
|
||||||
|
form.name.data = enemy.name
|
||||||
|
form.is_boss.data = enemy.boss
|
||||||
|
form.enemy_id.data = enemy_id
|
||||||
|
|
||||||
|
model.form_title = f'Edit Enemy "{enemy.name}"'
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
else:
|
||||||
|
form = forms.EnemyForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
db = orm.new_session()
|
||||||
|
enemy = db.query(Enemy).filter(Enemy.id == enemy_id).first()
|
||||||
|
if not enemy:
|
||||||
|
enemy = Enemy()
|
||||||
|
db.add(enemy)
|
||||||
|
enemy.populate_from_form(form)
|
||||||
|
db.commit()
|
||||||
|
if form.submit_continue_button.data:
|
||||||
|
form.name.data = None
|
||||||
|
return enemy_new(preselect_season=enemy.season_id)
|
||||||
|
return redirect("/enemy")
|
||||||
|
|
||||||
|
model.form_title = "Incorrect Data"
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
137
estusshots/views/episodes.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from flask import render_template, request, redirect
|
||||||
|
|
||||||
|
from estusshots import app
|
||||||
|
from estusshots import forms, models, db
|
||||||
|
from estusshots.util import authorize
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season/<season_id>/episode/<episode_id>")
|
||||||
|
@authorize
|
||||||
|
def episode_detail(season_id: int, episode_id: int):
|
||||||
|
sql, args = db.load_season(season_id)
|
||||||
|
season = db.query_db(sql, args, one=True, cls=models.Season)
|
||||||
|
sql, args = db.load_episode(episode_id)
|
||||||
|
episode = db.query_db(sql, args, one=True, cls=models.Episode)
|
||||||
|
sql, args = db.load_episode_players(episode_id)
|
||||||
|
ep_players = db.query_db(sql, args, cls=models.Player)
|
||||||
|
sql, args = db.load_events(episode_id)
|
||||||
|
ep_events: List[models.Event] = db.query_db(sql, args, cls=models.Event)
|
||||||
|
sql, args = db.load_enemies(season_id)
|
||||||
|
enemies = db.query_db(sql, args, cls=models.Enemy)
|
||||||
|
|
||||||
|
deaths = [ev for ev in ep_events if ev.type == models.EventType.Death]
|
||||||
|
entries = []
|
||||||
|
for death in deaths:
|
||||||
|
entries.append({
|
||||||
|
"time": death.time.time(),
|
||||||
|
"type": death.type,
|
||||||
|
"player_name": [p.name for p in ep_players if p.id == death.player_id],
|
||||||
|
"enemy_name": [e.name for e in enemies if e.id == death.enemy_id]
|
||||||
|
})
|
||||||
|
events = None
|
||||||
|
if ep_events:
|
||||||
|
events = {"entries": death, "victory_count": 0, "defeat_count": 0}
|
||||||
|
|
||||||
|
model = {
|
||||||
|
"title": f"{season.code}{episode.code}",
|
||||||
|
"episode": episode,
|
||||||
|
"season": season,
|
||||||
|
"players": ep_players,
|
||||||
|
"events": events,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template("episode_details.html", model=model)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season/<season_id>/episode", methods=["GET"])
|
||||||
|
@authorize
|
||||||
|
def episode_list(season_id: int):
|
||||||
|
sql, args = db.load_season(season_id)
|
||||||
|
season = db.query_db(sql, args, one=True, cls=models.Season)
|
||||||
|
sql, args = db.load_episodes(season_id)
|
||||||
|
episodes = db.query_db(sql, args, cls=models.Episode)
|
||||||
|
|
||||||
|
model = {"season_id": season_id, "season_code": season.code}
|
||||||
|
return render_template("episode_list.html", model=model)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season/<season_id>/episode/new", methods=["GET"])
|
||||||
|
@authorize
|
||||||
|
def episode_new(season_id: int):
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="New Episode",
|
||||||
|
form_title="Create New Episode",
|
||||||
|
post_url=f"/season/{season_id}/episode/null/edit",
|
||||||
|
)
|
||||||
|
|
||||||
|
form = forms.EpisodeForm(request.form)
|
||||||
|
form.season_id.data = season_id
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season/<season_id>/episode/<episode_id>/edit", methods=["GET", "POST"])
|
||||||
|
@authorize
|
||||||
|
def episode_edit(season_id: int, episode_id: int):
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="Edit Episode",
|
||||||
|
form_title="Edit Episode",
|
||||||
|
post_url=f"/season/{season_id}/episode/{episode_id}/edit",
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
sql, args = db.load_episode(episode_id)
|
||||||
|
episode: models.Episode = db.query_db(sql, args, one=True, cls=models.Episode)
|
||||||
|
|
||||||
|
sql, args = db.load_episode_players(episode_id)
|
||||||
|
ep_players = db.query_db(sql, args, cls=models.Player)
|
||||||
|
|
||||||
|
form = forms.EpisodeForm()
|
||||||
|
form.season_id.data = episode.season_id
|
||||||
|
form.episode_id.data = episode.id
|
||||||
|
form.code.data = episode.code
|
||||||
|
form.date.data = episode.date
|
||||||
|
form.start.data = episode.start
|
||||||
|
form.end.data = episode.end
|
||||||
|
form.title.data = episode.title
|
||||||
|
form.players.data = [p.id for p in ep_players]
|
||||||
|
|
||||||
|
model.form_title = f"Edit Episode '{episode.code}: {episode.title}'"
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
else:
|
||||||
|
form = forms.EpisodeForm()
|
||||||
|
|
||||||
|
if not form.validate_on_submit():
|
||||||
|
model.errors = form.errors
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
errors = False
|
||||||
|
episode = models.Episode.from_form(form)
|
||||||
|
sql, args = db.save_episode(episode)
|
||||||
|
|
||||||
|
last_key = db.update_db(sql, args, return_key=True)
|
||||||
|
|
||||||
|
episode_id = episode.id if episode.id else last_key
|
||||||
|
|
||||||
|
form_ids = form.players.data
|
||||||
|
|
||||||
|
sql, args = db.load_episode_players(episode_id)
|
||||||
|
ep_players = db.query_db(sql, args, cls=models.Player)
|
||||||
|
pids = [p.id for p in ep_players]
|
||||||
|
|
||||||
|
new_ids = [pid for pid in form_ids if pid not in pids]
|
||||||
|
removed_ids = [pid for pid in pids if pid not in form_ids]
|
||||||
|
|
||||||
|
if removed_ids:
|
||||||
|
sql, args = db.remove_episode_player(episode_id, removed_ids)
|
||||||
|
errors = db.update_db(sql, args)
|
||||||
|
|
||||||
|
if new_ids:
|
||||||
|
sql, args = db.save_episode_players(episode_id, new_ids)
|
||||||
|
errors = db.update_db(sql, args)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
model.errors = {"Error saving episode": [errors]}
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
return redirect(url_for("season_overview", season_id=season_id))
|
||||||
56
estusshots/views/events.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from flask import render_template, request, redirect
|
||||||
|
|
||||||
|
from estusshots import app
|
||||||
|
from estusshots import forms, models, db, choices
|
||||||
|
from estusshots.util import authorize
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season/<s_id>/episode/<ep_id>/event/new", methods=["GET"])
|
||||||
|
@authorize
|
||||||
|
def event_new(s_id: int, ep_id: int):
|
||||||
|
model = {
|
||||||
|
"page_title": "New Event",
|
||||||
|
"form_title": "Create New Event",
|
||||||
|
"post_url": f"/season/{s_id}/episode/{ep_id}/event/null/edit",
|
||||||
|
}
|
||||||
|
sql, args = db.load_episode(ep_id)
|
||||||
|
episode: models.Episode = db.query_db(sql, args, one=True, cls=models.Episode)
|
||||||
|
|
||||||
|
sql, args = db.load_episode_players(ep_id)
|
||||||
|
ep_players = db.query_db(sql, args, cls=models.Player)
|
||||||
|
|
||||||
|
form = forms.EventForm()
|
||||||
|
form.episode_id.data = ep_id
|
||||||
|
form.enemy.choices = choices.enemy_choice_for_season(s_id)
|
||||||
|
form.event_type.data = 1
|
||||||
|
|
||||||
|
Penalty = namedtuple("Penalty", ["penalty_id", "player_id", "player", "drink"])
|
||||||
|
for player in ep_players:
|
||||||
|
form.penalties.append_entry(Penalty(None, player.id, player.name, 1))
|
||||||
|
|
||||||
|
return render_template("event_editor.html", model=model, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season/<s_id>/episode/<ep_id>/event/<ev_id>/edit", methods=["GET", "POST"])
|
||||||
|
@authorize
|
||||||
|
def event_edit(s_id: int, ep_id: int, ev_id: int):
|
||||||
|
model = {
|
||||||
|
"page_title": "Edit Event",
|
||||||
|
"form_title": "Edit Event",
|
||||||
|
"post_url": f"/season/{s_id}/episode/{ep_id}/event/{ev_id}/edit",
|
||||||
|
}
|
||||||
|
if request.method == "GET":
|
||||||
|
return render_template("event_editor.html", model=model)
|
||||||
|
else:
|
||||||
|
form = forms.EventForm()
|
||||||
|
form.enemy.choices = choices.enemy_choice_for_season(s_id)
|
||||||
|
if not form.validate_on_submit():
|
||||||
|
model["errors"] = form.errors
|
||||||
|
return render_template("event_editor.html", model=model, form=form)
|
||||||
|
|
||||||
|
event = models.Event.from_form(form)
|
||||||
|
sql, args = db.save_event(event)
|
||||||
|
errors = db.update_db(sql, args)
|
||||||
|
return redirect(f"/season/{s_id}/episode/{ep_id}")
|
||||||
28
estusshots/views/login.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from flask import render_template, request, redirect, session
|
||||||
|
|
||||||
|
from estusshots import app
|
||||||
|
from estusshots.util import authorize, get_user_type
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
if request.method == "GET":
|
||||||
|
return render_template("login.html")
|
||||||
|
else:
|
||||||
|
user_type = get_user_type(request.form.get("password"))
|
||||||
|
if not user_type:
|
||||||
|
return redirect("/login")
|
||||||
|
session["user_type"] = user_type
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/logout")
|
||||||
|
def logout():
|
||||||
|
session.pop("role", None)
|
||||||
|
return redirect("login")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
@authorize
|
||||||
|
def landing():
|
||||||
|
return redirect("/season")
|
||||||
71
estusshots/views/players.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
from flask import render_template, request, redirect
|
||||||
|
|
||||||
|
from estusshots import app
|
||||||
|
from estusshots import forms, models, orm
|
||||||
|
from estusshots.util import authorize
|
||||||
|
from estusshots.orm import Player
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/player/new", methods=["GET"])
|
||||||
|
@authorize
|
||||||
|
def player_new():
|
||||||
|
form = forms.PlayerForm()
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="Players",
|
||||||
|
form_title="Create a new Player",
|
||||||
|
post_url="/player/null/edit",
|
||||||
|
)
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/player/<player_id>/edit", methods=["GET", "POST"])
|
||||||
|
@authorize
|
||||||
|
def player_edit(player_id: int):
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="Players",
|
||||||
|
form_title=f"Edit Player",
|
||||||
|
post_url=f"/player/{player_id}/edit",
|
||||||
|
)
|
||||||
|
# Edit Existing Player
|
||||||
|
if request.method == "GET":
|
||||||
|
db = orm.new_session()
|
||||||
|
player = db.query(Player).filter(Player.id == player_id).first()
|
||||||
|
|
||||||
|
form = forms.PlayerForm()
|
||||||
|
form.player_id.data = player.id
|
||||||
|
form.anonymize.data = player.anon
|
||||||
|
form.real_name.data = player.real_name
|
||||||
|
form.alias.data = player.alias
|
||||||
|
form.hex_id.data = player.hex_id
|
||||||
|
|
||||||
|
model.form_title = f'Edit Player "{player.name}"'
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
# Save POSTed data
|
||||||
|
else:
|
||||||
|
form = forms.PlayerForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
db = orm.new_session()
|
||||||
|
player = db.query(Player).filter(Player.id == player_id).first()
|
||||||
|
player.populate_from_form(form)
|
||||||
|
db.commit()
|
||||||
|
return redirect("/player")
|
||||||
|
|
||||||
|
model.form_title = "Incorrect Data"
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/player")
|
||||||
|
@authorize
|
||||||
|
def player_list():
|
||||||
|
db = orm.new_session()
|
||||||
|
players = db.query(Player)
|
||||||
|
model = {
|
||||||
|
"player_list": players,
|
||||||
|
"columns": [
|
||||||
|
("name", "Player Name"),
|
||||||
|
("alias", "Alias"),
|
||||||
|
("hex_id", "Hex ID"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return render_template("player_list.html", model=model)
|
||||||
94
estusshots/views/seasons.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from flask import render_template, request, redirect, url_for
|
||||||
|
|
||||||
|
from estusshots import app
|
||||||
|
from estusshots import forms, models, orm
|
||||||
|
from estusshots.util import authorize
|
||||||
|
from estusshots.orm import Season
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season")
|
||||||
|
@authorize
|
||||||
|
def season_list():
|
||||||
|
db = orm.new_session()
|
||||||
|
seasons = db.query(Season).order_by(Season.code).all()
|
||||||
|
model = {
|
||||||
|
"seasons": seasons,
|
||||||
|
"columns": [
|
||||||
|
("code", "#"),
|
||||||
|
("game", "Game"),
|
||||||
|
("description", "Season Description"),
|
||||||
|
("start", "Started At"),
|
||||||
|
("end", "Ended At"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return render_template("season_list.html", model=model)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season/new", methods=["GET"])
|
||||||
|
@authorize
|
||||||
|
def season_new():
|
||||||
|
form = forms.SeasonForm()
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="New Season",
|
||||||
|
form_title="Create New Season",
|
||||||
|
post_url="/season/edit/null",
|
||||||
|
)
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season/edit/<season_id>", methods=["GET", "POST"])
|
||||||
|
@authorize
|
||||||
|
def season_edit(season_id: int):
|
||||||
|
model = models.GenericFormModel(
|
||||||
|
page_title="Seasons",
|
||||||
|
form_title="Edit Season",
|
||||||
|
post_url=f"/season/edit/{season_id}",
|
||||||
|
)
|
||||||
|
db = orm.new_session()
|
||||||
|
season = db.query(Season).filter(Season.id == season_id).first()
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
form = forms.SeasonForm()
|
||||||
|
form.season_id.data = season.id
|
||||||
|
form.code.data = season.code
|
||||||
|
form.game_name.data = season.game
|
||||||
|
form.description.data = season.description
|
||||||
|
form.start.data = season.start
|
||||||
|
form.end.data = season.end
|
||||||
|
|
||||||
|
model.form_title = f"Edit Season '{season.code}: {season.game}'"
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
else:
|
||||||
|
form = forms.SeasonForm()
|
||||||
|
|
||||||
|
if not form.validate_on_submit():
|
||||||
|
model.errors = form.errors
|
||||||
|
return render_template("generic_form.html", model=model, form=form)
|
||||||
|
|
||||||
|
if not season:
|
||||||
|
season = Season()
|
||||||
|
db.add(season)
|
||||||
|
|
||||||
|
season.populate_from_form(form)
|
||||||
|
db.commit()
|
||||||
|
return redirect(url_for("season_list"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/season/<season_id>", methods=["GET"])
|
||||||
|
@authorize
|
||||||
|
def season_overview(season_id: int):
|
||||||
|
db = orm.new_session()
|
||||||
|
season = db.query(Season).filter(Season.id == season_id).first()
|
||||||
|
|
||||||
|
infos = {
|
||||||
|
"Number": season.code,
|
||||||
|
"Game": season.game,
|
||||||
|
"Start Date": season.start,
|
||||||
|
"End Date": season.end if season.end else "Ongoing",
|
||||||
|
}
|
||||||
|
model = {
|
||||||
|
"title": f"{season.code} {season.game}",
|
||||||
|
"season_info": infos,
|
||||||
|
"episodes": season.episodes,
|
||||||
|
}
|
||||||
|
return render_template("season_overview.html", model=model)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
sqlalchemy
|
||||||
Click==7.0
|
Click==7.0
|
||||||
dataclasses==0.6
|
dataclasses==0.6
|
||||||
dominate==2.3.5
|
dominate==2.3.5
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% import "bootstrap/wtf.html" as wtf %}
|
|
||||||
{% set active_page = "seasons" %}
|
|
||||||
{% block title %}{{ model.page_title }} {{ super() }}{% endblock %}
|
|
||||||
|
|
||||||
{% block page %}
|
|
||||||
<div class="text-center">
|
|
||||||
<h1>{{ model.form_title }}</h1>
|
|
||||||
|
|
||||||
{% if model.errors %}
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
{% for field, errors in model.errors.items() %}
|
|
||||||
<div>
|
|
||||||
<strong class="text-capitalize">{{ field }}</strong>:
|
|
||||||
{{ errors|join(', ') }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form action="{{ model.post_url }}" method="post">
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./estusshots/static/js"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
],
|
||||||
|
}
|
||||||
47
util.py
@@ -1,47 +0,0 @@
|
|||||||
import datetime
|
|
||||||
|
|
||||||
TIME_FMT = "%H:%M"
|
|
||||||
DATE_FMT = "%Y-%m-%d"
|
|
||||||
|
|
||||||
|
|
||||||
def str_to_datetime(data: str) -> datetime.datetime:
|
|
||||||
"""
|
|
||||||
Convert %H:%M formatted string into a python datetime object
|
|
||||||
"""
|
|
||||||
data = ":".join(data.split(":")[:2])
|
|
||||||
return datetime.datetime.strptime(data, TIME_FMT)
|
|
||||||
|
|
||||||
|
|
||||||
def datetime_time_str(data: datetime) -> str:
|
|
||||||
"""
|
|
||||||
Convert a datetime object into a formatted string for display
|
|
||||||
:param data: datetime
|
|
||||||
:return: str
|
|
||||||
"""
|
|
||||||
return data.strftime(TIME_FMT)
|
|
||||||
|
|
||||||
|
|
||||||
def timedelta_to_str(data: datetime.timedelta) -> str:
|
|
||||||
"""
|
|
||||||
Remove second and microsecond portion from timedeltas for display
|
|
||||||
:param data: datetime.timedelta
|
|
||||||
:return: str
|
|
||||||
"""
|
|
||||||
return str(
|
|
||||||
data - datetime.timedelta(seconds=data.seconds, microseconds=data.microseconds)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def combine_datetime(date: datetime.date, time: datetime.time):
|
|
||||||
"""
|
|
||||||
Combine a date and time object into a datetime object
|
|
||||||
"""
|
|
||||||
return datetime.datetime(
|
|
||||||
date.year,
|
|
||||||
date.month,
|
|
||||||
date.day,
|
|
||||||
time.hour,
|
|
||||||
time.minute,
|
|
||||||
time.second,
|
|
||||||
time.microsecond,
|
|
||||||
)
|
|
||||||