Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions backend/apps/nest/auth/calendar_events.py
Original file line number Diff line number Diff line change
@@ -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()
20 changes: 10 additions & 10 deletions backend/apps/nest/schedulers/calendar_events/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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)

Expand All @@ -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)
10 changes: 10 additions & 0 deletions backend/apps/nest/schedulers/calendar_events/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
12 changes: 12 additions & 0 deletions backend/apps/owasp/migrations/0055_merge_20251008_1259.py
Original file line number Diff line number Diff line change
@@ -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 = []
25 changes: 25 additions & 0 deletions backend/apps/slack/common/handlers/calendar_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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(
Expand All @@ -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")
Expand All @@ -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,
Expand Down
65 changes: 64 additions & 1 deletion backend/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading