Start of Game-creation HTML
This commit is contained in:
parent
4517b4e31f
commit
e7831948b2
12
NOTES.md
12
NOTES.md
@ -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`?
|
||||||
|
@ -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
72
app/routers/games.py
Normal 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}
|
||||||
|
)
|
@ -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)
|
||||||
|
@ -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"))
|
||||||
|
@ -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
|
||||||
|
@ -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"))
|
||||||
|
@ -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
|
||||||
|
67
app/static/js/game_create.js
Normal file
67
app/static/js/game_create.js
Normal 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
|
||||||
|
});
|
37
app/templates/games/create.html
Normal file
37
app/templates/games/create.html
Normal 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 %}
|
13
app/templates/games/detail.html
Normal file
13
app/templates/games/detail.html
Normal 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>
|
24
app/templates/games/list.html
Normal file
24
app/templates/games/list.html
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user