plone.app.event.base — plone.app.event 1.0dev documentation

### Navigation

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

# Source code for plone.app.event.base

    from Acquisition import aq_inner
    from Acquisition import aq_parent
    from DateTime import DateTime
    from Products.CMFCore.utils import getToolByName
    from Products.CMFPlone.i18nl10n import ulocalized_time as orig_ulocalized_time
    from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot
    from Products.CMFPlone.utils import safe_callable
    from calendar import monthrange
    from datetime import date
    from datetime import datetime
    from datetime import timedelta
    from persistent.dict import PersistentDict
    from plone.app.event.interfaces import IEventSettings
    from plone.app.event.interfaces import ISO_DATE_FORMAT
    from plone.app.event.vocabularies import replacement_zones
    from plone.app.layout.navigation.root import getNavigationRootObject
    from plone.event.interfaces import IEvent
    from plone.event.interfaces import IEventAccessor
    from plone.event.interfaces import IEventRecurrence
    from plone.event.interfaces import IRecurrenceSupport
    from plone.event.utils import default_timezone as fallback_default_timezone
    from plone.event.utils import is_date
    from plone.event.utils import is_datetime
    from plone.event.utils import is_same_day
    from plone.event.utils import is_same_time
    from plone.event.utils import pydt
    from plone.event.utils import validated_timezone
    from plone.registry.interfaces import IRegistry
    from zope.annotation.interfaces import IAnnotations
    from zope.component import getUtility
    from zope.component import queryUtility
    from zope.component.hooks import getSite
    from zope.component.interfaces import ISite

    import pytz

    DEFAULT_END_DELTA = 1  # hours
    FALLBACK_TIMEZONE = 'UTC'

    # RETRIEVE EVENTS

    def get_events(context, start=None, end=None, limit=None,
                   ret_mode=1, expand=False,
                   sort='start', sort_reverse=False, **kw):
        """Return all events as catalog brains, possibly within a given
        timeframe.

        :param context: [required] A context object.
        :type context: Content object

        :param start: Date, from which on events should be searched.
        :type start: Python datetime.

        :param end: Date, until which events should be searched.
        :type end: Python datetime

        :param limit: Number of items to be returned.
        :type limit: integer

        :param ret_mode: Return type of search results. These options are
                         available:
                             * 1 (brains): Return results as catalog brains.
                             * 2 (objects): Return results as IEvent and/or
                                            IOccurrence objects.
                             * 3 (accessors): Return results as IEventAccessor
                                              wrapper objects.
        :type ret_mode: integer [1|2|3]

        :param expand: Expand the results to all occurrences (within a timeframe,
                       if given). With this option set to True, the resultset also
                       includes the event's recurrence occurrences and is sorted by
                       the start date.
                       Only available in ret_mode 2 (objects) and 3 (accessors).
        :type expand: boolean

        :param sort: Catalog index id to sort on. Not available with expand=True.
        :type sort: string

        :param sort_reverse: Change the order of the sorting.
        :type sort_reverse: boolean

        :returns: Portal events, matching the search criteria.
        :rtype: catalog brains

        """
        start, end = _prepare_range(context, start, end)

        query = {}
        query['object_provides'] = IEvent.__identifier__

        if 'path' not in kw:
            # limit to the current navigation root, usually (not always) site
            portal = getSite()
            navroot = getNavigationRootObject(context, portal)
            query['path'] = '/'.join(navroot.getPhysicalPath())
        else:
            query['path'] = kw['path']

        if start:
            # All events from start date ongoing:
            # The minimum end date of events is the date from which we search.
            query['end'] = {'query': start, 'range': 'min'}
        if end:
            # All events until end date:
            # The maximum start date must be the date until we search.
            query['start'] = {'query': end, 'range': 'max'}

        # Sorting
        # In expand mode we sort after calculation of recurrences again. But we
        # need to leave this sorting here in place, since no sort definition could
        # lead to arbitrary results when limiting with sort_limit.
        query['sort_on'] = sort
        if sort_reverse:
            query['sort_order'] = 'reverse'

        # No sort_limit here! See below.

        query.update(kw)

        cat = getToolByName(context, 'portal_catalog')
        result = cat(**query)

        # Helper functions
        def _obj_or_acc(obj, ret_mode):
            if ret_mode == 2:
                return obj
            elif ret_mode == 3:
                return IEventAccessor(obj)

        def _get_compare_attr(obj, attr):
            val = getattr(obj, attr, None)
            if safe_callable(val):
                val = val()
            if isinstance(val, DateTime):
                val = pydt(val)
            return val

        if ret_mode in (2, 3) and expand is False:
            result = [_obj_or_acc(it.getObject(), ret_mode) for it in result]
        elif ret_mode in (2, 3) and expand is True:
            exp_result = []
            for it in result:
                obj = it.getObject()
                if IEventRecurrence.providedBy(obj):
                    occurrences = [_obj_or_acc(occ, ret_mode) for occ in
                                   IRecurrenceSupport(obj).occurrences(start, end)]
                else:
                    occurrences = [_obj_or_acc(obj, ret_mode)]
                exp_result += occurrences
            if sort:
                # support AT and DX without wrapped by IEventAccessor (mainly for
                # sorting after "start" or "end").
                exp_result.sort(key=lambda x: _get_compare_attr(x, sort))
            if sort_reverse:
                exp_result.reverse()
            result = exp_result

        if limit:
            # Limiting the result set can only happen here, after possibly exanding
            # the result set with it's occurrences.
            # Otherwise we might get wrong results - see :
            # p.a.event.tests.test_base_module.TestGetEventsDX.test_get_event_limit
            result = result[:limit]

        return result

    def construct_calendar(events, start=None, end=None):
        """Return a dictionary with dates in a given timeframe as keys and the
        actual occurrences for that date for building calendars.
        Long lasting events will occur on every day until their end.

        :param events: List of IEvent and/or IOccurrence objects, to construct a
                       calendar data structure from.
        :type events: list

        :param start: An optional start range date.
        :type start: Python datetime or date

        :param end: An optional start range date.
        :type end: Python datetime or date

        :returns: Dictionary with isoformat date strings as keys and event
                  occurrences as values.
        :rtype: dict

        """
        if start:
            if is_datetime(start):
                start = start.date()
            assert is_date(start)
        if end:
            if is_datetime(end):
                end = end.date()
            assert is_date(end)

        cal = {}

        def _add_to_cal(cal_data, event, date):
            date_str = date.isoformat()
            if date_str not in cal_data:
                cal_data[date_str] = [event]
            else:
                cal_data[date_str].append(event)
            return cal_data

        for event in events:
            acc = IEventAccessor(event)
            start_date = acc.start.date()
            end_date = acc.end.date()

            # day span between start and end + 1 for the initial date
            range_days = (end_date - start_date).days + 1
            for add_day in range(range_days):
                next_start_date = start_date + timedelta(add_day)  # initial = 0

                # avoid long loops
                if start and end_date < start:
                    break  # if the date is completly outside the range
                if start and next_start_date <= start:
                    continue  # if start is outside but end reaches into range
                if end and next_start_date > end:
                    break  # if date is outside range

                _add_to_cal(cal, event, next_start_date)
        return cal

    def _prepare_range(context, start, end):
        """Prepare a date-range to contain timezone info and set end to next day,
        if end is a date.

        :param context: [required] Context object.
        :type context: Content object

        :param start: [required] Range start.
        :type start: Python date or datetime

        :param end: [required] Range end.
        :type end: Python date or datetime

        :returns: Localized start and end datetime.
        :rtype: tuple

        """
        tz = default_timezone(context, as_tzinfo=True)
        start = pydt(start, missing_zone=tz)
        if is_date(end):
            # set range_end to the next day, time will be 0:00
            # so the whole previous day is also used for search
            end = end + timedelta(days=1)
        end = pydt(end, missing_zone=tz)
        return start, end

    # TIMEZONE HANDLING

    def default_timezone(context=None, as_tzinfo=False):
        """Return the timezone from the portal or user.

        :param context: Optional context. If not given, the current Site is used.
        :type context: Content object

        :param as_tzinfo: Return the default timezone as tzinfo object.
        :type as_tzinfo: boolean

        :returns: Timezone identifier or tzinfo object.
        :rtype: string or tzinfo object

        """
        # TODO: test member timezone
        if not context:
            context = getSite()

        membership = getToolByName(context, 'portal_membership', None)
        if membership and not membership.isAnonymousUser():  # user not logged in
            member = membership.getAuthenticatedMember()
            member_timezone = member.getProperty('timezone', None)
            if member_timezone:
                info = pytz.timezone(member_timezone)
                return info if as_tzinfo else info.zone

        portal_timezone = None
        reg = queryUtility(IRegistry, context=context, default=None)
        if reg:
            portal_timezone = reg.forInterface(
                IEventSettings,
                prefix="plone.app.event",
                check=False  # Don't fail, if portal_timezone isn't set.
            ).portal_timezone

        # fallback to what plone.event is doing
        if not portal_timezone:
            portal_timezone = fallback_default_timezone()

        # Change any ambiguous timezone abbreviations to their most common
        # non-ambigious timezone name.
        if portal_timezone in replacement_zones.keys():
            portal_timezone = replacement_zones[portal_timezone]
        portal_timezone = validated_timezone(portal_timezone, FALLBACK_TIMEZONE)

        if as_tzinfo:
            return pytz.timezone(portal_timezone)

        return portal_timezone

    def localized_now(context=None):
        """Return the current datetime localized to the default timezone.

        :param context: Context object.
        :type context: Content object
        :returns: Localized current datetime.
        :rtype: Python datetime

        """
        if not context:
            context = getSite()
        tzinfo = default_timezone(context=context, as_tzinfo=True)
        return datetime.now(tzinfo).replace(microsecond=0)

    def localized_today(context=None):
        """Return the current date localized to the default timezone.

        :param context: Context object.
        :type context: Content object
        :returns: Localized current date.
        :rtype: Python date

        """
        now = localized_now(context)
        return date(now.year, now.month, now.day)

    # DATETIME HELPERS

    def first_weekday():
        """Returns the number of the first Weekday in a Week, as defined in
        the registry. 0 is Monday, 6 is Sunday, as expected by Python's datetime.

        PLEASE NOTE: strftime %w interprets 0 as Sunday unlike the calendar module!

        :returns: Index of first weekday [0(Monday)..6(Sunday)]
        :rtype: integer

        """
        controlpanel = getUtility(IRegistry).forInterface(IEventSettings,
                                                          prefix="plone.app.event")
        first_wd = controlpanel.first_weekday
        if not first_wd:
            return 0
        else:
            return int(first_wd)

    def wkday_to_mon0(day):
        """Converts an integer weekday number to a representation where Monday is 0
        and Sunday is 6 (the datetime default), from a representation where Sunday
        is 0, Monday is 1 and Saturday is 6 (the strftime behavior).

        :param day: The weekday number [0(Sunday)..6]
        :type day: integer

        :returns: The weekday number [0(Monday)..6]
        :rtype: integer

        """
        if day == 0:
            return 6
        else:
            return day - 1

    def wkday_to_mon1(day):
        """Converts an integer weekday number to a representation where Monday is
        1, Saturday is 6 and Sunday is 0 (the strftime behavior), from a
        representation where Monday is 0 and Sunday is 6 (the datetime default).

        :param day: The weekday number [0(Monday)..6]
        :type day: integer

        :returns: The weekday number [0(Sunday)..6]
        :rtype: integer

        """
        if day == 6:
            return 0
        else:
            return day + 1

    def DT(dt, exact=False):
        """Return a Zope DateTime instance from a Python datetime instance.

        :param dt: Python datetime, Python date, Zope DateTime instance or string.
        :param exact: If True, the resolution goes down to microseconds. If False,
                      the resolution are seconds. Defaul is False.
        :type exact: Boolean
        :returns: Zope DateTime
        :rtype: Zope DateTime

        """

        def _adjust_DT(DT, exact):
            if exact:
                ret = DT
            else:
                ret = DateTime(
                    DT.year(),
                    DT.month(),
                    DT.day(),
                    DT.hour(),
                    DT.minute(),
                    int(DT.second()),
                    DT.timezone()
                )
            return ret

        tz = default_timezone(getSite())
        ret = None
        if is_datetime(dt):
            zone_id = getattr(dt.tzinfo, 'zone', tz)
            tz = validated_timezone(zone_id, tz)
            second = dt.second
            if exact:
                second += dt.microsecond / 1000000.0
            ret = DateTime(
                dt.year, dt.month, dt.day,
                dt.hour, dt.minute, second,
                tz
            )
        elif is_date(dt):
            ret = DateTime(dt.year, dt.month, dt.day, 0, 0, 0, tz)
        elif isinstance(dt, DateTime):
            # No timezone validation. DateTime knows how to handle it's zones.
            ret = _adjust_DT(dt, exact=exact)
        else:
            # Try to convert by DateTime itself
            ret = _adjust_DT(DateTime(dt), exact=exact)
        return ret

    def guess_date_from(datestr, context=None):
        """Returns a timezone aware date object if an arbitrary ASCII string is
        formatted in an ISO date format, otherwise None is returned.

        Used for traversing and Occurence ids.

        :param datestr: Date string in an ISO format.
        :type datestr: string
        :param context: Context object (for retrieving the timezone).
        :type context: Content object
        :returns: Localized date object.
        :rtype: Python date

        """
        try:
            dateobj = datetime.strptime(datestr, ISO_DATE_FORMAT)
        except ValueError:
            return

        ret = pytz.timezone(default_timezone(context)).localize(dateobj)
        return ret

    def dt_start_of_day(dt):
        """Returns a Python datetime instance set to the start time of the given
        day (00:00:00).

        :param dt: datetime to set to the start time of the day.
        :type dt: Python datetime
        :returns: datetime set to the start time of the day (00:00:00).
        :rtype: Python datetime

        """
        if not isinstance(dt, datetime):
            # is a date
            dt = datetime.fromordinal(dt.toordinal())
        return dt.replace(hour=0, minute=0, second=0, microsecond=0)

    def dt_end_of_day(dt):
        """Returns a Python datetime instance set to the end time of the given day
        (23:59:59).

        :param dt: datetime to set to the end time of the day.
        :type dt: Python datetime
        :returns: datetime set to the end time of the day (23:59:59).
        :rtype: Python datetime

        """
        if not isinstance(dt, datetime):
            # is a date
            dt = datetime.fromordinal(dt.toordinal())
        return dt.replace(hour=23, minute=59, second=59, microsecond=0)

    def start_end_from_mode(mode, dt=None, context=None):
        """Return a start and end date from a given mode string, like
        "today", "past" or "future". This can be used in event retrieval
        functions.

        :param mode: One of the following modes:
                        'all' Show all events.
                        'past': Show only past events with descending sorting.
                        'future': Show only future events (default).
                        'today': Show todays events.
                        'now': Show todays upcoming events.
                        '7days': Show events until 7 days in future.
                        'day': Return all events on the given day (dt parameter
                               required)
                        'week': Show a weeks events, optionally from a given date.
                     These settings override the start and end parameters.
                        Not implemented yet:
                        'month': Show this month's events.
        :type mode: string

        :param dt: Optional datetime for day mode.
        :type dt: Python datetime

        """
        if not context:
            context = getSite()
        now = localized_now(context)
        start = end = None

        if mode == 'all':
            start = None
            end = None

        elif mode == 'past':
            start = None
            end = now

        elif mode == 'future':
            start = now
            end = None

        elif mode == 'now':
            start = now
            end = dt_end_of_day(now)

        elif mode == '7days':
            start = now
            end = dt_end_of_day(now + timedelta(days=6))

        elif mode == 'day' or mode == 'today':
            if not dt:
                dt = now  # show today
            start = dt_start_of_day(dt)
            end = dt_end_of_day(dt)

        elif mode == 'week':
            if not dt:
                dt = now  # show this week
            wkd = dt.weekday()
            first = first_weekday()

            if first <= wkd:
                delta = wkd - first  # >= 0
            if first > wkd:
                delta = wkd + 7 - first  # > 0

            start = dt_start_of_day(dt - timedelta(days=delta))
            end = dt_end_of_day(start + timedelta(days=6))

        elif mode == 'month':
            if not dt:
                dt = now  # show this month
            year = dt.year
            month = dt.month
            last_day = monthrange(year, month)[1]  # (wkday, days)
            start = dt_start_of_day(datetime(year, month, 1))
            end = dt_end_of_day(datetime(year, month, last_day))

        return start, end

    # DISPLAY HELPERS

    def dates_for_display(occurrence):
        """ Return a dictionary containing pre-calculated information for building
        <start>-<end> date strings.

        Keys are:
            'start_date' - date string of the start date
            'start_time' - time string of the start date
            'end_date'   - date string of the end date
            'end_time'   - time string of the end date
            'start_iso'  - start date in iso format
            'end_iso'    - end date in iso format
            'same_day'   - event ends on the same day
            'same_time'  - event ends at same time
            'whole_day'  - whole day events
            'open_end'   - events without end time

        The behavior os ulocalized_time() with time_only is odd.
        Setting time_only=False should return the date part only and *not*
        the time

        >>> from DateTime import DateTime
        >>> start = DateTime(2010,3,16,14,40)
        >>> from zope.componen.hooks import getSite
        >>> site = getSite()
        >>> ulocalized_time(start, False,  time_only=True, context=site)
        u'14:40'
        >>> ulocalized_time(start, False,  time_only=False, context=site)
        u'14:40'
        >>> ulocalized_time(start, False,  time_only=None, context=site)
        u'16.03.2010'

        """
        acc = IEventAccessor(occurrence)

        # this needs to separate date and time as ulocalized_time does
        DT_start = DT(acc.start)
        DT_end = DT(acc.end)
        start_date = ulocalized_time(
            DT_start, long_format=False, time_only=None, context=occurrence
        )
        start_time = ulocalized_time(
            DT_start, long_format=False, time_only=True, context=occurrence
        )
        end_date = ulocalized_time(
            DT_end, long_format=False, time_only=None, context=occurrence
        )
        end_time = ulocalized_time(
            DT_end, long_format=False, time_only=True, context=occurrence
        )

        same_day = is_same_day(acc.start, acc.end)
        same_time = is_same_time(acc.start, acc.end)

        # set time fields to None for whole day events
        if acc.whole_day:
            start_time = end_time = None
        if acc.open_end:
            end_time = None

        start_iso = acc.whole_day and acc.start.date().isoformat()\
            or acc.start.isoformat()
        end_iso = acc.whole_day and acc.end.date().isoformat()\
            or acc.end.isoformat()

        return dict(
            # Start
            start_date=start_date,
            start_time=start_time,
            start_iso=start_iso,

            # End
            end_date=end_date,
            end_time=end_time,
            end_iso=end_iso,

            # Meta
            same_day=same_day,
            same_time=same_time,
            whole_day=acc.whole_day,
            open_end=acc.open_end,
        )

    def date_speller(context, dt):
        """Return a dictionary with localized and readably formatted date parts.

        """
        dt = DT(dt)
        util = getToolByName(context, 'translation_service')
        dom = 'plonelocales'

        def zero_pad(num):
            return '%02d' % num

        date_dict = dict(
            year=dt.year(),

            month=util.translate(
                util.month_msgid(dt.month()),
                domain=dom, context=context
            ),

            month_abbr=util.translate(
                util.month_msgid(dt.month(), 'a'),
                domain=dom, context=context
            ),

            wkday=util.translate(
                util.day_msgid(dt.dow()),
                domain=dom, context=context
            ),

            wkday_abbr=util.translate(
                util.day_msgid(dt.dow(), 'a'),
                domain=dom, context=context
            ),

            day=dt.day(),
            day2=zero_pad(dt.day()),

            hour=dt.hour(),
            hour2=zero_pad(dt.hour()),

            minute=dt.minute(),
            minute2=zero_pad(dt.minute()),

            second=dt.second(),
            second2=zero_pad(dt.second())
        )
        return date_dict

    def default_start(context=None):
        """Return the default start as python datetime for prefilling forms.

        :returns: Default start datetime.
        :rtype: Python datetime

        """
        now = localized_now(context=context)
        return now.replace(minute=0, second=0, microsecond=0)

    def default_end(context=None):
        """Return the default end as python datetime for prefilling forms.

        :returns: Default end datetime.
        :rtype: Python datetime

        """
        return default_start(context=context) + timedelta(hours=DEFAULT_END_DELTA)

    # General utils
    # TODO: Better fits to CMFPlone. (Taken from CMFPlone's new syndication tool)

    class AnnotationAdapter(object):
        """Abstract Base Class for an annotation storage.

        If the annotation wasn't set, it won't be created until the first attempt
        to set a property on this adapter.
        So, the context doesn't get polluted with annotations by accident.

        """
        ANNOTATION_KEY = None

        def __init__(self, context):
            self.context = context
            annotations = IAnnotations(context)
            self._data = annotations.get(self.ANNOTATION_KEY, None)

        def __setattr__(self, name, value):
            if name in ('context', '_data', 'ANNOTATION_KEY'):
                self.__dict__[name] = value
            else:
                if self._data is None:
                    self._data = PersistentDict()
                    annotations = IAnnotations(self.context)
                    annotations[self.ANNOTATION_KEY] = self._data
                self._data[name] = value

        def __getattr__(self, name):
            return self._data and self._data.get(name, None) or None

    def find_context(context, viewname=None, iface=None,
                     as_url=False, append_view=True):
        """Find the next context with a given view name or interface, up in the
        content tree, starting from the given context. This might not be the
        IPloneSiteRoot, but another subsite.

        :param context: The context to start the search from.
        :param viewname: (optional) The name of a view which a context should have
                         configured as defaultView.
        :param iface: (optional) The interface, the context to search for should
                      implement.
        :param as_url: (optional) Return the URL of the context found.
        :param append_view: (optional) In case of a given viewname and called with
                            as_url, append the viewname to the url, if the context
                            hasn't configured it as defaultView. Otherwise ignore
                            this parameter.
        :returns: A context with the given view name, inteface or ISite root.
        """
        context = aq_inner(context)
        ret = None
        if viewname and context.defaultView() == viewname\
           or iface and iface.providedBy(context)\
           or IPloneSiteRoot.providedBy(context):
            # Search for viewname or interface but stop at IPloneSiteRoot
            ret = context
        else:
            ret = find_context(aq_parent(context), viewname=viewname, iface=iface,
                               as_url=False, append_view=False)
        if as_url:
            url = ret.absolute_url()
            if viewname and append_view and ret.defaultView() != viewname:
                url = '%s/%s' % (url, viewname)
            return url
        return ret

    def find_site(context, as_url=False):
        return find_context(context, iface=ISite, as_url=as_url)

    def find_ploneroot(context, as_url=False):
        return find_context(context, iface=IPloneSiteRoot, as_url=as_url)

    def find_event_listing(context, as_url=False):
        return find_context(context, viewname='event_listing', iface=ISite,
                            as_url=as_url, append_view=True)

    # Workaround for buggy strftime with timezone handling in DateTime.
    # See: https://github.com/plone/plone.app.event/pull/47
    # TODO: should land in CMFPlone or fixed in DateTime.
    _strftime = lambda v, fmt: pydt(v).strftime(fmt)

    class PatchedDateTime(DateTime):
        def strftime(self, fmt):
            return _strftime(self, fmt)

    def ulocalized_time(time, *args, **kwargs):
        """Corrects for DateTime bugs doing wrong thing with timezones"""
        wrapped_time = PatchedDateTime(time)
        return orig_ulocalized_time(wrapped_time, *args, **kwargs)

### Navigation

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