Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(massEmail): create/register celery tasks mass emails TASK-750 #5574

Merged
merged 22 commits into from
Mar 26, 2025

Conversation

Guitlle
Copy link
Contributor

@Guitlle Guitlle commented Mar 7, 2025

🗒️ Checklist

  1. run linter locally
  2. update all related docs (API, README, inline, etc.), if any
  3. draft PR with a title <type>(<scope>)<!>: <title> TASK-1234
  4. tag PR: at least frontend or backend unless it's global
  5. fill in the template below and delete template comments
  6. review thyself: read the diff and repro the preview as written
  7. open PR & confirm that CI passes
  8. request reviewers, if needed
  9. delete this section before merging

📣 Summary

Celery task and admin action to send emails for mass email configurations asynchronously by user request on the admin screen.

📖 Description

The mass email configs admin action "Send emails" triggers a celery task for each email config that creates or uses an existing mass email job that is associated to a set of mass email records. The records are linked to a user, they start with enqueued status and when they are processed by the celery task, their status is set to sent or failed. This is work in progress, future PRs will enable more functionality.

👀 Preview steps

  1. (Optional) set the email backend: EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' to debug
  2. Go to the Mass email configs admin page
  3. Create a new mass email config instance
  4. On the mass email configs admin page enable the checkbox to select the new mass email config, then in the action dropdown select "Send emails" and click on "Go"
  5. Check that the emails were sent in the emails folder (kpi_home/emails/)
  6. Check the logs at kobo-docker/log/kpi/celery_kpi_worker.log and ensure that they mention the emails sent.

@Guitlle Guitlle marked this pull request as ready for review March 10, 2025 21:08
@Guitlle Guitlle requested review from jnm and noliveleger as code owners March 10, 2025 21:08
@Guitlle Guitlle requested review from rgraber and rajpatel24 March 10, 2025 21:08
@Guitlle Guitlle force-pushed the 750-create-register-celery-tasks-mass-emails branch from c5ad703 to b0075f7 Compare March 10, 2025 23:04
@noliveleger noliveleger requested review from rajpatel24 and removed request for jnm, noliveleger and rajpatel24 March 11, 2025 07:30
@Guitlle Guitlle requested review from rajpatel24 and rgraber March 13, 2025 23:09
@Guitlle Guitlle force-pushed the 750-create-register-celery-tasks-mass-emails branch from d8d0eb2 to 5f47ecd Compare March 24, 2025 18:38
@Guitlle Guitlle requested a review from magicznyleszek as a code owner March 24, 2025 18:38
@Guitlle Guitlle requested review from rgraber and rajpatel24 and removed request for magicznyleszek March 24, 2025 22:34
Copy link
Contributor

@rgraber rgraber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 small things and a medium thing

f'Processing {limit} records for MassEmailConfig({email_config})'
)
for record in records:
self.send_email(email_config, record)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should decrement the limit by 1 before sending each email. That way we won't risk the job dying in between sending the emails and updating the limit. Also it's safer to say we sent an email when we didn't than to say we didn't send an email when we did, which is why we should decrease and then send rather than the other way around.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's better, we should not have it all in one dict but have separate values for each config, that way it is faster

now_mock_B.return_value = now_mock.return_value
send_emails()
assert len(mail.outbox) == 20
now_mock.return_value = datetime(2025, 1, 2, 0, 0, 0, 0, pytz.UTC)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since the return value hasn't changed, do we need to reset it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it's unnecessary

f'Processing {limit} records for MassEmailConfig({email_config})'
)
for record in records:
self.send_email(email_config, record)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Becca. It would be better to decrease the limit as each email is sent instead of setting it to zero immediately. This way, if the task fails before finishing, the remaining emails for that config can still be processed in subsequent runs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think we want to decrement the counter before sending the email though. It's a little counter-intuitive but it reduces the risk of sending too many emails because the task was canceled or died between sending the email and decrementing the counter

rendered = render_template(self.template, data)
assert 'Username: Test Username' in rendered
assert 'Full name: Test Full Name' in rendered
assert 'Plan name: Test Plan Name' in rendered
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add a test to ensure that the limits decrement as each email is sent, rather than being reset immediately? :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can make that unit test

@Guitlle Guitlle requested review from rgraber and rajpatel24 March 25, 2025 17:51
Copy link
Contributor

@rgraber rgraber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more hopefully small request and a non-blocking thought about naming things


def __init__(self):
self.today = localdate()
self.cache_key_prefix = f'mass_emails_{self.today.isoformat()}_email_limits'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about changing this to emails_remaining instead of email_limits? By no means mandatory but I think it would be a little clearer.

f'Processing {limit} records for MassEmailConfig({email_config})'
)
for record in records:
self.send_email(email_config, record)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think we want to decrement the counter before sending the email though. It's a little counter-intuitive but it reduces the risk of sending too many emails because the task was canceled or died between sending the email and decrementing the counter

@rgraber
Copy link
Contributor

rgraber commented Mar 25, 2025

For posterity: to test this, I had to update the get_inactive_users query to look for users who were inactive for 2 minutes rather than a year

@Guitlle Guitlle requested a review from rgraber March 25, 2025 21:43
self.limits[email_config.id] = stored_limit
else:
logging.info('Setting up MassEmailConfig limits for the current day')
MAX_EMAILS = settings.MAX_MASS_EMAILS_PER_DAY
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The declaration of MAX_EMAILS = settings.MAX_MASS_EMAILS_PER_DAY is redundant as it is already defined above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The declaration of MAX_EMAILS = settings.MAX_MASS_EMAILS_PER_DAY is redundant as it is already defined above

Do we need this? I think it is redundant and should be removed

)
if total_limit + config_limit > MAX_EMAILS:
config_limit = MAX_EMAILS - total_limits
self.cache_limit_value(email_config, config_limit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please correct the undefined variable total_limits to total_limit

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's supposed to be total_limits, since the idea is just to give this config however many slots we have left.

Copy link
Contributor Author

@Guitlle Guitlle Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed it and changed the name to avoid confusion with existing names

@Guitlle Guitlle requested a review from rajpatel24 March 26, 2025 15:44
@Guitlle Guitlle requested a review from rajpatel24 March 26, 2025 16:31
Signed-off-by: Guillermo <lgar89@gmail.com>
Copy link
Contributor

@rgraber rgraber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work

Copy link
Contributor

@rajpatel24 rajpatel24 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Guitlle Guitlle merged commit 9d6b029 into main Mar 26, 2025
5 checks passed
@Guitlle Guitlle deleted the 750-create-register-celery-tasks-mass-emails branch March 26, 2025 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants