diff --git a/backend/app.py b/backend/app.py index 572e394..d4545d2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Literal from fastapi import FastAPI, HTTPException, status, Depends from pydantic import BaseModel @@ -7,6 +8,7 @@ from .db import MongoModel, PyObjectId, client from .settings import settings db = client["party"] +meta = client["party-meta"] description = """ Party party \U0001F973 @@ -44,25 +46,44 @@ Coming = Literal["yes", "no", "maybe"] GrammaticalGender = Literal["m", "f", "d"] -class Guest(MongoModel): +class HTTPError(BaseModel): + detail: str + + +# error_responses = {status.HTTP_401_UNAUTHORIZED: {"model": HTTPError}} +def error_responses(*args): + return {arg: {"model": HTTPError} for arg in args} + + +class Guest(BaseModel): token: str name: str coming: Coming | None grammatical_gender: GrammaticalGender + extra: dict[str, str] -async def find_guest(party: str, token: str) -> Guest: +class DBGuest(Guest, MongoModel): + pass + + +async def find_guest(party: str, token: str) -> DBGuest: guest = await db[party].find_one({"token": token}) if not guest: raise HTTPException(status.HTTP_401_UNAUTHORIZED) - return Guest.parse_obj(guest) + return DBGuest.parse_obj(guest) # Guest methods -@app.get("/{party}/{token}/me", response_model=Guest, tags=["guests"]) -async def get_self(guest: Guest = Depends(find_guest)): +@app.get( + "/{party}/{token}/me", + response_model=Guest, + responses=error_responses(401), + tags=["guests"], +) +async def get_self(guest: DBGuest = Depends(find_guest)): return guest @@ -70,13 +91,19 @@ class GuestUpdate(BaseModel): coming: Coming -@app.patch("/{party}/{token}/me", tags=["guests"]) +@app.patch( + "/{party}/{token}/me", + response_model=Guest, + responses=error_responses(401), + tags=["guests"], +) async def update_self( - party: str, update: GuestUpdate, guest: Guest = Depends(find_guest) + party: str, update: GuestUpdate, guest: DBGuest = Depends(find_guest) ): - guest_dict = guest.dict() + guest_dict = guest.dict(exclude={"id"}) guest_dict.update(update.dict()) await db[party].replace_one({"_id": guest.id}, guest_dict) + return await db[party].find_one({"_id": guest.id}) class PartyStatus(BaseModel): @@ -84,7 +111,12 @@ class PartyStatus(BaseModel): maybe_coming: int -@app.get("/{party}/{token}/status", response_model=PartyStatus, tags=["guests"]) +@app.get( + "/{party}/{token}/status", + response_model=PartyStatus, + responses=error_responses(401), + tags=["guests"], +) async def get_party_status(party: str, _=Depends(find_guest)): definitely_coming = await db[party].count_documents({"coming": "yes"}) maybe_coming = await db[party].count_documents({"coming": "maybe"}) @@ -103,17 +135,32 @@ async def auth_admin(admin_token: str): raise HTTPException(status.HTTP_401_UNAUTHORIZED) -@app.get("/{admin_token}", response_model=list[str], tags=["admin"]) +class Party(MongoModel): + name: str + created: datetime + + +@app.get( + "/{admin_token}", + response_model=list[Party], + responses=error_responses(401), + tags=["admin"], +) async def list_parties(_=Depends(auth_admin)): - filter = {"name": {"$regex": r"^(?!system\.)"}} - return await db.list_collection_names(filter=filter) + return await meta["parties"].find().to_list(None) class PartyCreate(BaseModel): name: str -@app.post("/{admin_token}", status_code=status.HTTP_204_NO_CONTENT, tags=["admin"]) +@app.post( + "/{admin_token}", + response_model=Party, + status_code=status.HTTP_201_CREATED, + responses=error_responses(400, 401), + tags=["admin"], +) async def create_party(party: PartyCreate, _=Depends(auth_admin)): try: await db.create_collection(party.name) @@ -121,17 +168,37 @@ async def create_party(party: PartyCreate, _=Depends(auth_admin)): raise HTTPException( status.HTTP_400_BAD_REQUEST, f"Party {party.name!r} already exists" ) + inserted = await meta["parties"].insert_one( + { + "name": party.name, + "created": datetime.now(), + } + ) + return await meta["parties"].find_one({"_id": inserted.inserted_id}) @app.delete( - "/{admin_token}/{party}", status_code=status.HTTP_204_NO_CONTENT, tags=["admin"] + "/{admin_token}/{party}", + status_code=status.HTTP_204_NO_CONTENT, + responses=error_responses(401, 404), + tags=["admin"], ) async def delete_party(party: str, _=Depends(auth_admin)): + deleted = await meta["parties"].delete_one({"name": party}) + if deleted.deleted_count < 1: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) await db.drop_collection(party) -@app.get("/{admin_token}/{party}", response_model=list[Guest], tags=["admin"]) +@app.get( + "/{admin_token}/{party}", + response_model=list[DBGuest], + responses=error_responses(401, 404), + tags=["admin"], +) async def list_guests(party: str, _=Depends(auth_admin)): + if not await meta["parties"].find_one({"name": party}): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return await db[party].find().to_list(None) @@ -140,15 +207,20 @@ class GuestCreate(BaseModel): name: str coming: Coming | None grammatical_gender: GrammaticalGender + extra: dict[str, str] = dict() @app.post( "/{admin_token}/{party}", - response_model=Guest, + response_model=DBGuest, status_code=status.HTTP_201_CREATED, + responses=error_responses(400, 401, 404), tags=["admin"], ) async def create_new_guest(party: str, new_guest: GuestCreate, _=Depends(auth_admin)): + if not await meta["parties"].find_one({"name": party}): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + existing = await db[party].find_one({"token": new_guest.token}) if existing: raise HTTPException( @@ -165,12 +237,21 @@ class GuestModify(BaseModel): name: str | None coming: Coming | None grammatical_gender: GrammaticalGender | None + extra: dict[str, str] | None -@app.patch("/{admin_token}/{party}/{id}", response_model=Guest, tags=["admin"]) +@app.patch( + "/{admin_token}/{party}/{id}", + response_model=DBGuest, + responses=error_responses(401, 404), + tags=["admin"], +) async def modify_guest( party: str, id: PyObjectId, modified_guest: GuestModify, _=Depends(auth_admin) ): + if not await meta["parties"].find_one({"name": party}): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + existing = await db[party].find_one({"_id": id}) if not existing: raise HTTPException(status.HTTP_404_NOT_FOUND) @@ -184,6 +265,7 @@ async def modify_guest( @app.delete( "/{admin_token}/{party}/{id}", status_code=status.HTTP_204_NO_CONTENT, + responses=error_responses(401, 404), tags=["admin"], ) async def delete_guest(party: str, id: PyObjectId, _=Depends(auth_admin)):