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.
This commit is contained in:
parent
f7a3da7893
commit
b1d3fa0600
|
@ -5,7 +5,7 @@
|
||||||
# You should NOT run this script in production.
|
# You should NOT run this script in production.
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import date, time
|
from datetime import UTC, datetime
|
||||||
from typing import cast, final
|
from typing import cast, final
|
||||||
|
|
||||||
from beanie import PydanticObjectId
|
from beanie import PydanticObjectId
|
||||||
|
@ -93,10 +93,8 @@ EVENTS: list[EventConstructData] = [
|
||||||
title="Event 1",
|
title="Event 1",
|
||||||
description="Description 1",
|
description="Description 1",
|
||||||
category_ids=[],
|
category_ids=[],
|
||||||
start_date=date(2025, 1, 1),
|
start_time=datetime(2025, 1, 1, 12, 00, tzinfo=UTC),
|
||||||
start_time=time(12, 00),
|
end_time=datetime(2025, 1, 1, 12, 30, tzinfo=UTC),
|
||||||
end_date=date(2025, 1, 1),
|
|
||||||
end_time=time(12, 30),
|
|
||||||
color=Color("#ff0000"),
|
color=Color("#ff0000"),
|
||||||
),
|
),
|
||||||
owner_username="user1",
|
owner_username="user1",
|
||||||
|
@ -108,10 +106,8 @@ EVENTS: list[EventConstructData] = [
|
||||||
title="Event 2",
|
title="Event 2",
|
||||||
description="Description 2",
|
description="Description 2",
|
||||||
category_ids=[],
|
category_ids=[],
|
||||||
start_date=date(2025, 1, 1),
|
start_time=datetime(2025, 1, 1, 12, 00, tzinfo=UTC),
|
||||||
start_time=time(12, 00),
|
end_time=datetime(2025, 1, 1, 12, 30, tzinfo=UTC),
|
||||||
end_date=date(2025, 1, 1),
|
|
||||||
end_time=time(12, 30),
|
|
||||||
color=Color("#ff0000"),
|
color=Color("#ff0000"),
|
||||||
),
|
),
|
||||||
owner_username="user2",
|
owner_username="user2",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from datetime import date, datetime, time
|
from datetime import datetime
|
||||||
from typing import Annotated, cast, final
|
from typing import Annotated, cast, final
|
||||||
|
|
||||||
from beanie import DeleteRules, Link, PydanticObjectId
|
from beanie import DeleteRules, Link, PydanticObjectId
|
||||||
|
@ -31,10 +31,8 @@ class _BaseEventData(BaseModel):
|
||||||
title: Annotated[str, StringConstraints(min_length=1, max_length=50)]
|
title: Annotated[str, StringConstraints(min_length=1, max_length=50)]
|
||||||
description: Annotated[str, StringConstraints(max_length=1000)]
|
description: Annotated[str, StringConstraints(max_length=1000)]
|
||||||
category_ids: list[PydanticObjectId]
|
category_ids: list[PydanticObjectId]
|
||||||
start_date: date
|
start_time: datetime
|
||||||
start_time: time
|
end_time: datetime
|
||||||
end_date: date
|
|
||||||
end_time: time
|
|
||||||
color: Color
|
color: Color
|
||||||
|
|
||||||
@field_validator("color", mode="after")
|
@field_validator("color", mode="after")
|
||||||
|
@ -77,9 +75,7 @@ class EventData(_BaseEventData):
|
||||||
title=event.title,
|
title=event.title,
|
||||||
description=event.description,
|
description=event.description,
|
||||||
category_ids=get_id_list(event.categories),
|
category_ids=get_id_list(event.categories),
|
||||||
start_date=event.start_date,
|
|
||||||
start_time=event.start_time,
|
start_time=event.start_time,
|
||||||
end_date=event.end_date,
|
|
||||||
end_time=event.end_time,
|
end_time=event.end_time,
|
||||||
color=Color(event.color),
|
color=Color(event.color),
|
||||||
attendee_ids=get_id_list(event.attendees),
|
attendee_ids=get_id_list(event.attendees),
|
||||||
|
@ -104,9 +100,7 @@ class EventCreateData(_BaseEventData):
|
||||||
title=self.title,
|
title=self.title,
|
||||||
description=self.description,
|
description=self.description,
|
||||||
categories=await from_id_list(self.category_ids, Category, link_return=True),
|
categories=await from_id_list(self.category_ids, Category, link_return=True),
|
||||||
start_date=self.start_date,
|
|
||||||
start_time=self.start_time,
|
start_time=self.start_time,
|
||||||
end_date=self.end_date,
|
|
||||||
end_time=self.end_time,
|
end_time=self.end_time,
|
||||||
color=self.color.as_hex(format="long"),
|
color=self.color.as_hex(format="long"),
|
||||||
attendees=[],
|
attendees=[],
|
||||||
|
@ -120,9 +114,7 @@ class EventCreateData(_BaseEventData):
|
||||||
title=self.title,
|
title=self.title,
|
||||||
description=self.description,
|
description=self.description,
|
||||||
categories=await from_id_list(self.category_ids, Category),
|
categories=await from_id_list(self.category_ids, Category),
|
||||||
start_date=self.start_date,
|
|
||||||
start_time=self.start_time,
|
start_time=self.start_time,
|
||||||
end_date=self.end_date,
|
|
||||||
end_time=self.end_time,
|
end_time=self.end_time,
|
||||||
color=self.color.as_hex(format="long"),
|
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]
|
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]
|
description: Annotated[str, StringConstraints(max_length=1000)] | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
category_ids: list[PydanticObjectId] | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
category_ids: list[PydanticObjectId] | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
start_date: date | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
start_time: datetime | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
start_time: time | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
end_time: datetime | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
end_date: date | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
|
||||||
end_time: time | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
|
||||||
color: Color | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
color: Color | None = None # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
async def update_event(self, event: Event) -> Event:
|
async def update_event(self, event: Event) -> Event:
|
||||||
|
@ -152,12 +142,8 @@ class PartialEventUpdateData(_BaseEventData):
|
||||||
update_dct["description"] = self.description
|
update_dct["description"] = self.description
|
||||||
if self.category_ids is not None:
|
if self.category_ids is not None:
|
||||||
update_dct["categories"] = await from_id_list(self.category_ids, Category)
|
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:
|
if self.start_time is not None:
|
||||||
update_dct["start_time"] = self.start_time
|
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:
|
if self.end_time is not None:
|
||||||
update_dct["end_time"] = self.end_time
|
update_dct["end_time"] = self.end_time
|
||||||
if self.color is not None:
|
if self.color is not None:
|
||||||
|
@ -170,17 +156,17 @@ class PartialEventUpdateData(_BaseEventData):
|
||||||
async def get_user_events(
|
async def get_user_events(
|
||||||
user_id: PydanticObjectId,
|
user_id: PydanticObjectId,
|
||||||
user: CurrentUserDep,
|
user: CurrentUserDep,
|
||||||
start_date_from: Annotated[date | None, Query()] = None,
|
start_from: Annotated[datetime | None, Query()] = None,
|
||||||
start_date_to: Annotated[date | None, Query()] = None,
|
start_to: Annotated[datetime | None, Query()] = None,
|
||||||
end_date_from: Annotated[date | None, Query()] = None,
|
end_from: Annotated[datetime | None, Query()] = None,
|
||||||
end_date_to: Annotated[date | None, Query()] = None,
|
end_to: Annotated[datetime | None, Query()] = None,
|
||||||
) -> list[EventData]:
|
) -> list[EventData]:
|
||||||
"""Get all events that belong to given user.
|
"""Get all events that belong to given user.
|
||||||
|
|
||||||
Optionally, it's possible to use query params to filter the events
|
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:
|
if user_id != user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -191,15 +177,15 @@ async def get_user_events(
|
||||||
# Initial query (all events for the user)
|
# Initial query (all events for the user)
|
||||||
query = Event.find(expr(Event.user).id == user.id)
|
query = Event.find(expr(Event.user).id == user.id)
|
||||||
|
|
||||||
# Filter by date
|
# Filter by date-time
|
||||||
if start_date_from is not None:
|
if start_from is not None:
|
||||||
query = query.find(expr(Event.start_date) >= start_date_from)
|
query = query.find(expr(Event.start_time) >= start_from)
|
||||||
if start_date_to is not None:
|
if start_to is not None:
|
||||||
query = query.find(expr(Event.start_date) <= start_date_to)
|
query = query.find(expr(Event.start_time) <= start_to)
|
||||||
if end_date_from is not None:
|
if end_from is not None:
|
||||||
query = query.find(expr(Event.end_date) >= end_date_from)
|
query = query.find(expr(Event.end_time) >= end_from)
|
||||||
if end_date_to is not None:
|
if end_to is not None:
|
||||||
query = query.find(expr(Event.end_date) <= end_date_to)
|
query = query.find(expr(Event.end_time) <= end_to)
|
||||||
|
|
||||||
query = query.find(fetch_links=True)
|
query = query.find(fetch_links=True)
|
||||||
|
|
||||||
|
@ -211,10 +197,10 @@ async def get_user_events(
|
||||||
async def get_user_invited_events(
|
async def get_user_invited_events(
|
||||||
user_id: PydanticObjectId,
|
user_id: PydanticObjectId,
|
||||||
user: CurrentUserDep,
|
user: CurrentUserDep,
|
||||||
start_date_from: Annotated[date | None, Query()] = None,
|
start_from: Annotated[datetime | None, Query()] = None,
|
||||||
start_date_to: Annotated[date | None, Query()] = None,
|
start_to: Annotated[datetime | None, Query()] = None,
|
||||||
end_date_from: Annotated[date | None, Query()] = None,
|
end_from: Annotated[datetime | None, Query()] = None,
|
||||||
end_date_to: Annotated[date | None, Query()] = None,
|
end_to: Annotated[datetime | None, Query()] = None,
|
||||||
) -> list[EventData]:
|
) -> list[EventData]:
|
||||||
"""Get all events that given user was invited to.
|
"""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.
|
Events pending acceptance are not included.
|
||||||
|
|
||||||
Optionally, it's possible to use query params to filter the events
|
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:
|
if user_id != user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -235,15 +221,15 @@ async def get_user_invited_events(
|
||||||
# Initial query (all events the user is attending)
|
# Initial query (all events the user is attending)
|
||||||
query = Event.find(expr(Event.attendees).id == user_id, fetch_links=True)
|
query = Event.find(expr(Event.attendees).id == user_id, fetch_links=True)
|
||||||
|
|
||||||
# Filter by date
|
# Filter by date-time
|
||||||
if start_date_from is not None:
|
if start_from is not None:
|
||||||
query = query.find(expr(Event.start_date) >= start_date_from)
|
query = query.find(expr(Event.start_time) >= start_from)
|
||||||
if start_date_to is not None:
|
if start_to is not None:
|
||||||
query = query.find(expr(Event.start_date) <= start_date_to)
|
query = query.find(expr(Event.start_time) <= start_to)
|
||||||
if end_date_from is not None:
|
if end_from is not None:
|
||||||
query = query.find(expr(Event.end_date) >= end_date_from)
|
query = query.find(expr(Event.end_time) >= end_from)
|
||||||
if end_date_to is not None:
|
if end_to is not None:
|
||||||
query = query.find(expr(Event.end_date) <= end_date_to)
|
query = query.find(expr(Event.end_time) <= end_to)
|
||||||
|
|
||||||
events = await query.to_list()
|
events = await query.to_list()
|
||||||
return [EventData.from_event(event) for event in events]
|
return [EventData.from_event(event) for event in events]
|
||||||
|
|
|
@ -3,6 +3,7 @@ from typing import Annotated, ClassVar, final
|
||||||
|
|
||||||
from beanie import Document, Indexed
|
from beanie import Document, Indexed
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
from pymongo import ASCENDING
|
||||||
|
|
||||||
from src.db.models.category import Category
|
from src.db.models.category import Category
|
||||||
from src.db.models.user import User
|
from src.db.models.user import User
|
||||||
|
@ -17,10 +18,8 @@ class Event(Document):
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
categories: Annotated[list[BeanieLink[Category]], Indexed()]
|
categories: Annotated[list[BeanieLink[Category]], Indexed()]
|
||||||
start_date: Annotated[date, Indexed()]
|
start_time: Annotated[datetime, Indexed()]
|
||||||
start_time: time
|
end_time: Annotated[datetime, Indexed()]
|
||||||
end_date: Annotated[date, Indexed()]
|
|
||||||
end_time: time
|
|
||||||
color: str # Stored as a hex string (# + 6 characters)
|
color: str # Stored as a hex string (# + 6 characters)
|
||||||
attendees: Annotated[list[BeanieLink[User]], Indexed()]
|
attendees: Annotated[list[BeanieLink[User]], Indexed()]
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||||
|
@ -29,3 +28,11 @@ class Event(Document):
|
||||||
class Settings:
|
class Settings:
|
||||||
name: ClassVar = "events"
|
name: ClassVar = "events"
|
||||||
bson_encoders: ClassVar = {datetime: str, time: str, date: str}
|
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),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in a new issue