This commit is contained in:
2019-01-11 19:12:03 +01:00
commit c090d65038
27 changed files with 14887 additions and 0 deletions

96
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
static/bootstrap-grid.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

319
static/bootstrap-reboot.css vendored Normal file
View 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 */

File diff suppressed because one or more lines are too long

8
static/bootstrap-reboot.min.css vendored Normal file
View 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 */

File diff suppressed because one or more lines are too long

9887
static/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

7
static/custom.css Normal file
View 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
View 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 %}
&copy; A product of D⁵: <a href="#">Durstiger Donnerstag Digital Drinking Divison</a>.
{% endblock %}
</div>
</div>
</body>
</html>

53
templates/drinks.html Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% set active_page = "seasons" %}
{% block title %}Seasons{% endblock %}
{% block content %}
There are no seasons.
{% endblock %}