from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.Archetypes.interfaces import IObjectPostValidation
from Products.ATContentTypes.configuration import zconf
from Products.ATContentTypes.content.base import ATCTContent
from Products.ATContentTypes.content.base import registerATCT
from Products.ATContentTypes.content.schemata import ATContentTypeSchema
from Products.ATContentTypes.content.schemata import finalizeATCTSchema
from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin
from Products.CMFCore.permissions import ModifyPortalContent
from Products.CMFCore.permissions import View
from Products.CMFPlone.utils import safe_unicode
from plone.app.event import messageFactory as _
from plone.app.event.at import atapi
from plone.app.event.at import packageName
from plone.app.event.at.interfaces import IATEvent, IATEventRecurrence
from plone.app.event.base import DT
from plone.app.event.base import default_end as default_end_dt
from plone.app.event.base import default_start as default_start_dt
from plone.app.event.base import default_timezone
from plone.app.event.base import first_weekday
from plone.app.event.base import wkday_to_mon1
from plone.event.interfaces import IEvent
from plone.event.interfaces import IEventAccessor
from plone.event.utils import pydt
from plone.event.utils import utc
from plone.formwidget.datetime.at import DatetimeWidget
from plone.formwidget.recurrence.at.widget import RecurrenceWidget
from plone.uuid.interfaces import IUUID
from zope.component import adapts
from zope.event import notify
from zope.interface import implements
from zope.lifecycleevent import ObjectModifiedEvent
def default_start():
return DT(default_start_dt())
def default_end():
return DT(default_end_dt())
def first_weekday_sun0():
return wkday_to_mon1(first_weekday())
ATEventSchema = ATContentTypeSchema.copy() + atapi.Schema((
atapi.DateTimeField(
'startDate',
required=True,
searchable=False,
accessor='start',
write_permission=ModifyPortalContent,
default_method=default_start,
languageIndependent=True,
widget=DatetimeWidget(
label=_(
u'label_event_start',
default=u'Event Starts'
),
description=_(
u'help_event_start',
default=u"Date and Time, when the event begins."
),
with_time=1,
first_day=first_weekday_sun0,
),
),
atapi.DateTimeField(
'endDate',
required=True,
searchable=False,
accessor='end',
write_permission=ModifyPortalContent,
default_method=default_end,
languageIndependent=True,
widget=DatetimeWidget(
label=_(
u'label_event_end',
default=u'Event Ends'
),
description=_(
u'help_event_end',
default=u"Date and Time, when the event ends."
),
with_time=1,
first_day=first_weekday_sun0,
),
),
atapi.BooleanField(
'wholeDay',
required=False,
default=False,
write_permission=ModifyPortalContent,
languageIndependent=True,
widget=atapi.BooleanWidget(
label=_(
u'label_event_whole_day',
default=u'Whole Day'
),
description=_(
u'help_event_whole_day',
default=u"Event lasts whole day."
),
),
),
atapi.BooleanField(
'openEnd',
required=False,
default=False,
write_permission=ModifyPortalContent,
widget=atapi.BooleanWidget(
label=_(
u'label_event_open_end',
default=u"Open End"
),
description=_(
u'help_event_open_end',
default=u"This event is open ended."
),
),
),
atapi.StringField(
'timezone',
required=True,
searchable=False,
languageIndependent=True,
vocabulary_factory=u"plone.app.event.AvailableTimezones",
enforceVocabulary=True,
default_method=default_timezone,
widget=atapi.SelectionWidget(
label=_(
u'label_event_timezone',
default=u"Timezone"
),
description=_(
u'help_event_timezone',
default=u"Select the Timezone, where this event happens."
),
),
),
atapi.StringField(
'recurrence',
languageIndependent=True,
write_permission=ModifyPortalContent,
validators=('isRecurrence',),
widget=RecurrenceWidget(
label=_(
u'label_event_recurrence',
default=u'Recurrence'
),
description=_(
u'help_event_recurrence',
default='Define the event recurrence rule.'
),
startFieldYear='startDate-year',
startFieldMonth='startDate-month',
startFieldDay='startDate-day',
first_day=first_weekday_sun0,
),
),
atapi.StringField(
'location',
searchable=True,
write_permission=ModifyPortalContent,
widget=atapi.StringWidget(
label=_(
u'label_event_location',
default=u'Location'
),
description=_(
u'help_event_location',
default=u"Location of the event."
),
),
),
atapi.LinesField(
'attendees',
languageIndependent=True,
searchable=True,
write_permission=ModifyPortalContent,
widget=atapi.LinesWidget(
label=_(
u'label_event_attendees',
default=u'Attendees'
),
description=_(
u'help_event_attendees',
default=u'List of attendees.'
),
),
),
atapi.StringField(
'contactName',
required=False,
searchable=True,
accessor='contact_name',
write_permission=ModifyPortalContent,
widget=atapi.StringWidget(
label=_(
u'label_event_contact_name',
default=u'Contact Name'
),
description=_(
u'help_event_contact_name',
default=u'Name of a person to contact about this event.'
),
),
),
atapi.StringField(
'contactEmail',
required=False,
searchable=True,
accessor='contact_email',
write_permission=ModifyPortalContent,
validators=('isEmail',),
widget=atapi.StringWidget(
label=_(
u'label_event_contact_email',
default=u'Contact E-mail'
),
description=_(
u'help_event_contact_email',
default=u'Email address to contact about this event.'
),
),
),
atapi.StringField(
'contactPhone',
required=False,
searchable=True,
accessor='contact_phone',
write_permission=ModifyPortalContent,
validators=(),
widget=atapi.StringWidget(
label=_(
u'label_event_contact_phone',
default=u'Contact Phone'
),
description=_(
u'help_event_contact_phone',
default=u'Phone number to contact about this event.'
),
),
),
atapi.StringField(
'eventUrl',
required=False,
searchable=True,
accessor='event_url',
write_permission=ModifyPortalContent,
validators=('isURL',),
widget=atapi.StringWidget(
label=_(
u'label_event_url',
default=u'Event URL'
),
description=_(
u'help_event_url',
default=u"Web address with more info about the event. "
u"Add http:// for external links."
),
),
),
atapi.TextField(
'text',
required=False,
searchable=True,
primary=True,
storage=atapi.AnnotationStorage(migrate=True),
validators=('isTidyHtmlWithCleanup',),
default_output_type='text/x-html-safe',
widget=atapi.RichWidget(
label=_(
u'label_event_announcement',
default=u'Event body text'
),
description=_(
u'help_event_announcement',
default=u''
),
rows=25,
allow_file_upload=zconf.ATDocument.allow_document_upload
),
),
), marshall=atapi.RFC822Marshaller())
# Repurpose the subject field for the event type
ATEventSchema.moveField('subject', before='eventUrl')
ATEventSchema['subject'].write_permission = ModifyPortalContent
ATEventSchema['subject'].widget.size = 6
ATEventSchema.changeSchemataForField('subject', 'default')
ATEventSchema.changeSchemataForField('timezone', 'dates')
ATEventSchema.moveField('timezone', before='effectiveDate')
finalizeATCTSchema(ATEventSchema)
# finalizeATCTSchema moves 'location' into 'categories', we move it back:
ATEventSchema.changeSchemataForField('location', 'default')
ATEventSchema.moveField('location', before='attendees')
[docs]class ATEvent(ATCTContent, HistoryAwareMixin):
"""Information about an upcoming event, which can be displayed in the
calendar.
"""
implements(IATEvent, IATEventRecurrence)
schema = ATEventSchema
security = ClassSecurityInfo()
portal_type = archetype_name = 'Event'
cmf_edit_kws = ('effectiveDay', 'effectiveMo', 'effectiveYear',
'expirationDay', 'expirationMo', 'expirationYear',
'start_time', 'startAMPM', 'stop_time', 'stopAMPM',
'start_date', 'end_date', 'contact_name', 'contact_email',
'contact_phone', 'event_url')
security.declarePrivate('cmf_edit')
def cmf_edit(
self, title=None, description=None, effectiveDay=None,
effectiveMo=None, effectiveYear=None, expirationDay=None,
expirationMo=None, expirationYear=None, start_date=None,
start_time=None, startAMPM=None, end_date=None,
stop_time=None, stopAMPM=None, location=None,
contact_name=None, contact_email=None, contact_phone=None,
event_url=None):
if effectiveDay and effectiveMo and effectiveYear and start_time:
sdate = '%s-%s-%s %s %s' % (
effectiveDay, effectiveMo, effectiveYear,
start_time, startAMPM
)
elif start_date:
if not start_time:
start_time = '00:00:00'
sdate = '%s %s' % (start_date, start_time)
else:
sdate = None
if expirationDay and expirationMo and expirationYear and stop_time:
edate = '%s-%s-%s %s %s' % (expirationDay, expirationMo,
expirationYear, stop_time, stopAMPM)
elif end_date:
if not stop_time:
stop_time = '00:00:00'
edate = '%s %s' % (end_date, stop_time)
else:
edate = None
if sdate and edate:
if edate < sdate:
edate = sdate
self.setStartDate(sdate)
self.setEndDate(edate)
self.update(
title=title, description=description, location=location,
contactName=contact_name, contactEmail=contact_email,
contactPhone=contact_phone, eventUrl=event_url)
###
# Timezone / start / end getter / setter
security.declareProtected(ModifyPortalContent, 'setTimezone')
def setTimezone(self, value, **kwargs):
tz = self.getField('timezone').get(self)
if tz:
# The event is edited and not newly created, otherwise the timezone
# info wouldn't exist.
# In order to avoid converting the datetime input to the new target
# zone after changing the zone but to treat user datetime input as
# localized, we have to store the old timezone. This way we can
# restore the datetime input from the context's UTC value before
# applying the new zone in the data_postprocessing event
# subscriber.
self.previous_timezone = tz
self.getField('timezone').set(self, value, **kwargs)
security.declarePrivate('_dt_getter')
def _dt_getter(self, field):
# Always get the date in event's timezone
timezone = self.getField('timezone').get(self)
dt = self.getField(field).get(self)
return dt.toZone(timezone)
security.declarePrivate('_dt_setter')
def _dt_setter(self, fieldtoset, value, **kwargs):
"""Always set the date in UTC, saving the timezone in another field.
But since the timezone value isn't known at the time of saving the
form, we have to save it timezone-naive first and let
timezone_handler convert it to the target zone afterwards.
"""
# Note: The name of the first parameter shouldn't be field, because
# it's already in kwargs in some case.
if not isinstance(value, DateTime):
value = DT(value)
# This way, we set DateTime timezoneNaive
value = DateTime(
'%04d-%02d-%02dT%02d:%02d:%02d' % (
value.year(),
value.month(),
value.day(),
value.hour(),
value.minute(),
int(value.second()) # No microseconds
)
)
self.getField(fieldtoset).set(self, value, **kwargs)
security.declareProtected('View', 'start')
def start(self):
return self._dt_getter('startDate')
security.declareProtected('View', 'end')
def end(self):
return self._dt_getter('endDate')
security.declareProtected(ModifyPortalContent, 'setStartDate')
def setStartDate(self, value, **kwargs):
self._dt_setter('startDate', value, **kwargs)
security.declareProtected(ModifyPortalContent, 'setEndDate')
def setEndDate(self, value, **kwargs):
self._dt_setter('endDate', value, **kwargs)
security.declareProtected(View, 'start_date')
@property
[docs] def start_date(self):
""" Return start date as Python datetime.
:returns: Start of the event.
:rtype: Python datetime
"""
return pydt(self.start(), exact=False)
security.declareProtected(View, 'end_date')
@property
[docs] def end_date(self):
""" Return end date as Python datetime.
Please note, the end date marks only the end of an individual
occurrence and not the end of a recurrence sequence.
:returns: End of the event.
:rtype: Python datetime
"""
return pydt(self.end(), exact=False)
security.declareProtected(View, 'duration')
@property
[docs] def duration(self):
""" Return duration of the event as Python timedelta.
:returns: Duration of the event.
:rtype: Python timedelta
"""
return self.end_date - self.start_date
# TODO: Why is this needed?
#
security.declareProtected(ModifyPortalContent, 'update')
def update(self, event=None, **kwargs):
# Clashes with BaseObject.update, so
# we handle gracefully
info = {}
if event is not None:
for field in event.Schema().fields():
info[field.getName()] = event[field.getName()]
elif kwargs:
info = kwargs
ATCTContent.update(self, **info)
def __cmp__(self, other):
"""Compare method.
If other is based on ATEvent, compare start, duration and title.
#If other is a number, compare duration and number
If other is a DateTime instance, compare start date with date
In all other cases there is no specific order.
"""
# TODO: maybe also include location to compate two events.
# Please note that we can not use self.Title() here: the generated
# edit accessor uses getToolByName, which ends up in
# five.localsitemanager looking for a parent using a comparison
# on this object -> infinite recursion.
if IATEvent.providedBy(other):
return cmp((self.start_date, self.duration, self.title),
(other.start_date, other.duration, other.title))
elif isinstance(other, DateTime):
return cmp(self.start(), other)
else:
# TODO come up with a nice cmp for types
return cmp(self.title, other)
def __hash__(self):
return hash((self.start_date, self.duration, self.title))
registerATCT(ATEvent, packageName)
[docs]class StartEndDateValidator(object):
"""Checks whether startDate is before endDate. In case the event is
openEnded this check is skipped.
"""
implements(IObjectPostValidation)
adapts(IATEvent)
def __init__(self, context):
self.context = context
def __call__(self, request):
rstartDate = request.form.get('startDate', None)
rendDate = request.form.get('endDate', None)
errors = {}
if rstartDate:
try:
start = DateTime(rstartDate)
except:
errors['startDate'] = _(u'error_invalid_start_date',
default=u'Start date is not valid.')
else:
start = self.context.start()
openEnd = request.form.get('openEnd', False)
if openEnd:
# In case the event has an open end, enddate is set automatically
# later and we need not check it
return errors
if rendDate:
try:
end = DateTime(rendDate)
except:
errors['endDate'] = _(u'error_invalid_end_date',
default=u'End date is not valid.')
else:
end = self.context.end()
if 'startDate' in errors or 'endDate' in errors:
# No point in validating bad input
return errors
if start > end:
errors['endDate'] = _(u'error_end_must_be_after_start_date',
default=u'End date must be after start date.')
return errors and errors or None
## Event handlers
[docs]def data_postprocessing(obj, event):
"""When setting the startDate and endDate, the value of the timezone field
isn't known, so we have to convert those timezone-naive dates into
timezone-aware ones afterwards.
For whole day events, set start time to 0:00:00 and end time to 23:59:59.
For open end events, set end time to 23:59:59.
"""
if not IEvent.providedBy(obj):
# don't run me, if i'm not installed
return
timezone = obj.getField('timezone').get(obj)
start_field = obj.getField('startDate')
end_field = obj.getField('endDate')
# The previous_timezone is set, when the timezone has changed to another
# value. In this case we need to convert the UTC dt values to the
# previous_timezone, so that we get the datetime values, as the user
# entered them. However, this value might be always set, even when creating
# an event, since ObjectModifiedEvent is called several times when editing.
prev_tz = getattr(obj, 'previous_timezone', None)
if prev_tz:
delattr(obj, 'previous_timezone')
def _fix_zone(dt, tz):
if not dt.timezoneNaive():
# The object is edited and the value alreadty stored in UTC on the
# object. In this case we want the value converted to the given
# timezone, in which the user entered the data.
dt = dt.toZone(tz)
return dt
start = _fix_zone(start_field.get(obj), prev_tz and prev_tz or timezone)
end = _fix_zone(end_field.get(obj), prev_tz and prev_tz or timezone)
def make_DT(value, timezone):
return DateTime(
value.year(),
value.month(),
value.day(),
value.hour(),
value.minute(),
int(value.second()), # No microseconds
timezone)
start = make_DT(start, timezone)
end = make_DT(end, timezone)
whole_day = obj.getWholeDay()
open_end = obj.getOpenEnd()
if whole_day:
start = DateTime('%s 0:00:00 %s' % (start.Date(), timezone))
if open_end:
end = start # Open end events end on same day
if open_end or whole_day:
end = DateTime('%s 23:59:59 %s' % (end.Date(), timezone))
start_field.set(obj, start.toZone('UTC'))
end_field.set(obj, end.toZone('UTC'))
obj.reindexObject()
## Object adapters
[docs]class EventAccessor(object):
""" Generic event accessor adapter implementation for Archetypes content
objects.
"""
implements(IEventAccessor)
adapts(IATEvent)
event_type = 'Event' # If you use a custom content-type, override this.
def __init__(self, context):
self.context = context
# Unified create method via Accessor
@classmethod
def create(cls, container, content_id, title, description=None,
start=None, end=None, timezone=None, whole_day=None,
open_end=None, **kwargs):
container.invokeFactory(cls.event_type,
id=content_id,
title=title,
description=description,
startDate=start,
endDate=end,
wholeDay=whole_day,
open_end=open_end,
timezone=timezone)
content = container[content_id]
acc = IEventAccessor(content)
acc.edit(**kwargs)
return acc
def edit(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
notify(ObjectModifiedEvent(self.context))
# RO PROPERTIES
@property
def uid(self):
return IUUID(self.context, None)
@property
def url(self):
return safe_unicode(self.context.absolute_url())
@property
def created(self):
return utc(self.context.creation_date)
@property
def last_modified(self):
return utc(self.context.modification_date)
@property
def duration(self):
return self.end - self.start
# rw properties not in behaviors (yet) # TODO revisit
@property
def title(self):
return safe_unicode(getattr(self.context, 'title', None))
@title.setter
def title(self, value):
setattr(self.context, 'title', safe_unicode(value))
@property
def description(self):
return safe_unicode(self.context.Description())
@description.setter
def description(self, value):
self.context.setDescription(safe_unicode(value))
@property
def start(self):
return self.context.start_date
@start.setter
def start(self, value):
self.context.setStartDate(value)
@property
def end(self):
return self.context.end_date
@end.setter
def end(self, value):
self.context.setEndDate(value)
@property
def whole_day(self):
return self.context.getWholeDay()
@whole_day.setter
def whole_day(self, value):
self.context.setWholeDay(value)
@property
def open_end(self):
return self.context.getOpenEnd()
@open_end.setter
def open_end(self, value):
self.context.setOpenEnd(value)
@property
def timezone(self):
return safe_unicode(self.context.getTimezone())
@timezone.setter
def timezone(self, value):
self.context.setTimezone(safe_unicode(value))
@property
def recurrence(self):
return safe_unicode(self.context.getRecurrence())
@recurrence.setter
def recurrence(self, value):
self.context.setRecurrence(safe_unicode(value))
@property
def location(self):
return safe_unicode(self.context.getLocation())
@location.setter
def location(self, value):
self.context.setLocation(safe_unicode(value))
@property
def attendees(self):
return self.context.getAttendees()
@attendees.setter
def attendees(self, value):
if value:
self.context.setAttendees(value)
@property
def contact_name(self):
return safe_unicode(self.context.contact_name())
@contact_name.setter
def contact_name(self, value):
self.context.setContactName(safe_unicode(value))
@property
def contact_email(self):
return safe_unicode(self.context.contact_email())
@contact_email.setter
def contact_email(self, value):
self.context.setContactEmail(safe_unicode(value))
@property
def contact_phone(self):
return safe_unicode(self.context.contact_phone())
@contact_phone.setter
def contact_phone(self, value):
self.context.setContactPhone(safe_unicode(value))
@property
def event_url(self):
return safe_unicode(self.context.event_url())
@event_url.setter
def event_url(self, value):
self.context.setEventUrl(safe_unicode(value))
@property
def subjects(self):
return self.context.Subject()
@subjects.setter
def subjects(self, value):
if value:
self.context.setSubject(value)
@property
def text(self):
return safe_unicode(self.context.getText())
@text.setter
def text(self, value):
self.context.setText(safe_unicode(value))