From 537c8e9fc24f1981e53914967b86bf06833d24b4 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 19 Sep 2025 09:04:02 +0300 Subject: [PATCH 1/7] Add permission --- backend/apps/nest/auth/calendar_events.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 backend/apps/nest/auth/calendar_events.py diff --git a/backend/apps/nest/auth/calendar_events.py b/backend/apps/nest/auth/calendar_events.py new file mode 100644 index 0000000000..ef5919a503 --- /dev/null +++ b/backend/apps/nest/auth/calendar_events.py @@ -0,0 +1,13 @@ +"""Nest calendar events authorization permissions.""" + +from apps.owasp.models.entity_member import EntityMember +from apps.slack.models.member import Member + + +def has_calendar_events_permission(slack_user_id: str): + """Check if a user has permission to access calendar events feature.""" + try: + member = Member.objects.get(slack_user_id=slack_user_id) + except Member.DoesNotExist: + return False + return EntityMember.objects.filter(member=member.user, role=EntityMember.Role.LEADER).exists() From 9495b0c8d370993714fd92e8fa5c633979bc3e8f Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 24 Sep 2025 19:54:32 +0300 Subject: [PATCH 2/7] Add send_and_delete to the scheduler classes to have one scheduled job only for the once recurring reminder --- .../nest/schedulers/calendar_events/base.py | 20 +++++++++---------- .../nest/schedulers/calendar_events/slack.py | 10 ++++++++++ .../schedulers/calendar_events/base_test.py | 18 ++++++++++------- .../schedulers/calendar_events/slack_test.py | 12 +++++++++++ 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/backend/apps/nest/schedulers/calendar_events/base.py b/backend/apps/nest/schedulers/calendar_events/base.py index 5a7ac95d9a..ab495575b7 100644 --- a/backend/apps/nest/schedulers/calendar_events/base.py +++ b/backend/apps/nest/schedulers/calendar_events/base.py @@ -1,6 +1,5 @@ """Base Scheduler for Nest Calendar Events.""" -from django.utils import timezone from django_rq import get_scheduler from apps.nest.models.reminder_schedule import ReminderSchedule @@ -19,16 +18,11 @@ def schedule(self): if self.reminder_schedule.recurrence == ReminderSchedule.Recurrence.ONCE: self.reminder_schedule.job_id = self.scheduler.enqueue_at( self.reminder_schedule.scheduled_time, - self.__class__.send_message, + self.__class__.send_and_delete, message=self.reminder_schedule.reminder.message, channel_id=self.reminder_schedule.reminder.channel_id, + reminder_schedule_id=self.reminder_schedule.pk, ).get_id() - - # Schedule deletion of the reminder after sending the message - self.scheduler.enqueue_at( - self.reminder_schedule.scheduled_time + timezone.timedelta(minutes=1), - self.reminder_schedule.reminder.delete, - ) else: self.reminder_schedule.job_id = self.scheduler.cron( self.reminder_schedule.cron_expression, @@ -52,8 +46,8 @@ def cancel(self): self.reminder_schedule.reminder.delete() @staticmethod - def send_message(message: str, channel_id: str): - """Send message to the specified channel. To be implemented by subclasses.""" + def send_and_delete(message: str, channel_id: str, reminder_schedule_id: int): + """Send message to the specified channel and delete the reminder.""" error_message = "Subclasses must implement this method." raise NotImplementedError(error_message) @@ -62,3 +56,9 @@ def send_and_update(message: str, channel_id: str, reminder_schedule_id: int): """Send message and update the reminder schedule.""" error_message = "Subclasses must implement this method." raise NotImplementedError(error_message) + + @staticmethod + def send_message(message: str, channel_id: str): + """Send message to the specified channel.""" + error_message = "Subclasses must implement this method." + raise NotImplementedError(error_message) diff --git a/backend/apps/nest/schedulers/calendar_events/slack.py b/backend/apps/nest/schedulers/calendar_events/slack.py index 6e7f9d50ae..b909d9c06e 100644 --- a/backend/apps/nest/schedulers/calendar_events/slack.py +++ b/backend/apps/nest/schedulers/calendar_events/slack.py @@ -17,6 +17,16 @@ def send_message(message: str, channel_id: str): text=message, ) + @staticmethod + def send_and_delete(message: str, channel_id: str, reminder_schedule_id: int): + """Send message to the specified channel and delete the reminder.""" + # Import here to avoid circular import issues + from apps.nest.models.reminder_schedule import ReminderSchedule + + SlackScheduler.send_message(message, channel_id) + if reminder_schedule := ReminderSchedule.objects.filter(pk=reminder_schedule_id).first(): + reminder_schedule.reminder.delete() + @staticmethod def send_and_update(message: str, channel_id: str, reminder_schedule_id: int): """Send message and update the reminder schedule.""" diff --git a/backend/tests/apps/nest/schedulers/calendar_events/base_test.py b/backend/tests/apps/nest/schedulers/calendar_events/base_test.py index 76533d276c..3fcad12601 100644 --- a/backend/tests/apps/nest/schedulers/calendar_events/base_test.py +++ b/backend/tests/apps/nest/schedulers/calendar_events/base_test.py @@ -32,6 +32,7 @@ def test_schedule_once(self, mock_get_scheduler): mock_reminder_schedule.scheduled_time = timezone.datetime(2024, 10, 10, 10, 0, 0) mock_reminder_schedule.reminder.message = "Test Message" mock_reminder_schedule.reminder.channel_id = "C123456" + mock_reminder_schedule.pk = 4 scheduler_instance = MagicMock() mock_get_scheduler.return_value = scheduler_instance @@ -39,15 +40,12 @@ def test_schedule_once(self, mock_get_scheduler): base_scheduler = BaseScheduler(reminder_schedule=mock_reminder_schedule) base_scheduler.schedule() - scheduler_instance.enqueue_at.assert_any_call( + scheduler_instance.enqueue_at.assert_called_once_with( mock_reminder_schedule.scheduled_time, - BaseScheduler.send_message, + BaseScheduler.send_and_delete, message="Test Message", channel_id="C123456", - ) - scheduler_instance.enqueue_at.assert_any_call( - mock_reminder_schedule.scheduled_time + timezone.timedelta(minutes=1), - mock_reminder_schedule.reminder.delete, + reminder_schedule_id=mock_reminder_schedule.pk, ) mock_reminder_schedule.save.assert_called_once_with(update_fields=["job_id"]) @@ -99,8 +97,14 @@ def test_send_message_not_implemented(self): BaseScheduler.send_message("Test Message", "C123456") assert str(exc_info.value) == "Subclasses must implement this method." + def test_send_and_delete_not_implemented(self): + """Test that send_and_delete raises NotImplementedError.""" + with pytest.raises(NotImplementedError) as exc_info: + BaseScheduler.send_and_delete("Test Message", "C123456", 4) + assert str(exc_info.value) == "Subclasses must implement this method." + def test_send_and_update_not_implemented(self): """Test that send_and_update raises NotImplementedError.""" with pytest.raises(NotImplementedError) as exc_info: - BaseScheduler.send_and_update("Test Message", "C123456", MagicMock()) + BaseScheduler.send_and_update("Test Message", "C123456", 4) assert str(exc_info.value) == "Subclasses must implement this method." diff --git a/backend/tests/apps/nest/schedulers/calendar_events/slack_test.py b/backend/tests/apps/nest/schedulers/calendar_events/slack_test.py index 3ba0c16e1b..ccb98d7ed1 100644 --- a/backend/tests/apps/nest/schedulers/calendar_events/slack_test.py +++ b/backend/tests/apps/nest/schedulers/calendar_events/slack_test.py @@ -20,6 +20,18 @@ def test_send_message(self, mock_slack_config): text="Test Message", ) + @patch("apps.nest.schedulers.calendar_events.slack.SlackScheduler.send_message") + @patch("apps.nest.models.reminder_schedule.ReminderSchedule.objects.filter") + def test_send_and_delete(self, mock_filter, mock_send_message): + """Test sending a message and deleting it via Slack.""" + mock_schedule = MagicMock() + mock_filter.return_value.first.return_value = mock_schedule + SlackScheduler.send_and_delete("Test Message", "C123456", 4) + + mock_send_message.assert_called_once_with("Test Message", "C123456") + mock_filter.assert_called_once_with(pk=4) + mock_schedule.reminder.delete.assert_called_once() + @patch("apps.nest.schedulers.calendar_events.slack.SlackScheduler.send_message") @patch("apps.nest.schedulers.calendar_events.slack.update_reminder_schedule_date") def test_send_and_update(self, mock_update_reminder_schedule_date, mock_send_message): From eb90c6b2059d5069f556105eeae238e624033c0b Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 24 Sep 2025 21:05:30 +0300 Subject: [PATCH 3/7] Update and add tests --- .../slack/common/handlers/calendar_events.py | 25 +++ .../common/handlers/calendar_events_test.py | 164 ++++++++++++++++-- 2 files changed, 179 insertions(+), 10 deletions(-) diff --git a/backend/apps/slack/common/handlers/calendar_events.py b/backend/apps/slack/common/handlers/calendar_events.py index 8054b764ff..646d320da8 100644 --- a/backend/apps/slack/common/handlers/calendar_events.py +++ b/backend/apps/slack/common/handlers/calendar_events.py @@ -8,12 +8,17 @@ from apps.common.constants import NL from apps.slack.blocks import get_pagination_buttons, markdown +PERMISSIONS_BLOCK = [markdown("*You do not have the permission to access calendar events.*")] + def get_cancel_reminder_blocks(reminder_schedule_id: int, slack_user_id: str) -> list[dict]: """Get the blocks for canceling a reminder.""" + from apps.nest.auth.calendar_events import has_calendar_events_permission from apps.nest.models.reminder_schedule import ReminderSchedule from apps.nest.schedulers.calendar_events.slack import SlackScheduler + if not has_calendar_events_permission(slack_user_id): + return PERMISSIONS_BLOCK try: reminder_schedule = ReminderSchedule.objects.get(pk=reminder_schedule_id) except ReminderSchedule.DoesNotExist: @@ -31,18 +36,26 @@ def get_cancel_reminder_blocks(reminder_schedule_id: int, slack_user_id: str) -> def get_events_blocks(slack_user_id: str, presentation, page: int = 1) -> list[dict]: """Get Google Calendar events blocks for Slack home view.""" + from apps.nest.auth.calendar_events import has_calendar_events_permission from apps.nest.clients.google_calendar import GoogleCalendarClient from apps.nest.models.google_account_authorization import GoogleAccountAuthorization from apps.owasp.models.event import Event + if not has_calendar_events_permission(slack_user_id): + return PERMISSIONS_BLOCK + + # Authorize with Google auth = GoogleAccountAuthorization.authorize(slack_user_id) + # If not authorized, we will get a tuple with the authorization URL if not isinstance(auth, GoogleAccountAuthorization): return [markdown(f"*Please sign in with Google first through this <{auth[0]}|link>*")] try: + # Get a 24-hour window of events client = GoogleCalendarClient(auth) min_time = timezone.now() + timezone.timedelta(days=(page - 1)) max_time = min_time + timezone.timedelta(days=1) events = client.get_events(min_time=min_time, max_time=max_time) + # Catch network errors except ServerNotFoundError: return [markdown("*Please check your internet connection.*")] parsed_events = Event.parse_google_calendar_events(events) @@ -58,7 +71,11 @@ def get_events_blocks(slack_user_id: str, presentation, page: int = 1) -> list[d ) ] for i, event in enumerate(parsed_events): + # We will need this number later to set reminders + # We are multiplying by 1000 to avoid collisions between pages + # as we don't get the length of events list from Google Calendar API. event_number = (i + 1) + 1000 * (page - 1) + # We will show the user the event number and cache the event ID for later use. cache.set(f"{slack_user_id}_{event_number}", event.google_calendar_id, timeout=3600) blocks.append( markdown( @@ -82,8 +99,12 @@ def get_events_blocks(slack_user_id: str, presentation, page: int = 1) -> list[d def get_reminders_blocks(slack_user_id: str) -> list[dict]: """Get reminders blocks for Slack home view.""" + from apps.nest.auth.calendar_events import has_calendar_events_permission from apps.nest.models.reminder_schedule import ReminderSchedule + if not has_calendar_events_permission(slack_user_id): + return PERMISSIONS_BLOCK + reminders_schedules = ReminderSchedule.objects.filter( reminder__member__slack_user_id=slack_user_id, ).order_by("scheduled_time") @@ -106,9 +127,13 @@ def get_reminders_blocks(slack_user_id: str) -> list[dict]: def get_setting_reminder_blocks(args, slack_user_id: str) -> list[dict]: """Get the blocks for setting a reminder.""" + from apps.nest.auth.calendar_events import has_calendar_events_permission from apps.nest.handlers.calendar_events import set_reminder from apps.nest.schedulers.calendar_events.slack import SlackScheduler + if not has_calendar_events_permission(slack_user_id): + return PERMISSIONS_BLOCK + try: reminder_schedule = set_reminder( channel=args.channel, diff --git a/backend/tests/apps/slack/common/handlers/calendar_events_test.py b/backend/tests/apps/slack/common/handlers/calendar_events_test.py index 3cf6332a03..27f5979465 100644 --- a/backend/tests/apps/slack/common/handlers/calendar_events_test.py +++ b/backend/tests/apps/slack/common/handlers/calendar_events_test.py @@ -12,6 +12,7 @@ from apps.nest.models.reminder_schedule import ReminderSchedule from apps.owasp.models.event import Event from apps.slack.common.handlers.calendar_events import ( + get_cancel_reminder_blocks, get_events_blocks, get_reminders_blocks, get_setting_reminder_blocks, @@ -19,16 +20,91 @@ from apps.slack.common.presentation import EntityPresentation from apps.slack.models.member import Member +PERMISSION_STRING = "*You do not have the permission to access calendar events.*" + class TestCalendarEvents: """Slack calendar events handlers test cases.""" + @patch("apps.nest.schedulers.calendar_events.slack.SlackScheduler") + @patch("apps.nest.models.reminder_schedule.ReminderSchedule.objects.get") + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_cancel_reminder_blocks_success( + self, mock_has_permission, mock_get, mock_slack_scheduler + ): + """Test get_cancel_reminder_blocks function for successful cancellation.""" + mock_reminder_schedule = MagicMock( + spec=ReminderSchedule, + pk=1, + reminder=MagicMock( + spec=Reminder, + channel_id="C123456", + message="Test reminder message", + member=MagicMock(slack_user_id="test_slack_user_id", spec=Member), + event=MagicMock(spec=Event, name="Test Event"), + ), + ) + mock_get.return_value = mock_reminder_schedule + mock_has_permission.return_value = True + blocks = get_cancel_reminder_blocks(1, "test_slack_user_id") + assert len(blocks) == 1 + assert "*Canceled the reminder for event" in blocks[0]["text"]["text"] + mock_get.assert_called_once_with(pk=1) + mock_slack_scheduler.assert_called_once_with(mock_reminder_schedule) + mock_slack_scheduler.return_value.cancel.assert_called_once() + mock_has_permission.assert_called_once_with("test_slack_user_id") + + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_cancel_reminder_blocks_no_permission(self, mock_has_permission): + """Test get_cancel_reminder_blocks function when no permission.""" + mock_has_permission.return_value = False + blocks = get_cancel_reminder_blocks(1, "test_slack_user_id") + assert len(blocks) == 1 + assert PERMISSION_STRING in blocks[0]["text"]["text"] + mock_has_permission.assert_called_once_with("test_slack_user_id") + + @patch("apps.nest.models.reminder_schedule.ReminderSchedule.objects.get") + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_cancel_reminder_blocks_invalid_id(self, mock_has_permission, mock_get): + """Test get_cancel_reminder_blocks function when invalid reminder ID.""" + mock_get.side_effect = ReminderSchedule.DoesNotExist + mock_has_permission.return_value = True + blocks = get_cancel_reminder_blocks(99, "test_slack_user_id") + assert len(blocks) == 1 + assert "*Please provide a valid reminder number.*" in blocks[0]["text"]["text"] + mock_get.assert_called_once_with(pk=99) + mock_has_permission.assert_called_once_with("test_slack_user_id") + + @patch("apps.nest.models.reminder_schedule.ReminderSchedule.objects.get") + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_cancel_reminder_blocks_not_owner(self, mock_has_permission, mock_get): + """Test get_cancel_reminder_blocks function when user is not the owner of the reminder.""" + mock_reminder_schedule = MagicMock( + spec=ReminderSchedule, + pk=1, + reminder=MagicMock( + spec=Reminder, + channel_id="C123456", + message="Test reminder message", + member=MagicMock(slack_user_id="other_slack_user_id", spec=Member), + event=MagicMock(spec=Event, name="Test Event"), + ), + ) + mock_get.return_value = mock_reminder_schedule + mock_has_permission.return_value = True + blocks = get_cancel_reminder_blocks(1, "test_slack_user_id") + assert len(blocks) == 1 + assert "*You can only cancel your own reminders.*" in blocks[0]["text"]["text"] + mock_get.assert_called_once_with(pk=1) + mock_has_permission.assert_called_once_with("test_slack_user_id") + @override_settings(IS_GOOGLE_AUTH_ENABLED=True, IS_AWS_KMS_ENABLED=True) @patch("apps.nest.clients.google_calendar.GoogleCalendarClient") @patch("apps.nest.models.google_account_authorization.GoogleAccountAuthorization.authorize") @patch("apps.owasp.models.event.Event.parse_google_calendar_events") + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") def test_get_events_blocks( - self, mock_parse_events, mock_authorize, mock_google_calendar_client + self, mock_has_permission, mock_parse_events, mock_authorize, mock_google_calendar_client ): """Test get_events_blocks function.""" auth = GoogleAccountAuthorization( @@ -36,6 +112,7 @@ def test_get_events_blocks( refresh_token="test_refresh_token", # noqa: S106 expires_at=timezone.now() + timezone.timedelta(hours=1), ) + mock_has_permission.return_value = True mock_authorize.return_value = auth mock_google_calendar_client.return_value.get_events.return_value = [ { @@ -79,12 +156,26 @@ def test_get_events_blocks( assert blocks[3]["type"] == "actions" mock_google_calendar_client.assert_called_once_with(auth) mock_google_calendar_client.return_value.get_events.assert_called_once() + mock_has_permission.assert_called_once_with("test_slack_user_id") + + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_events_blocks_no_permission(self, mock_has_permission): + """Test get_events_blocks function when no permission.""" + mock_has_permission.return_value = False + blocks = get_events_blocks( + "test_slack_user_id", presentation=EntityPresentation(include_pagination=True), page=1 + ) + assert len(blocks) == 1 + assert PERMISSION_STRING in blocks[0]["text"]["text"] + mock_has_permission.assert_called_once_with("test_slack_user_id") @override_settings(IS_GOOGLE_AUTH_ENABLED=True, IS_AWS_KMS_ENABLED=True) @patch("apps.nest.models.google_account_authorization.GoogleAccountAuthorization.authorize") - def test_get_events_blocks_no_auth(self, mock_authorize): + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_events_blocks_no_auth(self, mock_has_permission, mock_authorize): """Test get_events_blocks function when no authorization.""" mock_authorize.return_value = ("http://auth.url", "state123") # NOSONAR + mock_has_permission.return_value = True blocks = get_events_blocks( "test_slack_user_id", presentation=EntityPresentation(include_pagination=True), page=1 ) @@ -97,13 +188,17 @@ def test_get_events_blocks_no_auth(self, mock_authorize): @override_settings(IS_GOOGLE_AUTH_ENABLED=True, IS_AWS_KMS_ENABLED=True) @patch("apps.nest.clients.google_calendar.GoogleCalendarClient") @patch("apps.nest.models.google_account_authorization.GoogleAccountAuthorization.authorize") - def test_get_events_blocks_service_error(self, mock_authorize, mock_google_calendar_client): + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_events_blocks_service_error( + self, mock_has_permission, mock_authorize, mock_google_calendar_client + ): """Test get_events_blocks function when Google Calendar service error.""" mock_authorize.return_value = GoogleAccountAuthorization( access_token="test_access_token", # noqa: S106 refresh_token="test_refresh_token", # noqa: S106 expires_at=timezone.now() + timezone.timedelta(hours=1), ) + mock_has_permission.return_value = True mock_google_calendar_client.side_effect = ServerNotFoundError() blocks = get_events_blocks( "test_slack_user_id", presentation=EntityPresentation(include_pagination=True), page=1 @@ -114,13 +209,17 @@ def test_get_events_blocks_service_error(self, mock_authorize, mock_google_calen @override_settings(IS_GOOGLE_AUTH_ENABLED=True, IS_AWS_KMS_ENABLED=True) @patch("apps.nest.clients.google_calendar.GoogleCalendarClient") @patch("apps.nest.models.google_account_authorization.GoogleAccountAuthorization.authorize") - def test_get_events_blocks_no_events(self, mock_authorize, mock_google_calendar_client): + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_events_blocks_no_events( + self, mock_has_permission, mock_authorize, mock_google_calendar_client + ): """Test get_events_blocks function when no events.""" mock_authorize.return_value = GoogleAccountAuthorization( access_token="test_access_token", # noqa: S106 refresh_token="test_refresh_token", # noqa: S106 expires_at=timezone.now() + timezone.timedelta(hours=1), ) + mock_has_permission.return_value = True mock_google_calendar_client.return_value.get_events.return_value = [] blocks = get_events_blocks( "test_slack_user_id", presentation=EntityPresentation(include_pagination=True), page=1 @@ -129,8 +228,10 @@ def test_get_events_blocks_no_events(self, mock_authorize, mock_google_calendar_ assert "*No upcoming calendar events found.*" in blocks[0]["text"]["text"] @patch("apps.nest.models.reminder_schedule.ReminderSchedule.objects.filter") - def test_get_reminders_blocks_success(self, mock_filter): + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_reminders_blocks_success(self, mock_has_permission, mock_filter): """Test get_reminders_blocks function.""" + mock_has_permission.return_value = True mock_filter.return_value.order_by.return_value = [ MagicMock( spec=ReminderSchedule, @@ -162,11 +263,23 @@ def test_get_reminders_blocks_success(self, mock_filter): assert "Another reminder message" in blocks[2]["text"]["text"] mock_filter.assert_called_once_with(reminder__member__slack_user_id="test_slack_user_id") mock_filter.return_value.order_by.assert_called_once_with("scheduled_time") + mock_has_permission.assert_called_once_with("test_slack_user_id") + + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_reminders_blocks_no_permission(self, mock_has_permission): + """Test get_reminders_blocks function when no permission.""" + mock_has_permission.return_value = False + blocks = get_reminders_blocks("test_slack_user_id") + assert len(blocks) == 1 + assert PERMISSION_STRING in blocks[0]["text"]["text"] + mock_has_permission.assert_called_once_with("test_slack_user_id") @patch("apps.nest.models.reminder_schedule.ReminderSchedule.objects.filter") - def test_get_reminders_blocks_no_reminders(self, mock_filter): + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_reminders_blocks_no_reminders(self, mock_has_permission, mock_filter): """Test get_reminders_blocks function when no reminders.""" mock_filter.return_value.order_by.return_value = [] + mock_has_permission.return_value = True blocks = get_reminders_blocks("test_slack_user_id") assert len(blocks) == 1 assert ( @@ -178,7 +291,10 @@ def test_get_reminders_blocks_no_reminders(self, mock_filter): @patch("apps.nest.handlers.calendar_events.set_reminder") @patch("apps.nest.schedulers.calendar_events.slack.SlackScheduler") - def test_get_setting_reminder_blocks_success(self, mock_slack_scheduler, mock_set_reminder): + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_setting_reminder_blocks_success( + self, mock_has_permission, mock_slack_scheduler, mock_set_reminder + ): """Test get_setting_reminder_blocks function for successful reminder setting.""" mock_set_reminder.return_value = MagicMock( spec=ReminderSchedule, @@ -199,6 +315,7 @@ def test_get_setting_reminder_blocks_success(self, mock_slack_scheduler, mock_se message=["Test", "reminder", "message"], recurrence="daily", ) + mock_has_permission.return_value = True blocks = get_setting_reminder_blocks(args, "test_slack_user_id") assert len(blocks) == 1 assert "*10-minute reminder set for event " in blocks[0]["text"]["text"] @@ -213,9 +330,29 @@ def test_get_setting_reminder_blocks_success(self, mock_slack_scheduler, mock_se mock_slack_scheduler.assert_called_once_with(mock_set_reminder.return_value) mock_slack_scheduler.return_value.schedule.assert_called_once() mock_slack_scheduler.send_message.assert_called_once() + mock_has_permission.assert_called_once_with("test_slack_user_id") + + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_setting_reminder_blocks_no_permission(self, mock_has_permission): + """Test get_setting_reminder_blocks function when no permission.""" + mock_has_permission.return_value = False + args = MagicMock( + channel="C123456", + event_number=1, + minutes_before=10, + message=["Test", "reminder", "message"], + recurrence="daily", + ) + blocks = get_setting_reminder_blocks(args, "test_slack_user_id") + assert len(blocks) == 1 + assert PERMISSION_STRING in blocks[0]["text"]["text"] + mock_has_permission.assert_called_once_with("test_slack_user_id") @patch("apps.nest.handlers.calendar_events.set_reminder") - def test_get_setting_reminder_blocks_validation_error(self, mock_set_reminder): + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_setting_reminder_blocks_validation_error( + self, mock_has_permission, mock_set_reminder + ): """Test get_setting_reminder_blocks function when ValidationError is raised.""" mock_set_reminder.side_effect = ValidationError("Invalid event number.") args = MagicMock( @@ -225,14 +362,17 @@ def test_get_setting_reminder_blocks_validation_error(self, mock_set_reminder): message=["Test", "reminder", "message"], recurrence="daily", ) + mock_has_permission.return_value = True blocks = get_setting_reminder_blocks(args, "test_slack_user_id") assert len(blocks) == 1 assert "*Invalid event number.*" in blocks[0]["text"]["text"] @patch("apps.nest.handlers.calendar_events.set_reminder") - def test_get_setting_reminder_blocks_value_error(self, mock_set_reminder): + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_setting_reminder_blocks_value_error(self, mock_has_permission, mock_set_reminder): """Test get_setting_reminder_blocks function when ValueError is raised.""" mock_set_reminder.side_effect = ValueError("Some value error occurred.") + mock_has_permission.return_value = True args = MagicMock( channel="C123456", event_number=1, @@ -245,8 +385,12 @@ def test_get_setting_reminder_blocks_value_error(self, mock_set_reminder): assert "*Some value error occurred.*" in blocks[0]["text"]["text"] @patch("apps.nest.handlers.calendar_events.set_reminder") - def test_get_setting_reminder_blocks_service_error(self, mock_set_reminder): + @patch("apps.nest.auth.calendar_events.has_calendar_events_permission") + def test_get_setting_reminder_blocks_service_error( + self, mock_has_permission, mock_set_reminder + ): """Test get_setting_reminder_blocks function when service error occurs.""" + mock_has_permission.return_value = True mock_set_reminder.side_effect = ServerNotFoundError() args = MagicMock( channel="C123456", From c4628fcd151cb1fe8cb66d5b83bcbcdebc31a16e Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 25 Sep 2025 11:56:58 +0300 Subject: [PATCH 4/7] Add tests for permessions --- .../apps/nest/auth/calendar_events_test.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 backend/tests/apps/nest/auth/calendar_events_test.py diff --git a/backend/tests/apps/nest/auth/calendar_events_test.py b/backend/tests/apps/nest/auth/calendar_events_test.py new file mode 100644 index 0000000000..41477c1644 --- /dev/null +++ b/backend/tests/apps/nest/auth/calendar_events_test.py @@ -0,0 +1,32 @@ +"""Test cases for Nest Calendar Events permissions.""" + +from unittest.mock import MagicMock, patch + +from apps.nest.auth.calendar_events import has_calendar_events_permission +from apps.owasp.models.entity_member import EntityMember +from apps.slack.models.member import Member + + +class TestCalendarEventsPermissions: + """Test cases for Nest Calendar Events permissions.""" + + @patch("apps.nest.auth.calendar_events.Member.objects.get") + @patch("apps.nest.auth.calendar_events.EntityMember.objects.filter") + def test_user_with_leader_role(self, mock_filter, mock_get): + """Test user with leader role has permission.""" + mock_member = MagicMock() + mock_member.user = MagicMock() + mock_get.return_value = mock_member + mock_filter.return_value.exists.return_value = True + + assert has_calendar_events_permission("U123456") is True + mock_get.assert_called_once_with(slack_user_id="U123456") + mock_filter.assert_called_once_with(member=mock_member.user, role=EntityMember.Role.LEADER) + + @patch("apps.nest.auth.calendar_events.Member.objects.get") + def test_user_not_found(self, mock_get): + """Test user not found has no permission.""" + mock_get.side_effect = Member.DoesNotExist + + assert has_calendar_events_permission("U123456") is False + mock_get.assert_called_once_with(slack_user_id="U123456") From db7d1b4ef5a3f849fe300fce83fd343479c451ca Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 25 Sep 2025 12:09:08 +0300 Subject: [PATCH 5/7] Add tests --- .../tests/apps/nest/auth/calendar_events_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/tests/apps/nest/auth/calendar_events_test.py b/backend/tests/apps/nest/auth/calendar_events_test.py index 41477c1644..f7b4c35727 100644 --- a/backend/tests/apps/nest/auth/calendar_events_test.py +++ b/backend/tests/apps/nest/auth/calendar_events_test.py @@ -23,6 +23,19 @@ def test_user_with_leader_role(self, mock_filter, mock_get): mock_get.assert_called_once_with(slack_user_id="U123456") mock_filter.assert_called_once_with(member=mock_member.user, role=EntityMember.Role.LEADER) + @patch("apps.nest.auth.calendar_events.Member.objects.get") + @patch("apps.nest.auth.calendar_events.EntityMember.objects.filter") + def test_user_with_no_leader_role(self, mock_filter, mock_get): + """Test user with no leader role has no permission.""" + mock_member = MagicMock() + mock_member.user = MagicMock() + mock_get.return_value = mock_member + mock_filter.return_value.exists.return_value = False + + assert has_calendar_events_permission("U123456") is False + mock_get.assert_called_once_with(slack_user_id="U123456") + mock_filter.assert_called_once_with(member=mock_member.user, role=EntityMember.Role.LEADER) + @patch("apps.nest.auth.calendar_events.Member.objects.get") def test_user_not_found(self, mock_get): """Test user not found has no permission.""" From 67337e6e255181def217f8dc19353b9a8608769f Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 8 Oct 2025 08:20:42 +0300 Subject: [PATCH 6/7] Update poetry lock --- backend/poetry.lock | 65 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 6e691f0d8f..bdec9646f7 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -3584,6 +3584,13 @@ optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, @@ -4378,6 +4385,62 @@ optional = false python-versions = ">=3.7" groups = ["main"] files = [ + {file = "SQLAlchemy-2.0.43-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21ba7a08a4253c5825d1db389d4299f64a100ef9800e4624c8bf70d8f136e6ed"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11b9503fa6f8721bef9b8567730f664c5a5153d25e247aadc69247c4bc605227"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07097c0a1886c150ef2adba2ff7437e84d40c0f7dcb44a2c2b9c905ccfc6361c"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cdeff998cb294896a34e5b2f00e383e7c5c4ef3b4bfa375d9104723f15186443"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:bcf0724a62a5670e5718957e05c56ec2d6850267ea859f8ad2481838f889b42c"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-win32.whl", hash = "sha256:c697575d0e2b0a5f0433f679bda22f63873821d991e95a90e9e52aae517b2e32"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-win_amd64.whl", hash = "sha256:d34c0f6dbefd2e816e8f341d0df7d4763d382e3f452423e752ffd1e213da2512"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-win32.whl", hash = "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-win_amd64.whl", hash = "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-win32.whl", hash = "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-win_amd64.whl", hash = "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4e6aeb2e0932f32950cf56a8b4813cb15ff792fc0c9b3752eaf067cfe298496a"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f964a05356f4bca4112e6334ed7c208174511bd56e6b8fc86dad4d024d4185"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46293c39252f93ea0910aababa8752ad628bcce3a10d3f260648dd472256983f"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136063a68644eca9339d02e6693932116f6a8591ac013b0014479a1de664e40a"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6e2bf13d9256398d037fef09fd8bf9b0bf77876e22647d10761d35593b9ac547"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:44337823462291f17f994d64282a71c51d738fc9ef561bf265f1d0fd9116a782"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-win32.whl", hash = "sha256:13194276e69bb2af56198fef7909d48fd34820de01d9c92711a5fa45497cc7ed"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-win_amd64.whl", hash = "sha256:334f41fa28de9f9be4b78445e68530da3c5fa054c907176460c81494f4ae1f5e"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ceb5c832cc30663aeaf5e39657712f4c4241ad1f638d487ef7216258f6d41fe7"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11f43c39b4b2ec755573952bbcc58d976779d482f6f832d7f33a8d869ae891bf"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:413391b2239db55be14fa4223034d7e13325a1812c8396ecd4f2c08696d5ccad"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c379e37b08c6c527181a397212346be39319fb64323741d23e46abd97a400d34"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8cee08f15d9e238ede42e9bbc1d6e7158d0ca4f176e4eab21f88ac819ae3bd7b"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-win32.whl", hash = "sha256:b3edaec7e8b6dc5cd94523c6df4f294014df67097c8217a89929c99975811414"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-win_amd64.whl", hash = "sha256:227119ce0a89e762ecd882dc661e0aa677a690c914e358f0dd8932a2e8b2765b"}, + {file = "sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc"}, {file = "sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417"}, ] @@ -4913,4 +4976,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "5fdda024ec3165263bf855686ffc38f202a424fd096cb679aec56795d8ebdceb" +content-hash = "6ddafdcb592a2e1911b680141f08050c102b2d562a6679ccb82faeac2425611c" From f8864b9d0528f97bc99dc09c39db3e18f3877b58 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 8 Oct 2025 15:59:38 +0300 Subject: [PATCH 7/7] Merge migrations --- .../owasp/migrations/0055_merge_20251008_1259.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 backend/apps/owasp/migrations/0055_merge_20251008_1259.py diff --git a/backend/apps/owasp/migrations/0055_merge_20251008_1259.py b/backend/apps/owasp/migrations/0055_merge_20251008_1259.py new file mode 100644 index 0000000000..538436fba0 --- /dev/null +++ b/backend/apps/owasp/migrations/0055_merge_20251008_1259.py @@ -0,0 +1,12 @@ +# Generated by Django 5.2.6 on 2025-10-08 12:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0053_merge_20250918_1659"), + ("owasp", "0054_event_event_end_date_desc_idx"), + ] + + operations = []