diff --git a/alerts/tasks.py b/alerts/tasks.py index c8c09a2..c95f8a8 100644 --- a/alerts/tasks.py +++ b/alerts/tasks.py @@ -1,7 +1,9 @@ from celery import shared_task +from django.conf import settings +from django.template.defaultfilters import truncatechars + from projects.models import ProjectMembership -from issues.models import Issue from .utils import send_rendered_email @@ -13,24 +15,36 @@ def _get_users_for_email_alert(issue): @shared_task def send_new_issue_alert(issue_id): - issue = Issue.objects.get(id=issue_id) - for membership in _get_users_for_email_alert(issue): - send_rendered_email( - subject=f"New issue: {issue.title()}", - base_template_name="alerts/new_issue", - recipient_list=[membership.user.email], - context={ - "issue": issue, - "project": issue.project, - }, - ) + _send_alert(issue_id, "New issue:", "a", "NEW") @shared_task def send_regression_alert(issue_id): - raise NotImplementedError("TODO") + _send_alert(issue_id, "Regression:", "a", "REGRESSED") @shared_task def send_unmute_alert(issue_id): - raise NotImplementedError("TODO") + _send_alert(issue_id, "Unmuted issue:", "an", "UNMUTED") + + +def _send_alert(issue_id, subject_prefix, alert_article, alert_reason): + from issues.models import Issue # avoid circular import + + issue = Issue.objects.get(id=issue_id) + for membership in _get_users_for_email_alert(issue): + send_rendered_email( + subject=truncatechars(f"{subject_prefix} {issue.title()} in {issue.project.name}", 100), + base_template_name="alerts/issue_alert", + recipient_list=[membership.user.email], + context={ + "site_name": settings.SITE_NAME, + "base_url": settings.BASE_URL + "/", + "issue_title": issue.title(), + "project_name": issue.project.name, + "issue_url": settings.BASE_URL + issue.get_absolute_url(), + "alert_article": alert_article, + "alert_reason": alert_reason, + "settings_url": settings.BASE_URL + "/", # TODO + }, + ) diff --git a/alerts/templates/alerts/new_issue.html b/alerts/templates/alerts/issue_alert.html similarity index 96% rename from alerts/templates/alerts/new_issue.html rename to alerts/templates/alerts/issue_alert.html index fd5b920..704e94c 100644 --- a/alerts/templates/alerts/new_issue.html +++ b/alerts/templates/alerts/issue_alert.html @@ -462,7 +462,7 @@ - Bugsink + {{ site_name }} @@ -470,14 +470,14 @@ - {% Body content #} + {# Body content #} + + {# Sub copy #} diff --git a/alerts/tests.py b/alerts/tests.py index 7ce503c..51a4e44 100644 --- a/alerts/tests.py +++ b/alerts/tests.py @@ -1,3 +1,83 @@ from django.test import TestCase -# Create your tests here. +from django.core import mail +from django.contrib.auth.models import User +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 .tasks import send_new_issue_alert, send_regression_alert, send_unmute_alert +from .views import DEBUG_CONTEXTS + + +class TestAlertSending(TestCase): + + def test_send_new_issue_alert(self): + project = Project.objects.create(name="Test project") + + user = User.objects.create_user(username="testuser", email="test@example.org") + ProjectMembership.objects.create( + project=project, + user=user, + send_email_alerts=True, + ) + + issue, _ = get_or_create_issue(project=project) + create_event(project=project, issue=issue) + + send_new_issue_alert(issue.id) + + self.assertEqual(len(mail.outbox), 1) + + def test_send_regression_alert(self): + project = Project.objects.create(name="Test project") + + user = User.objects.create_user(username="testuser", email="test@example.org") + ProjectMembership.objects.create( + project=project, + user=user, + send_email_alerts=True, + ) + + issue, _ = get_or_create_issue(project=project) + create_event(project=project, issue=issue) + + send_regression_alert(issue.id) + + self.assertEqual(len(mail.outbox), 1) + + def test_send_unmute_alert(self): + project = Project.objects.create(name="Test project") + + user = User.objects.create_user(username="testuser", email="test@example.org") + ProjectMembership.objects.create( + project=project, + user=user, + send_email_alerts=True, + ) + + issue, _ = get_or_create_issue(project=project) + create_event(project=project, issue=issue) + + send_unmute_alert(issue.id) + + self.assertEqual(len(mail.outbox), 1) + + def test_txt_and_html_have_relevant_variables_defined(self): + example_context = DEBUG_CONTEXTS["issue_alert"] + html_template = get_template("alerts/issue_alert.html") + text_template = get_template("alerts/issue_alert.txt") + + unused_in_text = [ + "base_url", # link to the site is not included at the top of the text template + ] + + for type_, template in [("html", html_template), ("text", text_template)]: + for variable in example_context.keys(): + if type_ == "text" and variable in unused_in_text: + continue + + self.assertTrue( + "{{ %s" % variable in template.template.source, "'{{ %s ' not in %s template" % (variable, type_)) diff --git a/alerts/utils.py b/alerts/utils.py index 3fa7fd7..b3c8918 100644 --- a/alerts/utils.py +++ b/alerts/utils.py @@ -1,5 +1,4 @@ from django.core.mail import EmailMultiAlternatives -from django.template import Context from django.template.loader import get_template @@ -7,8 +6,8 @@ def send_rendered_email(subject, base_template_name, recipient_list, context=Non if context is None: context = {} - html_content = get_template(base_template_name + ".html").render(Context(context)) - text_content = get_template(base_template_name + ".txt").render(Context(context)) + html_content = get_template(base_template_name + ".html").render(context) + text_content = get_template(base_template_name + ".txt").render(context) # Configure and send an EmailMultiAlternatives msg = EmailMultiAlternatives( diff --git a/alerts/views.py b/alerts/views.py index 81d64a8..b7c3ded 100644 --- a/alerts/views.py +++ b/alerts/views.py @@ -2,11 +2,15 @@ from django.shortcuts import render from django.conf import settings DEBUG_CONTEXTS = { - "new_issue": { + "issue_alert": { + "site_name": settings.SITE_NAME, "base_url": settings.BASE_URL + "/", "issue_title": "AttributeError: 'NoneType' object has no attribute 'data'", "project_name": "My first project", + "alert_article": "a", + "alert_reason": "NEW", "issue_url": settings.BASE_URL + "/issues/issue/00000000-0000-0000-0000-000000000000/", + "settings_url": settings.BASE_URL + "/", # TODO }, } diff --git a/bugsink/settings.py b/bugsink/settings.py index 99e4498..228d4f2 100644 --- a/bugsink/settings.py +++ b/bugsink/settings.py @@ -164,6 +164,7 @@ if SENTRY_DSN is not None: ) BASE_URL = "http://bugsink:9000" # no trailing slash +SITE_NAME = "Bugsink" # you can customize this as e.g. "My Bugsink" or "Bugsink for My Company" CELERY_BROKER_URL = 'amqp://bugsink:bugsink@localhost/' diff --git a/issues/factories.py b/issues/factories.py index fbdfaea..b6bc014 100644 --- a/issues/factories.py +++ b/issues/factories.py @@ -2,12 +2,19 @@ import uuid from django.utils import timezone +from projects.models import Project + from .models import Issue from .utils import get_hash_for_data -def get_or_create_issue(project, event_data): - # create issue for testing purposes (code basically stolen from ingest/views.py) +def get_or_create_issue(project=None, event_data=None): + """create issue for testing purposes (code basically stolen from ingest/views.py)""" + if event_data is None: + event_data = create_event_data() + if project is None: + project = Project.objects.create(name="Test project") + hash_ = get_hash_for_data(event_data) issue, issue_created = Issue.objects.get_or_create( project=project, @@ -17,7 +24,7 @@ def get_or_create_issue(project, event_data): def create_event_data(): - # create minimal event data that is valid as per from_json() + """create minimal event data that is valid as per from_json()""" return { "event_id": uuid.uuid4().hex, diff --git a/issues/models.py b/issues/models.py index 51fdcc4..10db238 100644 --- a/issues/models.py +++ b/issues/models.py @@ -5,6 +5,7 @@ import uuid from django.db import models from bugsink.volume_based_condition import VolumeBasedCondition +from alerts.tasks import send_unmute_alert class Issue(models.Model): @@ -129,7 +130,6 @@ class IssueStateManager(object): # methods in this class. from bugsink.registry import get_pc_registry, UNMUTE_PURPOSE # avoid circular import - from alerts.tasks import send_unmute_alert if issue.is_muted: # we check on is_muted explicitly: it may be so that multiple unmute conditions happens simultaneously (and