Skip to content

Commit

Permalink
[#2643] Host-instructors introduction (new email action)
Browse files Browse the repository at this point in the history
  • Loading branch information
pbanaszkiewicz committed May 13, 2024
1 parent db8b8be commit 74c44ee
Show file tree
Hide file tree
Showing 6 changed files with 399 additions and 0 deletions.
5 changes: 5 additions & 0 deletions amy/emails/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
from emails.actions.admin_signs_instructor_up_for_workshop import (
admin_signs_instructor_up_for_workshop_receiver,
)
from emails.actions.host_instructors_introduction import (
host_instructors_introduction_receiver,
host_instructors_introduction_remove_receiver,
host_instructors_introduction_update_receiver,
)
from emails.actions.instructor_badge_awarded import instructor_badge_awarded_receiver
from emails.actions.instructor_confirmed_for_workshop import (
instructor_confirmed_for_workshop_receiver,
Expand Down
319 changes: 319 additions & 0 deletions amy/emails/actions/host_instructors_introduction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
from datetime import datetime, timedelta
import logging
from typing import Unpack

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

from emails.actions.base_action import BaseAction, BaseActionCancel, BaseActionUpdate
from emails.actions.exceptions import EmailStrategyException
from emails.models import ScheduledEmail, ScheduledEmailStatus
from emails.schemas import ContextModel, ToHeaderModel
from emails.signals import (
HOST_INSTRUCTORS_INTRODUCTION_SIGNAL_NAME,
Signal,
host_instructors_introduction_remove_signal,
host_instructors_introduction_signal,
host_instructors_introduction_update_signal,
)
from emails.types import (
HostInstructorsIntroductionContext,
HostInstructorsIntroductionKwargs,
StrategyEnum,
)
from emails.utils import api_model_url, immediate_action
from recruitment.models import InstructorRecruitment
from workshops.models import Event, Task

logger = logging.getLogger("amy")


def host_instructors_introduction_strategy(event: Event) -> StrategyEnum:
logger.info(f"Running HostInstructorsIntroduction strategy for {event}")

not_self_organised = (
event.administrator and event.administrator.domain != "self-organized"
)
no_open_recruitment = not InstructorRecruitment.objects.filter(
status="o", event=event
).exists()
start_date_in_at_least_7days = event.start and event.start >= (
timezone.now().date() + timedelta(days=7)
)
logger.debug(
f"{no_open_recruitment=}, {not_self_organised=}, "
f"{start_date_in_at_least_7days=}"
)

active = not event.tags.filter(name__in=["cancelled", "unresponsive", "stalled"])
host = Task.objects.filter(role__name="host", event=event).first()
at_least_2_instructors = (
Task.objects.filter(role__name="instructor", event=event).count() >= 2
)
logger.debug(f"{active=}, {host=}, {at_least_2_instructors=}")

email_should_exist = (
not_self_organised
and start_date_in_at_least_7days
and active
and host
and at_least_2_instructors
and no_open_recruitment
)
logger.debug(f"{email_should_exist=}")

ct = ContentType.objects.get_for_model(event) # type: ignore
has_email_scheduled = ScheduledEmail.objects.filter(
generic_relation_content_type=ct,
generic_relation_pk=event.pk,
template__signal=HOST_INSTRUCTORS_INTRODUCTION_SIGNAL_NAME,
state=ScheduledEmailStatus.SCHEDULED,
).exists()
logger.debug(f"{has_email_scheduled=}")

if not has_email_scheduled and email_should_exist:
result = StrategyEnum.CREATE
elif has_email_scheduled and not email_should_exist:
result = StrategyEnum.REMOVE
elif has_email_scheduled and email_should_exist:
result = StrategyEnum.UPDATE
else:
result = StrategyEnum.NOOP

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


# TODO: turn into a generic function/class
def run_host_instructors_introduction_strategy(
strategy: StrategyEnum, request: HttpRequest, event: Event
) -> None:
mapping: dict[StrategyEnum, Signal | None] = {
StrategyEnum.CREATE: host_instructors_introduction_signal,
StrategyEnum.UPDATE: host_instructors_introduction_update_signal,
StrategyEnum.REMOVE: host_instructors_introduction_remove_signal,
StrategyEnum.NOOP: None,
}
if strategy not in mapping:
raise EmailStrategyException(f"Unknown strategy {strategy}")

signal = mapping[strategy]

if not signal:
logger.debug(f"Strategy {strategy} for {event} is a no-op")
return

logger.debug(f"Sending signal for {event} as result of strategy {strategy}")
signal.send(
sender=event,
request=request,
event=event,
event_start_date=event.start,
)


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


def get_context(
**kwargs: Unpack[HostInstructorsIntroductionKwargs],
) -> HostInstructorsIntroductionContext:
event = kwargs["event"]

# TODO: watch out for this
host = Task.objects.filter(role__name="host", event=event)[0]

instructors = [
task.person
for task in Task.objects.filter(role__name="instructor", event=event)
]
return {
"event": event,
"host": host.person,
"instructors": instructors,
}


def get_context_json(
**kwargs: Unpack[HostInstructorsIntroductionKwargs],
) -> ContextModel:
host = Task.objects.filter(role__name="host", event=kwargs["event"])[0]
return ContextModel(
{
"event": api_model_url("event", kwargs["event"].pk),
"host": api_model_url("person", host.person.pk),
"instructors": [
api_model_url("person", task.person.pk)
for task in Task.objects.filter(
role__name="instructor", event=kwargs["event"]
)
],
}
)


def get_generic_relation_object(
context: HostInstructorsIntroductionContext,
**kwargs: Unpack[HostInstructorsIntroductionKwargs],
) -> Event:
return context["event"]


def get_recipients(
context: HostInstructorsIntroductionContext,
**kwargs: Unpack[HostInstructorsIntroductionKwargs],
) -> list[str]:
host_part = [context["host"].email] if context["host"].email else []
instructors_part = [
instructor.email for instructor in context["instructors"] if instructor.email
]
return host_part + instructors_part


def get_recipients_context_json(
context: HostInstructorsIntroductionContext,
**kwargs: Unpack[HostInstructorsIntroductionKwargs],
) -> ToHeaderModel:
return ToHeaderModel(
[
{
"api_uri": api_model_url("person", context["host"].pk),
"property": "email",
}
]
+ [
{
"api_uri": api_model_url("person", instructor.pk),
"property": "email",
}
for instructor in context["instructors"]
], # type: ignore
)


class HostInstructorsIntroductionReceiver(BaseAction):
signal = host_instructors_introduction_signal.signal_name

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

def get_context(
self, **kwargs: Unpack[HostInstructorsIntroductionKwargs]
) -> HostInstructorsIntroductionContext:
return get_context(**kwargs)

def get_context_json(
self, **kwargs: Unpack[HostInstructorsIntroductionKwargs]
) -> ContextModel:
return get_context_json(**kwargs)

def get_generic_relation_object(
self,
context: HostInstructorsIntroductionContext,
**kwargs: Unpack[HostInstructorsIntroductionKwargs],
) -> Event:
return get_generic_relation_object(context, **kwargs)

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

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


class HostInstructorsIntroductionUpdateReceiver(BaseActionUpdate):
signal = host_instructors_introduction_signal.signal_name

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

def get_context(
self, **kwargs: Unpack[HostInstructorsIntroductionKwargs]
) -> HostInstructorsIntroductionContext:
return get_context(**kwargs)

def get_context_json(
self, **kwargs: Unpack[HostInstructorsIntroductionKwargs]
) -> ContextModel:
return get_context_json(**kwargs)

def get_generic_relation_object(
self,
context: HostInstructorsIntroductionContext,
**kwargs: Unpack[HostInstructorsIntroductionKwargs],
) -> Event:
return get_generic_relation_object(context, **kwargs)

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

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


class HostInstructorsIntroductionCancelReceiver(BaseActionCancel):
signal = host_instructors_introduction_signal.signal_name

def get_context(
self, **kwargs: Unpack[HostInstructorsIntroductionKwargs]
) -> HostInstructorsIntroductionContext:
return get_context(**kwargs)

def get_context_json(
self, **kwargs: Unpack[HostInstructorsIntroductionKwargs]
) -> ContextModel:
return get_context_json(**kwargs)

def get_generic_relation_object(
self,
context: HostInstructorsIntroductionContext,
**kwargs: Unpack[HostInstructorsIntroductionKwargs],
) -> Event:
return get_generic_relation_object(context, **kwargs)

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


host_instructors_introduction_receiver = HostInstructorsIntroductionReceiver()
host_instructors_introduction_signal.connect(host_instructors_introduction_receiver)

host_instructors_introduction_update_receiver = (
HostInstructorsIntroductionUpdateReceiver()
)
host_instructors_introduction_update_signal.connect(
host_instructors_introduction_update_receiver
)

host_instructors_introduction_remove_receiver = (
HostInstructorsIntroductionCancelReceiver()
)
host_instructors_introduction_remove_signal.connect(
host_instructors_introduction_remove_receiver
)
16 changes: 16 additions & 0 deletions amy/emails/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from emails.types import (
AdminSignsInstructorUpContext,
HostInstructorsIntroductionContext,
InstructorBadgeAwardedContext,
InstructorConfirmedContext,
InstructorDeclinedContext,
Expand All @@ -28,6 +29,7 @@ class SignalNameEnum(StrEnum):
"instructor_training_completed_not_badged"
)
new_membership_onboarding = "new_membership_onboarding"
host_instructors_introduction = "host_instructors_introduction"

@staticmethod
def choices() -> list[tuple[str, str]]:
Expand Down Expand Up @@ -126,4 +128,18 @@ def __init__(self, *args, **kwargs):
context_type=NewMembershipOnboardingContext,
)

HOST_INSTRUCTORS_INTRODUCTION_SIGNAL_NAME = "host_instructors_introduction"
host_instructors_introduction_signal = Signal(
signal_name=HOST_INSTRUCTORS_INTRODUCTION_SIGNAL_NAME,
context_type=HostInstructorsIntroductionContext,
)
host_instructors_introduction_update_signal = Signal(
signal_name=NEW_MEMBERSHIP_ONBOARDING_SIGNAL_NAME,
context_type=NewMembershipOnboardingContext,
)
host_instructors_introduction_remove_signal = Signal(
signal_name=NEW_MEMBERSHIP_ONBOARDING_SIGNAL_NAME,
context_type=NewMembershipOnboardingContext,
)

ALL_SIGNALS = [item for item in locals().values() if isinstance(item, Signal)]
10 changes: 10 additions & 0 deletions amy/emails/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ class NewMembershipOnboardingContext(TypedDict):
membership: Membership


class HostInstructorsIntroductionContext(TypedDict):
event: Event
host: Person
instructors: list[Person]


class HostInstructorsIntroductionKwargs(TypedDict):
event: Event


class StrategyEnum(StrEnum):
CREATE = "create"
UPDATE = "update"
Expand Down

0 comments on commit 74c44ee

Please sign in to comment.