Add Deck CRUD
This commit is contained in:
parent
af0ab022ce
commit
ada7473610
4
NOTES.md
4
NOTES.md
@ -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
|
||||||
|
54
app/main.py
54
app/main.py
@ -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
|
||||||
|
@ -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):
|
||||||
|
14
app/templates/deck_detail.html
Normal file
14
app/templates/deck_detail.html
Normal 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>
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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"})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user