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] Basic List pages for entities
- [X] Swagger API - [X] Swagger API
- [ ] Local development tool to clear/seed database - [ ] 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 - [ ] Load Game-history from file
- [ ] Favicon
... ...
- [ ] Authentication (will need to link `user` table to `player`) - [ ] Authentication (will need to link `user` table to `player`)
... ...
- [ ] Helm chart including an initContainer to create the database if it doesn't exist already - [ ] 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 # Tables
@ -46,5 +50,7 @@ https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-yo
# Things to learn/fix/understand # 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 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 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 fastapi import APIRouter
from . import decks, players from . import decks, games, players
api_router = APIRouter(prefix="/api") api_router = APIRouter(prefix="/api")
html_router = APIRouter() html_router = APIRouter()
api_router.include_router(decks.api_router) api_router.include_router(decks.api_router)
api_router.include_router(players.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(decks.html_router)
html_router.include_router(players.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) 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) @api_router.get("/{player_id}", response_model=schemas.Player)
def read_player(player_id: int, db=Depends(get_db)): def read_player(player_id: int, db=Depends(get_db)):
db_player = crud.get_player_by_id(db, player_id) db_player = crud.get_player_by_id(db, player_id)

View File

@ -4,8 +4,6 @@ from .database import SessionLocal
def prime_database(): def prime_database():
db = SessionLocal() db = SessionLocal()
win_types = db.query(models.WinType).all() win_types = db.query(models.WinType).all()
print('Win types are:')
print(win_types)
if not win_types: if not win_types:
db.add(models.WinType(name="Combat Damage")) db.add(models.WinType(name="Combat Damage"))
db.add(models.WinType(name="Commander 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.query(models.Deck).filter(models.Deck.id == deck_id).delete()
db.commit() db.commit()
return "", 204 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 sqlalchemy.orm import relationship
from .database import Base from .database import Base
@ -35,6 +35,7 @@ class Game(Base):
__tablename__ = "games" __tablename__ = "games"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
date = Column(DateTime, nullable=False)
deck_id_1 = Column(Integer, ForeignKey("decks.id"), nullable=False) deck_id_1 = Column(Integer, ForeignKey("decks.id"), nullable=False)
deck_id_2 = 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")) 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 from pydantic import BaseModel
@ -47,6 +47,7 @@ class WinType(WinTypeBase):
class GameBase(BaseModel): class GameBase(BaseModel):
date: int
deck_id_1: int deck_id_1: int
deck_id_2: int deck_id_2: int
deck_id_3: 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>