From 7cbdadef454adeb6062e46a63d86ce81335b9a3e Mon Sep 17 00:00:00 2001 From: ZanSara Date: Sat, 5 Aug 2023 22:27:31 +0100 Subject: [PATCH] templates management --- flashcards_htmx/api/cards.py | 192 ++++++++++++++ flashcards_htmx/api/components.py | 139 ---------- flashcards_htmx/api/decks.py | 161 ++++++++++++ flashcards_htmx/api/private.py | 245 +++--------------- flashcards_htmx/api/templates.py | 110 ++++++++ flashcards_htmx/app.py | 57 ++-- flashcards_htmx/static/css/private-base.css | 1 + flashcards_htmx/static/css/public-base.css | 6 + .../templates/components/template.html | 9 + flashcards_htmx/templates/private/deck.html | 3 - .../templates/private/profile.html | 17 +- .../templates/private/template.html | 31 +++ .../templates/private/templates.html | 11 + .../templates/responses/templates.html | 5 + flashcards_htmx/tmp/11.json | 54 ++++ 15 files changed, 663 insertions(+), 378 deletions(-) create mode 100644 flashcards_htmx/api/cards.py delete mode 100644 flashcards_htmx/api/components.py create mode 100644 flashcards_htmx/api/decks.py create mode 100644 flashcards_htmx/api/templates.py create mode 100644 flashcards_htmx/templates/components/template.html create mode 100644 flashcards_htmx/templates/private/template.html create mode 100644 flashcards_htmx/templates/private/templates.html create mode 100644 flashcards_htmx/templates/responses/templates.html create mode 100644 flashcards_htmx/tmp/11.json diff --git a/flashcards_htmx/api/cards.py b/flashcards_htmx/api/cards.py new file mode 100644 index 0000000..a9c84f6 --- /dev/null +++ b/flashcards_htmx/api/cards.py @@ -0,0 +1,192 @@ +from typing import Optional +from pathlib import Path +import shelve + +from jinja2 import Template +import starlette.status as status +from fastapi import APIRouter, Request, Depends, HTTPException +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates + +from flashcards_htmx.app import template, database + + +templates = Jinja2Templates(directory=Path(__file__).parent / "templates") +router = APIRouter() + + +@router.get("/decks/{deck_id}/cards", response_class=HTMLResponse) +async def cards_page( + deck_id: str, request: Request, render=Depends(template("private/cards.html")) +): + with shelve.open(database) as db: + deck = db["decks"].get(deck_id, {}) + if not deck: + raise HTTPException(status_code=404, detail="Deck not found") + return render( + navbar_title=deck["name"], + deck=deck, + deck_id=deck_id, + searchable=True, + new_item_endpoint=request.url_for("create_card_page", deck_id=deck_id), + new_item_text="New Card...", + ) + + +@router.get("/htmx/components/decks/{deck_id}/cards", response_class=HTMLResponse) +async def cards_component( + deck_id: str, render=Depends(template("responses/cards.html")) +): + with shelve.open(database) as db: + deck = db["decks"].get(deck_id, {}) + if not deck: + raise HTTPException(status_code=404, detail="Deck not found") + + card_templates = db["templates"] + for card in deck["cards"].values(): + card["rendered_preview"] = Template( + card_templates[card["type"]]["preview"] + ).render(**card["data"]) + return render(deck=deck, deck_id=deck_id) + + +@router.get("/decks/{deck_id}/cards/new", response_class=HTMLResponse) +async def create_card_page(deck_id: str, render=Depends(template("private/card.html"))): + with shelve.open(database) as db: + deck = db["decks"].get(deck_id, {}) + if not deck: + raise HTTPException(status_code=404, detail="Deck not found") + + id = len(db["decks"].get(deck_id, {}).get("cards", {})) + 1 + card_templates = db["templates"] + for template in card_templates.values(): + template["rendered_form"] = Template(template["form"]).render( + question={}, answer={}, preview={} + ) + return render( + navbar_title=deck["name"], + deck=deck, + deck_id=deck_id, + card={ + "id": id, + "data": { + "question": {}, + "answer": {}, + "preview": {}, + }, + "tags": [], + "reviews": {}, + }, + card_id=id, + card_templates=card_templates, + ) + + +@router.get("/decks/{deck_id}/cards/{card_id}", response_class=HTMLResponse) +async def edit_card_page( + deck_id: str, card_id: str, render=Depends(template("private/card.html")) +): + with shelve.open(database) as db: + deck = db["decks"].get(deck_id, {}) + if not deck: + raise HTTPException(status_code=404, detail="Deck not found") + + card_templates = db["templates"] + card = deck["cards"].get(card_id, {}) + if not card: + raise HTTPException(status_code=404, detail="Card not found") + + for template in card_templates.values(): + template["rendered_form"] = Template(template["form"]).render( + **card["data"] + ) + + return render( + navbar_title=deck["name"], + deck=deck, + deck_id=deck_id, + card=card, + card_id=card_id, + card_templates=card_templates, + ) + + +@router.post("/decks/{deck_id}/cards/{card_id}", response_class=RedirectResponse) +async def save_card_endpoint(deck_id: str, card_id: Optional[str], request: Request): + async with request.form() as form: + with shelve.open(database) as db: + deck = db["decks"].get(deck_id, {}) + deck["cards"][card_id] = { + **deck["cards"].get(card_id, {"reviews": {}}), + "data": { + "question": { + key[len("question.") :]: value + for key, value in form.items() + if key.startswith("question.") + }, + "answer": { + key[len("answer.") :]: value + for key, value in form.items() + if key.startswith("answer.") + }, + "preview": { + key[len("preview.") :]: value + for key, value in form.items() + if key.startswith("preview.") + }, + }, + "tags": [tag.strip() for tag in form["tags"].split(",") if tag.strip()], + "type": form["type"], + } + + return RedirectResponse( + request.url_for("cards_page", deck_id=deck_id), + status_code=status.HTTP_302_FOUND, + ) + + +@router.get( + "/htmx/components/decks/{deck_id}/cards/{card_id}/confirm-delete", + response_class=HTMLResponse, +) +async def card_confirm_delete_component( + deck_id: str, + card_id: str, + render=Depends(template("components/message-modal.html")), +): + with shelve.open(database) as db: + deck = db["decks"].get(deck_id, {}) + if not deck: + raise HTTPException(status_code=404, detail="Deck not found") + card = deck["cards"].get(card_id, {}) + if not card: + raise HTTPException(status_code=404, detail="Card not found") + card_templates = db["templates"] + return render( + title=f"Deleting card", + content=f"

Are you really sure you wanna delete this card?


" + + Template(card_templates[card["type"]]["preview"]).render(**card["data"]), + positive=f"Yes, delete it", + negative=f"No, don't delete", + delete_endpoint="delete_card_endpoint", + endpoint_params={"deck_id": deck_id, "card_id": card_id}, + ) + + +@router.get("/decks/{deck_id}/cards/{card_id}/delete", response_class=RedirectResponse) +async def delete_card_endpoint( + request: Request, + deck_id: str, + card_id: str, +): + with shelve.open(database) as db: + if deck_id not in db["decks"]: + raise HTTPException(status_code=404, detail="Deck not found") + if card_id not in db["decks"][deck_id]["cards"]: + raise HTTPException(status_code=404, detail="Card not found") + del db["decks"][deck_id]["cards"][card_id] + + return RedirectResponse( + request.url_for("cards_page", deck_id=deck_id), + status_code=status.HTTP_302_FOUND, + ) diff --git a/flashcards_htmx/api/components.py b/flashcards_htmx/api/components.py deleted file mode 100644 index 7b67126..0000000 --- a/flashcards_htmx/api/components.py +++ /dev/null @@ -1,139 +0,0 @@ -from random import randint -from pathlib import Path -import datetime -import shelve - -from jinja2 import Template -import starlette.status as status -from fastapi import APIRouter, Request, Depends, HTTPException -from fastapi.responses import HTMLResponse, RedirectResponse -from fastapi.templating import Jinja2Templates - -from flashcards_htmx.app import template, database - - -templates = Jinja2Templates(directory=Path(__file__).parent / "templates") -router = APIRouter(prefix="/htmx/components") - - -@router.get("/decks", response_class=HTMLResponse) -async def decks_component(render=Depends(template("responses/decks.html"))): - with shelve.open(database) as db: - return render(decks=db["decks"]) - - -@router.get("/decks/search_filters", response_class=HTMLResponse) -async def decks_search_component( - render=Depends(template("components/filter-modal.html")), -): - return render( - title=f"decks", content=f"Content here", positive=f"Search", negative=f"Cancel" - ) - - -@router.get("/decks/{deck_id}/cards", response_class=HTMLResponse) -async def cards_component( - deck_id: str, render=Depends(template("responses/cards.html")) -): - with shelve.open(database) as db: - deck = db["decks"].get(deck_id, {}) - if not deck: - raise HTTPException(status_code=404, detail="Deck not found") - - card_templates = db["templates"] - for card in deck["cards"].values(): - card["rendered_preview"] = Template(card_templates[card["type"]]["preview"]).render(**card["data"]) - return render(deck=deck, deck_id=deck_id) - - -@router.get("/decks/{deck_id}/study", response_class=HTMLResponse) -async def study_component( - deck_id: str, render=Depends(template("responses/study.html")) -): - with shelve.open(database) as db: - deck = db["decks"].get(deck_id, {}) - if not deck: - raise HTTPException(status_code=404, detail="Deck not found") - - if not len(deck["cards"]): - return render(card=None, deck_id=deck_id) - - # TODO actually get the card to study from the deck - card_id = randint(1, len(deck["cards"])) - # if card_id == "1": - # return render(error="Test Error") - - card = deck["cards"].get(str(card_id), {}) - if not card: - raise HTTPException(status_code=404, detail="Card not found") - - card["rendered_question"] = Template(db["templates"][card["type"]]["question"]).render(**card["data"]["question"]) - card["rendered_answer"] = Template(db["templates"][card["type"]]["answer"]).render(**card["data"]["answer"]) - - return render(deck=deck, deck_id=deck_id, card=deck["cards"][str(card_id)], card_id=str(card_id)) - - -@router.post( - "/decks/{deck_id}/study/{card_id}/{result}", response_class=RedirectResponse -) -async def save_review_component( - deck_id: str, card_id: str, result: str, request: Request -): - with shelve.open(database) as db: - deck = db["decks"].get(deck_id, {}) - if not deck: - raise HTTPException(status_code=404, detail="Deck not found") - - deck["cards"][card_id]["reviews"][len(deck["cards"][card_id]["reviews"])] = { - "date": datetime.datetime.utcnow().isoformat(), - "result": result, - } - return RedirectResponse( - request.url_for("study_component", deck_id=deck_id), - status_code=status.HTTP_302_FOUND, - ) - - -@router.get("/decks/{deck_id}/confirm-delete", response_class=HTMLResponse) -async def deck_confirm_delete_component( - deck_id: str, render=Depends(template("components/message-modal.html")) -): - with shelve.open(database) as db: - deck = db["decks"].get(deck_id, {}) - if not deck: - raise HTTPException(status_code=404, detail="Deck not found") - - return render( - title=f"Deleting deck", - content=f"Are you really sure you wanna delete the deck '{deck['name']}'? It contains {len(deck['cards'])} cards.", - positive=f"Yes, delete {deck['name']}", - negative=f"No, don't delete", - delete_endpoint="delete_deck_endpoint", - endpoint_params={"deck_id": deck_id}, - ) - - -@router.get( - "/decks/{deck_id}/cards/{card_id}/confirm-delete", response_class=HTMLResponse -) -async def card_confirm_delete_component( - deck_id: str, - card_id: str, - render=Depends(template("components/message-modal.html")), -): - with shelve.open(database) as db: - deck = db["decks"].get(deck_id, {}) - if not deck: - raise HTTPException(status_code=404, detail="Deck not found") - card = deck["cards"].get(card_id, {}) - if not card: - raise HTTPException(status_code=404, detail="Card not found") - card_templates = db["templates"] - return render( - title=f"Deleting card", - content=f"

Are you really sure you wanna delete this card?


" + Template(card_templates[card["type"]]["preview"]).render(**card["data"]), - positive=f"Yes, delete it", - negative=f"No, don't delete", - delete_endpoint="delete_card_endpoint", - endpoint_params={"deck_id": deck_id, "card_id": card_id}, - ) diff --git a/flashcards_htmx/api/decks.py b/flashcards_htmx/api/decks.py new file mode 100644 index 0000000..0d6c570 --- /dev/null +++ b/flashcards_htmx/api/decks.py @@ -0,0 +1,161 @@ +from typing import Optional +from pathlib import Path +import shelve +import json +from copy import deepcopy + +from jinja2 import Template +import starlette.status as status +from fastapi import APIRouter, Request, Depends, HTTPException, UploadFile, File +from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse +from fastapi.templating import Jinja2Templates + +from flashcards_htmx.app import template, database + + +templates = Jinja2Templates(directory=Path(__file__).parent / "templates") +router = APIRouter() + + +@router.get("/home", response_class=HTMLResponse) +async def home_page(request: Request, render=Depends(template("private/home.html"))): + with shelve.open(database) as db: + print([len(deck["cards"]) for deck in db["decks"].values()]) + return render( + navbar_title="Home", + searchable=True, + new_item_endpoint=request.url_for("create_deck_page"), + upload_item_endpoint=request.url_for("import_deck_page"), + new_item_text="New Deck...", + ) + + +@router.get("/htmx/components/decks", response_class=HTMLResponse) +async def decks_component(render=Depends(template("responses/decks.html"))): + with shelve.open(database) as db: + return render(decks=db["decks"]) + + +@router.get("/htmx/components/decks/search_filters", response_class=HTMLResponse) +async def decks_search_component( + render=Depends(template("components/filter-modal.html")), +): + return render( + title=f"decks", content=f"Content here", positive=f"Search", negative=f"Cancel" + ) + + +@router.get("/decks/new", response_class=HTMLResponse) +async def create_deck_page(render=Depends(template("private/deck.html"))): + with shelve.open(database) as db: + id = len(db["decks"]) + 1 + return render( + navbar_title="New Deck", + deck={"name": "", "description": "", "algorithm": "Random"}, + deck_id=id, + ) + + +@router.get("/decks/import", response_class=HTMLResponse) +async def import_deck_page(render=Depends(template("private/import.html"))): + return render() + + +@router.post("/decks/import", response_class=RedirectResponse) +async def import_deck_endpoint(file: UploadFile): + try: + contents = await file.read() + deck = json.loads(contents) + with shelve.open(database) as db: + db["decks"][str(len(db["decks"]) + 1)] = deck + except Exception: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="There was an error importing the deck", + ) + finally: + await file.close() + return RedirectResponse( + router.url_path_for("home_page"), status_code=status.HTTP_302_FOUND + ) + + +@router.get("/decks/{deck_id}/export", response_class=FileResponse) +async def export_deck_endpoint(request: Request): + with shelve.open(database) as db: + deck = deepcopy(db["decks"].get(request.path_params["deck_id"], {})) + if not deck: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + deck["cards"] = { + card_id: { + key: val for key, val in card.items() if not key.startswith("rendered_") + } + for card_id, card in deck["cards"].items() + } + path = Path(__file__).parent.parent / f"tmp/{deck['name']}.json" + + with open(path, "w") as file: + json.dump(deck, file, indent=4) + return FileResponse( + path, media_type="application/octet-stream", filename=f"{deck['name']}.json" + ) + + +@router.get("/decks/{deck_id}", response_class=HTMLResponse) +async def edit_deck_page(deck_id: str, render=Depends(template("private/deck.html"))): + with shelve.open(database) as db: + deck = db["decks"].get(deck_id, {}) + if not deck: + raise HTTPException(status_code=404, detail="Deck not found") + return render(navbar_title=deck["name"], deck=deck, deck_id=deck_id) + + +@router.post("/decks/{deck_id}", response_class=RedirectResponse) +async def save_deck_endpoint(deck_id: str, request: Request): + async with request.form() as form: + with shelve.open(database) as db: + if not "decks" in db: + db["decks"] = {} + db["decks"][deck_id] = { + **db["decks"].get(deck_id, {"cards": {}}), + "name": form["name"], + "description": form["description"], + "algorithm": form["algorithm"], + } + return RedirectResponse( + request.url_for("home_page"), status_code=status.HTTP_302_FOUND + ) + + +@router.get( + "/htmx/components/decks/{deck_id}/confirm-delete", response_class=HTMLResponse +) +async def deck_confirm_delete_component( + deck_id: str, render=Depends(template("components/message-modal.html")) +): + with shelve.open(database) as db: + deck = db["decks"].get(deck_id, {}) + if not deck: + raise HTTPException(status_code=404, detail="Deck not found") + + return render( + title=f"Deleting deck", + content=f"Are you really sure you wanna delete the deck '{deck['name']}'? It contains {len(deck['cards'])} cards.", + positive=f"Yes, delete {deck['name']}", + negative=f"No, don't delete", + delete_endpoint="delete_deck_endpoint", + endpoint_params={"deck_id": deck_id}, + ) + + +@router.get("/decks/{deck_id}/delete", response_class=RedirectResponse) +async def delete_deck_endpoint(request: Request, deck_id: str): + with shelve.open(database) as db: + if deck_id not in db["decks"]: + raise HTTPException(status_code=404, detail="Deck not found") + del db["decks"][deck_id] + + return RedirectResponse( + request.url_for("home_page"), status_code=status.HTTP_302_FOUND + ) diff --git a/flashcards_htmx/api/private.py b/flashcards_htmx/api/private.py index aa4e719..f224f54 100644 --- a/flashcards_htmx/api/private.py +++ b/flashcards_htmx/api/private.py @@ -1,13 +1,11 @@ -from typing import Optional from pathlib import Path +from random import randint import shelve -import json -from copy import deepcopy from jinja2 import Template import starlette.status as status -from fastapi import APIRouter, Request, Depends, HTTPException, UploadFile, File -from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse +from fastapi import APIRouter, Request, Depends, HTTPException +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from flashcards_htmx.app import template, database @@ -17,23 +15,9 @@ router = APIRouter() -@router.get("/home", response_class=HTMLResponse) -async def home_page(request: Request, render=Depends(template("private/home.html"))): - with shelve.open(database) as db: - print([len(deck["cards"]) for deck in db["decks"].values()]) - return render( - navbar_title="Home", - searchable=True, - new_item_endpoint=request.url_for("create_deck_page"), - upload_item_endpoint=request.url_for("import_deck_page"), - new_item_text="New Deck...", - ) - - @router.get("/profile", response_class=HTMLResponse) async def profile_page(render=Depends(template("private/profile.html"))): - # TODO actually get the profile information - return render(navbar_title="My name") + return render(navbar_title="Settings") @router.get("/study/{deck_id}", response_class=HTMLResponse) @@ -45,218 +29,65 @@ async def study_page(deck_id: str, render=Depends(template("private/study.html") return render(navbar_title=deck["name"], deck=deck, deck_id=deck_id) -@router.get("/decks/new", response_class=HTMLResponse) -async def create_deck_page(render=Depends(template("private/deck.html"))): - with shelve.open(database) as db: - id = len(db["decks"]) + 1 - return render( - navbar_title="New Deck", - deck={"name": "", "description": "", "algorithm": "Random"}, - deck_id= id - ) - - -@router.get("/decks/import", response_class=HTMLResponse) -async def import_deck_page(render=Depends(template("private/import.html"))): - return render() - - -@router.post('/decks/import', response_class=RedirectResponse) -async def import_deck_endpoint(file: UploadFile): - try: - contents = await file.read() - deck = json.loads(contents) - with shelve.open(database) as db: - db["decks"][str(len(db["decks"])+1)] = deck - except Exception: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail='There was an error importing the deck', - ) - finally: - await file.close() - return RedirectResponse(router.url_path_for("home_page"), status_code=status.HTTP_302_FOUND) - - -@router.get("/decks/{deck_id}/export", response_class=FileResponse) -async def export_deck_endpoint(request: Request): - with shelve.open(database) as db: - deck = deepcopy(db["decks"].get(request.path_params["deck_id"], {})) - if not deck: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - - # Remove reviews and renderings - deck["cards"] = { - card_id: {key: val for key, val in card.items() if key != "reviews" and not key.startswith("rendered_")} - for card_id, card in deck["cards"].items() - } - - - path = Path(__file__).parent.parent / f"tmp/{deck['name']}.json" - - with open(path, 'w') as file: - json.dump(deck, file, indent=4) - return FileResponse(path, media_type='application/octet-stream', filename=f"{deck['name']}.json") - - -@router.get("/decks/{deck_id}", response_class=HTMLResponse) -async def edit_deck_page(deck_id: str, render=Depends(template("private/deck.html"))): +@router.get("/htmx/components/decks/{deck_id}/study", response_class=HTMLResponse) +async def study_component( + deck_id: str, render=Depends(template("responses/study.html")) +): with shelve.open(database) as db: deck = db["decks"].get(deck_id, {}) if not deck: raise HTTPException(status_code=404, detail="Deck not found") - return render(navbar_title=deck["name"], deck=deck, deck_id=deck_id) + if not len(deck["cards"]): + return render(card=None, deck_id=deck_id) -@router.post("/decks/{deck_id}", response_class=RedirectResponse) -async def save_deck_endpoint(deck_id: str, request: Request): - async with request.form() as form: - with shelve.open(database) as db: - if not "decks" in db: - db["decks"] = {} - db["decks"][deck_id] = { - **db["decks"].get(deck_id, {"cards": {}}), - "name": form["name"], - "description": form["description"], - "algorithm": form["algorithm"] - } - return RedirectResponse( - request.url_for("home_page"), status_code=status.HTTP_302_FOUND - ) - - -@router.get("/decks/{deck_id}/delete", response_class=RedirectResponse) -async def delete_deck_endpoint( - request: Request, - deck_id: str -): - with shelve.open(database) as db: - if deck_id not in db["decks"]: - raise HTTPException(status_code=404, detail="Deck not found") - del db["decks"][deck_id] - - return RedirectResponse(request.url_for("home_page"), status_code=status.HTTP_302_FOUND) - + # TODO actually get the card to study from the deck + card_id = randint(1, len(deck["cards"])) + # if card_id == "1": + # return render(error="Test Error") -@router.get("/decks/{deck_id}/cards", response_class=HTMLResponse) -async def cards_page( - deck_id: str, request: Request, render=Depends(template("private/cards.html")) -): - with shelve.open(database) as db: - deck = db["decks"].get(deck_id, {}) - if not deck: - raise HTTPException(status_code=404, detail="Deck not found") - return render( - navbar_title=deck["name"], - deck=deck, - deck_id=deck_id, - searchable=True, - new_item_endpoint=request.url_for("create_card_page", deck_id=deck_id), - new_item_text="New Card...", - ) + card = deck["cards"].get(str(card_id), {}) + if not card: + raise HTTPException(status_code=404, detail="Card not found") + card["rendered_question"] = Template( + db["templates"][card["type"]]["question"] + ).render(**card["data"]["question"]) + card["rendered_answer"] = Template( + db["templates"][card["type"]]["answer"] + ).render(**card["data"]["answer"]) -@router.get("/decks/{deck_id}/cards/new", response_class=HTMLResponse) -async def create_card_page(deck_id: str, render=Depends(template("private/card.html"))): - with shelve.open(database) as db: - deck = db["decks"].get(deck_id, {}) - if not deck: - raise HTTPException(status_code=404, detail="Deck not found") - - id = len(db["decks"].get(deck_id, {}).get("cards", {})) + 1 - card_templates = db["templates"] - for template in card_templates.values(): - template["rendered_form"] = Template(template["form"]).render(question={}, answer={}, preview={}) return render( - navbar_title=deck["name"], deck=deck, deck_id=deck_id, - card={ - "id": id, - "data": { - "question": {}, - "answer": {}, - "preview": {}, - }, - "tags": [], - "type": "Q/A", - "reviews": {}, - }, - card_id=id, - card_templates=card_templates + card=deck["cards"][str(card_id)], + card_id=str(card_id), ) -@router.get("/decks/{deck_id}/cards/{card_id}", response_class=HTMLResponse) -async def edit_card_page( - deck_id: str, card_id: str, render=Depends(template("private/card.html")) +@router.post( + "/htmx/components/decks/{deck_id}/study/{card_id}/{result}", + response_class=RedirectResponse, +) +async def save_review_component( + deck_id: str, card_id: str, result: str, request: Request ): with shelve.open(database) as db: deck = db["decks"].get(deck_id, {}) if not deck: raise HTTPException(status_code=404, detail="Deck not found") - - card_templates = db["templates"] - card = deck["cards"].get(card_id, {}) - if not card: - raise HTTPException(status_code=404, detail="Card not found") - - for template in card_templates.values(): - template["rendered_form"] = Template(template["form"]).render(**card["data"]) - - return render( - navbar_title=deck["name"], - deck=deck, - deck_id=deck_id, - card=card, - card_id=card_id, - card_templates=card_templates - ) - - -@router.post("/decks/{deck_id}/cards/{card_id}", response_class=RedirectResponse) -async def save_card_endpoint(deck_id: str, card_id: Optional[str], request: Request): - async with request.form() as form: - with shelve.open(database) as db: - deck = db["decks"].get(deck_id, {}) - deck["cards"][card_id] = { - **deck["cards"].get(card_id, {"reviews": {}}), - "data": { - "question": { - key[len("question."):] : value for key, value in form.items() if key.startswith("question.") - }, - "answer": { - key[len("answer."):] : value for key, value in form.items() if key.startswith("answer.") - }, - "preview": { - key[len("preview."):]: value for key, value in form.items() if key.startswith("preview.") - } - }, - "tags": [tag.strip() for tag in form["tags"].split(",") if tag.strip()], - "type": form["type"], - } + # TODO save meaningful review data + # deck["cards"][card_id]["reviews"][len(deck["cards"][card_id]["reviews"])] = { + # "date": datetime.datetime.utcnow().isoformat(), + # "result": result, + # } return RedirectResponse( - request.url_for("cards_page", deck_id=deck_id), status_code=status.HTTP_302_FOUND + request.url_for("study_component", deck_id=deck_id), + status_code=status.HTTP_302_FOUND, ) -@router.get("/decks/{deck_id}/cards/{card_id}/delete", response_class=RedirectResponse) -async def delete_card_endpoint( - request: Request, - deck_id: str, - card_id: str, -): - with shelve.open(database) as db: - if deck_id not in db["decks"]: - raise HTTPException(status_code=404, detail="Deck not found") - if card_id not in db["decks"][deck_id]["cards"]: - raise HTTPException(status_code=404, detail="Card not found") - del db["decks"][deck_id]["cards"][card_id] - - return RedirectResponse(request.url_for("cards_page", deck_id=deck_id), status_code=status.HTTP_302_FOUND) - - @router.post("/logout", response_class=RedirectResponse) async def logout_page(request: Request): return RedirectResponse( diff --git a/flashcards_htmx/api/templates.py b/flashcards_htmx/api/templates.py new file mode 100644 index 0000000..768827c --- /dev/null +++ b/flashcards_htmx/api/templates.py @@ -0,0 +1,110 @@ +from pathlib import Path +import shelve + +from jinja2 import Template +import starlette.status as status +from fastapi import APIRouter, Request, Depends, HTTPException +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates + +from flashcards_htmx.app import template, database + + +templates = Jinja2Templates(directory=Path(__file__).parent / "templates") +router = APIRouter() + + +@router.get("/templates", response_class=HTMLResponse) +async def templates_page( + request: Request, render=Depends(template("private/templates.html")) +): + return render( + navbar_title="Templates", + searchable=True, + new_item_endpoint=request.url_for("create_template_page"), + new_item_text="New Template...", + ) + + +@router.get("/htmx/components/templates", response_class=HTMLResponse) +async def templates_component(render=Depends(template("responses/templates.html"))): + with shelve.open(database) as db: + return render(templates=db["templates"]) + + +@router.get("/templates/new", response_class=HTMLResponse) +async def create_template_page(render=Depends(template("private/template.html"))): + with shelve.open(database) as db: + template_id = str(len(db["templates"]) + 1) + return render( + navbar_title="New Template", + template_id=template_id, + template={ + "name": "Q/A", + "question": "{{ word }}", + "answer": "{{ word }}", + "preview": "{{ question }} -> {{ answer }}", + "form": "", + }, + ) + + +@router.get("/templates/{template_id}", response_class=HTMLResponse) +async def edit_template_page( + template_id: str, render=Depends(template("private/template.html")) +): + with shelve.open(database) as db: + template = db["templates"].get(template_id, {}) + if not template: + raise HTTPException(status_code=404, detail="Template not found") + return render( + navbar_title=template_id, + template_id=template_id, + template=template, + ) + + +@router.post("/template/{template_id}", response_class=RedirectResponse) +async def save_template_endpoint(template_id: str, request: Request): + async with request.form() as form: + with shelve.open(database) as db: + db["templates"][template_id] = { + **db["templates"][template_id], + "name": form["name"], + } + return RedirectResponse( + request.url_for("templates_page"), status_code=status.HTTP_302_FOUND + ) + + +@router.get( + "/htmx/components/template/{template_id}/confirm-delete", response_class=HTMLResponse +) +async def template_confirm_delete_component( + template_id: str, render=Depends(template("components/message-modal.html")) +): + with shelve.open(database) as db: + deck = db["templates"].get(template_id, {}) + if not deck: + raise HTTPException(status_code=404, detail="Template not found") + + return render( + title=f"Deleting template", + content=f"Are you really sure you wanna delete the template '{template_id}'?", + positive=f"Yes, delete {template_id}", + negative=f"No, don't delete", + delete_endpoint="delete_template_endpoint", + endpoint_params={"template_id": template_id}, + ) + + +@router.get("/templates/{template_id}/delete", response_class=RedirectResponse) +async def delete_deck_endpoint(request: Request, template_id: str): + with shelve.open(database) as db: + if template_id not in db["templates"]: + raise HTTPException(status_code=404, detail="Template not found") + del db["templates"][template_id] + + return RedirectResponse( + request.url_for("templates_page"), status_code=status.HTTP_302_FOUND + ) diff --git a/flashcards_htmx/app.py b/flashcards_htmx/app.py index ef600ad..a1f219c 100644 --- a/flashcards_htmx/app.py +++ b/flashcards_htmx/app.py @@ -22,12 +22,20 @@ { "templates": { - "Q/A": { - "question": "{{ word }}" - "answer": "{{ word }}" - "preview": "{{ question }} -> {{ answer }}" - "form": "" - } + "1": { + "name": "Q/A", + "description": "Simple template with a question and an answer.", + "question": "{{ word }}", + "answer": "{{ word }}", + "preview": "{{ question.word }} -> {{ answer.word }}", + "form": ''' + + + + + + ''', + } } "decks": { "0": { @@ -72,20 +80,25 @@ with shelve.open(database) as db: db.setdefault("decks", {}) - db.setdefault("templates", { - "Q/A": { - "question": "{{ word }}", - "answer": "{{ word }}", - "preview": "{{ question.word }} -> {{ answer.word }}", - "form": """ + db.setdefault( + "templates", + { + "1": { + "name": "Q/A", + "description": "Simple template with a question and an answer.", + "question": "{{ word }}", + "answer": "{{ word }}", + "preview": "{{ question.word }} -> {{ answer.word }}", + "form": """ - """ - } - }) + """, + } + }, + ) def get_jinja2(): @@ -131,16 +144,20 @@ def render(*args, **kwargs): @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request: Request, exc: HTTPException): template = get_jinja2().get_template("public/http_error.html") - response = template.render(request=request, code=exc.status_code, message=exc.detail) + response = template.render( + request=request, code=exc.status_code, message=exc.detail + ) return HTMLResponse(response, status_code=exc.status_code) from flashcards_htmx.api.public import router as public_router # noqa: F401, E402 from flashcards_htmx.api.private import router as private_router # noqa: F401, E402 -from flashcards_htmx.api.components import ( - router as private_components, -) # noqa: F401, E402 +from flashcards_htmx.api.decks import router as decks_router # noqa: F401, E402 +from flashcards_htmx.api.cards import router as cards_router # noqa: F401, E402 +from flashcards_htmx.api.templates import router as templates_router # noqa: F401, E402 app.include_router(public_router) app.include_router(private_router) -app.include_router(private_components) +app.include_router(decks_router) +app.include_router(cards_router) +app.include_router(templates_router) diff --git a/flashcards_htmx/static/css/private-base.css b/flashcards_htmx/static/css/private-base.css index d6ec0a7..ab1d2e4 100644 --- a/flashcards_htmx/static/css/private-base.css +++ b/flashcards_htmx/static/css/private-base.css @@ -319,6 +319,7 @@ section#double-card { display: flex; flex-direction: column; justify-content: space-between; + gap: 1rem; } section#double-card .main-card { diff --git a/flashcards_htmx/static/css/public-base.css b/flashcards_htmx/static/css/public-base.css index a900eaa..b8cade7 100644 --- a/flashcards_htmx/static/css/public-base.css +++ b/flashcards_htmx/static/css/public-base.css @@ -116,6 +116,12 @@ input[type=submit]:hover { border: 1px solid var(--white); } +.comment { + color: var(--gray); + font-size: 0.8rem; + font-style: italic; +} + /*----------------------------------------------------------------------------*/ /* Wrapper Styles /*----------------------------------------------------------------------------*/ diff --git a/flashcards_htmx/templates/components/template.html b/flashcards_htmx/templates/components/template.html new file mode 100644 index 0000000..7569e70 --- /dev/null +++ b/flashcards_htmx/templates/components/template.html @@ -0,0 +1,9 @@ +
+ +
\ No newline at end of file diff --git a/flashcards_htmx/templates/private/deck.html b/flashcards_htmx/templates/private/deck.html index a13f3ae..437e780 100644 --- a/flashcards_htmx/templates/private/deck.html +++ b/flashcards_htmx/templates/private/deck.html @@ -31,9 +31,6 @@ {% endif %} - -
- diff --git a/flashcards_htmx/templates/private/profile.html b/flashcards_htmx/templates/private/profile.html index a120cb5..623acf9 100644 --- a/flashcards_htmx/templates/private/profile.html +++ b/flashcards_htmx/templates/private/profile.html @@ -7,17 +7,16 @@ {% block page %}
-

My Profile

+ +

Templates

+ Manage Templates -

...

-

... all the stats ...

-

...

-
- Logout - Change Password - Delete Account -
+

Account

+ Logout + Change Password + Delete Account +
{% endblock %} \ No newline at end of file diff --git a/flashcards_htmx/templates/private/template.html b/flashcards_htmx/templates/private/template.html new file mode 100644 index 0000000..45ee371 --- /dev/null +++ b/flashcards_htmx/templates/private/template.html @@ -0,0 +1,31 @@ +{% extends "private/base.html" %} + + +{% block css%} + + +{% endblock %} + + +{% block page %} +
+
+ +
+ +
+ + Cancel + {% if deck_id %} + + {% endif %} +
+
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/flashcards_htmx/templates/private/templates.html b/flashcards_htmx/templates/private/templates.html new file mode 100644 index 0000000..0be03be --- /dev/null +++ b/flashcards_htmx/templates/private/templates.html @@ -0,0 +1,11 @@ +{% extends "private/base.html" %} + + +{% block css%} +{% endblock %} + +{% block page %} +
+ {% include "components/loading.html" %} +
+{% endblock %} \ No newline at end of file diff --git a/flashcards_htmx/templates/responses/templates.html b/flashcards_htmx/templates/responses/templates.html new file mode 100644 index 0000000..34f9b53 --- /dev/null +++ b/flashcards_htmx/templates/responses/templates.html @@ -0,0 +1,5 @@ +
+ {% for id, template in templates.items() %} + {% include "components/template.html" %} + {% endfor %} +
diff --git a/flashcards_htmx/tmp/11.json b/flashcards_htmx/tmp/11.json new file mode 100644 index 0000000..d90f64d --- /dev/null +++ b/flashcards_htmx/tmp/11.json @@ -0,0 +1,54 @@ +{ + "cards": { + "1": { + "reviews": {}, + "data": { + "question": { + "word": "a" + }, + "answer": { + "word": "b" + }, + "preview": {} + }, + "tags": [ + "test" + ], + "type": "1" + }, + "2": { + "reviews": {}, + "data": { + "question": { + "word": "b" + }, + "answer": { + "word": "c" + }, + "preview": {} + }, + "tags": [ + "t", + "tt" + ], + "type": "1" + }, + "4": { + "reviews": {}, + "data": { + "question": { + "word": "d" + }, + "answer": { + "word": "e" + }, + "preview": {} + }, + "tags": [], + "type": "1" + } + }, + "name": "11", + "description": "dwqfeeas", + "algorithm": "Random" +} \ No newline at end of file