Implement 'send_email_alerts'

* cascading from team to project; user is base-level-default
* implemented at form-level
* implemented when emails are actually sent
This commit is contained in:
Klaas van Schelven
2024-06-13 13:23:14 +02:00
parent 35448c9855
commit 95cb39f5af
7 changed files with 126 additions and 17 deletions

View File

@@ -3,14 +3,48 @@ from snappea.decorators import shared_task
from django.template.defaultfilters import truncatechars
from projects.models import ProjectMembership
from teams.models import TeamMembership
from bugsink.app_settings import get_settings
from bugsink.utils import send_rendered_email
def _get_users_for_email_alert(issue):
# more like memberships as currently implemented :-D
return ProjectMembership.objects.filter(project=issue.project, send_email_alerts=True).select_related("user")
# _perhaps_ it's possible to make some super-smart 3-way join that does the below, but I'd say that "just doing it
# with a (constant) few separate queries and some work in Python" is absolutely fine. (especially for something in
# an async task)
pms = list(
ProjectMembership.objects.filter(project=issue.project).exclude(send_email_alerts=False).select_related("user"))
user_ids = [pm.user_id for pm in pms]
tms = {tm.user_id: tm for tm in TeamMembership.objects.filter(team=issue.project.team, user_id__in=user_ids)}
for pm in pms:
if pm.send_email_alerts is True:
yield pm.user
# elif pm.send_email_alerts is False: # we do this with the .exclude in the above
# continue
else: # (pm.send_email_alerts is None)
if pm.user_id in tms:
if tms[pm.user_id].send_email_alerts is True:
yield pm.user
elif tms[pm.user_id].send_email_alerts is False:
continue
else: # tm exists, but is set to None
if pm.user.send_email_alerts is True:
yield pm.user
elif pm.user.send_email_alerts is False:
continue
else: # no team-level definition
if pm.user.send_email_alerts is True:
yield pm.user
elif pm.user.send_email_alerts is False:
continue
# there is no None at this level
@shared_task
@@ -32,11 +66,11 @@ def _send_alert(issue_id, state_description, alert_article, alert_reason, **kwar
from issues.models import Issue # avoid circular import
issue = Issue.objects.get(id=issue_id)
for membership in _get_users_for_email_alert(issue):
for user in _get_users_for_email_alert(issue):
send_rendered_email(
subject=truncatechars(f'"{issue.title()}" in "{issue.project.name}" ({state_description})', 100),
base_template_name="mails/issue_alert",
recipient_list=[membership.user.email],
recipient_list=[user.email],
context={
"site_title": get_settings().SITE_TITLE,
"base_url": get_settings().BASE_URL + "/",

View File

@@ -7,8 +7,9 @@ from django.template.loader import get_template
from issues.factories import get_or_create_issue
from projects.models import Project, ProjectMembership
from events.factories import create_event
from teams.models import Team, TeamMembership
from .tasks import send_new_issue_alert, send_regression_alert, send_unmute_alert
from .tasks import send_new_issue_alert, send_regression_alert, send_unmute_alert, _get_users_for_email_alert
from .views import DEBUG_CONTEXTS
User = get_user_model()
@@ -83,3 +84,51 @@ class TestAlertSending(DjangoTestCase):
self.assertTrue(
"{{ %s" % variable in template.template.source, "'{{ %s ' not in %s template" % (variable, type_))
def test_get_users_for_email_alert(self):
team = Team.objects.create(name="Test team")
project = Project.objects.create(name="Test project", team=team)
user = User.objects.create_user(username="testuser", email="test@example.org", send_email_alerts=True)
issue, _ = get_or_create_issue(project=project)
# no ProjectMembership, user should not be included
self.assertEqual(list(_get_users_for_email_alert(issue)), [])
# ProjectMembership w/ send=False, should not be included
pm = ProjectMembership.objects.create(project=project, user=user, send_email_alerts=False)
self.assertEqual(list(_get_users_for_email_alert(issue)), [])
# ProjectMembership w/ send=True, should be included
pm.send_email_alerts = True
pm.save()
self.assertEqual(list(_get_users_for_email_alert(issue)), [user])
# Set send=None, fall back to User (which has True)
pm.send_email_alerts = None
pm.save()
self.assertEqual(list(_get_users_for_email_alert(issue)), [user])
# (User has False)
user.send_email_alerts = False
user.save()
self.assertEqual(list(_get_users_for_email_alert(issue)), [])
# Insert TeamMembership - this provides an intermediate layer of configuration between User and
# ProjectMembership; we start with send=True at the tm level and expect the user to be included
tm = TeamMembership.objects.create(team=team, user=user, send_email_alerts=True)
self.assertEqual(list(_get_users_for_email_alert(issue)), [user])
# Set send=False at the tm level, user should not be included
tm.send_email_alerts = False
tm.save()
self.assertEqual(list(_get_users_for_email_alert(issue)), [])
# Set send=None at the tm level, back to the user level (which is False)
tm.send_email_alerts = None
tm.save()
self.assertEqual(list(_get_users_for_email_alert(issue)), [])
# Set send=True at the user level, user should be included
user.send_email_alerts = True
user.save()
self.assertEqual(list(_get_users_for_email_alert(issue)), [user])