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 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
|
||||
|
54
app/main.py
54
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/<player_id>", 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/<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
|
||||
|
@ -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):
|
||||
|
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
|
||||
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 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()
|
||||
|
||||
|
||||
|
@ -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"})
|
||||
|
Loading…
x
Reference in New Issue
Block a user