diff --git a/hc/accounts/management/commands/senddeletionscheduled.py b/hc/accounts/management/commands/senddeletionscheduled.py index 457a31d8f99..5f48b7e5b59 100644 --- a/hc/accounts/management/commands/senddeletionscheduled.py +++ b/hc/accounts/management/commands/senddeletionscheduled.py @@ -39,7 +39,7 @@ def send_channel_notifications(self, profile, skip_emails): f"please contact {settings.SUPPORT_EMAIL} ASAP." ) for channel in Channel.objects.filter(project__owner_id=profile.user_id): - if channel.kind == "email" and channel.email_value in skip_emails: + if channel.kind == "email" and channel.email.value in skip_emails: continue dummy = Check(name=name, desc=desc, status="down", project=channel.project) diff --git a/hc/api/admin.py b/hc/api/admin.py index 760ebd6ed2f..67146cbb6d5 100644 --- a/hc/api/admin.py +++ b/hc/api/admin.py @@ -219,7 +219,7 @@ def created_(self, obj): def project_(self, obj): url = self.view_on_site(obj) name = escape(obj.project_name or "Default") - email = escape(obj.email) + email = escape(obj.owner_email) return f"{email} › {name}" def time(self, obj): @@ -230,7 +230,7 @@ def get_queryset(self, request): qs = super().get_queryset(request) qs = qs.annotate(project_code=F("project__code")) qs = qs.annotate(project_name=F("project__name")) - qs = qs.annotate(email=F("project__owner__email")) + qs = qs.annotate(owner_email=F("project__owner__email")) return qs def view_on_site(self, obj): diff --git a/hc/api/models.py b/hc/api/models.py index a49dacf4786..29639fadf54 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -727,6 +727,20 @@ class PhoneConf(BaseModel): notify_down: bool | None = Field(None, alias="down") +class EmailConf(BaseModel): + value: str + notify_up: bool = Field(alias="up") + notify_down: bool = Field(alias="down") + + @classmethod + def load(cls, data: Any) -> EmailConf: + # Is it a plain email address? + if not data.startswith("{"): + return cls.model_validate({"value": data, "up": True, "down": True}) + + return super().model_validate_json(data) + + class Channel(models.Model): name = models.CharField(max_length=100, blank=True) code = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) @@ -745,7 +759,7 @@ def __str__(self) -> str: if self.name: return self.name if self.kind == "email": - return "Email to %s" % self.email_value + return "Email to %s" % self.email.value elif self.kind == "sms": return "SMS to %s" % self.phone.value elif self.kind == "slack": @@ -779,7 +793,7 @@ def send_verify_link(self) -> None: args = [self.code, self.make_token()] verify_link = reverse("hc-verify-email", args=args) verify_link = settings.SITE_ROOT + verify_link - emails.verify_email(self.email_value, {"verify_link": verify_link}) + emails.verify_email(self.email.value, {"verify_link": verify_link}) def get_unsub_link(self) -> str: signer = TimestampSigner(salt="alerts") @@ -977,28 +991,8 @@ def trello_board_list(self) -> tuple[str, str]: return doc["board_name"], doc["list_name"] @property - def email_value(self) -> str: - assert self.kind == "email" - if not self.value.startswith("{"): - return self.value - - return self.json["value"] - - @property - def email_notify_up(self) -> bool: - assert self.kind == "email" - if not self.value.startswith("{"): - return True - - return self.json.get("up") - - @property - def email_notify_down(self) -> bool: - assert self.kind == "email" - if not self.value.startswith("{"): - return True - - return self.json.get("down") + def email(self) -> EmailConf: + return EmailConf.load(self.value) @property def opsgenie_key(self) -> str: diff --git a/hc/api/tests/test_flip_model.py b/hc/api/tests/test_flip_model.py index 298c9913648..e5976d2f57f 100644 --- a/hc/api/tests/test_flip_model.py +++ b/hc/api/tests/test_flip_model.py @@ -25,7 +25,9 @@ def test_select_channels_works(self) -> None: self.assertEqual(channels, [self.channel]) def test_select_channels_handles_noop(self) -> None: - self.channel.value = json.dumps({"down": False}) + self.channel.value = json.dumps( + {"value": "alice@example.org", "up": False, "down": False} + ) self.channel.save() channels = self.flip.select_channels() diff --git a/hc/api/transports.py b/hc/api/transports.py index 1fe8083229b..58e65fd2a0b 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -155,7 +155,7 @@ def notify(self, check: Check, notification: Notification | None = None) -> None # If this email address has an associated account, include # a summary of projects the account has access to try: - profile = Profile.objects.get(user__email=self.channel.email_value) + profile = Profile.objects.get(user__email=self.channel.email.value) projects = list(profile.projects()) except Profile.DoesNotExist: projects = None @@ -170,13 +170,13 @@ def notify(self, check: Check, notification: Notification | None = None) -> None "unsub_link": unsub_link, } - emails.alert(self.channel.email_value, ctx, headers) + emails.alert(self.channel.email.value, ctx, headers) def is_noop(self, check: Check) -> bool: if check.status == "down": - return not self.channel.email_notify_down + return not self.channel.email.notify_down else: - return not self.channel.email_notify_up + return not self.channel.email.notify_up class Shell(Transport): diff --git a/hc/front/tests/test_edit_email.py b/hc/front/tests/test_edit_email.py index 8e18b789a0a..b67441acd69 100644 --- a/hc/front/tests/test_edit_email.py +++ b/hc/front/tests/test_edit_email.py @@ -39,9 +39,9 @@ def test_it_saves_changes(self) -> None: self.assertRedirects(r, self.channels_url) self.channel.refresh_from_db() - self.assertEqual(self.channel.email_value, "new@example.org") - self.assertTrue(self.channel.email_notify_down) - self.assertFalse(self.channel.email_notify_up) + self.assertEqual(self.channel.email.value, "new@example.org") + self.assertTrue(self.channel.email.notify_down) + self.assertFalse(self.channel.email.notify_up) # It should send a verification link email = mail.outbox[0] @@ -58,9 +58,9 @@ def test_it_skips_verification_if_email_unchanged(self) -> None: self.client.post(self.url, form) self.channel.refresh_from_db() - self.assertEqual(self.channel.email_value, "alerts@example.org") - self.assertFalse(self.channel.email_notify_down) - self.assertTrue(self.channel.email_notify_up) + self.assertEqual(self.channel.email.value, "alerts@example.org") + self.assertFalse(self.channel.email.notify_down) + self.assertTrue(self.channel.email.notify_up) self.assertTrue(self.channel.email_verified) # The email address did not change, so we should skip verification @@ -73,7 +73,7 @@ def test_team_access_works(self) -> None: self.client.post(self.url, form) self.channel.refresh_from_db() - self.assertEqual(self.channel.email_value, "new@example.org") + self.assertEqual(self.channel.email.value, "new@example.org") @override_settings(EMAIL_USE_VERIFICATION=False) def test_it_hides_confirmation_needed_notice(self) -> None: @@ -90,7 +90,7 @@ def test_it_auto_verifies_email(self) -> None: self.assertRedirects(r, self.channels_url) self.channel.refresh_from_db() - self.assertEqual(self.channel.email_value, "dan@example.org") + self.assertEqual(self.channel.email.value, "dan@example.org") # Email should *not* have been sent self.assertEqual(len(mail.outbox), 0) @@ -103,7 +103,7 @@ def test_it_auto_verifies_own_email(self) -> None: self.assertRedirects(r, self.channels_url) self.channel.refresh_from_db() - self.assertEqual(self.channel.email_value, "alice@example.org") + self.assertEqual(self.channel.email.value, "alice@example.org") # Email should *not* have been sent self.assertEqual(len(mail.outbox), 0) diff --git a/hc/front/views.py b/hc/front/views.py index 2cd905c77b1..cb4f4c4aa52 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -1255,7 +1255,7 @@ def email_form(request: HttpRequest, channel: Channel) -> HttpResponse: if request.method == "POST": form = forms.EmailForm(request.POST) if form.is_valid(): - if channel.disabled or form.cleaned_data["value"] != channel.email_value: + if channel.disabled or form.cleaned_data["value"] != channel.email.value: channel.disabled = False if not settings.EMAIL_USE_VERIFICATION: @@ -1284,9 +1284,9 @@ def email_form(request: HttpRequest, channel: Channel) -> HttpResponse: else: form = forms.EmailForm( { - "value": channel.email_value, - "up": channel.email_notify_up, - "down": channel.email_notify_down, + "value": channel.email.value, + "up": channel.email.notify_up, + "down": channel.email.notify_down, } ) diff --git a/templates/front/channels.html b/templates/front/channels.html index 1aa4a19fa39..b33291c69ff 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -39,11 +39,11 @@ {% endif %}
{% if ch.kind == "email" %} - Email to {{ ch.email_value }} - {% if ch.email_notify_down and not ch.email_notify_up %} + Email to {{ ch.email.value }} + {% if ch.email.notify_down and not ch.email.notify_up %} (down only) {% endif %} - {% if ch.email_notify_up and not ch.email_notify_down %} + {% if ch.email.notify_up and not ch.email.notify_down %} (up only) {% endif %} {% elif ch.kind == "pd" %} diff --git a/templates/front/event_summary.html b/templates/front/event_summary.html index 5717bceeadd..538aacc7b9e 100644 --- a/templates/front/event_summary.html +++ b/templates/front/event_summary.html @@ -1,5 +1,5 @@ {% if event.channel.kind == "email" %} - Sent email to {{ event.channel.email_value }} + Sent email to {{ event.channel.email.value }} {% elif event.channel.kind == "slack" %} Sent Slack alert {% if event.channel.slack_channel %}