diff --git a/hc/api/management/commands/sendalerts.py b/hc/api/management/commands/sendalerts.py index 33d9906..e555505 100644 --- a/hc/api/management/commands/sendalerts.py +++ b/hc/api/management/commands/sendalerts.py @@ -21,8 +21,9 @@ def handle_many(self): now = timezone.now() going_down = query.filter(alert_after__lt=now, status="up") going_up = query.filter(alert_after__gt=now, status="down") + nag = query.filter(nag_after__lt=now, nag_status=True, status="down") # Don't combine this in one query so Postgres can query using index: - checks = list(going_down.iterator()) + list(going_up.iterator()) + checks = list(going_down.iterator()) + list(going_up.iterator()) + list(nag.iterator()) if not checks: return False @@ -40,9 +41,14 @@ def handle_one(self, check): """ - # Save the new status. If sendalerts crashes, + # Saves the new status. If sendalerts crashes, # it won't process this check again. check.status = check.get_status() + now = timezone.now() + # Set the next nag time + if check.nag() == "nag": + interval = check.new_nag_after + check.nag_after = now + interval check.save() tmpl = "\nSending alert, status=%s, code=%s\n" diff --git a/hc/api/migrations/0027_auto_20180712_0652.py b/hc/api/migrations/0027_auto_20180712_0652.py new file mode 100644 index 0000000..d748736 --- /dev/null +++ b/hc/api/migrations/0027_auto_20180712_0652.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-12 06:52 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0026_auto_20160415_1824'), + ] + + operations = [ + migrations.AddField( + model_name='check', + name='nag_after', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='check', + name='nag_status', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='check', + name='status', + field=models.CharField(choices=[('up', 'Up'), ('down', 'Down'), ('new', 'New'), ('paused', 'Paused'), ('nag', 'Nag')], default='new', max_length=6), + ), + ] diff --git a/hc/api/migrations/0028_check_new_nag_after.py b/hc/api/migrations/0028_check_new_nag_after.py new file mode 100644 index 0000000..b5ca718 --- /dev/null +++ b/hc/api/migrations/0028_check_new_nag_after.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-12 12:21 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0027_auto_20180712_0652'), + ] + + operations = [ + migrations.AddField( + model_name='check', + name='new_nag_after', + field=models.DurationField(default=datetime.timedelta(1, 3600)), + ), + ] diff --git a/hc/api/migrations/0031_merge_20180713_0904.py b/hc/api/migrations/0031_merge_20180713_0904.py new file mode 100644 index 0000000..aaa40cd --- /dev/null +++ b/hc/api/migrations/0031_merge_20180713_0904.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-13 09:04 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0028_check_new_nag_after'), + ('api', '0030_remove_check_profile'), + ] + + operations = [ + ] diff --git a/hc/api/models.py b/hc/api/models.py index a1cdd7d..8717529 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -17,7 +17,8 @@ ("up", "Up"), ("down", "Down"), ("new", "New"), - ("paused", "Paused") + ("paused", "Paused"), + ("nag", "Nag") ) DEFAULT_TIMEOUT = td(days=1) DEFAULT_GRACE = td(hours=1) @@ -52,6 +53,9 @@ class Meta: last_ping = models.DateTimeField(null=True, blank=True) alert_after = models.DateTimeField(null=True, blank=True, editable=False) status = models.CharField(max_length=6, choices=STATUSES, default="new") + nag_after = models.DateTimeField(null=True, blank=True) + nag_status = models.BooleanField(default=False) + new_nag_after = models.DurationField(default=DEFAULT_GRACE+DEFAULT_TIMEOUT) def name_then_code(self): if self.name: @@ -130,6 +134,11 @@ def to_dict(self): return result + def nag(self): + """Sends a nag to the client if the service is still down""" + if self.get_status() == 'down' and self.nag_status: + return "nag" + class Ping(models.Model): n = models.IntegerField(null=True) diff --git a/hc/api/tests/test_prunechecks.py b/hc/api/tests/test_prunechecks.py new file mode 100644 index 0000000..c421ccb --- /dev/null +++ b/hc/api/tests/test_prunechecks.py @@ -0,0 +1,22 @@ +from datetime import timedelta + +from django.utils import timezone +from hc.api.management.commands.prunechecks import Command +from hc.api.models import Check +from hc.test import BaseTestCase +from mock import patch + + +class PruneChecksTestCase(BaseTestCase): + + @patch("hc.api.management.commands.sendalerts.Command.handle_one") + def test_it_reaches_prunes(self, mock): + check = Check(user=None) + check.created = timezone.now() - timedelta(days=1, minutes=30) + check.save() + + # Expect no exceptions-- + + result = Command().handle(check) + self.assertEqual(result, "Done! Pruned 0 checks.") + \ No newline at end of file diff --git a/hc/api/tests/test_sendalerts.py b/hc/api/tests/test_sendalerts.py index 19a22ea..db99d5e 100644 --- a/hc/api/tests/test_sendalerts.py +++ b/hc/api/tests/test_sendalerts.py @@ -41,6 +41,7 @@ def test_it_handles_grace_period(self): result = Command().handle_one(check) self.assertEqual(True, result, "handle_one should return True") + @patch("hc.api.management.commands.sendalerts.Command.handle_one") def test_handle_many_true(self, mock): """Assert when Command's handle many that when handle_many should return True""" @@ -51,9 +52,10 @@ def test_handle_many_true(self, mock): check = Check(user=self.bob, name=name) check.alert_after = time check.status = "up" + check.nag_status=True check.save() result = Command().handle_many() self.assertEqual(result, True, "handle_many should return True") - - + + \ No newline at end of file diff --git a/hc/front/forms.py b/hc/front/forms.py index 8a4ea05..6b0fb89 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -18,10 +18,9 @@ def clean_tags(self): class TimeoutForm(forms.Form): - """Increase timeout and grace maximum value from 30days to 6 months in value of seconds""" - timeout = forms.IntegerField(min_value=60, max_value=15552000) - grace = forms.IntegerField(min_value=60, max_value=15552000) - + timeout = forms.IntegerField(min_value=60, max_value=2592000) + grace = forms.IntegerField(min_value=60, max_value=2592000) + new_nag_after = forms.IntegerField(min_value=60, max_value=2592000) class AddChannelForm(forms.ModelForm): @@ -42,3 +41,8 @@ class AddWebhookForm(forms.Form): def get_value(self): return "{value_down}\n{value_up}".format(**self.cleaned_data) + + +class NagUserForm(forms.Form): + """Form for nagging the user""" + nag = forms.BooleanField(required=False) diff --git a/hc/front/tests/test_update_timeout.py b/hc/front/tests/test_update_timeout.py index 06ccad3..1b44f32 100644 --- a/hc/front/tests/test_update_timeout.py +++ b/hc/front/tests/test_update_timeout.py @@ -11,7 +11,7 @@ def setUp(self): def test_it_works(self): url = "/checks/%s/timeout/" % self.check.code - payload = {"timeout": 3600, "grace": 60} + payload = {"timeout": 3600, "grace": 60, "new_nag_after": 60} self.client.login(username="alice@example.org", password="password") r = self.client.post(url, data=payload) @@ -20,10 +20,11 @@ def test_it_works(self): check = Check.objects.get(code=self.check.code) assert check.timeout.total_seconds() == 3600 assert check.grace.total_seconds() == 60 + assert check.new_nag_after.total_seconds() == 60 def test_team_access_works(self): url = "/checks/%s/timeout/" % self.check.code - payload = {"timeout": 7200, "grace": 60} + payload = {"timeout": 7200, "grace": 60, "new_nag_after": 60} # Logging in as bob, not alice. Bob has team access so this # should work. diff --git a/hc/front/urls.py b/hc/front/urls.py index 53befc6..81417cd 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -8,6 +8,7 @@ url(r'^pause/$', views.pause, name="hc-pause"), url(r'^remove/$', views.remove_check, name="hc-remove-check"), url(r'^log/$', views.log, name="hc-log"), + url(r'^nag_status/$', views.nag_user, name="hc-update-nag"), ] channel_urls = [ diff --git a/hc/front/views.py b/hc/front/views.py index 967e973..c7028a6 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -15,10 +15,10 @@ from django.utils.six.moves.urllib.parse import urlencode from hc.api.decorators import uuid_or_400 from hc.api.models import DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, Ping -from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm, - TimeoutForm) +from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm,TimeoutForm, NagUserForm) from hc.accounts.models import Member + # from itertools recipes: def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." @@ -173,6 +173,7 @@ def update_timeout(request, code): if form.is_valid(): check.timeout = td(seconds=form.cleaned_data["timeout"]) check.grace = td(seconds=form.cleaned_data["grace"]) + check.new_nag_after = td(seconds=form.cleaned_data["new_nag_after"]) check.save() return redirect("hc-checks") @@ -575,3 +576,21 @@ def privacy(request): def terms(request): return render(request, "front/terms.html", {}) + + +@login_required +@uuid_or_400 +def nag_user(request, code): + """Function for updating the nag option""" + assert request.method == "POST" + + check = get_object_or_404(Check, code=code) + if check.user_id != request.team.user.id: + return HttpResponseForbidden() + + form = NagUserForm(request.POST) + if form.is_valid(): + check.nag_status = form.cleaned_data["nag"] + check.save() + + return redirect("hc-checks") \ No newline at end of file diff --git a/static/css/my_checks.css b/static/css/my_checks.css index 6a1a925..26df8e8 100644 --- a/static/css/my_checks.css +++ b/static/css/my_checks.css @@ -38,6 +38,14 @@ #grace-slider.noUi-connect { background: #f0ad4e; } +/*Add nag slider*/ +#nag-slider { + margin: 20px 50px 110px 50px; +} + +#nag-slider.noUi-connect { + background: #f0ad4e; +} #period-slider .noUi-value, #grace-slider .noUi-value { width: 60px; diff --git a/static/css/my_checks_desktop.css b/static/css/my_checks_desktop.css index 6ef2e13..b91e13b 100644 --- a/static/css/my_checks_desktop.css +++ b/static/css/my_checks_desktop.css @@ -48,6 +48,15 @@ table.table tr > th.th-name { padding: 6px; display: block; } +/* Add nag*/ +#checks-table .timeout-nag { + border: 1px solid rgba(0, 0, 0, 0); + padding: 6px; + display: block; +} +#checks-table tr:hover .timeout-grace { + border: 1px dotted #AAA; +} #checks-table tr:hover .timeout-grace { border: 1px dotted #AAA; diff --git a/static/js/checks.js b/static/js/checks.js index 6f255c5..19e88e8 100644 --- a/static/js/checks.js +++ b/static/js/checks.js @@ -92,6 +92,34 @@ $(function () { $("#update-timeout-grace").val(rounded); }); + var nagSlider = document.getElementById("nag-slider"); + noUiSlider.create(nagSlider, { + start: [20], + connect: "lower", + range: { + 'min': [60, 60], + '33%': [3600, 3600], + '66%': [86400, 86400], + '83%': [604800, 604800], + 'max': 2592000, + }, + pips: { + mode: 'values', + values: [60, 1800, 3600, 43200, 86400, 604800, 2592000], + density: 4, + format: { + to: secsToText, + from: function() {} + } + } + }); + + nagSlider.noUiSlider.on("update", function(a, b, value) { + var rounded = Math.round(value); + $("#nag-slider-value").text(secsToText(rounded)); + $("#update-timeout-nag").val(rounded); + }); + $('[data-toggle="tooltip"]').tooltip(); @@ -203,5 +231,16 @@ $(function () { prompt("Press Ctrl+C to select:", text) }); + $(".update-nag").click(function() { + var $this = $(this); + + $("#update-nag-form").attr("action", $this.data("url")); + $("#update-nag-input").val($this.data("nag")); + $('#update-nag-modal').modal("show"); + $("#update-nag-input").focus(); + + return false; + }); + }); diff --git a/templates/front/my_checks.html b/templates/front/my_checks.html index e28a7cb..21f35be 100644 --- a/templates/front/my_checks.html +++ b/templates/front/my_checks.html @@ -116,6 +116,7 @@