allow naive UNTILs to be specified for schedule rrules

This commit is contained in:
Ryan Petrello 2018-05-02 16:24:55 -04:00 committed by Jared Tabor
parent fbe2391b86
commit 441e5cc9c2
2 changed files with 61 additions and 21 deletions

View File

@ -1,8 +1,10 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
import logging
import datetime
import logging
import re
import dateutil.rrule
from operator import itemgetter
import dateutil.parser
@ -11,7 +13,7 @@ from dateutil.tz import datetime_exists, tzutc
# Django
from django.db import models
from django.db.models.query import QuerySet
from django.utils.timezone import now
from django.utils.timezone import now, make_aware
from django.utils.translation import ugettext_lazy as _
# AWX
@ -129,6 +131,56 @@ class Schedule(CommonModel, LaunchTimeConfig):
Apply our own custom rrule parsing requirements
"""
kwargs['forceset'] = True
#
# RFC5545 specifies that the UNTIL rule part MUST ALWAYS be a date
# with UTC time. This is extra work for API implementers because
# it requires them to perform DTSTART local -> UTC datetime coercion on
# POST and UTC -> DTSTART local coercion on GET.
#
# This block of code is a departure from the RFC. If you send an
# rrule like this to the API (without a Z on the UNTIL):
#
# DTSTART;TZID=America/New_York:20180502T150000 RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL=20180502T180000
#
# ...we'll assume that the naive UNTIL is intended to match the DTSTART
# timezone (America/New_York), and so we'll coerce to UTC _for you_
# automatically.
#
if 'until=' in rrule.lower():
# if DTSTART;TZID= is used, coerce "naive" UNTIL values
# to the proper UTC date
match_until = re.match(".*?UNTIL\=(?P<until>[0-9]+T[0-9]+)(?P<utcflag>Z?)", rrule)
if not len(match_until.group('utcflag')):
# rrule = DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
# Find the UNTIL=N part of the string
# naive_until = 20200601T170000
naive_until = match_until.group('until')
# What is the DTSTART timezone for:
# DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000Z
# local_tz = tzfile('/usr/share/zoneinfo/America/New_York')
local_tz = dateutil.rrule.rrulestr(
rrule.replace(naive_until, naive_until + 'Z'),
tzinfos={x: tzutc() for x in dateutil.parser.parserinfo().UTCZONE}
)._dtstart.tzinfo
# Make a datetime object with tzinfo=<the DTSTART timezone>
# localized_until = datetime.datetime(2020, 6, 1, 17, 0, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
localized_until = make_aware(
datetime.datetime.strptime(naive_until, "%Y%m%dT%H%M%S"),
local_tz
)
# Coerce the datetime to UTC and format it as a string w/ Zulu format
# utc_until = 20200601T220000Z
utc_until = localized_until.astimezone(pytz.utc).strftime('%Y%m%dT%H%M%SZ')
# rrule was: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
# rrule is now: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T220000Z
rrule = rrule.replace(naive_until, utc_until)
x = dateutil.rrule.rrulestr(rrule, **kwargs)
for r in x._rrule:

View File

@ -1,5 +1,6 @@
from datetime import datetime
from django.utils.timezone import now
import mock
import pytest
import pytz
@ -131,31 +132,19 @@ def test_utc_until(job_template, until, dtend):
@pytest.mark.django_db
@pytest.mark.parametrize('dtstart, until', [
['20180601T120000Z', '20180602T170000'],
['TZID=America/New_York:20180601T120000', '20180602T170000'],
['DTSTART:20380601T120000Z', '20380601T170000'], # noon UTC to 5PM UTC
['DTSTART;TZID=America/New_York:20380601T120000', '20380601T170000'], # noon EST to 5PM EST
])
def test_tzinfo_naive_until(job_template, dtstart, until):
rrule = 'DTSTART;{} RRULE:FREQ=DAILY;INTERVAL=1;UNTIL={}'.format(dtstart, until) # noqa
rrule = '{} RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL={}'.format(dtstart, until) # noqa
s = Schedule(
name='Some Schedule',
rrule=rrule,
unified_job_template=job_template
)
with pytest.raises(ValueError):
s.save()
@pytest.mark.django_db
def test_until_must_be_utc(job_template):
rrule = 'DTSTART;TZID=America/New_York:20180601T120000 RRULE:FREQ=DAILY;INTERVAL=1;UNTIL=20180602T000000' # noqa the Z is required
s = Schedule(
name='Some Schedule',
rrule=rrule,
unified_job_template=job_template
)
with pytest.raises(ValueError) as e:
s.save()
assert 'RRULE UNTIL values must be specified in UTC' in str(e)
s.save()
gen = Schedule.rrulestr(s.rrule).xafter(now(), count=20)
assert len(list(gen)) == 6 # noon, 1PM, 2, 3, 4, 5PM
@pytest.mark.django_db
@ -211,7 +200,6 @@ def test_beginning_of_time(job_template):
['DTSTART;TZID=America/New_York:20300112T210000 RRULE:FREQ=DAILY;INTERVAL=1', 'America/New_York']
])
def test_timezone_property(job_template, rrule, tz):
# ensure that really large generators don't have performance issues
s = Schedule(
name='Some Schedule',
rrule=rrule,