Rewrite data layer using SqlAlchemy Part 1
This commit is contained in:
36
estusshots/__init__.py
Normal file
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
|
||||
80
estusshots/choices.py
Normal file
80
estusshots/choices.py
Normal file
@@ -0,0 +1,80 @@
|
||||
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.
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
def player_choice():
|
||||
"""
|
||||
Query database for a list of available players to bind them to a select box
|
||||
"""
|
||||
sql, args = db.load_players()
|
||||
players = db.query_db(sql, args, cls=models.Player)
|
||||
return [(p.id, p.name) for p in players]
|
||||
|
||||
|
||||
def drink_choice():
|
||||
"""
|
||||
Query database for a list of all available drinks to select from
|
||||
"""
|
||||
sql, args = db.load_drinks()
|
||||
drinks = db.query_db(sql, args, cls=models.Drink)
|
||||
choices = [(d.id, d.name) for d in drinks]
|
||||
choices.insert(0, (-1, "None"))
|
||||
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.
|
||||
Be aware that this uses an undocumented WTForms feature and is not guaranteed to work.
|
||||
"""
|
||||
|
||||
_loader = None
|
||||
|
||||
def __iter__(self):
|
||||
if self._loader:
|
||||
for choice in self._loader():
|
||||
yield choice
|
||||
|
||||
|
||||
class SeasonChoiceIterable(IterableBase):
|
||||
def __init__(self):
|
||||
self._loader = season_choices
|
||||
|
||||
|
||||
class PlayerChoiceIterable(IterableBase):
|
||||
def __init__(self):
|
||||
self._loader = player_choice
|
||||
|
||||
|
||||
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
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
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()
|
||||
|
||||
306
estusshots/db.py
Normal file
306
estusshots/db.py
Normal file
@@ -0,0 +1,306 @@
|
||||
import sqlite3
|
||||
import logging as log
|
||||
from typing import Sequence
|
||||
|
||||
from flask import g
|
||||
|
||||
from estusshots import models
|
||||
from estusshots.config import config
|
||||
|
||||
|
||||
class DataBaseError(Exception):
|
||||
"""General exception class for SQL errors"""
|
||||
|
||||
|
||||
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)
|
||||
|
||||
db.row_factory = sqlite3.Row
|
||||
return db
|
||||
|
||||
|
||||
def query_db(query, args=(), one=False, cls=None):
|
||||
"""Runs an SQL query on an new database connection, returning the fetched rv"""
|
||||
log.debug(f"Running query ({query}) with arguments ({args})")
|
||||
cur = connect_db().execute(query, args)
|
||||
rv = cur.fetchall()
|
||||
cur.close()
|
||||
if cls:
|
||||
rv = [cls(**row) for row in rv]
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
|
||||
|
||||
def update_db(query, args=(), return_key: bool = False):
|
||||
"""
|
||||
Runs an changing query on the database
|
||||
Returns either False if no error has occurred, or an sqlite3 Exception
|
||||
:param query: An SQL query string
|
||||
:param args: Tuple for inserting into a row
|
||||
:param return_key: Changes return behavior of the function:
|
||||
If used function will return last row id.
|
||||
Exceptions will be raised instead of returned.
|
||||
"""
|
||||
log.debug(f"Running query ({query}) with arguments ({args})")
|
||||
with connect_db() as con:
|
||||
cur = con.cursor()
|
||||
|
||||
multi_args = any(isinstance(i, tuple) for i in args)
|
||||
|
||||
try:
|
||||
if multi_args:
|
||||
cur.executemany(query, args)
|
||||
else:
|
||||
cur.execute(query, args)
|
||||
except sqlite3.Error as err:
|
||||
if not return_key:
|
||||
return err
|
||||
raise
|
||||
else:
|
||||
con.commit()
|
||||
return cur.lastrowid if return_key else False
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Initialize the database from the 'schema.sql' script file"""
|
||||
file_name = "schema.sql"
|
||||
print(f'Creating database from file: "{file_name}"')
|
||||
with connect_db() as conn:
|
||||
with open(file_name, "r") as f:
|
||||
try:
|
||||
conn.cursor().executescript(f.read())
|
||||
except sqlite3.OperationalError as err:
|
||||
log.error(f"Cannot create database: {err}")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def save_player_query(player):
|
||||
if not player.id:
|
||||
sql = "insert into player values (?, ?, ?, ?, ?)"
|
||||
args = (None, player.real_name, player.alias, player.hex_id, player.anon)
|
||||
else:
|
||||
sql = (
|
||||
"update player " "set real_name=?, alias=?, hex_id=?, anon=? " "where id==?"
|
||||
)
|
||||
args = (player.real_name, player.alias, player.hex_id, player.anon, player.id)
|
||||
return sql, args
|
||||
|
||||
|
||||
def save_player(player):
|
||||
sql, args = save_player_query(player)
|
||||
return update_db(sql, args)
|
||||
|
||||
|
||||
def load_players(id=None):
|
||||
sql = "select * from player"
|
||||
args = ()
|
||||
if id:
|
||||
sql += " where player.id = ?"
|
||||
args = (id,)
|
||||
sql += " order by player.id"
|
||||
return sql, args
|
||||
|
||||
|
||||
def load_drinks(id=None):
|
||||
sql = "select * from drink"
|
||||
args = ()
|
||||
if id:
|
||||
sql += " where drink.id = ?"
|
||||
args = (id,)
|
||||
sql += " order by drink.id"
|
||||
return sql, args
|
||||
|
||||
|
||||
def save_drink_query(drink):
|
||||
if not drink.id:
|
||||
sql = "insert into drink values (?, ?, ?)"
|
||||
args = (None, drink.name, drink.vol)
|
||||
else:
|
||||
sql = "update drink " \
|
||||
"set name=?, vol=? " \
|
||||
"where id==?"
|
||||
args = (drink.name, drink.vol, drink.id)
|
||||
return sql, args
|
||||
|
||||
|
||||
def save_drink(drink):
|
||||
sql, args = save_drink_query(drink)
|
||||
return update_db(sql, args)
|
||||
|
||||
|
||||
def load_enemies(id=None):
|
||||
sql = "select * from enemy"
|
||||
args = ()
|
||||
if id:
|
||||
sql += " where enemy.id = ?"
|
||||
args = (id,)
|
||||
sql += " order by enemy.id"
|
||||
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==?"
|
||||
args = (enemy.name, enemy.boss, enemy.season_id, enemy.id)
|
||||
return sql, args
|
||||
|
||||
|
||||
def save_season_query(season: models.Season):
|
||||
if not season.id:
|
||||
sql = "insert into season values (?, ?, ?, ?, ?, ?)"
|
||||
args = (
|
||||
None,
|
||||
season.game,
|
||||
season.description,
|
||||
season.start,
|
||||
season.end,
|
||||
season.code,
|
||||
)
|
||||
else:
|
||||
sql = (
|
||||
"update season "
|
||||
"set game=?, description=?, start=?, end=?, code=? "
|
||||
"where id==?"
|
||||
)
|
||||
args = (
|
||||
season.game,
|
||||
season.description,
|
||||
season.start,
|
||||
season.end,
|
||||
season.code,
|
||||
season.id,
|
||||
)
|
||||
return sql, args
|
||||
|
||||
|
||||
def load_season(id=None):
|
||||
sql = "select * from season"
|
||||
args = ()
|
||||
if id:
|
||||
sql += " where season.id = ?"
|
||||
args = (id,)
|
||||
sql += " order by season.code"
|
||||
return sql, args
|
||||
|
||||
|
||||
def load_episode(episode_id: int = None):
|
||||
sql = "select * from episode"
|
||||
args = ()
|
||||
if episode_id:
|
||||
sql += " where episode.id = ?"
|
||||
args = (episode_id,)
|
||||
sql += " order by episode.code"
|
||||
return sql, args
|
||||
|
||||
|
||||
def load_episodes(season_id: int = None):
|
||||
sql = "select * from episode"
|
||||
args = ()
|
||||
if season_id:
|
||||
sql += " where episode.season_id = ?"
|
||||
args = (season_id,)
|
||||
sql += " order by episode.code"
|
||||
return sql, args
|
||||
|
||||
|
||||
def load_episode_player_links(episode_id: int):
|
||||
sql = "select * from episode_player where episode_id = ?"
|
||||
args = (episode_id,)
|
||||
return sql, args
|
||||
|
||||
|
||||
def load_episode_players(episode_id: int):
|
||||
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
|
||||
|
||||
|
||||
def save_episode_players(episode_id: int, player_ids: Sequence[int]):
|
||||
sql = "insert into episode_player values (?, ?, ?)"
|
||||
args = tuple((None, episode_id, i) for i in player_ids)
|
||||
return sql, args
|
||||
|
||||
|
||||
def remove_episode_player(episode_id: int, player_ids: Sequence[int]):
|
||||
sql = "delete from episode_player " "where episode_id = ? and player_id = ?"
|
||||
args = tuple((episode_id, pid) for pid in player_ids)
|
||||
return sql, args
|
||||
|
||||
|
||||
def save_episode(episode: models.Episode):
|
||||
if not episode.id:
|
||||
sql = "insert into episode values (?, ?, ?, ?, ?, ?, ?)"
|
||||
args = (
|
||||
None,
|
||||
episode.season_id,
|
||||
episode.title,
|
||||
episode.date,
|
||||
episode.start.timestamp(),
|
||||
episode.end.timestamp(),
|
||||
episode.code,
|
||||
)
|
||||
else:
|
||||
sql = (
|
||||
"update episode "
|
||||
"set season_id=?, title=?, date=?, start=?, end=?, code=?"
|
||||
"where id==?"
|
||||
)
|
||||
args = (
|
||||
episode.season_id,
|
||||
episode.title,
|
||||
episode.date,
|
||||
episode.start.timestamp(),
|
||||
episode.end.timestamp(),
|
||||
episode.code,
|
||||
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
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();
|
||||
90
estusshots/forms.py
Normal file
90
estusshots/forms.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
DateField,
|
||||
TimeField,
|
||||
StringField,
|
||||
SubmitField,
|
||||
BooleanField,
|
||||
DecimalField,
|
||||
SelectField,
|
||||
SelectMultipleField,
|
||||
HiddenField,
|
||||
FieldList,
|
||||
FormField,
|
||||
)
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
|
||||
from estusshots import choices
|
||||
|
||||
|
||||
class SeasonForm(FlaskForm):
|
||||
season_id = HiddenField("Season ID", render_kw={"readonly": True})
|
||||
code = StringField("Season Code", validators=[DataRequired()])
|
||||
game_name = StringField("Game Name", validators=[DataRequired()])
|
||||
description = StringField("Season Description")
|
||||
start = DateField("Season Start", format="%Y-%m-%d", validators=[DataRequired()])
|
||||
end = DateField("Season End", format="%Y-%m-%d", validators=[Optional()])
|
||||
submit_button = SubmitField("Submit")
|
||||
|
||||
|
||||
class EpisodeForm(FlaskForm):
|
||||
season_id = HiddenField("Season ID", render_kw={"readonly": True})
|
||||
episode_id = HiddenField("Episode ID", render_kw={"readonly": True})
|
||||
code = StringField("Episode Code", validators=[DataRequired()])
|
||||
title = StringField("Title", validators=[DataRequired()])
|
||||
date = DateField("Episode Date", format="%Y-%m-%d", validators=[DataRequired()])
|
||||
start = TimeField("Start Time", format="%H:%M", validators=[DataRequired()])
|
||||
end = TimeField("End Time", format="%H:%M", validators=[DataRequired()])
|
||||
players = SelectMultipleField(
|
||||
"Players", coerce=int, choices=choices.PlayerChoiceIterable()
|
||||
)
|
||||
submit_button = SubmitField("Submit")
|
||||
|
||||
|
||||
class PlayerForm(FlaskForm):
|
||||
player_id = HiddenField("Player ID", render_kw={"readonly": True})
|
||||
real_name = StringField("Real Name")
|
||||
alias = StringField("Player Alias", validators=[DataRequired()])
|
||||
hex_id = StringField("Hex ID")
|
||||
anonymize = BooleanField("Anonymize (Show only player alias)")
|
||||
submit_button = SubmitField("Submit")
|
||||
|
||||
|
||||
class DrinkForm(FlaskForm):
|
||||
drink_id = HiddenField("Drink ID", render_kw={"readonly": True})
|
||||
name = StringField("Name", validators=[DataRequired()])
|
||||
vol = DecimalField("Alcohol %", validators=[DataRequired()])
|
||||
submit_button = SubmitField("Submit")
|
||||
|
||||
|
||||
class EnemyForm(FlaskForm):
|
||||
enemy_id = HiddenField("Enemy ID", render_kw={"readonly": True})
|
||||
season_id = SelectField(
|
||||
"Season", choices=choices.SeasonChoiceIterable(), coerce=int
|
||||
)
|
||||
name = StringField("Name", validators=[DataRequired()])
|
||||
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")
|
||||
189
estusshots/models.py
Normal file
189
estusshots/models.py
Normal file
@@ -0,0 +1,189 @@
|
||||
import datetime
|
||||
import enum
|
||||
from numbers import Rational
|
||||
from typing import Dict, List, Union, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from estusshots import forms, util
|
||||
|
||||
|
||||
class EventType(enum.Enum):
|
||||
Pause = 0
|
||||
Death = 1
|
||||
Victory = 2
|
||||
|
||||
|
||||
@dataclass
|
||||
class GenericFormModel:
|
||||
page_title: str
|
||||
form_title: str
|
||||
post_url: str
|
||||
errors: Dict[str, List[str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Player:
|
||||
id: int
|
||||
real_name: str
|
||||
alias: str
|
||||
hex_id: str
|
||||
anon: bool = False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.real_name if self.real_name and not self.anon else self.alias
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
hex_id = str(form.hex_id.data) if form.hex_id.data else None
|
||||
anon = bool(form.anonymize.data)
|
||||
return cls(id=id, real_name=real_name, alias=alias, hex_id=hex_id, anon=anon)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Drink:
|
||||
id: int
|
||||
name: str
|
||||
vol: float
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
|
||||
self = cls(id=id, name=name, vol=vol)
|
||||
return self
|
||||
|
||||
|
||||
@dataclass
|
||||
class Enemy:
|
||||
id: int
|
||||
name: str
|
||||
boss: bool
|
||||
season_id: int
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
season = int(form.season_id.data)
|
||||
|
||||
self = cls(id=id, name=name, boss=boss, season_id=season)
|
||||
return self
|
||||
|
||||
|
||||
@dataclass
|
||||
class Season:
|
||||
id: int
|
||||
game: str
|
||||
description: str
|
||||
start: datetime.date
|
||||
end: datetime.date
|
||||
code: str
|
||||
|
||||
def __post_init__(self):
|
||||
try:
|
||||
self.start = datetime.datetime.strptime(self.start, "%Y-%m-%d").date()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.end = datetime.datetime.strptime(self.end, "%Y-%m-%d").date()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
description = str(form.description.data) if form.description.data else None
|
||||
start = form.start.data
|
||||
end = form.end.data
|
||||
|
||||
self = cls(season_id, game, description, start, end, code)
|
||||
return self
|
||||
|
||||
|
||||
@dataclass
|
||||
class Episode:
|
||||
id: int
|
||||
season_id: int
|
||||
title: str
|
||||
date: Union[datetime.date, Rational]
|
||||
start: Union[datetime.datetime, Rational]
|
||||
end: Union[datetime.datetime, Rational]
|
||||
code: str
|
||||
|
||||
@property
|
||||
def playtime(self):
|
||||
return self.end - self.start
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.date, str):
|
||||
self.date = datetime.datetime.strptime(self.date, util.DATE_FMT).date()
|
||||
if isinstance(self.start, Rational):
|
||||
self.start = datetime.datetime.fromtimestamp(self.start)
|
||||
if isinstance(self.end, Rational):
|
||||
self.end = datetime.datetime.fromtimestamp(self.end)
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
title = str(form.title.data)
|
||||
|
||||
date = form.date.data
|
||||
start = util.combine_datetime(date, form.start.data)
|
||||
end = util.combine_datetime(date, form.end.data)
|
||||
if end < start:
|
||||
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
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()
|
||||
133
estusshots/schema.sql
Normal file
133
estusshots/schema.sql
Normal file
@@ -0,0 +1,133 @@
|
||||
create table if not exists season
|
||||
(
|
||||
id integer not null
|
||||
constraint season_pk
|
||||
primary key autoincrement,
|
||||
game text,
|
||||
description text,
|
||||
start text,
|
||||
end text,
|
||||
code text not null
|
||||
);
|
||||
|
||||
create unique index if not exists season_id_uindex
|
||||
on season (id);
|
||||
|
||||
create table if not exists player
|
||||
(
|
||||
id integer not null
|
||||
constraint player_pk
|
||||
primary key autoincrement,
|
||||
real_name text,
|
||||
alias text not null,
|
||||
hex_id text,
|
||||
anon integer not null
|
||||
);
|
||||
|
||||
create unique index if not exists player_id_uindex
|
||||
on player (id);
|
||||
|
||||
|
||||
create table if not exists drink
|
||||
(
|
||||
id integer not null
|
||||
constraint drink_pk
|
||||
primary key autoincrement,
|
||||
name text not null,
|
||||
vol real not null
|
||||
);
|
||||
|
||||
create unique index if not exists drink_id_uindex
|
||||
on drink (id);
|
||||
|
||||
|
||||
create table if not exists enemy
|
||||
(
|
||||
id integer not null
|
||||
constraint enemy_pk
|
||||
primary key autoincrement,
|
||||
name text not null,
|
||||
boss integer not null,
|
||||
season_id integer,
|
||||
|
||||
foreign key (season_id) references season(id)
|
||||
);
|
||||
|
||||
create unique index if not exists enemy_id_uindex
|
||||
on enemy (id);
|
||||
|
||||
|
||||
|
||||
create table if not exists episode
|
||||
(
|
||||
id integer not null
|
||||
constraint episode_pk
|
||||
primary key autoincrement,
|
||||
season_id integer not null
|
||||
constraint episode_season_id_fk
|
||||
references season,
|
||||
title text not null,
|
||||
date text not null,
|
||||
start timestamp not null,
|
||||
end timestamp not null,
|
||||
code text not null default 'EXX'
|
||||
);
|
||||
|
||||
create unique index if not exists episode_id_uindex
|
||||
on episode (id);
|
||||
|
||||
create table if not exists episode_player
|
||||
(
|
||||
link_id integer not null
|
||||
constraint episode_player_pk
|
||||
primary key autoincrement,
|
||||
episode_id integer not null
|
||||
references episode,
|
||||
player_id integer not null
|
||||
references player
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
BIN
estusshots/static/OptimusPrinceps.ttf
Normal file
BIN
estusshots/static/OptimusPrinceps.ttf
Normal file
Binary file not shown.
BIN
estusshots/static/OptimusPrincepsSemiBold.ttf
Normal file
BIN
estusshots/static/OptimusPrincepsSemiBold.ttf
Normal file
Binary file not shown.
74
estusshots/static/custom.css
Normal file
74
estusshots/static/custom.css
Normal file
@@ -0,0 +1,74 @@
|
||||
@font-face {
|
||||
font-family: 'DarkSouls';
|
||||
src: url('/static/OptimusPrinceps.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'DarkSoulsBold';
|
||||
src: url('/static/OptimusPrincepsSemiBold.ttf');
|
||||
}
|
||||
|
||||
/* Some Colors */
|
||||
a {
|
||||
color: #b52828
|
||||
}
|
||||
a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #fd5454;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #b52828;
|
||||
border-color: #b52828;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #fd5454;
|
||||
border-color: #fd5454;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background-color: #3b3b3b;
|
||||
border-color: #3b3b3b;
|
||||
}
|
||||
.btn-default:hover {
|
||||
background-color: #3b3b3b;
|
||||
border-color: #fd5454;
|
||||
}
|
||||
|
||||
.icon-primary{
|
||||
color: #b52828
|
||||
}
|
||||
|
||||
.brand-icon{
|
||||
height: 40px;
|
||||
}
|
||||
.navbar-brand{
|
||||
font-family: 'DarkSoulsBold', serif;
|
||||
}
|
||||
.nav-link {
|
||||
font-family: 'DarkSouls', serif;
|
||||
}
|
||||
|
||||
.btn-toolbar{
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
min-height: 100%; /* Fallback for browsers do NOT support vh unit */
|
||||
min-height: 100vh; /* These two lines are counted as one :-) */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Theme customizations */
|
||||
.list-group-item:hover {
|
||||
color: inherit;
|
||||
background-color: rgb(34, 34, 34);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
14
estusshots/static/dark_theme.css
Normal file
14
estusshots/static/dark_theme.css
Normal file
File diff suppressed because one or more lines are too long
BIN
estusshots/static/favicon.ico
Normal file
BIN
estusshots/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
estusshots/static/img/brand.png
Normal file
BIN
estusshots/static/img/brand.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
BIN
estusshots/static/img/elite.png
Normal file
BIN
estusshots/static/img/elite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
BIN
estusshots/static/img/estus.png
Normal file
BIN
estusshots/static/img/estus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 292 KiB |
BIN
estusshots/static/img/solaire.png
Normal file
BIN
estusshots/static/img/solaire.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 251 KiB |
BIN
estusshots/static/img/solaire300x300.png
Normal file
BIN
estusshots/static/img/solaire300x300.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
20
estusshots/static/js/estus-shots.js
Normal file
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
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
estusshots/static/moment-with-locals.js
Normal file
2
estusshots/static/moment-with-locals.js
Normal file
File diff suppressed because one or more lines are too long
4603
estusshots/static/moment.js
Normal file
4603
estusshots/static/moment.js
Normal file
File diff suppressed because it is too large
Load Diff
68
estusshots/templates/base.html
Normal file
68
estusshots/templates/base.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends "bootstrap/base.html" %}
|
||||
|
||||
{% 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">
|
||||
<link rel="stylesheet" href="/static/dark_theme.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block navbar %}
|
||||
{% set nav_bar = [
|
||||
('/season', 'seasons', 'Seasons', 'fas fa-calendar'),
|
||||
('/player', 'players', 'Players', 'fas fa-users'),
|
||||
('/enemy', 'enemies', 'Enemies', 'fas fa-skull-crossbones'),
|
||||
('/drink', 'drinks', 'Drinks', 'fas fa-beer')
|
||||
]-%}
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
|
||||
|
||||
<a class="navbar-brand">
|
||||
<img class="brand-icon" alt="" src="/static/img/brand.png">
|
||||
Estus Shots
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
{% for href, id, caption, icon in nav_bar %}
|
||||
<li class="nav-item{% if id == active_page %} active{% endif %}">
|
||||
<a class="nav-link" href="{{ href|e }}">
|
||||
<i class="{{ icon|e }} icon-primary"></i> {{ caption|e }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul class="nav navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/logout">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% block page %}
|
||||
|
||||
{% endblock %}
|
||||
<div>
|
||||
{% block footer %}
|
||||
© Sauf Software by D⁵: <a href="#">Durstiger Donnerstag Digital Drinking Divison</a>.
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
48
estusshots/templates/drink_list.html
Normal file
48
estusshots/templates/drink_list.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}drink_save
|
||||
{% set active_page = "drinks" %}
|
||||
{% block title %}Drinks {{ super() }}{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
{% if g.is_editor %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-primary" href="{{ url_for('new_drink') }}" role="button">
|
||||
<span class="fas fa-plus"></span> Drink
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not model.drinks %}
|
||||
There are no drinks.
|
||||
{% else %}
|
||||
<table class="table table-hover table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for prop, caption in model.columns %}
|
||||
<th scope="col" class="col-sm-auto text-center">{{ caption }}</th>
|
||||
{% endfor %}
|
||||
|
||||
{% if g.is_editor %}
|
||||
<th scope="col" class="col-sm-auto text-center"></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for drink in model.drinks %}
|
||||
<tr>
|
||||
{% for prop, caption in model.columns %}
|
||||
<td class="col-sm-auto text-center">{{ drink[prop] }}</td>
|
||||
{% endfor %}
|
||||
|
||||
{% if g.is_editor %}
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-default" href="{{ url_for('drink_edit', drink_id = drink.id) }}">
|
||||
<span class="fas fa-pencil-alt"></span>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
52
estusshots/templates/enemies.html
Normal file
52
estusshots/templates/enemies.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "enemies" %}
|
||||
{% block title %}Enemies {{ super() }}{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
{% if g.is_editor %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-primary" href="{{ url_for('enemy_new') }}" role="button">
|
||||
<span class="fas fa-plus"></span> Enemy
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not model.enemies %}
|
||||
There are no enemies.
|
||||
{% else %}
|
||||
<table class="table table-hover table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<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 %}
|
||||
<th scope="col" class="col-sm-auto text-center">Editor</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for enemy in model.enemies %}
|
||||
<tr>
|
||||
<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 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 = enemy.id) }}">
|
||||
<span class="fas fa-pencil-alt"></span>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
133
estusshots/templates/episode_details.html
Normal file
133
estusshots/templates/episode_details.html
Normal file
@@ -0,0 +1,133 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "seasons" %}
|
||||
{% block title %}{{ model.title }} {{ super() }}{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
{% if g.is_editor %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-primary" role="button"
|
||||
href="{{ url_for('episode_edit', season_id = model.season.id, episode_id = model.episode.id) }}">
|
||||
<span class="fas fa-pencil-alt"></span> Edit Episode
|
||||
</a>
|
||||
<a class="btn btn-primary" role="button"
|
||||
href="{{ url_for("event_new", ep_id = model.episode.id, s_id = model.season.id) }}" >
|
||||
<span class="fas fa-plus"></span> Event
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
|
||||
<!--region Info Card-->
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
{{ model.episode.code }}: {{ model.episode.title }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-text">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">Date:</span>
|
||||
{{ model.episode.date }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">Start:</span>
|
||||
{{ model.episode.start|format_time }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">End:</span>
|
||||
{{ model.episode.end|format_time }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">Play Time:</span>
|
||||
{{ model.episode.playtime }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">Enemies Defeated:</span>
|
||||
{{ model.events.victory_count }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">Deaths:</span>
|
||||
{{ model.events.defeat_count }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--endregion-->
|
||||
|
||||
<hr>
|
||||
|
||||
<!--region Player Card-->
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
Players in this episode
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="col-sm-auto text-center">Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for player in model.players %}
|
||||
<tr>
|
||||
<td class="col-sm-auto text-center">{{ player.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--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 %}
|
||||
58
estusshots/templates/episode_list.html
Normal file
58
estusshots/templates/episode_list.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "seasons" %}
|
||||
{% block title %}{{ model.season_code }} - Episodes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if g.is_editor %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-primary" href="{{ url_for("episode_new", season_id = model.season_id)}}" role="button">
|
||||
<span class="fas fa-plus"></span>Episode
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not model.player_list %}
|
||||
<div class="alert alert-info">There are no episodes.</div>
|
||||
{% else %}
|
||||
<table class="table table-hover table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="col-sm-auto text-center">#</th>
|
||||
<th scope="col" class="col-sm-auto text-center">Title</th>
|
||||
<th scope="col" class="col-sm-auto text-center">Date</th>
|
||||
<th scope="col" class="col-sm-auto text-center">From - To</th>
|
||||
<th scope="col" class="col-sm-auto text-center">Playtime</th>
|
||||
|
||||
<th scope="col" class="col-sm-auto text-center">Stats</th>
|
||||
{% if g.is_editor %}
|
||||
<th scope="col" class="col-sm-auto text-center">Editor</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in model.episodes %}
|
||||
<tr>
|
||||
<td class="col-sm-auto text-center">{{ item.code }}</td>
|
||||
<td class="col-sm-auto text-center">{{ item.title }}</td>
|
||||
<td class="col-sm-auto text-center">{{ item.date }}</td>
|
||||
<td class="col-sm-auto text-center">{{ item.start }} - {{ item.end }}</td>
|
||||
<td class="col-sm-auto text-center">{{ item.duration }}</td>
|
||||
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-default" href="{{ url_for("episode_stats", episode_id = item.id) }}">
|
||||
<span class="fas fa-eye"></span>
|
||||
</a>
|
||||
</td>
|
||||
{% if g.is_editor %}
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-default" href="{{ url_for("episode_edit", episode_id = item.id, season_id = model.season_id) }}">
|
||||
<span class="fas fa-pencil-alt"></span>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
95
estusshots/templates/event_editor.html
Normal file
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 %}
|
||||
25
estusshots/templates/generic_form.html
Normal file
25
estusshots/templates/generic_form.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "bootstrap/wtf.html" as wtf %}
|
||||
{% set active_page = model.active_page %}
|
||||
{% 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">
|
||||
{{ wtf.quick_form(form, button_map={'submit_button': 'primary'}, form_type='horizontal', horizontal_columns=('lg', 2, 10)) }}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
37
estusshots/templates/login.html
Normal file
37
estusshots/templates/login.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends "bootstrap/base.html" %}
|
||||
|
||||
{% block title %}Estus Shots{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="/static/dark_theme.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block body_attribs %}
|
||||
style="background-image: url(/static/img/elite.png), url(/static/img/solaire300x300.png);
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-position: left bottom, right bottom;"
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container vertical-center">
|
||||
<div class="card text-center mx-auto" style="width: 15rem">
|
||||
|
||||
<img src="/static/img/estus.png" alt="Login" style="height: 15rem">
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" class="form-control">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Enter</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
46
estusshots/templates/player_list.html
Normal file
46
estusshots/templates/player_list.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "players" %}
|
||||
{% block title %}Players {{ super() }}{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
{% if g.is_editor %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-primary" href="{{ url_for('player_new') }}" role="button"><span class="fas fa-plus"></span> Player</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not model.player_list %}
|
||||
There are no players.
|
||||
{% else %}
|
||||
<table class="table table-hover table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for prop, caption in model.columns %}
|
||||
<th scope="col" class="col-sm-auto text-center">{{ caption }}</th>
|
||||
{% endfor %}
|
||||
|
||||
{% if g.is_editor %}
|
||||
<th scope="col" class="col-sm-auto text-center">Edit</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for player in model.player_list %}
|
||||
<tr>
|
||||
{% for prop, caption in model.columns %}
|
||||
<td class="col-sm-auto text-center">{{ player[prop] }}</td>
|
||||
{% endfor %}
|
||||
|
||||
{% if g.is_editor %}
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-default" href="{{ url_for('player_edit', player_id = player.id) }}">
|
||||
<span class="fas fa-pencil-alt"></span>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
59
estusshots/templates/season_list.html
Normal file
59
estusshots/templates/season_list.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "seasons" %}
|
||||
{% block title %}Seasons {{ super() }}{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
{% if g.is_editor %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-primary" href="{{ url_for("season_new") }}" role="button"><span class="fas fa-plus"></span> Season</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not model.seasons %}
|
||||
There are no seasons.
|
||||
{% else %}
|
||||
<table class="table table-hover table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for prop, caption in model.columns %}
|
||||
<th scope="col" class="col-sm-auto text-center">{{ caption }}</th>
|
||||
{% endfor %}
|
||||
|
||||
{# Show #}
|
||||
<th scope="col" class="col-sm-auto text-center">View</th>
|
||||
|
||||
{% if g.is_editor %}
|
||||
{# Edit #}
|
||||
<th scope="col" class="col-sm-auto text-center">Editor</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in model.seasons %}
|
||||
<tr>
|
||||
{% for prop, caption in model.columns %}
|
||||
<td class="col-sm-auto text-center">{{ item[prop] }}</td>
|
||||
{% endfor %}
|
||||
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-default" href="{{ url_for("season_overview", season_id = item.id) }}">
|
||||
<span class="fas fa-eye"></span></a>
|
||||
</td>
|
||||
|
||||
{% if g.is_editor %}
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-default" href="{{ url_for('season_edit', season_id = item.id) }}">
|
||||
<span class="fas fa-pencil-alt"></span>
|
||||
</a>
|
||||
|
||||
<a class="btn btn-default" href="{{ url_for("episode_new", season_id = item.id) }}">
|
||||
<span class="fas fa-plus"></span> Episode
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
83
estusshots/templates/season_overview.html
Normal file
83
estusshots/templates/season_overview.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "seasons" %}
|
||||
{% block title %}{{ model.title }} {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{# Overview #}
|
||||
<div class="col-4">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
{{ model.title }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Infos</h5>
|
||||
{% for item in model.season_info %}
|
||||
<div class="card-text">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">{{ item }}:</span>
|
||||
{{ model.season_info[item] }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Episode List #}
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">Episodes</div>
|
||||
<div class="card-body">
|
||||
{% if not model.episodes %}
|
||||
<div class="alert alert-info">No Episodes in this Season</div>
|
||||
{% else %}
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="col-sm-auto text-center">#</th>
|
||||
<th scope="col" class="col-sm-auto text-center">Date</th>
|
||||
<th scope="col" class="col-sm-auto text-center">Title</th>
|
||||
|
||||
<th scope="col" class="col-sm-auto text-center">View</th>
|
||||
{% if g.is_editor %}
|
||||
<th scope="col" class="col-sm-auto text-center">Editor</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in model.episodes %}
|
||||
<tr>
|
||||
<td class="col-sm-auto text-center">{{ item.code }}</td>
|
||||
<td class="col-sm-auto text-center">{{ item.date }}</td>
|
||||
<td class="col-sm-auto text-center">{{ item.title }}</td>
|
||||
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-default" href="{{ url_for('episode_detail', season_id = item.season_id, episode_id = item.id) }}">
|
||||
<span class="fas fa-eye"></span></a>
|
||||
</td>
|
||||
|
||||
{% if g.is_editor %}
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-default" href="{{ url_for('episode_edit', season_id = item.season_id, episode_id = item.id) }}">
|
||||
<span class="fas fa-pencil-alt"></span>
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
120
estusshots/util.py
Normal file
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
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
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
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
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
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
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
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)
|
||||
Reference in New Issue
Block a user