def initialize(self, tz, start, end, repeat = None, until = None, description = None): """ Initializes a new or current blackout object to the given values. tz : string; time zone for the blackout start : datetime; start date/time, *in UTC* end : datetime; end date/time, *in UTC* repeat : Repeat object; repeat interval. until : datetime; end date for repeat, if used description : string; A brief description of the blackout. """ self.save() # the Blackout entry in the database needs an ID # so that blackout sequences can be added. self.description = description self.clear_sequences() self.timeZone = tz # Simple case: 'Once' if not repeat or repeat.repeat == 'Once': if repeat == None: repeat = Repeat.objects.get(repeat = 'Once') # NOTE: Strip out TZ info, just in case the database has # not been altered to use timestamp without time zone. # The tzinfo is UTC, as these times are provided as UTC # representations of the user's desired local time, and # that is what we want. However, if the tzinfo is not set # to None and the database has not been altered then these # get set to the local time by the time it is saved to a # sequence start = start.replace(tzinfo = None) end = end.replace(tzinfo = None) bs = Blackout_Sequence(start_date = start, end_date = end, repeat = repeat, until = until) self.blackout_sequence_set.add(bs) else: # We're looking at a series of blackouts that may continue # past a DST boundary. In addition, one of the blackouts # may itself straddle the DST bound. The blackout # sequences must be represented in the DB as UTC values, # since Antioch doesn't have the timezone libraries Python # has. But since blackouts are a user concept, they must # be continuous in the local time zone (i.e. 8:00 AM local # time, before or after DST starts). Thus the UTC values # will change as the series crosses DST bounds. In # addition we are looking at the possibility that one of # the blackout instances itself will cross the DST # bound. Through all this we wish to maintain the proper # phase and spacing of the repeating blackouts. This # diagram represents this idea: # # DST DST # |---|---------------|-|=|=======================|===|=|----------------| # S E ... S E ... S E ... U # # Sequence 1 # |---|---------------| # S E U # Seq 2 # |---| # S E # Rep. # |-------| Sequence 3 # |---|-------------------| # S E ... U # Rep. # |-------| Sequence 4 # |---|----------| # S E ... U # # Naive algorithm: # # 1) Get DST bounds over desired time range # 2) Map blackout instances (SE) over entire range # 3) Iterate over SEs and check: # a) Has S crossed over a DST? # i) If so, previous E becomes U of last sequence; save sequence, # start another working sequence, and go to next iteration. # b) Has E crossed over a DST? # i) If so, previous E can become U of last sequence, as above, but: # Create and save a 'Once' sequence with current S and E; next SE # becomes working sequence, and go to next iteration # c) if we got this far, until of current SE becomes until of working # sequence dstb = dst_boundaries(tz, start, until) dst_date = None if len(dstb): dstb.reverse() dst_date = tz_to_tz(dstb.pop(), 'UTC', tz, naive = True) localstart = tz_to_tz(start, 'UTC', tz, naive = True) localend = tz_to_tz(end, 'UTC', tz, naive = True) localuntil = tz_to_tz(until, 'UTC', tz, naive = True) days = truncateDt(localend) - truncateDt(localstart) periodicity = repeat.repeat bls = [] while localstart <= localuntil: bls.append([localstart, localend, periodicity, localuntil]) localstart, localend = self.get_next_period(localstart, localend, periodicity) seq = bls[0] seqs = [] for i in range(0, len(bls)): if dst_date: # if we are contending with a DST boundary... # keep DST boundary date current if seq[0] > dst_date: if len(dstb): dst_date = tz_to_tz(dstb.pop(), 'UTC', tz, naive = True) else: dst_date = None continue if bls[i][0] > dst_date: # start crosses dst_date seq[3] = bls[i - 1][1] # set until to previous instance's end seqs.append(seq) # save seq = bls[i] continue if bls[i][1] > dst_date: # start hasn't, but end has crossed seq[3] = bls[i - 1][1] # dst_date; set until to previous end seqs.append(seq) seq = bls[i] seq[2] = 'Once' seqs.append(seq) if i < len(bls): seq = bls[i + 1] continue seq[3] = bls[i][3] seqs.append(seq) # get the last sequence for i in seqs: bs = Blackout_Sequence(start_date = tz_to_tz(i[0], tz, 'UTC', True), end_date = tz_to_tz(i[1], tz, 'UTC', True), repeat = Repeat.objects.get(repeat = i[2]), until = tz_to_tz(i[3], tz, 'UTC', True)) self.blackout_sequence_set.add(bs) self.save()
def test_pt_dst(self): # dates are given as UTC dates even though the timezone is # given as a local timezone. This is the way the blackout # view works. :/ localstart = datetime(2011, 1, 1, 11) localend = datetime(2011, 1, 4, 13) localuntil = datetime(2011, 12, 4, 11) utcstart = tz_to_tz(localstart, 'US/Pacific', 'UTC', naive = True) utcend = tz_to_tz(localend, 'US/Pacific', 'UTC', True) utcuntil = tz_to_tz(localuntil, 'US/Pacific', 'UTC', True) spring, fall = dst_boundaries('US/Pacific', utcstart, utcuntil) my_bo = create_blackout(user = self.u, repeat = 'Weekly', start = utcstart, end = utcend, until = utcuntil, timezone = 'US/Pacific') # generate 'UTC' sequence of blackout dates for standard time # until spring transition. dates = my_bo.generateDates(utcstart, spring, local_timezone = False) self.assertNotEquals(len(dates), 0) # All the dates except the last one are in standard time. for i in range(0, len(dates) - 1): self.assertEquals(dates[i][0].time(), utcstart.time()) self.assertEquals(dates[i][1].time(), utcend.time()) # the last one straddles DST, so end should be an hour earlier in UTC. self.assertEquals(dates[-1][0].time(), utcstart.time()) self.assertEquals(dates[-1][1].time(), (utcend - timedelta(hours = 1)).time()) # generate 'UTC' sequence of blackout dates for spring DST # transition until fall transition. This sequence will # include 2 transition blackouts over both DST transitions: one_hour = timedelta(hours = 1) dates = my_bo.generateDates(spring, fall, local_timezone = False) self.assertNotEquals(len(dates), 0) self.assertEquals(dates[0][0].time(), utcstart.time()) self.assertEquals(dates[0][1].time(), (utcend - one_hour).time()) for i in range(1, len(dates) - 1): self.assertEquals(dates[i][0].time(), (utcstart - one_hour).time()) self.assertEquals(dates[i][1].time(), (utcend - one_hour).time()) self.assertEquals(dates[-1][0].time(), (utcstart - one_hour).time()) self.assertEquals(dates[-1][1].time(), utcend.time()) # generate 'UTC' sequence of blackout dates from fall # transition until the 'until' time. Back to standard time. # The first blackout in the range will be a transition # blackout. dates = my_bo.generateDates(fall, utcuntil, local_timezone = False) self.assertNotEquals(len(dates), 0) self.assertEquals(dates[0][0].time(), (utcstart - one_hour).time()) self.assertEquals(dates[0][1].time(), utcend.time()) for i in range(1, len(dates)): self.assertEquals(dates[i][0].time(), utcstart.time()) self.assertEquals(dates[i][1].time(), utcend.time()) # generate local timezone sequence of blackout dates for the # entire range. Despite the complexity of the underlying UTC # representation, the local times should all be the same. dates = my_bo.generateDates(utcstart, utcuntil, local_timezone = True) self.assertNotEquals(len(dates), 0) for i in dates: self.assertEquals(i[0].time(), localstart.time()) self.assertEquals(i[1].time(), localend.time())