Start of Game-creation HTML

This commit is contained in:
Jack Jackson 2024-02-05 21:13:34 -08:00
parent 4517b4e31f
commit e7831948b2
12 changed files with 251 additions and 11 deletions

View File

@ -9,12 +9,16 @@
- [X] Basic List pages for entities
- [X] Swagger API
- [ ] Local development tool to clear/seed database
- [ ] CRUD APIs for Games
- [X] CRUD APIs for Games
- [ ] HTML Page to Create Game
- Mostly done, just need to implement actual submission, (if desired) deck-creation in-page, and non-deck metadata (who won, turn count, etc.).
- [ ] Load Game-history from file
- [ ] Favicon
...
- [ ] Authentication (will need to link `user` table to `player`)
...
- [ ] Helm chart including an initContainer to create the database if it doesn't exist already
- [ ] GroupId (i.e. so I can host other people's data? That's _probably_ a YAGNI - see if there's demand!)
# Tables
@ -46,5 +50,7 @@ https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-yo
# Things to learn/fix/understand
* How to use an existing Python class as the spec in a Swagger API definition? (See, for instance, `responses.200.content.application/json.schema.id` under `get_player`). Perhaps it's not possible just by name, which would be understandable (the context of a Docstring probably doesn't include the context of imported code) - but, in that case, at least I'd like to be able to specify `definitions`` in a way that is independent of a method.
* How to specify the content-type that should be sent with a Swagger request without needing to use `requestBody` (and thus, implying that there _should_ be a body)?
* ~~How to use an existing Python class as the spec in a Swagger API definition? (See, for instance, `responses.200.content.application/json.schema.id` under `get_player`). Perhaps it's not possible just by name, which would be understandable (the context of a Docstring probably doesn't include the context of imported code) - but, in that case, at least I'd like to be able to specify `definitions`` in a way that is independent of a method.~~
* ~~How to specify the content-type that should be sent with a Swagger request without needing to use `requestBody` (and thus, implying that there _should_ be a body)?~~
* Fixed by using FastApi instead of Flask
* How to abstract out the standard-definitions of `routers/*` and `sql/crud.py`?

View File

@ -1,12 +1,14 @@
from fastapi import APIRouter
from . import decks, players
from . import decks, games, players
api_router = APIRouter(prefix="/api")
html_router = APIRouter()
api_router.include_router(decks.api_router)
api_router.include_router(players.api_router)
api_router.include_router(games.api_router)
html_router.include_router(decks.html_router)
html_router.include_router(players.html_router)
html_router.include_router(games.html_router)

72
app/routers/games.py Normal file
View File

@ -0,0 +1,72 @@
import json
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from .players import list_players
from ..templates import jinja_templates
from ..sql import crud, schemas
from ..sql.database import get_db
api_router = APIRouter(prefix="/game", tags=["game"])
html_router = APIRouter(prefix="/game", include_in_schema=False)
@api_router.post("/", response_model=schemas.Game, status_code=201)
def create_game(game: schemas.GameCreate, db: Session = Depends(get_db)):
return crud.create_game(db=db, game=game)
@api_router.get("/list", response_model=list[schemas.Game])
def list_games(skip: int = 0, limit: int = 100, db=Depends(get_db)):
return crud.get_games(db, skip=skip, limit=limit)
@api_router.get("/{game_id}", response_model=schemas.Game)
def read_game(game_id: int, db=Depends(get_db)):
db_game = crud.get_game_by_id(db, game_id)
if db_game is None:
raise HTTPException(status_code=404, detail="Game not found")
return db_game
@api_router.delete("/{game_id}", status_code=204)
def delete_game(game_id: str, db=Depends(get_db)):
crud.delete_game_by_id(db, int(game_id))
@html_router.get("/create", response_class=HTMLResponse)
def game_create_html(request: Request, db=Depends(get_db)):
players = list_players(db=db)
return jinja_templates.TemplateResponse(
request, "games/create.html", {
"players": players,
# `json.dumps` is necessary because otherwise
# the keys are surrounded with single-quotes,
# on which JavaScript's `JSON.parse` will choke.
"player_decks": json.dumps({
str(player.id): [{
key: getattr(deck, key)
for key in ['id', 'name']
} for deck in player.decks]
for player in players
})
})
# TODO - pagination
@html_router.get("/list", response_class=HTMLResponse)
def games_html(request: Request, db=Depends(get_db)):
games = list_games(db=db)
return jinja_templates.TemplateResponse(
request, "games/list.html", {"games": games}
)
# This must be after the static-path routes, lest it take priority over them
@html_router.get("/{game_id}", response_class=HTMLResponse)
def game_html(request: Request, game_id: str, db=Depends(get_db)):
game_info = read_game(game_id, db)
return jinja_templates.TemplateResponse(
request, "games/detail.html", {"game": game_info}
)

View File

@ -20,9 +20,6 @@ def list_players(skip: int = 0, limit: int = 100, db=Depends(get_db)):
return crud.get_players(db, skip=skip, limit=limit)
# TODO - https://fastapi.tiangolo.com/tutorial/path-params/#order-matters
# suggests that putting this after `/list` should allow `/api/player/list` to properly
# trigger `list_players`, but it doesn't.
@api_router.get("/{player_id}", response_model=schemas.Player)
def read_player(player_id: int, db=Depends(get_db)):
db_player = crud.get_player_by_id(db, player_id)

View File

@ -4,8 +4,6 @@ from .database import SessionLocal
def prime_database():
db = SessionLocal()
win_types = db.query(models.WinType).all()
print('Win types are:')
print(win_types)
if not win_types:
db.add(models.WinType(name="Combat Damage"))
db.add(models.WinType(name="Commander Damage"))

View File

@ -44,3 +44,25 @@ def delete_deck_by_id(db: Session, deck_id: int):
db.query(models.Deck).filter(models.Deck.id == deck_id).delete()
db.commit()
return "", 204
def get_game_by_id(db: Session, game_id: int):
return db.query(models.Game).filter(models.Game.id == game_id).first()
def get_games(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Game).offset(skip).limit(limit).all()
def create_game(db: Session, game: schemas.GameCreate):
db_game = models.Game(**game.model_dump)
db.add(db_game)
db.commit()
db.refresh(db_game)
return db_game
def delete_game_by_id(db: Session, game_id: int):
db.query(models.Game).filter(models.Game.id == game_id).delete()
db.commit()
return "", 204

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from .database import Base
@ -35,6 +35,7 @@ class Game(Base):
__tablename__ = "games"
id = Column(Integer, primary_key=True)
date = Column(DateTime, nullable=False)
deck_id_1 = Column(Integer, ForeignKey("decks.id"), nullable=False)
deck_id_2 = Column(Integer, ForeignKey("decks.id"), nullable=False)
deck_id_3 = Column(Integer, ForeignKey("decks.id"))

View File

@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Optional
from pydantic import BaseModel
@ -47,6 +47,7 @@ class WinType(WinTypeBase):
class GameBase(BaseModel):
date: int
deck_id_1: int
deck_id_2: int
deck_id_3: int

View File

@ -0,0 +1,67 @@
function change_num_players(eventData) {
const num_players = $(eventData['target']).val();
console.log('num_players ' + num_players)
$('.player_div').each(function(_, elem) {
id = elem.id;
const player_id = parseInt(id.substring('div_for_player_'.length));
if (player_id > num_players) {
$(elem).hide();
} else {
$(elem).show();
}
})
}
function change_player(eventData) {
target = $(eventData['target']);
deck_select = target.siblings('.deck_select');
console.log('Deck select is');
console.log(deck_select);
const target_val = parseInt(target.val());
if (target_val == -1) {
deck_select.hide();
} else {
// A player has been selected. If the deck_select dropdown does not
// already exist, create it. Either way, then (re-)populate it.
if (deck_select.length == 0) {
target.parent().append('<select class="deck_select"></select>');
deck_select = target.siblings('.deck_select');
}
console.log('DEBUG - player_deck_data is');
console.log($('#player_deck_data').val());
player_deck_data = JSON.parse($('#player_deck_data').val());
if (!(target_val.toString() in player_deck_data)) {
alert(`Could not find player_id ${target_val} in player_deck_data.`)
return;
}
player_decks = player_deck_data[target_val.toString()];
actual_select = $(deck_select[0]);
actual_select.empty();
actual_select.append('<option val="-1">Select Deck...</option>');
for (deck of player_decks) {
actual_select.append(`<option val="${deck.id}">${deck.name}</option>`);
}
// Just in case it's been previously hidden
actual_select.show();
}
}
function initialize_dropdowns() {
console.log('TODO - initialize dropdowns');
}
$(document).ready(function() {
$('#number_of_players').on("change", change_num_players)
$('.player_select').on("change", change_player)
initialize_dropdowns()
// TODO - initialize dropdowns
// TODO - submit logic should:
// * Check that Players are unique
});

View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}Create Game{% endblock %}
{% block head %}
{{ super() }}
<script src="/static/js/game_create.js"></script>
{% endblock %}
{% block content %}
<input type="hidden" id="player_deck_data" value="{{ player_decks }}"/>
<label for="date">Date:</label>
<input type="date" id="date" name="date">
<label for="number_of_players">Number Of Players:</label>
<select id="number_of_players" name="number_of_players">
{% for num in range(5) %}
<option value="{{ num+2 }}">{{ num+2 }}</option>
{% endfor %}
</select>
<p>Note - currently no ability to create a player from this page. Go <a href="/player/create">here</a> if you need to add someone.</p>
{% for num in range(6) %}
<div id="div_for_player_{{ num+1 }}" class="player_div" {% if num > 1 %}style="display:none;"{% endif %}>
<label for="player_select_{{ num+1 }}">Player {{ num+1 }}</label>
<select id="player_select_{{ num+1 }}" class="player_select" name="player_id_{{ num+1 }}">
<option value="-1">Select player...</option>
{% for player in players %}
<option value="{{ player.id }}">{{ player.name }}</option>
{% endfor %}
</select>
</div>
{% endfor %}
<input type="button" id="submit" value="Submit"/>
{% endblock %}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Game - {{ game.id }}</title>
</head>
<body>
<h2>This is the page for game with id {{ game.id }}, played on date {{ game.date }}</h2>
{% if deck.description %}
<p>The description of the game is: {{ game.description }}</p>
{% endif %}
</body>
</html>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Games</title>
</head>
<body>
<h1>Games</h1>
<table>
<tr>
<th>Date</th>
<th>Deck ID 1</th>
</tr>
{% for game in games %}
<tr>
<td>{{ game.date }}</td>
<td>
{{ game.deck_id_1 }}
</td>
</tr>
{% endfor %}
</table>
</body>
</html>