From e7831948b2dc042793a1e599adc00d0ad4ebe1f5 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Mon, 5 Feb 2024 21:13:34 -0800 Subject: [PATCH] Start of Game-creation HTML --- NOTES.md | 12 ++++-- app/routers/__init__.py | 4 +- app/routers/games.py | 72 +++++++++++++++++++++++++++++++++ app/routers/players.py | 3 -- app/sql/__init__.py | 2 - app/sql/crud.py | 22 ++++++++++ app/sql/models.py | 3 +- app/sql/schemas.py | 3 +- app/static/js/game_create.js | 67 ++++++++++++++++++++++++++++++ app/templates/games/create.html | 37 +++++++++++++++++ app/templates/games/detail.html | 13 ++++++ app/templates/games/list.html | 24 +++++++++++ 12 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 app/routers/games.py create mode 100644 app/static/js/game_create.js create mode 100644 app/templates/games/create.html create mode 100644 app/templates/games/detail.html create mode 100644 app/templates/games/list.html diff --git a/NOTES.md b/NOTES.md index e785a28..ff31d8f 100644 --- a/NOTES.md +++ b/NOTES.md @@ -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)? \ No newline at end of file +* ~~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`? diff --git a/app/routers/__init__.py b/app/routers/__init__.py index a68b1f2..8941d8c 100644 --- a/app/routers/__init__.py +++ b/app/routers/__init__.py @@ -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) diff --git a/app/routers/games.py b/app/routers/games.py new file mode 100644 index 0000000..54bd565 --- /dev/null +++ b/app/routers/games.py @@ -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} + ) \ No newline at end of file diff --git a/app/routers/players.py b/app/routers/players.py index 73b7811..1e5068d 100644 --- a/app/routers/players.py +++ b/app/routers/players.py @@ -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) diff --git a/app/sql/__init__.py b/app/sql/__init__.py index 12c1ce2..daef4e1 100644 --- a/app/sql/__init__.py +++ b/app/sql/__init__.py @@ -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")) diff --git a/app/sql/crud.py b/app/sql/crud.py index 183ce00..3d526c3 100644 --- a/app/sql/crud.py +++ b/app/sql/crud.py @@ -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 diff --git a/app/sql/models.py b/app/sql/models.py index 8e86114..cb6c6e5 100644 --- a/app/sql/models.py +++ b/app/sql/models.py @@ -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")) diff --git a/app/sql/schemas.py b/app/sql/schemas.py index 80d6c3c..b2fd237 100644 --- a/app/sql/schemas.py +++ b/app/sql/schemas.py @@ -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 diff --git a/app/static/js/game_create.js b/app/static/js/game_create.js new file mode 100644 index 0000000..4574db5 --- /dev/null +++ b/app/static/js/game_create.js @@ -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(''); + 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(''); + for (deck of player_decks) { + actual_select.append(``); + } + // 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 +}); \ No newline at end of file diff --git a/app/templates/games/create.html b/app/templates/games/create.html new file mode 100644 index 0000000..7d173b9 --- /dev/null +++ b/app/templates/games/create.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}Create Game{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block content %} + + + + + + + +

Note - currently no ability to create a player from this page. Go here if you need to add someone.

+ +{% for num in range(6) %} +
1 %}style="display:none;"{% endif %}> + + +
+{% endfor %} + + +{% endblock %} diff --git a/app/templates/games/detail.html b/app/templates/games/detail.html new file mode 100644 index 0000000..a1e4d22 --- /dev/null +++ b/app/templates/games/detail.html @@ -0,0 +1,13 @@ + + + + + Game - {{ game.id }} + + +

This is the page for game with id {{ game.id }}, played on date {{ game.date }}

+ {% if deck.description %} +

The description of the game is: {{ game.description }}

+ {% endif %} + + diff --git a/app/templates/games/list.html b/app/templates/games/list.html new file mode 100644 index 0000000..ca05137 --- /dev/null +++ b/app/templates/games/list.html @@ -0,0 +1,24 @@ + + + + + Games + + +

Games

+ + + + + + {% for game in games %} + + + + + {% endfor %} +
DateDeck ID 1
{{ game.date }} + {{ game.deck_id_1 }} +
+ +