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
|
||||
import db
|
||||
from estusshots import orm, models, db
|
||||
from estusshots.orm import Season
|
||||
|
||||
|
||||
def event_choices():
|
||||
return [(member.value, member.name) for member in models.EventType]
|
||||
|
||||
|
||||
def season_choices():
|
||||
""" Query the database for available seasons.
|
||||
This returns a list of tuples with the season ID and a display string.
|
||||
"""
|
||||
sql, args = db.load_season()
|
||||
seasons = db.query_db(sql, args, cls=models.Season)
|
||||
db = orm.new_session()
|
||||
seasons = db.query(Season).order_by(Season.code).all()
|
||||
choices = [(s.id, f"{s.code}: {s.game}") for s in seasons]
|
||||
choices.insert(0, (-1, "No Season"))
|
||||
return choices
|
||||
@@ -33,6 +37,15 @@ def drink_choice():
|
||||
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:
|
||||
"""
|
||||
This is used to declare choices for WTForms SelectFields at class definition time.
|
||||
@@ -60,3 +73,8 @@ class PlayerChoiceIterable(IterableBase):
|
||||
class DrinkChoiceIterable(IterableBase):
|
||||
def __init__(self):
|
||||
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
|
||||
|
||||
import models
|
||||
from config import Config
|
||||
from estusshots import models
|
||||
from estusshots.config import config
|
||||
|
||||
|
||||
class DataBaseError(Exception):
|
||||
@@ -16,8 +16,8 @@ def connect_db():
|
||||
"""Create a new sqlite3 connection and register it in 'g._database'"""
|
||||
db = getattr(g, "_database", None)
|
||||
if db is None:
|
||||
log.info(f"Connecting {Config.DATABASE_PATH}")
|
||||
db = g._database = sqlite3.connect(Config.DATABASE_PATH)
|
||||
log.info(f"Connecting {config.DATABASE_PATH}")
|
||||
db = g._database = sqlite3.connect(config.DATABASE_PATH)
|
||||
|
||||
db.row_factory = sqlite3.Row
|
||||
return db
|
||||
@@ -119,7 +119,9 @@ def save_drink_query(drink):
|
||||
sql = "insert into drink values (?, ?, ?)"
|
||||
args = (None, drink.name, drink.vol)
|
||||
else:
|
||||
sql = "update drink " "set name=?, vol=? " "where id==?"
|
||||
sql = "update drink " \
|
||||
"set name=?, vol=? " \
|
||||
"where id==?"
|
||||
args = (drink.name, drink.vol, drink.id)
|
||||
return sql, args
|
||||
|
||||
@@ -139,12 +141,22 @@ def load_enemies(id=None):
|
||||
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):
|
||||
if not enemy.id:
|
||||
sql = "insert into enemy values (?, ?, ?, ?)"
|
||||
args = (None, enemy.name, enemy.boss, enemy.season_id)
|
||||
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)
|
||||
return sql, args
|
||||
|
||||
@@ -214,10 +226,12 @@ def load_episode_player_links(episode_id: int):
|
||||
|
||||
|
||||
def load_episode_players(episode_id: int):
|
||||
sql = "select player.* " \
|
||||
"from player " \
|
||||
"left join episode_player ep on player.id = ep.player_id " \
|
||||
sql = (
|
||||
"select player.* "
|
||||
"from player "
|
||||
"left join episode_player ep on player.id = ep.player_id "
|
||||
"where ep.episode_id = ?"
|
||||
)
|
||||
args = (episode_id,)
|
||||
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]):
|
||||
sql = "delete from episode_player " \
|
||||
"where episode_id = ? and player_id = ?"
|
||||
sql = "delete from episode_player " "where episode_id = ? and player_id = ?"
|
||||
args = tuple((episode_id, pid) for pid in player_ids)
|
||||
return sql, args
|
||||
|
||||
@@ -263,3 +276,31 @@ def save_episode(episode: models.Episode):
|
||||
episode.id,
|
||||
)
|
||||
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,
|
||||
SelectField,
|
||||
SelectMultipleField,
|
||||
HiddenField
|
||||
HiddenField,
|
||||
FieldList,
|
||||
FormField,
|
||||
)
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
|
||||
import choices
|
||||
from estusshots import choices
|
||||
|
||||
|
||||
class SeasonForm(FlaskForm):
|
||||
@@ -64,3 +66,25 @@ class EnemyForm(FlaskForm):
|
||||
is_boss = BooleanField("Is Boss")
|
||||
submit_button = SubmitField("Submit")
|
||||
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 enum
|
||||
from numbers import Rational
|
||||
from typing import Dict, List, Union
|
||||
from typing import Dict, List, Union, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
import forms
|
||||
import util
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from estusshots import forms, util
|
||||
|
||||
|
||||
class EventType(enum.Enum):
|
||||
Pause = 0
|
||||
Death = 1
|
||||
Victory = 2
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -28,7 +36,7 @@ class Player:
|
||||
return self.real_name if self.real_name and not self.anon else self.alias
|
||||
|
||||
@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
|
||||
real_name = str(form.real_name.data) if form.real_name.data else None
|
||||
alias = str(form.alias.data)
|
||||
@@ -44,7 +52,7 @@ class Drink:
|
||||
vol: float
|
||||
|
||||
@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
|
||||
name = str(form.name.data)
|
||||
vol = float(form.vol.data)
|
||||
@@ -61,7 +69,7 @@ class Enemy:
|
||||
season_id: int
|
||||
|
||||
@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
|
||||
name = str(form.name.data)
|
||||
boss = bool(form.is_boss.data)
|
||||
@@ -91,7 +99,7 @@ class Season:
|
||||
pass
|
||||
|
||||
@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
|
||||
code = str(form.code.data)
|
||||
game = str(form.game_name.data)
|
||||
@@ -126,7 +134,7 @@ class Episode:
|
||||
self.end = datetime.datetime.fromtimestamp(self.end)
|
||||
|
||||
@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
|
||||
season_id = int(form.season_id.data)
|
||||
code = str(form.code.data)
|
||||
@@ -139,3 +147,43 @@ class Episode:
|
||||
end = end + datetime.timedelta(days=1)
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
create unique index if not exists episode_player_link_id_uindex
|
||||
on episode_player (link_id);
|
||||
create table if not exists penalty
|
||||
(
|
||||
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;
|
||||
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 scripts %}
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/estus-shots.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<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" %}
|
||||
{% block title %}Drinks {{ super() }}{% endblock %}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<table class="table table-hover table-striped table-bordered">
|
||||
<thead>
|
||||
<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">Season</th>
|
||||
<th scope="col" class="col-sm-auto text-center">Boss Enemy</th>
|
||||
|
||||
{% if g.is_editor %}
|
||||
@@ -26,19 +26,19 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in model.enemies %}
|
||||
{% for enemy in model.enemies %}
|
||||
<tr>
|
||||
<td class="col-sm-auto text-center">{{ item.id }}</td>
|
||||
<td class="col-sm-auto text-center">{{ item.name }}</td>
|
||||
<td class="col-sm-auto text-center">{{ enemy.name }}</td>
|
||||
<td class="col-sm-auto text-center">{{ enemy.season.game }}</td>
|
||||
<td class="col-sm-auto text-center">
|
||||
{% if item.boss %}
|
||||
{% if enemy.boss %}
|
||||
<span class="fas fa-check"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{% if g.is_editor %}
|
||||
<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>
|
||||
</a>
|
||||
</td>
|
||||
@@ -88,15 +88,46 @@
|
||||
<!--endregion-->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
|
||||
{% if model.events %}
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
Event List
|
||||
</div>
|
||||
<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>
|
||||
{% 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>
|
||||
{% 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
|
||||
dataclasses==0.6
|
||||
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,
|
||||
)
|
||||