Browse Source

Fix downtime summary to handle months when the check didn't exist

Fixes: #472
pull/476/head
Pēteris Caune 4 years ago
parent
commit
5979204691
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
7 changed files with 133 additions and 34 deletions
  1. +5
    -0
      CHANGELOG.md
  2. +7
    -1
      hc/api/models.py
  3. +61
    -23
      hc/api/tests/test_check_model.py
  4. +50
    -2
      hc/front/tests/test_details.py
  5. +1
    -1
      hc/lib/date.py
  6. +5
    -5
      templates/emails/summary-downtimes-html.html
  7. +4
    -2
      templates/front/details_downtimes.html

+ 5
- 0
CHANGELOG.md View File

@ -1,6 +1,11 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## v1.20.0 - Unreleased
## Bug Fixes
- Fix downtime summary to handle months when the check didn't exist yet (#472)
## v1.19.0 - 2021-02-03 ## v1.19.0 - 2021-02-03
## Improvements ## Improvements


+ 7
- 1
hc/api/models.py View File

@ -304,7 +304,7 @@ class Check(models.Model):
ping.exitstatus = exitstatus ping.exitstatus = exitstatus
ping.save() ping.save()
def downtimes(self, months=3):
def downtimes(self, months=2):
""" Calculate the number of downtimes and downtime minutes per month. """ Calculate the number of downtimes and downtime minutes per month.
Returns a list of (datetime, downtime_in_secs, number_of_outages) tuples. Returns a list of (datetime, downtime_in_secs, number_of_outages) tuples.
@ -340,6 +340,12 @@ class Check(models.Model):
if prev_status != "---": if prev_status != "---":
status = prev_status status = prev_status
# Set counters to None for months when the check didn't exist yet
for ym in totals:
if ym < monthkey(self.created):
totals[ym][1] = None
totals[ym][2] = None
return sorted(totals.values()) return sorted(totals.values())


+ 61
- 23
hc/api/tests/test_check_model.py View File

@ -1,10 +1,13 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import patch
from unittest.mock import Mock, patch
from django.utils import timezone from django.utils import timezone
from hc.api.models import Check, Flip from hc.api.models import Check, Flip
from hc.test import BaseTestCase from hc.test import BaseTestCase
CURRENT_TIME = datetime(2020, 1, 15, tzinfo=timezone.utc)
MOCK_NOW = Mock(return_value=CURRENT_TIME)
class CheckModelTestCase(BaseTestCase): class CheckModelTestCase(BaseTestCase):
def test_it_strips_tags(self): def test_it_strips_tags(self):
@ -162,29 +165,45 @@ class CheckModelTestCase(BaseTestCase):
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")
@patch("hc.api.models.timezone.now", MOCK_NOW)
def test_downtimes_handles_no_flips(self): def test_downtimes_handles_no_flips(self):
check = Check.objects.create(project=self.project)
r = check.downtimes(10)
self.assertEqual(len(r), 10)
for dt, downtime, outages in r:
self.assertEqual(downtime.total_seconds(), 0)
self.assertEqual(outages, 0)
check = Check(project=self.project)
check.created = datetime(2019, 1, 1, tzinfo=timezone.utc)
nov, dec, jan = check.downtimes(3)
# Nov. 2019
self.assertEqual(nov[0].strftime("%m-%Y"), "11-2019")
self.assertEqual(nov[1], timedelta())
self.assertEqual(nov[2], 0)
# Dec. 2019
self.assertEqual(dec[0].strftime("%m-%Y"), "12-2019")
self.assertEqual(dec[1], timedelta())
self.assertEqual(dec[2], 0)
# Jan. 2020
self.assertEqual(jan[0].strftime("%m-%Y"), "01-2020")
self.assertEqual(jan[1], timedelta())
self.assertEqual(jan[2], 0)
@patch("hc.api.models.timezone.now", MOCK_NOW)
def test_downtimes_handles_currently_down_check(self): def test_downtimes_handles_currently_down_check(self):
check = Check.objects.create(project=self.project, status="down")
check = Check(project=self.project, status="down")
check.created = datetime(2019, 1, 1, tzinfo=timezone.utc)
r = check.downtimes(10) r = check.downtimes(10)
self.assertEqual(len(r), 10) self.assertEqual(len(r), 10)
for dt, downtime, outages in r: for dt, downtime, outages in r:
self.assertEqual(outages, 1) self.assertEqual(outages, 1)
@patch("hc.api.models.timezone.now")
def test_downtimes_handles_flip_one_day_ago(self, mock_now):
mock_now.return_value = datetime(2019, 7, 19, tzinfo=timezone.utc)
@patch("hc.api.models.timezone.now", MOCK_NOW)
def test_downtimes_handles_flip_one_day_ago(self):
check = Check.objects.create(project=self.project, status="down") check = Check.objects.create(project=self.project, status="down")
check.created = datetime(2019, 1, 1, tzinfo=timezone.utc)
flip = Flip(owner=check) flip = Flip(owner=check)
flip.created = datetime(2019, 7, 18, tzinfo=timezone.utc)
flip.created = datetime(2020, 1, 14, tzinfo=timezone.utc)
flip.old_status = "up" flip.old_status = "up"
flip.new_status = "down" flip.new_status = "down"
flip.save() flip.save()
@ -192,20 +211,20 @@ class CheckModelTestCase(BaseTestCase):
r = check.downtimes(10) r = check.downtimes(10)
self.assertEqual(len(r), 10) self.assertEqual(len(r), 10)
for dt, downtime, outages in r: for dt, downtime, outages in r:
if dt.month == 7:
if dt.month == 1:
self.assertEqual(downtime.total_seconds(), 86400) self.assertEqual(downtime.total_seconds(), 86400)
self.assertEqual(outages, 1) self.assertEqual(outages, 1)
else: else:
self.assertEqual(downtime.total_seconds(), 0) self.assertEqual(downtime.total_seconds(), 0)
self.assertEqual(outages, 0) self.assertEqual(outages, 0)
@patch("hc.api.models.timezone.now")
def test_downtimes_handles_flip_two_months_ago(self, mock_now):
mock_now.return_value = datetime(2019, 7, 19, tzinfo=timezone.utc)
@patch("hc.api.models.timezone.now", MOCK_NOW)
def test_downtimes_handles_flip_two_months_ago(self):
check = Check.objects.create(project=self.project, status="down") check = Check.objects.create(project=self.project, status="down")
check.created = datetime(2019, 1, 1, tzinfo=timezone.utc)
flip = Flip(owner=check) flip = Flip(owner=check)
flip.created = datetime(2019, 5, 19, tzinfo=timezone.utc)
flip.created = datetime(2019, 11, 15, tzinfo=timezone.utc)
flip.old_status = "up" flip.old_status = "up"
flip.new_status = "down" flip.new_status = "down"
flip.save() flip.save()
@ -213,13 +232,32 @@ class CheckModelTestCase(BaseTestCase):
r = check.downtimes(10) r = check.downtimes(10)
self.assertEqual(len(r), 10) self.assertEqual(len(r), 10)
for dt, downtime, outages in r: for dt, downtime, outages in r:
if dt.month == 7:
if dt.month == 11:
self.assertEqual(outages, 1) self.assertEqual(outages, 1)
elif dt.month == 6:
self.assertEqual(downtime.total_seconds(), 30 * 86400)
elif dt.month == 12:
self.assertEqual(downtime.total_seconds(), 31 * 86400)
self.assertEqual(outages, 1) self.assertEqual(outages, 1)
elif dt.month == 5:
elif dt.month == 1:
self.assertEqual(outages, 1) self.assertEqual(outages, 1)
else: else:
self.assertEqual(downtime.total_seconds(), 0) self.assertEqual(downtime.total_seconds(), 0)
self.assertEqual(outages, 0) self.assertEqual(outages, 0)
@patch("hc.api.models.timezone.now", MOCK_NOW)
def test_downtimes_handles_months_when_check_did_not_exist(self):
check = Check(project=self.project)
check.created = datetime(2020, 1, 1, tzinfo=timezone.utc)
nov, dec, jan = check.downtimes(3)
# Nov. 2019
self.assertIsNone(nov[1])
self.assertIsNone(nov[2])
# Dec. 2019
self.assertIsNone(dec[1])
self.assertIsNone(dec[2])
# Jan. 2020
self.assertEqual(jan[1], timedelta())
self.assertEqual(jan[2], 0)

+ 50
- 2
hc/front/tests/test_details.py View File

@ -1,6 +1,8 @@
from datetime import timedelta as td
from datetime import datetime, timedelta as td
from unittest.mock import patch
from hc.api.models import Check, Ping
from django.utils import timezone
from hc.api.models import Flip, Check, Ping
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -105,3 +107,49 @@ class DetailsTestCase(BaseTestCase):
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertContains(r, f"* * * * * /your/command.sh") self.assertContains(r, f"* * * * * /your/command.sh")
self.assertContains(r, 'FIXME: replace "* * * * *"') self.assertContains(r, 'FIXME: replace "* * * * *"')
@patch("hc.lib.date.timezone.now")
def test_it_calculates_downtime_summary(self, mock_now):
mock_now.return_value = datetime(2020, 2, 1, tzinfo=timezone.utc)
self.check.created = datetime(2019, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
self.check.save()
# going down on Jan 15, at 12:00
f1 = Flip(owner=self.check)
f1.created = datetime(2020, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
f1.old_status = "up"
f1.new_status = "down"
f1.save()
# back up on Jan 15, at 13:00
f2 = Flip(owner=self.check)
f2.created = datetime(2020, 1, 15, 13, 0, 0, tzinfo=timezone.utc)
f2.old_status = "down"
f2.new_status = "up"
f2.save()
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertContains(r, "Feb. 2020")
self.assertContains(r, "Jan. 2020")
self.assertContains(r, "Dec. 2019")
# The summary for Jan. 2020 should be "1 downtime, 1 hour total"
self.assertContains(r, "1 downtime, 1 hour total", html=True)
@patch("hc.lib.date.timezone.now")
def test_it_handles_months_when_check_did_not_exist(self, mock_now):
mock_now.return_value = datetime(2020, 2, 1, tzinfo=timezone.utc)
self.check.created = datetime(2020, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
self.check.save()
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertContains(r, "Feb. 2020")
self.assertContains(r, "Jan. 2020")
self.assertContains(r, "Dec. 2019")
# The summary for Dec. 2019 should be "–"
self.assertContains(r, "<td>–</td>", html=True)

+ 1
- 1
hc/lib/date.py View File

@ -71,7 +71,7 @@ def format_approx_duration(td):
return "" return ""
def month_boundaries(months=3):
def month_boundaries(months=2):
result = [] result = []
now = timezone.now() now = timezone.now()


+ 5
- 5
templates/emails/summary-downtimes-html.html View File

@ -7,11 +7,9 @@
{{ group.grouper|mangle_link }} {{ group.grouper|mangle_link }}
</td> </td>
{% for dt in month_boundaries %} {% for dt in month_boundaries %}
{% if not forloop.last %}
<td style="padding: 32px 8px 8px 8px; margin: 0; font-size: 12px; color: #9BA2AB; font-family: Helvetica, Arial, sans-serif;"> <td style="padding: 32px 8px 8px 8px; margin: 0; font-size: 12px; color: #9BA2AB; font-family: Helvetica, Arial, sans-serif;">
{{ dt|date:"N Y"}} {{ dt|date:"N Y"}}
</td> </td>
{% endif %}
{% endfor %} {% endfor %}
</tr> </tr>
{% for check in group.list|sortchecks:sort %} {% for check in group.list|sortchecks:sort %}
@ -63,7 +61,6 @@
{% endif %} {% endif %}
</td> </td>
{% for boundary, seconds, count in check.downtimes %} {% for boundary, seconds, count in check.downtimes %}
{% if not forloop.last %}
{% if count %} {% if count %}
<td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif;"> <td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif;">
{{ count }}&nbsp;downtime{{ count|pluralize }}, {{ count }}&nbsp;downtime{{ count|pluralize }},
@ -72,10 +69,13 @@
</td> </td>
{% else %} {% else %}
<td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif; color: #9BA2AB;"> <td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif; color: #9BA2AB;">
All good!
{% if count is None %}
{% comment %} The check didn't exist yet {% endcomment %}
{% else %}
All good!
{% endif %}
</td> </td>
{% endif %} {% endif %}
{% endif %}
{% endfor %} {% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}


+ 4
- 2
templates/front/details_downtimes.html View File

@ -1,12 +1,14 @@
{% load hc_extras %} {% load hc_extras %}
<table class="table"> <table class="table">
{% for boundary, seconds, count in downtimes reversed %}
{% for boundary, down_timedelta, count in downtimes reversed %}
<tr> <tr>
<th>{{ boundary|date:"N Y"}}</th> <th>{{ boundary|date:"N Y"}}</th>
<td> <td>
{% if count %} {% if count %}
{{ count }} downtime{{ count|pluralize }}, {{ count }} downtime{{ count|pluralize }},
{{ seconds|hc_approx_duration }} total
{{ down_timedelta|hc_approx_duration }} total
{% elif count is None %}
{% else %} {% else %}
All good! All good!
{% endif %} {% endif %}


Loading…
Cancel
Save