def migrate_schema_fields(self): newacc = IEventAccessor(self.new) newacc.start = self.old.start_date newacc.end = self.old.end_date newacc.timezone = str(self.old.start_date.tzinfo) \ if self.old.start_date.tzinfo \ else default_timezone(fallback='UTC') if hasattr(self.old, 'location'): newacc.location = self.old.location if hasattr(self.old, 'attendees'): newacc.attendees = tuple(self.old.attendees.splitlines()) if hasattr(self.old, 'event_url'): newacc.event_url = self.old.event_url if hasattr(self.old, 'contact_name'): newacc.contact_name = self.old.contact_name if hasattr(self.old, 'contact_email'): newacc.contact_email = self.old.contact_email if hasattr(self.old, 'contact_phone'): newacc.contact_phone = self.old.contact_phone if hasattr(self.old, 'text'): # Copy the entire richtext object, not just it's representation IEventSummary(self.new).text = self.old.text # Trigger ObjectModified, so timezones can be fixed up. notify(ObjectModifiedEvent(self.new))
def test_pae_dxevent_is_migrated(self): from datetime import datetime from plone.app.contenttypes.migration.migration import migrate_events from plone.app.textfield.value import RichTextValue from plone.app.event.dx.behaviors import IEventSummary # Enable plone.app.event.dx from plone.app.testing import applyProfile applyProfile(self.portal, 'plone.app.event.dx:default') old_event = self.portal[self.portal.invokeFactory( 'plone.app.event.dx.event', 'dx-event', start=datetime(2011, 11, 11, 11, 0), end=datetime(2011, 11, 11, 12, 0), timezone="Asia/Tbilisi", whole_day=False, )] old_event_acc = IEventAccessor(old_event) old_event_acc.contact_name = 'George' old_event_acc.contact_email = '*****@*****.**' old_event_acc.contact_phone = '+99512345' old_event_acc.event_url = 'http://geor.ge/event' old_event_acc.text = RichTextValue( raw='Woo, yeah', mimeType='text/plain', outputMimeType='text/x-html-safe' ) # migrate applyProfile(self.portal, 'plone.app.contenttypes:default') migrate_events(self.portal) # Compare new and old events new_event = self.portal['dx-event'] new_event_acc = IEventAccessor(new_event) self.assertEqual(False, old_event.exclude_from_nav) self.assertEqual('Event', new_event.portal_type) self.assertEqual(2011, new_event_acc.start.year) self.assertEqual(11, new_event_acc.start.month) self.assertEqual(11, new_event_acc.start.day) self.assertEqual(11, new_event_acc.start.hour) self.assertEqual('Asia/Tbilisi', str(new_event_acc.start.tzinfo)) self.assertEqual(2011, new_event_acc.end.year) self.assertEqual(11, new_event_acc.end.month) self.assertEqual(11, new_event_acc.end.day) self.assertEqual(12, new_event_acc.end.hour) self.assertEqual('Asia/Tbilisi', str(new_event_acc.end.tzinfo)) self.assertEqual(u'Asia/Tbilisi', new_event_acc.timezone) self.assertEqual(u'George', new_event_acc.contact_name) self.assertEqual(u'*****@*****.**', new_event_acc.contact_email) self.assertEqual(u'+99512345', new_event_acc.contact_phone) self.assertEqual(u'http://geor.ge/event', new_event_acc.event_url) self.assertEqual(u'<p>Woo, yeah</p>', new_event_acc.text) self.assertEqual('Woo, yeah', IEventSummary(new_event).text.raw) self.assertEqual(False, new_event.exclude_from_nav)
def setUp(self): self.request = self.layer['request'] portal = self.layer['portal'] setRoles(portal, TEST_USER_ID, ['Manager']) portal.invokeFactory('Folder', id='events', title=u"Events", Description=u"The portal's Events") portal.events.invokeFactory('Event', id='ploneconf2007', title='Plone Conf 2007', startDate='2007/10/10', endDate='2007/10/12', location='Naples', eventUrl='http://plone.org/events/conferences/2007-naples', attendees=['anne','bob','cesar']) portal.events.invokeFactory('Event', id='ploneconf2008', title='Plone Conf 2008', startDate='2008/10/08', endDate='2008/10/10', location='DC', recurrence=u'RRULE:FREQ=DAILY;COUNT=5\r\nEXDATE:20081011T000000,20081012T000000\r\nRDATE:20081007T000000', eventUrl='http://plone.org/events/conferences/2008-washington-dc') portal.events.invokeFactory('plone.app.event.dx.event', id='ploneconf2012', title='Plone Conf 2012', recurrence=u'RRULE:FREQ=DAILY;COUNT=5\r\nEXDATE:20121013T000000,20121014T000000\r\nRDATE:20121009T000000', start=datetime(2012,10,10,8,0), end=datetime(2012,10,10,18,0), timezone='Europe/Amsterdam') pc12 = IEventAccessor(portal.events.ploneconf2012) pc12.location='Arnhem' pc12.contact_name='Four Digits' pc12.contact_email='*****@*****.**' notify(ObjectModifiedEvent(pc12)) portal.events.invokeFactory('plone.app.event.dx.event', id='artsprint2013', title='Artsprint 2013', start=datetime(2013,2,18), end=datetime(2012,2,22), whole_day=True, timezone='Europe/Vienna') portal.invokeFactory("Collection", "collection", title="New Collection", sort_on='start') portal['collection'].setQuery([{ 'i': 'Type', 'o': 'plone.app.querystring.operation.string.is', 'v': 'Event', }, ]) self.portal = portal
def migrate_schema_fields(self): oldacc = IEventAccessor(self.old) newacc = IEventAccessor(self.new) newacc.start = oldacc.start newacc.end = oldacc.end newacc.timezone = oldacc.timezone newacc.location = oldacc.location newacc.attendees = oldacc.attendees newacc.event_url = oldacc.event_url newacc.contact_name = oldacc.contact_name newacc.contact_email = oldacc.contact_email newacc.contact_phone = oldacc.contact_phone # Copy the entire richtext object, not just it's representation IEventSummary(self.new).text = IEventSummary(self.old).text # Trigger ObjectModified, so timezones can be fixed up. notify(ObjectModifiedEvent(self.new))
def migrate_schema_fields(self): from plone.app.event.dx.behaviors import IEventSummary newacc = IEventAccessor(self.new) newacc.location = self.old.location newacc.start = self.old.start_date newacc.end = self.old.end_date if self.old.start_date.tzinfo: newacc.timezone = str(self.old.start_date.tzinfo) else: newacc.timezone = default_timezone(fallback='UTC') newacc.attendees = self.old.attendees newacc.event_url = self.old.event_url newacc.contact_name = self.old.contact_name newacc.contact_email = self.old.contact_email newacc.contact_phone = self.old.contact_phone # Copy the entire richtext object, not just it's representation IEventSummary(self.new).text = self.old.text # Trigger ObjectModified, so timezones can be fixed up. notify(ObjectModifiedEvent(self.new))
def migrate_schema_fields(self): from plone.app.event.dx.behaviors import IEventSummary old_start = self.old.getField('startDate').get(self.old) old_end = self.old.getField('endDate').get(self.old) old_location = self.old.getField('location').get(self.old) old_attendees = self.old.getField('attendees').get(self.old) old_eventurl = self.old.getField('eventUrl').get(self.old) old_contactname = self.old.getField('contactName').get(self.old) old_contactemail = self.old.getField('contactEmail').get(self.old) old_contactphone = self.old.getField('contactPhone').get(self.old) old_text_field = self.old.getField('text') raw_text = safe_unicode(old_text_field.getRaw(self.old)) mime_type = old_text_field.getContentType(self.old) if raw_text.strip() == '': raw_text = '' old_richtext = RichTextValue(raw=raw_text, mimeType=mime_type, outputMimeType='text/x-html-safe') if self.old.getField('timezone'): old_timezone = self.old.getField('timezone').get(self.old) else: old_timezone = default_timezone(fallback='UTC') acc = IEventAccessor(self.new) acc.start = old_start.asdatetime() # IEventBasic acc.end = old_end.asdatetime() # IEventBasic acc.timezone = old_timezone # IEventBasic acc.location = old_location # IEventLocation acc.attendees = old_attendees # IEventAttendees acc.event_url = old_eventurl # IEventContact acc.contact_name = old_contactname # IEventContact acc.contact_email = old_contactemail # IEventContact acc.contact_phone = old_contactphone # IEventContact # Copy the entire richtext object, not just it's representation IEventSummary(self.new).text = old_richtext # Trigger ObjectModified, so timezones can be fixed up. notify(ObjectModifiedEvent(self.new))
def ical_import(container, ics_resource, event_type): cal = icalendar.Calendar.from_ical(ics_resource) events = cal.walk('VEVENT') def _get_prop(prop, item): ret = None 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 = _get_prop('ATTENDEE', item) contact = _get_prop('CONTACT', item) categories = _get_prop('CATEGORIES', item) if hasattr(categories, '__iter__'): categories = [safe_unicode(it) for it in categories] ## for sync #created = _get_prop('CREATED', item) #modified = _get_prop('LAST-MODIFIED', item) # TODO: better use plone.api, from which some of the code here is # copied content_id = str(random.randint(0, 99999999)) # TODO: if AT had the same attrs like IDXEventBase, we could set # everything within this invokeFactory call. container.invokeFactory(event_type, id=content_id, title=title, description=description) content = container[content_id] event = IEventAccessor(content) 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 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() if 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) transaction.savepoint(optimistic=True) # Commit before renaming content.aq_parent.manage_renameObject(content_id, new_id) else: transaction.savepoint(optimistic=True) 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 = 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 ical_import(container, ics_resource, event_type): cal = icalendar.Calendar.from_ical(ics_resource) events = cal.walk('VEVENT') def _get_prop(prop, item): ret = None if prop in item: ret = safe_unicode(item.decoded(prop)) return ret 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 '' rdate = _get_prop('RDATE', item) rrule = rdate and '%s\nRDATE:%s' % (rrule, rdate.to_ical()) or '' exdate = _get_prop('EXDATE', item) rrule = exdate and '%s\nEXDATE:%s' % (rrule, exdate.to_ical()) or '' attendees = _get_prop('ATTENDEE', item) contact = _get_prop('CONTACT', item) categories = _get_prop('CATEGORIES', item) if hasattr(categories, '__iter__'): categories = [safe_unicode(it) for it in categories] # for sync created = _get_prop('CREATED', item) modified = _get_prop('LAST-MODIFIED', item) # TODO: better use plone.api, from which some of the code here is # copied content_id = str(random.randint(0, 99999999)) # TODO: if AT had the same attrs like IDXEventBase, we could set # everything within this invokeFactory call. container.invokeFactory(event_type, id=content_id, title=title, description=description) content = container[content_id] event = IEventAccessor(content) 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 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() if 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) transaction.savepoint(optimistic=True) # Commit before renaming content.aq_parent.manage_renameObject(content_id, new_id) else: transaction.savepoint(optimistic=True) 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 test_event_accessor(self): utc = pytz.utc vienna = pytz.timezone('Europe/Vienna') self.portal.invokeFactory('Event', 'event1', description='a description', startDate=datetime(2011, 11, 11, 11, 0, tzinfo=utc), endDate=datetime(2011, 11, 11, 12, 0, tzinfo=utc), timezone='UTC', wholeDay=False) e1 = self.portal['event1'] acc = IEventAccessor(e1) # TEST DATES self.assertEqual(acc.start, datetime(2011, 11, 11, 11, 0, tzinfo=utc)) self.assertEqual(acc.end, datetime(2011, 11, 11, 12, 0, tzinfo=utc)) acc.start = datetime(2011, 11, 13, 9, 0) # tzinfo does not matter, acc.end = datetime(2011, 11, 13, 10, 0) # it's set by subscription # adapter # If using EventAccessor's edit method, calling notify isn't needed acc.edit(timezone=u'Europe/Vienna') # accessor should return start/end datetimes in the event's timezone self.assertEqual( acc.start, datetime(2011, 11, 13, 9, 0, tzinfo=vienna)) self.assertEqual( acc.end, datetime(2011, 11, 13, 10, 0, tzinfo=vienna)) # start/end dates are stored in UTC zone on the context, but converted # to event's timezone via the attribute getter. self.assertEqual( e1.end(), DateTime('2011/11/13 10:00:00 Europe/Vienna') ) # timezone should be the same on the event object and accessor self.assertEqual(e1.getTimezone(), acc.timezone) # Open End Test acc.edit(open_end=True) self.assertEqual( acc.start, datetime(2011, 11, 13, 9, 0, tzinfo=vienna)) self.assertEqual( acc.end, datetime(2011, 11, 13, 23, 59, 59, tzinfo=vienna)) # Whole Day Test acc.edit(whole_day=True, open_end=False) self.assertEqual( acc.start, datetime(2011, 11, 13, 0, 0, tzinfo=vienna)) self.assertEqual( acc.end, datetime(2011, 11, 13, 23, 59, 59, tzinfo=vienna)) # TEST DESCRIPTION self.assertTrue(acc.description == 'a description') acc.description = 'another desc' self.assertTrue(acc.description == 'another desc') # TEST OTHER PROPERTIES acc.title = u"An Event" acc.recurrence = u'RRULE:FREQ=DAILY;COUNT=5' acc.location = u"Home" acc.attendees = [u'me', u'you'] acc.contact_name = u"Max Mustermann" acc.contact_email = u"*****@*****.**" acc.contact_phone = u"+1234567890" acc.event_url = u"http://plone.org/" acc.subjects = [u"tag1", u"tag2"] acc.text = u"body text with <b>html</b> formating." # If not using EventAccessor's edit method, call notify manually notify(ObjectModifiedEvent(acc.context)) self.assertEqual(acc.recurrence, u'RRULE:FREQ=DAILY;COUNT=5') self.assertEqual(acc.location, u'Home') self.assertEqual(acc.attendees, (u'me', u'you')) self.assertEqual(acc.contact_name, u"Max Mustermann") self.assertEqual(acc.contact_email, u'*****@*****.**') self.assertEqual(acc.contact_phone, u"+1234567890") self.assertEqual(acc.event_url, u"http://plone.org/") self.assertEqual(acc.subjects, (u"tag1", u"tag2")) self.assertEqual(acc.text, u"body text with <b>html</b> formating.") # CLEANUP self.portal.manage_delObjects(['event1'])
def setUp(self): self.request = self.layer['request'] portal = self.layer['portal'] setRoles(portal, TEST_USER_ID, ['Manager']) set_browserlayer(self.request) portal.invokeFactory('Folder', id='events', title=u"Events", Description=u"The portal's Events") portal.events.invokeFactory( 'Event', id='ploneconf2007', title='Plone Conf 2007', startDate='2007/10/10', endDate='2007/10/12', location='Naples', eventUrl='http://plone.org/events/conferences/2007-naples', attendees=['anne', 'bob', 'cesar']) portal.events.invokeFactory( 'Event', id='ploneconf2008', title='Plone Conf 2008', startDate='2008/10/08', endDate='2008/10/10', location='DC', recurrence= u'RRULE:FREQ=DAILY;COUNT=5\r\nEXDATE:20081011T000000,20081012T000000\r\nRDATE:20081007T000000', eventUrl='http://plone.org/events/conferences/2008-washington-dc') portal.events.invokeFactory( 'plone.app.event.dx.event', id='ploneconf2012', title='Plone Conf 2012', recurrence= u'RRULE:FREQ=DAILY;COUNT=5\r\nEXDATE:20121013T000000,20121014T000000\r\nRDATE:20121009T000000', start=datetime(2012, 10, 10, 8, 0), end=datetime(2012, 10, 10, 18, 0), timezone='Europe/Amsterdam') pc12 = IEventAccessor(portal.events.ploneconf2012) pc12.location = 'Arnhem' pc12.contact_name = 'Four Digits' pc12.contact_email = '*****@*****.**' pc12.contact_phone = '+123456789' pc12.event_url = 'http://ploneconf.org' pc12.subjects = [ 'plone', 'conference', ] notify(ObjectModifiedEvent(portal.events.ploneconf2012)) portal.events.invokeFactory('plone.app.event.dx.event', id='artsprint2013', title='Artsprint 2013', start=datetime(2013, 2, 18), end=datetime(2013, 2, 22), whole_day=True, timezone='Europe/Vienna') # Standard Time portal.events.invokeFactory('plone.app.event.dx.event', id='standardtime', title='Standard Time', start=datetime(2013, 12, 24, 12, 0), end=datetime(2013, 12, 29, 12, 0), open_end=True, timezone='Europe/Vienna') portal.invokeFactory("Collection", "collection", title="New Collection", sort_on='start') portal['collection'].setQuery([ { 'i': 'Type', 'o': 'plone.app.querystring.operation.string.is', 'v': 'Event', }, ]) self.portal = portal
def ical_import(container, ics_resource, event_type): cal = icalendar.Calendar.from_ical(ics_resource) events = cal.walk('VEVENT') def _get_prop(prop, item): ret = None 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 = _get_prop('ATTENDEE', item) contact = _get_prop('CONTACT', item) categories = _get_prop('CATEGORIES', item) if hasattr(categories, '__iter__'): categories = [safe_unicode(it) for it in categories] # for sync created = _get_prop('CREATED', item) modified = _get_prop('LAST-MODIFIED', item) # TODO: better use plone.api, from which some of the code here is # copied content_id = str(random.randint(0, 99999999)) # TODO: if AT had the same attrs like IDXEventBase, we could set # everything within this invokeFactory call. container.invokeFactory(event_type, id=content_id, title=title, description=description) content = container[content_id] event = IEventAccessor(content) 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 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() if 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) transaction.savepoint(optimistic=True) # Commit before renaming content.aq_parent.manage_renameObject(content_id, new_id) else: transaction.savepoint(optimistic=True) 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 = 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}