Skip to content

Commit

Permalink
feat: Implement schedule history entities (M2-8417) (#1718)
Browse files Browse the repository at this point in the history
This ticket makes several updates to support the implementation of schedule versioning and schedule history.

First off, the `events` table has been updated with the following columns:
- **version** - a string in the format `YYYYMMDD-n`, where `n` is the nth change on a given date
- **periodicity** - the `type` column from the `periodicity` table
- **start_date** - the `start_date` column from the `periodicity` table
- **end_date** - the `end_date` column from the `periodicity` table
- **selected_date** - the `selected_date` column from the `periodicity` table

Only the **version** column is required to support schedule versioning, but merging the periodicity columns with the events table simplifies a lot of the querying logic.

A number of new tables have also been created according to the ER diagram in the ticket:
- **event_histories** - keeps track of event versions
- **applet_events** - keeps track of which versions of a schedule are related to which versions of an applet
- **notification_histories** - The notifications tied to a particular schedule version
- **reminder_histories** - The reminder tied to a particular schedule version

As part of this implementation, I decided to forego the creation of the `user_event_histories`, `activity_event_histories`, and `flow_event_histories` tables and include the `user_id`, `activity_id`, and `activity_flow_id` columns respectively directly in the `event_histories` table. This merging simplifies queries, since these would have been one-to-one relationships with no extra data.

Finally, I updated the various event data structures being returned from the backend to include the version property. Since no data is being migrated as part of this ticket, the version property will always be `null` for now.
  • Loading branch information
sultanofcardio authored Feb 6, 2025
1 parent cbf2869 commit 0ff23ae
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 43 deletions.
50 changes: 26 additions & 24 deletions src/apps/schedule/api/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async def schedule_get_by_id(
async with atomic(session):
await AppletService(session, user.id).exist_by_id(applet_id)
schedule = await ScheduleService(session).get_schedule_by_id(applet_id=applet_id, schedule_id=schedule_id)
return Response(result=PublicEvent(**schedule.dict()))
return Response(result=schedule)


async def schedule_get_all(
Expand All @@ -88,19 +88,20 @@ async def schedule_get_all(
is not provided, it will return only general events for the applet."""
async with atomic(session):
await AppletService(session, user.id).exist_by_id(applet_id)
schedules = await ScheduleService(session).get_all_schedules(applet_id, deepcopy(query_params))

roles: set = set(await UserAppletAccessCRUD(session).get_user_roles_to_applet(user.id, applet_id))
accessed_roles: set = {
Role.SUPER_ADMIN.value,
Role.OWNER.value,
Role.MANAGER.value,
Role.COORDINATOR.value,
}
if not roles & accessed_roles:
raise UserDoesNotHavePermissionError()

return ResponseMulti(result=schedules, count=len(schedules))
roles: set = set(await UserAppletAccessCRUD(session).get_user_roles_to_applet(user.id, applet_id))
accessed_roles: set = {
Role.SUPER_ADMIN.value,
Role.OWNER.value,
Role.MANAGER.value,
Role.COORDINATOR.value,
}
if not roles & accessed_roles:
raise UserDoesNotHavePermissionError()

public_events = await ScheduleService(session).get_all_schedules(applet_id, deepcopy(query_params))

return ResponseMulti(result=public_events, count=len(public_events))


async def public_schedule_get_all(
Expand Down Expand Up @@ -190,12 +191,11 @@ async def schedule_update(
await applet_service.exist_by_id(applet_id)
await CheckAccessService(session, user.id).check_applet_schedule_create_access(applet_id)
service = ScheduleService(session)
schedule = await service.update_schedule(applet_id, schedule_id, schema)
public_event = await service.update_schedule(applet_id, schedule_id, schema)

try:
respondent_ids = None
if schedule.respondent_id:
respondent_ids = [schedule.respondent_id]
if public_event.respondent_id:
respondent_ids = [public_event.respondent_id]
else:
respondent_ids = await service.get_default_respondents(applet_id)

Expand All @@ -211,7 +211,7 @@ async def schedule_update(
# mute error
logger.exception(e)

return Response(result=PublicEvent(**schedule.dict()))
return Response(result=public_event)


async def schedule_count(
Expand Down Expand Up @@ -259,9 +259,9 @@ async def schedule_get_all_by_user(
) -> ResponseMulti[PublicEventByUser]:
"""Get all schedules for a user."""
async with atomic(session):
schedules = await ScheduleService(session).get_events_by_user(user_id=user.id)
public_events_by_user = await ScheduleService(session).get_events_by_user(user_id=user.id)
count = await ScheduleService(session).count_events_by_user(user_id=user.id)
return ResponseMulti(result=schedules, count=count)
return ResponseMulti(result=public_events_by_user, count=count)


async def schedule_get_all_by_respondent_user(
Expand Down Expand Up @@ -290,13 +290,13 @@ async def schedule_get_all_by_respondent_user(
)
applet_ids: list[uuid.UUID] = [applet.id for applet in applets]

schedules = await ScheduleService(session).get_upcoming_events_by_user(
public_events_by_user = await ScheduleService(session).get_upcoming_events_by_user(
user_id=user.id,
applet_ids=applet_ids,
min_end_date=min_end_date,
max_start_date=max_start_date,
)
return ResponseMulti(result=schedules, count=len(schedules))
return ResponseMulti(result=public_events_by_user, count=len(public_events_by_user))


async def schedule_get_by_user(
Expand All @@ -307,8 +307,10 @@ async def schedule_get_by_user(
"""Get all schedules for a respondent per applet id."""
async with atomic(session):
await AppletService(session, user.id).exist_by_id(applet_id)
schedules = await ScheduleService(session).get_events_by_user_and_applet(user_id=user.id, applet_id=applet_id)
return Response(result=schedules)
public_event_by_user = await ScheduleService(session).get_events_by_user_and_applet(
user_id=user.id, applet_id=applet_id
)
return Response(result=public_event_by_user)


async def schedule_remove_individual_calendar(
Expand Down
4 changes: 4 additions & 0 deletions src/apps/schedule/crud/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ async def get_all_by_applet_and_user(self, applet_id: uuid.UUID, user_id: uuid.U
one_time_completion=row.EventSchema.one_time_completion,
timer=row.EventSchema.timer,
timer_type=row.EventSchema.timer_type,
version=row.EventSchema.version,
user_id=user_id,
periodicity=Periodicity(
id=row.EventSchema.periodicity_id,
Expand Down Expand Up @@ -274,6 +275,7 @@ async def get_all_by_applets_and_user(
one_time_completion=row.EventSchema.one_time_completion,
timer=row.EventSchema.timer,
timer_type=row.EventSchema.timer_type,
version=row.EventSchema.version,
user_id=user_id,
periodicity=Periodicity(
id=row.EventSchema.periodicity_id,
Expand Down Expand Up @@ -467,6 +469,7 @@ async def get_general_events_by_user(self, applet_id: uuid.UUID, user_id: uuid.U
one_time_completion=row.EventSchema.one_time_completion,
timer=row.EventSchema.timer,
timer_type=row.EventSchema.timer_type,
version=row.EventSchema.version,
user_id=user_id,
periodicity=Periodicity(
id=row.EventSchema.periodicity_id,
Expand Down Expand Up @@ -604,6 +607,7 @@ async def get_general_events_by_applets_and_user(
one_time_completion=row.EventSchema.one_time_completion,
timer=row.EventSchema.timer,
timer_type=row.EventSchema.timer_type,
version=row.EventSchema.version,
user_id=user_id,
periodicity=Periodicity(
id=row.EventSchema.periodicity_id,
Expand Down
84 changes: 72 additions & 12 deletions src/apps/schedule/db/schemas.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from sqlalchemy import Boolean, Column, Date, ForeignKey, Integer, Interval, String, Time, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.dialects.postgresql import ENUM, UUID

from infrastructure.database.base import Base
from infrastructure.database.mixins import HistoryAware


class PeriodicitySchema(Base):
Expand All @@ -13,19 +14,57 @@ class PeriodicitySchema(Base):
selected_date = Column(Date, nullable=True)


class EventSchema(Base):
__tablename__ = "events"

periodicity_id = Column(ForeignKey("periodicity.id", ondelete="RESTRICT"), nullable=False)
class _BaseEventSchema:
start_time = Column(Time, nullable=True)
end_time = Column(Time, nullable=True)
access_before_schedule = Column(Boolean, nullable=True)
one_time_completion = Column(Boolean, nullable=True)
timer = Column(Interval, nullable=True)
timer_type = Column(String(10), nullable=False) # NOT_SET, TIMER, IDLE
version = Column(String(13), nullable=True) # TODO: Remove nullable=True with M2-8494

# Periodicity columns
# TODO: Remove nullable=True with M2-8494
periodicity = Column(String(10), nullable=True) # Options: ONCE, DAILY, WEEKLY, WEEKDAYS, MONTHLY, ALWAYS
start_date = Column(Date, nullable=True)
end_date = Column(Date, nullable=True)
selected_date = Column(Date, nullable=True)


class EventSchema(_BaseEventSchema, Base):
__tablename__ = "events"

periodicity_id = Column(ForeignKey("periodicity.id", ondelete="RESTRICT"), nullable=False)
applet_id = Column(ForeignKey("applets.id", ondelete="CASCADE"), nullable=False)


class EventHistorySchema(_BaseEventSchema, HistoryAware, Base):
__tablename__ = "event_histories"

id_version = Column(String(), primary_key=True)
id = Column(UUID(as_uuid=True))
event_type = Column(ENUM("activity", "flow", name="event_type_enum", create_type=False), nullable=False)
activity_id = Column(UUID(as_uuid=True), nullable=True)
activity_flow_id = Column(UUID(as_uuid=True), nullable=True)
user_id = Column(ForeignKey("users.id", ondelete="RESTRICT"), nullable=True)


class AppletEventsSchema(Base):
__tablename__ = "applet_events"

applet_id = Column(ForeignKey("applet_histories.id_version", ondelete="CASCADE"), nullable=False)
event_id = Column(ForeignKey("event_histories.id_version", ondelete="CASCADE"), nullable=False)

__table_args__ = (
UniqueConstraint(
"applet_id",
"event_id",
"is_deleted",
name="_unique_applet_events",
),
)


class UserEventsSchema(Base):
__tablename__ = "user_events"

Expand Down Expand Up @@ -74,21 +113,42 @@ class FlowEventsSchema(Base):
)


class NotificationSchema(Base):
__tablename__ = "notifications"

event_id = Column(ForeignKey("events.id", ondelete="CASCADE"), nullable=False)
class _BaseNotificationSchema:
from_time = Column(Time, nullable=True)
to_time = Column(Time, nullable=True)
at_time = Column(Time, nullable=True)
trigger_type = Column(String(10), nullable=False) # fixed, random
order = Column(Integer, nullable=True)


class ReminderSchema(Base):
__tablename__ = "reminders"
class NotificationSchema(_BaseNotificationSchema, Base):
__tablename__ = "notifications"

event_id = Column(ForeignKey("events.id", ondelete="CASCADE"), nullable=False)

activity_incomplete = Column(Integer(), nullable=False)

class NotificationHistorySchema(_BaseNotificationSchema, HistoryAware, Base):
__tablename__ = "notification_histories"

id_version = Column(String(), primary_key=True)
id = Column(UUID(as_uuid=True))
event_id = Column(ForeignKey("event_histories.id_version", ondelete="RESTRICT"), nullable=False)


class _BaseReminderSchema:
activity_incomplete = Column(Integer, nullable=False)
reminder_time = Column(Time, nullable=False)


class ReminderSchema(_BaseReminderSchema, Base):
__tablename__ = "reminders"

event_id = Column(ForeignKey("events.id", ondelete="CASCADE"), nullable=False)


class ReminderHistorySchema(_BaseReminderSchema, HistoryAware, Base):
__tablename__ = "reminder_histories"

id_version = Column(String(), primary_key=True)
id = Column(UUID(as_uuid=True))
event_id = Column(ForeignKey("event_histories.id_version", ondelete="RESTRICT"), nullable=False)
2 changes: 2 additions & 0 deletions src/apps/schedule/domain/schedule/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class EventUpdate(EventCreate):

class Event(EventCreate, InternalModel):
id: uuid.UUID
version: str | None = None


class Periodicity(BasePeriodicity, InternalModel):
Expand Down Expand Up @@ -94,3 +95,4 @@ class EventFull(InternalModel, BaseEvent):
user_id: uuid.UUID | None = None
activity_id: uuid.UUID | None = None
flow_id: uuid.UUID | None = None
version: str | None = None
2 changes: 2 additions & 0 deletions src/apps/schedule/domain/schedule/public.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class PublicEvent(PublicModel, BaseEvent):
activity_id: uuid.UUID | None
flow_id: uuid.UUID | None
notification: PublicNotification | None = None
version: str | None = None


class ActivityEventCount(PublicModel):
Expand Down Expand Up @@ -122,6 +123,7 @@ class ScheduleEventDto(PublicModel):
timers: TimerDto
availabilityType: AvailabilityType
notificationSettings: NotificationDTO | None = None
version: str | None = None


class PublicEventByUser(PublicModel):
Expand Down
15 changes: 8 additions & 7 deletions src/apps/schedule/service/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ async def get_public_all_schedules(self, key: uuid.UUID) -> PublicEventByUser:
periodicity=periodicity,
activity_id=activity_id,
flow_id=flow_id,
version=event.version,
)
)

Expand Down Expand Up @@ -620,21 +621,20 @@ async def get_events_by_user(self, user_id: uuid.UUID) -> list[PublicEventByUser
roles=Role.as_list(),
query_params=QueryParams(),
)
applet_ids = [applet.id for applet in applets]
events = []

for applet_id in applet_ids:
user_events: list = await EventCRUD(self.session).get_all_by_applet_and_user(
applet_id=applet_id,
for applet in applets:
user_events = await EventCRUD(self.session).get_all_by_applet_and_user(
applet_id=applet.id,
user_id=user_id,
)
general_events: list = await EventCRUD(self.session).get_general_events_by_user(
applet_id=applet_id, user_id=user_id
general_events = await EventCRUD(self.session).get_general_events_by_user(
applet_id=applet.id, user_id=user_id
)
all_events = user_events + general_events
events.append(
PublicEventByUser(
applet_id=applet_id,
applet_id=applet.id,
events=[
self._convert_to_dto(
event=event,
Expand Down Expand Up @@ -794,6 +794,7 @@ def _convert_to_dto(
availability=availability,
selectedDate=event.periodicity.selected_date,
notificationSettings=notificationSettings,
version=event.version,
)

async def get_events_by_user_and_applet(self, user_id: uuid.UUID, applet_id: uuid.UUID) -> PublicEventByUser:
Expand Down
1 change: 1 addition & 0 deletions src/apps/schedule/tests/test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ async def test_respondent_schedules_get_user_two_weeks(self, client: TestClient,
"timers",
"availabilityType",
"notificationSettings",
"version",
}

async def test_schedule_get_user_by_applet(self, client: TestClient, applet: AppletFull, user: User):
Expand Down
Loading

0 comments on commit 0ff23ae

Please sign in to comment.