def data_postprocessing(obj, event): # newly created object, without start/end/timezone (e.g. invokeFactory() # called without data from add form), ignore event; it will be notified # again later: if getattr(obj, 'start', None) is None: return # We handle date inputs as floating dates without timezones and apply # timezones afterwards. start = tzdel(obj.start) end = tzdel(obj.end) # set the timezone tz = pytz.timezone(obj.timezone) start = tz.localize(start) end = tz.localize(end) # adapt for whole day if IEventBasic(obj).whole_day: start = start.replace(hour=0,minute=0,second=0) end = end.replace(hour=23,minute=59,second=59) # save back obj.start = utc(start) obj.end = utc(end) # reindex obj.reindexObject()
def add_to_zones_map(tzmap, tzid, dt): if tzid.lower() == 'utc' or not is_datetime(dt): # no need to define UTC nor timezones for date objects. return tzmap null = datetime(1, 1, 1) tz = pytz.timezone(tzid) transitions = getattr(tz, '_utc_transition_times', None) if not transitions: return tzmap # we need transition definitions dtzl = tzdel(utc(dt)) # get transition time, which is the dtstart of timezone. # the key function returns the value to compare with. as long as item # is smaller or equal like the dt value in UTC, return the item. as # soon as it becomes greater, compare with the smallest possible # datetime, which wouldn't create a match within the max-function. this # way we get the maximum transition time which is smaller than the # given datetime. transition = max(transitions, key=lambda item: item <= dtzl and item or null) # get previous transition to calculate tzoffsetfrom idx = transitions.index(transition) prev_idx = idx > 0 and idx - 1 or idx prev_transition = transitions[prev_idx] def localize(tz, dt): if dt is null: # dummy time, edge case # (dt at beginning of all transitions, see above.) return null return pytz.utc.localize(dt).astimezone(tz) # naive to utc + localize transition = localize(tz, transition) dtstart = tzdel(transition) # timezone dtstart must be in local time prev_transition = localize(tz, prev_transition) if tzid not in tzmap: tzmap[tzid] = {} # initial if dtstart in tzmap[tzid]: return tzmap # already there tzmap[tzid][dtstart] = { 'dst': transition.dst() > timedelta(0), 'name': transition.tzname(), 'tzoffsetfrom': prev_transition.utcoffset(), 'tzoffsetto': transition.utcoffset(), # TODO: recurrence rule } return tzmap
def _prepare_dt_set(self, dt): # Always set the date in UTC, saving the timezone in another field. # But since the timezone value isn't known at the time of saving the # form, we have to save it timezone-naive first and let # timezone_handler convert it to the target zone afterwards. # TODO: end and start should be updated if the timezone changes. ? return tzdel(dt)
def test_occurrence_accessor(self): start = datetime.datetime.today() end = datetime.datetime.today() occ = Occurrence('ignored', start, end) occ = occ.__of__(self.portal['at']) data = IEventAccessor(occ) self.assertNotEqual(data.start, tzdel(self.portal['at'].start_date)) self.assertEqual(start, data.start)
def data_postprocessing(obj, event): # We handle date inputs as floating dates without timezones and apply # timezones afterwards. start = tzdel(obj.start) end = tzdel(obj.end) # set the timezone tz = pytz.timezone(obj.timezone) start = tz.localize(start) end = tz.localize(end) # adapt for whole Day if obj.whole_day: start = start.replace(hour=0,minute=0,second=0) end = end.replace(hour=23,minute=59,second=59) # save back obj.start = utc(start) obj.end = utc(end) # reindex obj.reindexObject()
def _fix_zone(dt, zone): if dt.tzinfo is not None and isinstance(dt.tzinfo, FakeZone): # Delete the tzinfo only, if it was set by IEventBasic setter. # Only in this case the start value on the object itself is what # was entered by the user. After running this event subscriber, # it's in UTC then. # If tzinfo it's not available at all, a naive datetime was set # probably by invokeFactory in tests. dt = tzdel(dt) if dt.tzinfo is None: # In case the tzinfo was deleted above or was not present, we can # localize the dt value to the target timezone. dt = tz.localize(dt) else: # In this case, no changes to start, end or the timezone were made. # Just return the object's datetime (which is in UTC) localized to # the target timezone. dt = dt.astimezone(tz) return dt.replace(microsecond=0)
def recurrence_sequence_ical(start, recrule=None, from_=None, until=None, count=None): """ Calculates a sequence of datetime objects from a recurrence rule following the RFC2445 specification, using python-dateutil recurrence rules. @param start: datetime or DateTime instance of the date from which the recurrence sequence is calculated. @param recrule: String with RFC2445 compatible recurrence definition, dateutil.rrule or dateutil.rruleset instances. @param from_: datetime or DateTime instance of the date, to limit - possibly with until - the result within a timespan - The Date Horizon. @param until: datetime or DateTime instance of the date, until the recurrence is calculated. If not given, count or MAXDATE limit the recurrence calculation. @param count: Integer which defines the number of occurences. If not given, until or MAXDATE limits the recurrence calculation. @return: A generator which generates a sequence of datetime instances. """ start = pydt(start) # always use python datetime objects from_ = pydt(from_) until = pydt(until) tz = start.tzinfo start = tzdel(start) # tznaive | start defines tz _from = tzdel(from_) _until = tzdel(until) if isinstance(recrule, str): # RFC2445 string # forceset: always return a rruleset # dtstart: optional used when no dtstart is in RFC2445 string # dtstart is given as timezone naive time. timezones are # applied afterwards, since rrulestr doesn't normalize # timezones over DST boundaries rset = rrule.rrulestr(recrule, dtstart=start, forceset=True, # ignoretz=True # compatible=True # RFC2445 compatibility ) else: rset = rrule.rruleset() rset.rdate(start) # RCF2445: always include start date # limit if _from and _until: # between doesn't add a ruleset but returns a list rset = rset.between(_from, _until, inc=True) for cnt, date in enumerate(rset): # Localize tznaive dates from rrulestr sequence date = tz.localize(date) # Limit number of recurrences otherwise calculations take too long if MAXCOUNT and cnt+1 > MAXCOUNT: break if count and cnt+1 > count: break if from_ and utc(date) < utc(from_): continue if until and utc(date) > utc(until): break yield date return
def recurrence_sequence_ical( start, recrule=None, from_=None, until=None, count=None, duration=None, ): """Calculates a sequence of datetime objects from a recurrence rule following the RFC2445 specification, using python-dateutil recurrence rules. The resolution of the resulting datetime objects is one second, since python-dateutil rrulestr doesn't support microseconds. :param start: datetime or DateTime instance of the date from which the recurrence sequence is calculated. :type start: datetime.datetime :param recrule: Optional string with RFC2445 compatible recurrence definition, dateutil.rrule or dateutil.rruleset instances. :type recrule: string :param from_: Optional datetime or DateTime instance of the date, to limit the result within a timespan. :type from_: datetime.datetime :param until: Optional datetime or DateTime instance of the date, until the recurrence is calculated. If not given, count or MAXDATE limit the recurrence calculation. :type until: datetime.datetime :param count: Optional integer which defines the number of occurences. If not given, until or MAXDATE limits the recurrence calculation. :type count: integer :param duration: Optional timedelta instance, which is used to calculate if a occurence datetime plus duration is within the queried timerange. :type duration: datetime.timedelta :returns: A generator which generates a sequence of datetime instances. :rtype: generator """ # Always use python datetime objects and remove the microseconds start = pydt(start, exact=False) from_ = pydt(from_, exact=False) until = pydt(until, exact=False) tz = start.tzinfo start = tzdel(start) # tznaive | start defines tz _from = tzdel(from_) _until = tzdel(until) if duration: assert (isinstance(duration, datetime.timedelta)) else: duration = datetime.timedelta(0) if recrule: # TODO BUGFIX WRONG TIME DEFINITIONS # THIS HACK ensures, that UNTIL, RDATE and EXDATE definitions with # incorrect time (currently always set to 0:00 by the recurrence # widget) are handled correctly. # # Following fixes are made: # - The UNTIL date should be included in the recurrence set, as defined # by RFC5545 (fix sets it to the end of the day) # - RDATE definitions should have the same time as the start date. # - EXDATE definitions should exclude occurrences on the specific date # only the same time as the start date. # In the long term ,the recurrence widget should be able to set the # time for UNTIL, RDATE and EXDATE. t0 = start.time() # set initial time information. # First, replace all times in the recurring rule with starttime t0str = 'T{0:02d}{1:02d}{2:02d}'.format(t0.hour, t0.minute, t0.second) # Replace any times set to 000000 with start time, not all # rrules are set by a specific broken widget. Don't waste time # subbing if the start time is already 000000. if t0str != 'T000000': recrule = re.sub(r'T000000', t0str, recrule) # Then, replace incorrect until times with the end of the day recrule = re.sub( r'(UNTIL[^T]*[0-9]{8})T(000000)', r'\1T235959', recrule, ) # RFC2445 string # forceset: always return a rruleset # dtstart: optional used when no dtstart is in RFC2445 string # dtstart is given as timezone naive time. timezones are # applied afterwards, since rrulestr doesn't normalize # timezones over DST boundaries rset = rrule.rrulestr( recrule, dtstart=start, forceset=True, ignoretz=True, # compatible=True # RFC2445 compatibility ) else: rset = rrule.rruleset() rset.rdate(start) # RCF2445: always include start date # limit if _from and _until: # between doesn't add a ruleset but returns a list rset = rset.between(_from - duration, _until, inc=True) for cnt, date in enumerate(rset): # Localize tznaive dates from rrulestr sequence date = tz.localize(date) # Limit number of recurrences otherwise calculations take too long if MAXCOUNT and cnt + 1 > MAXCOUNT: break if count and cnt + 1 > count: break if from_ and utc(date) + duration < utc(from_): continue if until and utc(date) > utc(until): break yield date return
def add_to_zones_map(tzmap, tzid, dt): """Build a dictionary of timezone information from a timezone identifier and a date/time object for which the timezone information should be calculated. :param tzmap: An existing dictionary of timezone information to be extended or an empty dictionary. :type tzmap: dictionary :param tzid: A timezone identifier. :type tzid: string :param dt: A datetime object. :type dt: datetime :returns: A dictionary with timezone information needed to build VTIMEZONE entries. :rtype: dictionary """ if tzid.lower() == 'utc' or not is_datetime(dt): # no need to define UTC nor timezones for date objects. return tzmap null = datetime(1, 1, 1) tz = pytz.timezone(tzid) transitions = getattr(tz, '_utc_transition_times', None) if not transitions: return tzmap # we need transition definitions dtzl = tzdel(utc(dt)) # get transition time, which is the dtstart of timezone. # the key function returns the value to compare with. as long as item # is smaller or equal like the dt value in UTC, return the item. as # soon as it becomes greater, compare with the smallest possible # datetime, which wouldn't create a match within the max-function. this # way we get the maximum transition time which is smaller than the # given datetime. transition = max(transitions, key=lambda item: item if item <= dtzl else null) # get previous transition to calculate tzoffsetfrom idx = transitions.index(transition) prev_idx = idx - 1 if idx > 0 else idx prev_transition = transitions[prev_idx] def localize(tz, dt): if dt is null: # dummy time, edge case # (dt at beginning of all transitions, see above.) return null return pytz.utc.localize(dt).astimezone(tz) # naive to utc + localize transition = localize(tz, transition) dtstart = tzdel(transition) # timezone dtstart must be in local time prev_transition = localize(tz, prev_transition) if tzid not in tzmap: tzmap[tzid] = {} # initial if dtstart in tzmap[tzid]: return tzmap # already there tzmap[tzid][dtstart] = { 'dst': transition.dst() > timedelta(0), 'name': transition.tzname(), 'tzoffsetfrom': prev_transition.utcoffset(), 'tzoffsetto': transition.utcoffset(), # TODO: recurrence rule } return tzmap