def generateDates(self, calstart, calend, local_timezone = False): """ Takes two UTC datetimes representing a period of time on the calendar. Returns a list of (datetime, datetime) tuples representing all the events generated by this blackout in that period. calstart : the start datetime calend : the end datetime local_timezone : boolean, if True results are in the local timezone; if False, in UTC. """ # short-circuit if calstart-calend outside of range start = self.getStartDate() end = self.getEndDate() if self.getRepeat() == 'Once' else self.getUntil() if end < calstart or start > calend: return [] dates = [] for seq in self.blackout_sequence_set.order_by("start_date"): start = seq.start_date end = seq.end_date until = min(seq.until, calend) if seq.until else calend periodicity = seq.repeat.repeat # take care of simple scenarios first if start is None or end is None: continue # ignore this sequence if periodicity == "Once": # A 'Once' sequence may belong to a 'Once' blackout, # or may be a DST transition sequence in a repeat # blackout. The date test is for the latter, but will # work in either case. if not (end < calstart or start > calend): dates.append((start, end)) else: # Check to see if this *sequence* (not the entire # blackout) is outside the calstart-calend range. If # so, skip to next sequence. if until < calstart or start > calend: continue # Otherwise, get the dates that are within the range. while start <= until: if start >= calstart: dates.append((start, end)) start, end = self.get_next_period(start, end, periodicity) if local_timezone: return map(lambda x: (tz_to_tz(x[0], 'UTC', self.timeZone, True),\ tz_to_tz(x[1], 'UTC', self.timeZone, True)), dates) else: return dates
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 getEndDateTZ(self, tz = None): if not tz: tz = self.timeZone return tz_to_tz(self.getEndDate(), 'UTC', tz)
def getUntilTZ(self, tz = None): if not tz: tz = self.timeZone until = self.getUntil() return tz_to_tz(until, 'UTC', tz) if until is not None else until
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())