1.1 --- a/EventAggregatorSupport/Filter.py Wed Jan 29 18:24:35 2014 +0100
1.2 +++ b/EventAggregatorSupport/Filter.py Sat Mar 29 15:10:46 2014 +0100
1.3 @@ -198,8 +198,8 @@
1.4 Return a scale for the given coverage so that the times involved are
1.5 exposed. The scale consists of a list of non-overlapping timespans forming
1.6 a contiguous period of time, where each timespan is accompanied in a tuple
1.7 - by a limit and a list of original time details. Thus, the scale consists of
1.8 - (timespan, limit, set-of-times) tuples.
1.9 + by a limit and two sets of original time details. Thus, the scale consists
1.10 + of (timespan, limit, set-of-start-times, set-of-end-times) tuples.
1.11 """
1.12
1.13 times = {}
1.14 @@ -241,7 +241,7 @@
1.15
1.16 for time, limit in keys:
1.17 if not first:
1.18 - scale.append((Timespan(start, time), limit, times[(start, start_limit)]))
1.19 + scale.append((Timespan(start, time), limit, times[(start, start_limit)], times[(time, limit)]))
1.20 else:
1.21 first = 0
1.22 start, start_limit = time, limit
2.1 --- a/EventAggregatorSupport/Resources.py Wed Jan 29 18:24:35 2014 +0100
2.2 +++ b/EventAggregatorSupport/Resources.py Sat Mar 29 15:10:46 2014 +0100
2.3 @@ -11,7 +11,7 @@
2.4
2.5 from DateSupport import Date, Month
2.6 from MoinSupport import *
2.7 -from MoinRemoteSupport import getCachedResource, getCachedResourceMetadata
2.8 +from MoinRemoteSupport import getCachedResource, getCachedResourceMetadata, imapreader
2.9
2.10 import urllib
2.11
2.12 @@ -96,6 +96,16 @@
2.13 if url.startswith("file:"):
2.14 return None
2.15
2.16 + # Support IMAP access.
2.17 +
2.18 + elif url.startswith("imap"):
2.19 + reader = imapreader
2.20 +
2.21 + # Otherwise, use the default access mechanism.
2.22 +
2.23 + else:
2.24 + reader = None
2.25 +
2.26 # Parameterise the URL.
2.27 # Where other parameters are used, care must be taken to encode them
2.28 # properly.
2.29 @@ -111,15 +121,15 @@
2.30 parser = parseEventsInCalendarFromResource
2.31 required_content_type = expected_content_type or "text/calendar"
2.32 elif format == "xcal":
2.33 - parser = parseEventsInXMLCalendarFromResource
2.34 - required_content_type = expected_content_type or "application/calendar+xml"
2.35 + parser = parseEventsInXMLCalendarsFromResource
2.36 + required_content_type = expected_content_type or "multipart/mixed"
2.37 else:
2.38 return None
2.39
2.40 # Obtain the resource, using a cached version if appropriate.
2.41
2.42 max_cache_age = int(getattr(request.cfg, "event_aggregator_max_cache_age", "300"))
2.43 - data = getCachedResource(request, url, "EventAggregator", "wiki", max_cache_age)
2.44 + data = getCachedResource(request, url, "EventAggregator", "wiki", max_cache_age, reader)
2.45 if not data:
2.46 return None
2.47
3.1 --- a/EventAggregatorSupport/Types.py Wed Jan 29 18:24:35 2014 +0100
3.2 +++ b/EventAggregatorSupport/Types.py Sat Mar 29 15:10:46 2014 +0100
3.3 @@ -15,6 +15,7 @@
3.4 import vCalendar
3.5
3.6 from codecs import getreader
3.7 +from email.parser import Parser
3.8 from email.utils import parsedate
3.9 import re
3.10
3.11 @@ -131,6 +132,36 @@
3.12 else:
3.13 return None
3.14
3.15 +def parseEventsInXMLCalendarsFromResource(f, encoding=None, url=None, metadata=None):
3.16 +
3.17 + """
3.18 + Parse a collection of events in xCalendar format from the given file-like
3.19 + object 'f', with content having any specified 'encoding' and being described
3.20 + by the given 'url' and 'metadata'.
3.21 + """
3.22 +
3.23 + new_url = "" # hide the IMAP URL
3.24 +
3.25 + message = Parser().parse(f)
3.26 + resources = EventResourceCollection(new_url, metadata or {})
3.27 +
3.28 + for data in message.get_payload():
3.29 +
3.30 + # Find the calendar data.
3.31 +
3.32 + if data.is_multipart():
3.33 + for part in data.get_payload():
3.34 + if part.get_content_type() == "application/calendar+xml":
3.35 + text = part
3.36 + else:
3.37 + text = data
3.38 +
3.39 + # Obtain a calendar and merge it into the collection.
3.40 +
3.41 + resources.append(parseEventsInXMLCalendarFromResource(StringIO(text.get_payload(decode=True)), part.get_charset(), new_url))
3.42 +
3.43 + return resources
3.44 +
3.45 def parseEvents(text, event_page, fragment=None):
3.46
3.47 """
3.48 @@ -239,8 +270,10 @@
3.49
3.50 "A resource providing event information."
3.51
3.52 - def __init__(self, url):
3.53 + def __init__(self, url, metadata=None):
3.54 self.url = url
3.55 + self.metadata = metadata
3.56 + self.events = None
3.57
3.58 def getPageURL(self):
3.59
3.60 @@ -262,13 +295,13 @@
3.61 "last-comment" made about the last edit.
3.62 """
3.63
3.64 - return {}
3.65 + return self.metadata or {}
3.66
3.67 def getEvents(self):
3.68
3.69 "Return a list of events from this resource."
3.70
3.71 - return []
3.72 + return self.events or []
3.73
3.74 def linkToPage(self, request, text, query_string=None, anchor=None):
3.75
3.76 @@ -291,14 +324,30 @@
3.77
3.78 return fmt.text(text)
3.79
3.80 +class EventResourceCollection(EventResource):
3.81 +
3.82 + "A collection of resources."
3.83 +
3.84 + def __init__(self, url, metadata=None):
3.85 + self.url = url
3.86 + self.metadata = metadata
3.87 + self.resources = []
3.88 +
3.89 + def append(self, resource):
3.90 + self.resources.append(resource)
3.91 +
3.92 + def getEvents(self):
3.93 + events = []
3.94 + for resource in self.resources:
3.95 + events += resource.getEvents()
3.96 + return events
3.97 +
3.98 class EventCalendarResource(EventResource):
3.99
3.100 "A generic calendar resource."
3.101
3.102 def __init__(self, url, metadata):
3.103 - EventResource.__init__(self, url)
3.104 - self.metadata = metadata
3.105 - self.events = None
3.106 + EventResource.__init__(self, url, metadata)
3.107
3.108 if not self.metadata.has_key("created") and self.metadata.has_key("date"):
3.109 self.metadata["created"] = DateTime(parsedate(self.metadata["date"])[:7])
3.110 @@ -306,16 +355,6 @@
3.111 if self.metadata.has_key("last-modified") and not isinstance(self.metadata["last-modified"], DateTime):
3.112 self.metadata["last-modified"] = DateTime(parsedate(self.metadata["last-modified"])[:7])
3.113
3.114 - def getMetadata(self):
3.115 -
3.116 - """
3.117 - Return a dictionary containing items describing the page's "created"
3.118 - time, "last-modified" time, "sequence" (or revision number) and the
3.119 - "last-comment" made about the last edit.
3.120 - """
3.121 -
3.122 - return self.metadata
3.123 -
3.124 class EventCalendar(EventCalendarResource):
3.125
3.126 "An iCalendar resource."
3.127 @@ -324,16 +363,6 @@
3.128 EventCalendarResource.__init__(self, url, metadata)
3.129 self.calendar = calendar
3.130
3.131 - def getMetadata(self):
3.132 -
3.133 - """
3.134 - Return a dictionary containing items describing the page's "created"
3.135 - time, "last-modified" time, "sequence" (or revision number) and the
3.136 - "last-comment" made about the last edit.
3.137 - """
3.138 -
3.139 - return self.metadata
3.140 -
3.141 def getEvents(self):
3.142
3.143 "Return a list of events from this resource."
3.144 @@ -451,6 +480,8 @@
3.145
3.146 return self.events
3.147
3.148 + # Parsing methods.
3.149 +
3.150 def _getValue(self, values, type):
3.151 for element in values[0].xpath("xcal:%s" % type, namespaces=self.XCAL):
3.152 return element.textContent
3.153 @@ -945,6 +976,8 @@
3.154 the absence of any URL in the event details.
3.155 """
3.156
3.157 + # NOTE: Redirect empty URLs to an action showing the resource details.
3.158 +
3.159 return self.details.get("url") and \
3.160 self.valueToString(self.details["url"]) or \
3.161 self.page.getPageURL()
4.1 --- a/EventAggregatorSupport/View.py Wed Jan 29 18:24:35 2014 +0100
4.2 +++ b/EventAggregatorSupport/View.py Sat Mar 29 15:10:46 2014 +0100
4.3 @@ -2,10 +2,11 @@
4.4 """
4.5 MoinMoin - EventAggregator user interface library
4.6
4.7 - @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
4.8 + @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie <paul@boddie.org.uk>
4.9 @license: GNU GPL (v2 or later), see COPYING.txt for details.
4.10 """
4.11
4.12 +from DateSupport import Date
4.13 from EventAggregatorSupport.Filter import getCalendarPeriod, getEventsInPeriod, \
4.14 getCoverage, getCoverageScale
4.15 from EventAggregatorSupport.Locations import getMapsPage, getLocationsPage, Location
4.16 @@ -140,9 +141,11 @@
4.17
4.18 self.first = first
4.19 self.last = last
4.20 - self.duration = abs(last - first) + 1
4.21 -
4.22 - if self.calendar_name:
4.23 + self.initDuration()
4.24 +
4.25 + self.showing_everything = not self.calendar_start and not self.calendar_end
4.26 +
4.27 + if not self.showing_everything:
4.28
4.29 # Store the view parameters.
4.30
4.31 @@ -156,6 +159,28 @@
4.32 self.previous_set_end = last.update(-self.duration)
4.33 self.next_set_end = last.update(self.duration)
4.34
4.35 + def initDuration(self):
4.36 +
4.37 + "Limit the duration of the calendar to prevent excessive output."
4.38 +
4.39 + request = self.page.request
4.40 +
4.41 + self.duration = abs(self.last - self.first) + 1
4.42 +
4.43 + # Limit to the specified number of units.
4.44 +
4.45 + limit = int(getattr(request.cfg, "event_aggregator_max_duration", 31))
4.46 +
4.47 + if self.duration > limit:
4.48 + if isinstance(self.first, Date):
4.49 + self.last = self.first.day_update(limit - 1)
4.50 + else:
4.51 + self.last = self.first.month_update(limit - 1)
4.52 +
4.53 + self.calendar_start = self.calendar_start or self.first
4.54 + self.calendar_end = self.calendar_end or self.last
4.55 + self.duration = limit
4.56 +
4.57 def getIdentifier(self):
4.58
4.59 "Return a unique identifier to be used to refer to this view."
4.60 @@ -168,7 +193,10 @@
4.61
4.62 "Return the 'argname' qualified using the calendar name."
4.63
4.64 - return getQualifiedParameterName(self.calendar_name, argname)
4.65 + if self.calendar_name:
4.66 + return getQualifiedParameterName(self.calendar_name, argname)
4.67 + else:
4.68 + return argname
4.69
4.70 def getDateQueryString(self, argname, date, prefix=1):
4.71
4.72 @@ -743,56 +771,53 @@
4.73 output = []
4.74 append = output.append
4.75
4.76 - if self.calendar_name:
4.77 - calendar_name = self.calendar_name
4.78 -
4.79 - # Links to the previous set of months and to a calendar shifted
4.80 - # back one month.
4.81 -
4.82 - previous_set_link = self.getNavigationLink(
4.83 - self.previous_set_start, self.previous_set_end
4.84 - )
4.85 - previous_link = self.getNavigationLink(
4.86 - self.previous_start, self.previous_end
4.87 - )
4.88 - previous_set_update_link = self.getUpdateLink(
4.89 - self.previous_set_start, self.previous_set_end
4.90 - )
4.91 - previous_update_link = self.getUpdateLink(
4.92 - self.previous_start, self.previous_end
4.93 - )
4.94 -
4.95 - # Links to the next set of months and to a calendar shifted
4.96 - # forward one month.
4.97 -
4.98 - next_set_link = self.getNavigationLink(
4.99 - self.next_set_start, self.next_set_end
4.100 - )
4.101 - next_link = self.getNavigationLink(
4.102 - self.next_start, self.next_end
4.103 - )
4.104 - next_set_update_link = self.getUpdateLink(
4.105 - self.next_set_start, self.next_set_end
4.106 - )
4.107 - next_update_link = self.getUpdateLink(
4.108 - self.next_start, self.next_end
4.109 - )
4.110 -
4.111 - append(fmt.div(on=1, css_class="event-calendar-navigation"))
4.112 -
4.113 - append(fmt.span(on=1, css_class="previous"))
4.114 - append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link, title=_("Previous set")))
4.115 - append(fmt.text(" "))
4.116 - append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link, title=_("Previous")))
4.117 - append(fmt.span(on=0))
4.118 -
4.119 - append(fmt.span(on=1, css_class="next"))
4.120 - append(linkToPage(request, page, ">", next_link, onclick=next_update_link, title=_("Next")))
4.121 - append(fmt.text(" "))
4.122 - append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link, title=_("Next set")))
4.123 - append(fmt.span(on=0))
4.124 -
4.125 - append(fmt.div(on=0))
4.126 + # Links to the previous set of months and to a calendar shifted
4.127 + # back one month.
4.128 +
4.129 + previous_set_link = self.getNavigationLink(
4.130 + self.previous_set_start, self.previous_set_end
4.131 + )
4.132 + previous_link = self.getNavigationLink(
4.133 + self.previous_start, self.previous_end
4.134 + )
4.135 + previous_set_update_link = self.getUpdateLink(
4.136 + self.previous_set_start, self.previous_set_end
4.137 + )
4.138 + previous_update_link = self.getUpdateLink(
4.139 + self.previous_start, self.previous_end
4.140 + )
4.141 +
4.142 + # Links to the next set of months and to a calendar shifted
4.143 + # forward one month.
4.144 +
4.145 + next_set_link = self.getNavigationLink(
4.146 + self.next_set_start, self.next_set_end
4.147 + )
4.148 + next_link = self.getNavigationLink(
4.149 + self.next_start, self.next_end
4.150 + )
4.151 + next_set_update_link = self.getUpdateLink(
4.152 + self.next_set_start, self.next_set_end
4.153 + )
4.154 + next_update_link = self.getUpdateLink(
4.155 + self.next_start, self.next_end
4.156 + )
4.157 +
4.158 + append(fmt.div(on=1, css_class="event-calendar-navigation"))
4.159 +
4.160 + append(fmt.span(on=1, css_class="previous"))
4.161 + append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link, title=_("Previous set")))
4.162 + append(fmt.text(" "))
4.163 + append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link, title=_("Previous")))
4.164 + append(fmt.span(on=0))
4.165 +
4.166 + append(fmt.span(on=1, css_class="next"))
4.167 + append(linkToPage(request, page, ">", next_link, onclick=next_update_link, title=_("Next")))
4.168 + append(fmt.text(" "))
4.169 + append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link, title=_("Next set")))
4.170 + append(fmt.span(on=0))
4.171 +
4.172 + append(fmt.div(on=0))
4.173
4.174 return "".join(output)
4.175
4.176 @@ -812,7 +837,7 @@
4.177 output = []
4.178 append = output.append
4.179
4.180 - if self.calendar_name:
4.181 + if not self.showing_everything:
4.182
4.183 # A link leading to this date being at the top of the calendar.
4.184
4.185 @@ -1339,7 +1364,7 @@
4.186
4.187 day_rows = []
4.188
4.189 - for period, limit, times in scale:
4.190 + for period, limit, start_times, end_times in scale:
4.191
4.192 # Ignore timespans before this day.
4.193
4.194 @@ -1363,7 +1388,7 @@
4.195 rowspans[event] += 1
4.196 day_row.append((location, event))
4.197
4.198 - day_rows.append((period, day_row, times))
4.199 + day_rows.append((period, day_row, start_times, end_times))
4.200
4.201 # Output the locations.
4.202
4.203 @@ -1393,7 +1418,7 @@
4.204 last_period = period = None
4.205 events_written = set()
4.206
4.207 - for period, day_row, times in day_rows:
4.208 + for period, day_row, start_times, end_times in day_rows:
4.209
4.210 # Write a heading describing the time.
4.211
4.212 @@ -1402,7 +1427,7 @@
4.213 # Show times only for distinct periods.
4.214
4.215 if not last_period or period.start != last_period.start:
4.216 - append(self.writeDayScaleHeading(times))
4.217 + append(self.writeDayScaleHeading(start_times))
4.218 else:
4.219 append(self.writeDayScaleHeading([]))
4.220
4.221 @@ -1432,7 +1457,7 @@
4.222 if period is not None:
4.223 if period.end == date:
4.224 append(fmt.table_row(on=1))
4.225 - append(self.writeDayScaleHeading(times))
4.226 + append(self.writeDayScaleHeading(end_times))
4.227
4.228 for slot in day_row:
4.229 append(self.writeDaySpacer())
4.230 @@ -1679,7 +1704,7 @@
4.231 output = []
4.232 append = output.append
4.233
4.234 - append(fmt.div(on=1, css_class="event-calendar", id=("EventAggregator-%s" % self.getIdentifier())))
4.235 + append(fmt.div(on=1, css_class="event-calendar-region", id=("EventAggregator-%s" % self.getIdentifier())))
4.236
4.237 # Output download controls.
4.238
4.239 @@ -1687,6 +1712,8 @@
4.240 append(self.writeDownloadControls())
4.241 append(fmt.div(on=0))
4.242
4.243 + append(fmt.div(on=1, css_class="event-display"))
4.244 +
4.245 # Output a table.
4.246
4.247 if self.mode == "table":
4.248 @@ -2184,6 +2211,8 @@
4.249 append(fmt.table(on=0))
4.250 append(fmt.div(on=0))
4.251
4.252 + append(fmt.div(on=0)) # end of event-display
4.253 +
4.254 # Output view controls.
4.255
4.256 append(fmt.div(on=1, css_class="event-controls"))
5.1 --- a/TO_DO.txt Wed Jan 29 18:24:35 2014 +0100
5.2 +++ b/TO_DO.txt Sat Mar 29 15:10:46 2014 +0100
5.3 @@ -1,3 +1,25 @@
5.4 +Event Invitations and Attendance
5.5 +--------------------------------
5.6 +
5.7 +iTIP invitations (RFC 5546) could be supported. REQUEST method payloads are
5.8 +effectively equivalent to plain iCalendar payloads; ADD method payloads are
5.9 +similar to plain iCalendar payloads but augment previously received data,
5.10 +whereas CANCEL method payloads remove or retract previously received data;
5.11 +REFRESH method payloads are minimal requests for complete iCalendar payloads
5.12 +to be sent in response. Other methods (REPLY, COUNTER, DECLINECOUNTER) update
5.13 +the state of events according to attendance notifications.
5.14 +
5.15 +For iTIP exchanges to work effectively, a mapping of the UID of each event to
5.16 +the received information needs to be maintained. (An awareness of each
5.17 +RECURRENCE-ID in an event is also useful where recurring events are being
5.18 +handled.) Here, a form of index needs to be supported for efficient access via
5.19 +event UIDs to event data. Other indexes might be supported for efficient
5.20 +free/busy resource generation.
5.21 +
5.22 +The actual sending and receiving of iTIP messages needs to be supported by
5.23 +other components such as MoinMessage. It might be interesting to support iTIP
5.24 +notifications if events are changed directly on a wiki.
5.25 +
5.26 Navigation Controls
5.27 -------------------
5.28
6.1 --- a/css/event-aggregator.css Wed Jan 29 18:24:35 2014 +0100
6.2 +++ b/css/event-aggregator.css Sat Mar 29 15:10:46 2014 +0100
6.3 @@ -6,10 +6,18 @@
6.4
6.5 ...before any rules.
6.6
6.7 -Copyright (c) 2009, 2010, 2011, 2012, 2013 by Paul Boddie
6.8 +Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie
6.9 Licensed under the GNU GPL (v2 or later), see COPYING.txt for details.
6.10 */
6.11
6.12 +.event-display {
6.13 + text-align: center; /* to put the actual calendar details in the centre */
6.14 +}
6.15 +
6.16 +.event-display > * {
6.17 + text-align: left;
6.18 +}
6.19 +
6.20 /* Controls. */
6.21
6.22 .event-controls {
6.23 @@ -146,9 +154,7 @@
6.24 text-align: right;
6.25 }
6.26
6.27 -/* Calendar view. */
6.28 -
6.29 -.event-calendar {
6.30 +.event-calendar-navigation {
6.31 position: relative;
6.32 }
6.33
6.34 @@ -167,6 +173,8 @@
6.35 right: 1em;
6.36 }
6.37
6.38 +/* Calendar view. */
6.39 +
6.40 .event-month {
6.41 width: 98%;
6.42 border-bottom: 1px solid #dddddd;
6.43 @@ -530,11 +538,12 @@
6.44 /* Map view. */
6.45
6.46 .event-map {
6.47 - text-align: center;
6.48 + display: inline-block; /* confines the navigation controls to the map width */
6.49 }
6.50
6.51 .event-map table {
6.52 - display: inline-block;
6.53 + margin-top: 0; /* confines the navigation controls to the map header */
6.54 + text-align: center;
6.55 }
6.56
6.57 caption.event-map-heading {
7.1 --- a/macros/EventAggregator.py Wed Jan 29 18:24:35 2014 +0100
7.2 +++ b/macros/EventAggregator.py Sat Mar 29 15:10:46 2014 +0100
7.3 @@ -41,7 +41,7 @@
7.4 mode=map shows a map of events
7.5
7.6 calendar=NAME uses the given NAME to provide request parameters which
7.7 - can be used to control the calendar view
7.8 + name=NAME can be used to control the calendar view
7.9
7.10 template=PAGE uses the given PAGE as the default template for new
7.11 events (or the default template from the configuration
7.12 @@ -49,6 +49,10 @@
7.13
7.14 parent=PAGE uses the given PAGE as the parent of any new event page
7.15
7.16 + source=SOURCE uses the given SOURCE to provide events
7.17 +
7.18 + search=SEARCH uses the given SEARCH expression to search for events
7.19 +
7.20 Calendar view options:
7.21
7.22 names=daily shows the name of an event on every day of that event
7.23 @@ -61,6 +65,20 @@
7.24 page specified in the configuration by the
7.25 'event_aggregator_maps_page' setting) along with an
7.26 attached map image
7.27 +
7.28 + Request parameters configured by the calendar argument include the
7.29 + following:
7.30 +
7.31 + start equivalent to the above start argument
7.32 + end equivalent to the above end argument
7.33 +
7.34 + wider-start indicates the start of a view from a wider context
7.35 + wider-end indicates the end of a view from a wider context
7.36 +
7.37 + mode equivalent to the above mode argument
7.38 +
7.39 + resolution=month indicates that dates have a month level of precision
7.40 + resolution=date indicates that dates have a day/date level of precision
7.41 """
7.42
7.43 request = macro.request
7.44 @@ -109,6 +127,9 @@
7.45 elif arg.startswith("calendar="):
7.46 calendar_name = arg[9:]
7.47
7.48 + elif arg.startswith("name="):
7.49 + calendar_name = arg[5:]
7.50 +
7.51 elif arg.startswith("template="):
7.52 template_name = arg[9:]
7.53
8.1 --- a/pages/HelpOnEventAggregator Wed Jan 29 18:24:35 2014 +0100
8.2 +++ b/pages/HelpOnEventAggregator Sat Mar 29 15:10:46 2014 +0100
8.3 @@ -261,15 +261,21 @@
8.4
8.5 === Navigation Controls ===
8.6
8.7 -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:
8.8 +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:
8.9
8.10 {{{
8.11 ## Provide a navigable calendar.
8.12 <<EventAggregator(CategoryEvents,start=current,end=current,calendar=monthly)>>
8.13 }}}
8.14
8.15 +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.
8.16 +
8.17 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.
8.18
8.19 +=== Limits on Displayed Calendars ===
8.20 +
8.21 +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).
8.22 +
8.23 === Showing Calendar Days ===
8.24
8.25 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: