diff --git a/populate_db.py b/populate_db.py index 2e6dd75..99fcc0d 100755 --- a/populate_db.py +++ b/populate_db.py @@ -5,7 +5,7 @@ # You should NOT run this script in production. import asyncio -from datetime import date, time +from datetime import UTC, datetime from typing import cast, final from beanie import PydanticObjectId @@ -93,10 +93,8 @@ EVENTS: list[EventConstructData] = [ title="Event 1", description="Description 1", category_ids=[], - start_date=date(2025, 1, 1), - start_time=time(12, 00), - end_date=date(2025, 1, 1), - end_time=time(12, 30), + start_time=datetime(2025, 1, 1, 12, 00, tzinfo=UTC), + end_time=datetime(2025, 1, 1, 12, 30, tzinfo=UTC), color=Color("#ff0000"), ), owner_username="user1", @@ -108,10 +106,8 @@ EVENTS: list[EventConstructData] = [ title="Event 2", description="Description 2", category_ids=[], - start_date=date(2025, 1, 1), - start_time=time(12, 00), - end_date=date(2025, 1, 1), - end_time=time(12, 30), + start_time=datetime(2025, 1, 1, 12, 00, tzinfo=UTC), + end_time=datetime(2025, 1, 1, 12, 30, tzinfo=UTC), color=Color("#ff0000"), ), owner_username="user2", diff --git a/src/api/events.py b/src/api/events.py index e00eed3..05a8add 100644 --- a/src/api/events.py +++ b/src/api/events.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, time +from datetime import datetime from typing import Annotated, cast, final from beanie import DeleteRules, Link, PydanticObjectId @@ -31,10 +31,8 @@ class _BaseEventData(BaseModel): title: Annotated[str, StringConstraints(min_length=1, max_length=50)] description: Annotated[str, StringConstraints(max_length=1000)] category_ids: list[PydanticObjectId] - start_date: date - start_time: time - end_date: date - end_time: time + start_time: datetime + end_time: datetime color: Color @field_validator("color", mode="after") @@ -77,9 +75,7 @@ class EventData(_BaseEventData): title=event.title, description=event.description, category_ids=get_id_list(event.categories), - start_date=event.start_date, start_time=event.start_time, - end_date=event.end_date, end_time=event.end_time, color=Color(event.color), attendee_ids=get_id_list(event.attendees), @@ -104,9 +100,7 @@ class EventCreateData(_BaseEventData): title=self.title, description=self.description, categories=await from_id_list(self.category_ids, Category, link_return=True), - start_date=self.start_date, start_time=self.start_time, - end_date=self.end_date, end_time=self.end_time, color=self.color.as_hex(format="long"), attendees=[], @@ -120,9 +114,7 @@ class EventCreateData(_BaseEventData): title=self.title, description=self.description, categories=await from_id_list(self.category_ids, Category), - start_date=self.start_date, start_time=self.start_time, - end_date=self.end_date, end_time=self.end_time, color=self.color.as_hex(format="long"), ) @@ -135,10 +127,8 @@ class PartialEventUpdateData(_BaseEventData): title: Annotated[str, StringConstraints(min_length=1, max_length=50)] | None = None # pyright: ignore[reportIncompatibleVariableOverride] description: Annotated[str, StringConstraints(max_length=1000)] | None = None # pyright: ignore[reportIncompatibleVariableOverride] category_ids: list[PydanticObjectId] | None = None # pyright: ignore[reportIncompatibleVariableOverride] - start_date: date | None = None # pyright: ignore[reportIncompatibleVariableOverride] - start_time: time | None = None # pyright: ignore[reportIncompatibleVariableOverride] - end_date: date | None = None # pyright: ignore[reportIncompatibleVariableOverride] - end_time: time | None = None # pyright: ignore[reportIncompatibleVariableOverride] + start_time: datetime | None = None # pyright: ignore[reportIncompatibleVariableOverride] + end_time: datetime | None = None # pyright: ignore[reportIncompatibleVariableOverride] color: Color | None = None # pyright: ignore[reportIncompatibleVariableOverride] async def update_event(self, event: Event) -> Event: @@ -152,12 +142,8 @@ class PartialEventUpdateData(_BaseEventData): update_dct["description"] = self.description if self.category_ids is not None: update_dct["categories"] = await from_id_list(self.category_ids, Category) - if self.start_date is not None: - update_dct["start_date"] = self.start_date if self.start_time is not None: update_dct["start_time"] = self.start_time - if self.end_date is not None: - update_dct["end_date"] = self.end_date if self.end_time is not None: update_dct["end_time"] = self.end_time if self.color is not None: @@ -170,17 +156,17 @@ class PartialEventUpdateData(_BaseEventData): async def get_user_events( user_id: PydanticObjectId, user: CurrentUserDep, - start_date_from: Annotated[date | None, Query()] = None, - start_date_to: Annotated[date | None, Query()] = None, - end_date_from: Annotated[date | None, Query()] = None, - end_date_to: Annotated[date | None, Query()] = None, + start_from: Annotated[datetime | None, Query()] = None, + start_to: Annotated[datetime | None, Query()] = None, + end_from: Annotated[datetime | None, Query()] = None, + end_to: Annotated[datetime | None, Query()] = None, ) -> list[EventData]: """Get all events that belong to given user. Optionally, it's possible to use query params to filter the events - based on the date of the event. The dates are specified in ISO 8601 format. + based on the date-time of the event. The dates are specified in ISO 8601 format. - E.g.: /users/{user_id}/events?start_date_from=2025-01-01&start_date_to=2025-01-10 + E.g.: /users/{user_id}/events?start_from=2024-12-29T13:48:53.228234Z """ if user_id != user.id: raise HTTPException( @@ -191,15 +177,15 @@ async def get_user_events( # Initial query (all events for the user) query = Event.find(expr(Event.user).id == user.id) - # Filter by date - if start_date_from is not None: - query = query.find(expr(Event.start_date) >= start_date_from) - if start_date_to is not None: - query = query.find(expr(Event.start_date) <= start_date_to) - if end_date_from is not None: - query = query.find(expr(Event.end_date) >= end_date_from) - if end_date_to is not None: - query = query.find(expr(Event.end_date) <= end_date_to) + # Filter by date-time + if start_from is not None: + query = query.find(expr(Event.start_time) >= start_from) + if start_to is not None: + query = query.find(expr(Event.start_time) <= start_to) + if end_from is not None: + query = query.find(expr(Event.end_time) >= end_from) + if end_to is not None: + query = query.find(expr(Event.end_time) <= end_to) query = query.find(fetch_links=True) @@ -211,10 +197,10 @@ async def get_user_events( async def get_user_invited_events( user_id: PydanticObjectId, user: CurrentUserDep, - start_date_from: Annotated[date | None, Query()] = None, - start_date_to: Annotated[date | None, Query()] = None, - end_date_from: Annotated[date | None, Query()] = None, - end_date_to: Annotated[date | None, Query()] = None, + start_from: Annotated[datetime | None, Query()] = None, + start_to: Annotated[datetime | None, Query()] = None, + end_from: Annotated[datetime | None, Query()] = None, + end_to: Annotated[datetime | None, Query()] = None, ) -> list[EventData]: """Get all events that given user was invited to. @@ -222,9 +208,9 @@ async def get_user_invited_events( Events pending acceptance are not included. Optionally, it's possible to use query params to filter the events - based on the date of the event. The dates are specified in ISO 8601 format. + based on the date-time of the event. The dates are specified in ISO 8601 format. - E.g.: /users/{user_id}/events/invited?start_date_from=2025-01-01&start_date_to=2025-01-10 + E.g.: /users/{user_id}/events/invited?start_from=2024-12-29T13:48:53.228234Z """ if user_id != user.id: raise HTTPException( @@ -235,15 +221,15 @@ async def get_user_invited_events( # Initial query (all events the user is attending) query = Event.find(expr(Event.attendees).id == user_id, fetch_links=True) - # Filter by date - if start_date_from is not None: - query = query.find(expr(Event.start_date) >= start_date_from) - if start_date_to is not None: - query = query.find(expr(Event.start_date) <= start_date_to) - if end_date_from is not None: - query = query.find(expr(Event.end_date) >= end_date_from) - if end_date_to is not None: - query = query.find(expr(Event.end_date) <= end_date_to) + # Filter by date-time + if start_from is not None: + query = query.find(expr(Event.start_time) >= start_from) + if start_to is not None: + query = query.find(expr(Event.start_time) <= start_to) + if end_from is not None: + query = query.find(expr(Event.end_time) >= end_from) + if end_to is not None: + query = query.find(expr(Event.end_time) <= end_to) events = await query.to_list() return [EventData.from_event(event) for event in events] diff --git a/src/db/models/event.py b/src/db/models/event.py index a5124da..4ef880e 100644 --- a/src/db/models/event.py +++ b/src/db/models/event.py @@ -3,6 +3,7 @@ from typing import Annotated, ClassVar, final from beanie import Document, Indexed from pydantic import Field +from pymongo import ASCENDING from src.db.models.category import Category from src.db.models.user import User @@ -17,10 +18,8 @@ class Event(Document): title: str description: str categories: Annotated[list[BeanieLink[Category]], Indexed()] - start_date: Annotated[date, Indexed()] - start_time: time - end_date: Annotated[date, Indexed()] - end_time: time + start_time: Annotated[datetime, Indexed()] + end_time: Annotated[datetime, Indexed()] color: str # Stored as a hex string (# + 6 characters) attendees: Annotated[list[BeanieLink[User]], Indexed()] created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) @@ -29,3 +28,11 @@ class Event(Document): class Settings: name: ClassVar = "events" bson_encoders: ClassVar = {datetime: str, time: str, date: str} + indexes: ClassVar = [ + # On top of just having start & end time standalone indexes + # also make a compound index for fast searching by both at once + [ + ("start_time", ASCENDING), + ("end_time", ASCENDING), + ], + ]