Add Deck CRUD

This commit is contained in:
Jack Jackson 2024-01-24 19:57:47 -08:00
parent af0ab022ce
commit ada7473610
7 changed files with 141 additions and 15 deletions

View File

@ -2,10 +2,10 @@
- [X] Basic Game Definition - [X] Basic Game Definition
- [X] Basic Player Definition - [X] Basic Player Definition
- [ ] Basic Deck Definition - [X] Basic Deck Definition
- [X] Figure out how to return JSON or html (`render_template`) - [X] Figure out how to return JSON or html (`render_template`)
- [X] Basic testing - [X] Basic testing
- [ ] ruff - [X] ruff
- [ ] GitHub Actions for tests and linters - [ ] GitHub Actions for tests and linters
- [ ] Swagger API - [ ] Swagger API
- [ ] Local development tool to clear/seed database - [ ] Local development tool to clear/seed database

View File

@ -1,6 +1,6 @@
from flask import Blueprint, render_template, request from flask import Blueprint, render_template, request
from . import db from . import db
from .models import Player from .models import Deck, Player
main = Blueprint("main", __name__) main = Blueprint("main", __name__)
@ -34,8 +34,56 @@ def get_player(player_id: str):
return render_template("player_detail.html", **player_data) return render_template("player_detail.html", **player_data)
# TODO - implement a GET method - can it be a separate method, or must it be the same annotation with an `if method==`? @main.route("/player/<player_id>", methods=["DELETE"])
# Time for testing, methinks! def delete_player(player_id: str):
# Note - no checking that the player exists, because HTTP semantics specify
# that `DELETE` should be idempotent.
db.session.query(Player).filter(Player.id == int(player_id)).delete()
db.session.commit()
return "", 204
@main.route("/deck", methods=["POST"])
def create_deck():
data = request.json
owner_id = data["owner_id"]
player_from_db = db.session.get(Player, int(owner_id))
if not player_from_db:
return f"Owner id {owner_id} not found", 400
deck = Deck(
name=data["name"], description=data.get("description"), owner_id=owner_id
)
db.session.add(deck)
db.session.commit()
print("Finished creating the deck!")
return {"id": deck.id}
@main.route("/deck/<deck_id>")
def get_deck(deck_id: str):
deck_from_db = db.session.get(Deck, int(deck_id))
if not deck_from_db:
return "Not Found", 404
deck_data = _jsonify(deck_from_db)
content_type = request.headers.get("Content-Type")
if content_type == "application/json":
return deck_data
else: # Assume they want HTML
owner_data = db.session.get(Player, int(deck_data["owner_id"]))
return render_template(
"deck_detail.html", deck=deck_data, owner=_jsonify(owner_data)
)
@main.route("/deck/<deck_id>", methods=["DELETE"])
def delete_deck(deck_id: str):
db.session.query(Deck).filter(Deck.id == int(deck_id)).delete()
db.session.commit()
return "", 204
# TODO - would this be better as a method on a class extending `db.Model` that the classes in `models.py` could then # TODO - would this be better as a method on a class extending `db.Model` that the classes in `models.py` could then

View File

@ -13,7 +13,9 @@ class Deck(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), nullable=False) name = db.Column(db.String(60), nullable=False)
description = db.Column(db.String) description = db.Column(db.String)
owner = db.Column(db.String, db.ForeignKey(Player.__table__.c.id), nullable=False) owner_id = db.Column(
db.String, db.ForeignKey(Player.__table__.c.id), nullable=False
)
class Game(db.Model): class Game(db.Model):

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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>
{% endif %}
</body>
</html>

View File

@ -2,4 +2,5 @@
# Idempotent # Idempotent
source .venv/bin/activate source .venv/bin/activate
FLASK_APP=app SECRET_KEY=super-secret flask run # `--debug` enables live-refresh
FLASK_APP=app SECRET_KEY=super-secret flask --debug run

View File

@ -2,6 +2,9 @@ import os
import pathlib import pathlib
import pytest import pytest
from flask import Flask
from flask.testing import FlaskClient
# TODO - this seems to be a "magic path" which makes fixtures available. Learn more about it by reading # TODO - this seems to be a "magic path" which makes fixtures available. Learn more about it by reading
# https://stackoverflow.com/questions/34466027/what-is-conftest-py-for-in-pytest # https://stackoverflow.com/questions/34466027/what-is-conftest-py-for-in-pytest
@ -11,7 +14,7 @@ from app import create_app
@pytest.fixture() @pytest.fixture()
def app_fixture(): def app_fixture() -> Flask:
# Start afresh! # Start afresh!
test_database_name = "testing-db.sqlite" test_database_name = "testing-db.sqlite"
database_location = pathlib.Path("instance").joinpath(test_database_name) database_location = pathlib.Path("instance").joinpath(test_database_name)
@ -34,7 +37,7 @@ def app_fixture():
@pytest.fixture() @pytest.fixture()
def client(app_fixture): def client(app_fixture: Flask) -> FlaskClient:
return app_fixture.test_client() return app_fixture.test_client()

View File

@ -1,13 +1,71 @@
from typing import Mapping
from flask.testing import FlaskClient
from werkzeug.test import TestResponse
# These tests expect that the database starts empty. # These tests expect that the database starts empty.
# TODO: create tests with initialized states # TODO: create tests with initialized states
# TODO - these cannot be run in parallel because they involve assumptions about the same database
def test_add_and_retrieve(client): def test_add_and_retrieve_player(client: FlaskClient):
response = client.get("/player/1", headers={"Content-Type": "application/json"}) response = _json_get(client, "/player/1")
assert response.status_code == 404 assert response.status_code == 404
client.post( _json_post(client, "/player", {"name": "jason"})
"/player", headers={"Content-Type": "application/json"}, json={"name": "jason"} response_1 = _json_get(client, "/player/1")
)
response_1 = client.get("/player/1", headers={"Content-Type": "application/json"})
assert response_1.json["name"] == "jason" assert response_1.json["name"] == "jason"
# Cleanup
# TODO - put this in a finally clause (or similar as provided by pytest)
delete_response = _json_delete(client, "/player/1")
assert delete_response.status_code == 204
def test_add_and_retrieve_deck(client: FlaskClient):
not_found_response = _json_get(client, "/deck/1")
assert not_found_response.status_code == 404
# Try (and fail) to create a deck owned by a non-existent player
invalid_owner_response = _json_post(
client, "/deck", {"name": "Baby's First Deck", "owner_id": 1}
)
assert invalid_owner_response.status_code == 400
assert invalid_owner_response.text == "Owner id 1 not found"
create_jim_response = _json_post(client, "/player", {"name": "jim"})
assert create_jim_response.status_code == 200
jim_id = create_jim_response.json["id"]
create_deck_response = _json_post(
client, "/deck", {"name": "Baby's First Deck", "owner_id": str(jim_id)}
)
assert create_deck_response.status_code == 200
# _Should_ always be 1, since we expect to start with an empty database, but why risk it?
deck_id = create_deck_response.json["id"]
get_deck_response = _json_get(client, f"/deck/{deck_id}")
assert get_deck_response.status_code == 200
assert get_deck_response.json["name"] == "Baby's First Deck"
############
# VERY basic template testing!
############
html_response = client.get(f"/deck/{deck_id}")
assert "owned by jim" in html_response.text
# Cleanup
delete_response = _json_delete(client, f"/deck/{deck_id}")
assert delete_response.status_code == 204
def _json_get(c: FlaskClient, path: str) -> TestResponse:
return c.get(path, headers={"Content-Type": "application/json"})
def _json_post(c: FlaskClient, path: str, body: Mapping) -> TestResponse:
return c.post(path, headers={"Content-Type": "application/json"}, json=body)
def _json_delete(c: FlaskClient, path: str) -> TestResponse:
return c.delete(path, headers={"Content-Type": "application/json"})