diff --git a/src/apps/schedule/api/schedule.py b/src/apps/schedule/api/schedule.py index 3c244e2884b..8290eb9da2a 100644 --- a/src/apps/schedule/api/schedule.py +++ b/src/apps/schedule/api/schedule.py @@ -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( @@ -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( @@ -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) @@ -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( @@ -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( @@ -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( @@ -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( diff --git a/src/apps/schedule/crud/events.py b/src/apps/schedule/crud/events.py index a5edb1d11e6..43f1b656902 100644 --- a/src/apps/schedule/crud/events.py +++ b/src/apps/schedule/crud/events.py @@ -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, @@ -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, @@ -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, @@ -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, diff --git a/src/apps/schedule/db/schemas.py b/src/apps/schedule/db/schemas.py index 1e30265d437..94cec833db1 100644 --- a/src/apps/schedule/db/schemas.py +++ b/src/apps/schedule/db/schemas.py @@ -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): @@ -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" @@ -74,10 +113,7 @@ 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) @@ -85,10 +121,34 @@ class NotificationSchema(Base): 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) diff --git a/src/apps/schedule/domain/schedule/internal.py b/src/apps/schedule/domain/schedule/internal.py index 2783af8d4e1..785cc0a12d2 100644 --- a/src/apps/schedule/domain/schedule/internal.py +++ b/src/apps/schedule/domain/schedule/internal.py @@ -33,6 +33,7 @@ class EventUpdate(EventCreate): class Event(EventCreate, InternalModel): id: uuid.UUID + version: str | None = None class Periodicity(BasePeriodicity, InternalModel): @@ -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 diff --git a/src/apps/schedule/domain/schedule/public.py b/src/apps/schedule/domain/schedule/public.py index 13658965c15..2bb95d6f975 100644 --- a/src/apps/schedule/domain/schedule/public.py +++ b/src/apps/schedule/domain/schedule/public.py @@ -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): @@ -122,6 +123,7 @@ class ScheduleEventDto(PublicModel): timers: TimerDto availabilityType: AvailabilityType notificationSettings: NotificationDTO | None = None + version: str | None = None class PublicEventByUser(PublicModel): diff --git a/src/apps/schedule/service/schedule.py b/src/apps/schedule/service/schedule.py index c1a5018a747..f69a1a09d7b 100644 --- a/src/apps/schedule/service/schedule.py +++ b/src/apps/schedule/service/schedule.py @@ -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, ) ) @@ -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, @@ -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: diff --git a/src/apps/schedule/tests/test_schedule.py b/src/apps/schedule/tests/test_schedule.py index 90e5f072e43..74230ac896b 100644 --- a/src/apps/schedule/tests/test_schedule.py +++ b/src/apps/schedule/tests/test_schedule.py @@ -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): diff --git a/src/infrastructure/database/migrations/versions/2025_01_23_01_36-create_schedule_history_tables.py b/src/infrastructure/database/migrations/versions/2025_01_23_01_36-create_schedule_history_tables.py new file mode 100644 index 00000000000..718dc0610f1 --- /dev/null +++ b/src/infrastructure/database/migrations/versions/2025_01_23_01_36-create_schedule_history_tables.py @@ -0,0 +1,167 @@ +"""Create schedule history tables + +Revision ID: 62b491c18ace +Revises: dc2dd9e195d5 +Create Date: 2025-01-22 01:36:53.968076 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "62b491c18ace" +down_revision = "032d8458aa63" +branch_labels = None +depends_on = None + +EVENT_TYPE_ENUM = 'event_type_enum' +EVENT_TYPE_ENUM_VALUES = ['activity', 'flow'] + +def upgrade() -> None: + # Create the event_type enum + op.execute( + f""" + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = '{EVENT_TYPE_ENUM}') THEN + CREATE TYPE {EVENT_TYPE_ENUM} AS ENUM ({', '.join(f"'{value}'" for value in EVENT_TYPE_ENUM_VALUES)}); + END IF; + END $$; + """ + ) + + # Create the `event_histories` table + op.create_table( + "event_histories", + sa.Column("is_deleted", sa.Boolean(), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("timezone('utc', now())"), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("timezone('utc', now())"), nullable=True), + sa.Column("migrated_date", sa.DateTime(), nullable=True), + sa.Column("migrated_updated", sa.DateTime(), nullable=True), + sa.Column("start_time", sa.Time(), nullable=True), + sa.Column("end_time", sa.Time(), nullable=True), + sa.Column("access_before_schedule", sa.Boolean(), nullable=True), + sa.Column("one_time_completion", sa.Boolean(), nullable=True), + sa.Column("timer", sa.Interval(), nullable=True), + sa.Column("timer_type", sa.String(length=10), nullable=False), + sa.Column("version", sa.String(length=13), nullable=False), + sa.Column("periodicity", sa.String(length=10), nullable=False), + sa.Column("start_date", sa.Date(), nullable=True), + sa.Column("end_date", sa.Date(), nullable=True), + sa.Column("selected_date", sa.Date(), nullable=True), + sa.Column("id_version", sa.String(), nullable=False), + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column( + "event_type", + postgresql.ENUM(*EVENT_TYPE_ENUM_VALUES, name=EVENT_TYPE_ENUM, create_type=False), + nullable=False + ), + sa.Column("activity_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("activity_flow_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint( + ["user_id"], ["users.id"], name=op.f("fk_event_histories_user_id_users"), ondelete="RESTRICT" + ), + sa.PrimaryKeyConstraint("id_version", name=op.f("pk_event_histories")), + ) + + # Create the `applet_events` table + op.create_table( + "applet_events", + sa.Column("is_deleted", sa.Boolean(), nullable=True), + sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("timezone('utc', now())"), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("timezone('utc', now())"), nullable=True), + sa.Column("migrated_date", sa.DateTime(), nullable=True), + sa.Column("migrated_updated", sa.DateTime(), nullable=True), + sa.Column("applet_id", sa.String(), nullable=False), + sa.Column("event_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["applet_id"], + ["applet_histories.id_version"], + name=op.f("fk_applet_events_applet_id_applet_histories"), + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["event_id"], + ["event_histories.id_version"], + name=op.f("fk_applet_events_event_id_event_histories"), + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_applet_events")), + sa.UniqueConstraint("applet_id", "event_id", "is_deleted", name="_unique_applet_events"), + ) + + # Create the `notification_histories` table + op.create_table( + "notification_histories", + sa.Column("is_deleted", sa.Boolean(), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("timezone('utc', now())"), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("timezone('utc', now())"), nullable=True), + sa.Column("migrated_date", sa.DateTime(), nullable=True), + sa.Column("migrated_updated", sa.DateTime(), nullable=True), + sa.Column("from_time", sa.Time(), nullable=True), + sa.Column("to_time", sa.Time(), nullable=True), + sa.Column("at_time", sa.Time(), nullable=True), + sa.Column("trigger_type", sa.String(length=10), nullable=False), + sa.Column("order", sa.Integer(), nullable=True), + sa.Column("id_version", sa.String(), nullable=False), + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("event_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["event_id"], + ["event_histories.id_version"], + name=op.f("fk_notification_histories_event_id_event_histories"), + ondelete="RESTRICT", + ), + sa.PrimaryKeyConstraint("id_version", name=op.f("pk_notification_histories")), + ) + + # Create the `reminder_histories` table + op.create_table( + "reminder_histories", + sa.Column("is_deleted", sa.Boolean(), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("timezone('utc', now())"), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("timezone('utc', now())"), nullable=True), + sa.Column("migrated_date", sa.DateTime(), nullable=True), + sa.Column("migrated_updated", sa.DateTime(), nullable=True), + sa.Column("activity_incomplete", sa.Integer(), nullable=False), + sa.Column("reminder_time", sa.Time(), nullable=False), + sa.Column("id_version", sa.String(), nullable=False), + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("event_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["event_id"], + ["event_histories.id_version"], + name=op.f("fk_reminder_histories_event_id_event_histories"), + ondelete="RESTRICT", + ), + sa.PrimaryKeyConstraint("id_version", name=op.f("pk_reminder_histories")), + ) + + # Update the `events` table + op.add_column("events", sa.Column("version", sa.String(length=13), nullable=True)) + op.add_column("events", sa.Column("periodicity", sa.String(length=10), nullable=True)) + op.add_column("events", sa.Column("start_date", sa.Date(), nullable=True)) + op.add_column("events", sa.Column("end_date", sa.Date(), nullable=True)) + op.add_column("events", sa.Column("selected_date", sa.Date(), nullable=True)) + + +def downgrade() -> None: + # Drop the new columns from the `events` table + op.drop_column("events", "selected_date") + op.drop_column("events", "end_date") + op.drop_column("events", "start_date") + op.drop_column("events", "periodicity") + op.drop_column("events", "version") + + # Drop the new tables + op.drop_table("reminder_histories") + op.drop_table("notification_histories") + op.drop_table("applet_events") + op.drop_table("event_histories") + + # Drop the event_type enum + op.execute(f"DROP TYPE IF EXISTS {EVENT_TYPE_ENUM};")