diff --git a/.gitignore b/.gitignore index bdc8131..efc215e 100644 --- a/.gitignore +++ b/.gitignore @@ -94,4 +94,6 @@ Screenshots/ .vscode typesheds/ *.db -*.env \ No newline at end of file +*.env + +estusshots/config/production.py diff --git a/estusshots/__init__.py b/estusshots/__init__.py index cfd76e4..d33c48f 100644 --- a/estusshots/__init__.py +++ b/estusshots/__init__.py @@ -1,31 +1,23 @@ 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() +app.config.from_pyfile("config/default.py") +config_path = f"config/{app.env}.py" +app.config.from_pyfile(config_path, silent=True) -logging.basicConfig(filename=config.LOG_PATH, level=logging.DEBUG) +logging.basicConfig(filename=app.config.get("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 diff --git a/estusshots/config.ini b/estusshots/config.ini deleted file mode 100644 index 3cf7668..0000000 --- a/estusshots/config.ini +++ /dev/null @@ -1,6 +0,0 @@ -[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 diff --git a/estusshots/config.py b/estusshots/config.py deleted file mode 100644 index 8611630..0000000 --- a/estusshots/config.py +++ /dev/null @@ -1,16 +0,0 @@ -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() - diff --git a/estusshots/config/__init__.py b/estusshots/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/estusshots/config/default.py b/estusshots/config/default.py new file mode 100644 index 0000000..8fa9d63 --- /dev/null +++ b/estusshots/config/default.py @@ -0,0 +1,6 @@ +READ_PW = None +WRITE_PW = None +SECRET_KEY = None +DATABASE_PATH = None +LOG_PATH = None + diff --git a/estusshots/config/development.py b/estusshots/config/development.py new file mode 100644 index 0000000..30f46b2 --- /dev/null +++ b/estusshots/config/development.py @@ -0,0 +1,5 @@ +READ_PW = "123" +WRITE_PW = "1234" +SECRET_KEY = "1234" +DATABASE_PATH = "../databases/test.db" +LOG_PATH = "../logs/debug.log" diff --git a/estusshots/db.py b/estusshots/db.py deleted file mode 100644 index 5c40f31..0000000 --- a/estusshots/db.py +++ /dev/null @@ -1,306 +0,0 @@ -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 diff --git a/estusshots/forms.py b/estusshots/forms.py index d2bdec8..8b316f1 100644 --- a/estusshots/forms.py +++ b/estusshots/forms.py @@ -1,3 +1,6 @@ +from typing import Dict, List + +from dataclasses import dataclass from flask_wtf import FlaskForm from wtforms import ( DateField, @@ -17,6 +20,13 @@ from wtforms.validators import DataRequired, Optional from estusshots import choices +@dataclass +class GenericFormModel: + page_title: str + form_title: str + post_url: str + errors: Dict[str, List[str]] = None + class SeasonForm(FlaskForm): season_id = HiddenField("Season ID", render_kw={"readonly": True}) code = StringField("Season Code", validators=[DataRequired()]) diff --git a/estusshots/models.py b/estusshots/models.py deleted file mode 100644 index a574a1b..0000000 --- a/estusshots/models.py +++ /dev/null @@ -1,189 +0,0 @@ -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) diff --git a/estusshots/orm.py b/estusshots/orm.py index dae29f3..af734e5 100644 --- a/estusshots/orm.py +++ b/estusshots/orm.py @@ -7,9 +7,10 @@ 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 +from estusshots import util, forms, app -engine = create_engine('sqlite:///../databases/test.db') +connection = f"sqlite:///{app.config.get('DATABASE_PATH')}" +engine = create_engine(connection) Base = declarative_base() player_episode = Table( diff --git a/estusshots/schema.sql b/estusshots/schema.sql deleted file mode 100644 index 9e685dc..0000000 --- a/estusshots/schema.sql +++ /dev/null @@ -1,133 +0,0 @@ -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 -); diff --git a/estusshots/util.py b/estusshots/util.py index 91ef43f..6e2662b 100644 --- a/estusshots/util.py +++ b/estusshots/util.py @@ -3,7 +3,7 @@ from datetime import datetime, time, date, timedelta from flask import g, session, redirect -from estusshots import config, app, db +from estusshots import app TIME_FMT = "%H:%M" DATE_FMT = "%Y-%m-%d" @@ -69,9 +69,9 @@ def combine_datetime(date: datetime.date, time: datetime.time): def get_user_type(password): # TODO password hashing? - if password == config.WRITE_PW: + if password == app.config.get("WRITE_PW"): return "editor" - if password == config.READ_PW: + if password == app.config.get("READ_PW"): return "readonly" return False @@ -109,12 +109,6 @@ def format_timedelta(value): 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) diff --git a/estusshots/views/drinks.py b/estusshots/views/drinks.py index f005b85..02ebe1c 100644 --- a/estusshots/views/drinks.py +++ b/estusshots/views/drinks.py @@ -1,7 +1,7 @@ from flask import render_template, redirect from estusshots import app -from estusshots import forms, models, orm +from estusshots import forms, orm from estusshots.util import authorize from estusshots.orm import Drink @@ -29,7 +29,7 @@ def drink_edit(drink_id: int): form.name.data = drink.name form.vol.data = drink.vol - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="Edit Drink", form_title=f'Edit Drink "{drink.name}"', post_url="/drink/save", @@ -42,7 +42,7 @@ def drink_edit(drink_id: int): @authorize def new_drink(): form = forms.DrinkForm() - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="New Drink", form_title=f"Create a new Drink", post_url="/drink/save", @@ -66,7 +66,7 @@ def drink_save(): err = db.commit() return redirect("/drink") - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="Drinks", form_title="Edit Drink", post_url="/drink/save" ) return render_template("generic_form.html", model=model, form=form) diff --git a/estusshots/views/enemies.py b/estusshots/views/enemies.py index 99eafc2..60cd38e 100644 --- a/estusshots/views/enemies.py +++ b/estusshots/views/enemies.py @@ -1,7 +1,6 @@ from flask import render_template, request, redirect, url_for -from estusshots import app -from estusshots import forms, models, orm +from estusshots import app, forms, orm from estusshots.util import authorize from estusshots.orm import Enemy from sqlalchemy.orm import subqueryload @@ -25,7 +24,7 @@ def enemy_new(): form.season_id.process_data(request.args['preselect']) form.is_boss.data = True - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="Enemies", form_title="Create a new Enemy", post_url=f"/enemy/null/edit", @@ -36,7 +35,7 @@ def enemy_new(): @app.route("/enemy//edit", methods=["GET", "POST"]) @authorize def enemy_edit(enemy_id: int): - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="Enemies", form_title="Edit Enemy", post_url=f"/enemy/{enemy_id}/edit", diff --git a/estusshots/views/episodes.py b/estusshots/views/episodes.py index 9568aef..c05b008 100644 --- a/estusshots/views/episodes.py +++ b/estusshots/views/episodes.py @@ -1,7 +1,7 @@ from flask import render_template, request, redirect, url_for from estusshots import app -from estusshots import forms, models, orm +from estusshots import forms, orm from estusshots.util import authorize from estusshots.orm import Season, Episode, Player @@ -37,7 +37,7 @@ def episode_list(season_id: int): @app.route("/season//episode/new", methods=["GET"]) @authorize def episode_new(season_id: int): - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="New Episode", form_title="Create New Episode", post_url=f"/season/{season_id}/episode/null/edit", @@ -50,7 +50,7 @@ def episode_new(season_id: int): @app.route("/season//episode//edit", methods=["GET", "POST"]) @authorize def episode_edit(season_id: int, episode_id: int): - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="Edit Episode", form_title="Edit Episode", post_url=f"/season/{season_id}/episode/{episode_id}/edit", diff --git a/estusshots/views/players.py b/estusshots/views/players.py index 4fb2db0..f4f3233 100644 --- a/estusshots/views/players.py +++ b/estusshots/views/players.py @@ -1,7 +1,7 @@ from flask import render_template, request, redirect from estusshots import app -from estusshots import forms, models, orm +from estusshots import forms, orm from estusshots.util import authorize from estusshots.orm import Player @@ -10,7 +10,7 @@ from estusshots.orm import Player @authorize def player_new(): form = forms.PlayerForm() - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="Players", form_title="Create a new Player", post_url="/player/null/edit", @@ -21,7 +21,7 @@ def player_new(): @app.route("/player//edit", methods=["GET", "POST"]) @authorize def player_edit(player_id: int): - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="Players", form_title=f"Edit Player", post_url=f"/player/{player_id}/edit", diff --git a/estusshots/views/seasons.py b/estusshots/views/seasons.py index e935a69..c7684d5 100644 --- a/estusshots/views/seasons.py +++ b/estusshots/views/seasons.py @@ -1,7 +1,7 @@ from flask import render_template, request, redirect, url_for from estusshots import app -from estusshots import forms, models, orm +from estusshots import forms, orm from estusshots.util import authorize from estusshots.orm import Season @@ -28,7 +28,7 @@ def season_list(): @authorize def season_new(): form = forms.SeasonForm() - model = models.GenericFormModel( + model = forms.GenericFormModel( page_title="New Season", form_title="Create New Season", post_url="/season/edit/null",