-
-
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 #2667 from carpentries/feature/2462-emails-documen…
…tation [Emails] Updated technical documentation
- Loading branch information
Showing
6 changed files
with
174 additions
and
7 deletions.
There are no files selected for viewing
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
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
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,4 +1,4 @@ | ||
# Automated emails | ||
# Automated emails v1 (2019) | ||
|
||
GitHub project link unavailable. | ||
|
||
|
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,144 @@ | ||
# Automated emails v2 (2023) | ||
|
||
Github project: https://github.com/orgs/carpentries/projects/10 | ||
|
||
The aim of this project is to replace the current system of automated emails, which is | ||
based on Redis and Python RQ, with a new system based on decoupled systems. | ||
The new system should be more robust, easier to maintain, and allow for more complex | ||
scheduling of emails, at the same time allowing to add the new types of emails more | ||
easily. | ||
|
||
Apart from being decoupled, another important feature of this system is that it fetches | ||
the newest representation of objects used in the scheduled email content using AMY API, | ||
and creating new actions is simpler and more elegant thanks to the use of | ||
[Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/). | ||
|
||
|
||
## Infrastructure | ||
|
||
At the core of automated emails there are: | ||
|
||
* AMY API exposing endpoints for managing scheduled emails, | ||
* AMY API exposing endpoints for details of objects used in emails' context, | ||
* A separate application for accessing the AMY API and sending emails. | ||
|
||
At the core of the new system in AMY there are used: | ||
|
||
* Django signals for triggering actions, | ||
* a controller for managing scheduled emails (scheduling, rescheduling, updating, | ||
cancelling, etc.), | ||
* types defined for improved type-safety, | ||
* management panel for viewing and managing scheduled emails. | ||
|
||
## How it works | ||
|
||
Emails can be scheduled by sending a signal with a specific | ||
payload. There always is one signal for scheduling the email, but | ||
some emails allow for updating or cancelling them, which is done | ||
through two other signals. | ||
|
||
To help deciding if an email should be sent, updated or cancelled, | ||
these emails provide strategies, which implement checks for | ||
conditions that should be met for the email to be sent (or updated, | ||
or cancelled). | ||
|
||
Once the email has been scheduled, it is stored in the database as | ||
`ScheduledEmail` record. This record contains all the information | ||
needed to send the email, including the email's content, the time | ||
when it should be sent, and the email's context objects. | ||
|
||
The context contains information about the objects that should be | ||
used when generating MD and HTML content of the email. It consists of | ||
model name and model's primary key, which allows to fetch the object | ||
from the API when rendering the email. | ||
|
||
Once emails have been scheduled, they can be retrieved by the email worker. This is | ||
a Python lambda application that runs every 5 minutes, fetches the emails and sends | ||
them. The accurate algorithm is described in another section. | ||
|
||
## Email worker algorithm | ||
|
||
After the worker sets up, it fetches all emails that should be sent by now. Then it | ||
processes them individually asynchronously: | ||
|
||
1. Lock the email record to prevent UI work on it. | ||
2. Create context objects for email recipient list and email body content. | ||
3. Create the email context with actual data from the API. | ||
4. Create the email recipient list with actual data from the API. | ||
5. Render the email body and subject with Jinja2 and the context. | ||
6. Render the Markdown version of the email body (this generates the HTML). | ||
7. Send the email. | ||
8. Update the email record with the status. | ||
|
||
At any step this process can fail and the email will be marked as failed. The worker | ||
will pick it up again in the next run. | ||
|
||
## Implementation of new actions | ||
|
||
All actions are defined in `emails.actions` module. Each action is a class | ||
inheriting from `BaseAction` class (for scheduling emails). If a specific action | ||
could allow for updating or cancelling, it should consist of 2 additional classes | ||
inheriting from `BaseActionUpdate` and `BaseActionCancel` respectively. | ||
|
||
Each action must implement the following required methods and fields: | ||
|
||
1. inheriting from `BaseAction`: | ||
* `signal` - parameter that contains a value uniquely identifying the action signal, | ||
and therefore also the email template | ||
* `get_scheduled_at()` - method that calculates when the action should be run | ||
* `get_context()` - method that returns the context for the email | ||
* `get_context_json()` - method that returns the context for the email in JSON | ||
format (this is used by the email worker to fetch the context from the API) | ||
* `get_generic_relation_object()` - method that returns the main object for the | ||
email (e.g. an event or a person) | ||
* `get_recipients()` - method that returns the list of recipients of the email | ||
* `get_recipients_context_json()` - method that returns the recipients of the email | ||
in JSON format (this is used by the email worker to fetch the recipients from the | ||
API) | ||
|
||
2. inheriting from `BaseActionUpdate`: | ||
* the same fields and methods as in `BaseAction` class | ||
|
||
3. inheriting from `BaseActionCancel`: | ||
* the same fields and methods as in `BaseAction` class, except for `get_recipients()` | ||
and `get_scheduled_at()` methods, which are not needed for the cancelling action. | ||
|
||
Each base class implements a `__call__()` method that in turn uses appropriate | ||
`EmailController` method to schedule, update, or cancel the email. | ||
|
||
### Implementing a new action - checklist | ||
|
||
1. Add new action signal name to `emails.signals.SignalNameEnum` enum. | ||
2. Define the context TypedDict in `emails.types` module. This should be a dictionary | ||
with keys and types of values that will be passed to the email template as context. | ||
3. Define the kwargs TypedDict in `emails.types` module. This should be a dictionary | ||
with keys and types of values that will be passed to the action's constructor (when | ||
the signal for email is being sent). | ||
4. Define the action class in a new module in `emails.actions` package. This class should | ||
inherit from `BaseAction` class and implement all required methods. | ||
5. If the action should allow for updating or cancelling, define additional classes | ||
inheriting from `BaseActionUpdate` and `BaseActionCancel` respectively. | ||
6. Create receivers as instances of the action classes. Link the receivers to the | ||
appropriate signals in `emails.signals` module: | ||
```python | ||
receiver = MyAction() | ||
signal.connect(receiver) | ||
``` | ||
|
||
7. If the action consists of scheduling, updating, and cancelling, create `action_strategy` and `run_action_strategy` functions. Follow examples from other actions. | ||
|
||
|
||
### Using a new action | ||
|
||
If the action contains a strategy, then using it is quite simple: | ||
|
||
```python | ||
run_action_strategy( | ||
action_strategy(object), | ||
request, | ||
object, | ||
) | ||
``` | ||
|
||
Strategies may accept other parameters, but the selected strategy and request (as in | ||
Django view request) are required. |
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
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