From 2711823bd2ce7de7f731e306343add68bda67215 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 8 Oct 2025 15:44:21 +0300 Subject: [PATCH 1/8] 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 f4e82af9f479f87bb0f62fd2326e02bbe9a1abbf Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 8 Oct 2025 15:57:42 +0300 Subject: [PATCH 2/8] Merge migrations --- .../owasp/migrations/0055_merge_20251008_1253.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 backend/apps/owasp/migrations/0055_merge_20251008_1253.py diff --git a/backend/apps/owasp/migrations/0055_merge_20251008_1253.py b/backend/apps/owasp/migrations/0055_merge_20251008_1253.py new file mode 100644 index 0000000000..7975c00fd3 --- /dev/null +++ b/backend/apps/owasp/migrations/0055_merge_20251008_1253.py @@ -0,0 +1,12 @@ +# Generated by Django 5.2.6 on 2025-10-08 12:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0053_merge_20250918_1659"), + ("owasp", "0054_event_event_end_date_desc_idx"), + ] + + operations = [] From e4b2878406f48f1b247fc88966451175ccb2db4c Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 8 Oct 2025 16:01:04 +0300 Subject: [PATCH 3/8] Add channel and remove channel id --- ...ve_reminder_channel_id_reminder_channel.py | 29 +++++++++++++++++++ backend/apps/nest/models/reminder.py | 10 +++++-- 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_channel.py diff --git a/backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_channel.py b/backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_channel.py new file mode 100644 index 0000000000..7f32f1989b --- /dev/null +++ b/backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_channel.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.6 on 2025-10-08 13:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nest", "0013_merge_0006_delete_apikey_0012_reminderschedule_job_id"), + ("owasp", "0055_merge_20251008_1253"), + ] + + operations = [ + migrations.RemoveField( + model_name="reminder", + name="channel_id", + ), + migrations.AddField( + model_name="reminder", + name="channel", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="reminders", + to="owasp.entitychannel", + verbose_name="Channel", + ), + ), + ] diff --git a/backend/apps/nest/models/reminder.py b/backend/apps/nest/models/reminder.py index 807bc1b15e..7defd0a564 100644 --- a/backend/apps/nest/models/reminder.py +++ b/backend/apps/nest/models/reminder.py @@ -11,7 +11,13 @@ class Meta: verbose_name = "Nest Reminder" verbose_name_plural = "Nest Reminders" - channel_id = models.CharField(verbose_name="Channel ID", max_length=15, default="") + channel = models.ForeignKey( + "owasp.EntityChannel", + verbose_name="Channel", + on_delete=models.CASCADE, + related_name="reminders", + null=True, + ) event = models.ForeignKey( "owasp.Event", verbose_name="Event", @@ -30,4 +36,4 @@ class Meta: def __str__(self) -> str: """Reminder human readable representation.""" - return f"Reminder for {self.member} in channel: {self.channel_id}: {self.message}" + return f"Reminder for {self.member} in channel: {self.channel}: {self.message}" From 1b030b21f6f35816eb3c376b7b22de68a1ff00bc Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 8 Oct 2025 18:27:59 +0300 Subject: [PATCH 4/8] Update logic and refactor --- backend/apps/nest/handlers/calendar_events.py | 34 ++++---- ...der_channel_id_reminder_entity_channel.py} | 4 +- backend/apps/nest/models/reminder.py | 2 +- .../nest/schedulers/calendar_events/base.py | 8 +- .../nest/schedulers/calendar_events/slack.py | 9 ++- backend/apps/slack/admin/entity_channel.py | 2 - backend/apps/slack/commands/command.py | 12 +++ backend/apps/slack/commands/nestbot.py | 4 +- .../slack/common/handlers/calendar_events.py | 77 ++++++++++++++----- 9 files changed, 101 insertions(+), 51 deletions(-) rename backend/apps/nest/migrations/{0014_remove_reminder_channel_id_reminder_channel.py => 0014_remove_reminder_channel_id_reminder_entity_channel.py} (89%) diff --git a/backend/apps/nest/handlers/calendar_events.py b/backend/apps/nest/handlers/calendar_events.py index 75b3ac808d..0ee68fd7e6 100644 --- a/backend/apps/nest/handlers/calendar_events.py +++ b/backend/apps/nest/handlers/calendar_events.py @@ -6,11 +6,20 @@ from django.utils import timezone from apps.nest.clients.google_calendar import GoogleCalendarClient -from apps.nest.models.google_account_authorization import GoogleAccountAuthorization from apps.nest.models.reminder import Reminder from apps.nest.models.reminder_schedule import ReminderSchedule +from apps.owasp.models.entity_channel import EntityChannel from apps.owasp.models.event import Event -from apps.slack.models.member import Member + + +def get_calendar_id(user_id: str, event_number: str) -> str: + """Get the Google Calendar ID for a user.""" + if google_calendar_id := cache.get(f"{user_id}_{event_number}"): + return google_calendar_id + message = ( + "Invalid or expired event number. Please get a new event number from the events list." + ) + raise ValidationError(message) def schedule_reminder( @@ -33,29 +42,19 @@ def schedule_reminder( def set_reminder( - channel: str, - event_number: str, - user_id: str, + channel: EntityChannel, minutes_before: int, + client: GoogleCalendarClient, + member, recurrence: str | None = None, + google_calendar_id: str = "", message: str = "", ) -> ReminderSchedule: """Set a reminder for a user.""" if minutes_before <= 0: message = "Minutes before must be a positive integer." raise ValidationError(message) - auth = GoogleAccountAuthorization.authorize(user_id) - if not isinstance(auth, GoogleAccountAuthorization): - message = "User is not authorized with Google. Please sign in first." - raise ValidationError(message) - google_calendar_id = cache.get(f"{user_id}_{event_number}") - if not google_calendar_id: - message = ( - "Invalid or expired event number. Please get a new event number from the events list." - ) - raise ValidationError(message) - client = GoogleCalendarClient(auth) event = Event.parse_google_calendar_event(client.get_event(google_calendar_id)) if not event: message = "Could not retrieve the event details. Please try again later." @@ -74,9 +73,8 @@ def set_reminder( # Saving event to the database after validation event.save() - member = Member.objects.get(slack_user_id=user_id) reminder, _ = Reminder.objects.get_or_create( - channel_id=channel, + entity_channel=channel, event=event, member=member, defaults={"message": f"{event.name} - {message}" if message else event.name}, diff --git a/backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_channel.py b/backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_entity_channel.py similarity index 89% rename from backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_channel.py rename to backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_entity_channel.py index 7f32f1989b..8c2e7b73ee 100644 --- a/backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_channel.py +++ b/backend/apps/nest/migrations/0014_remove_reminder_channel_id_reminder_entity_channel.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.6 on 2025-10-08 13:00 +# Generated by Django 5.2.6 on 2025-10-08 15:16 import django.db.models.deletion from django.db import migrations, models @@ -17,7 +17,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name="reminder", - name="channel", + name="entity_channel", field=models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, diff --git a/backend/apps/nest/models/reminder.py b/backend/apps/nest/models/reminder.py index 7defd0a564..fef12abd97 100644 --- a/backend/apps/nest/models/reminder.py +++ b/backend/apps/nest/models/reminder.py @@ -11,7 +11,7 @@ class Meta: verbose_name = "Nest Reminder" verbose_name_plural = "Nest Reminders" - channel = models.ForeignKey( + entity_channel = models.ForeignKey( "owasp.EntityChannel", verbose_name="Channel", on_delete=models.CASCADE, diff --git a/backend/apps/nest/schedulers/calendar_events/base.py b/backend/apps/nest/schedulers/calendar_events/base.py index 5a7ac95d9a..d9877951d5 100644 --- a/backend/apps/nest/schedulers/calendar_events/base.py +++ b/backend/apps/nest/schedulers/calendar_events/base.py @@ -21,7 +21,7 @@ def schedule(self): self.reminder_schedule.scheduled_time, self.__class__.send_message, message=self.reminder_schedule.reminder.message, - channel_id=self.reminder_schedule.reminder.channel_id, + channel=self.reminder_schedule.reminder.entity_channel.pk, ).get_id() # Schedule deletion of the reminder after sending the message @@ -35,7 +35,7 @@ def schedule(self): func=self.__class__.send_and_update, args=( self.reminder_schedule.reminder.message, - self.reminder_schedule.reminder.channel_id, + self.reminder_schedule.reminder.entity_channel.pk, self.reminder_schedule.pk, ), queue_name="default", @@ -52,13 +52,13 @@ def cancel(self): self.reminder_schedule.reminder.delete() @staticmethod - def send_message(message: str, channel_id: str): + def send_message(message: str, channel_id: int): """Send message to the specified channel. To be implemented by subclasses.""" error_message = "Subclasses must implement this method." raise NotImplementedError(error_message) @staticmethod - def send_and_update(message: str, channel_id: str, reminder_schedule_id: int): + def send_and_update(message: str, channel_id: int, reminder_schedule_id: int): """Send message and update the reminder schedule.""" 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..858e34b0a6 100644 --- a/backend/apps/nest/schedulers/calendar_events/slack.py +++ b/backend/apps/nest/schedulers/calendar_events/slack.py @@ -2,6 +2,7 @@ from apps.nest.schedulers.calendar_events.base import BaseScheduler from apps.nest.utils.calendar_events import update_reminder_schedule_date +from apps.owasp.models.entity_channel import EntityChannel from apps.slack.apps import SlackConfig @@ -9,16 +10,18 @@ class SlackScheduler(BaseScheduler): """Slack Scheduler Class for Nest Calendar Events.""" @staticmethod - def send_message(message: str, channel_id: str): + def send_message(message: str, channel_id: int): """Send message to the specified Slack channel.""" + entity_channel = EntityChannel.objects.get(pk=channel_id) + if app := SlackConfig.app: app.client.chat_postMessage( - channel=channel_id, + channel=entity_channel.channel.slack_channel_id, text=message, ) @staticmethod - def send_and_update(message: str, channel_id: str, reminder_schedule_id: int): + def send_and_update(message: str, channel_id: int, reminder_schedule_id: int): """Send message and update the reminder schedule.""" SlackScheduler.send_message(message, channel_id) update_reminder_schedule_date(reminder_schedule_id) diff --git a/backend/apps/slack/admin/entity_channel.py b/backend/apps/slack/admin/entity_channel.py index 4309559f69..2f2bcf3c86 100644 --- a/backend/apps/slack/admin/entity_channel.py +++ b/backend/apps/slack/admin/entity_channel.py @@ -25,8 +25,6 @@ class EntityChannelAdmin(admin.ModelAdmin): "entity_type", "entity_id", "channel_type", - "search_button", - "channel_search", "channel_id", "platform", "is_default", diff --git a/backend/apps/slack/commands/command.py b/backend/apps/slack/commands/command.py index 92b4ec256d..1caef946f1 100644 --- a/backend/apps/slack/commands/command.py +++ b/backend/apps/slack/commands/command.py @@ -87,6 +87,18 @@ def get_user_id(self, command) -> str: """ return command.get("user_id") + def get_workspace_id(self, command) -> str: + """Get the workspace ID from the command. + + Args: + command (dict): The Slack event payload. + + Returns: + str: The workspace ID. + + """ + return command.get("team_id") + def render_blocks(self, command): """Get the rendered blocks. diff --git a/backend/apps/slack/commands/nestbot.py b/backend/apps/slack/commands/nestbot.py index 1d64f60bd2..dba8ab2042 100644 --- a/backend/apps/slack/commands/nestbot.py +++ b/backend/apps/slack/commands/nestbot.py @@ -30,6 +30,8 @@ def render_blocks(self, command): markdown("*Invalid command format. Please check your input and try again.*") ] else: - return get_setting_reminder_blocks(args, self.get_user_id(command)) + return get_setting_reminder_blocks( + args, self.get_user_id(command), self.get_workspace_id(command) + ) else: return get_cancel_reminder_blocks(int(args.number), self.get_user_id(command)) diff --git a/backend/apps/slack/common/handlers/calendar_events.py b/backend/apps/slack/common/handlers/calendar_events.py index 8054b764ff..f62bc6001f 100644 --- a/backend/apps/slack/common/handlers/calendar_events.py +++ b/backend/apps/slack/common/handlers/calendar_events.py @@ -104,16 +104,43 @@ def get_reminders_blocks(slack_user_id: str) -> list[dict]: return blocks -def get_setting_reminder_blocks(args, slack_user_id: str) -> list[dict]: +def get_setting_reminder_blocks(args, slack_user_id: str, workspace_id: str) -> list[dict]: """Get the blocks for setting a reminder.""" - from apps.nest.handlers.calendar_events import set_reminder + from django.contrib.contenttypes.models import ContentType + + from apps.nest.clients.google_calendar import GoogleCalendarClient + from apps.nest.handlers.calendar_events import get_calendar_id, set_reminder + from apps.nest.models.google_account_authorization import GoogleAccountAuthorization from apps.nest.schedulers.calendar_events.slack import SlackScheduler + from apps.owasp.models.entity_channel import EntityChannel + from apps.slack.models.conversation import Conversation + from apps.slack.models.member import Member + return_block = None try: + auth = GoogleAccountAuthorization.authorize(slack_user_id) + if not isinstance(auth, GoogleAccountAuthorization): + return [markdown(f"*Please sign in with Google first through this <{auth[0]}|link>*")] + content_type_member = ContentType.objects.get_for_model(Member) + content_type_conversation = ContentType.objects.get_for_model(Conversation) + conversation = Conversation.objects.get( + name=args.channel.lstrip("#"), + workspace__slack_workspace_id=workspace_id, + ) + member = Member.objects.get(slack_user_id=slack_user_id) + channel = EntityChannel.objects.get( + channel_id=conversation.pk, + channel_type=content_type_conversation, + entity_id=member.pk, + entity_type=content_type_member, + ) + google_calendar_id = get_calendar_id(slack_user_id, args.event_number) + client = GoogleCalendarClient(auth) reminder_schedule = set_reminder( - channel=args.channel, - event_number=args.event_number, - user_id=slack_user_id, + channel=channel, + client=client, + member=member, + google_calendar_id=google_calendar_id, minutes_before=args.minutes_before, recurrence=args.recurrence, message=" ".join(args.message) if args.message else "", @@ -123,22 +150,32 @@ def get_setting_reminder_blocks(args, slack_user_id: str) -> list[dict]: f"<@{reminder_schedule.reminder.member.slack_user_id}> set a reminder: " f"{reminder_schedule.reminder.message}" f" at {reminder_schedule.scheduled_time.strftime('%Y-%m-%d %H:%M %Z')}", - reminder_schedule.reminder.channel_id, + reminder_schedule.reminder.entity_channel.channel.pk, ) except ValidationError as e: - return [markdown(f"*{e.message}*")] + return_block = [markdown(f"*{e.message}*")] except ValueError as e: - return [markdown(f"*{e!s}*")] + return_block = [markdown(f"*{e!s}*")] except ServerNotFoundError: - return [markdown("*Please check your internet connection.*")] - return [ - markdown( - f"*{args.minutes_before}-minute reminder set for event" - f" '{reminder_schedule.reminder.event.name}'*" - f" in {args.channel}" - f"{NL} Scheduled Time: " - f"{reminder_schedule.scheduled_time.strftime('%Y-%m-%d %H:%M %Z')}" - f"{NL} Recurrence: {reminder_schedule.recurrence}" - f"{NL} Message: {reminder_schedule.reminder.message}" - ) - ] + return_block = [markdown("*Please check your internet connection.*")] + except Conversation.DoesNotExist: + return_block = [markdown(f"*Channel '{args.channel}' does not exist in this workspace.*")] + except Member.DoesNotExist: + return_block = [markdown("*Member does not exist.*")] + except EntityChannel.DoesNotExist: + return_block = [markdown(f"*{args.channel} is not linked to your account.*")] + return ( + return_block + if return_block + else [ + markdown( + f"*{args.minutes_before}-minute reminder set for event" + f" '{reminder_schedule.reminder.event.name}'*" + f" in {args.channel}" + f"{NL} Scheduled Time: " + f"{reminder_schedule.scheduled_time.strftime('%Y-%m-%d %H:%M %Z')}" + f"{NL} Recurrence: {reminder_schedule.recurrence}" + f"{NL} Message: {reminder_schedule.reminder.message}" + ) + ] + ) From 4e18dcd15b1fc0d507050554dd73ff5806ceee3a Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Wed, 8 Oct 2025 23:57:09 +0300 Subject: [PATCH 5/8] Fix bugs --- backend/apps/nest/schedulers/calendar_events/base.py | 2 +- backend/apps/slack/common/handlers/calendar_events.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/apps/nest/schedulers/calendar_events/base.py b/backend/apps/nest/schedulers/calendar_events/base.py index d9877951d5..5ac258cc6e 100644 --- a/backend/apps/nest/schedulers/calendar_events/base.py +++ b/backend/apps/nest/schedulers/calendar_events/base.py @@ -21,7 +21,7 @@ def schedule(self): self.reminder_schedule.scheduled_time, self.__class__.send_message, message=self.reminder_schedule.reminder.message, - channel=self.reminder_schedule.reminder.entity_channel.pk, + channel_id=self.reminder_schedule.reminder.entity_channel.pk, ).get_id() # Schedule deletion of the reminder after sending the message diff --git a/backend/apps/slack/common/handlers/calendar_events.py b/backend/apps/slack/common/handlers/calendar_events.py index f62bc6001f..57566798e0 100644 --- a/backend/apps/slack/common/handlers/calendar_events.py +++ b/backend/apps/slack/common/handlers/calendar_events.py @@ -150,7 +150,7 @@ def get_setting_reminder_blocks(args, slack_user_id: str, workspace_id: str) -> f"<@{reminder_schedule.reminder.member.slack_user_id}> set a reminder: " f"{reminder_schedule.reminder.message}" f" at {reminder_schedule.scheduled_time.strftime('%Y-%m-%d %H:%M %Z')}", - reminder_schedule.reminder.entity_channel.channel.pk, + reminder_schedule.reminder.entity_channel.pk, ) except ValidationError as e: return_block = [markdown(f"*{e.message}*")] From 9133fa3da8591f2ff00dbde467928d10bac5f66a Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 9 Oct 2025 15:57:40 +0300 Subject: [PATCH 6/8] Fix bugs, make entity channel non-nullable, and add send_and_delete method --- .../0015_alter_reminder_entity_channel.py | 24 +++++++++++++++++++ backend/apps/nest/models/reminder.py | 3 +-- .../nest/schedulers/calendar_events/base.py | 16 ++++++------- .../nest/schedulers/calendar_events/slack.py | 10 ++++++++ .../slack/common/handlers/calendar_events.py | 4 ++-- 5 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 backend/apps/nest/migrations/0015_alter_reminder_entity_channel.py diff --git a/backend/apps/nest/migrations/0015_alter_reminder_entity_channel.py b/backend/apps/nest/migrations/0015_alter_reminder_entity_channel.py new file mode 100644 index 0000000000..f121039cbf --- /dev/null +++ b/backend/apps/nest/migrations/0015_alter_reminder_entity_channel.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.6 on 2025-10-09 12:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nest", "0014_remove_reminder_channel_id_reminder_entity_channel"), + ("owasp", "0055_merge_20251008_1253"), + ] + + operations = [ + migrations.AlterField( + model_name="reminder", + name="entity_channel", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reminders", + to="owasp.entitychannel", + verbose_name="Channel", + ), + ), + ] diff --git a/backend/apps/nest/models/reminder.py b/backend/apps/nest/models/reminder.py index fef12abd97..5bcdeac7e3 100644 --- a/backend/apps/nest/models/reminder.py +++ b/backend/apps/nest/models/reminder.py @@ -16,7 +16,6 @@ class Meta: verbose_name="Channel", on_delete=models.CASCADE, related_name="reminders", - null=True, ) event = models.ForeignKey( "owasp.Event", @@ -36,4 +35,4 @@ class Meta: def __str__(self) -> str: """Reminder human readable representation.""" - return f"Reminder for {self.member} in channel: {self.channel}: {self.message}" + return f"Reminder for {self.member} in channel: {self.entity_channel}: {self.message}" diff --git a/backend/apps/nest/schedulers/calendar_events/base.py b/backend/apps/nest/schedulers/calendar_events/base.py index 5ac258cc6e..16edaa0e40 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.entity_channel.pk, + 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, @@ -57,6 +51,12 @@ def send_message(message: str, channel_id: int): error_message = "Subclasses must implement this method." raise NotImplementedError(error_message) + @staticmethod + def send_and_delete(message: str, channel_id: int, 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) + @staticmethod def send_and_update(message: str, channel_id: int, reminder_schedule_id: int): """Send message and update the reminder schedule.""" diff --git a/backend/apps/nest/schedulers/calendar_events/slack.py b/backend/apps/nest/schedulers/calendar_events/slack.py index 858e34b0a6..544801e50e 100644 --- a/backend/apps/nest/schedulers/calendar_events/slack.py +++ b/backend/apps/nest/schedulers/calendar_events/slack.py @@ -20,6 +20,16 @@ def send_message(message: str, channel_id: int): text=message, ) + @staticmethod + def send_and_delete(message: str, channel_id: int, 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: int, reminder_schedule_id: int): """Send message and update the reminder schedule.""" diff --git a/backend/apps/slack/common/handlers/calendar_events.py b/backend/apps/slack/common/handlers/calendar_events.py index 57566798e0..f7a1c9ef79 100644 --- a/backend/apps/slack/common/handlers/calendar_events.py +++ b/backend/apps/slack/common/handlers/calendar_events.py @@ -24,7 +24,7 @@ def get_cancel_reminder_blocks(reminder_schedule_id: int, slack_user_id: str) -> return [ markdown( f"*Canceled the reminder for event '{reminder_schedule.reminder.event.name}'*" - f" in {reminder_schedule.reminder.channel_id}" + f" in #{reminder_schedule.reminder.entity_channel.channel.name}" ) ] @@ -93,7 +93,7 @@ def get_reminders_blocks(slack_user_id: str) -> list[dict]: blocks.extend( markdown( f"{NL}- Reminder Number: {reminder_schedule.pk}" - f"{NL}- Channel: {reminder_schedule.reminder.channel_id}" + f"{NL}- Channel: #{reminder_schedule.reminder.entity_channel.channel.name}" f"{NL}- Scheduled Time: " f"{reminder_schedule.scheduled_time.strftime('%Y-%m-%d, %H:%M %Z')}" f"{NL}- Recurrence: {reminder_schedule.recurrence}" From 45952dc0b974d64f4c6295e9d1e50b781dbdb275 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 9 Oct 2025 16:13:43 +0300 Subject: [PATCH 7/8] Update tests --- .../nest/models/reminder_schedule_test.py | 10 +++++--- .../tests/apps/nest/models/reminder_test.py | 11 +++++--- .../schedulers/calendar_events/base_test.py | 25 +++++++++++-------- .../schedulers/calendar_events/slack_test.py | 21 ++++++++++++++-- 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/backend/tests/apps/nest/models/reminder_schedule_test.py b/backend/tests/apps/nest/models/reminder_schedule_test.py index e111a900bf..fb44b48c35 100644 --- a/backend/tests/apps/nest/models/reminder_schedule_test.py +++ b/backend/tests/apps/nest/models/reminder_schedule_test.py @@ -5,6 +5,7 @@ from apps.nest.models.reminder import Reminder from apps.nest.models.reminder_schedule import ReminderSchedule from apps.slack.models.member import Member +from apps.owasp.models.entity_channel import EntityChannel class TestReminderScheduleModel: @@ -13,9 +14,10 @@ class TestReminderScheduleModel: def test_str_representation(self): """Test string representation of the ReminderSchedule model.""" member = Member(slack_user_id="U123456", username="Test User") + channel = EntityChannel(channel_id=5) reminder = Reminder( member=member, - channel_id="C123456", + entity_channel=channel, message="Test reminder", ) schedule = ReminderSchedule( @@ -31,9 +33,10 @@ def test_str_representation(self): def test_verbose_names(self): """Test verbose names of the ReminderSchedule model.""" member = Member(slack_user_id="U123456", username="Test User") + channel = EntityChannel(channel_id=5) reminder = Reminder( member=member, - channel_id="C123456", + entity_channel=channel, message="Test reminder", ) schedule = ReminderSchedule( @@ -51,9 +54,10 @@ def test_verbose_names(self): def test_cron_expression_property(self): """Test cron_expression property of the ReminderSchedule model.""" member = Member(slack_user_id="U123456", username="Test User") + channel = EntityChannel(channel_id=5) reminder = Reminder( member=member, - channel_id="C123456", + entity_channel=channel, message="Test reminder", ) date = timezone.make_aware(timezone.datetime(2023, 1, 1, 12, 0, 0)) diff --git a/backend/tests/apps/nest/models/reminder_test.py b/backend/tests/apps/nest/models/reminder_test.py index 8a5f971fd6..a25e7f0bfb 100644 --- a/backend/tests/apps/nest/models/reminder_test.py +++ b/backend/tests/apps/nest/models/reminder_test.py @@ -2,6 +2,7 @@ from apps.nest.models.reminder import Reminder from apps.slack.models.member import Member +from apps.owasp.models.entity_channel import EntityChannel class TestReminderModel: @@ -10,23 +11,25 @@ class TestReminderModel: def test_str_representation(self): """Test string representation of the Reminder model.""" member = Member(slack_user_id="U123456", username="Test User") + channel = EntityChannel(channel_id=5) reminder = Reminder( member=member, - channel_id="C123456", + entity_channel=channel, message="Test reminder", ) - assert str(reminder) == f"Reminder for {member} in channel: C123456: Test reminder" + assert str(reminder) == f"Reminder for {member} in channel: {channel}: Test reminder" def test_verbose_names(self): """Test verbose names of the Reminder model.""" + channel = EntityChannel(channel_id=5) reminder = Reminder( member=Member(slack_user_id="U123456", username="Test User"), - channel_id="C123456", + entity_channel=channel, message="Test reminder", ) assert reminder._meta.verbose_name == "Nest Reminder" assert reminder._meta.verbose_name_plural == "Nest Reminders" assert reminder._meta.db_table == "nest_reminders" assert reminder._meta.get_field("member").verbose_name == "Slack Member" - assert reminder._meta.get_field("channel_id").verbose_name == "Channel ID" + assert reminder._meta.get_field("entity_channel").verbose_name == "Channel" assert reminder._meta.get_field("message").verbose_name == "Reminder Message" 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..809338f0bb 100644 --- a/backend/tests/apps/nest/schedulers/calendar_events/base_test.py +++ b/backend/tests/apps/nest/schedulers/calendar_events/base_test.py @@ -31,7 +31,9 @@ def test_schedule_once(self, mock_get_scheduler): mock_reminder_schedule.recurrence = "once" 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.reminder.entity_channel = MagicMock() + mock_reminder_schedule.reminder.entity_channel.pk = 5 + mock_reminder_schedule.pk = 1 scheduler_instance = MagicMock() mock_get_scheduler.return_value = scheduler_instance @@ -41,13 +43,10 @@ def test_schedule_once(self, mock_get_scheduler): scheduler_instance.enqueue_at.assert_any_call( 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, + channel_id=5, + reminder_schedule_id=mock_reminder_schedule.pk, ) mock_reminder_schedule.save.assert_called_once_with(update_fields=["job_id"]) @@ -58,7 +57,7 @@ def test_schedule_recurring(self, mock_get_scheduler): mock_reminder_schedule.recurrence = "daily" mock_reminder_schedule.cron_expression = "0 9 * * *" mock_reminder_schedule.reminder.message = "Daily Reminder" - mock_reminder_schedule.reminder.channel_id = "C123456" + mock_reminder_schedule.reminder.entity_channel.pk = 5 mock_reminder_schedule.pk = 4 scheduler_instance = MagicMock() @@ -70,7 +69,7 @@ def test_schedule_recurring(self, mock_get_scheduler): scheduler_instance.cron.assert_called_once_with( "0 9 * * *", func=BaseScheduler.send_and_update, - args=("Daily Reminder", "C123456", 4), + args=("Daily Reminder", 5, 4), queue_name="default", use_local_timezone=True, result_ttl=500, @@ -99,8 +98,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", 5, 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", 5, 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..957c8c7eb4 100644 --- a/backend/tests/apps/nest/schedulers/calendar_events/slack_test.py +++ b/backend/tests/apps/nest/schedulers/calendar_events/slack_test.py @@ -9,17 +9,34 @@ class TestSlackScheduler: """Test cases for the SlackScheduler class.""" @patch("apps.nest.schedulers.calendar_events.slack.SlackConfig") - def test_send_message(self, mock_slack_config): + @patch("apps.nest.schedulers.calendar_events.slack.EntityChannel.objects.get") + def test_send_message(self, mock_get, mock_slack_config): """Test sending a message via Slack.""" mock_slack_config.app = MagicMock() mock_client = mock_slack_config.app.client - SlackScheduler.send_message("Test Message", "C123456") + mock_entity_channel = MagicMock() + mock_entity_channel.channel.slack_channel_id = "C123456" + mock_get.return_value = mock_entity_channel + SlackScheduler.send_message("Test Message", 5) mock_client.chat_postMessage.assert_called_once_with( channel="C123456", 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_reminder_schedule = MagicMock() + mock_filter.return_value.first.return_value = mock_reminder_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_reminder_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 a9b2c1592652dbdaa54b67c8d004344919cbf315 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 9 Oct 2025 22:34:42 +0300 Subject: [PATCH 8/8] Update tests --- .../nest/handlers/calendar_events_test.py | 268 +++++++----------- .../nest/models/reminder_schedule_test.py | 2 +- .../tests/apps/nest/models/reminder_test.py | 2 +- .../common/handlers/calendar_events_test.py | 230 +++++++++++---- 4 files changed, 271 insertions(+), 231 deletions(-) diff --git a/backend/tests/apps/nest/handlers/calendar_events_test.py b/backend/tests/apps/nest/handlers/calendar_events_test.py index 71e1d4137b..68c1c3f433 100644 --- a/backend/tests/apps/nest/handlers/calendar_events_test.py +++ b/backend/tests/apps/nest/handlers/calendar_events_test.py @@ -1,30 +1,46 @@ """Test cases for Nest Calendar Events handlers.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from django.core.exceptions import ValidationError from django.utils import timezone -from apps.nest.handlers.calendar_events import schedule_reminder, set_reminder -from apps.nest.models.google_account_authorization import GoogleAccountAuthorization +from apps.nest.handlers.calendar_events import get_calendar_id, schedule_reminder, set_reminder from apps.nest.models.reminder import Reminder from apps.nest.models.reminder_schedule import ReminderSchedule +from apps.owasp.models.entity_channel import EntityChannel from apps.owasp.models.event import Event from apps.slack.models.member import Member MESSAGE = "Reminder Message" +USER_ID = "U123456" +EVENT_NUMBER = 1 class TestCalendarEventsHandlers: """Test cases for Nest Calendar Events handlers.""" + @patch("apps.nest.handlers.calendar_events.cache.get") + def test_get_calendar_id_cache_hit(self, mock_cache_get): + """Test get_calendar_id function when cache hit.""" + mock_cache_get.return_value = "calendar_id" + result = get_calendar_id(USER_ID, EVENT_NUMBER) + assert result == "calendar_id" + mock_cache_get.assert_called_once_with(f"{USER_ID}_1") + + @patch("apps.nest.handlers.calendar_events.cache.get") + def test_get_calendar_id_cache_miss(self, mock_cache_get): + """Test get_calendar_id function when cache miss.""" + mock_cache_get.return_value = None + with pytest.raises(ValidationError) as excinfo: + get_calendar_id(USER_ID, EVENT_NUMBER) + assert "Invalid or expired event number" in str(excinfo.value) + mock_cache_get.assert_called_once_with(f"{USER_ID}_1") + @patch("apps.nest.handlers.calendar_events.GoogleCalendarClient") - @patch("apps.nest.handlers.calendar_events.cache") - @patch("apps.nest.handlers.calendar_events.GoogleAccountAuthorization.authorize") @patch("apps.nest.handlers.calendar_events.Event.parse_google_calendar_event") @patch("apps.nest.handlers.calendar_events.Event.save") - @patch("apps.nest.handlers.calendar_events.Member.objects.get") @patch("apps.nest.handlers.calendar_events.Reminder.objects.get_or_create") @patch("apps.nest.handlers.calendar_events.schedule_reminder") @patch("apps.nest.handlers.calendar_events.transaction.atomic") @@ -33,34 +49,22 @@ def test_set_reminder_success( mock_transaction_atomic, mock_schedule_reminder, mock_reminder_get_or_create, - mock_member_get, mock_event_save, mock_parse_event, - mock_authorize, - mock_cache, mock_google_client, ): """Test setting a reminder successfully.""" mock_transaction_atomic.return_value.__enter__.return_value = None mock_transaction_atomic.return_value.__exit__.return_value = None + # Mock inputs - channel = "C123456" - event_number = "1" - user_id = "U123456" + mock_channel = MagicMock(spec=EntityChannel) + mock_member = MagicMock(spec=Member) + google_calendar_id = "google_calendar_event_id" minutes_before = 15 recurrence = "daily" message = MESSAGE - # Mock return values - mock_member = Member(slack_user_id=user_id) - mock_member_get.return_value = mock_member - mock_auth = GoogleAccountAuthorization( - access_token="access_token", # noqa: S106 - refresh_token="refresh_token", # noqa: S106 - expires_at=timezone.now() + timezone.timedelta(hours=1), - ) - mock_authorize.return_value = mock_auth - mock_cache.get.return_value = "google_calendar_event_id" mock_event = Event( google_calendar_id="event_id", key="event_key", @@ -70,148 +74,78 @@ def test_set_reminder_success( description="Event Description", ) mock_parse_event.return_value = mock_event - mock_reminder = Reminder( - member=mock_member, - event=mock_event, - message=message, - channel_id=channel, - ) + + mock_reminder = MagicMock(spec=Reminder) mock_reminder_get_or_create.return_value = (mock_reminder, False) - mock_schedule_reminder.return_value = ReminderSchedule( - reminder=mock_reminder, - scheduled_time=mock_event.start_date - timezone.timedelta(minutes=minutes_before), - recurrence=recurrence, - ) + + mock_schedule = MagicMock(spec=ReminderSchedule) + mock_schedule_reminder.return_value = mock_schedule # Call the function result = set_reminder( - channel=channel, - event_number=event_number, - user_id=user_id, + channel=mock_channel, + client=mock_google_client, + member=mock_member, + google_calendar_id=google_calendar_id, minutes_before=minutes_before, recurrence=recurrence, message=message, ) + # Assertions - assert result.reminder.member == mock_member - assert result.reminder.event == mock_event - assert result.recurrence == recurrence - mock_member_get.assert_called_once_with(slack_user_id=user_id) - mock_authorize.assert_called_once_with(user_id) - mock_cache.get.assert_called_once_with(f"{user_id}_{event_number}") + assert result == mock_schedule mock_parse_event.assert_called_once() mock_event_save.assert_called_once() - mock_google_client.assert_called_once_with(mock_auth) + mock_reminder_get_or_create.assert_called_once() + mock_schedule_reminder.assert_called_once() def test_set_reminder_invalid_minutes_before(self): """Test setting a reminder with invalid minutes_before.""" - with pytest.raises(ValidationError) as excinfo: - set_reminder( - channel="C123456", - event_number="1", - user_id="U123456", - minutes_before=0, - recurrence="daily", - message=MESSAGE, - ) - assert excinfo.value.message == "Minutes before must be a positive integer." + mock_channel = MagicMock(spec=EntityChannel) + mock_client = MagicMock() + mock_member = MagicMock(spec=Member) - @patch("apps.nest.handlers.calendar_events.GoogleAccountAuthorization.authorize") - def test_set_reminder_unauthorized_user(self, mock_authorize): - """Test setting a reminder with an unauthorized user.""" - mock_authorize.return_value = ("http://auth.url", "state") # NOSONAR - with pytest.raises(ValidationError) as excinfo: - set_reminder( - channel="C123456", - event_number="1", - user_id="U123456", - minutes_before=15, - recurrence="daily", - message=MESSAGE, - ) - assert excinfo.value.message == "User is not authorized with Google. Please sign in first." - mock_authorize.assert_called_once_with("U123456") - - @patch("apps.nest.handlers.calendar_events.cache") - @patch("apps.nest.handlers.calendar_events.GoogleAccountAuthorization.authorize") - def test_set_reminder_invalid_event_number(self, mock_authorize, mock_cache): - """Test setting a reminder with an invalid event number.""" - mock_authorize.return_value = GoogleAccountAuthorization( - access_token="access_token", # noqa: S106 - refresh_token="refresh_token", # noqa: S106 - expires_at=timezone.now() + timezone.timedelta(hours=1), - ) - mock_cache.get.return_value = None with pytest.raises(ValidationError) as excinfo: set_reminder( - channel="C123456", - event_number="invalid_event_number", - user_id="U123456", - minutes_before=15, + channel=mock_channel, + client=mock_client, + member=mock_member, + google_calendar_id="calendar_id", + minutes_before=0, recurrence="daily", message=MESSAGE, ) - assert excinfo.value.message == ( - "Invalid or expired event number. Please get a new event number from the events list." - ) - mock_authorize.assert_called_once_with("U123456") - mock_cache.get.assert_called_once_with("U123456_invalid_event_number") + assert "Minutes before must be a positive integer" in str(excinfo.value) - @patch("apps.nest.handlers.calendar_events.GoogleAccountAuthorization.authorize") @patch("apps.nest.handlers.calendar_events.Event.parse_google_calendar_event") - @patch("apps.nest.handlers.calendar_events.cache") - @patch("apps.nest.handlers.calendar_events.GoogleCalendarClient") - def test_set_reminder_event_retrieval_failure( - self, - mock_google_client, - mock_cache, - mock_parse_event, - mock_authorize, - ): + def test_set_reminder_event_retrieval_failure(self, mock_parse_event): """Test setting a reminder when event retrieval fails.""" - mock_authorize.return_value = GoogleAccountAuthorization( - access_token="access_token", # noqa: S106 - refresh_token="refresh_token", # noqa: S106 - expires_at=timezone.now() + timezone.timedelta(hours=1), - ) - mock_cache.get.return_value = "google_calendar_event_id" + mock_channel = MagicMock(spec=EntityChannel) + mock_client = MagicMock() + mock_member = MagicMock(spec=Member) + + mock_client.get_event.return_value = {"id": "event_id", "summary": "Test Event"} mock_parse_event.return_value = None - with pytest.raises(ValidationError) as excinfo: + + with pytest.raises((ValidationError, ValueError)): set_reminder( - channel="C123456", - event_number="1", - user_id="U123456", + channel=mock_channel, + client=mock_client, + member=mock_member, + google_calendar_id="calendar_id", minutes_before=15, recurrence="daily", message=MESSAGE, ) - assert ( - excinfo.value.message - == "Could not retrieve the event details. Please try again later." - ) - mock_authorize.assert_called_once_with("U123456") - mock_cache.get.assert_called_once_with("U123456_1") - mock_parse_event.assert_called_once() - mock_google_client.assert_called_once() - @patch("apps.nest.handlers.calendar_events.GoogleAccountAuthorization.authorize") @patch("apps.nest.handlers.calendar_events.Event.parse_google_calendar_event") - @patch("apps.nest.handlers.calendar_events.cache") - @patch("apps.nest.handlers.calendar_events.GoogleCalendarClient") - def test_set_reminder_past_reminder_time( - self, - mock_google_client, - mock_cache, - mock_parse_event, - mock_authorize, - ): + def test_set_reminder_past_reminder_time(self, mock_parse_event): """Test setting a reminder with a past reminder time.""" - mock_authorize.return_value = GoogleAccountAuthorization( - access_token="access_token", # noqa: S106 - refresh_token="refresh_token", # noqa: S106 - expires_at=timezone.now() + timezone.timedelta(hours=1), - ) - mock_cache.get.return_value = "google_calendar_event_id" + mock_channel = MagicMock(spec=EntityChannel) + mock_client = MagicMock() + mock_member = MagicMock(spec=Member) + + mock_client.get_event.return_value = {"id": "event_id", "summary": "Test Event"} mock_event = Event( google_calendar_id="event_id", key="event_key", @@ -221,42 +155,27 @@ def test_set_reminder_past_reminder_time( description="Event Description", ) mock_parse_event.return_value = mock_event + with pytest.raises(ValidationError) as excinfo: set_reminder( - channel="C123456", - event_number="1", - user_id="U123456", - minutes_before=15, + channel=mock_channel, + client=mock_client, + member=mock_member, + google_calendar_id="calendar_id", + minutes_before=15, # More than 10 minutes, so reminder would be in the past recurrence="daily", message=MESSAGE, ) - assert ( - excinfo.value.message - == "Reminder time must be in the future. Please adjust the minutes before." - ) - mock_authorize.assert_called_once_with("U123456") - mock_cache.get.assert_called_once_with("U123456_1") - mock_parse_event.assert_called_once() - mock_google_client.assert_called_once() + assert "must be in the future" in str(excinfo.value) - @patch("apps.nest.handlers.calendar_events.GoogleAccountAuthorization.authorize") @patch("apps.nest.handlers.calendar_events.Event.parse_google_calendar_event") - @patch("apps.nest.handlers.calendar_events.cache") - @patch("apps.nest.handlers.calendar_events.GoogleCalendarClient") - def test_set_reminder_invalid_recurrence( - self, - mock_google_client, - mock_cache, - mock_parse_event, - mock_authorize, - ): + def test_set_reminder_invalid_recurrence(self, mock_parse_event): """Test setting a reminder with an invalid recurrence value.""" - mock_authorize.return_value = GoogleAccountAuthorization( - access_token="access_token", # noqa: S106 - refresh_token="refresh_token", # noqa: S106 - expires_at=timezone.now() + timezone.timedelta(hours=1), - ) - mock_cache.get.return_value = "google_calendar_event_id" + mock_channel = MagicMock(spec=EntityChannel) + mock_client = MagicMock() + mock_member = MagicMock(spec=Member) + + mock_client.get_event.return_value = {"id": "event_id", "summary": "Test Event"} mock_event = Event( google_calendar_id="event_id", key="event_key", @@ -266,25 +185,24 @@ def test_set_reminder_invalid_recurrence( description="Event Description", ) mock_parse_event.return_value = mock_event + with pytest.raises(ValidationError) as excinfo: set_reminder( - channel="C123456", - event_number="1", - user_id="U123456", + channel=mock_channel, + client=mock_client, + member=mock_member, + google_calendar_id="calendar_id", minutes_before=15, recurrence="invalid_recurrence", message=MESSAGE, ) - assert excinfo.value.message == "Invalid recurrence value." - mock_authorize.assert_called_once_with("U123456") - mock_cache.get.assert_called_once_with("U123456_1") - mock_parse_event.assert_called_once() - mock_google_client.assert_called_once() + assert "Invalid recurrence" in str(excinfo.value) @patch("apps.nest.handlers.calendar_events.ReminderSchedule.objects.create") def test_schedule_reminder_success(self, mock_reminder_create): """Test scheduling a reminder successfully.""" # Mock inputs + mock_entity_channel = EntityChannel(channel_id=5) reminder = Reminder( member=Member(slack_user_id="U123456"), event=Event( @@ -296,7 +214,7 @@ def test_schedule_reminder_success(self, mock_reminder_create): description="Event Description", ), message="Test Reminder", - channel_id="C123456", + entity_channel=mock_entity_channel, ) scheduled_time = timezone.now() + timezone.timedelta(hours=1) recurrence = "weekly" @@ -322,6 +240,7 @@ def test_schedule_reminder_success(self, mock_reminder_create): def test_schedule_reminder_past_time(self): """Test scheduling a reminder with a past scheduled time.""" + mock_entity_channel = EntityChannel(channel_id=5) reminder = Reminder( member=Member(slack_user_id="U123456"), event=Event( @@ -333,15 +252,17 @@ def test_schedule_reminder_past_time(self): description="Event Description", ), message="Test Reminder", - channel_id="C123456", + entity_channel=mock_entity_channel, ) past_time = timezone.now() - timezone.timedelta(hours=1) + with pytest.raises(ValidationError) as excinfo: schedule_reminder(reminder, past_time, "daily") - assert excinfo.value.message == "Scheduled time must be in the future." + assert "must be in the future" in str(excinfo.value) def test_schedule_reminder_invalid_recurrence(self): """Test scheduling a reminder with an invalid recurrence value.""" + mock_entity_channel = EntityChannel(channel_id=5) reminder = Reminder( member=Member(slack_user_id="U123456"), event=Event( @@ -353,10 +274,11 @@ def test_schedule_reminder_invalid_recurrence(self): description="Event Description", ), message="Test Reminder", - channel_id="C123456", + entity_channel=mock_entity_channel, ) + with pytest.raises(ValidationError) as excinfo: schedule_reminder( reminder, timezone.now() + timezone.timedelta(hours=1), "invalid_recurrence" ) - assert excinfo.value.message == "Invalid recurrence value." + assert "Invalid recurrence" in str(excinfo.value) diff --git a/backend/tests/apps/nest/models/reminder_schedule_test.py b/backend/tests/apps/nest/models/reminder_schedule_test.py index fb44b48c35..0a13b207fb 100644 --- a/backend/tests/apps/nest/models/reminder_schedule_test.py +++ b/backend/tests/apps/nest/models/reminder_schedule_test.py @@ -4,8 +4,8 @@ from apps.nest.models.reminder import Reminder from apps.nest.models.reminder_schedule import ReminderSchedule -from apps.slack.models.member import Member from apps.owasp.models.entity_channel import EntityChannel +from apps.slack.models.member import Member class TestReminderScheduleModel: diff --git a/backend/tests/apps/nest/models/reminder_test.py b/backend/tests/apps/nest/models/reminder_test.py index a25e7f0bfb..c31c7f1fcd 100644 --- a/backend/tests/apps/nest/models/reminder_test.py +++ b/backend/tests/apps/nest/models/reminder_test.py @@ -1,8 +1,8 @@ """Test cases for Reminder model.""" from apps.nest.models.reminder import Reminder -from apps.slack.models.member import Member from apps.owasp.models.entity_channel import EntityChannel +from apps.slack.models.member import Member class TestReminderModel: 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..4516e02710 100644 --- a/backend/tests/apps/slack/common/handlers/calendar_events_test.py +++ b/backend/tests/apps/slack/common/handlers/calendar_events_test.py @@ -8,7 +8,6 @@ from httplib2.error import ServerNotFoundError from apps.nest.models.google_account_authorization import GoogleAccountAuthorization -from apps.nest.models.reminder import Reminder from apps.nest.models.reminder_schedule import ReminderSchedule from apps.owasp.models.event import Event from apps.slack.common.handlers.calendar_events import ( @@ -131,30 +130,25 @@ def test_get_events_blocks_no_events(self, mock_authorize, mock_google_calendar_ @patch("apps.nest.models.reminder_schedule.ReminderSchedule.objects.filter") def test_get_reminders_blocks_success(self, mock_filter): """Test get_reminders_blocks function.""" + mock_reminder_schedule_1 = MagicMock(spec=ReminderSchedule) + mock_reminder_schedule_1.pk = 1 + mock_reminder_schedule_1.reminder.entity_channel.channel.name = "test-channel" + mock_reminder_schedule_1.reminder.message = "Test reminder message" + mock_reminder_schedule_1.scheduled_time = timezone.now() + timezone.timedelta(minutes=10) + mock_reminder_schedule_1.recurrence = "once" + + mock_reminder_schedule_2 = MagicMock(spec=ReminderSchedule) + mock_reminder_schedule_2.pk = 2 + mock_reminder_schedule_2.reminder.entity_channel.channel.name = "another-channel" + mock_reminder_schedule_2.reminder.message = "Another reminder message" + mock_reminder_schedule_2.scheduled_time = timezone.now() + timezone.timedelta(minutes=20) + mock_reminder_schedule_2.recurrence = "daily" + mock_filter.return_value.order_by.return_value = [ - MagicMock( - spec=ReminderSchedule, - pk=1, - reminder=MagicMock( - spec=Reminder, - channel_id="C123456", - message="Test reminder message", - ), - scheduled_time=timezone.now() + timezone.timedelta(minutes=10), - recurrence="once", - ), - MagicMock( - spec=ReminderSchedule, - pk=2, - reminder=MagicMock( - spec=Reminder, - channel_id="C654321", - message="Another reminder message", - ), - scheduled_time=timezone.now() + timezone.timedelta(minutes=20), - recurrence="daily", - ), + mock_reminder_schedule_1, + mock_reminder_schedule_2, ] + blocks = get_reminders_blocks("test_slack_user_id") assert len(blocks) == 3 # 1 header + 2 reminders assert "*Your upcoming reminders:*" in blocks[0]["text"]["text"] @@ -178,83 +172,207 @@ 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.models.google_account_authorization.GoogleAccountAuthorization.authorize") + @patch("apps.nest.handlers.calendar_events.get_calendar_id") + @patch("apps.nest.clients.google_calendar.GoogleCalendarClient") + @patch("apps.slack.models.member.Member.objects.get") + @patch("apps.slack.models.conversation.Conversation.objects.get") + @patch("apps.owasp.models.entity_channel.EntityChannel.objects.get") + @patch("django.contrib.contenttypes.models.ContentType.objects.get_for_model") + def test_get_setting_reminder_blocks_success( + self, + mock_get_content_type, + mock_get_entity_channel, + mock_get_conversation, + mock_get_member, + mock_google_calendar_client, + mock_get_calendar_id, + mock_authorize, + mock_slack_scheduler, + mock_set_reminder, + ): """Test get_setting_reminder_blocks function for successful reminder setting.""" - mock_set_reminder.return_value = MagicMock( - spec=ReminderSchedule, - 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"), - ), - scheduled_time=timezone.now() + timezone.timedelta(minutes=10), - recurrence="daily", + # Setup mocks + mock_auth = GoogleAccountAuthorization( + access_token="test_access_token", # noqa: S106 + refresh_token="test_refresh_token", # noqa: S106 + expires_at=timezone.now() + timezone.timedelta(hours=1), ) + mock_authorize.return_value = mock_auth + + mock_member = MagicMock(spec=Member) + mock_member.slack_user_id = "test_slack_user_id" + mock_get_member.return_value = mock_member + + mock_conversation = MagicMock() + mock_conversation.pk = 1 + mock_get_conversation.return_value = mock_conversation + + mock_entity_channel = MagicMock() + mock_get_entity_channel.return_value = mock_entity_channel + + mock_get_calendar_id.return_value = "calendar_id_123" + + mock_reminder_schedule = MagicMock(spec=ReminderSchedule) + mock_reminder_schedule.reminder.event.name = "Test Event" + mock_reminder_schedule.reminder.member.slack_user_id = "test_slack_user_id" + mock_reminder_schedule.reminder.entity_channel.pk = 1 + mock_reminder_schedule.scheduled_time = timezone.now() + timezone.timedelta(minutes=10) + mock_reminder_schedule.recurrence = "daily" + mock_set_reminder.return_value = mock_reminder_schedule + args = MagicMock( - channel="C123456", + channel="#test-channel", event_number=1, minutes_before=10, message=["Test", "reminder", "message"], recurrence="daily", ) - blocks = get_setting_reminder_blocks(args, "test_slack_user_id") + + blocks = get_setting_reminder_blocks(args, "test_slack_user_id", "workspace_123") + assert len(blocks) == 1 - assert "*10-minute reminder set for event " in blocks[0]["text"]["text"] - mock_set_reminder.assert_called_once_with( - channel="C123456", - event_number=1, - user_id="test_slack_user_id", - minutes_before=10, - recurrence="daily", - message="Test reminder message", - ) - mock_slack_scheduler.assert_called_once_with(mock_set_reminder.return_value) + assert "*10-minute reminder set for event 'Test Event'*" in blocks[0]["text"]["text"] + + mock_set_reminder.assert_called_once() + mock_slack_scheduler.assert_called_once_with(mock_reminder_schedule) mock_slack_scheduler.return_value.schedule.assert_called_once() mock_slack_scheduler.send_message.assert_called_once() @patch("apps.nest.handlers.calendar_events.set_reminder") - def test_get_setting_reminder_blocks_validation_error(self, mock_set_reminder): + @patch("apps.nest.models.google_account_authorization.GoogleAccountAuthorization.authorize") + @patch("apps.nest.handlers.calendar_events.get_calendar_id") + @patch("apps.nest.clients.google_calendar.GoogleCalendarClient") + @patch("apps.slack.models.member.Member.objects.get") + @patch("apps.slack.models.conversation.Conversation.objects.get") + @patch("apps.owasp.models.entity_channel.EntityChannel.objects.get") + @patch("django.contrib.contenttypes.models.ContentType.objects.get_for_model") + def test_get_setting_reminder_blocks_validation_error( + self, + mock_get_content_type, + mock_get_entity_channel, + mock_get_conversation, + mock_get_member, + mock_google_calendar_client, + mock_get_calendar_id, + mock_authorize, + mock_set_reminder, + ): """Test get_setting_reminder_blocks function when ValidationError is raised.""" + # Setup basic mocks + mock_auth = GoogleAccountAuthorization( + access_token="test_access_token", # noqa: S106 + refresh_token="test_refresh_token", # noqa: S106 + expires_at=timezone.now() + timezone.timedelta(hours=1), + ) + mock_authorize.return_value = mock_auth + mock_get_member.return_value = MagicMock(spec=Member) + mock_get_conversation.return_value = MagicMock() + mock_get_entity_channel.return_value = MagicMock() + mock_get_calendar_id.return_value = "calendar_id_123" + mock_set_reminder.side_effect = ValidationError("Invalid event number.") + args = MagicMock( - channel="C123456", + channel="#test-channel", event_number=99, minutes_before=10, message=["Test", "reminder", "message"], recurrence="daily", ) - blocks = get_setting_reminder_blocks(args, "test_slack_user_id") + + blocks = get_setting_reminder_blocks(args, "test_slack_user_id", "workspace_123") 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.models.google_account_authorization.GoogleAccountAuthorization.authorize") + @patch("apps.nest.handlers.calendar_events.get_calendar_id") + @patch("apps.nest.clients.google_calendar.GoogleCalendarClient") + @patch("apps.slack.models.member.Member.objects.get") + @patch("apps.slack.models.conversation.Conversation.objects.get") + @patch("apps.owasp.models.entity_channel.EntityChannel.objects.get") + @patch("django.contrib.contenttypes.models.ContentType.objects.get_for_model") + def test_get_setting_reminder_blocks_value_error( + self, + mock_get_content_type, + mock_get_entity_channel, + mock_get_conversation, + mock_get_member, + mock_google_calendar_client, + mock_get_calendar_id, + mock_authorize, + mock_set_reminder, + ): """Test get_setting_reminder_blocks function when ValueError is raised.""" + # Setup basic mocks + mock_auth = GoogleAccountAuthorization( + access_token="test_access_token", # noqa: S106 + refresh_token="test_refresh_token", # noqa: S106 + expires_at=timezone.now() + timezone.timedelta(hours=1), + ) + mock_authorize.return_value = mock_auth + mock_get_member.return_value = MagicMock(spec=Member) + mock_get_conversation.return_value = MagicMock() + mock_get_entity_channel.return_value = MagicMock() + mock_get_calendar_id.return_value = "calendar_id_123" + mock_set_reminder.side_effect = ValueError("Some value error occurred.") + args = MagicMock( - channel="C123456", + channel="#test-channel", event_number=1, minutes_before=10, message=["Test", "reminder", "message"], recurrence="daily", ) - blocks = get_setting_reminder_blocks(args, "test_slack_user_id") + + blocks = get_setting_reminder_blocks(args, "test_slack_user_id", "workspace_123") assert len(blocks) == 1 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.models.google_account_authorization.GoogleAccountAuthorization.authorize") + @patch("apps.nest.handlers.calendar_events.get_calendar_id") + @patch("apps.nest.clients.google_calendar.GoogleCalendarClient") + @patch("apps.slack.models.member.Member.objects.get") + @patch("apps.slack.models.conversation.Conversation.objects.get") + @patch("apps.owasp.models.entity_channel.EntityChannel.objects.get") + @patch("django.contrib.contenttypes.models.ContentType.objects.get_for_model") + def test_get_setting_reminder_blocks_service_error( + self, + mock_get_content_type, + mock_get_entity_channel, + mock_get_conversation, + mock_get_member, + mock_google_calendar_client, + mock_get_calendar_id, + mock_authorize, + mock_set_reminder, + ): """Test get_setting_reminder_blocks function when service error occurs.""" + # Setup basic mocks + mock_auth = GoogleAccountAuthorization( + access_token="test_access_token", # noqa: S106 + refresh_token="test_refresh_token", # noqa: S106 + expires_at=timezone.now() + timezone.timedelta(hours=1), + ) + mock_authorize.return_value = mock_auth + mock_get_member.return_value = MagicMock(spec=Member) + mock_get_conversation.return_value = MagicMock() + mock_get_entity_channel.return_value = MagicMock() + mock_get_calendar_id.return_value = "calendar_id_123" + mock_set_reminder.side_effect = ServerNotFoundError() + args = MagicMock( - channel="C123456", + channel="#test-channel", event_number=1, minutes_before=10, message=["Test", "reminder", "message"], recurrence="daily", ) - blocks = get_setting_reminder_blocks(args, "test_slack_user_id") + + blocks = get_setting_reminder_blocks(args, "test_slack_user_id", "workspace_123") assert len(blocks) == 1 assert "*Please check your internet connection.*" in blocks[0]["text"]["text"]