From 2a0ae809a7664ee0c9e1a94482a3b83cac45510b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?= <cuu508@gmail.com>
Date: Wed, 27 Sep 2023 17:43:18 +0300
Subject: [PATCH] Add DowntimeRecord.no_data field

---
 hc/api/models.py                          | 35 +++++++++++------------
 hc/api/tests/test_check_model.py          | 19 ++++++++++--
 templates/emails/report-summary-html.html |  2 +-
 templates/front/details_downtimes.html    |  6 ++--
 4 files changed, 37 insertions(+), 25 deletions(-)

diff --git a/hc/api/models.py b/hc/api/models.py
index 3df77d6e..95dda315 100644
--- a/hc/api/models.py
+++ b/hc/api/models.py
@@ -133,27 +133,34 @@ class CheckDict(TypedDict, total=False):
 
 @dataclass
 class DowntimeRecord:
-    boundary: datetime
-    tz: str
-    duration: td
-    count: int | None
+    boundary: datetime  # The start of this time interval (timezone-aware)
+    tz: str  # For calculating total seconds in a month
+    no_data: bool  # True if the check did not yet exist in this time interval
+    duration: td  # Total downtime in this time interval
+    count: int  # The number of downtime events in this time interval
 
     def monthly_uptime(self) -> float:
         # NB: this method assumes monthly boundaries.
         # It will yield incorrect results for weekly boundaries
-
         max_seconds = seconds_in_month(self.boundary.date(), self.tz)
         up_seconds = max_seconds - self.duration.total_seconds()
         return up_seconds / max_seconds
 
 
 class DowntimeSummary(object):
-    def __init__(self, boundaries: list[datetime], tz: str) -> None:
+    def __init__(self, boundaries: list[datetime], tz: str, created: datetime) -> None:
         """
-        `boundaries` are timezone-aware datetimes of the first days of time intervals
-        (months or weeks), and should be pre-sorted in descending order.
+        `boundaries` is a list of timezone-aware datetimes of the starts of time
+        intervals (months or weeks), and should be pre-sorted in descending order.
         """
-        self.records = [DowntimeRecord(b, tz, td(), 0) for b in boundaries]
+        self.records = []
+        prev_boundary = None
+        for b in boundaries:
+            # If the check was created *after* the start of the previous time
+            # interval then the check did not yet exist during this time interval:
+            no_data = prev_boundary and created > prev_boundary
+            self.records.append(DowntimeRecord(b, tz, no_data, td(), 0))
+            prev_boundary = b
 
     def add(self, when: datetime, duration: td) -> None:
         for record in self.records:
@@ -513,7 +520,7 @@ class Check(models.Model):
 
         """
 
-        summary = DowntimeSummary(boundaries, tz)
+        summary = DowntimeSummary(boundaries, tz, self.created)
 
         # A list of flips and time interval boundaries
         events = [(b, "---") for b in boundaries]
@@ -535,14 +542,6 @@ class Check(models.Model):
             if prev_status != "---":
                 status = prev_status
 
-        # Set count to None for intervals when the check didn't exist yet
-        prev_boundary = None
-        for record in summary.records:
-            if prev_boundary and self.created > prev_boundary:
-                record.count = None
-
-            prev_boundary = record.boundary
-
         return summary.records
 
     def downtimes(self, months: int, tz: str) -> list[DowntimeRecord]:
diff --git a/hc/api/tests/test_check_model.py b/hc/api/tests/test_check_model.py
index be732984..800f6f72 100644
--- a/hc/api/tests/test_check_model.py
+++ b/hc/api/tests/test_check_model.py
@@ -188,16 +188,22 @@ class CheckModelTestCase(BaseTestCase):
 
         # Jan. 2020
         self.assertEqual(jan.boundary.strftime("%m-%Y"), "01-2020")
+        self.assertEqual(jan.tz, "UTC")
+        self.assertFalse(jan.no_data)
         self.assertEqual(jan.duration, td())
         self.assertEqual(jan.count, 0)
 
         # Dec. 2019
         self.assertEqual(dec.boundary.strftime("%m-%Y"), "12-2019")
+        self.assertEqual(jan.tz, "UTC")
+        self.assertFalse(jan.no_data)
         self.assertEqual(dec.duration, td())
         self.assertEqual(dec.count, 0)
 
         # Nov. 2019
         self.assertEqual(nov.boundary.strftime("%m-%Y"), "11-2019")
+        self.assertEqual(jan.tz, "UTC")
+        self.assertFalse(jan.no_data)
         self.assertEqual(nov.duration, td())
         self.assertEqual(nov.count, 0)
 
@@ -272,16 +278,19 @@ class CheckModelTestCase(BaseTestCase):
         jan, dec, nov = r
 
         self.assertEqual(jan.boundary.isoformat(), "2020-01-01T00:00:00+00:00")
+        self.assertFalse(jan.no_data)
         self.assertEqual(jan.duration, td(days=14))
         self.assertEqual(jan.monthly_uptime(), (31 - 14) / 31)
         self.assertEqual(jan.count, 1)
 
         self.assertEqual(dec.boundary.isoformat(), "2019-12-01T00:00:00+00:00")
+        self.assertFalse(dec.no_data)
         self.assertEqual(dec.duration, td(days=31))
         self.assertEqual(dec.monthly_uptime(), 0.0)
         self.assertEqual(dec.count, 1)
 
         self.assertEqual(nov.boundary.isoformat(), "2019-11-01T00:00:00+00:00")
+        self.assertFalse(nov.no_data)
         self.assertEqual(nov.duration, td(days=16))
         self.assertEqual(nov.monthly_uptime(), 14 / 30)
         self.assertEqual(nov.count, 1)
@@ -304,6 +313,8 @@ class CheckModelTestCase(BaseTestCase):
         jan, dec = r
 
         self.assertEqual(jan.boundary.isoformat(), "2020-01-01T00:00:00+02:00")
+        self.assertEqual(jan.tz, "Europe/Riga")
+        self.assertFalse(jan.no_data)
         self.assertEqual(jan.duration, td(days=14, hours=1))
         total_hours = 31 * 24
         up_hours = total_hours - 14 * 24 - 1
@@ -311,6 +322,8 @@ class CheckModelTestCase(BaseTestCase):
         self.assertEqual(jan.count, 1)
 
         self.assertEqual(dec.boundary.isoformat(), "2019-12-01T00:00:00+02:00")
+        self.assertEqual(dec.tz, "Europe/Riga")
+        self.assertFalse(dec.no_data)
         self.assertEqual(dec.duration, td())
         self.assertEqual(dec.count, 0)
 
@@ -324,13 +337,13 @@ class CheckModelTestCase(BaseTestCase):
         jan, dec, nov = check.downtimes(3, "UTC")
 
         # Jan. 2020
-        self.assertEqual(jan.count, 0)
+        self.assertFalse(jan.no_data)
 
         # Dec. 2019
-        self.assertIsNone(dec.count)
+        self.assertTrue(dec.no_data)
 
         # Nov. 2019
-        self.assertIsNone(nov.count)
+        self.assertTrue(nov.no_data)
 
     @override_settings(S3_BUCKET=None)
     def test_it_prunes(self) -> None:
diff --git a/templates/emails/report-summary-html.html b/templates/emails/report-summary-html.html
index 52b4561d..e8ce6a56 100644
--- a/templates/emails/report-summary-html.html
+++ b/templates/emails/report-summary-html.html
@@ -73,7 +73,7 @@
             </td>
         {% else %}
             <td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif; color: #9BA2AB;">
-                {% if d.count is None %}
+                {% if d.no_data %}
                     {% comment %} The check didn't exist yet {% endcomment %}
                 {% else %}
                     All good!
diff --git a/templates/front/details_downtimes.html b/templates/front/details_downtimes.html
index 8aaa8ca1..48d353d4 100644
--- a/templates/front/details_downtimes.html
+++ b/templates/front/details_downtimes.html
@@ -5,11 +5,11 @@
     <tr>
         <th>{{ d.boundary|date:"N Y"}}</th>
         <td>
-            {% if d.count %}
+            {% if d.no_data %}
+                –
+            {% elif d.count %}
                 {{ d.count }} downtime{{ d.count|pluralize }}, {{ d.duration|hc_approx_duration }} total
                 <span class="uptime">{{ d.monthly_uptime|pct }}% uptime</span>
-            {% elif d.count is None %}
-                –
             {% else %}
                 All good!
             {% endif %}