Initial
This commit is contained in:
96
.gitignore
vendored
Normal file
96
.gitignore
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# IPython Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
.idea
|
||||
install.txt
|
||||
Screenshots/
|
||||
.vscode
|
||||
typesheds/
|
||||
*.db
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
178
app.py
Normal file
178
app.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import functools
|
||||
from flask import Flask, g, render_template, request, redirect, session
|
||||
|
||||
import db
|
||||
import model
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.secret_key = 'THIS IS A TEST KEY'
|
||||
|
||||
ROLES = {
|
||||
'123': 'readonly',
|
||||
'1234': 'editor'
|
||||
}
|
||||
|
||||
|
||||
@app.cli.command('initdb')
|
||||
def init_db_command():
|
||||
"""Initializes the database."""
|
||||
db.init_db()
|
||||
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_connection(exception):
|
||||
db = getattr(g, '_database', None)
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
|
||||
def set_user_role(data):
|
||||
"""Set the users role in the flask g object for later usage"""
|
||||
g.is_editor = data == 'editor'
|
||||
|
||||
|
||||
def authorize(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
set_user_role(session['role'])
|
||||
except KeyError:
|
||||
return redirect('/login')
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'GET':
|
||||
return render_template('login.html')
|
||||
else:
|
||||
try:
|
||||
password = request.form['password']
|
||||
session['role'] = ROLES[password]
|
||||
return redirect('/')
|
||||
except KeyError:
|
||||
return redirect('login')
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.pop('role', None)
|
||||
return redirect('login')
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@authorize
|
||||
def landing():
|
||||
if 'editor' in session['role']:
|
||||
print('editor')
|
||||
else:
|
||||
print('not editor')
|
||||
return render_template('seasons.html')
|
||||
|
||||
|
||||
@app.route('/seasons')
|
||||
@authorize
|
||||
def seasons():
|
||||
return render_template('seasons.html')
|
||||
|
||||
|
||||
@app.route('/newplayer', methods=['GET'])
|
||||
@authorize
|
||||
def new_player():
|
||||
return render_template('editplayer.html', model={})
|
||||
|
||||
|
||||
@app.route('/saveplayer', methods=['POST'])
|
||||
@authorize
|
||||
def update_player():
|
||||
data = request.form
|
||||
player = model.Player(
|
||||
id=data.get('id', None),
|
||||
real_name=data['real_name'],
|
||||
alias=data['alias'],
|
||||
hex_id=data['hex_id'],
|
||||
anon=data.get('anon', False))
|
||||
res = db.save_player(player)
|
||||
return redirect('/players')
|
||||
|
||||
|
||||
@app.route('/players')
|
||||
@authorize
|
||||
def players():
|
||||
loaded = db.load_players()
|
||||
model = {
|
||||
'player_list': loaded,
|
||||
'columns': [('id', 'ID'),
|
||||
('name', 'Player Name'),
|
||||
('alias', 'Alias'),
|
||||
('hex_id', 'Hex ID')],
|
||||
'controls': [('edit', 'Edit')]
|
||||
}
|
||||
return render_template('players.html', model=model)
|
||||
|
||||
|
||||
@app.route('/players/<id>', methods=['GET'])
|
||||
@authorize
|
||||
def edit_player(id: int):
|
||||
loaded = db.load_players(id)[0]
|
||||
return render_template('editplayer.html', model=loaded)
|
||||
|
||||
|
||||
@app.route('/drinks')
|
||||
@authorize
|
||||
def drinks():
|
||||
loaded = db.load_drinks()
|
||||
model = {
|
||||
'drinks': loaded,
|
||||
'columns': [
|
||||
('id', 'ID'),
|
||||
('name', 'Drink Name'),
|
||||
('vol', 'Alcohol %')
|
||||
],
|
||||
'controls': [('edit', 'Edit')]
|
||||
}
|
||||
return render_template('drinks.html', model=model)
|
||||
|
||||
|
||||
@app.route('/drinks/<id>', methods=['GET'])
|
||||
@authorize
|
||||
def show_drink(id: int):
|
||||
loaded = db.load_drinks(id)[0]
|
||||
return render_template('editdrink.html', model=loaded)
|
||||
|
||||
|
||||
@app.route('/newdrink', methods=['GET'])
|
||||
@authorize
|
||||
def new_drink():
|
||||
return render_template('editdrink.html', model={})
|
||||
|
||||
|
||||
@app.route('/savedrink', methods=['POST'])
|
||||
@authorize
|
||||
def save_drink():
|
||||
drink = model.Drink.from_form(request.form)
|
||||
res = db.save_drink(drink)
|
||||
return redirect('/drinks')
|
||||
|
||||
|
||||
@app.route('/enemies')
|
||||
@authorize
|
||||
def enemies():
|
||||
loaded = db.load_enemies()
|
||||
model = {
|
||||
'enemies': loaded,
|
||||
'columns': [
|
||||
('id', 'ID'),
|
||||
('name', 'Enemy Name'),
|
||||
('vol', 'Is Boss')
|
||||
],
|
||||
'controls': [('edit', 'Edit')]
|
||||
}
|
||||
return render_template('enemies.html', model=model)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
124
db.py
Normal file
124
db.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import sqlite3
|
||||
import logging as log
|
||||
from flask import g
|
||||
|
||||
import model
|
||||
|
||||
|
||||
DATABASE = 'es_debug.db'
|
||||
|
||||
|
||||
def connect_db():
|
||||
"""Create a new sqlite3 connection and register it in 'g._database'"""
|
||||
db = getattr(g, '_database', None)
|
||||
if db is None:
|
||||
db = g._database = sqlite3.connect(DATABASE)
|
||||
|
||||
db.row_factory = sqlite3.Row
|
||||
return db
|
||||
|
||||
|
||||
def query_db(query, args=(), one=False):
|
||||
"""Runs an SQL query on an new database connection, returning the fetched rows"""
|
||||
log.info(f'Running query ({query})\nwith arguments ({args})')
|
||||
cur = connect_db().execute(query, args)
|
||||
rv = cur.fetchall()
|
||||
cur.close()
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
|
||||
|
||||
def update_db(query, args=()):
|
||||
"""
|
||||
Runs an changing query on the database
|
||||
Returns either False if no error has occurred, or an sqlite3 Exception
|
||||
"""
|
||||
with connect_db() as con:
|
||||
try:
|
||||
con.cursor().execute(query, args)
|
||||
except sqlite3.Error as err:
|
||||
return err
|
||||
con.commit()
|
||||
return 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'
|
||||
rows = query_db(sql, args)
|
||||
players = [model.Player(**row) for row in rows]
|
||||
return players
|
||||
|
||||
|
||||
def load_drinks(id=None):
|
||||
sql = 'select * from drink'
|
||||
args = ()
|
||||
if id:
|
||||
sql += ' where drink.id = ?'
|
||||
args = (id, )
|
||||
sql += ' order by drink.id'
|
||||
rows = query_db(sql, args)
|
||||
drinks = [model.Drink(**row) for row in rows]
|
||||
return drinks
|
||||
|
||||
|
||||
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'
|
||||
rows = query_db(sql, args)
|
||||
enemies = [model.Enemy(**row) for row in rows]
|
||||
return enemies
|
||||
72
model.py
Normal file
72
model.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from _ctypes import ArgumentError
|
||||
from dataclasses import dataclass
|
||||
|
||||
INVALID_STR = 'Form entry "{}" is invalid'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Player:
|
||||
id: int
|
||||
real_name: str
|
||||
name: str
|
||||
alias: str
|
||||
hex_id: str
|
||||
anon: bool = False
|
||||
|
||||
def __init__(self, real_name, alias, hex_id, anon=False, id=None):
|
||||
self.real_name = str(real_name)
|
||||
self.name = str(real_name) if not anon else alias
|
||||
self.hex_id = str(hex_id)
|
||||
self.alias = str(alias)
|
||||
self.anon = bool(anon)
|
||||
self.id = id
|
||||
|
||||
|
||||
@dataclass
|
||||
class Drink:
|
||||
id: int
|
||||
name: str
|
||||
vol: float
|
||||
|
||||
@classmethod
|
||||
def from_form(cls, form):
|
||||
id = form.get('id', None)
|
||||
id = int(id) if id else None
|
||||
|
||||
name = form.get('name', None)
|
||||
if not name:
|
||||
raise ArgumentError('Form data contains no field "name"')
|
||||
name = str(name)
|
||||
|
||||
vol = form.get('vol', None)
|
||||
if not vol:
|
||||
raise ArgumentError('Form data contains no field "vol"')
|
||||
vol = float(vol)
|
||||
|
||||
self = cls(id=id, name=name, vol=vol)
|
||||
return self
|
||||
|
||||
|
||||
@dataclass
|
||||
class Enemy:
|
||||
id: int
|
||||
name: str
|
||||
boss: bool
|
||||
|
||||
@classmethod
|
||||
def from_form(cls, form):
|
||||
id = form.get('id', None)
|
||||
id = int(id) if id else None
|
||||
|
||||
name = form.get('name', None)
|
||||
if not name:
|
||||
raise ArgumentError(INVALID_STR.format('name'))
|
||||
name = str(name)
|
||||
|
||||
boss = form.get('boss', '')
|
||||
if boss not in [True, False, 'True', 'False']:
|
||||
raise ArgumentError(INVALID_STR.format('boss'))
|
||||
|
||||
self = cls(id=id, name=name, boss=boss)
|
||||
return self
|
||||
|
||||
72
schema.sql
Normal file
72
schema.sql
Normal file
@@ -0,0 +1,72 @@
|
||||
create table if not exists season
|
||||
(
|
||||
id integer not null
|
||||
constraint season_pk
|
||||
primary key autoincrement,
|
||||
game text,
|
||||
description text,
|
||||
start text,
|
||||
end text
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
create unique index if not exists enemy_id_uindex
|
||||
on enemy (id);
|
||||
|
||||
create table if not exists season_enemy
|
||||
(
|
||||
id integer not null
|
||||
constraint season_enemy_pk
|
||||
primary key autoincrement,
|
||||
season_id integer
|
||||
constraint season_enemy_season_id_fk
|
||||
references season,
|
||||
enemy_id integer
|
||||
constraint season_enemy_enemy_id_fk
|
||||
references enemy
|
||||
);
|
||||
|
||||
create unique index if not exists season_enemy_id_uindex
|
||||
on season_enemy (id);
|
||||
|
||||
|
||||
|
||||
3719
static/bootstrap-grid.css
vendored
Normal file
3719
static/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/bootstrap-grid.css.map
Normal file
1
static/bootstrap-grid.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/bootstrap-grid.min.css
vendored
Normal file
7
static/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/bootstrap-grid.min.css.map
Normal file
1
static/bootstrap-grid.min.css.map
Normal file
File diff suppressed because one or more lines are too long
319
static/bootstrap-reboot.css
vendored
Normal file
319
static/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,319 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v4.2.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-original-title] {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
border-bottom: 0;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: .5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
-webkit-appearance: listbox;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
static/bootstrap-reboot.css.map
Normal file
1
static/bootstrap-reboot.css.map
Normal file
File diff suppressed because one or more lines are too long
8
static/bootstrap-reboot.min.css
vendored
Normal file
8
static/bootstrap-reboot.min.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v4.2.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
||||
1
static/bootstrap-reboot.min.css.map
Normal file
1
static/bootstrap-reboot.min.css.map
Normal file
File diff suppressed because one or more lines are too long
9887
static/bootstrap.css
vendored
Normal file
9887
static/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/bootstrap.css.map
Normal file
1
static/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/bootstrap.min.css
vendored
Normal file
7
static/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/bootstrap.min.css.map
Normal file
1
static/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/custom.css
Normal file
7
static/custom.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.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;
|
||||
}
|
||||
48
templates/base.html
Normal file
48
templates/base.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="/static/bootstrap.css">
|
||||
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" integrity="sha384-gfdkjb5BdAXd+lj+gudLWI+BXq4IuLW5IT+brZEZsLFm++aCMlF1V92rMkPaX4PP" crossorigin="anonymous">
|
||||
<title>{% block title %}{% endblock %} - Estus Shots</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% set nav_bar = [
|
||||
('/seasons', 'seasons', 'Seasons', 'fas fa-calendar'),
|
||||
('/players', 'players', 'Players', 'fas fa-users'),
|
||||
('/enemies', 'enemies', 'Enemies', 'fas fa-skull-crossbones'),
|
||||
('/drinks', 'drinks', 'Drinks', 'fas fa-beer')
|
||||
]-%}
|
||||
<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
|
||||
<ul class="nav 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 }}"></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>
|
||||
</nav>
|
||||
|
||||
<div>{% block content %}{% endblock %}</div>
|
||||
|
||||
<div>
|
||||
{% block footer %}
|
||||
© A product of D⁵: <a href="#">Durstiger Donnerstag Digital Drinking Divison</a>.
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
53
templates/drinks.html
Normal file
53
templates/drinks.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "drinks" %}
|
||||
{% block title %}Drinks{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if g.is_editor %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-primary" href="/newdrink" role="button">New 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 %}
|
||||
{% for id, caption in model.controls %}
|
||||
<th scope="col" class="col-sm-auto text-center">{{ caption }}</th>
|
||||
{% endfor %}
|
||||
{% 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 %}
|
||||
{% for name, caption in model.controls %}
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-dark"
|
||||
href="{% if name == 'edit'%}
|
||||
/drinks/{{ drink.id }}
|
||||
{% endif %}">
|
||||
{{ caption }}
|
||||
</a>
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
60
templates/editdrink.html
Normal file
60
templates/editdrink.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "drinks" %}
|
||||
{% block title %}Drinks{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<form class="needs-validation" novalidate action="/savedrink" method="post">
|
||||
<div class="form-group">
|
||||
<label for="id">ID</label>
|
||||
<input name="id"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="id"
|
||||
value="{{ model['id'] }}"
|
||||
readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="name">Drink Name</label>
|
||||
<input name="name"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="name"
|
||||
placeholder="Drink name..."
|
||||
value="{{ model['name'] }}"
|
||||
required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="drinkVol">Alcohol %</label>
|
||||
<input name="vol"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="drinkVol"
|
||||
placeholder="Drink volume..."
|
||||
value="{{ model['vol'] }}"
|
||||
required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Example starter JavaScript for disabling form submissions if there are invalid fields
|
||||
(function() {
|
||||
'use strict';
|
||||
window.addEventListener('load', function() {
|
||||
// Fetch all the forms we want to apply custom Bootstrap validation styles to
|
||||
let forms = document.getElementsByClassName('needs-validation');
|
||||
// Loop over them and prevent submission
|
||||
let validation = Array.prototype.filter.call(forms, function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (form.checkValidity() === false) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
}, false);
|
||||
});
|
||||
}, false);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
79
templates/editplayer.html
Normal file
79
templates/editplayer.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "players" %}
|
||||
{% block title %}Players{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<form class="needs-validation" novalidate action="/saveplayer" method="post">
|
||||
<div class="form-group">
|
||||
<label for="playerId">ID</label>
|
||||
<input name="id"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="playerId"
|
||||
value="{{ model['id'] }}"
|
||||
readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPlayerRealName">Real Name</label>
|
||||
<input name="real_name"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="newPlayerRealName"
|
||||
placeholder="Enter name..."
|
||||
value="{{ model['real_name'] }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPlayerAlias">Player Alias</label>
|
||||
<input name="alias"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="newPlayerAlias"
|
||||
placeholder="Enter alias..."
|
||||
value="{{ model['alias'] }}"
|
||||
required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPlayerHexID">Hex ID</label>
|
||||
<input name="hex_id"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="newPlayerHexID"
|
||||
placeholder="Enter Hex ID..."
|
||||
value="{{ model['hex_id'] }}">
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input name="anon"
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
id="newPlayerAnon"
|
||||
value="True"
|
||||
{% if model['anon'] %}
|
||||
checked
|
||||
{% endif %}>
|
||||
<label class="form-check-label" for="newPlayerAnon">Anonymize (Show only player alias)</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Example starter JavaScript for disabling form submissions if there are invalid fields
|
||||
(function() {
|
||||
'use strict';
|
||||
window.addEventListener('load', function() {
|
||||
// Fetch all the forms we want to apply custom Bootstrap validation styles to
|
||||
let forms = document.getElementsByClassName('needs-validation');
|
||||
// Loop over them and prevent submission
|
||||
let validation = Array.prototype.filter.call(forms, function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (form.checkValidity() === false) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
}, false);
|
||||
});
|
||||
}, false);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
53
templates/enemies.html
Normal file
53
templates/enemies.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "enemies" %}
|
||||
{% block title %}Enemies{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if g.is_editor %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-primary" href="/newenemy" role="button">New Enemy</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not model.enemies %}
|
||||
There are no enemies.
|
||||
{% 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 %}
|
||||
{% for id, caption in model.controls %}
|
||||
<th scope="col" class="col-sm-auto text-center">{{ caption }}</th>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in model.enemies %}
|
||||
<tr>
|
||||
{% for prop, caption in model.columns %}
|
||||
<td class="col-sm-auto text-center">{{ item[prop] }}</td>
|
||||
{% endfor %}
|
||||
|
||||
{% if g.is_editor %}
|
||||
{% for name, caption in model.controls %}
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-dark"
|
||||
href="{% if name == 'edit'%}
|
||||
/enemies/{{ item.id }}
|
||||
{% endif %}">
|
||||
{{ caption }}
|
||||
</a>
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
31
templates/login.html
Normal file
31
templates/login.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="/static/bootstrap.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" integrity="sha384-gfdkjb5BdAXd+lj+gudLWI+BXq4IuLW5IT+brZEZsLFm++aCMlF1V92rMkPaX4PP" crossorigin="anonymous">
|
||||
<title>Login - Estus Shots</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container vertical-center">
|
||||
<div class="card text-center mx-auto" style="width: 15rem">
|
||||
<div class="card-header">Login</div>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
48
templates/players.html
Normal file
48
templates/players.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "players" %}
|
||||
{% block title %}Players{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if g.is_editor %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-primary" href="/newplayer" role="button">New 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 %}
|
||||
{% for name, caption in model.controls %}
|
||||
<th scope="col" class="col-sm-auto text-center">{{ caption }}</th>
|
||||
{% endfor %}
|
||||
{% 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 %}
|
||||
{% for name, caption in model.controls %}
|
||||
<td class="col-sm-auto text-center">
|
||||
<a class="btn btn-dark" href="/players/{{ player['id'] }}">{{ caption }}</a>
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
7
templates/seasons.html
Normal file
7
templates/seasons.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "seasons" %}
|
||||
{% block title %}Seasons{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
There are no seasons.
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user