# HG changeset patch # User Paul Boddie # Date 1396102246 -3600 # Node ID b8e096dd37d9cf351bb7219cc2f1b10315974f86 # Parent 5578b78b2041ec04c482c79d90113558c05324b4# Parent 077ec00b6108acdf059bd998d932c8ccecf993a2 Merged changes from the default branch. diff -r 5578b78b2041 -r b8e096dd37d9 EventAggregatorSupport/Filter.py --- a/EventAggregatorSupport/Filter.py Wed Jan 29 18:24:35 2014 +0100 +++ b/EventAggregatorSupport/Filter.py Sat Mar 29 15:10:46 2014 +0100 @@ -198,8 +198,8 @@ Return a scale for the given coverage so that the times involved are exposed. The scale consists of a list of non-overlapping timespans forming a contiguous period of time, where each timespan is accompanied in a tuple - by a limit and a list of original time details. Thus, the scale consists of - (timespan, limit, set-of-times) tuples. + by a limit and two sets of original time details. Thus, the scale consists + of (timespan, limit, set-of-start-times, set-of-end-times) tuples. """ times = {} @@ -241,7 +241,7 @@ for time, limit in keys: if not first: - scale.append((Timespan(start, time), limit, times[(start, start_limit)])) + scale.append((Timespan(start, time), limit, times[(start, start_limit)], times[(time, limit)])) else: first = 0 start, start_limit = time, limit diff -r 5578b78b2041 -r b8e096dd37d9 EventAggregatorSupport/Resources.py --- a/EventAggregatorSupport/Resources.py Wed Jan 29 18:24:35 2014 +0100 +++ b/EventAggregatorSupport/Resources.py Sat Mar 29 15:10:46 2014 +0100 @@ -11,7 +11,7 @@ from DateSupport import Date, Month from MoinSupport import * -from MoinRemoteSupport import getCachedResource, getCachedResourceMetadata +from MoinRemoteSupport import getCachedResource, getCachedResourceMetadata, imapreader import urllib @@ -96,6 +96,16 @@ if url.startswith("file:"): return None + # Support IMAP access. + + elif url.startswith("imap"): + reader = imapreader + + # Otherwise, use the default access mechanism. + + else: + reader = None + # Parameterise the URL. # Where other parameters are used, care must be taken to encode them # properly. @@ -111,15 +121,15 @@ parser = parseEventsInCalendarFromResource required_content_type = expected_content_type or "text/calendar" elif format == "xcal": - parser = parseEventsInXMLCalendarFromResource - required_content_type = expected_content_type or "application/calendar+xml" + parser = parseEventsInXMLCalendarsFromResource + required_content_type = expected_content_type or "multipart/mixed" else: return None # Obtain the resource, using a cached version if appropriate. max_cache_age = int(getattr(request.cfg, "event_aggregator_max_cache_age", "300")) - data = getCachedResource(request, url, "EventAggregator", "wiki", max_cache_age) + data = getCachedResource(request, url, "EventAggregator", "wiki", max_cache_age, reader) if not data: return None diff -r 5578b78b2041 -r b8e096dd37d9 EventAggregatorSupport/Types.py --- a/EventAggregatorSupport/Types.py Wed Jan 29 18:24:35 2014 +0100 +++ b/EventAggregatorSupport/Types.py Sat Mar 29 15:10:46 2014 +0100 @@ -15,6 +15,7 @@ import vCalendar from codecs import getreader +from email.parser import Parser from email.utils import parsedate import re @@ -131,6 +132,36 @@ else: return None +def parseEventsInXMLCalendarsFromResource(f, encoding=None, url=None, metadata=None): + + """ + Parse a collection of events in xCalendar format from the given file-like + object 'f', with content having any specified 'encoding' and being described + by the given 'url' and 'metadata'. + """ + + new_url = "" # hide the IMAP URL + + message = Parser().parse(f) + resources = EventResourceCollection(new_url, metadata or {}) + + for data in message.get_payload(): + + # Find the calendar data. + + if data.is_multipart(): + for part in data.get_payload(): + if part.get_content_type() == "application/calendar+xml": + text = part + else: + text = data + + # Obtain a calendar and merge it into the collection. + + resources.append(parseEventsInXMLCalendarFromResource(StringIO(text.get_payload(decode=True)), part.get_charset(), new_url)) + + return resources + def parseEvents(text, event_page, fragment=None): """ @@ -239,8 +270,10 @@ "A resource providing event information." - def __init__(self, url): + def __init__(self, url, metadata=None): self.url = url + self.metadata = metadata + self.events = None def getPageURL(self): @@ -262,13 +295,13 @@ "last-comment" made about the last edit. """ - return {} + return self.metadata or {} def getEvents(self): "Return a list of events from this resource." - return [] + return self.events or [] def linkToPage(self, request, text, query_string=None, anchor=None): @@ -291,14 +324,30 @@ return fmt.text(text) +class EventResourceCollection(EventResource): + + "A collection of resources." + + def __init__(self, url, metadata=None): + self.url = url + self.metadata = metadata + self.resources = [] + + def append(self, resource): + self.resources.append(resource) + + def getEvents(self): + events = [] + for resource in self.resources: + events += resource.getEvents() + return events + class EventCalendarResource(EventResource): "A generic calendar resource." def __init__(self, url, metadata): - EventResource.__init__(self, url) - self.metadata = metadata - self.events = None + EventResource.__init__(self, url, metadata) if not self.metadata.has_key("created") and self.metadata.has_key("date"): self.metadata["created"] = DateTime(parsedate(self.metadata["date"])[:7]) @@ -306,16 +355,6 @@ if self.metadata.has_key("last-modified") and not isinstance(self.metadata["last-modified"], DateTime): self.metadata["last-modified"] = DateTime(parsedate(self.metadata["last-modified"])[:7]) - def getMetadata(self): - - """ - Return a dictionary containing items describing the page's "created" - time, "last-modified" time, "sequence" (or revision number) and the - "last-comment" made about the last edit. - """ - - return self.metadata - class EventCalendar(EventCalendarResource): "An iCalendar resource." @@ -324,16 +363,6 @@ EventCalendarResource.__init__(self, url, metadata) self.calendar = calendar - def getMetadata(self): - - """ - Return a dictionary containing items describing the page's "created" - time, "last-modified" time, "sequence" (or revision number) and the - "last-comment" made about the last edit. - """ - - return self.metadata - def getEvents(self): "Return a list of events from this resource." @@ -451,6 +480,8 @@ return self.events + # Parsing methods. + def _getValue(self, values, type): for element in values[0].xpath("xcal:%s" % type, namespaces=self.XCAL): return element.textContent @@ -945,6 +976,8 @@ the absence of any URL in the event details. """ + # NOTE: Redirect empty URLs to an action showing the resource details. + return self.details.get("url") and \ self.valueToString(self.details["url"]) or \ self.page.getPageURL() diff -r 5578b78b2041 -r b8e096dd37d9 EventAggregatorSupport/View.py --- a/EventAggregatorSupport/View.py Wed Jan 29 18:24:35 2014 +0100 +++ b/EventAggregatorSupport/View.py Sat Mar 29 15:10:46 2014 +0100 @@ -2,10 +2,11 @@ """ MoinMoin - EventAggregator user interface library - @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie + @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie @license: GNU GPL (v2 or later), see COPYING.txt for details. """ +from DateSupport import Date from EventAggregatorSupport.Filter import getCalendarPeriod, getEventsInPeriod, \ getCoverage, getCoverageScale from EventAggregatorSupport.Locations import getMapsPage, getLocationsPage, Location @@ -140,9 +141,11 @@ self.first = first self.last = last - self.duration = abs(last - first) + 1 - - if self.calendar_name: + self.initDuration() + + self.showing_everything = not self.calendar_start and not self.calendar_end + + if not self.showing_everything: # Store the view parameters. @@ -156,6 +159,28 @@ self.previous_set_end = last.update(-self.duration) self.next_set_end = last.update(self.duration) + def initDuration(self): + + "Limit the duration of the calendar to prevent excessive output." + + request = self.page.request + + self.duration = abs(self.last - self.first) + 1 + + # Limit to the specified number of units. + + limit = int(getattr(request.cfg, "event_aggregator_max_duration", 31)) + + if self.duration > limit: + if isinstance(self.first, Date): + self.last = self.first.day_update(limit - 1) + else: + self.last = self.first.month_update(limit - 1) + + self.calendar_start = self.calendar_start or self.first + self.calendar_end = self.calendar_end or self.last + self.duration = limit + def getIdentifier(self): "Return a unique identifier to be used to refer to this view." @@ -168,7 +193,10 @@ "Return the 'argname' qualified using the calendar name." - return getQualifiedParameterName(self.calendar_name, argname) + if self.calendar_name: + return getQualifiedParameterName(self.calendar_name, argname) + else: + return argname def getDateQueryString(self, argname, date, prefix=1): @@ -743,56 +771,53 @@ output = [] append = output.append - if self.calendar_name: - calendar_name = self.calendar_name - - # Links to the previous set of months and to a calendar shifted - # back one month. - - previous_set_link = self.getNavigationLink( - self.previous_set_start, self.previous_set_end - ) - previous_link = self.getNavigationLink( - self.previous_start, self.previous_end - ) - previous_set_update_link = self.getUpdateLink( - self.previous_set_start, self.previous_set_end - ) - previous_update_link = self.getUpdateLink( - self.previous_start, self.previous_end - ) - - # Links to the next set of months and to a calendar shifted - # forward one month. - - next_set_link = self.getNavigationLink( - self.next_set_start, self.next_set_end - ) - next_link = self.getNavigationLink( - self.next_start, self.next_end - ) - next_set_update_link = self.getUpdateLink( - self.next_set_start, self.next_set_end - ) - next_update_link = self.getUpdateLink( - self.next_start, self.next_end - ) - - append(fmt.div(on=1, css_class="event-calendar-navigation")) - - append(fmt.span(on=1, css_class="previous")) - append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link, title=_("Previous set"))) - append(fmt.text(" ")) - append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link, title=_("Previous"))) - append(fmt.span(on=0)) - - append(fmt.span(on=1, css_class="next")) - append(linkToPage(request, page, ">", next_link, onclick=next_update_link, title=_("Next"))) - append(fmt.text(" ")) - append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link, title=_("Next set"))) - append(fmt.span(on=0)) - - append(fmt.div(on=0)) + # Links to the previous set of months and to a calendar shifted + # back one month. + + previous_set_link = self.getNavigationLink( + self.previous_set_start, self.previous_set_end + ) + previous_link = self.getNavigationLink( + self.previous_start, self.previous_end + ) + previous_set_update_link = self.getUpdateLink( + self.previous_set_start, self.previous_set_end + ) + previous_update_link = self.getUpdateLink( + self.previous_start, self.previous_end + ) + + # Links to the next set of months and to a calendar shifted + # forward one month. + + next_set_link = self.getNavigationLink( + self.next_set_start, self.next_set_end + ) + next_link = self.getNavigationLink( + self.next_start, self.next_end + ) + next_set_update_link = self.getUpdateLink( + self.next_set_start, self.next_set_end + ) + next_update_link = self.getUpdateLink( + self.next_start, self.next_end + ) + + append(fmt.div(on=1, css_class="event-calendar-navigation")) + + append(fmt.span(on=1, css_class="previous")) + append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link, title=_("Previous set"))) + append(fmt.text(" ")) + append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link, title=_("Previous"))) + append(fmt.span(on=0)) + + append(fmt.span(on=1, css_class="next")) + append(linkToPage(request, page, ">", next_link, onclick=next_update_link, title=_("Next"))) + append(fmt.text(" ")) + append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link, title=_("Next set"))) + append(fmt.span(on=0)) + + append(fmt.div(on=0)) return "".join(output) @@ -812,7 +837,7 @@ output = [] append = output.append - if self.calendar_name: + if not self.showing_everything: # A link leading to this date being at the top of the calendar. @@ -1339,7 +1364,7 @@ day_rows = [] - for period, limit, times in scale: + for period, limit, start_times, end_times in scale: # Ignore timespans before this day. @@ -1363,7 +1388,7 @@ rowspans[event] += 1 day_row.append((location, event)) - day_rows.append((period, day_row, times)) + day_rows.append((period, day_row, start_times, end_times)) # Output the locations. @@ -1393,7 +1418,7 @@ last_period = period = None events_written = set() - for period, day_row, times in day_rows: + for period, day_row, start_times, end_times in day_rows: # Write a heading describing the time. @@ -1402,7 +1427,7 @@ # Show times only for distinct periods. if not last_period or period.start != last_period.start: - append(self.writeDayScaleHeading(times)) + append(self.writeDayScaleHeading(start_times)) else: append(self.writeDayScaleHeading([])) @@ -1432,7 +1457,7 @@ if period is not None: if period.end == date: append(fmt.table_row(on=1)) - append(self.writeDayScaleHeading(times)) + append(self.writeDayScaleHeading(end_times)) for slot in day_row: append(self.writeDaySpacer()) @@ -1679,7 +1704,7 @@ output = [] append = output.append - append(fmt.div(on=1, css_class="event-calendar", id=("EventAggregator-%s" % self.getIdentifier()))) + append(fmt.div(on=1, css_class="event-calendar-region", id=("EventAggregator-%s" % self.getIdentifier()))) # Output download controls. @@ -1687,6 +1712,8 @@ append(self.writeDownloadControls()) append(fmt.div(on=0)) + append(fmt.div(on=1, css_class="event-display")) + # Output a table. if self.mode == "table": @@ -2184,6 +2211,8 @@ append(fmt.table(on=0)) append(fmt.div(on=0)) + append(fmt.div(on=0)) # end of event-display + # Output view controls. append(fmt.div(on=1, css_class="event-controls")) diff -r 5578b78b2041 -r b8e096dd37d9 TO_DO.txt --- a/TO_DO.txt Wed Jan 29 18:24:35 2014 +0100 +++ b/TO_DO.txt Sat Mar 29 15:10:46 2014 +0100 @@ -1,3 +1,25 @@ +Event Invitations and Attendance +-------------------------------- + +iTIP invitations (RFC 5546) could be supported. REQUEST method payloads are +effectively equivalent to plain iCalendar payloads; ADD method payloads are +similar to plain iCalendar payloads but augment previously received data, +whereas CANCEL method payloads remove or retract previously received data; +REFRESH method payloads are minimal requests for complete iCalendar payloads +to be sent in response. Other methods (REPLY, COUNTER, DECLINECOUNTER) update +the state of events according to attendance notifications. + +For iTIP exchanges to work effectively, a mapping of the UID of each event to +the received information needs to be maintained. (An awareness of each +RECURRENCE-ID in an event is also useful where recurring events are being +handled.) Here, a form of index needs to be supported for efficient access via +event UIDs to event data. Other indexes might be supported for efficient +free/busy resource generation. + +The actual sending and receiving of iTIP messages needs to be supported by +other components such as MoinMessage. It might be interesting to support iTIP +notifications if events are changed directly on a wiki. + Navigation Controls ------------------- diff -r 5578b78b2041 -r b8e096dd37d9 css/event-aggregator.css --- a/css/event-aggregator.css Wed Jan 29 18:24:35 2014 +0100 +++ b/css/event-aggregator.css Sat Mar 29 15:10:46 2014 +0100 @@ -6,10 +6,18 @@ ...before any rules. -Copyright (c) 2009, 2010, 2011, 2012, 2013 by Paul Boddie +Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie Licensed under the GNU GPL (v2 or later), see COPYING.txt for details. */ +.event-display { + text-align: center; /* to put the actual calendar details in the centre */ +} + +.event-display > * { + text-align: left; +} + /* Controls. */ .event-controls { @@ -146,9 +154,7 @@ text-align: right; } -/* Calendar view. */ - -.event-calendar { +.event-calendar-navigation { position: relative; } @@ -167,6 +173,8 @@ right: 1em; } +/* Calendar view. */ + .event-month { width: 98%; border-bottom: 1px solid #dddddd; @@ -530,11 +538,12 @@ /* Map view. */ .event-map { - text-align: center; + display: inline-block; /* confines the navigation controls to the map width */ } .event-map table { - display: inline-block; + margin-top: 0; /* confines the navigation controls to the map header */ + text-align: center; } caption.event-map-heading { diff -r 5578b78b2041 -r b8e096dd37d9 macros/EventAggregator.py --- a/macros/EventAggregator.py Wed Jan 29 18:24:35 2014 +0100 +++ b/macros/EventAggregator.py Sat Mar 29 15:10:46 2014 +0100 @@ -41,7 +41,7 @@ mode=map shows a map of events calendar=NAME uses the given NAME to provide request parameters which - can be used to control the calendar view + name=NAME can be used to control the calendar view template=PAGE uses the given PAGE as the default template for new events (or the default template from the configuration @@ -49,6 +49,10 @@ parent=PAGE uses the given PAGE as the parent of any new event page + source=SOURCE uses the given SOURCE to provide events + + search=SEARCH uses the given SEARCH expression to search for events + Calendar view options: names=daily shows the name of an event on every day of that event @@ -61,6 +65,20 @@ page specified in the configuration by the 'event_aggregator_maps_page' setting) along with an attached map image + + Request parameters configured by the calendar argument include the + following: + + start equivalent to the above start argument + end equivalent to the above end argument + + wider-start indicates the start of a view from a wider context + wider-end indicates the end of a view from a wider context + + mode equivalent to the above mode argument + + resolution=month indicates that dates have a month level of precision + resolution=date indicates that dates have a day/date level of precision """ request = macro.request @@ -109,6 +127,9 @@ elif arg.startswith("calendar="): calendar_name = arg[9:] + elif arg.startswith("name="): + calendar_name = arg[5:] + elif arg.startswith("template="): template_name = arg[9:] diff -r 5578b78b2041 -r b8e096dd37d9 pages/HelpOnEventAggregator --- a/pages/HelpOnEventAggregator Wed Jan 29 18:24:35 2014 +0100 +++ b/pages/HelpOnEventAggregator Sat Mar 29 15:10:46 2014 +0100 @@ -261,15 +261,21 @@ === Navigation Controls === -The above examples have all provided fixed views of known events. However, a set of controls can be added to a calendar in order to let users navigate different time periods. This is done by providing a `calendar` parameter, indicating the name of the calendar, and by specifying a period of time: +The above examples have all provided fixed views of known events. However, a set of controls can be added to a calendar in order to let users navigate different time periods. This is done by specifying a period of time: {{{ ## Provide a navigable calendar. <> }}} +Here, an optional `calendar` parameter, indicating the name of the calendar, is used to distinguish between this calendar and any others that might be displayed on the same page. Without it, any navigation might cause other such calendars to change their positions, too. + Without any time period, the calendar would show all events, and there would be no real need to provide navigation, since there would be no events outside the displayed period to navigate to. It is possible to omit either the `start` or the `end` parameter and still provide navigation, however. +=== Limits on Displayed Calendars === + +So that the display of a calendar does not result in a very large Web page being produced, a limit is enforced on the number of months or days that a calendar will show at any one time. This limit is defined in the `event_aggregator_max_duration` configuration setting or is given a default of 31 (months in the calendar view, days in the day view). + === Showing Calendar Days === To view the individual days in a calendar, it is possible to hover over or select a day number and select the "View day" link. However, a calendar view can be set up using the macro as follows: