Browse Source

Add "/ping/<code>/start" API endpoint

pull/211/head
Pēteris Caune 6 years ago
parent
commit
481848a749
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
25 changed files with 348 additions and 132 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +5
    -5
      hc/api/management/commands/sendalerts.py
  3. +23
    -0
      hc/api/migrations/0046_auto_20181218_1245.py
  4. +78
    -48
      hc/api/models.py
  5. +30
    -0
      hc/api/tests/test_check_alert_after.py
  6. +28
    -11
      hc/api/tests/test_check_model.py
  7. +11
    -12
      hc/api/tests/test_list_checks.py
  8. +15
    -0
      hc/api/tests/test_pause.py
  9. +59
    -4
      hc/api/tests/test_ping.py
  10. +2
    -2
      hc/api/urls.py
  11. +3
    -2
      hc/api/views.py
  12. +16
    -8
      hc/front/tests/test_pause.py
  13. +22
    -2
      hc/front/tests/test_ping_details.py
  14. +3
    -3
      hc/front/views.py
  15. +8
    -2
      static/css/base.css
  16. +14
    -13
      static/css/icomoon.css
  17. BIN
      static/fonts/icomoon.eot
  18. +20
    -20
      static/fonts/icomoon.svg
  19. BIN
      static/fonts/icomoon.ttf
  20. BIN
      static/fonts/icomoon.woff
  21. +2
    -0
      templates/emails/summary-html.html
  22. +2
    -0
      templates/front/details_events.html
  23. +2
    -0
      templates/front/log.html
  24. +2
    -0
      templates/front/log_status_text.html
  25. +2
    -0
      templates/front/ping_details.html

+ 1
- 0
CHANGELOG.md View File

@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- Allow simultaneous access to checks from different teams - Allow simultaneous access to checks from different teams
- Add CORS support to API endpoints - Add CORS support to API endpoints
- Flip model, for tracking status changes of the Check objects - Flip model, for tracking status changes of the Check objects
- Add "/ping/<code>/start" API endpoint
### Bug Fixes ### Bug Fixes
- Fix after-login redirects (the "?next=" query parameter) - Fix after-login redirects (the "?next=" query parameter)


+ 5
- 5
hc/api/management/commands/sendalerts.py View File

@ -85,16 +85,16 @@ class Command(BaseCommand):
# In PostgreSQL, add this index to run the below query efficiently: # In PostgreSQL, add this index to run the below query efficiently:
# CREATE INDEX api_check_up ON api_check (alert_after) WHERE status = 'up' # CREATE INDEX api_check_up ON api_check (alert_after) WHERE status = 'up'
q = Check.objects.filter(alert_after__lt=now, status="up")
q = Check.objects.filter(alert_after__lt=now).exclude(status="down")
# Sort by alert_after, to avoid unnecessary sorting by id: # Sort by alert_after, to avoid unnecessary sorting by id:
check = q.order_by("alert_after").first() check = q.order_by("alert_after").first()
if check is None: if check is None:
return False return False
q = Check.objects.filter(id=check.id, status="up")
old_status = check.status
q = Check.objects.filter(id=check.id, status=old_status)
current_status = check.get_status()
if current_status != "down":
if not check.is_down():
# It is not down yet. Update alert_after # It is not down yet. Update alert_after
q.update(alert_after=check.get_alert_after()) q.update(alert_after=check.get_alert_after())
return True return True
@ -107,7 +107,7 @@ class Command(BaseCommand):
flip = Flip(owner=check) flip = Flip(owner=check)
flip.created = check.get_alert_after() flip.created = check.get_alert_after()
flip.old_status = "up"
flip.old_status = old_status
flip.new_status = "down" flip.new_status = "down"
flip.save() flip.save()


+ 23
- 0
hc/api/migrations/0046_auto_20181218_1245.py View File

@ -0,0 +1,23 @@
# Generated by Django 2.1.4 on 2018-12-18 12:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0045_flip'),
]
operations = [
migrations.AddField(
model_name='check',
name='last_start',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='ping',
name='start',
field=models.NullBooleanField(default=False),
),
]

+ 78
- 48
hc/api/models.py View File

@ -25,6 +25,7 @@ STATUSES = (
) )
DEFAULT_TIMEOUT = td(days=1) DEFAULT_TIMEOUT = td(days=1)
DEFAULT_GRACE = td(hours=1) DEFAULT_GRACE = td(hours=1)
NEVER = datetime(3000, 1, 1, tzinfo=pytz.UTC)
CHECK_KINDS = (("simple", "Simple"), CHECK_KINDS = (("simple", "Simple"),
("cron", "Cron")) ("cron", "Cron"))
@ -55,7 +56,9 @@ PO_PRIORITIES = {
def isostring(dt): def isostring(dt):
"""Convert the datetime to ISO 8601 format with no microseconds. """ """Convert the datetime to ISO 8601 format with no microseconds. """
return dt.replace(microsecond=0).isoformat()
if dt:
return dt.replace(microsecond=0).isoformat()
class Check(models.Model): class Check(models.Model):
@ -73,6 +76,7 @@ class Check(models.Model):
tz = models.CharField(max_length=36, default="UTC") tz = models.CharField(max_length=36, default="UTC")
n_pings = models.IntegerField(default=0) n_pings = models.IntegerField(default=0)
last_ping = models.DateTimeField(null=True, blank=True) last_ping = models.DateTimeField(null=True, blank=True)
last_start = models.DateTimeField(null=True, blank=True)
last_ping_was_fail = models.NullBooleanField(default=False) last_ping_was_fail = models.NullBooleanField(default=False)
has_confirmation_link = models.BooleanField(default=False) has_confirmation_link = models.BooleanField(default=False)
alert_after = models.DateTimeField(null=True, blank=True, editable=False) alert_after = models.DateTimeField(null=True, blank=True, editable=False)
@ -110,34 +114,58 @@ class Check(models.Model):
return errors return errors
def get_grace_start(self): def get_grace_start(self):
""" Return the datetime when grace period starts. """
""" Return the datetime when the grace period starts.
# The common case, grace starts after timeout
if self.kind == "simple":
return self.last_ping + self.timeout
If the check is currently new, paused or down, return None.
# The complex case, next ping is expected based on cron schedule.
# Don't convert to naive datetimes (and so avoid ambiguities around
# DST transitions).
# croniter does handle timezone-aware datetimes.
"""
zone = pytz.timezone(self.tz)
last_local = timezone.localtime(self.last_ping, zone)
it = croniter(self.schedule, last_local)
return it.next(datetime)
# NEVER is a constant sentinel value (year 3000).
# Using None instead would make the logic clunky.
result = NEVER
def get_status(self, now=None):
""" Return "up" if the check is up or in grace, otherwise "down". """
if self.kind == "simple" and self.status == "up":
result = self.last_ping + self.timeout
elif self.kind == "cron" and self.status == "up":
# The complex case, next ping is expected based on cron schedule.
# Don't convert to naive datetimes (and so avoid ambiguities around
# DST transitions). Croniter will handle the timezone-aware datetimes.
if self.status in ("new", "paused"):
return self.status
zone = pytz.timezone(self.tz)
last_local = timezone.localtime(self.last_ping, zone)
it = croniter(self.schedule, last_local)
result = it.next(datetime)
if self.last_ping_was_fail:
return "down"
if self.last_start:
result = min(result, self.last_start)
if result != NEVER:
return result
def is_down(self):
""" Return True if the check is currently in alert state. """
alert_after = self.get_alert_after()
if alert_after is None:
return False
return timezone.now() >= self.get_alert_after()
def get_status(self, now=None):
""" Return current status for display. """
if now is None: if now is None:
now = timezone.now() now = timezone.now()
if self.last_start:
if now >= self.last_start + self.grace:
return "down"
else:
return "started"
if self.status in ("new", "paused", "down"):
return self.status
grace_start = self.get_grace_start() grace_start = self.get_grace_start()
grace_end = grace_start + self.grace grace_end = grace_start + self.grace
if now >= grace_end: if now >= grace_end:
@ -151,12 +179,9 @@ class Check(models.Model):
def get_alert_after(self): def get_alert_after(self):
""" Return the datetime when check potentially goes down. """ """ Return the datetime when check potentially goes down. """
# For "fail" pings, sendalerts should the check right
# after receiving the ping, without waiting for the grace time:
if self.last_ping_was_fail:
return self.last_ping
return self.get_grace_start() + self.grace
grace_start = self.get_grace_start()
if grace_start is not None:
return grace_start + self.grace
def assign_all_channels(self): def assign_all_channels(self):
if self.user: if self.user:
@ -183,7 +208,9 @@ class Check(models.Model):
"grace": int(self.grace.total_seconds()), "grace": int(self.grace.total_seconds()),
"n_pings": self.n_pings, "n_pings": self.n_pings,
"status": self.get_status(), "status": self.get_status(),
"channels": ",".join(sorted(channel_codes))
"channels": ",".join(sorted(channel_codes)),
"last_ping": isostring(self.last_ping),
"next_ping": isostring(self.get_grace_start())
} }
if self.kind == "simple": if self.kind == "simple":
@ -192,38 +219,40 @@ class Check(models.Model):
result["schedule"] = self.schedule result["schedule"] = self.schedule
result["tz"] = self.tz result["tz"] = self.tz
if self.last_ping:
result["last_ping"] = isostring(self.last_ping)
result["next_ping"] = isostring(self.get_grace_start())
else:
result["last_ping"] = None
result["next_ping"] = None
return result return result
def ping(self, remote_addr, scheme, method, ua, body, is_fail=False):
self.n_pings = models.F("n_pings") + 1
self.last_ping = timezone.now()
self.last_ping_was_fail = is_fail
self.has_confirmation_link = "confirm" in str(body).lower()
self.alert_after = self.get_alert_after()
def ping(self, remote_addr, scheme, method, ua, body, action):
if action == "start":
# If we receive multiple start events in a row,
# we remember the first one, not the last one
if self.last_start is None:
self.last_start = timezone.now()
# DOn't update "last_ping" field.
else:
self.last_start = None
self.last_ping = timezone.now()
self.last_ping_was_fail = action == "fail"
new_status = "down" if is_fail else "up"
if self.status != new_status:
flip = Flip(owner=self)
flip.created = self.last_ping
flip.old_status = self.status
flip.new_status = new_status
flip.save()
new_status = "down" if action == "fail" else "up"
if self.status != new_status:
flip = Flip(owner=self)
flip.created = self.last_ping
flip.old_status = self.status
flip.new_status = new_status
flip.save()
self.status = new_status
self.status = new_status
self.alert_after = self.get_alert_after()
self.n_pings = models.F("n_pings") + 1
self.has_confirmation_link = "confirm" in str(body).lower()
self.save() self.save()
self.refresh_from_db() self.refresh_from_db()
ping = Ping(owner=self) ping = Ping(owner=self)
ping.n = self.n_pings ping.n = self.n_pings
ping.fail = is_fail
ping.start = action == "start"
ping.fail = action == "fail"
ping.remote_addr = remote_addr ping.remote_addr = remote_addr
ping.scheme = scheme ping.scheme = scheme
ping.method = method ping.method = method
@ -238,6 +267,7 @@ class Ping(models.Model):
n = models.IntegerField(null=True) n = models.IntegerField(null=True)
owner = models.ForeignKey(Check, models.CASCADE) owner = models.ForeignKey(Check, models.CASCADE)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
start = models.NullBooleanField(default=False)
fail = models.NullBooleanField(default=False) fail = models.NullBooleanField(default=False)
scheme = models.CharField(max_length=10, default="http") scheme = models.CharField(max_length=10, default="http")
remote_addr = models.GenericIPAddressField(blank=True, null=True) remote_addr = models.GenericIPAddressField(blank=True, null=True)


+ 30
- 0
hc/api/tests/test_check_alert_after.py View File

@ -0,0 +1,30 @@
from datetime import timedelta as td
from django.test import TestCase
from django.utils import timezone
from hc.api.models import Check
class CheckModelTestCase(TestCase):
def test_it_handles_new_check(self):
check = Check()
self.assertEqual(check.get_alert_after(), None)
def test_it_handles_paused_check(self):
check = Check()
check.last_ping = timezone.now() - td(days=2)
self.assertEqual(check.get_alert_after(), None)
def test_it_handles_up(self):
check = Check(status="up")
check.last_ping = timezone.now() - td(hours=1)
expected_aa = check.last_ping + td(days=1, hours=1)
self.assertEqual(check.get_alert_after(), expected_aa)
def test_it_handles_paused_then_started_check(self):
check = Check(status="paused")
check.last_start = timezone.now() - td(days=2)
expected_aa = check.last_start + td(hours=1)
self.assertEqual(check.get_alert_after(), expected_aa)

+ 28
- 11
hc/api/tests/test_check_model.py View File

@ -27,7 +27,7 @@ class CheckModelTestCase(TestCase):
self.assertEqual(check.get_status(), "grace") self.assertEqual(check.get_status(), "grace")
def test_get_stauts_handles_paused_check(self):
def test_get_status_handles_paused_check(self):
check = Check() check = Check()
check.status = "up" check.status = "up"
@ -82,6 +82,33 @@ class CheckModelTestCase(TestCase):
now = dt + timedelta(days=1, minutes=60) now = dt + timedelta(days=1, minutes=60)
self.assertEqual(check.get_status(now), "down") self.assertEqual(check.get_status(now), "down")
def test_get_status_handles_past_grace(self):
check = Check()
check.status = "up"
check.last_ping = timezone.now() - timedelta(days=2)
self.assertEqual(check.get_status(), "down")
def test_get_status_obeys_down_status(self):
check = Check()
check.status = "down"
check.last_ping = timezone.now() - timedelta(minutes=1)
self.assertEqual(check.get_status(), "down")
def test_get_status_handles_started(self):
check = Check()
check.last_ping = timezone.now() - timedelta(hours=2)
check.last_start = timezone.now() - timedelta(minutes=5)
for status in ("new", "paused", "up", "down"):
check.status = status
self.assertEqual(check.get_status(), "started")
def test_get_status_handles_started_and_mia(self):
check = Check()
check.last_start = timezone.now() - timedelta(hours=2)
self.assertEqual(check.get_status(), "down")
def test_next_ping_with_cron_syntax(self): def test_next_ping_with_cron_syntax(self):
dt = timezone.make_aware(datetime(2000, 1, 1), timezone=timezone.utc) dt = timezone.make_aware(datetime(2000, 1, 1), timezone=timezone.utc)
@ -95,13 +122,3 @@ class CheckModelTestCase(TestCase):
d = check.to_dict() d = check.to_dict()
self.assertEqual(d["next_ping"], "2000-01-01T01:00:00+00:00") self.assertEqual(d["next_ping"], "2000-01-01T01:00:00+00:00")
def test_status_checks_the_fail_flag(self):
check = Check()
check.status = "up"
check.last_ping = timezone.now() - timedelta(minutes=5)
check.last_ping_was_fail = True
# The computed status should be "down" because last_ping_was_fail
# is set.
self.assertEqual(check.get_status(), "down")

+ 11
- 12
hc/api/tests/test_list_checks.py View File

@ -17,8 +17,7 @@ class ListChecksTestCase(BaseTestCase):
self.a1 = Check(user=self.alice, name="Alice 1") self.a1 = Check(user=self.alice, name="Alice 1")
self.a1.timeout = td(seconds=3600) self.a1.timeout = td(seconds=3600)
self.a1.grace = td(seconds=900) self.a1.grace = td(seconds=900)
self.a1.last_ping = self.now
self.a1.n_pings = 1
self.a1.n_pings = 0
self.a1.status = "new" self.a1.status = "new"
self.a1.tags = "a1-tag a1-additional-tag" self.a1.tags = "a1-tag a1-additional-tag"
self.a1.save() self.a1.save()
@ -45,19 +44,16 @@ class ListChecksTestCase(BaseTestCase):
doc = r.json() doc = r.json()
self.assertEqual(len(doc["checks"]), 2) self.assertEqual(len(doc["checks"]), 2)
a1 = None
a2 = None
by_name = {}
for check in doc["checks"]: for check in doc["checks"]:
if check["name"] == "Alice 1":
a1 = check
if check["name"] == "Alice 2":
a2 = check
by_name[check["name"]] = check
a1 = by_name["Alice 1"]
self.assertEqual(a1["timeout"], 3600) self.assertEqual(a1["timeout"], 3600)
self.assertEqual(a1["grace"], 900) self.assertEqual(a1["grace"], 900)
self.assertEqual(a1["ping_url"], self.a1.url()) self.assertEqual(a1["ping_url"], self.a1.url())
self.assertEqual(a1["last_ping"], self.now.isoformat())
self.assertEqual(a1["n_pings"], 1)
self.assertEqual(a1["last_ping"], None)
self.assertEqual(a1["n_pings"], 0)
self.assertEqual(a1["status"], "new") self.assertEqual(a1["status"], "new")
self.assertEqual(a1["channels"], str(self.c1.code)) self.assertEqual(a1["channels"], str(self.c1.code))
@ -66,13 +62,16 @@ class ListChecksTestCase(BaseTestCase):
self.assertEqual(a1["update_url"], update_url) self.assertEqual(a1["update_url"], update_url)
self.assertEqual(a1["pause_url"], pause_url) self.assertEqual(a1["pause_url"], pause_url)
next_ping = self.now + td(seconds=3600)
self.assertEqual(a1["next_ping"], next_ping.isoformat())
self.assertEqual(a1["next_ping"], None)
a2 = by_name["Alice 2"]
self.assertEqual(a2["timeout"], 86400) self.assertEqual(a2["timeout"], 86400)
self.assertEqual(a2["grace"], 3600) self.assertEqual(a2["grace"], 3600)
self.assertEqual(a2["ping_url"], self.a2.url()) self.assertEqual(a2["ping_url"], self.a2.url())
self.assertEqual(a2["status"], "up") self.assertEqual(a2["status"], "up")
next_ping = self.now + td(seconds=86400)
self.assertEqual(a2["last_ping"], self.now.isoformat())
self.assertEqual(a2["next_ping"], next_ping.isoformat())
def test_it_handles_options(self): def test_it_handles_options(self):
r = self.client.options("/api/v1/checks/") r = self.client.options("/api/v1/checks/")


+ 15
- 0
hc/api/tests/test_pause.py View File

@ -1,3 +1,4 @@
from django.utils.timezone import now
from hc.api.models import Check from hc.api.models import Check
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -55,3 +56,17 @@ class PauseTestCase(BaseTestCase):
HTTP_X_API_KEY="X" * 32) HTTP_X_API_KEY="X" * 32)
self.assertEqual(r.status_code, 404) self.assertEqual(r.status_code, 404)
def test_it_clears_last_start(self):
check = Check(user=self.alice, status="up", last_start=now())
check.save()
url = "/api/v1/checks/%s/pause" % check.code
r = self.client.post(url, "", content_type="application/json",
HTTP_X_API_KEY="X" * 32)
self.assertEqual(r.status_code, 200)
self.assertEqual(r["Access-Control-Allow-Origin"], "*")
check.refresh_from_db()
self.assertEqual(check.last_start, None)

+ 59
- 4
hc/api/tests/test_ping.py View File

@ -1,7 +1,8 @@
from datetime import timedelta as td
from django.test import Client, TestCase from django.test import Client, TestCase
from django.utils.timezone import now from django.utils.timezone import now
from hc.api.models import Check, Ping
from hc.api.models import Check, Flip, Ping
class PingTestCase(TestCase): class PingTestCase(TestCase):
@ -31,6 +32,16 @@ class PingTestCase(TestCase):
self.check.refresh_from_db() self.check.refresh_from_db()
self.assertEqual(self.check.status, "up") self.assertEqual(self.check.status, "up")
def test_it_clears_last_start(self):
self.check.last_start = now()
self.check.save()
r = self.client.get("/ping/%s/" % self.check.code)
self.assertEqual(r.status_code, 200)
self.check.refresh_from_db()
self.assertEqual(self.check.last_start, None)
def test_post_works(self): def test_post_works(self):
csrf_client = Client(enforce_csrf_checks=True) csrf_client = Client(enforce_csrf_checks=True)
r = csrf_client.post("/ping/%s/" % self.check.code, "hello world", r = csrf_client.post("/ping/%s/" % self.check.code, "hello world",
@ -132,7 +143,51 @@ class PingTestCase(TestCase):
self.check.refresh_from_db() self.check.refresh_from_db()
self.assertTrue(self.check.last_ping_was_fail) self.assertTrue(self.check.last_ping_was_fail)
self.assertTrue(self.check.alert_after <= now())
self.assertEqual(self.check.status, "down")
self.assertEqual(self.check.alert_after, None)
ping = Ping.objects.latest("id")
ping = Ping.objects.get()
self.assertTrue(ping.fail) self.assertTrue(ping.fail)
flip = Flip.objects.get()
self.assertEqual(flip.owner, self.check)
self.assertEqual(flip.new_status, "down")
def test_start_endpoint_works(self):
last_ping = now() - td(hours=2)
self.check.last_ping = last_ping
self.check.save()
r = self.client.get("/ping/%s/start" % self.check.code)
self.assertEqual(r.status_code, 200)
self.check.refresh_from_db()
self.assertTrue(self.check.last_start)
self.assertEqual(self.check.last_ping, last_ping)
ping = Ping.objects.get()
self.assertTrue(ping.start)
def test_start_does_not_change_status_of_paused_check(self):
self.check.status = "paused"
self.check.save()
r = self.client.get("/ping/%s/start" % self.check.code)
self.assertEqual(r.status_code, 200)
self.check.refresh_from_db()
self.assertTrue(self.check.last_start)
self.assertEqual(self.check.status, "paused")
def test_start_does_not_overwrite_last_start(self):
first_start = now() - td(hours=2)
self.check.last_start = first_start
self.check.save()
r = self.client.get("/ping/%s/start" % self.check.code)
self.assertEqual(r.status_code, 200)
self.check.refresh_from_db()
# Should still be the original value
self.assertEqual(self.check.last_start, first_start)

+ 2
- 2
hc/api/urls.py View File

@ -5,8 +5,8 @@ from hc.api import views
urlpatterns = [ urlpatterns = [
path('ping/<uuid:code>/', views.ping, name="hc-ping-slash"), path('ping/<uuid:code>/', views.ping, name="hc-ping-slash"),
path('ping/<uuid:code>', views.ping, name="hc-ping"), path('ping/<uuid:code>', views.ping, name="hc-ping"),
path('ping/<uuid:code>/fail', views.ping, {"is_fail": True},
name="hc-fail"),
path('ping/<uuid:code>/fail', views.ping, {"action": "fail"}, name="hc-fail"),
path('ping/<uuid:code>/start', views.ping, {"action": "start"}, name="hc-start"),
path('api/v1/checks/', views.checks), path('api/v1/checks/', views.checks),
path('api/v1/checks/<uuid:code>', views.update, name="hc-api-update"), path('api/v1/checks/<uuid:code>', views.update, name="hc-api-update"),


+ 3
- 2
hc/api/views.py View File

@ -19,7 +19,7 @@ from hc.lib.badges import check_signature, get_badge_svg
@csrf_exempt @csrf_exempt
@never_cache @never_cache
def ping(request, code, is_fail=False):
def ping(request, code, action="success"):
check = get_object_or_404(Check, code=code) check = get_object_or_404(Check, code=code)
headers = request.META headers = request.META
@ -30,7 +30,7 @@ def ping(request, code, is_fail=False):
ua = headers.get("HTTP_USER_AGENT", "") ua = headers.get("HTTP_USER_AGENT", "")
body = request.body.decode() body = request.body.decode()
check.ping(remote_addr, scheme, method, ua, body, is_fail)
check.ping(remote_addr, scheme, method, ua, body, action)
response = HttpResponse("OK") response = HttpResponse("OK")
response["Access-Control-Allow-Origin"] = "*" response["Access-Control-Allow-Origin"] = "*"
@ -188,6 +188,7 @@ def pause(request, code):
return HttpResponseForbidden() return HttpResponseForbidden()
check.status = "paused" check.status = "paused"
check.last_start = None
check.save() check.save()
return JsonResponse(check.to_dict()) return JsonResponse(check.to_dict())


+ 16
- 8
hc/front/tests/test_pause.py View File

@ -1,3 +1,4 @@
from django.utils.timezone import now
from hc.api.models import Check from hc.api.models import Check
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -9,28 +10,35 @@ class PauseTestCase(BaseTestCase):
self.check = Check(user=self.alice, status="up") self.check = Check(user=self.alice, status="up")
self.check.save() self.check.save()
def test_it_pauses(self):
url = "/checks/%s/pause/" % self.check.code
self.url = "/checks/%s/pause/" % self.check.code
def test_it_pauses(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.post(url)
r = self.client.post(self.url)
self.assertRedirects(r, "/checks/") self.assertRedirects(r, "/checks/")
self.check.refresh_from_db() self.check.refresh_from_db()
self.assertEqual(self.check.status, "paused") self.assertEqual(self.check.status, "paused")
def test_it_rejects_get(self): def test_it_rejects_get(self):
url = "/checks/%s/pause/" % self.check.code
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(url)
r = self.client.get(self.url)
self.assertEqual(r.status_code, 405) self.assertEqual(r.status_code, 405)
def test_it_allows_cross_team_access(self): def test_it_allows_cross_team_access(self):
self.bobs_profile.current_team = None self.bobs_profile.current_team = None
self.bobs_profile.save() self.bobs_profile.save()
url = "/checks/%s/pause/" % self.check.code
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.post(url)
r = self.client.post(self.url)
self.assertRedirects(r, "/checks/") self.assertRedirects(r, "/checks/")
def test_it_clears_last_start(self):
self.check.last_start = now()
self.check.save()
self.client.login(username="[email protected]", password="password")
self.client.post(self.url)
self.check.refresh_from_db()
self.assertEqual(self.check.last_start, None)

+ 22
- 2
hc/front/tests/test_ping_details.py View File

@ -14,6 +14,26 @@ class LastPingTestCase(BaseTestCase):
r = self.client.get("/checks/%s/last_ping/" % check.code) r = self.client.get("/checks/%s/last_ping/" % check.code)
self.assertContains(r, "this is body", status_code=200) self.assertContains(r, "this is body", status_code=200)
def test_it_shows_fail(self):
check = Check(user=self.alice)
check.save()
Ping.objects.create(owner=check, fail=True)
self.client.login(username="[email protected]", password="password")
r = self.client.get("/checks/%s/last_ping/" % check.code)
self.assertContains(r, "/fail", status_code=200)
def test_it_shows_start(self):
check = Check(user=self.alice)
check.save()
Ping.objects.create(owner=check, start=True)
self.client.login(username="[email protected]", password="password")
r = self.client.get("/checks/%s/last_ping/" % check.code)
self.assertContains(r, "/start", status_code=200)
def test_it_requires_user(self): def test_it_requires_user(self):
check = Check.objects.create() check = Check.objects.create()
r = self.client.get("/checks/%s/last_ping/" % check.code) r = self.client.get("/checks/%s/last_ping/" % check.code)
@ -24,8 +44,8 @@ class LastPingTestCase(BaseTestCase):
check.save() check.save()
# remote_addr, scheme, method, ua, body: # remote_addr, scheme, method, ua, body:
check.ping("1.2.3.4", "http", "post", "tester", "foo-123")
check.ping("1.2.3.4", "http", "post", "tester", "bar-456")
check.ping("1.2.3.4", "http", "post", "tester", "foo-123", "success")
check.ping("1.2.3.4", "http", "post", "tester", "bar-456", "success")
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")


+ 3
- 3
hc/front/views.py View File

@ -301,11 +301,10 @@ def update_timeout(request, code):
check.alert_after = check.get_alert_after() check.alert_after = check.get_alert_after()
# Changing timeout can change check's status: # Changing timeout can change check's status:
is_up = check.get_status() in ("up", "grace")
if is_up and check.status != "up":
if not check.is_down() and check.status == "down":
flip = Flip(owner=check) flip = Flip(owner=check)
flip.created = timezone.now() flip.created = timezone.now()
flip.old_status = check.status
flip.old_status = "down"
flip.new_status = "up" flip.new_status = "up"
flip.save() flip.save()
@ -365,6 +364,7 @@ def pause(request, code):
check = _get_check_for_user(request, code) check = _get_check_for_user(request, code)
check.status = "paused" check.status = "paused"
check.last_start = None
check.save() check.save()
if "/details/" in request.META.get("HTTP_REFERER", ""): if "/details/" in request.META.get("HTTP_REFERER", ""):


+ 8
- 2
static/css/base.css View File

@ -63,11 +63,17 @@ body {
width: 24px; width: 24px;
} }
.status.icon-up { color: #5cb85c; }
.status.icon-up, .status.icon-started { color: #5cb85c; }
.status.icon-new, .status.icon-paused { color: #CCC; } .status.icon-new, .status.icon-paused { color: #CCC; }
.status.icon-grace { color: #f0ad4e; } .status.icon-grace { color: #f0ad4e; }
.status.icon-down { color: #d9534f; } .status.icon-down { color: #d9534f; }
.label-start {
background-color: #FFF;
color: #117a3f;;
border: 1px solid #117a3f;
}
.hc-dialog { .hc-dialog {
background: #FFF; background: #FFF;
padding: 2em; padding: 2em;
@ -99,4 +105,4 @@ pre {
text-align: center; text-align: center;
font-size: small; font-size: small;
padding: 2px 0; padding: 2px 0;
}
}

+ 14
- 13
static/css/icomoon.css View File

@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('../fonts/icomoon.eot?b4dy0b');
src: url('../fonts/icomoon.eot?b4dy0b#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?b4dy0b') format('truetype'),
url('../fonts/icomoon.woff?b4dy0b') format('woff'),
url('../fonts/icomoon.svg?b4dy0b#icomoon') format('svg');
src: url('../fonts/icomoon.eot?swifyd');
src: url('../fonts/icomoon.eot?swifyd#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?swifyd') format('truetype'),
url('../fonts/icomoon.woff?swifyd') format('woff'),
url('../fonts/icomoon.svg?swifyd#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -24,6 +24,9 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-started:before {
content: "\e038";
}
.icon-pagertree:before { .icon-pagertree:before {
content: "\e90d"; content: "\e90d";
color: #33ade2; color: #33ade2;
@ -80,6 +83,10 @@
content: "\e911"; content: "\e911";
color: #0079bf; color: #0079bf;
} }
.icon-whatsapp:before {
content: "\e902";
color: #25d366;
}
.icon-zendesk:before { .icon-zendesk:before {
content: "\e907"; content: "\e907";
} }
@ -92,9 +99,6 @@
.icon-up:before, .icon-new:before, .icon-ok:before { .icon-up:before, .icon-new:before, .icon-ok:before {
content: "\e86c"; content: "\e86c";
} }
.icon-close:before {
content: "\e5cd";
}
.icon-grace:before { .icon-grace:before {
content: "\e000"; content: "\e000";
} }
@ -107,9 +111,6 @@
.icon-asc:before { .icon-asc:before {
content: "\e316"; content: "\e316";
} }
.icon-mail:before {
content: "\e0e1";
}
.icon-dots:before { .icon-dots:before {
content: "\e5d3"; content: "\e5d3";
} }
@ -120,8 +121,8 @@
content: "\e7f6"; content: "\e7f6";
} }
.icon-settings:before { .icon-settings:before {
content: "\e902";
content: "\e912";
} }
.icon-delete:before { .icon-delete:before {
content: "\e900";
content: "\e913";
} }

BIN
static/fonts/icomoon.eot View File


+ 20
- 20
static/fonts/icomoon.svg
File diff suppressed because it is too large
View File


BIN
static/fonts/icomoon.ttf View File


BIN
static/fonts/icomoon.woff View File


+ 2
- 0
templates/emails/summary-html.html View File

@ -21,6 +21,8 @@
<td style="background: #f0ad4e; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">LATE</td> <td style="background: #f0ad4e; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">LATE</td>
{% elif check.get_status == "up" %} {% elif check.get_status == "up" %}
<td style="background: #5cb85c; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">UP</td> <td style="background: #5cb85c; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">UP</td>
{% elif check.get_status == "started" %}
<td style="background: #5cb85c; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">STARTED</td>
{% elif check.get_status == "down" %} {% elif check.get_status == "down" %}
<td style="background: #d9534f; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">DOWN</td> <td style="background: #d9534f; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">DOWN</td>
{% endif %} {% endif %}


+ 2
- 0
templates/front/details_events.html View File

@ -12,6 +12,8 @@
<td class="text-right"> <td class="text-right">
{% if event.fail %} {% if event.fail %}
<span class="label label-danger">Failure</span> <span class="label label-danger">Failure</span>
{% elif event.start %}
<span class="label label-start">Started</span>
{% else %} {% else %}
<span class="label label-success">OK</span> <span class="label label-success">OK</span>
{% endif %} {% endif %}


+ 2
- 0
templates/front/log.html View File

@ -46,6 +46,8 @@
<td class="text-right"> <td class="text-right">
{% if event.fail %} {% if event.fail %}
<span class="label label-danger">Failure</span> <span class="label label-danger">Failure</span>
{% elif event.start %}
<span class="label label-start">Started</span>
{% else %} {% else %}
<span class="label label-success">OK</span> <span class="label label-success">OK</span>
{% endif %} {% endif %}


+ 2
- 0
templates/front/log_status_text.html View File

@ -10,5 +10,7 @@
This check is paused. This check is paused.
{% elif status == "new" %} {% elif status == "new" %}
This check has never received a ping. This check has never received a ping.
{% elif status == "started" %}
This check is currently running. Started {{ check.last_start|naturaltime }}.
{% endif %} {% endif %}
{% endwith %} {% endwith %}

+ 2
- 0
templates/front/ping_details.html View File

@ -2,6 +2,8 @@
<h3>Ping #{{ ping.n }} <h3>Ping #{{ ping.n }}
{% if ping.fail %} {% if ping.fail %}
<span class="text-danger">(received via the <code>/fail</code> endpoint)</span> <span class="text-danger">(received via the <code>/fail</code> endpoint)</span>
{% elif ping.start %}
<span class="text-success">(received via the <code>/start</code> endpoint)</span>
{% endif %} {% endif %}
</h3> </h3>


Loading…
Cancel
Save