Finalize migration to SQLAlchemy.
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
from estusshots import orm, models, db
|
||||
from estusshots.orm import Season
|
||||
from estusshots.orm import new_session, EventType, Season, Player, Drink, Enemy
|
||||
|
||||
|
||||
def event_choices():
|
||||
return [(member.value, member.name) for member in models.EventType]
|
||||
return [(member.value, member.name) for member in EventType]
|
||||
|
||||
|
||||
def season_choices():
|
||||
""" Query the database for available seasons.
|
||||
"""
|
||||
Query the database for available seasons.
|
||||
This returns a list of tuples with the season ID and a display string.
|
||||
"""
|
||||
db = orm.new_session()
|
||||
db = new_session()
|
||||
seasons = db.query(Season).order_by(Season.code).all()
|
||||
choices = [(s.id, f"{s.code}: {s.game}") for s in seasons]
|
||||
choices.insert(0, (-1, "No Season"))
|
||||
@@ -21,8 +21,8 @@ def player_choice():
|
||||
"""
|
||||
Query database for a list of available players to bind them to a select box
|
||||
"""
|
||||
sql, args = db.load_players()
|
||||
players = db.query_db(sql, args, cls=models.Player)
|
||||
db = new_session()
|
||||
players = sorted(db.query(Player).all(), key=lambda x: x.name)
|
||||
return [(p.id, p.name) for p in players]
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ def drink_choice():
|
||||
"""
|
||||
Query database for a list of all available drinks to select from
|
||||
"""
|
||||
sql, args = db.load_drinks()
|
||||
drinks = db.query_db(sql, args, cls=models.Drink)
|
||||
db = new_session()
|
||||
drinks = db.query(Drink).order_by(Drink.name).all()
|
||||
choices = [(d.id, d.name) for d in drinks]
|
||||
choices.insert(0, (-1, "None"))
|
||||
return choices
|
||||
@@ -41,9 +41,13 @@ def enemy_choice_for_season(season_id: int):
|
||||
"""
|
||||
Query database for all available enemies in this season
|
||||
"""
|
||||
sql, args = db.load_enemies_for_season(season_id)
|
||||
enemies = db.query_db(sql, args, cls=models.Enemy)
|
||||
return [(e.id, e.name) for e in enemies]
|
||||
db = new_session()
|
||||
season: Season = db.query(Season).get(season_id)
|
||||
global_enemies = db.query(Enemy).filter(Enemy.season_id == -1).all()
|
||||
if not season and not global_enemies:
|
||||
return []
|
||||
combined = global_enemies + season.enemies
|
||||
return [(e.id, e.name) for e in combined]
|
||||
|
||||
|
||||
class IterableBase:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import enum
|
||||
from typing import Iterable, List
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import create_engine, ForeignKey, Table
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
@@ -10,6 +12,13 @@ from estusshots import util, forms
|
||||
engine = create_engine('sqlite:///../databases/test.db')
|
||||
Base = declarative_base()
|
||||
|
||||
player_episode = Table(
|
||||
'player_episode',
|
||||
Base.metadata,
|
||||
Column('player_id', ForeignKey('players.id'), primary_key=True),
|
||||
Column('episode_id', ForeignKey('episodes.id'), primary_key=True)
|
||||
)
|
||||
|
||||
|
||||
class EventType(enum.Enum):
|
||||
Pause = 0
|
||||
@@ -27,6 +36,7 @@ class Player(Base):
|
||||
anon = Column(Boolean, default=False)
|
||||
|
||||
events = relationship("Event", back_populates="player")
|
||||
episodes = relationship("Episode", secondary=player_episode, back_populates="players")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -61,8 +71,8 @@ class Season(Base):
|
||||
start = Column(Date)
|
||||
end = Column(Date)
|
||||
|
||||
episodes = relationship("Episode", back_populates="season")
|
||||
enemies = relationship("Enemy", back_populates="season")
|
||||
episodes: Iterable["Episode"] = relationship("Episode", back_populates="season")
|
||||
enemies: Iterable["Enemy"] = relationship("Enemy", back_populates="season")
|
||||
|
||||
def populate_from_form(self, form: "forms.SeasonForm"):
|
||||
self.code = str(form.code.data)
|
||||
@@ -82,7 +92,7 @@ class Enemy(Base):
|
||||
season_id = Column(Integer, ForeignKey('seasons.id'))
|
||||
season = relationship("Season", back_populates="enemies")
|
||||
|
||||
events = relationship('Event', back_populates="enemy")
|
||||
events: Iterable["Event"] = relationship('Event', back_populates="enemy")
|
||||
|
||||
def populate_from_form(self, form: "forms.EnemyForm"):
|
||||
self.name = str(form.name.data)
|
||||
@@ -103,18 +113,26 @@ class Episode(Base):
|
||||
season_id = Column(Integer, ForeignKey('seasons.id'))
|
||||
season = relationship("Season", back_populates="episodes")
|
||||
|
||||
events = relationship('Event', back_populates='episode')
|
||||
events: List["Event"] = relationship('Event', back_populates='episode')
|
||||
players = relationship("Player", secondary=player_episode, back_populates="episodes")
|
||||
|
||||
@property
|
||||
def playtime(self):
|
||||
return util.timedelta(self.start, self.end)
|
||||
return util.compute_timedelta(self.start, self.end)
|
||||
|
||||
def populate_from_form(self, form: "forms.EpisodeForm"):
|
||||
self.code = str(form.code.data)
|
||||
self.title = str(form.title.data)
|
||||
self.date = form.date.data
|
||||
self.start = form.start.data
|
||||
self.end = form.end.data
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = 'events'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
type = Column(Enum(EventType))
|
||||
type: EventType = Column(Enum(EventType))
|
||||
time = Column(Time)
|
||||
comment = Column(String)
|
||||
|
||||
@@ -127,7 +145,15 @@ class Event(Base):
|
||||
enemy_id = Column(Integer, ForeignKey('enemies.id'))
|
||||
enemy = relationship('Enemy', back_populates='events')
|
||||
|
||||
penalties = relationship('Penalty', back_populates='event')
|
||||
penalties: List["Penalty"] = relationship('Penalty', back_populates='event')
|
||||
|
||||
def populate_from_form(self, form: "forms.EventForm"):
|
||||
self.episode_id = int(form.episode_id.data)
|
||||
self.type = EventType(form.event_type.data)
|
||||
self.time = form.time.data
|
||||
self.comment = str(form.comment.data) if form.comment.data else None
|
||||
self.player_id = int(form.player.data) if form.player.data else None
|
||||
self.enemy_id = int(form.enemy.data) if form.enemy.data else None
|
||||
|
||||
|
||||
class Penalty(Base):
|
||||
|
||||
@@ -34,23 +34,23 @@
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">Start:</span>
|
||||
{{ model.episode.start|format_time }}
|
||||
{{ model.episode.start|format_time or "Not started yet" }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">End:</span>
|
||||
{{ model.episode.end|format_time }}
|
||||
{{ model.episode.end|format_time or "Not ended yet"}}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">Play Time:</span>
|
||||
{{ model.episode.playtime }}
|
||||
{{ model.episode.playtime or 0 }} Hours
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">Enemies Defeated:</span>
|
||||
{{ model.events.victory_count }}
|
||||
{{ 0 }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold">Deaths:</span>
|
||||
{{ model.events.defeat_count }}
|
||||
{{ model.deaths|length or 0}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -88,46 +88,48 @@
|
||||
<!--endregion-->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
|
||||
{% if model.events %}
|
||||
<!--region Deaths Card-->
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
Event List
|
||||
Deaths
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
{% if model.deaths %}
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="col-sm-auto text-center">Time</th>
|
||||
<th scope="col" class="col-sm-auto text-center">Type</th>
|
||||
<th scope="col" class="col-sm-auto text-center">Player</th>
|
||||
<th scope="col" class="col-sm-auto text-center">Enemy</th>
|
||||
<th scope="col" class="col-xs-auto text-center">Time</th>
|
||||
<th scope="col" class="col-xs text-center">Enemy</th>
|
||||
<th scope="col" class="col-xs text-center">Player</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in model.events.entries %}
|
||||
{% for entry in model.deaths %}
|
||||
<tr>
|
||||
<td class="col-sm-auto text-center">{{ entry.time }}</td>
|
||||
<td class="col-sm-auto text-center">{{ entry.type.name }}</td>
|
||||
<td class="col-sm-auto text-center">{{ entry.type.player_name }}</td>
|
||||
<td class="col-sm-auto text-center">{{ entry.type.enemy_name }}</td>
|
||||
<td class="col-xs-auto text-center">{{ entry.time|format_time }}</td>
|
||||
<td class="col-xs text-center">{{ entry.enemy.name }}</td>
|
||||
<td class="col-xs text-center">{{ entry.player.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
Event List
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<div class="alert alert-info">Nothing did happen yet</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--endregion-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -37,17 +37,19 @@ def timedelta_to_str(data: timedelta) -> str:
|
||||
)
|
||||
|
||||
|
||||
def timedelta(start: time, end: time) -> float:
|
||||
startDateTime = datetime.combine(date.today(), start)
|
||||
def compute_timedelta(start: time, end: time) -> float:
|
||||
if not start or not end:
|
||||
return 0
|
||||
s = datetime.combine(date.today(), start)
|
||||
# Check if the the end is still on the same day
|
||||
if start.hour > end.hour:
|
||||
base = date.today() + timedelta(days=1)
|
||||
else:
|
||||
base = date.today()
|
||||
endDateTime = datetime.combine(base, end)
|
||||
difference = startDateTime - endDateTime
|
||||
e = datetime.combine(base, end)
|
||||
difference = s - e
|
||||
difference_hours = difference.total_seconds() / 3600
|
||||
return difference_hours
|
||||
return abs(difference_hours)
|
||||
|
||||
|
||||
def combine_datetime(date: datetime.date, time: datetime.time):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import render_template, request, redirect
|
||||
from flask import render_template, request, redirect, url_for
|
||||
|
||||
from estusshots import app
|
||||
from estusshots import forms, models, orm
|
||||
@@ -18,11 +18,12 @@ def enemy_list():
|
||||
|
||||
@app.route("/enemy/new", methods=["GET"])
|
||||
@authorize
|
||||
def enemy_new(preselect_season=None):
|
||||
def enemy_new():
|
||||
form = forms.EnemyForm()
|
||||
|
||||
if preselect_season:
|
||||
form.season_id.default = preselect_season
|
||||
if "preselect" in request.args:
|
||||
form.season_id.process_data(request.args['preselect'])
|
||||
form.is_boss.data = True
|
||||
|
||||
model = models.GenericFormModel(
|
||||
page_title="Enemies",
|
||||
@@ -64,9 +65,8 @@ def enemy_edit(enemy_id: int):
|
||||
enemy.populate_from_form(form)
|
||||
db.commit()
|
||||
if form.submit_continue_button.data:
|
||||
form.name.data = None
|
||||
return enemy_new(preselect_season=enemy.season_id)
|
||||
return redirect("/enemy")
|
||||
return redirect(url_for("enemy_new", preselect=form.season_id.data))
|
||||
return redirect(url_for("enemy_list"))
|
||||
|
||||
model.form_title = "Incorrect Data"
|
||||
return render_template("generic_form.html", model=model, form=form)
|
||||
|
||||
@@ -1,45 +1,24 @@
|
||||
from typing import List
|
||||
|
||||
from flask import render_template, request, redirect
|
||||
from flask import render_template, request, redirect, url_for
|
||||
|
||||
from estusshots import app
|
||||
from estusshots import forms, models, db
|
||||
from estusshots import forms, models, orm
|
||||
from estusshots.util import authorize
|
||||
from estusshots.orm import Season, Episode, Player, Event
|
||||
|
||||
|
||||
@app.route("/season/<season_id>/episode/<episode_id>")
|
||||
@authorize
|
||||
def episode_detail(season_id: int, episode_id: int):
|
||||
sql, args = db.load_season(season_id)
|
||||
season = db.query_db(sql, args, one=True, cls=models.Season)
|
||||
sql, args = db.load_episode(episode_id)
|
||||
episode = db.query_db(sql, args, one=True, cls=models.Episode)
|
||||
sql, args = db.load_episode_players(episode_id)
|
||||
ep_players = db.query_db(sql, args, cls=models.Player)
|
||||
sql, args = db.load_events(episode_id)
|
||||
ep_events: List[models.Event] = db.query_db(sql, args, cls=models.Event)
|
||||
sql, args = db.load_enemies(season_id)
|
||||
enemies = db.query_db(sql, args, cls=models.Enemy)
|
||||
|
||||
deaths = [ev for ev in ep_events if ev.type == models.EventType.Death]
|
||||
entries = []
|
||||
for death in deaths:
|
||||
entries.append({
|
||||
"time": death.time.time(),
|
||||
"type": death.type,
|
||||
"player_name": [p.name for p in ep_players if p.id == death.player_id],
|
||||
"enemy_name": [e.name for e in enemies if e.id == death.enemy_id]
|
||||
})
|
||||
events = None
|
||||
if ep_events:
|
||||
events = {"entries": death, "victory_count": 0, "defeat_count": 0}
|
||||
db = orm.new_session()
|
||||
episode: Episode = db.query(Episode).get(episode_id)
|
||||
deaths = [event for event in episode.events if event.type == orm.EventType.Death]
|
||||
|
||||
model = {
|
||||
"title": f"{season.code}{episode.code}",
|
||||
"title": f"{episode.season.code}{episode.code}",
|
||||
"episode": episode,
|
||||
"season": season,
|
||||
"players": ep_players,
|
||||
"events": events,
|
||||
"season": episode.season,
|
||||
"players": episode.players,
|
||||
"deaths": sorted(deaths, key=lambda x: x.time),
|
||||
}
|
||||
|
||||
return render_template("episode_details.html", model=model)
|
||||
@@ -48,12 +27,9 @@ def episode_detail(season_id: int, episode_id: int):
|
||||
@app.route("/season/<season_id>/episode", methods=["GET"])
|
||||
@authorize
|
||||
def episode_list(season_id: int):
|
||||
sql, args = db.load_season(season_id)
|
||||
season = db.query_db(sql, args, one=True, cls=models.Season)
|
||||
sql, args = db.load_episodes(season_id)
|
||||
episodes = db.query_db(sql, args, cls=models.Episode)
|
||||
|
||||
model = {"season_id": season_id, "season_code": season.code}
|
||||
db = orm.new_session()
|
||||
season = db.query(Season).filter(Season.id == season_id).first()
|
||||
model = {"season_id": season.id, "season_code": season.code}
|
||||
return render_template("episode_list.html", model=model)
|
||||
|
||||
|
||||
@@ -65,7 +41,6 @@ def episode_new(season_id: int):
|
||||
form_title="Create New Episode",
|
||||
post_url=f"/season/{season_id}/episode/null/edit",
|
||||
)
|
||||
|
||||
form = forms.EpisodeForm(request.form)
|
||||
form.season_id.data = season_id
|
||||
return render_template("generic_form.html", model=model, form=form)
|
||||
@@ -79,15 +54,10 @@ def episode_edit(season_id: int, episode_id: int):
|
||||
form_title="Edit Episode",
|
||||
post_url=f"/season/{season_id}/episode/{episode_id}/edit",
|
||||
)
|
||||
|
||||
if request.method == "GET":
|
||||
sql, args = db.load_episode(episode_id)
|
||||
episode: models.Episode = db.query_db(sql, args, one=True, cls=models.Episode)
|
||||
|
||||
sql, args = db.load_episode_players(episode_id)
|
||||
ep_players = db.query_db(sql, args, cls=models.Player)
|
||||
|
||||
form = forms.EpisodeForm()
|
||||
db = orm.new_session()
|
||||
episode: Episode = db.query(Episode).get(episode_id)
|
||||
if request.method == "GET":
|
||||
form.season_id.data = episode.season_id
|
||||
form.episode_id.data = episode.id
|
||||
form.code.data = episode.code
|
||||
@@ -95,43 +65,24 @@ def episode_edit(season_id: int, episode_id: int):
|
||||
form.start.data = episode.start
|
||||
form.end.data = episode.end
|
||||
form.title.data = episode.title
|
||||
form.players.data = [p.id for p in ep_players]
|
||||
|
||||
form.players.data = [p.id for p in episode.players]
|
||||
model.form_title = f"Edit Episode '{episode.code}: {episode.title}'"
|
||||
return render_template("generic_form.html", model=model, form=form)
|
||||
else:
|
||||
form = forms.EpisodeForm()
|
||||
|
||||
if not form.validate_on_submit():
|
||||
model.errors = form.errors
|
||||
return render_template("generic_form.html", model=model, form=form)
|
||||
|
||||
errors = False
|
||||
episode = models.Episode.from_form(form)
|
||||
sql, args = db.save_episode(episode)
|
||||
|
||||
last_key = db.update_db(sql, args, return_key=True)
|
||||
|
||||
episode_id = episode.id if episode.id else last_key
|
||||
|
||||
form_ids = form.players.data
|
||||
|
||||
sql, args = db.load_episode_players(episode_id)
|
||||
ep_players = db.query_db(sql, args, cls=models.Player)
|
||||
pids = [p.id for p in ep_players]
|
||||
|
||||
new_ids = [pid for pid in form_ids if pid not in pids]
|
||||
removed_ids = [pid for pid in pids if pid not in form_ids]
|
||||
|
||||
if removed_ids:
|
||||
sql, args = db.remove_episode_player(episode_id, removed_ids)
|
||||
errors = db.update_db(sql, args)
|
||||
|
||||
if new_ids:
|
||||
sql, args = db.save_episode_players(episode_id, new_ids)
|
||||
errors = db.update_db(sql, args)
|
||||
|
||||
if not episode:
|
||||
episode = Episode()
|
||||
db.add(episode)
|
||||
season: Season = db.query(Season).get(season_id)
|
||||
episode.populate_from_form(form)
|
||||
episode.season = season
|
||||
player_ids = list(form.players.data)
|
||||
players = db.query(Player).filter(Player.id.in_(player_ids)).all()
|
||||
episode.players = players
|
||||
errors = db.commit()
|
||||
if errors:
|
||||
model.errors = {"Error saving episode": [errors]}
|
||||
return render_template("generic_form.html", model=model, form=form)
|
||||
return redirect(url_for("season_overview", season_id=season_id))
|
||||
return redirect(url_for("episode_detail", season_id=season_id, episode_id=episode_id))
|
||||
|
||||
@@ -2,9 +2,10 @@ from collections import namedtuple
|
||||
|
||||
from flask import render_template, request, redirect
|
||||
|
||||
from estusshots import app
|
||||
from estusshots import forms, models, db, choices
|
||||
from estusshots import app, orm
|
||||
from estusshots import forms, models, choices
|
||||
from estusshots.util import authorize
|
||||
from estusshots.orm import new_session, EventType, Event, Episode, Enemy, Penalty
|
||||
|
||||
|
||||
@app.route("/season/<s_id>/episode/<ep_id>/event/new", methods=["GET"])
|
||||
@@ -15,19 +16,16 @@ def event_new(s_id: int, ep_id: int):
|
||||
"form_title": "Create New Event",
|
||||
"post_url": f"/season/{s_id}/episode/{ep_id}/event/null/edit",
|
||||
}
|
||||
sql, args = db.load_episode(ep_id)
|
||||
episode: models.Episode = db.query_db(sql, args, one=True, cls=models.Episode)
|
||||
|
||||
sql, args = db.load_episode_players(ep_id)
|
||||
ep_players = db.query_db(sql, args, cls=models.Player)
|
||||
db = new_session()
|
||||
episode: Episode = db.query(Episode).get(ep_id)
|
||||
|
||||
form = forms.EventForm()
|
||||
form.episode_id.data = ep_id
|
||||
form.enemy.choices = choices.enemy_choice_for_season(s_id)
|
||||
form.event_type.data = 1
|
||||
form.event_type.data = EventType.Death.value
|
||||
|
||||
Penalty = namedtuple("Penalty", ["penalty_id", "player_id", "player", "drink"])
|
||||
for player in ep_players:
|
||||
for player in episode.players:
|
||||
form.penalties.append_entry(Penalty(None, player.id, player.name, 1))
|
||||
|
||||
return render_template("event_editor.html", model=model, form=form)
|
||||
@@ -41,16 +39,43 @@ def event_edit(s_id: int, ep_id: int, ev_id: int):
|
||||
"form_title": "Edit Event",
|
||||
"post_url": f"/season/{s_id}/episode/{ep_id}/event/{ev_id}/edit",
|
||||
}
|
||||
if request.method == "GET":
|
||||
return render_template("event_editor.html", model=model)
|
||||
else:
|
||||
db = new_session()
|
||||
event: Event = db.query(Event).get(ev_id)
|
||||
form = forms.EventForm()
|
||||
form.enemy.choices = choices.enemy_choice_for_season(s_id)
|
||||
|
||||
if request.method == "GET":
|
||||
form.episode_id.process_data(event.episode_id)
|
||||
form.event_type.process_data(event.type.value)
|
||||
form.enemy.process_data(event.enemy_id)
|
||||
form.player.process_data(event.player_id)
|
||||
form.time.process_data(event.time)
|
||||
form.comment.process_data(event.comment)
|
||||
Penalty = namedtuple("Penalty", ["penalty_id", "player_id", "player", "drink"])
|
||||
for penalty in event.penalties:
|
||||
form.penalties.append_entry(Penalty(penalty.id, penalty.player_id, penalty.player.name, penalty.drink_id))
|
||||
return render_template("event_editor.html", model=model)
|
||||
else:
|
||||
if not form.validate_on_submit():
|
||||
model["errors"] = form.errors
|
||||
return render_template("event_editor.html", model=model, form=form)
|
||||
if not event:
|
||||
event = Event()
|
||||
for entry in form.penalties:
|
||||
penalty = orm.Penalty()
|
||||
penalty.player_id = entry.player_id.data
|
||||
penalty.drink_id = entry.drink.data
|
||||
db.add(penalty)
|
||||
event.penalties.append(penalty)
|
||||
db.add(event)
|
||||
else:
|
||||
for event in event.penalties:
|
||||
penalty = next((p for p in form.penalties.data if p["penalty_id"] == event.id), None)
|
||||
if not penalty:
|
||||
continue
|
||||
penalty.player_id = form.player_id.data
|
||||
penalty.drink_id = form.drink.data
|
||||
|
||||
event = models.Event.from_form(form)
|
||||
sql, args = db.save_event(event)
|
||||
errors = db.update_db(sql, args)
|
||||
event.populate_from_form(form)
|
||||
db.commit()
|
||||
return redirect(f"/season/{s_id}/episode/{ep_id}")
|
||||
|
||||
Reference in New Issue
Block a user