diff --git a/hc/api/models.py b/hc/api/models.py index 6fd4679e..7cf7059f 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -81,7 +81,7 @@ class Check(models.Model): return errors def get_status(self): - if self.status == "new": + if self.status in ("new", "paused"): return self.status now = timezone.now() @@ -108,9 +108,12 @@ class Check(models.Model): return [t.strip() for t in self.tags.split(" ") if t.strip()] def to_dict(self): + pause_rel_url = reverse("hc-api-pause", args=[self.code]) + result = { "name": self.name, "ping_url": self.url(), + "pause_url": settings.SITE_ROOT + pause_rel_url, "tags": self.tags, "timeout": int(self.timeout.total_seconds()), "grace": int(self.grace.total_seconds()), diff --git a/hc/api/tests/test_list_checks.py b/hc/api/tests/test_list_checks.py index 7a7a517a..5b1bbd3a 100644 --- a/hc/api/tests/test_list_checks.py +++ b/hc/api/tests/test_list_checks.py @@ -47,6 +47,8 @@ class ListChecksTestCase(BaseTestCase): self.assertEqual(checks["Alice 1"]["last_ping"], self.now.isoformat()) self.assertEqual(checks["Alice 1"]["n_pings"], 1) self.assertEqual(checks["Alice 1"]["status"], "new") + pause_url = "http://localhost:8000/api/v1/checks/%s/pause" % self.a1.code + self.assertEqual(checks["Alice 1"]["pause_url"], pause_url) next_ping = self.now + td(seconds=3600) self.assertEqual(checks["Alice 1"]["next_ping"], next_ping.isoformat()) diff --git a/hc/api/tests/test_pause.py b/hc/api/tests/test_pause.py new file mode 100644 index 00000000..413dce73 --- /dev/null +++ b/hc/api/tests/test_pause.py @@ -0,0 +1,34 @@ +from hc.api.models import Check +from hc.test import BaseTestCase + + +class PauseTestCase(BaseTestCase): + + def test_it_works(self): + check = Check(user=self.alice, status="up") + check.save() + + url = "/api/v1/checks/%s/pause" % check.code + r = self.client.post(url, "", content_type="application/json", + HTTP_X_API_KEY="abc") + + self.assertEqual(r.status_code, 200) + + check.refresh_from_db() + self.assertEqual(check.status, "paused") + + def test_it_only_allows_post(self): + url = "/api/v1/checks/1659718b-21ad-4ed1-8740-43afc6c41524/pause" + + r = self.client.get(url, HTTP_X_API_KEY="abc") + self.assertEqual(r.status_code, 405) + + def test_it_validates_ownership(self): + check = Check(user=self.bob, status="up") + check.save() + + url = "/api/v1/checks/%s/pause" % check.code + r = self.client.post(url, "", content_type="application/json", + HTTP_X_API_KEY="abc") + + self.assertEqual(r.status_code, 400) diff --git a/hc/api/tests/test_ping.py b/hc/api/tests/test_ping.py index ca942550..5dcfc497 100644 --- a/hc/api/tests/test_ping.py +++ b/hc/api/tests/test_ping.py @@ -13,12 +13,22 @@ class PingTestCase(TestCase): r = self.client.get("/ping/%s/" % self.check.code) assert r.status_code == 200 - same_check = Check.objects.get(code=self.check.code) - assert same_check.status == "up" + self.check.refresh_from_db() + assert self.check.status == "up" ping = Ping.objects.latest("id") assert ping.scheme == "http" + def test_it_changes_status_of_paused_check(self): + self.check.status = "paused" + self.check.save() + + r = self.client.get("/ping/%s/" % self.check.code) + assert r.status_code == 200 + + self.check.refresh_from_db() + assert self.check.status == "up" + def test_post_works(self): csrf_client = Client(enforce_csrf_checks=True) r = csrf_client.post("/ping/%s/" % self.check.code) diff --git a/hc/api/urls.py b/hc/api/urls.py index b0c84e17..b9353f73 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -6,5 +6,6 @@ urlpatterns = [ url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"), url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"), url(r'^api/v1/checks/$', views.checks), + url(r'^api/v1/checks/([\w-]+)/pause$', views.pause, name="hc-api-pause"), url(r'^badge/([\w-]+)/([\w-]{8})/([\w-]+).svg$', views.badge, name="hc-badge"), ] diff --git a/hc/api/views.py b/hc/api/views.py index 8f0fd354..70b3f3ed 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -23,7 +23,7 @@ def ping(request, code): check.n_pings = F("n_pings") + 1 check.last_ping = timezone.now() - if check.status == "new": + if check.status in ("new", "paused"): check.status = "up" check.save() @@ -76,6 +76,23 @@ def checks(request): return HttpResponse(status=405) +@csrf_exempt +@check_api_key +def pause(request, code): + if request.method != "POST": + # Method not allowed + return HttpResponse(status=405) + + try: + check = Check.objects.get(code=code, user=request.user) + except Check.DoesNotExist: + return HttpResponseBadRequest() + + check.status = "paused" + check.save() + return JsonResponse(check.to_dict()) + + @never_cache def badge(request, username, signature, tag): if not check_signature(username, tag, signature): diff --git a/hc/front/management/commands/pygmentize.py b/hc/front/management/commands/pygmentize.py index b64bc695..29835a45 100644 --- a/hc/front/management/commands/pygmentize.py +++ b/hc/front/management/commands/pygmentize.py @@ -40,3 +40,5 @@ class Command(BaseCommand): _process("list_checks_response", lexers.JsonLexer()) _process("create_check_request", lexers.BashLexer()) _process("create_check_response", lexers.JsonLexer()) + _process("pause_check_request", lexers.BashLexer()) + _process("pause_check_response", lexers.JsonLexer()) diff --git a/hc/front/tests/test_pause.py b/hc/front/tests/test_pause.py new file mode 100644 index 00000000..8a0c80d7 --- /dev/null +++ b/hc/front/tests/test_pause.py @@ -0,0 +1,20 @@ +from hc.api.models import Check +from hc.test import BaseTestCase + + +class PauseTestCase(BaseTestCase): + + def setUp(self): + super(PauseTestCase, self).setUp() + self.check = Check(user=self.alice, status="up") + self.check.save() + + def test_it_pauses(self): + url = "/checks/%s/pause/" % self.check.code + + self.client.login(username="alice@example.org", password="password") + r = self.client.post(url) + self.assertRedirects(r, "/checks/") + + self.check.refresh_from_db() + self.assertEqual(self.check.status, "paused") diff --git a/hc/front/urls.py b/hc/front/urls.py index 7f0bd29a..53befc60 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -1,34 +1,43 @@ -from django.conf.urls import url +from django.conf.urls import include, url from hc.front import views +check_urls = [ + url(r'^name/$', views.update_name, name="hc-update-name"), + url(r'^timeout/$', views.update_timeout, name="hc-update-timeout"), + 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"), +] + +channel_urls = [ + url(r'^$', views.channels, name="hc-channels"), + url(r'^add/$', views.add_channel, name="hc-add-channel"), + url(r'^add_email/$', views.add_email, name="hc-add-email"), + url(r'^add_webhook/$', views.add_webhook, name="hc-add-webhook"), + url(r'^add_pd/$', views.add_pd, name="hc-add-pd"), + url(r'^add_slack/$', views.add_slack, name="hc-add-slack"), + url(r'^add_slack_btn/$', views.add_slack_btn, name="hc-add-slack-btn"), + url(r'^add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"), + url(r'^add_pushbullet/$', views.add_pushbullet, name="hc-add-pushbullet"), + url(r'^add_pushover/$', views.add_pushover, name="hc-add-pushover"), + url(r'^add_victorops/$', views.add_victorops, name="hc-add-victorops"), + url(r'^([\w-]+)/checks/$', views.channel_checks, name="hc-channel-checks"), + url(r'^([\w-]+)/remove/$', views.remove_channel, name="hc-remove-channel"), + url(r'^([\w-]+)/verify/([\w-]+)/$', views.verify_email, + name="hc-verify-email"), +] + urlpatterns = [ - url(r'^$', views.index, name="hc-index"), - url(r'^checks/$', views.my_checks, name="hc-checks"), - url(r'^checks/add/$', views.add_check, name="hc-add-check"), - url(r'^checks/([\w-]+)/name/$', views.update_name, name="hc-update-name"), - url(r'^checks/([\w-]+)/timeout/$', views.update_timeout, name="hc-update-timeout"), - url(r'^checks/([\w-]+)/remove/$', views.remove_check, name="hc-remove-check"), - url(r'^checks/([\w-]+)/log/$', views.log, name="hc-log"), - url(r'^docs/$', views.docs, name="hc-docs"), - url(r'^docs/api/$', views.docs_api, name="hc-docs-api"), - url(r'^about/$', views.about, name="hc-about"), - url(r'^privacy/$', views.privacy, name="hc-privacy"), - url(r'^terms/$', views.terms, name="hc-terms"), - url(r'^integrations/$', views.channels, name="hc-channels"), - url(r'^integrations/add/$', views.add_channel, name="hc-add-channel"), - url(r'^integrations/add_email/$', views.add_email, name="hc-add-email"), - url(r'^integrations/add_webhook/$', views.add_webhook, name="hc-add-webhook"), - url(r'^integrations/add_pd/$', views.add_pd, name="hc-add-pd"), - url(r'^integrations/add_slack/$', views.add_slack, name="hc-add-slack"), - url(r'^integrations/add_slack_btn/$', views.add_slack_btn, name="hc-add-slack-btn"), - url(r'^integrations/add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"), - url(r'^integrations/add_pushbullet/$', views.add_pushbullet, name="hc-add-pushbullet"), - url(r'^integrations/add_pushover/$', views.add_pushover, name="hc-add-pushover"), - url(r'^integrations/add_victorops/$', views.add_victorops, name="hc-add-victorops"), - url(r'^integrations/([\w-]+)/checks/$', views.channel_checks, name="hc-channel-checks"), - url(r'^integrations/([\w-]+)/remove/$', views.remove_channel, name="hc-remove-channel"), - url(r'^integrations/([\w-]+)/verify/([\w-]+)/$', - views.verify_email, name="hc-verify-email"), + url(r'^$', views.index, name="hc-index"), + url(r'^checks/$', views.my_checks, name="hc-checks"), + url(r'^checks/add/$', views.add_check, name="hc-add-check"), + url(r'^checks/([\w-]+)/', include(check_urls)), + url(r'^integrations/', include(channel_urls)), + url(r'^docs/$', views.docs, name="hc-docs"), + url(r'^docs/api/$', views.docs_api, name="hc-docs-api"), + url(r'^about/$', views.about, name="hc-about"), + url(r'^privacy/$', views.privacy, name="hc-privacy"), + url(r'^terms/$', views.terms, name="hc-terms"), ] diff --git a/hc/front/views.py b/hc/front/views.py index b20194b1..1d2500d4 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -44,7 +44,7 @@ def my_checks(request): if status == "down": down_tags.add(tag) - elif check.in_grace_period(): + elif check.in_grace_period() and status != "paused": grace_tags.add(tag) ctx = { @@ -169,6 +169,21 @@ def update_timeout(request, code): return redirect("hc-checks") +@login_required +@uuid_or_400 +def pause(request, code): + assert request.method == "POST" + + check = get_object_or_404(Check, code=code) + if check.user_id != request.team.user.id: + return HttpResponseForbidden() + + check.status = "paused" + check.save() + + return redirect("hc-checks") + + @login_required @uuid_or_400 def remove_check(request, code): diff --git a/hc/urls.py b/hc/urls.py index f3f39a12..12c23c67 100644 --- a/hc/urls.py +++ b/hc/urls.py @@ -2,9 +2,9 @@ from django.conf.urls import include, url from django.contrib import admin urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', include(admin.site.urls)), url(r'^accounts/', include('hc.accounts.urls')), - url(r'^', include('hc.api.urls')), - url(r'^', include('hc.front.urls')), - url(r'^', include('hc.payments.urls')) + url(r'^', include('hc.api.urls')), + url(r'^', include('hc.front.urls')), + url(r'^', include('hc.payments.urls')) ] diff --git a/static/css/docs.css b/static/css/docs.css index b5fb823d..a0110b22 100644 --- a/static/css/docs.css +++ b/static/css/docs.css @@ -41,4 +41,13 @@ h3.api-section { font-family: monospace; font-weight: bold; margin-bottom: 1em; +} + +a.section { + color: #000; + text-decoration: none; +} + +a.section:hover { + text-decoration: none; } \ No newline at end of file diff --git a/static/js/checks.js b/static/js/checks.js index 2dd74d67..220687fe 100644 --- a/static/js/checks.js +++ b/static/js/checks.js @@ -123,21 +123,6 @@ $(function () { return false; }); - $("#show-urls").click(function() { - $("#show-urls").addClass("active"); - $(".my-checks-url").removeClass("off"); - - $("#show-emails").removeClass("active"); - $(".my-checks-email").addClass("off"); - }); - - $("#show-emails").click(function() { - $("#show-urls").removeClass("active"); - $(".my-checks-url").addClass("off"); - - $("#show-emails").addClass("active"); - $(".my-checks-email").removeClass("off"); - }); $("#my-checks-tags button").click(function() { // .active has not been updated yet by bootstrap code, @@ -176,6 +161,13 @@ $(function () { }); + $(".pause-check").click(function(e) { + var url = e.target.getAttribute("data-url"); + $("#pause-form").attr("action", url).submit(); + return false; + }); + + $(".usage-examples").click(function(e) { var a = e.target; var url = a.getAttribute("data-url"); diff --git a/templates/front/docs.html b/templates/front/docs.html index 12cb5b1f..bde87b61 100644 --- a/templates/front/docs.html +++ b/templates/front/docs.html @@ -205,6 +205,15 @@ powershell.exe -ExecutionPolicy bypass -File C:\Scripts\healthchecks.ps1 A check that has been created, but has not received any pings yet. + + + + + + Monitoring Paused. + You can resume monitoring of a paused check by pinging it. + + diff --git a/templates/front/docs_api.html b/templates/front/docs_api.html index c27d9e74..b176b095 100644 --- a/templates/front/docs_api.html +++ b/templates/front/docs_api.html @@ -7,9 +7,13 @@

REST API

-This is early days for healtchecks.io REST API. For now, there's just -one API resource for listing and creating checks. +This is early days for healtchecks.io REST API. For now, there's API calls to:

+

Authentication

Your requests to healtchecks.io REST API must authenticate using an @@ -47,7 +51,11 @@ and 5xx indicates a server error. The response may contain a JSON document with additional data.

-

List checks

+ + + +

List checks

+
GET {{ SITE_ROOT }}/api/v1/checks/
@@ -61,7 +69,11 @@ The response may contain a JSON document with additional data.

Example Response

{% include "front/snippets/list_checks_response.html" %} + + +

Create a check

+
POST {{ SITE_ROOT }}/api/v1/checks/
@@ -123,4 +135,32 @@ The response may contain a JSON document with additional data.

Example Response

{% include "front/snippets/create_check_response.html" %} + + + + + +

Pause Monitoring of a Check

+
+ +
POST {{ SITE_ROOT }}/api/v1/checks/<uuid>/pause
+ + + +

+ Disables monitoring for a check, without removing it. The check goes + into a "paused" state. You can resume monitoring of the check by pinging + it. +

+

+ This API call has no request parameters. +

+ +

Example Request

+{% include "front/snippets/pause_check_request.html" %} + +

Example Response

+{% include "front/snippets/pause_check_response.html" %} + + {% endblock %} diff --git a/templates/front/my_checks.html b/templates/front/my_checks.html index 46408180..e28a7cbc 100644 --- a/templates/front/my_checks.html +++ b/templates/front/my_checks.html @@ -274,6 +274,10 @@ +
+ {% csrf_token %} +
+ {% endblock %} {% block scripts %} diff --git a/templates/front/my_checks_desktop.html b/templates/front/my_checks_desktop.html index 2c2a9367..da049d1d 100644 --- a/templates/front/my_checks_desktop.html +++ b/templates/front/my_checks_desktop.html @@ -15,7 +15,11 @@ {% if check.get_status == "new" %} - + + {% elif check.get_status == "paused" %} + {% elif check.in_grace_period %} {% elif check.get_status == "up" %} @@ -75,6 +79,14 @@