diff --git a/NOTES.md b/NOTES.md index f50ee57..5c55052 100644 --- a/NOTES.md +++ b/NOTES.md @@ -2,10 +2,10 @@ - [X] Basic Game Definition - [X] Basic Player Definition -- [ ] Basic Deck Definition +- [X] Basic Deck Definition - [X] Figure out how to return JSON or html (`render_template`) - [X] Basic testing -- [ ] ruff +- [X] ruff - [ ] GitHub Actions for tests and linters - [ ] Swagger API - [ ] Local development tool to clear/seed database diff --git a/app/main.py b/app/main.py index 42e3108..cb9f746 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,6 @@ from flask import Blueprint, render_template, request from . import db -from .models import Player +from .models import Deck, Player main = Blueprint("main", __name__) @@ -34,8 +34,56 @@ def get_player(player_id: str): 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==`? -# Time for testing, methinks! +@main.route("/player/", methods=["DELETE"]) +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/") +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/", 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 diff --git a/app/models.py b/app/models.py index 1b3ecc6..c4ddb67 100644 --- a/app/models.py +++ b/app/models.py @@ -13,7 +13,9 @@ class Deck(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(60), nullable=False) 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): diff --git a/app/templates/deck_detail.html b/app/templates/deck_detail.html new file mode 100644 index 0000000..8025a03 --- /dev/null +++ b/app/templates/deck_detail.html @@ -0,0 +1,14 @@ + + + + + Deck - {{ deck.name }} + + +

Hello World!

+

This is the page for deck {{ deck.name }} with id {{ deck.id }}, owned by {{ owner.name }}

+ {% if description %} +

The description of the deck is: {{ description }}

+ {% endif %} + + \ No newline at end of file diff --git a/basic-run.sh b/basic-run.sh index 2345ede..2ef57f9 100755 --- a/basic-run.sh +++ b/basic-run.sh @@ -2,4 +2,5 @@ # Idempotent 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 diff --git a/tests/conftest.py b/tests/conftest.py index 0066299..532a47b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,9 @@ import os import pathlib 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 # https://stackoverflow.com/questions/34466027/what-is-conftest-py-for-in-pytest @@ -11,7 +14,7 @@ from app import create_app @pytest.fixture() -def app_fixture(): +def app_fixture() -> Flask: # Start afresh! test_database_name = "testing-db.sqlite" database_location = pathlib.Path("instance").joinpath(test_database_name) @@ -34,7 +37,7 @@ def app_fixture(): @pytest.fixture() -def client(app_fixture): +def client(app_fixture: Flask) -> FlaskClient: return app_fixture.test_client() diff --git a/tests/test_fresh_db_tests.py b/tests/test_fresh_db_tests.py index 837c985..1a7641e 100644 --- a/tests/test_fresh_db_tests.py +++ b/tests/test_fresh_db_tests.py @@ -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. # 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): - response = client.get("/player/1", headers={"Content-Type": "application/json"}) +def test_add_and_retrieve_player(client: FlaskClient): + response = _json_get(client, "/player/1") assert response.status_code == 404 - client.post( - "/player", headers={"Content-Type": "application/json"}, json={"name": "jason"} - ) - response_1 = client.get("/player/1", headers={"Content-Type": "application/json"}) + _json_post(client, "/player", {"name": "jason"}) + response_1 = _json_get(client, "/player/1") 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"})