Skip to content

Commit

Permalink
Merge pull request #2729 from carpentries/feature/2728-complex-strate…
Browse files Browse the repository at this point in the history
…gy-for-instructor-declined-for-workshop

[Emails] Complex strategy for instructor declined for workshop
  • Loading branch information
pbanaszkiewicz authored Dec 22, 2024
2 parents 791c084 + 4146d94 commit cf83e8c
Show file tree
Hide file tree
Showing 10 changed files with 1,310 additions and 81 deletions.
4 changes: 4 additions & 0 deletions amy/emails/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@
)
from emails.actions.instructor_badge_awarded import instructor_badge_awarded_receiver
from emails.actions.instructor_confirmed_for_workshop import (
instructor_confirmed_for_workshop_cancel_receiver,
instructor_confirmed_for_workshop_receiver,
instructor_confirmed_for_workshop_update_receiver,
)
from emails.actions.instructor_declined_from_workshop import (
instructor_declined_from_workshop_cancel_receiver,
instructor_declined_from_workshop_receiver,
instructor_declined_from_workshop_update_receiver,
)
from emails.actions.instructor_signs_up_for_workshop import (
instructor_signs_up_for_workshop_receiver,
Expand Down
288 changes: 251 additions & 37 deletions amy/emails/actions/instructor_declined_from_workshop.py
Original file line number Diff line number Diff line change
@@ -1,79 +1,293 @@
from datetime import datetime
import logging

from django.contrib.contenttypes.models import ContentType
from django.http import HttpRequest
from django.utils import timezone
from typing_extensions import Unpack

from emails.actions.base_action import BaseAction
from emails.actions.base_action import BaseAction, BaseActionCancel, BaseActionUpdate
from emails.actions.base_strategy import run_strategy
from emails.models import ScheduledEmail, ScheduledEmailStatus
from emails.schemas import ContextModel, ToHeaderModel
from emails.signals import instructor_declined_from_workshop_signal
from emails.types import InstructorDeclinedContext, InstructorDeclinedKwargs
from emails.utils import api_model_url, immediate_action
from emails.signals import (
INSTRUCTOR_DECLINED_FROM_WORKSHOP_SIGNAL_NAME,
Signal,
instructor_declined_from_workshop_cancel_signal,
instructor_declined_from_workshop_signal,
instructor_declined_from_workshop_update_signal,
)
from emails.types import (
InstructorDeclinedContext,
InstructorDeclinedKwargs,
StrategyEnum,
)
from emails.utils import api_model_url, immediate_action, log_condition_elements
from recruitment.models import InstructorRecruitmentSignup
from workshops.models import Event, Person
from workshops.models import Event, Person, TagQuerySet

logger = logging.getLogger("amy")


def instructor_declined_from_workshop_strategy(
signup: InstructorRecruitmentSignup,
) -> StrategyEnum:
logger.info(f"Running InstructorDeclinedFromWorkshop strategy for {signup=}")

signup_is_declined = signup.state == "d"
person_email_exists = bool(signup.person.email)
event = signup.recruitment.event
carpentries_tags = event.tags.filter(
name__in=TagQuerySet.CARPENTRIES_TAG_NAMES
).exclude(name__in=TagQuerySet.NON_CARPENTRIES_TAG_NAMES)
centrally_organised = (
event.administrator and event.administrator.domain != "self-organized"
)
start_date_in_future = event.start and event.start >= timezone.now().date()

log_condition_elements(
signup=signup,
signup_pk=signup.pk,
signup_is_declined=signup_is_declined,
event=event,
person_email_exists=person_email_exists,
carpentries_tags=carpentries_tags,
centrally_organised=centrally_organised,
start_date_in_future=start_date_in_future,
)

email_should_exist = (
signup_is_declined
and person_email_exists
and carpentries_tags
and centrally_organised
and start_date_in_future
)
logger.debug(f"{email_should_exist=}")

ct = ContentType.objects.get_for_model(InstructorRecruitmentSignup)
email_exists = ScheduledEmail.objects.filter(
generic_relation_content_type=ct,
generic_relation_pk=signup.pk,
template__signal=INSTRUCTOR_DECLINED_FROM_WORKSHOP_SIGNAL_NAME,
state=ScheduledEmailStatus.SCHEDULED,
).exists()
logger.debug(f"{email_exists=}")

if not email_exists and email_should_exist:
result = StrategyEnum.CREATE
elif email_exists and not email_should_exist:
result = StrategyEnum.CANCEL
elif email_exists and email_should_exist:
result = StrategyEnum.UPDATE
else:
result = StrategyEnum.NOOP

logger.debug(f"InstructorDeclinedFromWorkshop strategy {result = }")
return result


def run_instructor_declined_from_workshop_strategy(
strategy: StrategyEnum,
request: HttpRequest,
signup: InstructorRecruitmentSignup,
**kwargs,
) -> None:
signal_mapping: dict[StrategyEnum, Signal | None] = {
StrategyEnum.CREATE: instructor_declined_from_workshop_signal,
StrategyEnum.UPDATE: instructor_declined_from_workshop_update_signal,
StrategyEnum.CANCEL: instructor_declined_from_workshop_cancel_signal,
StrategyEnum.NOOP: None,
}
return run_strategy(
strategy,
signal_mapping,
request,
sender=signup,
signup=signup,
**kwargs,
)


def get_scheduled_at(**kwargs: Unpack[InstructorDeclinedKwargs]) -> datetime:
return immediate_action()


def get_context(
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> InstructorDeclinedContext:
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"]
)
return {
"person": person,
"event": event,
"instructor_recruitment_signup": instructor_recruitment_signup,
}


def get_context_json(context: InstructorDeclinedContext) -> ContextModel:
return ContextModel(
{
"person": api_model_url("person", context["person"].pk),
"event": api_model_url("event", context["event"].pk),
"instructor_recruitment_signup": api_model_url(
"instructorrecruitmentsignup",
context["instructor_recruitment_signup"].pk,
),
},
)


def get_generic_relation_object(
context: InstructorDeclinedContext,
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> InstructorRecruitmentSignup:
return context["instructor_recruitment_signup"]


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


def get_recipients_context_json(
context: InstructorDeclinedContext,
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> ToHeaderModel:
return ToHeaderModel(
[
{
"api_uri": api_model_url("person", context["person"].pk),
"property": "email",
}, # type: ignore
],
)


class InstructorDeclinedFromWorkshopReceiver(BaseAction):
signal = instructor_declined_from_workshop_signal.signal_name

def get_scheduled_at(self, **kwargs: Unpack[InstructorDeclinedKwargs]) -> datetime:
return immediate_action()
return get_scheduled_at(**kwargs)

def get_context(
self, **kwargs: Unpack[InstructorDeclinedKwargs]
) -> InstructorDeclinedContext:
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"]
)
return {
"person": person,
"event": event,
"instructor_recruitment_signup": instructor_recruitment_signup,
}
return get_context(**kwargs)

def get_context_json(self, context: InstructorDeclinedContext) -> ContextModel:
return ContextModel(
{
"person": api_model_url("person", context["person"].pk),
"event": api_model_url("event", context["event"].pk),
"instructor_recruitment_signup": api_model_url(
"instructorrecruitmentsignup",
context["instructor_recruitment_signup"].pk,
),
},
)
return get_context_json(context)

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

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

def get_recipients_context_json(
self,
context: InstructorDeclinedContext,
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> ToHeaderModel:
return ToHeaderModel(
[
{
"api_uri": api_model_url("person", context["person"].pk),
"property": "email",
}, # type: ignore
],
)
return get_recipients_context_json(context, **kwargs)


class InstructorDeclinedFromWorkshopUpdateReceiver(BaseActionUpdate):
signal = instructor_declined_from_workshop_signal.signal_name

def get_scheduled_at(self, **kwargs: Unpack[InstructorDeclinedKwargs]) -> datetime:
return get_scheduled_at(**kwargs)

def get_context(
self, **kwargs: Unpack[InstructorDeclinedKwargs]
) -> InstructorDeclinedContext:
return get_context(**kwargs)

def get_context_json(self, context: InstructorDeclinedContext) -> ContextModel:
return get_context_json(context)

def get_generic_relation_object(
self,
context: InstructorDeclinedContext,
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> InstructorRecruitmentSignup:
return get_generic_relation_object(context, **kwargs)

def get_recipients(
self,
context: InstructorDeclinedContext,
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> list[str]:
return get_recipients(context, **kwargs)

def get_recipients_context_json(
self,
context: InstructorDeclinedContext,
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> ToHeaderModel:
return get_recipients_context_json(context, **kwargs)


class InstructorDeclinedFromWorkshopCancelReceiver(BaseActionCancel):
signal = instructor_declined_from_workshop_signal.signal_name

def get_context(
self, **kwargs: Unpack[InstructorDeclinedKwargs]
) -> InstructorDeclinedContext:
return get_context(**kwargs)

def get_context_json(self, context: InstructorDeclinedContext) -> ContextModel:
return get_context_json(context)

def get_generic_relation_object(
self,
context: InstructorDeclinedContext,
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> InstructorRecruitmentSignup:
return get_generic_relation_object(context, **kwargs)

def get_recipients(
self,
context: InstructorDeclinedContext,
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> list[str]:
return get_recipients(context, **kwargs)

def get_recipients_context_json(
self,
context: InstructorDeclinedContext,
**kwargs: Unpack[InstructorDeclinedKwargs],
) -> ToHeaderModel:
return get_recipients_context_json(context, **kwargs)


instructor_declined_from_workshop_receiver = InstructorDeclinedFromWorkshopReceiver()
instructor_declined_from_workshop_signal.connect(
instructor_declined_from_workshop_receiver
)
instructor_declined_from_workshop_update_receiver = (
InstructorDeclinedFromWorkshopUpdateReceiver()
)
instructor_declined_from_workshop_update_signal.connect(
instructor_declined_from_workshop_update_receiver
)
instructor_declined_from_workshop_cancel_receiver = (
InstructorDeclinedFromWorkshopCancelReceiver()
)
instructor_declined_from_workshop_cancel_signal.connect(
instructor_declined_from_workshop_cancel_receiver
)
11 changes: 8 additions & 3 deletions amy/emails/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,14 @@ def triple_signals(name: str, context_type: Any) -> tuple[Signal, Signal, Signal
SignalNameEnum.instructor_confirmed_for_workshop,
InstructorConfirmedContext,
)
instructor_declined_from_workshop_signal = Signal(
signal_name=SignalNameEnum.instructor_declined_from_workshop,
context_type=InstructorDeclinedContext,
INSTRUCTOR_DECLINED_FROM_WORKSHOP_SIGNAL_NAME = "instructor_declined_from_workshop"
(
instructor_declined_from_workshop_signal,
instructor_declined_from_workshop_update_signal,
instructor_declined_from_workshop_cancel_signal,
) = triple_signals(
SignalNameEnum.instructor_declined_from_workshop,
InstructorDeclinedContext,
)
instructor_signs_up_for_workshop_signal = Signal(
signal_name=SignalNameEnum.instructor_signs_up_for_workshop,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from emails.signals import instructor_confirmed_for_workshop_signal
from emails.utils import api_model_url, scalar_value_url
from recruitment.models import InstructorRecruitment, InstructorRecruitmentSignup
from workshops.models import Event, Organization, Person, Role, Task
from workshops.models import Event, Organization, Person, Role, Tag, Task
from workshops.tests.base import TestBase


Expand Down Expand Up @@ -249,6 +249,8 @@ def test_integration(
) -> None:
# Arrange
self._setUpRoles()
self._setUpTags()
self._setUpAdministrators()
host = Organization.objects.create(domain="test.com", fullname="Test")
person = Person.objects.create_user( # type: ignore
username="test_test",
Expand All @@ -269,8 +271,13 @@ def test_integration(
person=person,
)
event = Event.objects.create(
slug="test-event", host=host, start=date(2023, 7, 22), end=date(2023, 7, 23)
slug="test-event",
host=host,
start=date.today() + timedelta(days=7),
end=date.today() + timedelta(days=8),
administrator=Organization.objects.get(domain="software-carpentry.org"),
)
event.tags.add(Tag.objects.get(name="SWC"))
recruitment = InstructorRecruitment.objects.create(status="o", event=event)
signup = InstructorRecruitmentSignup.objects.create(
recruitment=recruitment, person=person
Expand Down
Loading

0 comments on commit cf83e8c

Please sign in to comment.