diff --git a/db.py b/db.py index 70f8b33..7ecbab6 100644 --- a/db.py +++ b/db.py @@ -17,6 +17,7 @@ db = client[MONGO_DATABASE] eventsdb = db.events holidaysdb = db.holidays +event_reportsdb = db.event_reports try: # check if the holidays index exists @@ -25,5 +26,13 @@ holidaysdb.create_index( [("date", 1)], unique=True, name="one_holiday_on_day" ) + if "unique_event_code" not in eventsdb.index_information(): + eventsdb.create_index( + [("code", 1)], unique=True, name="unique_event_code" + ) + if "unique_event_id" not in event_reportsdb.index_information(): + event_reportsdb.create_index( + [("event_id", 1)], unique=True, name="unique_event_id" + ) except Exception: pass diff --git a/models.py b/models.py index feaa75c..182bcca 100644 --- a/models.py +++ b/models.py @@ -5,6 +5,7 @@ BaseModel, ConfigDict, Field, + HttpUrl, ValidationInfo, field_validator, ) @@ -17,15 +18,33 @@ Event_Mode, Event_Status, HttpUrlString, + PrizesType, PyObjectId, - event_desc_type, - event_name_type, - event_othr_type, event_popu_type, + long_str_type, + medium_str_type, + short_str_type, timezone, + very_short_str_type, ) +class EventReport(BaseModel): + eventid: str + summary: medium_str_type + attendance: event_popu_type + prizes: List[PrizesType] = [] + prizes_breakdown: long_str_type + winners: long_str_type + photos_link: HttpUrlString + feedback_cc: medium_str_type + feedback_college: medium_str_type + submitted_by: str + submitted_time: datetime = Field( + default_factory=lambda: datetime.now(timezone), frozen=True + ) + + class Event(BaseModel): id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") code: str | None = None @@ -33,9 +52,9 @@ class Event(BaseModel): collabclubs: List[str] = [] studentBodyEvent: bool = False - name: event_name_type + name: very_short_str_type - description: event_desc_type | None = "No description available." + description: medium_str_type | None = "No description available." datetimeperiod: Tuple[datetime, datetime] poster: str | None = None audience: List[Audience] = [] @@ -43,14 +62,15 @@ class Event(BaseModel): mode: Event_Mode = Event_Mode.hybrid location: List[Event_Location] = [] - equipment: event_othr_type | None = None - additional: event_othr_type | None = None + equipment: short_str_type | None = None + additional: short_str_type | None = None population: event_popu_type | None = None poc: str | None = None status: Event_Status = Event_Status() budget: List[BudgetType] = [] bills_status: Bills_Status = Bills_Status() + event_report_submitted: bool = False @field_validator("datetimeperiod") def check_end_year(cls, value, info: ValidationInfo): @@ -61,14 +81,16 @@ def check_end_year(cls, value, info: ValidationInfo): model_config = ConfigDict( populate_by_name=True, arbitrary_types_allowed=True, + extra="forbid", + str_strip_whitespace=True, ) class Holiday(BaseModel): id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") - name: str + name: very_short_str_type date: date - description: str | None = None + description: medium_str_type | None = None created_time: datetime = Field( default_factory=lambda: datetime.now(timezone), frozen=True ) diff --git a/mtypes.py b/mtypes.py index 7aeee7d..c6a09b7 100644 --- a/mtypes.py +++ b/mtypes.py @@ -247,12 +247,14 @@ class Event_Location(StrEnum): other = auto() -event_name_type = Annotated[ +event_popu_type = Annotated[int, Field(ge=0)] + +very_short_str_type = Annotated[ str, StringConstraints(min_length=1, max_length=200) ] -event_desc_type = Annotated[str, StringConstraints(max_length=5000)] -event_popu_type = Annotated[int, Field(ge=0)] -event_othr_type = Annotated[str, StringConstraints(max_length=1000)] +short_str_type = Annotated[str, StringConstraints(max_length=1000)] +medium_str_type = Annotated[str, StringConstraints(max_length=5000)] +long_str_type = Annotated[str, StringConstraints(max_length=10000)] @strawberry.type @@ -270,6 +272,16 @@ def positive_amount(cls, value): return value +@strawberry.enum +class PrizesType(StrEnum): + win_certificates = auto() + participation_certificates = auto() + cash_prizes = auto() + vouchers = auto() + medals = auto() + others = auto() + + # for handling mongo ObjectIds class PyObjectId(ObjectId): @classmethod diff --git a/mutations/__init__.py b/mutations/__init__.py index ed9d5cd..38f61d0 100644 --- a/mutations/__init__.py +++ b/mutations/__init__.py @@ -1,9 +1,11 @@ +from mutations.event_report import mutations as event_report_mutations from mutations.events import mutations as events_mutations from mutations.finances import mutations as finances_mutations from mutations.holidays import mutations as holidays_mutations mutations = [ *events_mutations, + *event_report_mutations, *finances_mutations, *holidays_mutations, ] \ No newline at end of file diff --git a/mutations/event_report.py b/mutations/event_report.py new file mode 100644 index 0000000..dd4303e --- /dev/null +++ b/mutations/event_report.py @@ -0,0 +1,76 @@ +from datetime import datetime + +import strawberry +from fastapi.encoders import jsonable_encoder + +from db import event_reportsdb, eventsdb +from models import EventReport +from mtypes import Event_State_Status +from otypes import EventReportType, Info, InputEventReport +from utils import getMember + + +@strawberry.mutation +def addEventReport(details: InputEventReport, info: Info) -> EventReportType: + """ + Add an event report + returns the added event report + """ + + user = info.context.user + if not user: + raise ValueError("User not authenticated") + + user_role = user["role"] + if user_role not in ["club"]: + raise ValueError("User not authorized") + + eventid = details.eventid + if not eventid: + raise ValueError("Event ID is required") + event = eventsdb.find_one( + { + "_id": eventid, + "datetimeperiod.1": { + "$lt": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ") + }, + "status.state": Event_State_Status.approved.value, + } + ) + if not event: + raise ValueError("Event not found") + if user_role == "club" and event["clubid"] != user["uid"]: + raise ValueError("User not authorized") + + searchspace = { + "event_id": eventid, + } + + event_report = event_reportsdb.find_one(searchspace) + + if event_report: + raise ValueError("Event report already exists") + + # Check if submitted_by is valid + cid = event["clubid"] + uid = details.submitted_by + if not getMember(cid, uid, info.context.cookies): + raise ValueError("Submitted by is not a valid member") + + report_dict = jsonable_encoder(details.to_pydantic()) + report_dict["event_id"] = details.eventid + event_report_id = event_reportsdb.insert_one(report_dict).inserted_id + event_report = event_reportsdb.find_one({"_id": event_report_id}) + + # Update event report submitted status to True + eventsdb.update_one( + {"_id": eventid}, + {"$set": {"event_report_submitted": True}}, + ) + + return EventReportType.from_pydantic( + EventReport.model_validate(event_report) + ) + + +mutations = [addEventReport] diff --git a/otypes.py b/otypes.py index 1731a4d..5c7e5a5 100644 --- a/otypes.py +++ b/otypes.py @@ -9,7 +9,7 @@ from strawberry.types import Info as _Info from strawberry.types.info import RootValueType -from models import Event, Holiday +from models import Event, EventReport, Holiday from mtypes import ( Audience, Bills_State_Status, @@ -48,6 +48,11 @@ def cookies(self) -> Dict | None: ) +@strawberry.experimental.pydantic.type(model=EventReport, all_fields=True) +class EventReportType: + pass + + @strawberry.experimental.pydantic.type(model=Event, all_fields=True) class EventType: pass @@ -68,6 +73,7 @@ class BillsStatusType: eventname: str clubid: str bills_status: Bills_Status + eventReportSubmitted: str @strawberry.input @@ -129,6 +135,11 @@ class InputDataReportDetails: status: str +@strawberry.experimental.pydantic.input(model=EventReport, all_fields=True) +class InputEventReport: + pass + + @strawberry.type class CSVResponse: csvFile: str diff --git a/queries/__init__.py b/queries/__init__.py index e8784c8..a5c1796 100644 --- a/queries/__init__.py +++ b/queries/__init__.py @@ -1,9 +1,11 @@ +from queries.event_report import queries as event_report_queries from queries.events import queries as events_queries from queries.finances import queries as finances_queries from queries.holidays import queries as holidays_queries queries = [ *events_queries, + *event_report_queries, *finances_queries, *holidays_queries, -] \ No newline at end of file +] diff --git a/queries/event_report.py b/queries/event_report.py new file mode 100644 index 0000000..34cc499 --- /dev/null +++ b/queries/event_report.py @@ -0,0 +1,46 @@ +import strawberry + +from db import event_reportsdb, eventsdb +from models import EventReport +from otypes import EventReportType, Info + + +@strawberry.field +def eventReport(eventid: str, info: Info) -> EventReportType: + """ + Get the event report of an event + returns the event report + """ + + user = info.context.user + if not user: + raise ValueError("User not authenticated") + + user_role = user["role"] + if user_role not in ["cc", "slo", "club"]: + raise ValueError("User not authorized") + + event = eventsdb.find_one({"_id": eventid, "event_report_submitted": True}) + if not event: + raise ValueError("Event not found") + if user_role == "club" and event["clubid"] != user["uid"] and ( + event["collabclubs"] is None + or user["uid"] not in event["collabclubs"] + ): + raise ValueError("User not authorized") + + event_report = event_reportsdb.find_one( + { + "event_id": eventid, + } + ) + + if not event_report: + raise ValueError("Event report not found") + + return EventReportType.from_pydantic( + EventReport.model_validate(event_report) + ) + + +queries = [eventReport] diff --git a/queries/events.py b/queries/events.py index 952fa8e..0261cb2 100644 --- a/queries/events.py +++ b/queries/events.py @@ -73,7 +73,14 @@ def event(eventid: str, info: Info) -> EventType: if ( user is None or user["role"] not in ["club", "cc", "slc", "slo"] - or (user["role"] == "club" and user["uid"] != event["clubid"]) + or ( + user["role"] == "club" + and user["uid"] != event["clubid"] + and ( + event["collabclubs"] is None + or user["uid"] not in event["collabclubs"] + ) + ) ): trim_public_events(event) diff --git a/queries/finances.py b/queries/finances.py index 87a6098..7e8d863 100644 --- a/queries/finances.py +++ b/queries/finances.py @@ -18,33 +18,43 @@ def eventBills(eventid: str, info: Info) -> Bills_Status: user = info.context.user if not user: raise ValueError("User not authenticated") - + user_role = user["role"] if user_role not in ["cc", "slo", "club"]: raise ValueError("User not authorized") - + searchspace = { "_id": eventid, - "status.state": Event_State_Status.approved.value, # type: ignore - "datetimeperiod.1": { - "$lt": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ") - }, - "budget": { - "$exists": True, - "$ne": [], - }, # Ensure the budget array exists and is not empty + "status.state": Event_State_Status.approved.value, } if user_role == "club": - searchspace.update({"clubid": user["uid"]}) + searchspace["$or"] = [ # type: ignore + {"clubid": user["uid"]}, + {"collabclubs": {"$in": [user["uid"]]}}, + ] event = eventsdb.find_one(searchspace) - if not event: - raise ValueError("Event not found") + raise ValueError( + "Event not found. Either the event does not exist or you don't have\ + access to it or it is not approved." + ) + + if event["datetimeperiod"][1] > datetime.now().strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ): + raise ValueError(f"{event["name"]} has not ended yet.") + + if ( + "budget" not in event + or not event["budget"] + or len(event["budget"]) == 0 + ): + raise ValueError(f"{event["name"]} has no budget.") if "bills_status" not in event: - raise ValueError("Bills status not found") + raise ValueError(f"{event["name"]} has no bills status.") return Bills_Status(**event["bills_status"]) @@ -77,7 +87,14 @@ def allEventsBills(info: Info) -> List[BillsStatusType]: } if user_role == "club": - searchspace.update({"clubid": user["uid"]}) + searchspace.update( + { + "$or": [ + {"clubid": user["uid"]}, + {"collabclubs": {"$in": [user["uid"]]}}, + ] + } + ) events = list(eventsdb.find(searchspace).sort("datetimeperiod.1", -1)) if not events or len(events) == 0: @@ -89,6 +106,7 @@ def allEventsBills(info: Info) -> List[BillsStatusType]: eventname=event["name"], clubid=event["clubid"], bills_status=Bills_Status(**event["bills_status"]), + eventReportSubmitted=event.get("event_report_submitted", "old"), ) for event in events ]