diff --git a/api/advent22_api/core/depends.py b/api/advent22_api/core/depends.py index c9bf5f1..b97d6dc 100644 --- a/api/advent22_api/core/depends.py +++ b/api/advent22_api/core/depends.py @@ -5,76 +5,95 @@ from fastapi import Depends from PIL import Image, ImageFont from .advent_image import _XY, AdventImage +from .calendar_config import CalendarConfig, get_calendar_config from .config import Config, get_config -from .image_helpers import list_images_auto, load_image -from .sequence_helpers import Random, set_len, shuffle +from .helpers import Random, list_images_auto, load_image, set_len from .webdav import WebDAV -async def shuffle_solution( +async def get_days( + cal_cfg: CalendarConfig = Depends(get_calendar_config), +) -> list[int]: + """ + Alle Tage, für die es ein Türchen gibt + """ + + return sorted(set(door.day for door in cal_cfg.doors)) + + +async def get_parts( cfg: Config = Depends(get_config), -) -> str: + days: list[int] = Depends(get_days), +) -> dict[int, str]: """ - Lösung: Reihenfolge zufällig bestimmen + Lösung auf vorhandene Tage aufteilen """ - return "".join(await shuffle(cfg.puzzle.solution)) + solution_length = len(cfg.puzzle.solution) + num_days = len(days) + + rnd = await Random.get() + solution_days = [ + *rnd.shuffled(days * (solution_length // num_days)), + *rnd.sample(days, solution_length % num_days), + ] + + result: dict[int, str] = {} + for day, letter in zip(solution_days, cfg.puzzle.solution): + result[day] = result.get(day, "") + result[day] += letter + + return result -async def shuffle_images_auto( - images: list[str] = Depends(list_images_auto), -) -> list[str]: - """ - Bilder: Reihenfolge zufällig bestimmen - """ - - ls = set_len(images, 24) - return await shuffle(ls) - - -async def get_part( +async def get_day_part( day: int, - shuffled_solution: str = Depends(shuffle_solution), + parts: dict[int, str] = Depends(get_parts), ) -> str: """ Heute angezeigter Teil der Lösung """ - return shuffled_solution[day] + return parts[day] -async def get_random( - day: int, -) -> Random: +async def get_auto_image_names( + days: list[int] = Depends(get_days), + images: list[str] = Depends(list_images_auto), +) -> dict[int, str]: """ - Tagesabhängige Zufallszahlen + Bilder: Reihenfolge zufällig bestimmen """ - return await Random.get(day) + rnd = await Random.get() + ls = set_len(images, len(days)) + + return dict(zip(days, rnd.shuffled(ls))) -async def gen_auto_image( +async def gen_day_auto_image( day: int, - auto_images: list[str] = Depends(shuffle_images_auto), cfg: Config = Depends(get_config), - rnd: Random = Depends(get_random), - part: str = Depends(get_part), + auto_image_names: list[str] = Depends(get_auto_image_names), + day_part: str = Depends(get_day_part), ) -> Image.Image: """ Automatisch generiertes Bild erstellen """ # Datei existiert garantiert! - img = await load_image(auto_images[day]) + img = await load_image(auto_image_names[day]) image = await AdventImage.from_img(img) + rnd = await Random.get(day) + font = ImageFont.truetype( font=BytesIO(await WebDAV.read_bytes(f"files/{cfg.server.font}")), size=50, ) # Buchstaben verstecken - for letter in part: + for letter in day_part: await image.hide_text( xy=cast(_XY, tuple(rnd.choices(range(30, 470), k=2))), text=letter, @@ -84,12 +103,11 @@ async def gen_auto_image( return image.img -async def get_image( +async def get_day_image( day: int, - auto_images: list[str] = Depends(shuffle_images_auto), cfg: Config = Depends(get_config), - rnd: Random = Depends(get_random), - part: str = Depends(get_part), + auto_image_names: list[str] = Depends(get_auto_image_names), + day_part: str = Depends(get_day_part), ) -> Image.Image: """ Bild für einen Tag abrufen @@ -105,6 +123,6 @@ async def get_image( except RuntimeError: # Erstelle automatisch generiertes Bild - return await gen_auto_image( - day=day, auto_images=auto_images, cfg=cfg, rnd=rnd, part=part + return await gen_day_auto_image( + day=day, cfg=cfg, auto_image_names=auto_image_names, day_part=day_part ) diff --git a/api/advent22_api/core/image_helpers.py b/api/advent22_api/core/helpers.py similarity index 60% rename from api/advent22_api/core/image_helpers.py rename to api/advent22_api/core/helpers.py index 8c67b29..e92dec8 100644 --- a/api/advent22_api/core/image_helpers.py +++ b/api/advent22_api/core/helpers.py @@ -1,11 +1,35 @@ +import itertools +import random import re from io import BytesIO +from typing import Any, Self, Sequence, TypeVar from fastapi.responses import StreamingResponse from PIL import Image +from .config import get_config from .webdav import WebDAV +T = TypeVar("T") + + +class Random(random.Random): + @classmethod + async def get(cls, bonus_salt: Any = "") -> Self: + cfg = await get_config() + return cls(f"{cfg.puzzle.solution}{cfg.puzzle.random_seed}{bonus_salt}") + + def shuffled(self, population: Sequence[T]) -> Sequence[T]: + return self.sample(population, k=len(population)) + + +def set_len(seq: Sequence[T], len: int) -> Sequence[T]: + # `seq` unendlich wiederholen + infinite = itertools.cycle(seq) + + # Die ersten `length` einträge nehmen + return list(itertools.islice(infinite, len)) + async def list_images_auto() -> list[str]: """ diff --git a/api/advent22_api/core/sequence_helpers.py b/api/advent22_api/core/sequence_helpers.py deleted file mode 100644 index a4911d0..0000000 --- a/api/advent22_api/core/sequence_helpers.py +++ /dev/null @@ -1,28 +0,0 @@ -import itertools -import random -from typing import Any, Self, Sequence - -from .config import get_config - - -class Random(random.Random): - @classmethod - async def get(cls, bonus_salt: Any = "") -> Self: - cfg = await get_config() - return cls(f"{cfg.puzzle.solution}{cfg.puzzle.random_seed}{bonus_salt}") - - -async def shuffle(seq: Sequence, rnd: random.Random | None = None) -> list: - # Zufallsgenerator - rnd = rnd or await Random.get() - - # Elemente mischen - return rnd.sample(seq, len(seq)) - - -def set_len(seq: Sequence, length: int) -> list: - # `seq` unendlich wiederholen - infinite = itertools.cycle(seq) - - # Die ersten `length` einträge nehmen - return list(itertools.islice(infinite, length)) diff --git a/api/advent22_api/routers/__init__.py b/api/advent22_api/routers/__init__.py index 82ac838..c841cfd 100644 --- a/api/advent22_api/routers/__init__.py +++ b/api/advent22_api/routers/__init__.py @@ -1,10 +1,8 @@ from fastapi import APIRouter -from . import admin, days, general, user +from . import admin, images router = APIRouter(prefix="/api") router.include_router(admin.router) -router.include_router(days.router) -router.include_router(general.router) -router.include_router(user.router) +router.include_router(images.router) diff --git a/api/advent22_api/routers/_security.py b/api/advent22_api/routers/_security.py index 24d4686..1e548fe 100644 --- a/api/advent22_api/routers/_security.py +++ b/api/advent22_api/routers/_security.py @@ -50,14 +50,14 @@ async def user_visible_doors() -> int: return 0 -async def user_can_view_door( +async def user_can_view_day( day: int, ) -> bool: """ True iff das Türchen von Tag `day` user-sichtbar ist """ - if day < 0: + if day < 1: raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY) - return day < await user_visible_doors() + return day <= await user_visible_doors() diff --git a/api/advent22_api/routers/admin.py b/api/advent22_api/routers/admin.py index ddcdde3..dd90ce6 100644 --- a/api/advent22_api/routers/admin.py +++ b/api/advent22_api/routers/admin.py @@ -3,9 +3,9 @@ from datetime import date from fastapi import APIRouter, Depends from pydantic import BaseModel -from ..core.calendar_config import CalendarConfig, get_calendar_config +from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config from ..core.config import Config, get_config -from ..core.depends import shuffle_solution +from ..core.depends import get_parts from ..core.settings import SETTINGS from ._security import require_admin, user_is_admin @@ -22,7 +22,6 @@ async def is_admin( class ConfigModel(BaseModel): class __Puzzle(BaseModel): solution: str - shuffled: str begin: date end: date closing: date @@ -57,13 +56,12 @@ async def get_config_model( _: None = Depends(require_admin), cfg: Config = Depends(get_config), cal_cfg: CalendarConfig = Depends(get_calendar_config), - shuffled_solution: str = Depends(shuffle_solution), + parts: dict[int, str] = Depends(get_parts), ) -> ConfigModel: return ConfigModel.model_validate( { "puzzle": { "solution": cfg.puzzle.solution, - "shuffled": shuffled_solution, "begin": date.today(), # TODO "end": date.today(), # TODO "closing": date.today(), # TODO @@ -85,3 +83,49 @@ async def get_config_model( }, } ) + + +class DayPartModel(BaseModel): + day: int + part: str + + +@router.get("/day_parts") +async def get_day_parts( + _: None = Depends(require_admin), + parts: dict[int, str] = Depends(get_parts), +) -> list[DayPartModel]: + return [ + DayPartModel.model_validate({"day": day, "part": part}) + for day, part in sorted(parts.items()) + ] + + +@router.get("/doors") +async def get_doors( + _: None = Depends(require_admin), + cal_cfg: CalendarConfig = Depends(get_calendar_config), +) -> DoorsSaved: + """ + Türchen lesen + """ + + return cal_cfg.doors + + +@router.put("/doors") +async def put_doors( + doors: DoorsSaved, + _: None = Depends(require_admin), + cfg: Config = Depends(get_config), + cal_cfg: CalendarConfig = Depends(get_calendar_config), +) -> None: + """ + Türchen ändern + """ + + cal_cfg.doors = sorted( + doors, + key=lambda door: door.day, + ) + await cal_cfg.change(cfg) diff --git a/api/advent22_api/routers/days.py b/api/advent22_api/routers/days.py deleted file mode 100644 index ece8e87..0000000 --- a/api/advent22_api/routers/days.py +++ /dev/null @@ -1,71 +0,0 @@ -from datetime import date - -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.responses import StreamingResponse -from PIL import Image - -from ..core.config import get_config -from ..core.depends import get_image, get_part, shuffle_solution -from ..core.image_helpers import api_return_image -from ._security import user_can_view_door, user_is_admin, user_visible_doors - -router = APIRouter(prefix="/days", tags=["days"]) - - -@router.on_event("startup") -async def startup() -> None: - cfg = await get_config() - print(cfg.puzzle.solution) - - shuffled_solution = await shuffle_solution(cfg) - print(shuffled_solution) - - -@router.get("/date") -async def get_date() -> str: - """ - Aktuelles Server-Datum - """ - - return date.today().isoformat() - - -@router.get("/visible_days") -async def get_visible_days( - visible_doors: int = Depends(user_visible_doors), -) -> int: - """ - Sichtbare Türchen - """ - - return visible_doors - - -@router.get("/part/{day}") -async def get_part_for_day( - part: str = Depends(get_part), -) -> str: - """ - Heutiger Lösungsteil - """ - - return part - - -@router.get( - "/image/{day}", - response_class=StreamingResponse, -) -async def get_image_for_day( - image: Image.Image = Depends(get_image), - can_view: bool = Depends(user_can_view_door), - is_admin: bool = Depends(user_is_admin), -) -> StreamingResponse: - """ - Bild für einen Tag erstellen - """ - - if not (can_view or is_admin): - raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Wie unhöflich!!!") - - return await api_return_image(image) diff --git a/api/advent22_api/routers/general.py b/api/advent22_api/routers/general.py deleted file mode 100644 index 556698b..0000000 --- a/api/advent22_api/routers/general.py +++ /dev/null @@ -1,50 +0,0 @@ -from fastapi import APIRouter, Depends -from fastapi.responses import StreamingResponse - -from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config -from ..core.config import Config, get_config -from ..core.image_helpers import api_return_image, load_image - -router = APIRouter(prefix="/general", tags=["general"]) - - -@router.get( - "/background", - response_class=StreamingResponse, -) -async def get_image_for_day( - cal_cfg: CalendarConfig = Depends(get_calendar_config), -) -> StreamingResponse: - """ - Hintergrundbild laden - """ - - return await api_return_image(await load_image(f"files/{cal_cfg.background}")) - - -@router.get("/doors") -async def get_doors( - cal_cfg: CalendarConfig = Depends(get_calendar_config), -) -> DoorsSaved: - """ - Türchen lesen - """ - - return cal_cfg.doors - - -@router.put("/doors") -async def put_doors( - doors: DoorsSaved, - cfg: Config = Depends(get_config), - cal_cfg: CalendarConfig = Depends(get_calendar_config), -) -> None: - """ - Türchen setzen - """ - - cal_cfg.doors = sorted( - doors, - key=lambda door: door.day, - ) - await cal_cfg.change(cfg) diff --git a/api/advent22_api/routers/images.py b/api/advent22_api/routers/images.py new file mode 100644 index 0000000..3f01981 --- /dev/null +++ b/api/advent22_api/routers/images.py @@ -0,0 +1,50 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.responses import StreamingResponse +from PIL import Image + +from ..core.calendar_config import CalendarConfig, get_calendar_config +from ..core.config import get_config +from ..core.depends import get_day_image +from ..core.helpers import api_return_image, load_image +from ._security import user_can_view_day, user_is_admin + +router = APIRouter(prefix="/images", tags=["images"]) + + +@router.on_event("startup") +async def startup() -> None: + cfg = await get_config() + print(cfg.puzzle.solution) + + +@router.get( + "/background", + response_class=StreamingResponse, +) +async def get_background( + cal_cfg: CalendarConfig = Depends(get_calendar_config), +) -> StreamingResponse: + """ + Hintergrundbild laden + """ + + return await api_return_image(await load_image(f"files/{cal_cfg.background}")) + + +@router.get( + "/{day}", + response_class=StreamingResponse, +) +async def get_image_for_day( + image: Image.Image = Depends(get_day_image), + can_view: bool = Depends(user_can_view_day), + is_admin: bool = Depends(user_is_admin), +) -> StreamingResponse: + """ + Bild für einen Tag erstellen + """ + + if not (can_view or is_admin): + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Wie unhöflich!!!") + + return await api_return_image(image) diff --git a/api/advent22_api/routers/user.py b/api/advent22_api/routers/user.py deleted file mode 100644 index 46fa28b..0000000 --- a/api/advent22_api/routers/user.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import APIRouter, Depends - -from ._security import require_admin - -router = APIRouter(prefix="/user", tags=["user"]) - - -@router.get("/admin") -def check_admin( - _: None = Depends(require_admin), -) -> bool: - return True diff --git a/ui/src/components/Calendar.vue b/ui/src/components/Calendar.vue index 313d43a..2345beb 100644 --- a/ui/src/components/Calendar.vue +++ b/ui/src/components/Calendar.vue @@ -3,7 +3,7 @@
- + - +

Alle Türchen

- - - - - {{ index }} - + icon="fa-solid fa-door-open" + :text="`${day_part.day}`" + @click.left="door_click(day_part.day)" + />

Buchstaben-Zuordnung

- 1: A - 2: G - 3: F - 4: C - 5: I - 6: N - 7: B - + + {{ day_part.day }}: {{ day_part.part.split("").join(", ") }} +

Bilder-Zuordnung

+
1: images_auto/1.jpg 2: images_manual/1.jpg @@ -48,6 +50,7 @@ diff --git a/ui/src/components/calendar/CalendarDoor.vue b/ui/src/components/calendar/CalendarDoor.vue index cf4a803..b48d79e 100644 --- a/ui/src/components/calendar/CalendarDoor.vue +++ b/ui/src/components/calendar/CalendarDoor.vue @@ -25,7 +25,7 @@ export default class extends Vue { this.$emit("doorClick"); this.$advent22 - .api_get_blob(`days/image/${this.door.day}`) + .api_get_blob(`images/${this.door.day}`) .then((data) => this.$emit("doorSuccess", data)) .catch(([reason]) => { let msg = "Unbekannter Fehler, bitte wiederholen!"; diff --git a/ui/src/components/editor/DoorChooser.vue b/ui/src/components/editor/DoorChooser.vue index fc2c6d8..4c3e333 100644 --- a/ui/src/components/editor/DoorChooser.vue +++ b/ui/src/components/editor/DoorChooser.vue @@ -11,7 +11,7 @@
- +
- + -
- +
+ {{ door.day }}
@@ -46,6 +46,7 @@ import SVGRect from "../calendar/SVGRect.vue"; }) export default class extends Vue { public door!: Door; + public readonly MIN_DAY = Door.MIN_DAY; public day_str = ""; public editing = false; diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts new file mode 100644 index 0000000..ac6b6c1 --- /dev/null +++ b/ui/src/lib/api.ts @@ -0,0 +1,38 @@ +export interface ConfigModel { + puzzle: { + solution: string; + begin: string; + end: string; + closing: string; + seed: string; + }; + calendar: { + config_file: string; + background: string; + }; + image: { + size: number; + border: number; + fonts: { file: string; size: number }[]; + }; + webdav: { + url: string; + cache_ttl: number; + config_file: string; + }; +} + +export interface DayPartModel { + day: number; + part: string; +} + +export interface DoorSaved { + day: number; + x1: number; + y1: number; + x2: number; + y2: number; +} + +export type DoorsSaved = DoorSaved[]; diff --git a/ui/src/lib/door.ts b/ui/src/lib/door.ts index 5ddd45a..3e0d9b3 100644 --- a/ui/src/lib/door.ts +++ b/ui/src/lib/door.ts @@ -1,23 +1,16 @@ +import { DoorSaved } from "./api"; import { Rectangle } from "./rectangle"; import { Vector2D } from "./vector2d"; -export interface DoorSaved { - day: number; - x1: number; - y1: number; - x2: number; - y2: number; -} - -export type DoorsSaved = DoorSaved[]; - export class Door { - private _day = -1; + public static readonly MIN_DAY = 1; + + private _day = Door.MIN_DAY; public position: Rectangle; constructor(position: Rectangle); constructor(position: Rectangle, day: number); - constructor(position: Rectangle, day = -1) { + constructor(position: Rectangle, day = Door.MIN_DAY) { this.day = day; this.position = position; } @@ -31,9 +24,9 @@ export class Door { const result = Number(day); if (isNaN(result)) { - this._day = -1; + this._day = Door.MIN_DAY; } else { - this._day = Math.max(Math.floor(result), -1); + this._day = Math.max(Math.floor(result), Door.MIN_DAY); } }