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 computed_state(self): task = self.task state = task.state if state != 'completed' and task.due and \ utc(datetime.now()) > utc(task.due): return 'overdue' return dict(id=state, title=TASK_STATES[state])
def recurrence_sequence_timedelta(start, delta=None, until=None, count=None, dst=DSTAUTO): """ Calculates a sequence of datetime objects from a timedelta integer, which defines the minutes between each occurence. :param start: datetime or DateTime instance of the date from which the recurrence sequence is calculated. :type start: datetime :param delta: Integer which defines the minutes between each date occurence. :type delta: integer :param until: 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 :param count: Integer which defines the number of occurences. If not given, until or MAXDATE limits the recurrence calculation. :param count: integer :param dst: Daylight Saving Time crossing behavior. DSTAUTO, DSTADJUST or DSTKEEP. For more information, see plone.event.utils.utcoffset_normalize. :param dst: string :return: A generator which generates a sequence of datetime instances. :rtype: generator """ start = pydt(start) yield start if delta is None or delta < 1 or until is None: return until = pydt(until) before = start delta = datetime.timedelta(minutes=delta) cnt = 0 while True: after = before + delta after = utcoffset_normalize(after, delta, dst) # Limit number of recurrences otherwise calculations take too long if MAXCOUNT and cnt + 1 > MAXCOUNT: break if count and cnt + 1 > count: break if until and utc(after) > utc(until): break cnt += 1 yield after before = after
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. 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) behavior = IEventBasic(obj) tz = pytz.timezone(behavior.timezone) # Fix zones start = _fix_zone(obj.start, tz) end = _fix_zone(obj.end, tz) # Adapt for whole day if behavior.whole_day: start = dt_start_of_day(start) if behavior.open_end: end = start # Open end events end on same day if behavior.open_end or behavior.whole_day: end = dt_end_of_day(end) # Save back obj.start = utc(start) obj.end = utc(end) # Reindex obj.reindexObject()
def event_component(context): """ Returns an icalendar object of the event. """ ical_event = icalendar.Event() # TODO: until VTIMETZONE component is added and TZID used, everything is # converted to UTC. use real TZID, when VTIMEZONE is used! ical_event.add('dtstamp', utc(pydt(datetime.now()))) ical_event.add('created', utc(pydt(context.creation_date))) ical_event.add('uid', context.UID()) ical_event.add('last-modified', utc(pydt(context.modification_date))) ical_event.add('summary', context.Title()) ical_event.add('dtstart', utc(pydt(context.start()))) ical_event.add('dtend', utc(pydt(context.end()))) recurrence = context.recurrence if recurrence: if recurrence.startswith('RRULE:'): recurrence = recurrence[6:] ical_event.add('rrule', icalendar.prop.vRecur.from_ical(recurrence)) description = context.Description() if description: ical_event.add('description', description) location = context.getLocation() if location: ical_event.add('location', location) subjects= context.Subject() for subject in subjects: ical_event.add('categories', subject) # TODO: revisit and implement attendee export according to RFC attendees = context.getAttendees() for attendee in attendees: ical_event.add('attendee', attendee) cn = [] contact = context.contact_name() if contact: cn.append(contact) # TODO safe_unicode conversion needed? phone = context.contact_phone() if phone: cn.append(phone) email = context.contact_email() if email: cn.append(email) if cn: ical_event.add('contact', u', '.join(cn)) url = context.event_url() if url: ical_event.add('url', url) return ical_event
def test_event_accessor__start_end(self): e1 = createContentInContainer( self.portal, 'plone.app.event.dx.event', title=u'event1' ) dt = datetime(2161, 1, 1) # United Federation of Planets DT = DateTime('2161/01/01 00:00:00 UTC') acc = IEventAccessor(e1) # Setting a timezone-naive datetime should convert it to UTC acc.start = dt self.assertEqual(acc.start, utils.utc(dt)) self.assertEqual(e1.start, utils.utc(dt)) # Setting a DateTime should convert it to datetime acc.start = DT self.assertEqual(acc.start, utils.utc(dt)) self.assertEqual(e1.start, utils.utc(dt)) # Same goes for acc.end # Setting a timezone-naive datetime should convert it to UTC acc.end = dt self.assertEqual(acc.end, utils.utc(dt)) self.assertEqual(e1.end, utils.utc(dt)) # Setting a DateTime should convert it to datetime acc.end = DT self.assertEqual(acc.end, utils.utc(dt)) self.assertEqual(e1.end, utils.utc(dt))
def test_event_accessor__start_end(self): e1 = createContentInContainer( self.portal, 'plone.app.event.dx.event', title='event1' ) dt = datetime(2161, 1, 1) # United Federation of Planets DT = DateTime('2161/01/01 00:00:00 UTC') acc = IEventAccessor(e1) # Setting a timezone-naive datetime should convert it to UTC acc.start = dt self.assertEqual(acc.start, utils.utc(dt)) self.assertEqual(e1.start, utils.utc(dt)) # Setting a DateTime should convert it to datetime acc.start = DT self.assertEqual(acc.start, utils.utc(dt)) self.assertEqual(e1.start, utils.utc(dt)) # Same goes for acc.end # Setting a timezone-naive datetime should convert it to UTC acc.end = dt self.assertEqual(acc.end, utils.utc(dt)) self.assertEqual(e1.end, utils.utc(dt)) # Setting a DateTime should convert it to datetime acc.end = DT self.assertEqual(acc.end, utils.utc(dt)) self.assertEqual(e1.end, utils.utc(dt))
def migrate_event(source, site): parent_folder = aq_parent(source) evid = source.getId() newid = '%s-new' % evid if newid not in parent_folder.objectIds(): timezone = sitetz(site) parent_folder.invokeFactory( id=newid, type_name='plone.app.event.dx.event', title=source.Title(), start=utc(source.start().asdatetime()), end=utc(source.start().asdatetime()), timezone=timezone, whole_day=False, ) dest = parent_folder.get(newid) # behaviors: basic = behaviors.IEventBasic(dest) recur = behaviors.IEventRecurrence(dest) loc = behaviors.IEventLocation(dest) contact = behaviors.IEventContact(dest) summary = behaviors.IEventSummary(dest) # default values for fields we will not copy: basic.timezone = timezone basic.wholeday = False recur.recurrence = None # default, no recurrence specified # start, end dates: basic.start = utc(source.start().asdatetime()) basic.end = utc(source.start().asdatetime()) # contact name, phone, email, event_url contact.contact_name = source.contact_name() contact.contact_email = source.contact_email() contact.contact_phone = source.contact_phone() contact.event_url = source.event_url() # location loc.location = source.getLocation() # modification date and Subject/tags metadata dest.modification_date = source.modification_date dest.setSubject(source.Subject()) # Deal with body text. text = source.getText() if text: summary.text = RichTextValue(raw=text.decode('utf-8')) dest.reindexObject() try: parent_folder.manage_delObjects([evid]) except LinkIntegrityNotificationException: print 'skipped deleting %s for link integrity' % repr(source)
def formatted_date(self, item): DT_item = DT(item) return dict( date=ulocalized_time( DT_item, long_format=False, time_only=None, context=self.context), time=ulocalized_time( DT_item, long_format=False, time_only=True, context=self.context), iso=item.isoformat(), extra=utc(datetime.now()) > utc(item) and ' (past-due)' or '', )
def data_postprocessing(obj, event): # set the timezone tz = pytz.timezone(obj.timezone) start = tz.localize(obj.start) end = obj.end.replace(tzinfo=tz) # 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 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 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 created(self): # must be in uc return {'value': utc(self.event.created)}
def to_ical(self): ical = icalendar.Event() event = self.event # TODO: event.text # must be in utc ical.add('dtstamp', utc(pydt(datetime.now()))) ical.add('created', utc(pydt(event.created))) ical.add('last-modified', utc(pydt(event.last_modified))) ical.add('uid', event.uid) ical.add('url', event.url) ical.add('summary', event.title) if event.description: ical.add('description', event.description) if event.whole_day: ical.add('dtstart', event.start.date()) # RFC5545, 3.6.1 # For cases where a "VEVENT" calendar component # specifies a "DTSTART" property with a DATE value type but no # "DTEND" nor "DURATION" property, the event's duration is taken to # be one day. # # RFC5545 doesn't define clearly, if all-day events should have # a end date on the same date or one day after the start day at # 0:00. Most icalendar libraries use the latter method. # Internally, whole_day events end on the same day one second # before midnight. Using the RFC5545 preferred method for # plone.app.event seems not appropriate, since we would have to fix # the date to end a day before for displaying. # For exporting, we let whole_day events end on the next day at # midnight. # See: # http://stackoverflow.com/questions/1716237/single-day-all-day # -appointments-in-ics-files # http://icalevents.com/1778-all-day-events-adding-a-day-or-not/ # http://www.innerjoin.org/iCalendar/all-day-events.html ical.add('dtend', event.end.date() + timedelta(days=1)) elif event.open_end: # RFC5545, 3.6.1 # For cases where a "VEVENT" calendar component # specifies a "DTSTART" property with a DATE-TIME value type but no # "DTEND" property, the event ends on the same calendar date and # time of day specified by the "DTSTART" property. ical.add('dtstart', event.start) else: ical.add('dtstart', event.start) ical.add('dtend', event.end) if event.recurrence: for recdef in event.recurrence.split(): prop, val = recdef.split(':') if prop == 'RRULE': ical.add(prop, icalendar.prop.vRecur.from_ical(val)) elif prop in ('EXDATE', 'RDATE'): factory = icalendar.prop.vDDDLists # localize ex/rdate # TODO: should better already be localized by event object tzid = event.timezone # get list of datetime values from ical string try: dtlist = factory.from_ical(val, timezone=tzid) except ValueError: # TODO: caused by a bug in plone.formwidget.recurrence, # where the recurrencewidget or plone.event fails with # COUNT=1 and a extra RDATE. # TODO: REMOVE this workaround, once this failure is # fixed in recurrence widget. continue ical.add(prop, dtlist) if event.location: ical.add('location', event.location) # TODO: revisit and implement attendee export according to RFC if event.attendees: for attendee in event.attendees: att = icalendar.prop.vCalAddress(attendee) att.params['cn'] = icalendar.prop.vText(attendee) att.params['ROLE'] = icalendar.prop.vText('REQ-PARTICIPANT') ical.add('attendee', att) cn = [] if event.contact_name: cn.append(event.contact_name) if event.contact_phone: cn.append(event.contact_phone) if event.contact_email: cn.append(event.contact_email) if event.event_url: cn.append(event.event_url) if cn: ical.add('contact', u', '.join(cn)) if event.subjects: for subject in event.subjects: ical.add('categories', subject) return ical
def to_ical(self): ical = icalendar.Event() event = self.event # TODO: event.text # must be in utc ical.add('dtstamp', utc(pydt(datetime.now()))) ical.add('created', utc(pydt(event.created))) ical.add('last-modified', utc(pydt(event.last_modified))) ical.add('uid', event.uid) ical.add('url', event.url) ical.add('summary', event.title) if event.description: ical.add('description', event.description) if event.whole_day: ical.add('dtstart', event.start.date()) # RFC5545, 3.6.1 # For cases where a "VEVENT" calendar component # specifies a "DTSTART" property with a DATE value type but no # "DTEND" nor "DURATION" property, the event's duration is taken to # be one day. # # RFC5545 doesn't define clearly, if all-day events should have # a end date on the same date or one day after the start day at # 0:00. Most icalendar libraries use the latter method. # Internally, whole_day events end on the same day one second # before midnight. Using the RFC5545 preferred method for # plone.app.event seems not appropriate, since we would have to fix # the date to end a day before for displaying. # For exporting, we let whole_day events end on the next day at # midnight. # See: # http://stackoverflow.com/questions/1716237/single-day-all-day-appointments-in-ics-files # http://icalevents.com/1778-all-day-events-adding-a-day-or-not/ # http://www.innerjoin.org/iCalendar/all-day-events.html ical.add('dtend', event.end.date() + timedelta(days=1)) elif event.open_end: # RFC5545, 3.6.1 # For cases where a "VEVENT" calendar component # specifies a "DTSTART" property with a DATE-TIME value type but no # "DTEND" property, the event ends on the same calendar date and # time of day specified by the "DTSTART" property. ical.add('dtstart', event.start) else: ical.add('dtstart', event.start) ical.add('dtend', event.end) if event.recurrence: for recdef in event.recurrence.split(): prop, val = recdef.split(':') if prop == 'RRULE': ical.add(prop, icalendar.prop.vRecur.from_ical(val)) elif prop in ('EXDATE', 'RDATE'): factory = icalendar.prop.vDDDLists # localize ex/rdate # TODO: should better already be localized by event object tzid = event.timezone # get list of datetime values from ical string try: dtlist = factory.from_ical(val, timezone=tzid) except ValueError: # TODO: caused by a bug in plone.formwidget.recurrence, # where the recurrencewidget or plone.event fails with # COUNT=1 and a extra RDATE. # TODO: REMOVE this workaround, once this failure is # fixed in recurrence widget. continue ical.add(prop, dtlist) if event.location: ical.add('location', event.location) # TODO: revisit and implement attendee export according to RFC if event.attendees: for attendee in event.attendees: att = icalendar.prop.vCalAddress(attendee) att.params['cn'] = icalendar.prop.vText(attendee) att.params['ROLE'] = icalendar.prop.vText('REQ-PARTICIPANT') ical.add('attendee', att) cn = [] if event.contact_name: cn.append(event.contact_name) if event.contact_phone: cn.append(event.contact_phone) if event.contact_email: cn.append(event.contact_email) if event.event_url: cn.append(event.event_url) if cn: ical.add('contact', u', '.join(cn)) if event.subjects: for subject in event.subjects: ical.add('categories', subject) return ical
def last_modified(self): # must be in uc return {'value': utc(self.event.last_modified)}
def ical_import(container, ics_resource, event_type, sync_strategy=base.SYNC_KEEP_NEWER): cal = icalendar.Calendar.from_ical(ics_resource) events = cal.walk('VEVENT') cat = getToolByName(container, 'portal_catalog') container_path = '/'.join(container.getPhysicalPath()) def _get_by_sync_uid(uid): return cat( sync_uid=uid, path={'query': container_path, 'depth': 1} ) def _get_prop(prop, item, default=None): ret = default if prop in item: ret = safe_unicode(item.decoded(prop)) return ret def _from_list(ical, prop): """For EXDATE and RDATE recurrence component properties, the dates can be defined within one EXDATE/RDATE line or for each date an individual line. In the latter case, icalendar creates a list. This method handles this case. TODO: component property parameters like TZID are not used here. """ val = prop in ical and ical[prop] or [] if not isinstance(val, list): val = [val] #ret = '' #for item in val: # ret = ret and '%s\n' % ret or ret # insert linebreak # ret = '%s%s:%s' % (ret, prop, item.to_ical()) #return ret # Zip multiple lines into one, since jquery.recurrenceinput.js does # not support multiple lines here # https://github.com/collective/jquery.recurrenceinput.js/issues/15 ret = '' for item in val: ret = ret and '%s,' % ret or ret # insert linebreak ret = '%s%s' % (ret, item.to_ical()) return ret and '%s:%s' % (prop, ret) or None count = 0 for item in events: start = _get_prop('DTSTART', item) end = _get_prop('DTEND', item) if not end: duration = _get_prop('DURATION', item) if duration: end = start + duration # else: whole day or open end timezone = getattr(getattr(start, 'tzinfo', None), 'zone', None) or\ base.default_timezone(container) whole_day = False open_end = False if is_date(start) and (is_date(end) or end is None): # All day / whole day events # End must be same type as start (RFC5545, 3.8.2.2) whole_day = True if end is None: end = start if start < end: # RFC5545 doesn't define clearly, if all day events should have # a end date one day after the start day at 0:00. # Internally, we handle all day events with start=0:00, # end=:23:59:59, so we substract one day here. end = end - datetime.timedelta(days=1) start = base.dt_start_of_day(date_to_datetime(start)) end = base.dt_end_of_day(date_to_datetime(end)) elif is_datetime(start) and end is None: # Open end event, see RFC 5545, 3.6.1 open_end = True end = base.dt_end_of_day(date_to_datetime(start)) assert(is_datetime(start)) assert(is_datetime(end)) title = _get_prop('SUMMARY', item) description = _get_prop('DESCRIPTION', item) location = _get_prop('LOCATION', item) url = _get_prop('URL', item) rrule = _get_prop('RRULE', item) rrule = rrule and 'RRULE:%s' % rrule.to_ical() or '' rdates = _from_list(item, 'RDATE') exdates = _from_list(item, 'EXDATE') rrule = '\n'.join([it for it in [rrule, rdates, exdates] if it]) # TODO: attendee-lists are not decoded properly and contain only # vCalAddress values attendees = item.get('ATTENDEE', ()) contact = _get_prop('CONTACT', item) categories = item.get('CATEGORIES', ()) if hasattr(categories, '__iter__'): categories = [safe_unicode(it) for it in categories] ext_modified = utc(_get_prop('LAST-MODIFIED', item)) # TODO: better use plone.api for content creation, from which some of # the code here is copied content = None new_content_id = None existing_event = None sync_uid = _get_prop('UID', item) if sync_strategy != base.SYNC_NONE and sync_uid: existing_event = _get_by_sync_uid(sync_uid) if existing_event: if sync_strategy == base.SYNC_KEEP_MINE: # On conflict, keep mine continue exist_event = existing_event[0].getObject() acc = IEventAccessor(exist_event) if sync_strategy == base.SYNC_KEEP_NEWER and\ (not ext_modified or acc.last_modified >= ext_modified): # Update only, if newer, if ext_modified exists continue # Else: update content = exist_event else: # TODO: if AT had the same attrs like IDXEventBase, we could set # everything within this invokeFactory call. new_content_id = str(random.randint(0, 99999999)) container.invokeFactory(event_type, id=new_content_id, title=title, description=description) content = container[new_content_id] assert(content) # At this point, a content must be available. event = IEventAccessor(content) event.title = title event.description = description event.start = start event.end = end event.timezone = timezone event.whole_day = whole_day event.open_end = open_end event.location = location event.event_url = url event.recurrence = rrule event.attendees = attendees event.contact_name = contact event.subjects = categories if sync_strategy != base.SYNC_NONE: # Don't import the sync_uid, if no sync strategy is chosen. Let the # sync_uid be autogenerated then. event.sync_uid = sync_uid notify(ObjectModifiedEvent(content)) # Archetypes specific code if getattr(content, 'processForm', False): # Will finish Archetypes content item creation process, # rename-after-creation and such content.processForm() # Use commits instead of savepoints to avoid "FileStorageError: # description too long" on large imports. transaction.get().commit() # Commit before rename if new_content_id and new_content_id in container: # Rename with new id from title, if processForm didn't do it. chooser = INameChooser(container) new_id = chooser.chooseName(title, content) content.aq_parent.manage_renameObject(new_content_id, new_id) # Do this at the end, otherwise it's overwritten if ext_modified: event.last_modified = ext_modified count += 1 return {'count': count}
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
def dtstamp(self): # must be in uc return {'value': utc(datetime.now())}
userId=admin['UserId'], pagination={ 'MaxNumberResults': page_size } ) if gudu_response['TotalNumberResponses'] > 0: # get the last page! gudu_response = usage.call_service( 'GetUserDetailedUsage', userId=admin['UserId'], pagination={ 'MaxNumberResults': page_size, 'PageNumber': int(ceil(gudu_response['TotalNumberResponses'] / float(page_size)) - 1) } ) endRange = datetime.utcnow() beginRange = endRange - timedelta(days=7) week_views = [v for v in gudu_response['PagedResponses']['DetailedUsageResponseItem'] if v['Time'] > utils.utc(beginRange)] if week_views: # let's get the sessionId of the first view and see who else has been watching it in the past week sessionId = week_views[0]['SessionId'] print('admin viewed session {} in the past week'.format(sessionId)) ssu_response = usage.call_service( 'GetSessionSummaryUsage', sessionId=sessionId, beginRange=beginRange, endRange=endRange, granularity='Daily' # zeep doesn't currently support parsing enumeration types, so this is a magic string ) view_days = [r for r in ssu_response if r['Views'] > 0] for day in sorted(view_days, key=lambda d:d['Time']): day_offset = int(ceil((endRange - day['Time'].replace(tzinfo=None)).total_seconds() / (3600 * 24)))
def created(self): return utc(self.context.creation_date)
def to_ical(self): ical = icalendar.Event() event = self.event # TODO: event.text # TODO: until VTIMETZONE component is added and TZID used, everything is # converted to UTC. use real TZID, when VTIMEZONE is used! ical.add('dtstamp', utc(pydt(datetime.now()))) ical.add('created', utc(pydt(event.created))) ical.add('uid', event.uid) ical.add('url', event.url) ical.add('last-modified', utc(pydt(event.last_modified))) ical.add('summary', event.title) if event.description: ical.add('description', event.description) if event.whole_day: ical.add('dtstart', utc(pydt(event.start)).date()) ical.add('dtend', utc(pydt(event.end + timedelta(days=1))).date()) else: ical.add('dtstart', utc(pydt(event.start))) ical.add('dtend', utc(pydt(event.end))) if event.recurrence: if event.recurrence.startswith('RRULE:'): recurrence = event.recurrence[6:] else: recurrence = event.recurrence ical.add('rrule', icalendar.prop.vRecur.from_ical(recurrence)) if event.location: ical.add('location', event.location) # TODO: revisit and implement attendee export according to RFC if event.attendees: for attendee in event.attendees: att = icalendar.prop.vCalAddress(attendee) att.params['cn'] = icalendar.prop.vText(attendee) att.params['ROLE'] = icalendar.prop.vText('REQ-PARTICIPANT') ical.add('attendee', att) cn = [] if event.contact_name: cn.append(event.contact_name) if event.contact_phone: cn.append(event.contact_phone) if event.contact_email: cn.append(event.contact_email) if event.event_url: cn.append(event.event_url) if cn: ical.add('contact', u', '.join(cn)) if event.subjects: for subject in event.subjects: ical.add('categories', subject) return ical
def to_ical(self): ical = icalendar.Event() event = self.event # TODO: event.text # must be in utc ical.add('dtstamp', utc(pydt(datetime.now()))) ical.add('created', utc(pydt(event.created))) ical.add('last-modified', utc(pydt(event.last_modified))) ical.add('uid', event.uid) ical.add('url', event.url) ical.add('summary', event.title) if event.description: ical.add('description', event.description) if event.whole_day: ical.add('dtstart', event.start.date()) ical.add('dtend', event.end.date()) else: ical.add('dtstart', event.start) ical.add('dtend', event.end) if event.recurrence: for recdef in event.recurrence.split(): prop, val = recdef.split(':') if prop == 'RRULE': ical.add(prop, icalendar.prop.vRecur.from_ical(val)) elif prop in ('EXDATE', 'RDATE'): factory = icalendar.prop.vDDDLists # localize ex/rdate # TODO: should better already be localized by event object tzid = event.timezone # get list of datetime values from ical string dtlist = factory.from_ical(val, timezone=tzid) ical.add(prop, dtlist) if event.location: ical.add('location', event.location) # TODO: revisit and implement attendee export according to RFC if event.attendees: for attendee in event.attendees: att = icalendar.prop.vCalAddress(attendee) att.params['cn'] = icalendar.prop.vText(attendee) att.params['ROLE'] = icalendar.prop.vText('REQ-PARTICIPANT') ical.add('attendee', att) cn = [] if event.contact_name: cn.append(event.contact_name) if event.contact_phone: cn.append(event.contact_phone) if event.contact_email: cn.append(event.contact_email) if event.event_url: cn.append(event.event_url) if cn: ical.add('contact', u', '.join(cn)) if event.subjects: for subject in event.subjects: ical.add('categories', subject) return ical
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 ical_import(container, ics_resource, event_type, sync_strategy=base.SYNC_KEEP_NEWER): cal = icalendar.Calendar.from_ical(ics_resource) events = cal.walk('VEVENT') cat = getToolByName(container, 'portal_catalog') container_path = '/'.join(container.getPhysicalPath()) def _get_by_sync_uid(uid): return cat(sync_uid=uid, path={'query': container_path, 'depth': 1}) def _get_prop(prop, item, default=None): ret = default if prop in item: ret = safe_unicode(item.decoded(prop)) return ret def _from_list(ical, prop): """For EXDATE and RDATE recurrence component properties, the dates can be defined within one EXDATE/RDATE line or for each date an individual line. In the latter case, icalendar creates a list. This method handles this case. TODO: component property parameters like TZID are not used here. """ val = ical[prop] if prop in ical else [] if not isinstance(val, list): val = [val] # Zip multiple lines into one, since jquery.recurrenceinput.js does # not support multiple lines here # https://github.com/collective/jquery.recurrenceinput.js/issues/15 ret = '' for item in val: ret = '%s,' % ret if ret else ret # insert linebreak ret = '%s%s' % (ret, item.to_ical()) return '%s:%s' % (prop, ret) if ret else None count = 0 for item in events: start = _get_prop('DTSTART', item) end = _get_prop('DTEND', item) if not end: duration = _get_prop('DURATION', item) if duration: end = start + duration # else: whole day or open end whole_day = False open_end = False if is_date(start) and (is_date(end) or end is None): # All day / whole day events # End must be same type as start (RFC5545, 3.8.2.2) whole_day = True if end is None: end = start if start < end: # RFC5545 doesn't define clearly, if all day events should have # a end date one day after the start day at 0:00. # Internally, we handle all day events with start=0:00, # end=:23:59:59, so we substract one day here. end = end - datetime.timedelta(days=1) start = base.dt_start_of_day(date_to_datetime(start)) end = base.dt_end_of_day(date_to_datetime(end)) elif is_datetime(start) and end is None: # Open end event, see RFC 5545, 3.6.1 open_end = True end = base.dt_end_of_day(date_to_datetime(start)) assert (is_datetime(start)) assert (is_datetime(end)) # Set timezone, if not already set tz = base.default_timezone(container, as_tzinfo=True) if not getattr(start, 'tzinfo', False): start = tz.localize(start) if not getattr(end, 'tzinfo', False): end = tz.localize(end) title = _get_prop('SUMMARY', item) description = _get_prop('DESCRIPTION', item) location = _get_prop('LOCATION', item) url = _get_prop('URL', item) rrule = _get_prop('RRULE', item) rrule = 'RRULE:%s' % rrule.to_ical() if rrule else '' rdates = _from_list(item, 'RDATE') exdates = _from_list(item, 'EXDATE') rrule = '\n'.join([it for it in [rrule, rdates, exdates] if it]) # TODO: attendee-lists are not decoded properly and contain only # vCalAddress values attendees = item.get('ATTENDEE', ()) contact = _get_prop('CONTACT', item) categories = item.get('CATEGORIES', ()) if getattr(categories, '__iter__', False): categories = tuple([safe_unicode(it) for it in categories]) ext_modified = utc(_get_prop('LAST-MODIFIED', item)) content = None new_content_id = None existing_event = None sync_uid = _get_prop('UID', item) if sync_uid and sync_strategy is not base.SYNC_NONE: existing_event = _get_by_sync_uid(sync_uid) if existing_event: if sync_strategy == base.SYNC_KEEP_MINE: # On conflict, keep mine continue exist_event = existing_event[0].getObject() acc = IEventAccessor(exist_event) if sync_strategy == base.SYNC_KEEP_NEWER and\ (not ext_modified or acc.last_modified > ext_modified): # Update only if modified date was passed in and it is not # older than the current modified date. The client is not # expected to update the "last-modified" property, it is the # job of the server (calendar store) to keep it up to date. # This makes sure the client did the change on an up-to-date # version of the object. See # http://tools.ietf.org/search/rfc5545#section-3.8.7.3 continue # Else: update content = exist_event else: new_content_id = str(random.randint(0, 99999999)) container.invokeFactory(event_type, id=new_content_id, title=title, description=description) content = container[new_content_id] assert (content) # At this point, a content must be available. event = IEventAccessor(content) event.title = title event.description = description event.start = start event.end = end event.whole_day = whole_day event.open_end = open_end event.location = location event.event_url = url event.recurrence = rrule event.attendees = attendees event.contact_name = contact event.subjects = categories if sync_uid and sync_strategy is not base.SYNC_NONE: # Set the external sync_uid for sync strategies other than # SYNC_NONE. event.sync_uid = sync_uid notify(ObjectModifiedEvent(content)) # Use commits instead of savepoints to avoid "FileStorageError: # description too long" on large imports. transaction.get().commit() # Commit before rename if new_content_id and new_content_id in container: # Rename with new id from title, if processForm didn't do it. chooser = INameChooser(container) new_id = chooser.chooseName(title, content) content.aq_parent.manage_renameObject(new_content_id, new_id) # Do this at the end, otherwise it's overwritten if ext_modified: event.last_modified = ext_modified count += 1 return {'count': count}
def ical_import(container, ics_resource, event_type, sync_strategy=base.SYNC_KEEP_NEWER): cal = icalendar.Calendar.from_ical(ics_resource) events = cal.walk('VEVENT') cat = getToolByName(container, 'portal_catalog') container_path = '/'.join(container.getPhysicalPath()) def _get_by_sync_uid(uid): return cat(sync_uid=uid, path={'query': container_path, 'depth': 1}) def _get_prop(prop, item, default=None): ret = default if prop in item: ret = safe_unicode(item.decoded(prop)) return ret def _from_list(ical, prop): """For EXDATE and RDATE recurrence component properties, the dates can be defined within one EXDATE/RDATE line or for each date an individual line. In the latter case, icalendar creates a list. This method handles this case. TODO: component property parameters like TZID are not used here. """ val = prop in ical and ical[prop] or [] if not isinstance(val, list): val = [val] #ret = '' #for item in val: # ret = ret and '%s\n' % ret or ret # insert linebreak # ret = '%s%s:%s' % (ret, prop, item.to_ical()) #return ret # Zip multiple lines into one, since jquery.recurrenceinput.js does # not support multiple lines here # https://github.com/collective/jquery.recurrenceinput.js/issues/15 ret = '' for item in val: ret = ret and '%s,' % ret or ret # insert linebreak ret = '%s%s' % (ret, item.to_ical()) return ret and '%s:%s' % (prop, ret) or None count = 0 for item in events: start = _get_prop('DTSTART', item) end = _get_prop('DTEND', item) if not end: duration = _get_prop('DURATION', item) if duration: end = start + duration # else: whole day or open end timezone = getattr(getattr(start, 'tzinfo', None), 'zone', None) or\ base.default_timezone(container) whole_day = False open_end = False if is_date(start) and (is_date(end) or end is None): # All day / whole day events # End must be same type as start (RFC5545, 3.8.2.2) whole_day = True if end is None: end = start if start < end: # RFC5545 doesn't define clearly, if all day events should have # a end date one day after the start day at 0:00. # Internally, we handle all day events with start=0:00, # end=:23:59:59, so we substract one day here. end = end - datetime.timedelta(days=1) start = base.dt_start_of_day(date_to_datetime(start)) end = base.dt_end_of_day(date_to_datetime(end)) elif is_datetime(start) and end is None: # Open end event, see RFC 5545, 3.6.1 open_end = True end = base.dt_end_of_day(date_to_datetime(start)) assert (is_datetime(start)) assert (is_datetime(end)) title = _get_prop('SUMMARY', item) description = _get_prop('DESCRIPTION', item) location = _get_prop('LOCATION', item) url = _get_prop('URL', item) rrule = _get_prop('RRULE', item) rrule = rrule and 'RRULE:%s' % rrule.to_ical() or '' rdates = _from_list(item, 'RDATE') exdates = _from_list(item, 'EXDATE') rrule = '\n'.join([it for it in [rrule, rdates, exdates] if it]) # TODO: attendee-lists are not decoded properly and contain only # vCalAddress values attendees = item.get('ATTENDEE', ()) contact = _get_prop('CONTACT', item) categories = item.get('CATEGORIES', ()) if hasattr(categories, '__iter__'): categories = [safe_unicode(it) for it in categories] ext_modified = utc(_get_prop('LAST-MODIFIED', item)) # TODO: better use plone.api for content creation, from which some of # the code here is copied content = None new_content_id = None existing_event = None sync_uid = _get_prop('UID', item) if sync_strategy != base.SYNC_NONE and sync_uid: existing_event = _get_by_sync_uid(sync_uid) if existing_event: if sync_strategy == base.SYNC_KEEP_MINE: # On conflict, keep mine continue exist_event = existing_event[0].getObject() acc = IEventAccessor(exist_event) if sync_strategy == base.SYNC_KEEP_NEWER and\ (not ext_modified or acc.last_modified >= ext_modified): # Update only, if newer, if ext_modified exists continue # Else: update content = exist_event else: # TODO: if AT had the same attrs like IDXEventBase, we could set # everything within this invokeFactory call. new_content_id = str(random.randint(0, 99999999)) container.invokeFactory(event_type, id=new_content_id, title=title, description=description) content = container[new_content_id] assert (content) # At this point, a content must be available. event = IEventAccessor(content) event.title = title event.description = description event.start = start event.end = end event.timezone = timezone event.whole_day = whole_day event.open_end = open_end event.location = location event.event_url = url event.recurrence = rrule event.attendees = attendees event.contact_name = contact event.subjects = categories if sync_strategy != base.SYNC_NONE: # Don't import the sync_uid, if no sync strategy is chosen. Let the # sync_uid be autogenerated then. event.sync_uid = sync_uid notify(ObjectModifiedEvent(content)) # Archetypes specific code if getattr(content, 'processForm', False): # Will finish Archetypes content item creation process, # rename-after-creation and such content.processForm() # Use commits instead of savepoints to avoid "FileStorageError: # description too long" on large imports. transaction.get().commit() # Commit before rename if new_content_id and new_content_id in container: # Rename with new id from title, if processForm didn't do it. chooser = INameChooser(container) new_id = chooser.chooseName(title, content) content.aq_parent.manage_renameObject(new_content_id, new_id) # Do this at the end, otherwise it's overwritten if ext_modified: event.last_modified = ext_modified count += 1 return {'count': count}
def last_modified(self): return utc(self.context.modification_date)
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 ical_import(container, ics_resource, event_type, sync_strategy=base.SYNC_KEEP_NEWER): cal = icalendar.Calendar.from_ical(ics_resource) events = cal.walk('VEVENT') cat = getToolByName(container, 'portal_catalog') container_path = '/'.join(container.getPhysicalPath()) def _get_by_sync_uid(uid): return cat( sync_uid=uid, path={'query': container_path, 'depth': 1} ) def _get_prop(prop, item, default=None): ret = default if prop in item: ret = safe_unicode(item.decoded(prop)) return ret def _from_list(ical, prop): """For EXDATE and RDATE recurrence component properties, the dates can be defined within one EXDATE/RDATE line or for each date an individual line. In the latter case, icalendar creates a list. This method handles this case. TODO: component property parameters like TZID are not used here. """ val = ical[prop] if prop in ical else [] if not isinstance(val, list): val = [val] # Zip multiple lines into one, since jquery.recurrenceinput.js does # not support multiple lines here # https://github.com/collective/jquery.recurrenceinput.js/issues/15 ret = '' for item in val: ret = '%s,' % ret if ret else ret # insert linebreak ical_val = item.to_ical() if six.PY3 and isinstance(ical_val, six.binary_type): ical_val = ical_val.decode('utf8') ret = '%s%s' % (ret, ical_val) return '%s:%s' % (prop, ret) if ret else None count = 0 for item in events: start = _get_prop('DTSTART', item) end = _get_prop('DTEND', item) if not end: duration = _get_prop('DURATION', item) if duration: end = start + duration # else: whole day or open end whole_day = False open_end = False if is_date(start) and (is_date(end) or end is None): # All day / whole day events # End must be same type as start (RFC5545, 3.8.2.2) whole_day = True if end is None: end = start if start < end: # RFC5545 doesn't define clearly, if all day events should have # a end date one day after the start day at 0:00. # Internally, we handle all day events with start=0:00, # end=:23:59:59, so we substract one day here. end = end - datetime.timedelta(days=1) start = base.dt_start_of_day(date_to_datetime(start)) end = base.dt_end_of_day(date_to_datetime(end)) elif is_datetime(start) and end is None: # Open end event, see RFC 5545, 3.6.1 open_end = True end = base.dt_end_of_day(date_to_datetime(start)) assert(is_datetime(start)) assert(is_datetime(end)) # Set timezone, if not already set tz = base.default_timezone(container, as_tzinfo=True) if not getattr(start, 'tzinfo', False): start = tz.localize(start) if not getattr(end, 'tzinfo', False): end = tz.localize(end) title = _get_prop('SUMMARY', item) description = _get_prop('DESCRIPTION', item) location = _get_prop('LOCATION', item) url = _get_prop('URL', item) rrule = _get_prop('RRULE', item) rrule = rrule.to_ical() if rrule else '' if rrule: if six.PY3 and isinstance(rrule, six.binary_type): rrule = rrule.decode('utf8') rrule = 'RRULE:%s' % rrule rdates = _from_list(item, 'RDATE') exdates = _from_list(item, 'EXDATE') rrule = '\n'.join([it for it in [rrule, rdates, exdates] if it]) # TODO: attendee-lists are not decoded properly and contain only # vCalAddress values attendees = item.get('ATTENDEE', ()) contact = _get_prop('CONTACT', item) categories = item.get('CATEGORIES', ()) if getattr(categories, '__iter__', False): categories = tuple([safe_unicode(it) for it in categories]) ext_modified = utc(_get_prop('LAST-MODIFIED', item)) content = None new_content_id = None existing_event = None sync_uid = _get_prop('UID', item) if sync_uid and sync_strategy is not base.SYNC_NONE: existing_event = _get_by_sync_uid(sync_uid) if existing_event: if sync_strategy == base.SYNC_KEEP_MINE: # On conflict, keep mine continue exist_event = existing_event[0].getObject() acc = IEventAccessor(exist_event) if sync_strategy == base.SYNC_KEEP_NEWER and\ (not ext_modified or acc.last_modified > ext_modified): # Update only if modified date was passed in and it is not # older than the current modified date. The client is not # expected to update the "last-modified" property, it is the # job of the server (calendar store) to keep it up to date. # This makes sure the client did the change on an up-to-date # version of the object. See # http://tools.ietf.org/search/rfc5545#section-3.8.7.3 continue # Else: update content = exist_event else: new_content_id = str(random.randint(0, 99999999)) container.invokeFactory(event_type, id=new_content_id, title=title, description=description) content = container[new_content_id] assert(content) # At this point, a content must be available. event = IEventAccessor(content) event.title = title event.description = description event.start = start event.end = end event.whole_day = whole_day event.open_end = open_end event.location = location event.event_url = url event.recurrence = rrule event.attendees = attendees event.contact_name = contact event.subjects = categories if sync_uid and sync_strategy is not base.SYNC_NONE: # Set the external sync_uid for sync strategies other than # SYNC_NONE. event.sync_uid = sync_uid notify(ObjectModifiedEvent(content)) # Use commits instead of savepoints to avoid "FileStorageError: # description too long" on large imports. transaction.get().commit() # Commit before rename if new_content_id and new_content_id in container: # Rename with new id from title, if processForm didn't do it. chooser = INameChooser(container) new_id = chooser.chooseName(title, content) content.aq_parent.manage_renameObject(new_content_id, new_id) # Do this at the end, otherwise it's overwritten if ext_modified: event.last_modified = ext_modified count += 1 return {'count': count}