Flesh out player and deck HTML

This commit is contained in:
Jack Jackson 2024-01-31 19:48:58 -08:00
parent 5472dbc8b9
commit fbbaadd098
15 changed files with 243 additions and 25 deletions

View File

@ -1,12 +1,14 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from .routers import decks, players
from .routers import api_router, html_router
from .sql.models import Base
from .sql.database import engine
Base.metadata.create_all(bind=engine)
app = FastAPI()
app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.include_router(players.router)
app.include_router(decks.router)
app.include_router(api_router)
app.include_router(html_router)

View File

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

View File

@ -1,13 +1,17 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from ..templates import jinja_templates, _jsonify
from ..sql import crud, schemas
from ..sql.database import get_db
router = APIRouter()
from .players import read_player, list_players
api_router = APIRouter(tags=["deck"])
html_router = APIRouter(include_in_schema=False)
@router.post("/deck", response_model=schemas.Deck, tags=["deck"], status_code=201)
@api_router.post("/deck", response_model=schemas.Deck, status_code=201)
def create_deck(deck: schemas.DeckCreate, db: Session = Depends(get_db)):
db_player = crud.get_player_by_id(db, deck.owner_id)
if db_player is None:
@ -16,7 +20,7 @@ def create_deck(deck: schemas.DeckCreate, db: Session = Depends(get_db)):
return crud.create_deck(db=db, deck=deck)
@router.get("/deck/{deck_id}", response_model=schemas.Deck, tags=["deck"])
@api_router.get("/deck/{deck_id}", response_model=schemas.Deck)
def read_deck(deck_id: str, db = Depends(get_db)):
db_deck = crud.get_deck_by_id(db, deck_id)
if db_deck is None:
@ -24,11 +28,52 @@ def read_deck(deck_id: str, db = Depends(get_db)):
return db_deck
@router.get("/decks", response_model=list[schemas.Deck], tags=["deck"])
@api_router.get("/decks", response_model=list[schemas.Deck])
def list_decks(skip: int = 0, limit: int = 100, db = Depends(get_db)):
return crud.get_decks(db, skip=skip, limit=limit)
@router.delete("/deck/{deck_id}", tags=["deck"], status_code=204)
@api_router.delete("/deck/{deck_id}", status_code=204)
def delete_deck(deck_id: str, db = Depends(get_db)):
crud.delete_deck_by_id(db, int(deck_id))
@html_router.get("/deck/create", response_class=HTMLResponse)
def deck_create_html(request: Request, db = Depends(get_db)):
players = list_players(db=db)
return jinja_templates.TemplateResponse(
request,
"deck_create.html",
{
"players": players
}
)
@html_router.get("/deck/{deck_id}", response_class=HTMLResponse)
def deck_html(request: Request, deck_id: str, db = Depends(get_db)):
deck_info = read_deck(deck_id, db)
return jinja_templates.TemplateResponse(
request,
"deck_detail.html",
{
"deck": _jsonify(deck_info),
"owner": _jsonify(deck_info.owner)
}
)
# TODO - pagination
@html_router.get("/decks", response_class=HTMLResponse)
def decks_html(request: Request, db = Depends(get_db)):
decks = list_decks(db=db)
print(decks)
return jinja_templates.TemplateResponse(
request,
"deck_list.html",
{
# TODO - investigate if there are any issues to passing the "live" object into the
# template, as opposed to the `_jsonify`'d one.
"decks": decks
}
)

View File

@ -1,18 +1,20 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from ..templates import jinja_templates
from ..sql import crud, schemas
from ..sql.database import get_db
router = APIRouter()
api_router = APIRouter(tags=["player"])
html_router = APIRouter()
@router.post("/player", response_model=schemas.Player, tags=["player"], status_code=201)
@api_router.post("/player", response_model=schemas.Player, status_code=201)
def create_player(player: schemas.PlayerCreate, db: Session = Depends(get_db)):
return crud.create_player(db=db, player=player)
@router.get("/player/{player_id}", response_model=schemas.Player, tags=["player"])
@api_router.get("/player/{player_id}", response_model=schemas.Player)
def read_player(player_id: str, db = Depends(get_db)):
db_player = crud.get_player_by_id(db, player_id)
if db_player is None:
@ -20,11 +22,31 @@ def read_player(player_id: str, db = Depends(get_db)):
return db_player
@router.get("/players", response_model=list[schemas.Player], tags=["player"])
@api_router.get("/players", response_model=list[schemas.Player])
def list_players(skip: int = 0, limit: int = 100, db = Depends(get_db)):
return crud.get_players(db, skip=skip, limit=limit)
@router.delete("/player/{player_id}", tags=["player"], status_code=204)
@api_router.delete("/player/{player_id}", status_code=204)
def delete_player(player_id: str, db = Depends(get_db)):
crud.delete_player_by_id(db, int(player_id))
@html_router.get("/player/create", response_class=HTMLResponse)
def player_create_html(request: Request, db = Depends(get_db)):
return jinja_templates.TemplateResponse(
request,
"player_create.html"
)
@html_router.get("/player/{player_id}", response_class=HTMLResponse)
def player_html(request: Request, player_id: str, db = Depends(get_db)):
player_info = read_player(player_id, db)
return jinja_templates.TemplateResponse(
request,
"player_detail.html",
{
"player": player_info
}
)

3
app/static/js/base.js Normal file
View File

@ -0,0 +1,3 @@
$.ajaxSetup({
contentType: "application/json; charset=utf-8"
});

View File

@ -0,0 +1,16 @@
$(document).ready(function() {
$('#create_button').click(function() {
$.post({
url: '/api/deck',
data: JSON.stringify({
'name': $('#name').val(),
'description': $('#description').val(),
'owner_id': $('#owner_id').val()
}),
contentType: 'application/json; charset=utf-8'
}).done(function (response) {
deck_id = response['id'];
window.location.href = '/deck/' + deck_id
});
})
})

View File

@ -0,0 +1,14 @@
$(document).ready(function() {
$('#create_button').click(function() {
$.post({
url: '/api/player',
data: JSON.stringify({
'name': $('#name').val()
}),
contentType: 'application/json; charset=utf-8'
}).done(function (response) {
player_id = response['id'];
window.location.href = '/player/' + player_id
});
})
})

16
app/templates/__init__.py Normal file
View File

@ -0,0 +1,16 @@
# *NO* idea if this layout is actually good or encouraged -
# but I couldn't put this instantiation in `app.__init__.py` or
# `app.routers.__init__.py` and then import from there into (say)
# `app.routers.decks.py` because:
#
# ImportError: cannot import name 'jinja_templates' from partially initialized module 'app.routers' (most likely due to a circular import) (/Users/scubbo/Code/edh-elo/app/routers/__init__.py)
from fastapi.templating import Jinja2Templates
jinja_templates = Jinja2Templates(directory="app/templates")
# TODO - would this be better as a method on a class extending `db.Model` that the classes in `models.py` could then
# extend?
# (Probably not, as we'd still need to explicitly call it - it wouldn't be implicitly called _by_ Flask)
def _jsonify(o):
return {k: v for (k, v) in o.__dict__.items() if k != "_sa_instance_state"}

19
app/templates/base.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<title>{% block title %}{% endblock %}</title>
<script
src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous"></script>
{% endblock %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">
{% block footer %}
{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}Create Deck{% endblock %}
{% block head %}
{{ super() }}
<script src="/static/js/deck_create.js"></script>
{% endblock %}
{% block content %}
<label for="name">Deck Name</label>
<input type="text" name="name" id="name" />
<label for="description">Description (optional)</label>
<input type="text" name="description" id="description" />
<label for="owner_id">Owner</label>
<select name="owner_id" id="owner_id">
{% for player in players %}
<option value="{{ player.id }}">{{ player.name }}</option>
{% endfor %}
</select>
<input type="button" id="create_button" value="Submit"/>
</form>
{% endblock %}

View File

@ -5,10 +5,9 @@
<title>Deck - {{ deck.name }}</title>
</head>
<body>
<h1>Hello World!</h1>
<h2>This is the page for deck {{ deck.name }} with id {{ deck.id }}, owned by {{ owner.name }}</h2>
{% if description %}
<p>The description of the deck is: {{ description }}</p>
{% if deck.description %}
<p>The description of the deck is: {{ deck.description }}</p>
{% endif %}
</body>
</html>
</html>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Decks</title>
</head>
<body>
<h1>Decks</h1>
<table>
<tr>
<th>Deck Name</th>
<th>Owner</th>
</tr>
{% for deck in decks %}
<tr>
<td>
<a href="/deck/{{ deck.id }}">{{ deck.name }}</a>
</td>
<td>{{ deck.owner.name }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Create Player{% endblock %}
{% block head %}
{{ super() }}
<script src="/static/js/player_create.js"></script>
{% endblock %}
{% block content %}
<label for="name">Player Name</label>
<input type="text" name="name" id="name" />
<input type="button" id="create_button" value="Submit"/>
</form>
{% endblock %}

View File

@ -2,10 +2,10 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Player - {{ name }}</title>
<title>Player - {{ player.name }}</title>
</head>
<body>
<h1>Hello World!</h1>
<h2>This is the page for player {{ name }} who has id {{ id }}</h2>
<h2>This is the page for player {{ player.name }} who has id {{ player.id }}</h2>
</body>
</html>

View File

@ -50,6 +50,9 @@ def test_add_and_retrieve_deck(test_client: TestClient):
assert get_deck_response.status_code == 200
assert get_deck_response.json()["name"] == "Baby's First Deck"
# Very basic HTML testing
html_response = test_client.get(f"/deck/{deck_id}")
assert "owned by jim" in html_response.text
# Cleanup
delete_response = _json_delete(test_client, f"/deck/{deck_id}")
@ -57,12 +60,12 @@ def test_add_and_retrieve_deck(test_client: TestClient):
def _json_get(c: TestClient, path: str) -> httpx.Response:
return c.get(path, headers={"Content-Type": "application/json"})
return c.get(f'/api{path}', headers={"Content-Type": "application/json"})
def _json_post(c: TestClient, path: str, body: Mapping) -> httpx.Response:
return c.post(path, headers={"Content-Type": "application/json"}, json=body)
return c.post(f'/api{path}', headers={"Content-Type": "application/json"}, json=body)
def _json_delete(c: TestClient, path: str) -> httpx.Response:
return c.delete(path, headers={"Content-Type": "application/json"})
return c.delete(f'/api{path}', headers={"Content-Type": "application/json"})