# TODO:
# - implement sync strategies,
# - cleanup,
# - tests
from Products.CMFPlone.utils import safe_unicode
from plone.app.event import base
from plone.event.interfaces import IEventAccessor
from plone.event.utils import date_to_datetime
from plone.event.utils import is_date
from plone.event.utils import is_datetime
from zope.container.interfaces import INameChooser
from zope.event import notify
from zope.lifecycleevent import ObjectModifiedEvent
import datetime
import icalendar
import random
import transaction
import urllib2
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}
from zope.interface import Interface
from zope import schema
from plone.app.event import messageFactory as _
from plone.namedfile.field import NamedFile
class IIcalendarImportSettings(Interface):
event_type = schema.Choice(
title=_(u'Event Type'),
vocabulary='plone.app.event.EventTypes',
required=True
)
ical_url = schema.URI(
title=_(u'Icalendar URL'),
required=False
)
ical_file = NamedFile(
title=_(u"Icalendar File"),
required=False
)
# TODO: to implement
#sync_strategy = schema.Choice(
# title=_(u"Synchronization Strategy"),
# vocabulary='plone.app.event.SynchronizationStrategies',
# required=True
#)
from zope.component import adapts
from zope.interface import implements
from plone.app.event.base import AnnotationAdapter
#from plone.folder.interfaces import IFolder
[docs]class IcalendarImportSettings(AnnotationAdapter):
"""Annotation Adapter for IIcalendarImportSettings.
"""
implements(IIcalendarImportSettings)
adapts(Interface)
#adapts(IFolder) ## ?? TODO: when adapting this in z3c.form, why is a
## ATFolder not adaptable to this adapter, when it
## implements IFolder?
ANNOTATION_KEY = "icalendar_import_settings"
from Products.Five.browser import BrowserView
from plone.folder.interfaces import IFolder
from plone.app.event.interfaces import IICalendarImportEnabled
class IcalendarImportTool(BrowserView):
@property
def available(self):
return IFolder.providedBy(self.context)
@property
def available_disabled(self):
return self.available and not self.enabled
@property
def enabled(self):
return IICalendarImportEnabled.providedBy(self.context)
from Products.statusmessages.interfaces import IStatusMessage
from z3c.form import button
from z3c.form import form, field
from zope.interface import alsoProvides, noLongerProvides
class IcalendarImportSettingsForm(form.Form):
fields = field.Fields(IIcalendarImportSettings)
ignoreContext = False
#TODO: needed?
def updateWidgets(self):
super(IcalendarImportSettingsForm, self).updateWidgets()
def getContent(self):
data = {}
settings = IIcalendarImportSettings(self.context)
data['event_type'] = settings.event_type
data['ical_url'] = settings.ical_url
#data['sync_strategy'] = settings.sync_strategy
return data
@button.buttonAndHandler(u'Save and Import')
def handleSave(self, action):
data, errors = self.extractData()
if errors:
return False
settings = IIcalendarImportSettings(self.context)
ical_file = data['ical_file']
if ical_file:
# File upload is not saved in settings
ical_resource = ical_file.data
ical_import_from = ical_file.filename
else:
ical_url = settings.ical_url = data['ical_url']
ical_resource = urllib2.urlopen(ical_url, 'rb').read()
ical_import_from = ical_url
event_type = settings.event_type = data['event_type']
import_metadata = ical_import(
self.context,
ics_resource=ical_resource,
event_type=event_type
)
count = import_metadata['count']
IStatusMessage(self.request).addStatusMessage(
"%s events imported from %s" % (count, ical_import_from),
'info')
self.request.response.redirect(self.context.absolute_url())
@button.buttonAndHandler(u'Cancel')
def handleCancel(self, action):
self.request.response.redirect(self.context.absolute_url())
from plone.z3cform.layout import FormWrapper
class IcalendarImportSettingsFormView(FormWrapper):
form = IcalendarImportSettingsForm
def enable(self):
"""Enable icalendar import on this context.
"""
alsoProvides(self.context, IICalendarImportEnabled)
self.context.reindexObject(idxs=('object_provides'))
self.request.response.redirect(self.context.absolute_url())
def disable(self):
"""Disable icalendar import on this context.
"""
noLongerProvides(self.context, IICalendarImportEnabled)
self.context.reindexObject(idxs=('object_provides'))
self.request.response.redirect(self.context.absolute_url())