Skip to content

Commit

Permalink
Merge pull request #2541 from carpentries/feature/2530-refactor-simpl…
Browse files Browse the repository at this point in the history
…e-actions

[Emails] Refactor receivers into class-based actions
  • Loading branch information
pbanaszkiewicz authored Oct 25, 2023
2 parents ed5bbe9 + f1e2a02 commit c6a87a1
Show file tree
Hide file tree
Showing 18 changed files with 1,262 additions and 626 deletions.
90 changes: 41 additions & 49 deletions amy/emails/actions/admin_signs_instructor_up_for_workshop.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,50 @@
from typing import Any
from datetime import datetime

from django.dispatch import receiver
from typing_extensions import Unpack

from emails.controller import EmailController, EmailControllerMissingRecipientsException
from emails.models import EmailTemplate
from emails.actions.base_action import BaseAction
from emails.signals import admin_signs_instructor_up_for_workshop_signal
from emails.types import AdminSignsInstructorUpContext, AdminSignsInstructorUpKwargs
from emails.utils import (
immediate_action,
messages_action_scheduled,
messages_missing_recipients,
messages_missing_template,
person_from_request,
)
from emails.utils import immediate_action
from recruitment.models import InstructorRecruitmentSignup
from workshops.models import Event, Person
from workshops.utils.feature_flags import feature_flag_enabled


@receiver(admin_signs_instructor_up_for_workshop_signal)
@feature_flag_enabled("EMAIL_MODULE")
def admin_signs_instructor_up_for_workshop_receiver(
sender: Any, **kwargs: Unpack[AdminSignsInstructorUpKwargs]
) -> None:
request = kwargs["request"]
person_id = kwargs["person_id"]
event_id = kwargs["event_id"]
instructor_recruitment_signup_id = kwargs["instructor_recruitment_signup_id"]

scheduled_at = immediate_action()
person = Person.objects.get(pk=person_id)
event = Event.objects.get(pk=event_id)
instructor_recruitment_signup = InstructorRecruitmentSignup.objects.get(
pk=instructor_recruitment_signup_id
)
context: AdminSignsInstructorUpContext = {
"person": person,
"event": event,
"instructor_recruitment_signup": instructor_recruitment_signup,
}


class AdminSignsInstructorUpForWorkshopReceiver(BaseAction):
signal = admin_signs_instructor_up_for_workshop_signal.signal_name
try:
scheduled_email = EmailController.schedule_email(
signal=signal,
context=context,
scheduled_at=scheduled_at,
to_header=[person.email] if person.email else [],
generic_relation_obj=instructor_recruitment_signup,
author=person_from_request(request),

def get_scheduled_at(self, **kwargs) -> datetime:
return immediate_action()

def get_context(
self, **kwargs: Unpack[AdminSignsInstructorUpKwargs]
) -> AdminSignsInstructorUpContext:
person = Person.objects.get(pk=kwargs["person_id"])
event = Event.objects.get(pk=kwargs["event_id"])
instructor_recruitment_signup = InstructorRecruitmentSignup.objects.get(
pk=kwargs["instructor_recruitment_signup_id"]
)
except EmailControllerMissingRecipientsException:
messages_missing_recipients(request, signal)
except EmailTemplate.DoesNotExist:
messages_missing_template(request, signal)
else:
messages_action_scheduled(request, signal, scheduled_email)
return {
"person": person,
"event": event,
"instructor_recruitment_signup": instructor_recruitment_signup,
}

def get_generic_relation_object(
self, context: AdminSignsInstructorUpContext, **kwargs
) -> InstructorRecruitmentSignup:
return context["instructor_recruitment_signup"]

def get_recipients(
self, context: AdminSignsInstructorUpContext, **kwargs
) -> list[str]:
person = context["person"]
return [person.email] if person.email else []


admin_signs_instructor_up_for_workshop_receiver = (
AdminSignsInstructorUpForWorkshopReceiver()
)
admin_signs_instructor_up_for_workshop_signal.connect(
admin_signs_instructor_up_for_workshop_receiver
)
181 changes: 181 additions & 0 deletions amy/emails/actions/base_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
from abc import ABC, abstractmethod
from datetime import datetime
import logging
from typing import Any

from django.contrib.contenttypes.models import ContentType
from flags.state import flag_enabled

from emails.controller import (
EmailController,
EmailControllerMissingRecipientsException,
EmailControllerMissingTemplateException,
)
from emails.models import EmailTemplate, ScheduledEmail
from emails.signals import SignalNameEnum
from emails.utils import (
messages_action_cancelled,
messages_action_scheduled,
messages_action_updated,
messages_missing_recipients,
messages_missing_template,
messages_missing_template_link,
person_from_request,
)

logger = logging.getLogger("amy")


def feature_flag_enabled(feature_flag: str, signal_name: str, **kwargs) -> bool:
request = kwargs.get("request")
if not request:
logger.debug(
f"Cannot check {feature_flag} feature flag, `request` parameter "
f"to {signal_name} is missing"
)
return False

if not flag_enabled(feature_flag, request=request):
logger.debug(f"{feature_flag} feature flag not set, skipping {signal_name}")
return False

return True


class BaseAction(ABC):
signal: SignalNameEnum

@abstractmethod
def get_scheduled_at(self, **kwargs) -> datetime:
raise NotImplementedError()

@abstractmethod
def get_context(self, **kwargs) -> dict[str, Any]:
raise NotImplementedError()

@abstractmethod
def get_generic_relation_object(self, context: dict[str, Any], **kwargs) -> Any:
raise NotImplementedError()

@abstractmethod
def get_recipients(self, context: dict[str, Any], **kwargs) -> list[str]:
raise NotImplementedError()

def __call__(self, sender: Any, **kwargs) -> None:
if not feature_flag_enabled("EMAIL_MODULE", self.signal, **kwargs):
return

request = kwargs.pop("request")

context = self.get_context(**kwargs)
scheduled_at = self.get_scheduled_at(**kwargs)

try:
scheduled_email = EmailController.schedule_email(
signal=self.signal,
context=context,
scheduled_at=scheduled_at,
to_header=self.get_recipients(context, **kwargs),
generic_relation_obj=self.get_generic_relation_object(
context, **kwargs
),
author=person_from_request(request),
)
except EmailControllerMissingRecipientsException:
messages_missing_recipients(request, self.signal)
except EmailTemplate.DoesNotExist:
messages_missing_template(request, self.signal)
else:
messages_action_scheduled(request, self.signal, scheduled_email)


class BaseActionUpdate(BaseAction):
def __call__(self, sender: Any, **kwargs) -> None:
if not feature_flag_enabled("EMAIL_MODULE", f"{self.signal}_update", **kwargs):
return

request = kwargs.pop("request")

context = self.get_context(**kwargs)
scheduled_at = self.get_scheduled_at(**kwargs)
generic_relation_obj = self.get_generic_relation_object(context, **kwargs)
signal_name = self.signal

ct = ContentType.objects.get_for_model(generic_relation_obj)
try:
scheduled_email = (
ScheduledEmail.objects.select_for_update()
.select_related("template")
.get(
generic_relation_content_type=ct,
generic_relation_pk=generic_relation_obj.pk,
template__signal=signal_name,
state="scheduled",
)
)

except ScheduledEmail.DoesNotExist:
logger.warning(
f"Scheduled email for signal {signal_name} and {generic_relation_obj=} "
"does not exist."
)
return

except ScheduledEmail.MultipleObjectsReturned:
logger.warning(
f"Too many scheduled emails for signal {signal_name} and "
f"{generic_relation_obj=}. Can't update them."
)
return

try:
scheduled_email = EmailController.update_scheduled_email(
scheduled_email=scheduled_email,
context=context,
scheduled_at=scheduled_at,
to_header=self.get_recipients(context, **kwargs),
generic_relation_obj=generic_relation_obj,
author=person_from_request(request),
)
except EmailControllerMissingRecipientsException:
messages_missing_recipients(request, signal_name)
except EmailControllerMissingTemplateException:
# Note: this is not realistically possible because the scheduled email
# is looked up using a specific template signal.
messages_missing_template_link(request, scheduled_email)
else:
messages_action_updated(request, signal_name, scheduled_email)


class BaseActionCancel(BaseAction):
# Method is not needed in this action.
def get_recipients(self, context: dict[str, Any], **kwargs) -> list[str]:
raise NotImplementedError()

# Method is not needed in this action.
def get_scheduled_at(self, **kwargs) -> datetime:
raise NotImplementedError()

def __call__(self, sender: Any, **kwargs) -> None:
if not feature_flag_enabled("EMAIL_MODULE", f"{self.signal}_remove", **kwargs):
return

request = kwargs["request"]
context = self.get_context(**kwargs)
generic_relation_obj = self.get_generic_relation_object(context, **kwargs)
signal_name = self.signal

ct = ContentType.objects.get_for_model(generic_relation_obj)
scheduled_emails = ScheduledEmail.objects.filter(
generic_relation_content_type=ct,
generic_relation_pk=generic_relation_obj.pk,
template__signal=signal_name,
state="scheduled",
).select_for_update()

for scheduled_email in scheduled_emails:
scheduled_email = EmailController.cancel_email(
scheduled_email=scheduled_email,
author=person_from_request(request),
)
messages_action_cancelled(request, signal_name, scheduled_email)
79 changes: 34 additions & 45 deletions amy/emails/actions/instructor_badge_awarded.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,41 @@
from typing import Any
from datetime import datetime

from django.dispatch import receiver
from typing_extensions import Unpack

from emails.controller import EmailController, EmailControllerMissingRecipientsException
from emails.models import EmailTemplate
from emails.actions.base_action import BaseAction
from emails.signals import instructor_badge_awarded_signal
from emails.types import InstructorBadgeAwardedContext, InstructorBadgeAwardedKwargs
from emails.utils import (
immediate_action,
messages_action_scheduled,
messages_missing_recipients,
messages_missing_template,
person_from_request,
)
from emails.utils import immediate_action
from workshops.models import Award, Person
from workshops.utils.feature_flags import feature_flag_enabled


@receiver(instructor_badge_awarded_signal)
@feature_flag_enabled("EMAIL_MODULE")
def instructor_badge_awarded_receiver(
sender: Any, **kwargs: Unpack[InstructorBadgeAwardedKwargs]
) -> None:
request = kwargs["request"]
person_id = kwargs["person_id"]
award_id = kwargs["award_id"]

scheduled_at = immediate_action()
person = Person.objects.get(pk=person_id)
award = Award.objects.get(pk=award_id)
context: InstructorBadgeAwardedContext = {
"person": person,
"award": award,
}


class InstructorBadgeAwardedReceiver(BaseAction):
signal = instructor_badge_awarded_signal.signal_name
try:
scheduled_email = EmailController.schedule_email(
signal=signal,
context=context,
scheduled_at=scheduled_at,
to_header=[person.email] if person.email else [],
generic_relation_obj=award,
author=person_from_request(request),
)
except EmailControllerMissingRecipientsException:
messages_missing_recipients(request, signal)
except EmailTemplate.DoesNotExist:
messages_missing_template(request, signal)
else:
messages_action_scheduled(request, signal, scheduled_email)

def get_scheduled_at(self, **kwargs) -> datetime:
return immediate_action()

def get_context(
self, **kwargs: Unpack[InstructorBadgeAwardedKwargs]
) -> InstructorBadgeAwardedContext:
person = Person.objects.get(pk=kwargs["person_id"])
award = Award.objects.get(pk=kwargs["award_id"])
return {
"person": person,
"award": award,
}

def get_generic_relation_object(
self, context: InstructorBadgeAwardedContext, **kwargs
) -> Award:
return context["award"]

def get_recipients(
self, context: InstructorBadgeAwardedContext, **kwargs
) -> list[str]:
person = context["person"]
return [person.email] if person.email else []


instructor_badge_awarded_receiver = InstructorBadgeAwardedReceiver()
instructor_badge_awarded_signal.connect(instructor_badge_awarded_receiver)
Loading

0 comments on commit c6a87a1

Please sign in to comment.