plone.app.event.at.content — plone.app.event 1.0dev documentation

### Navigation

- index
- modules |
- plone.app.event 1.0dev documentation »
- Module code »

# Source code for plone.app.event.at.content

    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')

    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
        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
        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
        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)

    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

    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

    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))

### Navigation

- index
- modules |
- plone.app.event 1.0dev documentation »
- Module code »