-
-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2541 from carpentries/feature/2530-refactor-simpl…
…e-actions [Emails] Refactor receivers into class-based actions
- Loading branch information
Showing
18 changed files
with
1,262 additions
and
626 deletions.
There are no files selected for viewing
90 changes: 41 additions & 49 deletions
90
amy/emails/actions/admin_signs_instructor_up_for_workshop.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.