Rewrite data layer using SqlAlchemy Part 1

This commit is contained in:
2019-10-14 19:02:25 +02:00
parent f6a38cc2db
commit fccd75d6c5
51 changed files with 1297 additions and 658 deletions

37
.editorconfig Normal file
View 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
View File

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

View File

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

View File

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

View File

@@ -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
View 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
View 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()

View File

@@ -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
View 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();

View File

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

View File

@@ -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
View 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()

View File

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

View File

@@ -67,4 +67,8 @@ a:hover {
.list-group-item:hover { .list-group-item:hover {
color: inherit; color: inherit;
background-color: rgb(34, 34, 34); background-color: rgb(34, 34, 34);
} }
h1 {
font-size: 2.5rem;
}

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 258 KiB

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

View File

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 292 KiB

View File

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 251 KiB

View File

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View 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

View 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"}

View File

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

View File

@@ -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 %}
@@ -45,4 +45,4 @@
</table> </table>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -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>
@@ -49,4 +49,4 @@
</table> </table>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

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

View 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 %}

View File

@@ -34,4 +34,4 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

120
estusshots/util.py Normal file
View 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()

View 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)

View 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)

View 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))

View 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
View 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")

View 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)

View 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)

View File

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

View File

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

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