From b1d3fa06005cc76fd70a8b32a5a93d081ec2d3a3 Mon Sep 17 00:00:00 2001 From: Peter Vacho Date: Sun, 29 Dec 2024 23:14:21 +0100 Subject: [PATCH] Use datetime for start & end in events Storing date & time individually did originally seem like a better idea, as it allowed nice indexing of the days which makes for faster lookups, however, since standalone day & time informations don't carry timezone data, doing this is problematic. One option would be to introduce another field for the timezone, but this just seems to be needlessly to complex, instead, store the start & end event times as full datetimes. These will still be indexed and should still result in sufficiently fast lookups. Additionally, this also adds a compound index for start time & end time, to improve speeds when searching by both at once. --- populate_db.py | 14 +++---- src/api/events.py | 84 ++++++++++++++++++------------------------ src/db/models/event.py | 15 ++++++-- 3 files changed, 51 insertions(+), 62 deletions(-) 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), + ], + ]