event-management-backend/populate_db.py
Peter Vacho b1d3fa0600
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.
2024-12-29 23:17:01 +01:00

264 lines
8.9 KiB
Python
Executable file

#!./.venv/bin/python
#
# This script is used to populate the database with some initial data.
# It's mostly useful for testing purposes, in order to have some data to work with.
# You should NOT run this script in production.
import asyncio
from datetime import UTC, datetime
from typing import cast, final
from beanie import PydanticObjectId
from pydantic import BaseModel
from pydantic_extra_types.color import Color
from src.api.categories import CategoryCreateData
from src.api.events import EventCreateData
from src.api.invitations import InvitationCreateData
from src.api.users import RegisterData
from src.db.init import initialize_db
from src.db.models.category import Category
from src.db.models.event import Event
from src.db.models.invitation import Invitation
from src.db.models.user import User
from src.utils.db import expr, from_id_list
from src.utils.logging import get_logger
log = get_logger(__name__)
@final
class CategoryConstructData(BaseModel):
"""Data necessary to create a new category."""
data: CategoryCreateData
owner_username: str
@final
class EventConstructData(BaseModel):
"""Data necessary to create a new event."""
data: EventCreateData
owner_username: str
attendee_usernames: list[str] = []
category_names: list[str] = []
@final
class InvitationConstructData(BaseModel):
"""Data necessary to create a new invitation."""
event_name: str
invitee_username: str
invitor_username: str
create_notificaton: bool
USERS: list[RegisterData] = [
RegisterData(
username="user1",
email="user1@example.org",
password="test1", # noqa: S106
),
RegisterData(
username="user2",
email="user2@example.org",
password="test2", # noqa: S106
),
]
CATEGORIES: list[CategoryConstructData] = [
CategoryConstructData(
data=CategoryCreateData(name="Category 1", color=Color("#ff0000")),
owner_username="user1",
),
CategoryConstructData(
data=CategoryCreateData(name="Category 2", color=Color("blue")),
owner_username="user1",
),
CategoryConstructData(
data=CategoryCreateData(name="Category A", color=Color("#ff00ff")),
owner_username="user2",
),
CategoryConstructData(
data=CategoryCreateData(name="Category B", color=Color("magenta")),
owner_username="user2",
),
]
EVENTS: list[EventConstructData] = [
EventConstructData(
data=EventCreateData(
title="Event 1",
description="Description 1",
category_ids=[],
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",
attendee_usernames=["user2"],
category_names=["Category 1"],
),
EventConstructData(
data=EventCreateData(
title="Event 2",
description="Description 2",
category_ids=[],
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",
attendee_usernames=[],
category_names=["Category A", "Category B"],
),
]
INVITATIONS: list[InvitationConstructData] = [
InvitationConstructData(
event_name="Event 2",
invitee_username="user1",
invitor_username="user2",
create_notificaton=True,
),
]
async def make_user(user: RegisterData) -> None:
"""Create a new user with configured credentials."""
db_user = await User.find_one(User.username == user.username)
if db_user is not None:
log.info(f"User {db_user.username!r} was already created. Deleting it.")
_ = await db_user.delete()
_ = await user.create_user()
log.info(f"User {user.username!r} created; password: {user.password!r}, email: {user.email!r}.")
async def make_category(category: CategoryConstructData) -> None:
"""Create a new category with configured data."""
db_user = await User.find_one(User.username == category.owner_username)
if db_user is None:
log.error(f"User {category.owner_username!r} not found, failed to create category.")
return
# The category name isn't actually unique, so this is not exactly correct
# but for the sake of simplicity, we'll assume it is.
# Note that category names might become unique eventually anyways
db_category = await Category.find(Category.name == category.data.name, Category.user == db_user).first_or_none()
if db_category is not None:
log.info(f"Category {db_category.name!r} for user {category.owner_username!r} already exists. Deleting it.")
_ = await db_category.delete()
_ = await category.data.create_category(db_user)
log.info(f"Category {category.data.name!r} created for user {category.owner_username!r}.")
async def make_event(event: EventConstructData) -> None:
"""Create a new event with configured data."""
db_user = await User.find_one(User.username == event.owner_username)
if db_user is None:
log.error(f"User {event.owner_username!r} not found, failed to create event.")
return
attendee_ids = []
for attendee_username in event.attendee_usernames:
db_attendee = await User.find_one(User.username == attendee_username)
if db_attendee is None:
log.error(f"Attendee user {attendee_username!r} not found, failed to create event.")
return
attendee_ids.append(db_attendee.id)
category_ids = []
for category_name in event.category_names:
db_category = await Category.find(
Category.name == category_name,
Category.user == User.link_from_id(db_user.id),
).first_or_none()
if db_category is None:
log.error(f"Category {category_name!r} not found, failed to create event.")
return
category_ids.append(db_category.id)
# The event title isn't actually unique, so this is not exactly correct
# but for the sake of simplicity, we'll assume it is.
db_event = await Event.find(Event.title == event.data.title, Event.user == db_user).first_or_none()
if db_event is not None:
log.info(f"Event {db_event.title!r} for user {event.owner_username!r} already exists. Deleting it.")
_ = await db_event.delete()
event.data.category_ids = category_ids
db_event = await event.data.create_event(db_user)
if len(attendee_ids) > 0:
db_event.attendees = await from_id_list(attendee_ids, User, link_return=True)
db_event = await db_event.replace()
log.info(f"Event {event.data.title!r} created for user {event.owner_username!r}.")
async def make_invitation(invitation: InvitationConstructData) -> None:
"""Create a new invitation with configured data."""
db_invitor = await User.find_one(User.username == invitation.invitor_username)
if db_invitor is None:
log.error(f"Invitor user {invitation.invitor_username!r} not found, failed to create invitation.")
return
db_invitee = await User.find_one(User.username == invitation.invitee_username)
if db_invitee is None:
log.error(f"Invitee user {invitation.invitee_username!r} not found, failed to create invitation.")
return
db_event = await Event.find_one(Event.title == invitation.event_name, expr(Event.user).id == db_invitor.id)
if db_event is None:
log.error(f"Event {invitation.event_name!r} not found, failed to create invitation.")
return
db_invitation = await Invitation.find_one(
expr(Invitation.event).id == db_event.id,
expr(Invitation.invitee).id == db_invitee.id,
expr(Invitation.invitor).id == db_invitor.id,
)
if db_invitation is not None:
log.info(
f"Invitation for event {invitation.event_name!r} created by {invitation.invitor_username!r} "
f"for {invitation.invitee_username!r} already exists. Deleting it.",
)
_ = await db_invitation.delete()
invitation_data = InvitationCreateData(
event_id=cast(PydanticObjectId, db_event.id),
invitee_id=cast(PydanticObjectId, db_invitee.id),
)
_ = await invitation_data.create_invitation(db_invitor, create_notification=invitation.create_notificaton)
log.info(
f"Invitation for event {invitation.event_name!r} created by {invitation.invitor_username!r} "
f"for {invitation.invitee_username!r}.",
)
async def main() -> None:
"""Create a new user with configured credentials."""
db_session, _ = await initialize_db()
for user in USERS:
await make_user(user)
for category in CATEGORIES:
await make_category(category)
for event in EVENTS:
await make_event(event)
for invitation in INVITATIONS:
await make_invitation(invitation)
db_session.close()
if __name__ == "__main__":
asyncio.run(main())