Rewrite data layer using SqlAlchemy Part 1

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

36
estusshots/__init__.py Normal file
View File

@@ -0,0 +1,36 @@
import os
import logging
import sys
from flask import Flask
from flask_bootstrap import Bootstrap
from estusshots.config import config
def create_app():
app = Flask(__name__)
Bootstrap(app)
return app
if not config.SECRET_KEY:
logging.error(
"No secret key provided for app. Are the environment variables set correctly?"
)
sys.exit(1)
app = create_app()
logging.basicConfig(filename=config.LOG_PATH, level=logging.DEBUG)
logging.info(f"Starting in working dir: {os.getcwd()}")
app.config.from_object(config)
import estusshots.views.drinks
import estusshots.views.enemies
import estusshots.views.episodes
import estusshots.views.events
import estusshots.views.login
import estusshots.views.players
import estusshots.views.seasons

80
estusshots/choices.py Normal file
View 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
View File

@@ -0,0 +1,6 @@
[Default]
ES_READ_PW = 123
ES_WRITE_PW = 1234
ES_SECRET_KEY = 1234
ES_DATABASE_PATH = ../databases/debug.db
ES_LOG_PATH = ../logs/debug.log

16
estusshots/config.py Normal file
View File

@@ -0,0 +1,16 @@
from configparser import ConfigParser
class Config:
def __init__(self):
parser = ConfigParser()
parser.read("config.ini")
self.SECRET_KEY = parser.get("Default", "ES_SECRET_KEY")
self.WRITE_PW = parser.get("Default", "ES_WRITE_PW")
self.READ_PW = parser.get("Default", "ES_READ_PW")
self.DATABASE_PATH = parser.get("Default", "ES_DATABASE_PATH")
self.LOG_PATH = parser.get("Default", "ES_LOG_PATH")
config = Config()

306
estusshots/db.py Normal file
View 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
View File

@@ -0,0 +1,17 @@
class EditorModule{
setCurrentTime = (elemId: string) => {
const elem = document.getElementById(elemId) as HTMLInputElement;
if (!elem) return;
elem.value = this.currentTimeHHMM()
};
currentTimeHHMM = () => {
const d = new Date();
const hours = (d.getHours()<10 ? '0' : '') + d.getHours();
const minutes = (d.getMinutes()<10 ? '0' : '') + d.getMinutes();
return `${hours}:${minutes}`;
};
}
const editorModule = new EditorModule();

90
estusshots/forms.py Normal file
View 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
View 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
View File

@@ -0,0 +1,154 @@
import enum
import sqlalchemy
from sqlalchemy import create_engine, ForeignKey, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Boolean, Float, Enum, Date, Time
from sqlalchemy.orm import sessionmaker, relationship
from estusshots import util, forms
engine = create_engine('sqlite:///../databases/test.db')
Base = declarative_base()
class EventType(enum.Enum):
Pause = 0
Death = 1
Victory = 2
class Player(Base):
__tablename__ = "players"
id = Column(Integer, primary_key=True)
real_name = Column(String)
alias = Column(String)
hex_id = Column(String)
anon = Column(Boolean, default=False)
events = relationship("Event", back_populates="player")
@property
def name(self) -> str:
return self.real_name if self.real_name and not self.anon else self.alias
def populate_from_form(self, form: "forms.PlayerForm"):
self.real_name = str(form.real_name.data) if form.real_name.data else None
self.alias = str(form.alias.data)
self.hex_id = str(form.hex_id.data) if form.hex_id.data else None
self.anon = bool(form.anonymize.data)
class Drink(Base):
__tablename__ = "drinks"
id = Column(Integer, primary_key=True)
name = Column(String)
vol = Column(Float)
def populate_from_form(self, form: "forms.DrinkForm"):
self.name = str(form.name.data)
self.vol = float(form.vol.data)
class Season(Base):
__tablename__ = "seasons"
id = Column(Integer, primary_key=True)
code = Column(String, default='SXX')
game = Column(String)
description = Column(String)
start = Column(Date)
end = Column(Date)
episodes = relationship("Episode", back_populates="season")
enemies = relationship("Enemy", back_populates="season")
def populate_from_form(self, form: "forms.SeasonForm"):
self.code = str(form.code.data)
self.game = str(form.game_name.data)
self.description = str(form.description.data) if form.description.data else None
self.start = form.start.data
self.end = form.end.data
class Enemy(Base):
__tablename__ = "enemies"
id = Column(Integer, primary_key=True)
name = Column(String)
boss = Column(Boolean, default=True)
season_id = Column(Integer, ForeignKey('seasons.id'))
season = relationship("Season", back_populates="enemies")
events = relationship('Event', back_populates="enemy")
def populate_from_form(self, form: "forms.EnemyForm"):
self.name = str(form.name.data)
self.boss = bool(form.is_boss.data)
self.season_id = int(form.season_id.data)
class Episode(Base):
__tablename__ = "episodes"
id = Column(Integer, primary_key=True)
code = Column(String, default='EXX')
title = Column(String)
date = Column(Date)
start = Column(Time)
end = Column(Time)
season_id = Column(Integer, ForeignKey('seasons.id'))
season = relationship("Season", back_populates="episodes")
events = relationship('Event', back_populates='episode')
@property
def playtime(self):
return util.timedelta(self.start, self.end)
class Event(Base):
__tablename__ = 'events'
id = Column(Integer, primary_key=True)
type = Column(Enum(EventType))
time = Column(Time)
comment = Column(String)
episode_id = Column(Integer, ForeignKey('episodes.id'))
episode = relationship('Episode', back_populates='events')
player_id = Column(Integer, ForeignKey('players.id'))
player = relationship('Player', back_populates='events')
enemy_id = Column(Integer, ForeignKey('enemies.id'))
enemy = relationship('Enemy', back_populates='events')
penalties = relationship('Penalty', back_populates='event')
class Penalty(Base):
__tablename__ = 'penalties'
id = Column(Integer, primary_key=True)
player_id = Column(Integer, ForeignKey('players.id'))
player = relationship('Player')
drink_id = Column(Integer, ForeignKey('drinks.id'))
drink = relationship('Drink')
event_id = Column(Integer, ForeignKey('events.id'))
event = relationship('Event', back_populates='penalties')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
def new_session() -> sqlalchemy.orm.Session:
"""Open up a new session. This function exists for ease of use, as the return type is hinted for the IDE."""
return Session()

133
estusshots/schema.sql Normal file
View 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
);

Binary file not shown.

Binary file not shown.

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

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -0,0 +1,20 @@
var EditorModule = (function () {
function EditorModule() {
var _this = this;
this.setCurrentTime = function (elemId) {
var elem = document.getElementById(elemId);
if (!elem)
return;
elem.value = _this.currentTimeHHMM();
};
this.currentTimeHHMM = function () {
var d = new Date();
var hours = (d.getHours() < 10 ? '0' : '') + d.getHours();
var minutes = (d.getMinutes() < 10 ? '0' : '') + d.getMinutes();
return hours + ":" + minutes;
};
}
return EditorModule;
}());
var editorModule = new EditorModule();
//# sourceMappingURL=estus-shots.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"estus-shots.js","sourceRoot":"","sources":["../../estus-shots.ts"],"names":[],"mappings":"AAAA;IAAA;QAAA,iBAcC;QAZC,mBAAc,GAAG,UAAC,MAAc;YAC9B,IAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAqB,CAAC;YACjE,IAAI,CAAC,IAAI;gBAAE,OAAO;YAClB,IAAI,CAAC,KAAK,GAAG,KAAI,CAAC,eAAe,EAAE,CAAA;QACrC,CAAC,CAAC;QAEF,oBAAe,GAAG;YAChB,IAAM,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,IAAM,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,GAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC1D,IAAM,OAAO,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,GAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC;YAChE,OAAU,KAAK,SAAI,OAAS,CAAC;QAC/B,CAAC,CAAC;IACJ,CAAC;IAAD,mBAAC;AAAD,CAAC,AAdD,IAcC;AAED,IAAM,YAAY,GAAG,IAAI,YAAY,EAAE,CAAC"}

File diff suppressed because one or more lines are too long

4603
estusshots/static/moment.js Normal file

File diff suppressed because it is too large Load Diff

View 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 %}
&copy; Sauf Software by D⁵: <a href="#">Durstiger Donnerstag Digital Drinking Divison</a>.
{% endblock %}
</div>
</div>
{% endblock %}

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

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

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

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

View File

@@ -0,0 +1,95 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% set active_page = "seasons" %}
{% block title %}{{ model.page_title }} {{ super() }}{% endblock %}
{% block page %}
<div class="text-center">
<h1>{{ model.form_title }}</h1>
{% if model.errors %}
<div class="alert alert-danger">
{% for field, errors in model.errors.items() %}
<div>
<strong class="text-capitalize">{{ field }}</strong>:
{{ errors|join(', ') }}
</div>
{% endfor %}
</div>
{% endif %}
<form action="{{ model.post_url }}" method="post">
{{ form.hidden_tag() }}
<div class="form-group row required">
<div class="col-lg-2">
{{ form.event_type.label(class_="form-control-label") }}
</div>
<div class="col-lg-10">
{{ form.event_type(class_="form-control") }}
</div>
</div>
<div class="form-group row required">
<div class="col-lg-2">
{{ form.time.label(class_="form-control-label") }}
</div>
<div class="col-lg-10 row">
<div class="col">
{{ form.time(class_="form-control") }}
</div>
<div class="col">
<button type="button" class="btn btn-default btn-block"
onclick="editorModule.setCurrentTime('time')">Now</button>
</div>
</div>
</div>
<div class="form-group row required">
<div class="col-lg-2">
{{ form.player.label(class_="form-control-label") }}
</div>
<div class="col-lg-10">
{{ form.player(class_="form-control") }}
</div>
</div>
<div class="form-group row required">
<div class="col-lg-2">
{{ form.enemy.label(class_="form-control-label") }}
</div>
<div class="col-lg-10">
{{ form.enemy(class_="form-control") }}
</div>
</div>
<div class="form-group row">
<div class="col-lg-2">
{{ form.comment.label(class_="form-control-label") }}
</div>
<div class="col-lg-10">
{{ form.comment(class_="form-control") }}
</div>
</div>
<div class="penalty-container">
{% for penalty in form.penalties %}
{{ penalty.hidden_tag() }}
<div class="penalty-item">
{{ penalty.penalty_id }}
{{ penalty.player_id }}
{{ penalty.player }}
<label class="form-control-label">{{ penalty.player.data }}</label>
{{ penalty.drink }}
</div>
{% endfor %}
</div>
<div class="offset-lg-2 col-lg-10">
{{ form.submit_button(class_="btn btn-primary") }}
</div>
</form>
</div>
{% endblock %}

View File

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

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

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

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

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

@@ -0,0 +1,120 @@
import functools
from datetime import datetime, time, date, timedelta
from flask import g, session, redirect
from estusshots import config, app, db
TIME_FMT = "%H:%M"
DATE_FMT = "%Y-%m-%d"
def str_to_datetime(data: str) -> datetime:
"""
Convert %H:%M formatted string into a python datetime object
"""
data = ":".join(data.split(":")[:2])
return datetime.strptime(data, TIME_FMT)
def datetime_time_str(data: datetime) -> str:
"""
Convert a datetime object into a formatted string for display
:param data: datetime
:return: str
"""
return data.strftime(TIME_FMT)
def timedelta_to_str(data: timedelta) -> str:
"""
Remove second and microsecond portion from timedeltas for display
:param data: datetime.timedelta
:return: str
"""
return str(
data - timedelta(seconds=data.seconds, microseconds=data.microseconds)
)
def timedelta(start: time, end: time) -> float:
startDateTime = datetime.combine(date.today(), start)
# Check if the the end is still on the same day
if start.hour > end.hour:
base = date.today() + timedelta(days=1)
else:
base = date.today()
endDateTime = datetime.combine(base, end)
difference = startDateTime - endDateTime
difference_hours = difference.total_seconds() / 3600
return difference_hours
def combine_datetime(date: datetime.date, time: datetime.time):
"""
Combine a date and time object into a datetime object
"""
return datetime(
date.year,
date.month,
date.day,
time.hour,
time.minute,
time.second,
time.microsecond,
)
def get_user_type(password):
# TODO password hashing?
if password == config.WRITE_PW:
return "editor"
if password == config.READ_PW:
return "readonly"
return False
def set_user_role(data):
"""Set the users role in the flask g object for later usage"""
g.is_editor = data == "editor"
def authorize(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
set_user_role(session["user_type"])
except KeyError:
return redirect("/login")
return func(*args, **kwargs)
return wrapper
@app.template_filter("format_time")
def format_time(value):
"""Make the datetime to time string formatting available to jinja2"""
if value is None:
return ""
return datetime_time_str(value)
@app.template_filter("format_timedelta")
def format_timedelta(value):
"""Make formatting for timedeltas available to jinja2"""
if value is None:
return ""
return timedelta_to_str(value)
@app.cli.command("initdb")
def init_db_command():
"""Initializes the database."""
db.init_db()
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, "_database", None)
if db is not None:
db.close()

View File

@@ -0,0 +1,72 @@
from flask import render_template, redirect
from estusshots import app
from estusshots import forms, models, orm
from estusshots.util import authorize
from estusshots.orm import Drink
@app.route("/drink")
@authorize
def drink_list():
db = orm.new_session()
drinks = db.query(Drink).order_by(Drink.name).all()
model = {
"drinks": drinks,
"columns": [("name", "Drink Name"), ("vol", "Alcohol %")],
"controls": [("edit", "Edit")],
}
return render_template("drink_list.html", model=model)
@app.route("/drink/<drink_id>/edit", methods=["GET"])
@authorize
def drink_edit(drink_id: int):
db = orm.new_session()
drink = db.query(Drink).filter(Drink.id == drink_id).first()
form = forms.DrinkForm()
form.drink_id.data = drink.id
form.name.data = drink.name
form.vol.data = drink.vol
model = models.GenericFormModel(
page_title="Edit Drink",
form_title=f'Edit Drink "{drink.name}"',
post_url="/drink/save",
)
return render_template("generic_form.html", model=model, form=form)
@app.route("/drink/new", methods=["GET"])
@authorize
def new_drink():
form = forms.DrinkForm()
model = models.GenericFormModel(
page_title="New Drink",
form_title=f"Create a new Drink",
post_url="/drink/save",
)
return render_template("generic_form.html", model=model, form=form)
@app.route("/drink/save", methods=["POST"])
@authorize
def drink_save():
form = forms.DrinkForm()
if form.validate_on_submit():
drink_id = int(form.drink_id.data) if form.drink_id.data else None
db = orm.new_session()
if drink_id:
drink = db.query(Drink).filter(Drink.id == drink_id).first()
else:
drink = Drink()
db.add(drink)
drink.populate_from_form(form)
err = db.commit()
return redirect("/drink")
model = models.GenericFormModel(
page_title="Drinks", form_title="Edit Drink", post_url="/drink/save"
)
return render_template("generic_form.html", model=model, form=form)

View File

@@ -0,0 +1,72 @@
from flask import render_template, request, redirect
from estusshots import app
from estusshots import forms, models, orm
from estusshots.util import authorize
from estusshots.orm import Enemy
from sqlalchemy.orm import subqueryload
@app.route("/enemy")
@authorize
def enemy_list():
db = orm.new_session()
enemies = db.query(Enemy).options(subqueryload(Enemy.season)).order_by(Enemy.name).all()
model = {"enemies": enemies}
return render_template("enemies.html", model=model)
@app.route("/enemy/new", methods=["GET"])
@authorize
def enemy_new(preselect_season=None):
form = forms.EnemyForm()
if preselect_season:
form.season_id.default = preselect_season
model = models.GenericFormModel(
page_title="Enemies",
form_title="Create a new Enemy",
post_url=f"/enemy/null/edit",
)
return render_template("generic_form.html", model=model, form=form)
@app.route("/enemy/<enemy_id>/edit", methods=["GET", "POST"])
@authorize
def enemy_edit(enemy_id: int):
model = models.GenericFormModel(
page_title="Enemies",
form_title="Edit Enemy",
post_url=f"/enemy/{enemy_id}/edit",
)
if request.method == "GET":
db = orm.new_session()
enemy = db.query(Enemy).filter(Enemy.id == enemy_id).first()
form = forms.EnemyForm()
form.season_id.data = enemy.season_id if enemy.season_id else -1
form.name.data = enemy.name
form.is_boss.data = enemy.boss
form.enemy_id.data = enemy_id
model.form_title = f'Edit Enemy "{enemy.name}"'
return render_template("generic_form.html", model=model, form=form)
else:
form = forms.EnemyForm()
if form.validate_on_submit():
db = orm.new_session()
enemy = db.query(Enemy).filter(Enemy.id == enemy_id).first()
if not enemy:
enemy = Enemy()
db.add(enemy)
enemy.populate_from_form(form)
db.commit()
if form.submit_continue_button.data:
form.name.data = None
return enemy_new(preselect_season=enemy.season_id)
return redirect("/enemy")
model.form_title = "Incorrect Data"
return render_template("generic_form.html", model=model, form=form)

View File

@@ -0,0 +1,137 @@
from typing import List
from flask import render_template, request, redirect
from estusshots import app
from estusshots import forms, models, db
from estusshots.util import authorize
@app.route("/season/<season_id>/episode/<episode_id>")
@authorize
def episode_detail(season_id: int, episode_id: int):
sql, args = db.load_season(season_id)
season = db.query_db(sql, args, one=True, cls=models.Season)
sql, args = db.load_episode(episode_id)
episode = db.query_db(sql, args, one=True, cls=models.Episode)
sql, args = db.load_episode_players(episode_id)
ep_players = db.query_db(sql, args, cls=models.Player)
sql, args = db.load_events(episode_id)
ep_events: List[models.Event] = db.query_db(sql, args, cls=models.Event)
sql, args = db.load_enemies(season_id)
enemies = db.query_db(sql, args, cls=models.Enemy)
deaths = [ev for ev in ep_events if ev.type == models.EventType.Death]
entries = []
for death in deaths:
entries.append({
"time": death.time.time(),
"type": death.type,
"player_name": [p.name for p in ep_players if p.id == death.player_id],
"enemy_name": [e.name for e in enemies if e.id == death.enemy_id]
})
events = None
if ep_events:
events = {"entries": death, "victory_count": 0, "defeat_count": 0}
model = {
"title": f"{season.code}{episode.code}",
"episode": episode,
"season": season,
"players": ep_players,
"events": events,
}
return render_template("episode_details.html", model=model)
@app.route("/season/<season_id>/episode", methods=["GET"])
@authorize
def episode_list(season_id: int):
sql, args = db.load_season(season_id)
season = db.query_db(sql, args, one=True, cls=models.Season)
sql, args = db.load_episodes(season_id)
episodes = db.query_db(sql, args, cls=models.Episode)
model = {"season_id": season_id, "season_code": season.code}
return render_template("episode_list.html", model=model)
@app.route("/season/<season_id>/episode/new", methods=["GET"])
@authorize
def episode_new(season_id: int):
model = models.GenericFormModel(
page_title="New Episode",
form_title="Create New Episode",
post_url=f"/season/{season_id}/episode/null/edit",
)
form = forms.EpisodeForm(request.form)
form.season_id.data = season_id
return render_template("generic_form.html", model=model, form=form)
@app.route("/season/<season_id>/episode/<episode_id>/edit", methods=["GET", "POST"])
@authorize
def episode_edit(season_id: int, episode_id: int):
model = models.GenericFormModel(
page_title="Edit Episode",
form_title="Edit Episode",
post_url=f"/season/{season_id}/episode/{episode_id}/edit",
)
if request.method == "GET":
sql, args = db.load_episode(episode_id)
episode: models.Episode = db.query_db(sql, args, one=True, cls=models.Episode)
sql, args = db.load_episode_players(episode_id)
ep_players = db.query_db(sql, args, cls=models.Player)
form = forms.EpisodeForm()
form.season_id.data = episode.season_id
form.episode_id.data = episode.id
form.code.data = episode.code
form.date.data = episode.date
form.start.data = episode.start
form.end.data = episode.end
form.title.data = episode.title
form.players.data = [p.id for p in ep_players]
model.form_title = f"Edit Episode '{episode.code}: {episode.title}'"
return render_template("generic_form.html", model=model, form=form)
else:
form = forms.EpisodeForm()
if not form.validate_on_submit():
model.errors = form.errors
return render_template("generic_form.html", model=model, form=form)
errors = False
episode = models.Episode.from_form(form)
sql, args = db.save_episode(episode)
last_key = db.update_db(sql, args, return_key=True)
episode_id = episode.id if episode.id else last_key
form_ids = form.players.data
sql, args = db.load_episode_players(episode_id)
ep_players = db.query_db(sql, args, cls=models.Player)
pids = [p.id for p in ep_players]
new_ids = [pid for pid in form_ids if pid not in pids]
removed_ids = [pid for pid in pids if pid not in form_ids]
if removed_ids:
sql, args = db.remove_episode_player(episode_id, removed_ids)
errors = db.update_db(sql, args)
if new_ids:
sql, args = db.save_episode_players(episode_id, new_ids)
errors = db.update_db(sql, args)
if errors:
model.errors = {"Error saving episode": [errors]}
return render_template("generic_form.html", model=model, form=form)
return redirect(url_for("season_overview", season_id=season_id))

View File

@@ -0,0 +1,56 @@
from collections import namedtuple
from flask import render_template, request, redirect
from estusshots import app
from estusshots import forms, models, db, choices
from estusshots.util import authorize
@app.route("/season/<s_id>/episode/<ep_id>/event/new", methods=["GET"])
@authorize
def event_new(s_id: int, ep_id: int):
model = {
"page_title": "New Event",
"form_title": "Create New Event",
"post_url": f"/season/{s_id}/episode/{ep_id}/event/null/edit",
}
sql, args = db.load_episode(ep_id)
episode: models.Episode = db.query_db(sql, args, one=True, cls=models.Episode)
sql, args = db.load_episode_players(ep_id)
ep_players = db.query_db(sql, args, cls=models.Player)
form = forms.EventForm()
form.episode_id.data = ep_id
form.enemy.choices = choices.enemy_choice_for_season(s_id)
form.event_type.data = 1
Penalty = namedtuple("Penalty", ["penalty_id", "player_id", "player", "drink"])
for player in ep_players:
form.penalties.append_entry(Penalty(None, player.id, player.name, 1))
return render_template("event_editor.html", model=model, form=form)
@app.route("/season/<s_id>/episode/<ep_id>/event/<ev_id>/edit", methods=["GET", "POST"])
@authorize
def event_edit(s_id: int, ep_id: int, ev_id: int):
model = {
"page_title": "Edit Event",
"form_title": "Edit Event",
"post_url": f"/season/{s_id}/episode/{ep_id}/event/{ev_id}/edit",
}
if request.method == "GET":
return render_template("event_editor.html", model=model)
else:
form = forms.EventForm()
form.enemy.choices = choices.enemy_choice_for_season(s_id)
if not form.validate_on_submit():
model["errors"] = form.errors
return render_template("event_editor.html", model=model, form=form)
event = models.Event.from_form(form)
sql, args = db.save_event(event)
errors = db.update_db(sql, args)
return redirect(f"/season/{s_id}/episode/{ep_id}")

28
estusshots/views/login.py Normal file
View File

@@ -0,0 +1,28 @@
from flask import render_template, request, redirect, session
from estusshots import app
from estusshots.util import authorize, get_user_type
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "GET":
return render_template("login.html")
else:
user_type = get_user_type(request.form.get("password"))
if not user_type:
return redirect("/login")
session["user_type"] = user_type
return redirect("/")
@app.route("/logout")
def logout():
session.pop("role", None)
return redirect("login")
@app.route("/")
@authorize
def landing():
return redirect("/season")

View File

@@ -0,0 +1,71 @@
from flask import render_template, request, redirect
from estusshots import app
from estusshots import forms, models, orm
from estusshots.util import authorize
from estusshots.orm import Player
@app.route("/player/new", methods=["GET"])
@authorize
def player_new():
form = forms.PlayerForm()
model = models.GenericFormModel(
page_title="Players",
form_title="Create a new Player",
post_url="/player/null/edit",
)
return render_template("generic_form.html", model=model, form=form)
@app.route("/player/<player_id>/edit", methods=["GET", "POST"])
@authorize
def player_edit(player_id: int):
model = models.GenericFormModel(
page_title="Players",
form_title=f"Edit Player",
post_url=f"/player/{player_id}/edit",
)
# Edit Existing Player
if request.method == "GET":
db = orm.new_session()
player = db.query(Player).filter(Player.id == player_id).first()
form = forms.PlayerForm()
form.player_id.data = player.id
form.anonymize.data = player.anon
form.real_name.data = player.real_name
form.alias.data = player.alias
form.hex_id.data = player.hex_id
model.form_title = f'Edit Player "{player.name}"'
return render_template("generic_form.html", model=model, form=form)
# Save POSTed data
else:
form = forms.PlayerForm()
if form.validate_on_submit():
db = orm.new_session()
player = db.query(Player).filter(Player.id == player_id).first()
player.populate_from_form(form)
db.commit()
return redirect("/player")
model.form_title = "Incorrect Data"
return render_template("generic_form.html", model=model, form=form)
@app.route("/player")
@authorize
def player_list():
db = orm.new_session()
players = db.query(Player)
model = {
"player_list": players,
"columns": [
("name", "Player Name"),
("alias", "Alias"),
("hex_id", "Hex ID"),
],
}
return render_template("player_list.html", model=model)

View File

@@ -0,0 +1,94 @@
from flask import render_template, request, redirect, url_for
from estusshots import app
from estusshots import forms, models, orm
from estusshots.util import authorize
from estusshots.orm import Season
@app.route("/season")
@authorize
def season_list():
db = orm.new_session()
seasons = db.query(Season).order_by(Season.code).all()
model = {
"seasons": seasons,
"columns": [
("code", "#"),
("game", "Game"),
("description", "Season Description"),
("start", "Started At"),
("end", "Ended At"),
],
}
return render_template("season_list.html", model=model)
@app.route("/season/new", methods=["GET"])
@authorize
def season_new():
form = forms.SeasonForm()
model = models.GenericFormModel(
page_title="New Season",
form_title="Create New Season",
post_url="/season/edit/null",
)
return render_template("generic_form.html", model=model, form=form)
@app.route("/season/edit/<season_id>", methods=["GET", "POST"])
@authorize
def season_edit(season_id: int):
model = models.GenericFormModel(
page_title="Seasons",
form_title="Edit Season",
post_url=f"/season/edit/{season_id}",
)
db = orm.new_session()
season = db.query(Season).filter(Season.id == season_id).first()
if request.method == "GET":
form = forms.SeasonForm()
form.season_id.data = season.id
form.code.data = season.code
form.game_name.data = season.game
form.description.data = season.description
form.start.data = season.start
form.end.data = season.end
model.form_title = f"Edit Season '{season.code}: {season.game}'"
return render_template("generic_form.html", model=model, form=form)
else:
form = forms.SeasonForm()
if not form.validate_on_submit():
model.errors = form.errors
return render_template("generic_form.html", model=model, form=form)
if not season:
season = Season()
db.add(season)
season.populate_from_form(form)
db.commit()
return redirect(url_for("season_list"))
@app.route("/season/<season_id>", methods=["GET"])
@authorize
def season_overview(season_id: int):
db = orm.new_session()
season = db.query(Season).filter(Season.id == season_id).first()
infos = {
"Number": season.code,
"Game": season.game,
"Start Date": season.start,
"End Date": season.end if season.end else "Ongoing",
}
model = {
"title": f"{season.code} {season.game}",
"season_info": infos,
"episodes": season.episodes,
}
return render_template("season_overview.html", model=model)