diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d8a2a24..39e6174a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog All notable changes to this project will be documented in this file. +## v1.13.0-dev - Unreleased + +### Improvements +- Show a red "!" in project's top navigation if any integration is not working + + ## v1.12.0 - 2020-01-02 ### Improvements diff --git a/hc/accounts/models.py b/hc/accounts/models.py index 036ade35..1eb5f8a5 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -282,6 +282,9 @@ class Project(models.Model): break return status + def have_broken_channels(self): + return self.channel_set.exclude(last_error="").exists() + class Member(models.Model): user = models.ForeignKey(User, models.CASCADE, related_name="memberships") diff --git a/hc/accounts/tests/test_project_model.py b/hc/accounts/tests/test_project_model.py index d6202a14..46862c58 100644 --- a/hc/accounts/tests/test_project_model.py +++ b/hc/accounts/tests/test_project_model.py @@ -1,6 +1,6 @@ from hc.test import BaseTestCase from hc.accounts.models import Project -from hc.api.models import Check +from hc.api.models import Check, Channel class ProjectModelTestCase(BaseTestCase): @@ -13,3 +13,11 @@ class ProjectModelTestCase(BaseTestCase): Check.objects.create(project=p2) self.assertEqual(self.project.num_checks_available(), 18) + + def test_it_handles_zero_broken_channels(self): + self.assertFalse(self.project.have_broken_channels()) + + def test_it_handles_one_broken_channel(self): + Channel.objects.create(kind="webhook", last_error="x", project=self.project) + + self.assertTrue(self.project.have_broken_channels()) diff --git a/hc/api/migrations/0066_channel_last_error.py b/hc/api/migrations/0066_channel_last_error.py new file mode 100644 index 00000000..ee7b5a9d --- /dev/null +++ b/hc/api/migrations/0066_channel_last_error.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.1 on 2020-01-02 12:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0065_auto_20191127_1240'), + ] + + operations = [ + migrations.AddField( + model_name='channel', + name='last_error', + field=models.CharField(blank=True, max_length=200), + ), + ] diff --git a/hc/api/migrations/0067_last_error_values.py b/hc/api/migrations/0067_last_error_values.py new file mode 100644 index 00000000..513cef30 --- /dev/null +++ b/hc/api/migrations/0067_last_error_values.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.1 on 2020-01-02 14:28 + +from django.db import migrations + + +def fill_last_errors(apps, schema_editor): + Channel = apps.get_model("api", "Channel") + Notification = apps.get_model("api", "Notification") + for ch in Channel.objects.all(): + error = "" + try: + n = Notification.objects.filter(channel=ch).latest() + error = n.error + except Notification.DoesNotExist: + pass + + ch.last_error = error + ch.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0066_channel_last_error"), + ] + + operations = [migrations.RunPython(fill_last_errors, migrations.RunPython.noop)] diff --git a/hc/api/models.py b/hc/api/models.py index 2a50a218..83917d04 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -338,6 +338,7 @@ class Channel(models.Model): kind = models.CharField(max_length=20, choices=CHANNEL_KINDS) value = models.TextField(blank=True) email_verified = models.BooleanField(default=False) + last_error = models.CharField(max_length=200, blank=True) checks = models.ManyToManyField(Check) def __str__(self): @@ -441,6 +442,9 @@ class Channel(models.Model): n.error = error n.save() + self.last_error = error + self.save() + return error def icon_path(self): diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py index 0d574941..ad1617ac 100644 --- a/hc/api/tests/test_notify.py +++ b/hc/api/tests/test_notify.py @@ -60,6 +60,7 @@ class NotifyTestCase(BaseTestCase): n = Notification.objects.get() self.assertEqual(n.error, "Connection timed out") + self.assertEqual(self.channel.last_error, "Connection timed out") @patch("hc.api.transports.requests.request", side_effect=ConnectionError) def test_webhooks_handle_connection_errors(self, mock_get): diff --git a/hc/front/tests/test_channels.py b/hc/front/tests/test_channels.py index 86ab1ec5..50bead97 100644 --- a/hc/front/tests/test_channels.py +++ b/hc/front/tests/test_channels.py @@ -125,3 +125,10 @@ class ChannelsTestCase(BaseTestCase): self.client.login(username="alice@example.org", password="password") r = self.client.get("/integrations/") self.assertRedirects(r, "/") + + def test_it_shows_broken_channel_indicator(self): + Channel.objects.create(kind="sms", project=self.project, last_error="x") + + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/integrations/") + self.assertContains(r, "broken-channels", status_code=200) diff --git a/static/css/base.css b/static/css/base.css index 0faf1837..7955132a 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -88,6 +88,9 @@ body { border-color: #22bc66; } +#broken-channels > a { + color: #a94442; +} .page-checks .container-fluid, .page-details .container-fluid { /* Fluid below 1320px, but max width capped to 1320px ... */ diff --git a/templates/base.html b/templates/base.html index bd3a1afa..7fc0fc00 100644 --- a/templates/base.html +++ b/templates/base.html @@ -97,9 +97,14 @@ Checks -
  • - Integrations + {% with b=project.have_broken_channels %} +
  • + + Integrations + {% if b %}{% endif %} +
  • + {% endwith %}
  • Badges