Browse Source

Use timezone-aware datetimes with croniter, avoid conversions to and from naive datetimes. This avoids ambiguities around DST transitions and properly solves #196

pull/211/head
Pēteris Caune 6 years ago
parent
commit
cf08f54c30
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
5 changed files with 27 additions and 22 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +10
    -6
      hc/api/models.py
  3. +8
    -10
      hc/front/views.py
  4. +2
    -2
      requirements.txt
  5. +6
    -4
      templates/front/cron_preview.html

+ 1
- 0
CHANGELOG.md View File

@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file.
### Bug Fixes ### Bug Fixes
- Fix after-login redirects (the "?next=" query parameter) - Fix after-login redirects (the "?next=" query parameter)
- Update Check.status field when user edits timeout & grace settings - Update Check.status field when user edits timeout & grace settings
- Use timezone-aware datetimes with croniter, avoid ambiguities around DST
## 1.3.0 - 2018-11-21 ## 1.3.0 - 2018-11-21


+ 10
- 6
hc/api/models.py View File

@ -15,6 +15,7 @@ from django.utils import timezone
from hc.api import transports from hc.api import transports
from hc.lib import emails from hc.lib import emails
import requests import requests
import pytz
STATUSES = ( STATUSES = (
("up", "Up"), ("up", "Up"),
@ -115,12 +116,15 @@ class Check(models.Model):
if self.kind == "simple": if self.kind == "simple":
return self.last_ping + self.timeout return self.last_ping + self.timeout
# The complex case, next ping is expected based on cron schedule
with timezone.override(self.tz):
last_naive = timezone.make_naive(self.last_ping)
it = croniter(self.schedule, last_naive)
next_naive = it.get_next(datetime)
return timezone.make_aware(next_naive, is_dst=True)
# 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)
def get_status(self, now=None): def get_status(self, now=None):
""" Return "up" if the check is up or in grace, otherwise "down". """ """ Return "up" if the check is up or in grace, otherwise "down". """


+ 8
- 10
hc/front/views.py View File

@ -28,7 +28,7 @@ from hc.front.schemas import telegram_callback
from hc.front.templatetags.hc_extras import (num_down_title, down_title, from hc.front.templatetags.hc_extras import (num_down_title, down_title,
sortchecks) sortchecks)
from hc.lib import jsonschema from hc.lib import jsonschema
from pytz import all_timezones
import pytz
from pytz.exceptions import UnknownTimeZoneError from pytz.exceptions import UnknownTimeZoneError
import requests import requests
@ -130,7 +130,7 @@ def my_checks(request):
"now": timezone.now(), "now": timezone.now(),
"tags": pairs, "tags": pairs,
"ping_endpoint": settings.PING_ENDPOINT, "ping_endpoint": settings.PING_ENDPOINT,
"timezones": all_timezones,
"timezones": pytz.all_timezones,
"num_available": request.team.check_limit - len(checks), "num_available": request.team.check_limit - len(checks),
"sort": request.profile.sort, "sort": request.profile.sort,
"selected_tags": selected_tags, "selected_tags": selected_tags,
@ -325,13 +325,11 @@ def cron_preview(request):
tz = request.POST.get("tz") tz = request.POST.get("tz")
ctx = {"tz": tz, "dates": []} ctx = {"tz": tz, "dates": []}
try: try:
with timezone.override(tz):
now_naive = timezone.make_naive(timezone.now())
it = croniter(schedule, now_naive)
for i in range(0, 6):
naive = it.get_next(datetime)
aware = timezone.make_aware(naive, is_dst=True)
ctx["dates"].append((naive, aware))
zone = pytz.timezone(tz)
now_local = timezone.localtime(timezone.now(), zone)
it = croniter(schedule, now_local)
for i in range(0, 6):
ctx["dates"].append(it.get_next(datetime))
except UnknownTimeZoneError: except UnknownTimeZoneError:
ctx["bad_tz"] = True ctx["bad_tz"] = True
except: except:
@ -420,7 +418,7 @@ def details(request, code):
"page": "details", "page": "details",
"check": check, "check": check,
"channels": channels, "channels": channels,
"timezones": all_timezones
"timezones": pytz.all_timezones
} }
return render(request, "front/details.html", ctx) return render(request, "front/details.html", ctx)


+ 2
- 2
requirements.txt View File

@ -1,5 +1,5 @@
croniter
Django==2.1.3
croniter==0.3.26
Django==2.1.4
django_compressor==2.2 django_compressor==2.2
psycopg2==2.7.5 psycopg2==2.7.5
pytz==2018.7 pytz==2018.7


+ 6
- 4
templates/front/cron_preview.html View File

@ -11,11 +11,13 @@
{% else %} {% else %}
<table id="cron-preview-table" class="table"> <table id="cron-preview-table" class="table">
<tr><th id="cron-preview-title" colspan="3">Expected Ping Dates</th></tr> <tr><th id="cron-preview-title" colspan="3">Expected Ping Dates</th></tr>
{% for naive, aware in dates %}
{% for d in dates %}
<tr> <tr>
<td>{{ naive|date:"M j, H:i" }}</td>
<td>{{ aware|naturaltime }}</td>
<td class="hidden-xs">{{ aware|date:"c" }}</td>
{% timezone tz %}
<td>{{ d|date:"M j, H:i" }}</td>
{% endtimezone %}
<td>{{ d|naturaltime }}</td>
<td class="hidden-xs">{{ d|date:"c" }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>


Loading…
Cancel
Save