EventAggregator

EventAggregatorSupport.py

353:9286eac9fc72
2013-05-01 Paul Boddie Added usage of resource-level metadata for remote calendars where essential properties may be missing from event records.
     1 # -*- coding: iso-8859-1 -*-     2 """     3     MoinMoin - EventAggregator library     4      5     @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk>     6     @copyright: 2000-2004 Juergen Hermann <jh@web.de>,     7                 2005-2008 MoinMoin:ThomasWaldmann.     8     @license: GNU GPL (v2 or later), see COPYING.txt for details.     9 """    10     11 from ContentTypeSupport import getContentTypeAndEncoding    12 from GeneralSupport import *    13 from LocationSupport import *    14 from MoinDateSupport import *    15 from MoinRemoteSupport import *    16 from MoinSupport import *    17 from ViewSupport import *    18     19 from MoinMoin.Page import Page    20 from MoinMoin.action import AttachFile    21 from MoinMoin import wikiutil    22     23 import codecs    24 import re    25 import urllib    26     27 try:    28     from cStringIO import StringIO    29 except ImportError:    30     from StringIO import StringIO    31     32 try:    33     set    34 except NameError:    35     from sets import Set as set    36     37 try:    38     import vCalendar    39 except ImportError:    40     vCalendar = None    41     42 escape = wikiutil.escape    43     44 __version__ = "0.9.1"    45     46 # Page parsing.    47     48 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE)    49 category_membership_regexp = re.compile(ur"^\s*(?:(Category\S+)(?:\s+(Category\S+))*)\s*$", re.MULTILINE | re.UNICODE)    50     51 # Value parsing.    52     53 country_code_regexp = re.compile(ur'(?:^|\W)(?P<code>[A-Z]{2})(?:$|\W+$)', re.UNICODE)    54     55 # Utility functions.    56     57 def to_plain_text(s, request):    58     59     "Convert 's' to plain text."    60     61     fmt = getFormatterClass(request, "plain")(request)    62     fmt.setPage(request.page)    63     return formatText(s, request, fmt)    64     65 def getLocationPosition(location, locations):    66     67     """    68     Attempt to return the position of the given 'location' using the 'locations'    69     dictionary provided. If no position can be found, return a latitude of None    70     and a longitude of None.    71     """    72     73     latitude, longitude = None, None    74     75     if location is not None:    76         try:    77             latitude, longitude = map(getMapReference, locations[location].split())    78         except (KeyError, ValueError):    79             pass    80     81     return latitude, longitude    82     83 # Utility classes and associated functions.    84     85 class ActionSupport(ActionSupport):    86     87     "Extend the generic action support."    88     89     def get_month_lists(self, default_as_current=0):    90     91         """    92         Return two lists of HTML element definitions corresponding to the start    93         and end month selection controls, with months selected according to any    94         values that have been specified via request parameters.    95         """    96     97         _ = self._    98         form = self.get_form()    99    100         # Initialise month lists.   101    102         start_month_list = []   103         end_month_list = []   104    105         start_month = self._get_input(form, "start-month", default_as_current and getCurrentMonth().month() or None)   106         end_month = self._get_input(form, "end-month", start_month)   107    108         # Prepare month lists, selecting specified months.   109    110         if not default_as_current:   111             start_month_list.append('<option value=""></option>')   112             end_month_list.append('<option value=""></option>')   113    114         for month in range(1, 13):   115             month_label = escape(_(getMonthLabel(month)))   116             selected = self._get_selected(month, start_month)   117             start_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label))   118             selected = self._get_selected(month, end_month)   119             end_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label))   120    121         return start_month_list, end_month_list   122    123     def get_year_defaults(self, default_as_current=0):   124    125         "Return defaults for the start and end years."   126    127         form = self.get_form()   128    129         start_year_default = form.get("start-year", [default_as_current and getCurrentYear() or ""])[0]   130         end_year_default = form.get("end-year", [default_as_current and start_year_default or ""])[0]   131    132         return start_year_default, end_year_default   133    134     def get_day_defaults(self, default_as_current=0):   135    136         "Return defaults for the start and end days."   137    138         form = self.get_form()   139    140         start_day_default = form.get("start-day", [default_as_current and getCurrentDate().day() or ""])[0]   141         end_day_default = form.get("end-day", [default_as_current and start_day_default or ""])[0]   142    143         return start_day_default, end_day_default   144    145 # Event parsing from page texts.   146    147 def parseEvents(text, event_page, fragment=None):   148    149     """   150     Parse events in the given 'text', returning a list of event objects for the   151     given 'event_page'. An optional 'fragment' can be specified to indicate a   152     specific region of the event page.   153    154     If the optional 'fragment' identifier is provided, the first heading may   155     also be used to provide an event summary/title.   156     """   157    158     template_details = {}   159     if fragment:   160         template_details["fragment"] = fragment   161    162     details = {}   163     details.update(template_details)   164     raw_details = {}   165    166     # Obtain a heading, if requested.   167    168     if fragment:   169         for level, title, (start, end) in getHeadings(text):   170             raw_details["title"] = text[start:end]   171             details["title"] = getSimpleWikiText(title.strip())   172             break   173    174     # Start populating events.   175    176     events = [Event(event_page, details, raw_details)]   177    178     for match in definition_list_regexp.finditer(text):   179    180         # Skip commented-out items.   181    182         if match.group("optcomment"):   183             continue   184    185         # Permit case-insensitive list terms.   186    187         term = match.group("term").lower()   188         raw_desc = match.group("desc")   189    190         # Special value type handling.   191    192         # Dates.   193    194         if term in Event.date_terms:   195             desc = getDateTime(raw_desc)   196    197         # Lists (whose elements may be quoted).   198    199         elif term in Event.list_terms:   200             desc = map(getSimpleWikiText, to_list(raw_desc, ","))   201    202         # Position details.   203    204         elif term == "geo":   205             try:   206                 desc = map(getMapReference, to_list(raw_desc, None))   207                 if len(desc) != 2:   208                     continue   209             except (KeyError, ValueError):   210                 continue   211    212         # Labels which may well be quoted.   213    214         elif term in Event.title_terms:   215             desc = getSimpleWikiText(raw_desc.strip())   216    217         # Plain Wiki text terms.   218    219         elif term in Event.other_terms:   220             desc = raw_desc.strip()   221    222         else:   223             desc = raw_desc   224    225         if desc is not None:   226    227             # Handle apparent duplicates by creating a new set of   228             # details.   229    230             if details.has_key(term):   231    232                 # Make a new event.   233    234                 details = {}   235                 details.update(template_details)   236                 raw_details = {}   237                 events.append(Event(event_page, details, raw_details))   238    239             details[term] = desc   240             raw_details[term] = raw_desc   241    242     return events   243    244 # Event resources providing collections of events.   245    246 class EventResource:   247    248     "A resource providing event information."   249    250     def __init__(self, url):   251         self.url = url   252    253     def getPageURL(self):   254    255         "Return the URL of this page."   256    257         return self.url   258    259     def getFormat(self):   260    261         "Get the format used by this resource."   262    263         return "plain"   264    265     def getMetadata(self):   266    267         """   268         Return a dictionary containing items describing the page's "created"   269         time, "last-modified" time, "sequence" (or revision number) and the   270         "last-comment" made about the last edit.   271         """   272    273         return {}   274    275     def getEvents(self):   276    277         "Return a list of events from this resource."   278    279         return []   280    281     def linkToPage(self, request, text, query_string=None, anchor=None):   282    283         """   284         Using 'request', return a link to this page with the given link 'text'   285         and optional 'query_string' and 'anchor'.   286         """   287    288         return linkToResource(self.url, request, text, query_string, anchor)   289    290     # Formatting-related functions.   291    292     def formatText(self, text, fmt):   293    294         """   295         Format the given 'text' using the specified formatter 'fmt'.   296         """   297    298         # Assume plain text which is then formatted appropriately.   299    300         return fmt.text(text)   301    302 class EventCalendar(EventResource):   303    304     "An iCalendar resource."   305    306     def __init__(self, url, calendar):   307         EventResource.__init__(self, url)   308         self.calendar = calendar   309         self.events = None   310    311     def getEvents(self):   312    313         "Return a list of events from this resource."   314    315         if self.events is None:   316             self.events = []   317    318             _calendar, _empty, calendar = self.calendar   319    320             for objtype, attrs, obj in calendar:   321    322                 # Read events.   323    324                 if objtype == "VEVENT":   325                     details = {}   326    327                     for property, attrs, value in obj:   328    329                         # Convert dates.   330    331                         if property in ("DTSTART", "DTEND", "CREATED", "DTSTAMP", "LAST-MODIFIED"):   332                             if property in ("DTSTART", "DTEND"):   333                                 property = property[2:]   334                             if attrs.get("VALUE") == "DATE":   335                                 value = getDateFromCalendar(value)   336                                 if value and property == "END":   337                                     value = value.previous_day()   338                             else:   339                                 value = getDateTimeFromCalendar(value)   340    341                         # Convert numeric data.   342    343                         elif property == "SEQUENCE":   344                             value = int(value)   345    346                         # Convert lists.   347    348                         elif property == "CATEGORIES":   349                             value = to_list(value, ",")   350    351                         # Convert positions (using decimal values).   352    353                         elif property == "GEO":   354                             try:   355                                 value = map(getMapReferenceFromDecimal, to_list(value, ";"))   356                                 if len(value) != 2:   357                                     continue   358                             except (KeyError, ValueError):   359                                 continue   360    361                         # Accept other textual data as it is.   362    363                         elif property in ("LOCATION", "SUMMARY", "URL"):   364                             value = value or None   365    366                         # Ignore other properties.   367    368                         else:   369                             continue   370    371                         property = property.lower()   372                         details[property] = value   373    374                     self.events.append(CalendarEvent(self, details))   375    376         return self.events   377    378 class EventPage:   379    380     "An event page acting as an event resource."   381    382     def __init__(self, page):   383         self.page = page   384         self.events = None   385         self.body = None   386         self.categories = None   387         self.metadata = None   388    389     def copyPage(self, page):   390    391         "Copy the body of the given 'page'."   392    393         self.body = page.getBody()   394    395     def getPageURL(self):   396    397         "Return the URL of this page."   398    399         return getPageURL(self.page)   400    401     def getFormat(self):   402    403         "Get the format used on this page."   404    405         return getFormat(self.page)   406    407     def getMetadata(self):   408    409         """   410         Return a dictionary containing items describing the page's "created"   411         time, "last-modified" time, "sequence" (or revision number) and the   412         "last-comment" made about the last edit.   413         """   414    415         if self.metadata is None:   416             self.metadata = getMetadata(self.page)   417         return self.metadata   418    419     def getRevisions(self):   420    421         "Return a list of page revisions."   422    423         return self.page.getRevList()   424    425     def getPageRevision(self):   426    427         "Return the revision details dictionary for this page."   428    429         return getPageRevision(self.page)   430    431     def getPageName(self):   432    433         "Return the page name."   434    435         return self.page.page_name   436    437     def getPrettyPageName(self):   438    439         "Return a nicely formatted title/name for this page."   440    441         return getPrettyPageName(self.page)   442    443     def getBody(self):   444    445         "Get the current page body."   446    447         if self.body is None:   448             self.body = self.page.get_raw_body()   449         return self.body   450    451     def getEvents(self):   452    453         "Return a list of events from this page."   454    455         if self.events is None:   456             self.events = []   457             if self.getFormat() == "wiki":   458                 for format, attributes, region in getFragments(self.getBody(), True):   459                     self.events += parseEvents(region, self, attributes.get("fragment"))   460    461         return self.events   462    463     def setEvents(self, events):   464    465         "Set the given 'events' on this page."   466    467         self.events = events   468    469     def getCategoryMembership(self):   470    471         "Get the category names from this page."   472    473         if self.categories is None:   474             body = self.getBody()   475             match = category_membership_regexp.search(body)   476             self.categories = match and [x for x in match.groups() if x] or []   477    478         return self.categories   479    480     def setCategoryMembership(self, category_names):   481    482         """   483         Set the category membership for the page using the specified   484         'category_names'.   485         """   486    487         self.categories = category_names   488    489     def flushEventDetails(self):   490    491         "Flush the current event details to this page's body text."   492    493         new_body_parts = []   494         end_of_last_match = 0   495         body = self.getBody()   496    497         events = iter(self.getEvents())   498    499         event = events.next()   500         event_details = event.getDetails()   501         replaced_terms = set()   502    503         for match in definition_list_regexp.finditer(body):   504    505             # Permit case-insensitive list terms.   506    507             term = match.group("term").lower()   508             desc = match.group("desc")   509    510             # Check that the term has not already been substituted. If so,   511             # get the next event.   512    513             if term in replaced_terms:   514                 try:   515                     event = events.next()   516    517                 # No more events.   518    519                 except StopIteration:   520                     break   521    522                 event_details = event.getDetails()   523                 replaced_terms = set()   524    525             # Add preceding text to the new body.   526    527             new_body_parts.append(body[end_of_last_match:match.start()])   528    529             # Get the matching regions, adding the term to the new body.   530    531             new_body_parts.append(match.group("wholeterm"))   532    533             # Special value type handling.   534    535             if event_details.has_key(term):   536    537                 # Dates.   538    539                 if term in event.date_terms:   540                     desc = desc.replace("YYYY-MM-DD", str(event_details[term]))   541    542                 # Lists (whose elements may be quoted).   543    544                 elif term in event.list_terms:   545                     desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]])   546    547                 # Labels which must be quoted.   548    549                 elif term in event.title_terms:   550                     desc = getEncodedWikiText(event_details[term])   551    552                 # Position details.   553    554                 elif term == "geo":   555                     desc = " ".join(map(str, event_details[term]))   556    557                 # Text which need not be quoted, but it will be Wiki text.   558    559                 elif term in event.other_terms:   560                     desc = event_details[term]   561    562                 replaced_terms.add(term)   563    564             # Add the replaced value.   565    566             new_body_parts.append(desc)   567    568             # Remember where in the page has been processed.   569    570             end_of_last_match = match.end()   571    572         # Write the rest of the page.   573    574         new_body_parts.append(body[end_of_last_match:])   575    576         self.body = "".join(new_body_parts)   577    578     def flushCategoryMembership(self):   579    580         "Flush the category membership to the page body."   581    582         body = self.getBody()   583         category_names = self.getCategoryMembership()   584         match = category_membership_regexp.search(body)   585    586         if match:   587             self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]])   588    589     def saveChanges(self):   590    591         "Save changes to the event."   592    593         self.flushEventDetails()   594         self.flushCategoryMembership()   595         self.page.saveText(self.getBody(), 0)   596    597     def linkToPage(self, request, text, query_string=None, anchor=None):   598    599         """   600         Using 'request', return a link to this page with the given link 'text'   601         and optional 'query_string' and 'anchor'.   602         """   603    604         return linkToPage(request, self.page, text, query_string, anchor)   605    606     # Formatting-related functions.   607    608     def getParserClass(self, format):   609    610         """   611         Return a parser class for the given 'format', returning a plain text   612         parser if no parser can be found for the specified 'format'.   613         """   614    615         return getParserClass(self.page.request, format)   616    617     def formatText(self, text, fmt):   618    619         """   620         Format the given 'text' using the specified formatter 'fmt'.   621         """   622    623         fmt.page = page = self.page   624         request = page.request   625    626         parser_cls = self.getParserClass(self.getFormat())   627         return formatText(text, request, fmt, parser_cls)   628    629 # Event details.   630    631 class Event(ActsAsTimespan):   632    633     "A description of an event."   634    635     title_terms = "title", "summary"   636     date_terms  = "start", "end"   637     list_terms  = "topics", "categories"   638     other_terms = "description", "location", "link"   639     geo_terms   = "geo",   640     all_terms = title_terms + date_terms + list_terms + other_terms + geo_terms   641    642     def __init__(self, page, details, raw_details=None):   643         self.page = page   644         self.details = details   645         self.raw_details = raw_details   646    647         # Permit omission of the end of the event by duplicating the start.   648    649         if self.details.has_key("start") and not self.details.get("end"):   650             end = self.details["start"]   651    652             # Make any end time refer to the day instead.   653    654             if isinstance(end, DateTime):   655                 end = end.as_date()   656    657             self.details["end"] = end   658    659     def __repr__(self):   660         return "<Event %r %r>" % (self.getSummary(), self.as_limits())   661    662     def __hash__(self):   663    664         """   665         Return a dictionary hash, avoiding mistaken equality of events in some   666         situations (notably membership tests) by including the URL as well as   667         the summary.   668         """   669    670         return hash(self.getSummary() + self.getEventURL())   671    672     def getPage(self):   673    674         "Return the page describing this event."   675    676         return self.page   677    678     def setPage(self, page):   679    680         "Set the 'page' describing this event."   681    682         self.page = page   683    684     def getEventURL(self):   685    686         "Return the URL of this event."   687    688         fragment = self.details.get("fragment")   689         return self.page.getPageURL() + (fragment and "#" + fragment or "")   690    691     def linkToEvent(self, request, text, query_string=None):   692    693         """   694         Using 'request', return a link to this event with the given link 'text'   695         and optional 'query_string'.   696         """   697    698         return self.page.linkToPage(request, text, query_string, self.details.get("fragment"))   699    700     def getMetadata(self):   701    702         """   703         Return a dictionary containing items describing the event's "created"   704         time, "last-modified" time, "sequence" (or revision number) and the   705         "last-comment" made about the last edit.   706         """   707    708         # Delegate this to the page.   709    710         return self.page.getMetadata()   711    712     def getSummary(self, event_parent=None):   713    714         """   715         Return either the given title or summary of the event according to the   716         event details, or a summary made from using the pretty version of the   717         page name.   718    719         If the optional 'event_parent' is specified, any page beneath the given   720         'event_parent' page in the page hierarchy will omit this parent information   721         if its name is used as the summary.   722         """   723    724         event_details = self.details   725    726         if event_details.has_key("title"):   727             return event_details["title"]   728         elif event_details.has_key("summary"):   729             return event_details["summary"]   730         else:   731             # If appropriate, remove the parent details and "/" character.   732    733             title = self.page.getPageName()   734    735             if event_parent and title.startswith(event_parent):   736                 title = title[len(event_parent.rstrip("/")) + 1:]   737    738             return getPrettyTitle(title)   739    740     def getDetails(self):   741    742         "Return the details for this event."   743    744         return self.details   745    746     def setDetails(self, event_details):   747    748         "Set the 'event_details' for this event."   749    750         self.details = event_details   751    752     def getRawDetails(self):   753    754         "Return the details for this event as they were written in a page."   755    756         return self.raw_details   757    758     # Timespan-related methods.   759    760     def __contains__(self, other):   761         return self == other   762    763     def __eq__(self, other):   764         if isinstance(other, Event):   765             return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other)   766         else:   767             return self._cmp(other) == 0   768    769     def __ne__(self, other):   770         return not self.__eq__(other)   771    772     def __lt__(self, other):   773         return self._cmp(other) == -1   774    775     def __le__(self, other):   776         return self._cmp(other) in (-1, 0)   777    778     def __gt__(self, other):   779         return self._cmp(other) == 1   780    781     def __ge__(self, other):   782         return self._cmp(other) in (0, 1)   783    784     def _cmp(self, other):   785    786         "Compare this event to an 'other' event purely by their timespans."   787    788         if isinstance(other, Event):   789             return cmp(self.as_timespan(), other.as_timespan())   790         else:   791             return cmp(self.as_timespan(), other)   792    793     def as_timespan(self):   794         details = self.details   795         if details.has_key("start") and details.has_key("end"):   796             return Timespan(details["start"], details["end"])   797         else:   798             return None   799    800     def as_limits(self):   801         ts = self.as_timespan()   802         return ts and ts.as_limits()   803    804 class CalendarEvent(Event):   805    806     "An event from a remote calendar."   807    808     def getEventURL(self):   809    810         "Return the URL of this event."   811    812         return self.details.get("url") or self.page.getPageURL()   813    814     def linkToEvent(self, request, text, query_string=None, anchor=None):   815    816         """   817         Using 'request', return a link to this event with the given link 'text'   818         and optional 'query_string' and 'anchor'.   819         """   820    821         return linkToResource(self.getEventURL(), request, text, query_string, anchor)   822    823     def getMetadata(self):   824    825         """   826         Return a dictionary containing items describing the event's "created"   827         time, "last-modified" time, "sequence" (or revision number) and the   828         "last-comment" made about the last edit.   829         """   830    831         return {   832             "created" : self.details.get("created") or self.details["dtstamp"],   833             "last-modified" : self.details.get("last-modified") or self.details["dtstamp"],   834             "sequence" : self.details.get("sequence") or 0,   835             "last-comment" : ""   836             }   837    838 # Obtaining event containers and events from such containers.   839    840 def getEventPages(pages):   841    842     "Return a list of events found on the given 'pages'."   843    844     # Get real pages instead of result pages.   845    846     return map(EventPage, pages)   847    848 def getAllEventSources(request):   849    850     "Return all event sources defined in the Wiki using the 'request'."   851    852     sources_page = getattr(request.cfg, "event_aggregator_sources_page", "EventSourcesDict")   853    854     # Remote sources are accessed via dictionary page definitions.   855    856     return getWikiDict(sources_page, request)   857    858 def getEventResources(sources, calendar_start, calendar_end, request):   859    860     """   861     Return resource objects for the given 'sources' using the given   862     'calendar_start' and 'calendar_end' to parameterise requests to the sources,   863     and the 'request' to access configuration settings in the Wiki.   864     """   865    866     sources_dict = getAllEventSources(request)   867     if not sources_dict:   868         return []   869    870     # Use dates for the calendar limits.   871    872     if isinstance(calendar_start, Date):   873         pass   874     elif isinstance(calendar_start, Month):   875         calendar_start = calendar_start.as_date(1)   876    877     if isinstance(calendar_end, Date):   878         pass   879     elif isinstance(calendar_end, Month):   880         calendar_end = calendar_end.as_date(-1)   881    882     resources = []   883    884     for source in sources:   885         try:   886             details = sources_dict[source].split()   887             url = details[0]   888             format = (details[1:] or ["ical"])[0]   889         except (KeyError, ValueError):   890             pass   891         else:   892             # Prevent local file access.   893    894             if url.startswith("file:"):   895                 continue   896    897             # Parameterise the URL.   898             # Where other parameters are used, care must be taken to encode them   899             # properly.   900    901             url = url.replace("{start}", urllib.quote_plus(calendar_start and str(calendar_start) or ""))   902             url = url.replace("{end}", urllib.quote_plus(calendar_end and str(calendar_end) or ""))   903    904             # Get a parser.   905             # NOTE: This could be done reactively by choosing a parser based on   906             # NOTE: the content type provided by the URL.   907    908             if format == "ical" and vCalendar is not None:   909                 parser = vCalendar.parse   910                 resource_cls = EventCalendar   911                 required_content_type = "text/calendar"   912             else:   913                 continue   914    915             # Obtain the resource, using a cached version if appropriate.   916    917             max_cache_age = int(getattr(request.cfg, "event_aggregator_max_cache_age", "300"))   918             data = getCachedResource(request, url, "EventAggregator", "wiki", max_cache_age)   919             if not data:   920                 continue   921    922             # Process the entry, parsing the content.   923    924             f = StringIO(data)   925             try:   926                 url = f.readline()   927    928                 # Get the content type and encoding, making sure that the data   929                 # can be parsed.   930    931                 content_type, encoding = getContentTypeAndEncoding(f.readline())   932                 if content_type != required_content_type:   933                     continue   934    935                 # Send the data to the parser.   936    937                 uf = codecs.getreader(encoding or "utf-8")(f)   938                 try:   939                     resources.append(resource_cls(url, parser(uf)))   940                 finally:   941                     uf.close()   942             finally:   943                 f.close()   944    945     return resources   946    947 def getEventsFromResources(resources):   948    949     "Return a list of events supplied by the given event 'resources'."   950    951     events = []   952    953     for resource in resources:   954    955         # Get all events described by the resource.   956    957         for event in resource.getEvents():   958    959             # Remember the event.   960    961             events.append(event)   962    963     return events   964    965 # Event filtering and limits.   966    967 def getEventsInPeriod(events, calendar_period):   968    969     """   970     Return a collection containing those of the given 'events' which occur   971     within the given 'calendar_period'.   972     """   973    974     all_shown_events = []   975    976     for event in events:   977    978         # Test for the suitability of the event.   979    980         if event.as_timespan() is not None:   981    982             # Compare the dates to the requested calendar window, if any.   983    984             if event in calendar_period:   985                 all_shown_events.append(event)   986    987     return all_shown_events   988    989 def getEventLimits(events):   990    991     "Return the earliest and latest of the given 'events'."   992    993     earliest = None   994     latest = None   995    996     for event in events:   997    998         # Test for the suitability of the event.   999   1000         if event.as_timespan() is not None:  1001             ts = event.as_timespan()  1002             if earliest is None or ts.start < earliest:  1003                 earliest = ts.start  1004             if latest is None or ts.end > latest:  1005                 latest = ts.end  1006   1007     return earliest, latest  1008   1009 def getLatestEventTimestamp(events):  1010   1011     """  1012     Return the latest timestamp found from the given 'events'.  1013     """  1014   1015     latest = None  1016   1017     for event in events:  1018         metadata = event.getMetadata()  1019   1020         if latest is None or latest < metadata["last-modified"]:  1021             latest = metadata["last-modified"]  1022   1023     return latest  1024   1025 def getOrderedEvents(events):  1026   1027     """  1028     Return a list with the given 'events' ordered according to their start and  1029     end dates.  1030     """  1031   1032     ordered_events = events[:]  1033     ordered_events.sort()  1034     return ordered_events  1035   1036 def getCalendarPeriod(calendar_start, calendar_end):  1037   1038     """  1039     Return a calendar period for the given 'calendar_start' and 'calendar_end'.  1040     These parameters can be given as None.  1041     """  1042   1043     # Re-order the window, if appropriate.  1044   1045     if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end:  1046         calendar_start, calendar_end = calendar_end, calendar_start  1047   1048     return Timespan(calendar_start, calendar_end)  1049   1050 def getConcretePeriod(calendar_start, calendar_end, earliest, latest, resolution):  1051   1052     """  1053     From the requested 'calendar_start' and 'calendar_end', which may be None,  1054     indicating that no restriction is imposed on the period for each of the  1055     boundaries, use the 'earliest' and 'latest' event months to define a  1056     specific period of interest.  1057     """  1058   1059     # Define the period as starting with any specified start month or the  1060     # earliest event known, ending with any specified end month or the latest  1061     # event known.  1062   1063     first = calendar_start or earliest  1064     last = calendar_end or latest  1065   1066     # If there is no range of months to show, perhaps because there are no  1067     # events in the requested period, and there was no start or end month  1068     # specified, show only the month indicated by the start or end of the  1069     # requested period. If all events were to be shown but none were found show  1070     # the current month.  1071   1072     if resolution == "date":  1073         get_current = getCurrentDate  1074     else:  1075         get_current = getCurrentMonth  1076   1077     if first is None:  1078         first = last or get_current()  1079     if last is None:  1080         last = first or get_current()  1081   1082     if resolution == "month":  1083         first = first.as_month()  1084         last = last.as_month()  1085   1086     # Permit "expiring" periods (where the start date approaches the end date).  1087   1088     return min(first, last), last  1089   1090 def getCoverage(events, resolution="date"):  1091   1092     """  1093     Determine the coverage of the given 'events', returning a collection of  1094     timespans, along with a dictionary mapping locations to collections of  1095     slots, where each slot contains a tuple of the form (timespans, events).  1096     """  1097   1098     all_events = {}  1099     full_coverage = TimespanCollection(resolution)  1100   1101     # Get event details.  1102   1103     for event in events:  1104         event_details = event.getDetails()  1105   1106         # Find the coverage of this period for the event.  1107   1108         # For day views, each location has its own slot, but for month  1109         # views, all locations are pooled together since having separate  1110         # slots for each location can lead to poor usage of vertical space.  1111   1112         if resolution == "datetime":  1113             event_location = event_details.get("location")  1114         else:  1115             event_location = None  1116   1117         # Update the overall coverage.  1118   1119         full_coverage.insert_in_order(event)  1120   1121         # Add a new events list for a new location.  1122         # Locations can be unspecified, thus None refers to all unlocalised  1123         # events.  1124   1125         if not all_events.has_key(event_location):  1126             all_events[event_location] = [TimespanCollection(resolution, [event])]  1127   1128         # Try and fit the event into an events list.  1129   1130         else:  1131             slot = all_events[event_location]  1132   1133             for slot_events in slot:  1134   1135                 # Where the event does not overlap with the events in the  1136                 # current collection, add it alongside these events.  1137   1138                 if not event in slot_events:  1139                     slot_events.insert_in_order(event)  1140                     break  1141   1142             # Make a new element in the list if the event cannot be  1143             # marked alongside existing events.  1144   1145             else:  1146                 slot.append(TimespanCollection(resolution, [event]))  1147   1148     return full_coverage, all_events  1149   1150 def getCoverageScale(coverage):  1151   1152     """  1153     Return a scale for the given coverage so that the times involved are  1154     exposed. The scale consists of a list of non-overlapping timespans forming  1155     a contiguous period of time.  1156     """  1157   1158     times = set()  1159     for timespan in coverage:  1160         start, end = timespan.as_limits()  1161   1162         # Add either genuine times or dates converted to times.  1163   1164         if isinstance(start, DateTime):  1165             times.add(start)  1166         else:  1167             times.add(start.as_start_of_day())  1168   1169         if isinstance(end, DateTime):  1170             times.add(end)  1171         else:  1172             times.add(end.as_date().next_day())  1173   1174     times = list(times)  1175     times.sort(cmp_dates_as_day_start)  1176   1177     scale = []  1178     first = 1  1179     start = None  1180     for time in times:  1181         if not first:  1182             scale.append(Timespan(start, time))  1183         else:  1184             first = 0  1185         start = time  1186   1187     return scale  1188   1189 # Event sorting.  1190   1191 def sort_start_first(x, y):  1192     x_ts = x.as_limits()  1193     if x_ts is not None:  1194         x_start, x_end = x_ts  1195         y_ts = y.as_limits()  1196         if y_ts is not None:  1197             y_start, y_end = y_ts  1198             start_order = cmp(x_start, y_start)  1199             if start_order == 0:  1200                 return cmp(x_end, y_end)  1201             else:  1202                 return start_order  1203     return 0  1204   1205 # Country code parsing.  1206   1207 def getCountry(s):  1208   1209     "Find a country code in the given string 's'."  1210   1211     match = country_code_regexp.search(s)  1212   1213     if match:  1214         return match.group("code")  1215     else:  1216         return None  1217   1218 # Page-related functions.  1219   1220 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames):  1221   1222     """  1223     Using the given 'template_page', complete the 'new_page' by copying the  1224     template and adding the given 'event_details' (a dictionary of event  1225     fields), setting also the 'category_pagenames' to define category  1226     membership.  1227     """  1228   1229     event_page = EventPage(template_page)  1230     new_event_page = EventPage(new_page)  1231     new_event_page.copyPage(event_page)  1232   1233     if new_event_page.getFormat() == "wiki":  1234         new_event = Event(new_event_page, event_details)  1235         new_event_page.setEvents([new_event])  1236         new_event_page.setCategoryMembership(category_pagenames)  1237         new_event_page.flushEventDetails()  1238   1239     return new_event_page.getBody()  1240   1241 def getMapsPage(request):  1242     return getattr(request.cfg, "event_aggregator_maps_page", "EventMapsDict")  1243   1244 def getLocationsPage(request):  1245     return getattr(request.cfg, "event_aggregator_locations_page", "EventLocationsDict")  1246   1247 class Location:  1248   1249     """  1250     A representation of a location acquired from the locations dictionary.  1251   1252     The locations dictionary is a mapping from location to a string containing  1253     white-space-separated values describing...  1254   1255       * The latitude and longitude of the location.  1256       * Optionally, the time regime used by the location.  1257     """  1258   1259     def __init__(self, location, locations):  1260   1261         """  1262         Initialise the given 'location' using the 'locations' dictionary  1263         provided.  1264         """  1265   1266         self.location = location  1267   1268         try:  1269             self.data = locations[location].split()  1270         except KeyError:  1271             self.data = []  1272   1273     def getPosition(self):  1274   1275         """  1276         Attempt to return the position of this location. If no position can be  1277         found, return a latitude of None and a longitude of None.  1278         """  1279   1280         try:  1281             latitude, longitude = map(getMapReference, self.data[:2])  1282             return latitude, longitude  1283         except ValueError:  1284             return None, None  1285   1286     def getTimeRegime(self):  1287   1288         """  1289         Attempt to return the time regime employed at this location. If no  1290         regime has been specified, return None.  1291         """  1292   1293         try:  1294             return self.data[2]  1295         except IndexError:  1296             return None  1297   1298 # User interface abstractions.  1299   1300 class View:  1301   1302     "A view of the event calendar."  1303   1304     def __init__(self, page, calendar_name,  1305         raw_calendar_start, raw_calendar_end,  1306         original_calendar_start, original_calendar_end,  1307         calendar_start, calendar_end,  1308         wider_calendar_start, wider_calendar_end,  1309         first, last, category_names, remote_sources, search_pattern, template_name,  1310         parent_name, mode, resolution, name_usage, map_name):  1311   1312         """  1313         Initialise the view with the current 'page', a 'calendar_name' (which  1314         may be None), the 'raw_calendar_start' and 'raw_calendar_end' (which  1315         are the actual start and end values provided by the request), the  1316         calculated 'original_calendar_start' and 'original_calendar_end' (which  1317         are the result of calculating the calendar's limits from the raw start  1318         and end values), the requested, calculated 'calendar_start' and  1319         'calendar_end' (which may involve different start and end values due to  1320         navigation in the user interface), and the requested  1321         'wider_calendar_start' and 'wider_calendar_end' (which indicate a wider  1322         view used when navigating out of the day view), along with the 'first'  1323         and 'last' months of event coverage.  1324   1325         The additional 'category_names', 'remote_sources', 'search_pattern',  1326         'template_name', 'parent_name' and 'mode' parameters are used to  1327         configure the links employed by the view.  1328   1329         The 'resolution' affects the view for certain modes and is also used to  1330         parameterise links.  1331   1332         The 'name_usage' parameter controls how names are shown on calendar mode  1333         events, such as how often labels are repeated.  1334   1335         The 'map_name' parameter provides the name of a map to be used in the  1336         map mode.  1337         """  1338   1339         self.page = page  1340         self.calendar_name = calendar_name  1341         self.raw_calendar_start = raw_calendar_start  1342         self.raw_calendar_end = raw_calendar_end  1343         self.original_calendar_start = original_calendar_start  1344         self.original_calendar_end = original_calendar_end  1345         self.calendar_start = calendar_start  1346         self.calendar_end = calendar_end  1347         self.wider_calendar_start = wider_calendar_start  1348         self.wider_calendar_end = wider_calendar_end  1349         self.template_name = template_name  1350         self.parent_name = parent_name  1351         self.mode = mode  1352         self.resolution = resolution  1353         self.name_usage = name_usage  1354         self.map_name = map_name  1355   1356         # Search-related parameters for links.  1357   1358         self.category_name_parameters = "&".join([("category=%s" % name) for name in category_names])  1359         self.remote_source_parameters = "&".join([("source=%s" % source) for source in remote_sources])  1360         self.search_pattern = search_pattern  1361   1362         # Calculate the duration in terms of the highest common unit of time.  1363   1364         self.first = first  1365         self.last = last  1366         self.duration = abs(last - first) + 1  1367   1368         if self.calendar_name:  1369   1370             # Store the view parameters.  1371   1372             self.previous_start = first.previous()  1373             self.next_start = first.next()  1374             self.previous_end = last.previous()  1375             self.next_end = last.next()  1376   1377             self.previous_set_start = first.update(-self.duration)  1378             self.next_set_start = first.update(self.duration)  1379             self.previous_set_end = last.update(-self.duration)  1380             self.next_set_end = last.update(self.duration)  1381   1382     def getIdentifier(self):  1383   1384         "Return a unique identifier to be used to refer to this view."  1385   1386         # NOTE: Nasty hack to get a unique identifier if no name is given.  1387   1388         return self.calendar_name or str(id(self))  1389   1390     def getQualifiedParameterName(self, argname):  1391   1392         "Return the 'argname' qualified using the calendar name."  1393   1394         return getQualifiedParameterName(self.calendar_name, argname)  1395   1396     def getDateQueryString(self, argname, date, prefix=1):  1397   1398         """  1399         Return a query string fragment for the given 'argname', referring to the  1400         month given by the specified 'year_month' object, appropriate for this  1401         calendar.  1402   1403         If 'prefix' is specified and set to a false value, the parameters in the  1404         query string will not be calendar-specific, but could be used with the  1405         summary action.  1406         """  1407   1408         suffixes = ["year", "month", "day"]  1409   1410         if date is not None:  1411             args = []  1412             for suffix, value in zip(suffixes, date.as_tuple()):  1413                 suffixed_argname = "%s-%s" % (argname, suffix)  1414                 if prefix:  1415                     suffixed_argname = self.getQualifiedParameterName(suffixed_argname)  1416                 args.append("%s=%s" % (suffixed_argname, value))  1417             return "&".join(args)  1418         else:  1419             return ""  1420   1421     def getRawDateQueryString(self, argname, date, prefix=1):  1422   1423         """  1424         Return a query string fragment for the given 'argname', referring to the  1425         date given by the specified 'date' value, appropriate for this  1426         calendar.  1427   1428         If 'prefix' is specified and set to a false value, the parameters in the  1429         query string will not be calendar-specific, but could be used with the  1430         summary action.  1431         """  1432   1433         if date is not None:  1434             if prefix:  1435                 argname = self.getQualifiedParameterName(argname)  1436             return "%s=%s" % (argname, wikiutil.url_quote_plus(date))  1437         else:  1438             return ""  1439   1440     def getNavigationLink(self, start, end, mode=None, resolution=None, wider_start=None, wider_end=None):  1441   1442         """  1443         Return a query string fragment for navigation to a view showing months  1444         from 'start' to 'end' inclusive, with the optional 'mode' indicating the  1445         view style and the optional 'resolution' indicating the resolution of a  1446         view, if configurable.  1447   1448         If the 'wider_start' and 'wider_end' arguments are given, parameters  1449         indicating a wider calendar view (when returning from a day view, for  1450         example) will be included in the link.  1451         """  1452   1453         return "%s&%s&%s=%s&%s=%s&%s&%s" % (  1454             self.getRawDateQueryString("start", start),  1455             self.getRawDateQueryString("end", end),  1456             self.getQualifiedParameterName("mode"), mode or self.mode,  1457             self.getQualifiedParameterName("resolution"), resolution or self.resolution,  1458             self.getRawDateQueryString("wider-start", wider_start),  1459             self.getRawDateQueryString("wider-end", wider_end),  1460             )  1461   1462     def getUpdateLink(self, start, end, mode=None, resolution=None, wider_start=None, wider_end=None):  1463   1464         """  1465         Return a query string fragment for navigation to a view showing months  1466         from 'start' to 'end' inclusive, with the optional 'mode' indicating the  1467         view style and the optional 'resolution' indicating the resolution of a  1468         view, if configurable. This link differs from the conventional  1469         navigation link in that it is sufficient to activate the update action  1470         and produce an updated region of the page without needing to locate and  1471         process the page or any macro invocation.  1472   1473         If the 'wider_start' and 'wider_end' arguments are given, parameters  1474         indicating a wider calendar view (when returning from a day view, for  1475         example) will be included in the link.  1476         """  1477   1478         parameters = [  1479             self.getRawDateQueryString("start", start, 0),  1480             self.getRawDateQueryString("end", end, 0),  1481             self.category_name_parameters,  1482             self.remote_source_parameters,  1483             self.getRawDateQueryString("wider-start", wider_start, 0),  1484             self.getRawDateQueryString("wider-end", wider_end, 0),  1485             ]  1486   1487         pairs = [  1488             ("calendar", self.calendar_name or ""),  1489             ("calendarstart", self.raw_calendar_start or ""),  1490             ("calendarend", self.raw_calendar_end or ""),  1491             ("mode", mode or self.mode),  1492             ("resolution", resolution or self.resolution),  1493             ("parent", self.parent_name or ""),  1494             ("template", self.template_name or ""),  1495             ("names", self.name_usage),  1496             ("map", self.map_name or ""),  1497             ("search", self.search_pattern or ""),  1498             ]  1499   1500         url = self.page.url(self.page.request,  1501             "action=EventAggregatorUpdate&%s" % (  1502                 "&".join([("%s=%s" % pair) for pair in pairs] + parameters)  1503             ), relative=True)  1504   1505         return "return replaceCalendar('EventAggregator-%s', '%s')" % (self.getIdentifier(), url)  1506   1507     def getNewEventLink(self, start):  1508   1509         """  1510         Return a query string activating the new event form, incorporating the  1511         calendar parameters, specialising the form for the given 'start' date or  1512         month.  1513         """  1514   1515         if start is not None:  1516             details = start.as_tuple()  1517             pairs = zip(["start-year=%d", "start-month=%d", "start-day=%d"], details)  1518             args = [(param % value) for (param, value) in pairs]  1519             args = "&".join(args)  1520         else:  1521             args = ""  1522   1523         # Prepare navigation details for the calendar shown with the new event  1524         # form.  1525   1526         navigation_link = self.getNavigationLink(  1527             self.calendar_start, self.calendar_end  1528             )  1529   1530         return "action=EventAggregatorNewEvent%s%s&template=%s&parent=%s&%s" % (  1531             args and "&%s" % args,  1532             self.category_name_parameters and "&%s" % self.category_name_parameters,  1533             self.template_name, self.parent_name or "",  1534             navigation_link)  1535   1536     def getFullDateLabel(self, date):  1537         return getFullDateLabel(self.page.request, date)  1538   1539     def getFullMonthLabel(self, year_month):  1540         return getFullMonthLabel(self.page.request, year_month)  1541   1542     def getFullLabel(self, arg):  1543         return self.resolution == "date" and self.getFullDateLabel(arg) or self.getFullMonthLabel(arg)  1544   1545     def _getCalendarPeriod(self, start_label, end_label, default_label):  1546   1547         """  1548         Return a label describing a calendar period in terms of the given  1549         'start_label' and 'end_label', with the 'default_label' being used where  1550         the supplied start and end labels fail to produce a meaningful label.  1551         """  1552   1553         output = []  1554         append = output.append  1555   1556         if start_label:  1557             append(start_label)  1558         if end_label and start_label != end_label:  1559             if output:  1560                 append(" - ")  1561             append(end_label)  1562         return "".join(output) or default_label  1563   1564     def getCalendarPeriod(self):  1565         _ = self.page.request.getText  1566         return self._getCalendarPeriod(  1567             self.calendar_start and self.getFullLabel(self.calendar_start),  1568             self.calendar_end and self.getFullLabel(self.calendar_end),  1569             _("All events")  1570             )  1571   1572     def getOriginalCalendarPeriod(self):  1573         _ = self.page.request.getText  1574         return self._getCalendarPeriod(  1575             self.original_calendar_start and self.getFullLabel(self.original_calendar_start),  1576             self.original_calendar_end and self.getFullLabel(self.original_calendar_end),  1577             _("All events")  1578             )  1579   1580     def getRawCalendarPeriod(self):  1581         _ = self.page.request.getText  1582         return self._getCalendarPeriod(  1583             self.raw_calendar_start,  1584             self.raw_calendar_end,  1585              _("No period specified")  1586             )  1587   1588     def writeDownloadControls(self):  1589   1590         """  1591         Return a representation of the download controls, featuring links for  1592         view, calendar and customised downloads and subscriptions.  1593         """  1594   1595         page = self.page  1596         request = page.request  1597         fmt = request.formatter  1598         _ = request.getText  1599   1600         output = []  1601         append = output.append  1602   1603         # The full URL is needed for webcal links.  1604   1605         full_url = "%s%s" % (request.getBaseURL(), getPathInfo(request))  1606   1607         # Generate the links.  1608   1609         download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s&search=%s%s%s" % (  1610             self.parent_name or "",  1611             self.resolution,  1612             self.search_pattern or "",  1613             self.category_name_parameters and "&%s" % self.category_name_parameters,  1614             self.remote_source_parameters and "&%s" % self.remote_source_parameters  1615             )  1616         download_all_link = download_dialogue_link + "&doit=1"  1617         download_link = download_all_link + ("&%s&%s" % (  1618             self.getDateQueryString("start", self.calendar_start, prefix=0),  1619             self.getDateQueryString("end", self.calendar_end, prefix=0)  1620             ))  1621   1622         # Subscription links just explicitly select the RSS format.  1623   1624         subscribe_dialogue_link = download_dialogue_link + "&format=RSS"  1625         subscribe_all_link = download_all_link + "&format=RSS"  1626         subscribe_link = download_link + "&format=RSS"  1627   1628         # Adjust the "download all" and "subscribe all" links if the calendar  1629         # has an inherent period associated with it.  1630   1631         period_limits = []  1632   1633         if self.raw_calendar_start:  1634             period_limits.append("&%s" %  1635                 self.getRawDateQueryString("start", self.raw_calendar_start, prefix=0)  1636                 )  1637         if self.raw_calendar_end:  1638             period_limits.append("&%s" %  1639                 self.getRawDateQueryString("end", self.raw_calendar_end, prefix=0)  1640                 )  1641   1642         period_limits = "".join(period_limits)  1643   1644         download_dialogue_link += period_limits  1645         download_all_link += period_limits  1646         subscribe_dialogue_link += period_limits  1647         subscribe_all_link += period_limits  1648   1649         # Pop-up descriptions of the downloadable calendars.  1650   1651         calendar_period = self.getCalendarPeriod()  1652         original_calendar_period = self.getOriginalCalendarPeriod()  1653         raw_calendar_period = self.getRawCalendarPeriod()  1654   1655         # Write the controls.  1656   1657         # Download controls.  1658   1659         append(fmt.div(on=1, css_class="event-download-controls"))  1660   1661         append(fmt.span(on=1, css_class="event-download"))  1662         append(fmt.text(_("Download...")))  1663         append(fmt.div(on=1, css_class="event-download-popup"))  1664   1665         append(fmt.div(on=1, css_class="event-download-item"))  1666         append(fmt.span(on=1, css_class="event-download-types"))  1667         append(fmt.span(on=1, css_class="event-download-webcal"))  1668         append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_link))  1669         append(fmt.span(on=0))  1670         append(fmt.span(on=1, css_class="event-download-http"))  1671         append(linkToPage(request, page, _("http"), download_link))  1672         append(fmt.span(on=0))  1673         append(fmt.span(on=0)) # end types  1674         append(fmt.span(on=1, css_class="event-download-label"))  1675         append(fmt.text(_("Download this view")))  1676         append(fmt.span(on=0)) # end label  1677         append(fmt.span(on=1, css_class="event-download-period"))  1678         append(fmt.text(calendar_period))  1679         append(fmt.span(on=0))  1680         append(fmt.div(on=0))  1681   1682         append(fmt.div(on=1, css_class="event-download-item"))  1683         append(fmt.span(on=1, css_class="event-download-types"))  1684         append(fmt.span(on=1, css_class="event-download-webcal"))  1685         append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_all_link))  1686         append(fmt.span(on=0))  1687         append(fmt.span(on=1, css_class="event-download-http"))  1688         append(linkToPage(request, page, _("http"), download_all_link))  1689         append(fmt.span(on=0))  1690         append(fmt.span(on=0)) # end types  1691         append(fmt.span(on=1, css_class="event-download-label"))  1692         append(fmt.text(_("Download this calendar")))  1693         append(fmt.span(on=0)) # end label  1694         append(fmt.span(on=1, css_class="event-download-period"))  1695         append(fmt.text(original_calendar_period))  1696         append(fmt.span(on=0))  1697         append(fmt.span(on=1, css_class="event-download-period-raw"))  1698         append(fmt.text(raw_calendar_period))  1699         append(fmt.span(on=0))  1700         append(fmt.div(on=0))  1701   1702         append(fmt.div(on=1, css_class="event-download-item"))  1703         append(fmt.span(on=1, css_class="event-download-link"))  1704         append(linkToPage(request, page, _("Edit download options..."), download_dialogue_link))  1705         append(fmt.span(on=0)) # end label  1706         append(fmt.div(on=0))  1707   1708         append(fmt.div(on=0)) # end of pop-up  1709         append(fmt.span(on=0)) # end of download  1710   1711         # Subscription controls.  1712   1713         append(fmt.span(on=1, css_class="event-download"))  1714         append(fmt.text(_("Subscribe...")))  1715         append(fmt.div(on=1, css_class="event-download-popup"))  1716   1717         append(fmt.div(on=1, css_class="event-download-item"))  1718         append(fmt.span(on=1, css_class="event-download-label"))  1719         append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link))  1720         append(fmt.span(on=0)) # end label  1721         append(fmt.span(on=1, css_class="event-download-period"))  1722         append(fmt.text(calendar_period))  1723         append(fmt.span(on=0))  1724         append(fmt.div(on=0))  1725   1726         append(fmt.div(on=1, css_class="event-download-item"))  1727         append(fmt.span(on=1, css_class="event-download-label"))  1728         append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link))  1729         append(fmt.span(on=0)) # end label  1730         append(fmt.span(on=1, css_class="event-download-period"))  1731         append(fmt.text(original_calendar_period))  1732         append(fmt.span(on=0))  1733         append(fmt.span(on=1, css_class="event-download-period-raw"))  1734         append(fmt.text(raw_calendar_period))  1735         append(fmt.span(on=0))  1736         append(fmt.div(on=0))  1737   1738         append(fmt.div(on=1, css_class="event-download-item"))  1739         append(fmt.span(on=1, css_class="event-download-link"))  1740         append(linkToPage(request, page, _("Edit subscription options..."), subscribe_dialogue_link))  1741         append(fmt.span(on=0)) # end label  1742         append(fmt.div(on=0))  1743   1744         append(fmt.div(on=0)) # end of pop-up  1745         append(fmt.span(on=0)) # end of download  1746   1747         append(fmt.div(on=0)) # end of controls  1748   1749         return "".join(output)  1750   1751     def writeViewControls(self):  1752   1753         """  1754         Return a representation of the view mode controls, permitting viewing of  1755         aggregated events in calendar, list or table form.  1756         """  1757   1758         page = self.page  1759         request = page.request  1760         fmt = request.formatter  1761         _ = request.getText  1762   1763         output = []  1764         append = output.append  1765   1766         # For day view links to other views, the wider view parameters should  1767         # be used in order to be able to return to those other views.  1768   1769         specific_start = self.calendar_start  1770         specific_end = self.calendar_end  1771   1772         start = self.wider_calendar_start or self.original_calendar_start and specific_start  1773         end = self.wider_calendar_end or self.original_calendar_end and specific_end  1774   1775         help_page = Page(request, "HelpOnEventAggregator")  1776   1777         calendar_link = self.getNavigationLink(start and start.as_month(), end and end.as_month(), "calendar", "month")  1778         calendar_update_link = self.getUpdateLink(start and start.as_month(), end and end.as_month(), "calendar", "month")  1779         list_link = self.getNavigationLink(start, end, "list", "month")  1780         list_update_link = self.getUpdateLink(start, end, "list", "month")  1781         table_link = self.getNavigationLink(start, end, "table", "month")  1782         table_update_link = self.getUpdateLink(start, end, "table", "month")  1783         map_link = self.getNavigationLink(start, end, "map", "month")  1784         map_update_link = self.getUpdateLink(start, end, "map", "month")  1785   1786         # Specific links permit date-level navigation.  1787   1788         specific_day_link = self.getNavigationLink(specific_start, specific_end, "day", wider_start=start, wider_end=end)  1789         specific_day_update_link = self.getUpdateLink(specific_start, specific_end, "day", wider_start=start, wider_end=end)  1790         specific_list_link = self.getNavigationLink(specific_start, specific_end, "list", wider_start=start, wider_end=end)  1791         specific_list_update_link = self.getUpdateLink(specific_start, specific_end, "list", wider_start=start, wider_end=end)  1792         specific_table_link = self.getNavigationLink(specific_start, specific_end, "table", wider_start=start, wider_end=end)  1793         specific_table_update_link = self.getUpdateLink(specific_start, specific_end, "table", wider_start=start, wider_end=end)  1794         specific_map_link = self.getNavigationLink(specific_start, specific_end, "map", wider_start=start, wider_end=end)  1795         specific_map_update_link = self.getUpdateLink(specific_start, specific_end, "map", wider_start=start, wider_end=end)  1796   1797         new_event_link = self.getNewEventLink(start)  1798   1799         # Write the controls.  1800   1801         append(fmt.div(on=1, css_class="event-view-controls"))  1802   1803         append(fmt.span(on=1, css_class="event-view"))  1804         append(linkToPage(request, help_page, _("Help")))  1805         append(fmt.span(on=0))  1806   1807         append(fmt.span(on=1, css_class="event-view"))  1808         append(linkToPage(request, page, _("New event"), new_event_link))  1809         append(fmt.span(on=0))  1810   1811         if self.mode != "calendar":  1812             view_label = self.resolution == "date" and _("View day in calendar") or _("View as calendar")  1813             append(fmt.span(on=1, css_class="event-view"))  1814             append(linkToPage(request, page, view_label, calendar_link, onclick=calendar_update_link))  1815             append(fmt.span(on=0))  1816   1817         if self.resolution == "date" and self.mode != "day":  1818             append(fmt.span(on=1, css_class="event-view"))  1819             append(linkToPage(request, page, _("View day as calendar"), specific_day_link, onclick=specific_day_update_link))  1820             append(fmt.span(on=0))  1821   1822         if self.resolution != "date" and self.mode != "list" or self.resolution == "date":  1823             view_label = self.resolution == "date" and _("View day in list") or _("View as list")  1824             append(fmt.span(on=1, css_class="event-view"))  1825             append(linkToPage(request, page, view_label, list_link, onclick=list_update_link))  1826             append(fmt.span(on=0))  1827   1828         if self.resolution == "date" and self.mode != "list":  1829             append(fmt.span(on=1, css_class="event-view"))  1830             append(linkToPage(request, page, _("View day as list"),  1831                 specific_list_link, onclick=specific_list_update_link  1832                 ))  1833             append(fmt.span(on=0))  1834   1835         if self.resolution != "date" and self.mode != "table" or self.resolution == "date":  1836             view_label = self.resolution == "date" and _("View day in table") or _("View as table")  1837             append(fmt.span(on=1, css_class="event-view"))  1838             append(linkToPage(request, page, view_label, table_link, onclick=table_update_link))  1839             append(fmt.span(on=0))  1840   1841         if self.resolution == "date" and self.mode != "table":  1842             append(fmt.span(on=1, css_class="event-view"))  1843             append(linkToPage(request, page, _("View day as table"),  1844                 specific_table_link, onclick=specific_table_update_link  1845                 ))  1846             append(fmt.span(on=0))  1847   1848         if self.map_name:  1849             if self.resolution != "date" and self.mode != "map" or self.resolution == "date":  1850                 view_label = self.resolution == "date" and _("View day in map") or _("View as map")  1851                 append(fmt.span(on=1, css_class="event-view"))  1852                 append(linkToPage(request, page, view_label, map_link, onclick=map_update_link))  1853                 append(fmt.span(on=0))  1854   1855             if self.resolution == "date" and self.mode != "map":  1856                 append(fmt.span(on=1, css_class="event-view"))  1857                 append(linkToPage(request, page, _("View day as map"),  1858                     specific_map_link, onclick=specific_map_update_link  1859                     ))  1860                 append(fmt.span(on=0))  1861   1862         append(fmt.div(on=0))  1863   1864         return "".join(output)  1865   1866     def writeMapHeading(self):  1867   1868         """  1869         Return the calendar heading for the current calendar, providing links  1870         permitting navigation to other periods.  1871         """  1872   1873         label = self.getCalendarPeriod()  1874   1875         if self.raw_calendar_start is None or self.raw_calendar_end is None:  1876             fmt = self.page.request.formatter  1877             output = []  1878             append = output.append  1879             append(fmt.span(on=1))  1880             append(fmt.text(label))  1881             append(fmt.span(on=0))  1882             return "".join(output)  1883         else:  1884             return self._writeCalendarHeading(label, self.calendar_start, self.calendar_end)  1885   1886     def writeDateHeading(self, date):  1887         if isinstance(date, Date):  1888             return self.writeDayHeading(date)  1889         else:  1890             return self.writeMonthHeading(date)  1891   1892     def writeMonthHeading(self, year_month):  1893   1894         """  1895         Return the calendar heading for the given 'year_month' (a Month object)  1896         providing links permitting navigation to other months.  1897         """  1898   1899         full_month_label = self.getFullMonthLabel(year_month)  1900         end_month = year_month.update(self.duration - 1)  1901         return self._writeCalendarHeading(full_month_label, year_month, end_month)  1902   1903     def writeDayHeading(self, date):  1904   1905         """  1906         Return the calendar heading for the given 'date' (a Date object)  1907         providing links permitting navigation to other dates.  1908         """  1909   1910         full_date_label = self.getFullDateLabel(date)  1911         end_date = date.update(self.duration - 1)  1912         return self._writeCalendarHeading(full_date_label, date, end_date)  1913   1914     def _writeCalendarHeading(self, label, start, end):  1915   1916         """  1917         Write a calendar heading providing links permitting navigation to other  1918         periods, using the given 'label' along with the 'start' and 'end' dates  1919         to provide a link to a particular period.  1920         """  1921   1922         page = self.page  1923         request = page.request  1924         fmt = request.formatter  1925         _ = request.getText  1926   1927         output = []  1928         append = output.append  1929   1930         # Prepare navigation links.  1931   1932         if self.calendar_name:  1933             calendar_name = self.calendar_name  1934   1935             # Links to the previous set of months and to a calendar shifted  1936             # back one month.  1937   1938             previous_set_link = self.getNavigationLink(  1939                 self.previous_set_start, self.previous_set_end  1940                 )  1941             previous_link = self.getNavigationLink(  1942                 self.previous_start, self.previous_end  1943                 )  1944             previous_set_update_link = self.getUpdateLink(  1945                 self.previous_set_start, self.previous_set_end  1946                 )  1947             previous_update_link = self.getUpdateLink(  1948                 self.previous_start, self.previous_end  1949                 )  1950   1951             # Links to the next set of months and to a calendar shifted  1952             # forward one month.  1953   1954             next_set_link = self.getNavigationLink(  1955                 self.next_set_start, self.next_set_end  1956                 )  1957             next_link = self.getNavigationLink(  1958                 self.next_start, self.next_end  1959                 )  1960             next_set_update_link = self.getUpdateLink(  1961                 self.next_set_start, self.next_set_end  1962                 )  1963             next_update_link = self.getUpdateLink(  1964                 self.next_start, self.next_end  1965                 )  1966   1967             # A link leading to this date being at the top of the calendar.  1968   1969             date_link = self.getNavigationLink(start, end)  1970             date_update_link = self.getUpdateLink(start, end)  1971   1972             append(fmt.span(on=1, css_class="previous"))  1973             append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link))  1974             append(fmt.text(" "))  1975             append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link))  1976             append(fmt.span(on=0))  1977   1978             append(fmt.span(on=1, css_class="next"))  1979             append(linkToPage(request, page, ">", next_link, onclick=next_update_link))  1980             append(fmt.text(" "))  1981             append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link))  1982             append(fmt.span(on=0))  1983   1984             append(linkToPage(request, page, label, date_link, onclick=date_update_link))  1985   1986         else:  1987             append(fmt.span(on=1))  1988             append(fmt.text(label))  1989             append(fmt.span(on=0))  1990   1991         return "".join(output)  1992   1993     def writeDayNumberHeading(self, date, busy):  1994   1995         """  1996         Return a link for the given 'date' which will activate the new event  1997         action for the given day. If 'busy' is given as a true value, the  1998         heading will be marked as busy.  1999         """  2000   2001         page = self.page  2002         request = page.request  2003         fmt = request.formatter  2004         _ = request.getText  2005   2006         output = []  2007         append = output.append  2008   2009         year, month, day = date.as_tuple()  2010         new_event_link = self.getNewEventLink(date)  2011   2012         # Prepare a link to the day view for this day.  2013   2014         day_view_link = self.getNavigationLink(date, date, "day", "date", self.calendar_start, self.calendar_end)  2015         day_view_update_link = self.getUpdateLink(date, date, "day", "date", self.calendar_start, self.calendar_end)  2016   2017         # Output the heading class.  2018   2019         today_attr = date == getCurrentDate() and "event-day-current" or ""  2020   2021         append(  2022             fmt.table_cell(on=1, attrs={  2023                 "class" : "event-day-heading event-day-%s %s" % (busy and "busy" or "empty", today_attr),  2024                 "colspan" : "3"  2025                 }))  2026   2027         # Output the number and pop-up menu.  2028   2029         append(fmt.div(on=1, css_class="event-day-box"))  2030   2031         append(fmt.span(on=1, css_class="event-day-number-popup"))  2032         append(fmt.span(on=1, css_class="event-day-number-link"))  2033         append(linkToPage(request, page, _("View day"), day_view_link, onclick=day_view_update_link))  2034         append(fmt.span(on=0))  2035         append(fmt.span(on=1, css_class="event-day-number-link"))  2036         append(linkToPage(request, page, _("New event"), new_event_link))  2037         append(fmt.span(on=0))  2038         append(fmt.span(on=0))  2039   2040         append(fmt.span(on=1, css_class="event-day-number"))  2041         append(fmt.text(unicode(day)))  2042         append(fmt.span(on=0))  2043   2044         append(fmt.div(on=0))  2045   2046         # End of heading.  2047   2048         append(fmt.table_cell(on=0))  2049   2050         return "".join(output)  2051   2052     # Common layout methods.  2053   2054     def getEventStyle(self, colour_seed):  2055   2056         "Generate colour style information using the given 'colour_seed'."  2057   2058         bg = getColour(colour_seed)  2059         fg = getBlackOrWhite(bg)  2060         return "background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg)  2061   2062     def writeEventSummaryBox(self, event):  2063   2064         "Return an event summary box linking to the given 'event'."  2065   2066         page = self.page  2067         request = page.request  2068         fmt = request.formatter  2069   2070         output = []  2071         append = output.append  2072   2073         event_details = event.getDetails()  2074         event_summary = event.getSummary(self.parent_name)  2075   2076         is_ambiguous = event.as_timespan().ambiguous()  2077         style = self.getEventStyle(event_summary)  2078   2079         # The event box contains the summary, alongside  2080         # other elements.  2081   2082         append(fmt.div(on=1, css_class="event-summary-box"))  2083         append(fmt.div(on=1, css_class="event-summary", style=style))  2084   2085         if is_ambiguous:  2086             append(fmt.icon("/!\\"))  2087   2088         append(event.linkToEvent(request, event_summary))  2089         append(fmt.div(on=0))  2090   2091         # Add a pop-up element for long summaries.  2092   2093         append(fmt.div(on=1, css_class="event-summary-popup", style=style))  2094   2095         if is_ambiguous:  2096             append(fmt.icon("/!\\"))  2097   2098         append(event.linkToEvent(request, event_summary))  2099         append(fmt.div(on=0))  2100   2101         append(fmt.div(on=0))  2102   2103         return "".join(output)  2104   2105     # Calendar layout methods.  2106   2107     def writeMonthTableHeading(self, year_month):  2108         page = self.page  2109         fmt = page.request.formatter  2110   2111         output = []  2112         append = output.append  2113   2114         append(fmt.table_row(on=1))  2115         append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"}))  2116   2117         append(self.writeMonthHeading(year_month))  2118   2119         append(fmt.table_cell(on=0))  2120         append(fmt.table_row(on=0))  2121   2122         return "".join(output)  2123   2124     def writeWeekdayHeadings(self):  2125         page = self.page  2126         request = page.request  2127         fmt = request.formatter  2128         _ = request.getText  2129   2130         output = []  2131         append = output.append  2132   2133         append(fmt.table_row(on=1))  2134   2135         for weekday in range(0, 7):  2136             append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"}))  2137             append(fmt.text(_(getDayLabel(weekday))))  2138             append(fmt.table_cell(on=0))  2139   2140         append(fmt.table_row(on=0))  2141         return "".join(output)  2142   2143     def writeDayNumbers(self, first_day, number_of_days, month, coverage):  2144         page = self.page  2145         fmt = page.request.formatter  2146   2147         output = []  2148         append = output.append  2149   2150         append(fmt.table_row(on=1))  2151   2152         for weekday in range(0, 7):  2153             day = first_day + weekday  2154             date = month.as_date(day)  2155   2156             # Output out-of-month days.  2157   2158             if day < 1 or day > number_of_days:  2159                 append(fmt.table_cell(on=1,  2160                     attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"}))  2161                 append(fmt.table_cell(on=0))  2162   2163             # Output normal days.  2164   2165             else:  2166                 # Output the day heading, making a link to a new event  2167                 # action.  2168   2169                 append(self.writeDayNumberHeading(date, date in coverage))  2170   2171         # End of day numbers.  2172   2173         append(fmt.table_row(on=0))  2174         return "".join(output)  2175   2176     def writeEmptyWeek(self, first_day, number_of_days, month):  2177         page = self.page  2178         fmt = page.request.formatter  2179   2180         output = []  2181         append = output.append  2182   2183         append(fmt.table_row(on=1))  2184   2185         for weekday in range(0, 7):  2186             day = first_day + weekday  2187             date = month.as_date(day)  2188   2189             today_attr = date == getCurrentDate() and "event-day-current" or ""  2190   2191             # Output out-of-month days.  2192   2193             if day < 1 or day > number_of_days:  2194                 append(fmt.table_cell(on=1,  2195                     attrs={"class" : "event-day-content event-day-excluded %s" % today_attr, "colspan" : "3"}))  2196                 append(fmt.table_cell(on=0))  2197   2198             # Output empty days.  2199   2200             else:  2201                 append(fmt.table_cell(on=1,  2202                     attrs={"class" : "event-day-content event-day-empty %s" % today_attr, "colspan" : "3"}))  2203   2204         append(fmt.table_row(on=0))  2205         return "".join(output)  2206   2207     def writeWeekSlots(self, first_day, number_of_days, month, week_end, week_slots):  2208         output = []  2209         append = output.append  2210   2211         locations = week_slots.keys()  2212         locations.sort(sort_none_first)  2213   2214         # Visit each slot corresponding to a location (or no location).  2215   2216         for location in locations:  2217   2218             # Visit each coverage span, presenting the events in the span.  2219   2220             for events in week_slots[location]:  2221   2222                 # Output each set.  2223   2224                 append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events))  2225   2226                 # Add a spacer.  2227   2228                 append(self.writeWeekSpacer(first_day, number_of_days, month))  2229   2230         return "".join(output)  2231   2232     def writeWeekSlot(self, first_day, number_of_days, month, week_end, events):  2233         page = self.page  2234         request = page.request  2235         fmt = request.formatter  2236   2237         output = []  2238         append = output.append  2239   2240         append(fmt.table_row(on=1))  2241   2242         # Then, output day details.  2243   2244         for weekday in range(0, 7):  2245             day = first_day + weekday  2246             date = month.as_date(day)  2247   2248             # Skip out-of-month days.  2249   2250             if day < 1 or day > number_of_days:  2251                 append(fmt.table_cell(on=1,  2252                     attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"}))  2253                 append(fmt.table_cell(on=0))  2254                 continue  2255   2256             # Output the day.  2257             # Where a day does not contain an event, a single cell is used.  2258             # Otherwise, multiple cells are used to provide space before, during  2259             # and after events.  2260   2261             today_attr = date == getCurrentDate() and "event-day-current" or ""  2262   2263             if date not in events:  2264                 append(fmt.table_cell(on=1,  2265                     attrs={"class" : "event-day-content event-day-empty %s" % today_attr, "colspan" : "3"}))  2266   2267             # Get event details for the current day.  2268   2269             for event in events:  2270                 event_details = event.getDetails()  2271   2272                 if date not in event:  2273                     continue  2274   2275                 # Get basic properties of the event.  2276   2277                 starts_today = event_details["start"] == date  2278                 ends_today = event_details["end"] == date  2279                 event_summary = event.getSummary(self.parent_name)  2280   2281                 style = self.getEventStyle(event_summary)  2282   2283                 # Determine if the event name should be shown.  2284   2285                 start_of_period = starts_today or weekday == 0 or day == 1  2286   2287                 if self.name_usage == "daily" or start_of_period:  2288                     hide_text = 0  2289                 else:  2290                     hide_text = 1  2291   2292                 # Output start of day gap and determine whether  2293                 # any event content should be explicitly output  2294                 # for this day.  2295   2296                 if starts_today:  2297   2298                     # Single day events...  2299   2300                     if ends_today:  2301                         colspan = 3  2302                         event_day_type = "event-day-single"  2303   2304                     # Events starting today...  2305   2306                     else:  2307                         append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap %s" % today_attr}))  2308                         append(fmt.table_cell(on=0))  2309   2310                         # Calculate the span of this cell.  2311                         # Events whose names appear on every day...  2312   2313                         if self.name_usage == "daily":  2314                             colspan = 2  2315                             event_day_type = "event-day-starting"  2316   2317                         # Events whose names appear once per week...  2318   2319                         else:  2320                             if event_details["end"] <= week_end:  2321                                 event_length = event_details["end"].day() - day + 1  2322                                 colspan = (event_length - 2) * 3 + 4  2323                             else:  2324                                 event_length = week_end.day() - day + 1  2325                                 colspan = (event_length - 1) * 3 + 2  2326   2327                             event_day_type = "event-day-multiple"  2328   2329                 # Events continuing from a previous week...  2330   2331                 elif start_of_period:  2332   2333                     # End of continuing event...  2334   2335                     if ends_today:  2336                         colspan = 2  2337                         event_day_type = "event-day-ending"  2338   2339                     # Events continuing for at least one more day...  2340   2341                     else:  2342   2343                         # Calculate the span of this cell.  2344                         # Events whose names appear on every day...  2345   2346                         if self.name_usage == "daily":  2347                             colspan = 3  2348                             event_day_type = "event-day-full"  2349   2350                         # Events whose names appear once per week...  2351   2352                         else:  2353                             if event_details["end"] <= week_end:  2354                                 event_length = event_details["end"].day() - day + 1  2355                                 colspan = (event_length - 1) * 3 + 2  2356                             else:  2357                                 event_length = week_end.day() - day + 1  2358                                 colspan = event_length * 3  2359   2360                             event_day_type = "event-day-multiple"  2361   2362                 # Continuing events whose names appear on every day...  2363   2364                 elif self.name_usage == "daily":  2365                     if ends_today:  2366                         colspan = 2  2367                         event_day_type = "event-day-ending"  2368                     else:  2369                         colspan = 3  2370                         event_day_type = "event-day-full"  2371   2372                 # Continuing events whose names appear once per week...  2373   2374                 else:  2375                     colspan = None  2376   2377                 # Output the main content only if it is not  2378                 # continuing from a previous day.  2379   2380                 if colspan is not None:  2381   2382                     # Colour the cell for continuing events.  2383   2384                     attrs={  2385                         "class" : "event-day-content event-day-busy %s %s" % (event_day_type, today_attr),  2386                         "colspan" : str(colspan)  2387                         }  2388   2389                     if not (starts_today and ends_today):  2390                         attrs["style"] = style  2391   2392                     append(fmt.table_cell(on=1, attrs=attrs))  2393   2394                     # Output the event.  2395   2396                     if starts_today and ends_today or not hide_text:  2397                         append(self.writeEventSummaryBox(event))  2398   2399                     append(fmt.table_cell(on=0))  2400   2401                 # Output end of day gap.  2402   2403                 if ends_today and not starts_today:  2404                     append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap %s" % today_attr}))  2405                     append(fmt.table_cell(on=0))  2406   2407         # End of set.  2408   2409         append(fmt.table_row(on=0))  2410         return "".join(output)  2411   2412     def writeWeekSpacer(self, first_day, number_of_days, month):  2413         page = self.page  2414         fmt = page.request.formatter  2415   2416         output = []  2417         append = output.append  2418   2419         append(fmt.table_row(on=1))  2420   2421         for weekday in range(0, 7):  2422             day = first_day + weekday  2423             date = month.as_date(day)  2424             today_attr = date == getCurrentDate() and "event-day-current" or ""  2425   2426             css_classes = "event-day-spacer %s" % today_attr  2427   2428             # Skip out-of-month days.  2429   2430             if day < 1 or day > number_of_days:  2431                 css_classes += " event-day-excluded"  2432   2433             append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"}))  2434             append(fmt.table_cell(on=0))  2435   2436         append(fmt.table_row(on=0))  2437         return "".join(output)  2438   2439     # Day layout methods.  2440   2441     def writeDayTableHeading(self, date, colspan=1):  2442         page = self.page  2443         fmt = page.request.formatter  2444   2445         output = []  2446         append = output.append  2447   2448         append(fmt.table_row(on=1))  2449   2450         append(fmt.table_cell(on=1, attrs={"class" : "event-full-day-heading", "colspan" : str(colspan)}))  2451         append(self.writeDayHeading(date))  2452         append(fmt.table_cell(on=0))  2453   2454         append(fmt.table_row(on=0))  2455         return "".join(output)  2456   2457     def writeEmptyDay(self, date):  2458         page = self.page  2459         fmt = page.request.formatter  2460   2461         output = []  2462         append = output.append  2463   2464         append(fmt.table_row(on=1))  2465   2466         append(fmt.table_cell(on=1,  2467             attrs={"class" : "event-day-content event-day-empty"}))  2468   2469         append(fmt.table_row(on=0))  2470         return "".join(output)  2471   2472     def writeDaySlots(self, date, full_coverage, day_slots):  2473   2474         """  2475         Given a 'date', non-empty 'full_coverage' for the day concerned, and a  2476         non-empty mapping of 'day_slots' (from locations to event collections),  2477         output the day slots for the day.  2478         """  2479   2480         page = self.page  2481         fmt = page.request.formatter  2482   2483         output = []  2484         append = output.append  2485   2486         locations = day_slots.keys()  2487         locations.sort(sort_none_first)  2488   2489         # Traverse the time scale of the full coverage, visiting each slot to  2490         # determine whether it provides content for each period.  2491   2492         scale = getCoverageScale(full_coverage)  2493   2494         # Define a mapping of events to rowspans.  2495   2496         rowspans = {}  2497   2498         # Populate each period with event details, recording how many periods  2499         # each event populates.  2500   2501         day_rows = []  2502   2503         for period in scale:  2504   2505             # Ignore timespans before this day.  2506   2507             if period != date:  2508                 continue  2509   2510             # Visit each slot corresponding to a location (or no location).  2511   2512             day_row = []  2513   2514             for location in locations:  2515   2516                 # Visit each coverage span, presenting the events in the span.  2517   2518                 for events in day_slots[location]:  2519                     event = self.getActiveEvent(period, events)  2520                     if event is not None:  2521                         if not rowspans.has_key(event):  2522                             rowspans[event] = 1  2523                         else:  2524                             rowspans[event] += 1  2525                     day_row.append((location, event))  2526   2527             day_rows.append((period, day_row))  2528   2529         # Output the locations.  2530   2531         append(fmt.table_row(on=1))  2532   2533         # Add a spacer.  2534   2535         append(self.writeDaySpacer(colspan=2, cls="location"))  2536   2537         for location in locations:  2538   2539             # Add spacers to the column spans.  2540   2541             columns = len(day_slots[location]) * 2 - 1  2542             append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)}))  2543             append(fmt.text(location or ""))  2544             append(fmt.table_cell(on=0))  2545   2546             # Add a trailing spacer.  2547   2548             append(self.writeDaySpacer(cls="location"))  2549   2550         append(fmt.table_row(on=0))  2551   2552         # Output the periods with event details.  2553   2554         period = None  2555         events_written = set()  2556   2557         for period, day_row in day_rows:  2558   2559             # Write an empty heading for the start of the day where the first  2560             # applicable timespan starts before this day.  2561   2562             if period.start < date:  2563                 append(fmt.table_row(on=1))  2564                 append(self.writeDayScaleHeading(""))  2565   2566             # Otherwise, write a heading describing the time.  2567   2568             else:  2569                 append(fmt.table_row(on=1))  2570                 append(self.writeDayScaleHeading(period.start.time_string()))  2571   2572             append(self.writeDaySpacer())  2573   2574             # Visit each slot corresponding to a location (or no location).  2575   2576             for location, event in day_row:  2577   2578                 # Output each location slot's contribution.  2579   2580                 if event is None or event not in events_written:  2581                     append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event]))  2582                     if event is not None:  2583                         events_written.add(event)  2584   2585                 # Add a trailing spacer.  2586   2587                 append(self.writeDaySpacer())  2588   2589             append(fmt.table_row(on=0))  2590   2591         # Write a final time heading if the last period ends in the current day.  2592   2593         if period is not None:  2594             if period.end == date:  2595                 append(fmt.table_row(on=1))  2596                 append(self.writeDayScaleHeading(period.end.time_string()))  2597   2598                 for slot in day_row:  2599                     append(self.writeDaySpacer())  2600                     append(self.writeEmptyDaySlot())  2601   2602                 append(fmt.table_row(on=0))  2603   2604         return "".join(output)  2605   2606     def writeDayScaleHeading(self, heading):  2607         page = self.page  2608         fmt = page.request.formatter  2609   2610         output = []  2611         append = output.append  2612   2613         append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"}))  2614         append(fmt.text(heading))  2615         append(fmt.table_cell(on=0))  2616   2617         return "".join(output)  2618   2619     def getActiveEvent(self, period, events):  2620         for event in events:  2621             if period not in event:  2622                 continue  2623             return event  2624         else:  2625             return None  2626   2627     def writeDaySlot(self, period, event, rowspan):  2628         page = self.page  2629         fmt = page.request.formatter  2630   2631         output = []  2632         append = output.append  2633   2634         if event is not None:  2635             event_summary = event.getSummary(self.parent_name)  2636             style = self.getEventStyle(event_summary)  2637   2638             append(fmt.table_cell(on=1, attrs={  2639                 "class" : "event-timespan-content event-timespan-busy",  2640                 "style" : style,  2641                 "rowspan" : str(rowspan)  2642                 }))  2643             append(self.writeEventSummaryBox(event))  2644             append(fmt.table_cell(on=0))  2645         else:  2646             append(self.writeEmptyDaySlot())  2647   2648         return "".join(output)  2649   2650     def writeEmptyDaySlot(self):  2651         page = self.page  2652         fmt = page.request.formatter  2653   2654         output = []  2655         append = output.append  2656   2657         append(fmt.table_cell(on=1,  2658             attrs={"class" : "event-timespan-content event-timespan-empty"}))  2659         append(fmt.table_cell(on=0))  2660   2661         return "".join(output)  2662   2663     def writeDaySpacer(self, colspan=1, cls="timespan"):  2664         page = self.page  2665         fmt = page.request.formatter  2666   2667         output = []  2668         append = output.append  2669   2670         append(fmt.table_cell(on=1, attrs={  2671             "class" : "event-%s-spacer" % cls,  2672             "colspan" : str(colspan)}))  2673         append(fmt.table_cell(on=0))  2674         return "".join(output)  2675   2676     # Map layout methods.  2677   2678     def writeMapTableHeading(self):  2679         page = self.page  2680         fmt = page.request.formatter  2681   2682         output = []  2683         append = output.append  2684   2685         append(fmt.table_cell(on=1, attrs={"class" : "event-map-heading"}))  2686         append(self.writeMapHeading())  2687         append(fmt.table_cell(on=0))  2688   2689         return "".join(output)  2690   2691     def showDictError(self, text, pagename):  2692         page = self.page  2693         request = page.request  2694         fmt = request.formatter  2695   2696         output = []  2697         append = output.append  2698   2699         append(fmt.div(on=1, attrs={"class" : "event-aggregator-error"}))  2700         append(fmt.paragraph(on=1))  2701         append(fmt.text(text))  2702         append(fmt.paragraph(on=0))  2703         append(fmt.paragraph(on=1))  2704         append(linkToPage(request, Page(request, pagename), pagename))  2705         append(fmt.paragraph(on=0))  2706   2707         return "".join(output)  2708   2709     def writeMapMarker(self, marker_x, marker_y, map_x_scale, map_y_scale, location, events):  2710   2711         "Put a marker on the map."  2712   2713         page = self.page  2714         request = page.request  2715         fmt = request.formatter  2716   2717         output = []  2718         append = output.append  2719   2720         append(fmt.listitem(on=1, css_class="event-map-label"))  2721   2722         # Have a positioned marker for the print mode.  2723   2724         append(fmt.div(on=1, css_class="event-map-label-only",  2725             style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % (  2726                 marker_x, marker_y, map_x_scale, map_y_scale))  2727         append(fmt.div(on=0))  2728   2729         # Have a marker containing a pop-up when using the screen mode,  2730         # providing a normal block when using the print mode.  2731   2732         append(fmt.div(on=1, css_class="event-map-label",  2733             style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % (  2734                 marker_x, marker_y, map_x_scale, map_y_scale))  2735         append(fmt.div(on=1, css_class="event-map-details"))  2736         append(fmt.div(on=1, css_class="event-map-shadow"))  2737         append(fmt.div(on=1, css_class="event-map-location"))  2738   2739         # The location may have been given as formatted text, but this will not  2740         # be usable in a heading, so it must be first converted to plain text.  2741   2742         append(fmt.heading(on=1, depth=2))  2743         append(fmt.text(to_plain_text(location, request)))  2744         append(fmt.heading(on=0, depth=2))  2745   2746         append(self.writeMapEventSummaries(events))  2747   2748         append(fmt.div(on=0))  2749         append(fmt.div(on=0))  2750         append(fmt.div(on=0))  2751         append(fmt.div(on=0))  2752         append(fmt.listitem(on=0))  2753   2754         return "".join(output)  2755   2756     def writeMapEventSummaries(self, events):  2757   2758         "Write summaries of the given 'events' for the map."  2759   2760         page = self.page  2761         request = page.request  2762         fmt = request.formatter  2763   2764         # Sort the events by date.  2765   2766         events.sort(sort_start_first)  2767   2768         # Write out a self-contained list of events.  2769   2770         output = []  2771         append = output.append  2772   2773         append(fmt.bullet_list(on=1, attr={"class" : "event-map-location-events"}))  2774   2775         for event in events:  2776   2777             # Get the event details.  2778   2779             event_summary = event.getSummary(self.parent_name)  2780             start, end = event.as_limits()  2781             event_period = self._getCalendarPeriod(  2782                 start and self.getFullDateLabel(start),  2783                 end and self.getFullDateLabel(end),  2784                 "")  2785   2786             append(fmt.listitem(on=1))  2787   2788             # Link to the page using the summary.  2789   2790             append(event.linkToEvent(request, event_summary))  2791   2792             # Add the event period.  2793   2794             append(fmt.text(" "))  2795             append(fmt.span(on=1, css_class="event-map-period"))  2796             append(fmt.text(event_period))  2797             append(fmt.span(on=0))  2798   2799             append(fmt.listitem(on=0))  2800   2801         append(fmt.bullet_list(on=0))  2802   2803         return "".join(output)  2804   2805     def render(self, all_shown_events):  2806   2807         """  2808         Render the view, returning the rendered representation as a string.  2809         The view will show a list of 'all_shown_events'.  2810         """  2811   2812         page = self.page  2813         request = page.request  2814         fmt = request.formatter  2815         _ = request.getText  2816   2817         # Make a calendar.  2818   2819         output = []  2820         append = output.append  2821   2822         append(fmt.div(on=1, css_class="event-calendar", id=("EventAggregator-%s" % self.getIdentifier())))  2823   2824         # Output download controls.  2825   2826         append(fmt.div(on=1, css_class="event-controls"))  2827         append(self.writeDownloadControls())  2828         append(fmt.div(on=0))  2829   2830         # Output a table.  2831   2832         if self.mode == "table":  2833   2834             # Start of table view output.  2835   2836             append(fmt.table(on=1, attrs={"tableclass" : "event-table"}))  2837   2838             append(fmt.table_row(on=1))  2839             append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"}))  2840             append(fmt.text(_("Event dates")))  2841             append(fmt.table_cell(on=0))  2842             append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"}))  2843             append(fmt.text(_("Event location")))  2844             append(fmt.table_cell(on=0))  2845             append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"}))  2846             append(fmt.text(_("Event details")))  2847             append(fmt.table_cell(on=0))  2848             append(fmt.table_row(on=0))  2849   2850             # Show the events in order.  2851   2852             all_shown_events.sort(sort_start_first)  2853   2854             for event in all_shown_events:  2855                 event_page = event.getPage()  2856                 event_summary = event.getSummary(self.parent_name)  2857                 event_details = event.getDetails()  2858   2859                 # Prepare CSS classes with category-related styling.  2860   2861                 css_classes = ["event-table-details"]  2862   2863                 for topic in event_details.get("topics") or event_details.get("categories") or []:  2864   2865                     # Filter the category text to avoid illegal characters.  2866   2867                     css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic)))  2868   2869                 attrs = {"class" : " ".join(css_classes)}  2870   2871                 append(fmt.table_row(on=1))  2872   2873                 # Start and end dates.  2874   2875                 append(fmt.table_cell(on=1, attrs=attrs))  2876                 append(fmt.span(on=1))  2877                 append(fmt.text(str(event_details["start"])))  2878                 append(fmt.span(on=0))  2879   2880                 if event_details["start"] != event_details["end"]:  2881                     append(fmt.text(" - "))  2882                     append(fmt.span(on=1))  2883                     append(fmt.text(str(event_details["end"])))  2884                     append(fmt.span(on=0))  2885   2886                 append(fmt.table_cell(on=0))  2887   2888                 # Location.  2889   2890                 append(fmt.table_cell(on=1, attrs=attrs))  2891   2892                 if event_details.has_key("location"):  2893                     append(event_page.formatText(event_details["location"], fmt))  2894   2895                 append(fmt.table_cell(on=0))  2896   2897                 # Link to the page using the summary.  2898   2899                 append(fmt.table_cell(on=1, attrs=attrs))  2900                 append(event.linkToEvent(request, event_summary))  2901                 append(fmt.table_cell(on=0))  2902   2903                 append(fmt.table_row(on=0))  2904   2905             # End of table view output.  2906   2907             append(fmt.table(on=0))  2908   2909         # Output a map view.  2910   2911         elif self.mode == "map":  2912   2913             # Special dictionary pages.  2914   2915             maps_page = getMapsPage(request)  2916             locations_page = getLocationsPage(request)  2917   2918             map_image = None  2919   2920             # Get the maps and locations.  2921   2922             maps = getWikiDict(maps_page, request)  2923             locations = getWikiDict(locations_page, request)  2924   2925             # Get the map image definition.  2926   2927             if maps is not None and self.map_name:  2928                 try:  2929                     map_details = maps[self.map_name].split()  2930   2931                     map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \  2932                         map(getMapReference, map_details[:4])  2933                     map_width, map_height = map(int, map_details[4:6])  2934                     map_image = map_details[6]  2935   2936                     map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees()  2937                     map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees()  2938   2939                 except (KeyError, ValueError):  2940                     pass  2941   2942             # Report errors.  2943   2944             if maps is None:  2945                 append(self.showDictError(  2946                     _("You do not have read access to the maps page:"),  2947                     maps_page))  2948   2949             elif not self.map_name:  2950                 append(self.showDictError(  2951                     _("Please specify a valid map name corresponding to an entry on the following page:"),  2952                     maps_page))  2953   2954             elif map_image is None:  2955                 append(self.showDictError(  2956                     _("Please specify a valid entry for %s on the following page:") % self.map_name,  2957                     maps_page))  2958   2959             elif locations is None:  2960                 append(self.showDictError(  2961                     _("You do not have read access to the locations page:"),  2962                     locations_page))  2963   2964             # Attempt to show the map.  2965   2966             else:  2967   2968                 # Get events by position.  2969   2970                 events_by_location = {}  2971                 event_locations = {}  2972   2973                 for event in all_shown_events:  2974                     event_details = event.getDetails()  2975   2976                     location = event_details.get("location")  2977                     geo = event_details.get("geo")  2978   2979                     # Make a temporary location if an explicit position is given  2980                     # but not a location name.  2981   2982                     if not location and geo:  2983                         location = "%s %s" % tuple(geo)  2984   2985                     # Map the location to a position.  2986   2987                     if location is not None and not event_locations.has_key(location):  2988   2989                         # Get any explicit position of an event.  2990   2991                         if geo:  2992                             latitude, longitude = geo  2993   2994                         # Or look up the position of a location using the locations  2995                         # page.  2996   2997                         else:  2998                             latitude, longitude = Location(location, locations).getPosition()  2999   3000                         # Use a normalised location if necessary.  3001   3002                         if latitude is None and longitude is None:  3003                             normalised_location = getNormalisedLocation(location)  3004                             if normalised_location is not None:  3005                                 latitude, longitude = getLocationPosition(normalised_location, locations)  3006                                 if latitude is not None and longitude is not None:  3007                                     location = normalised_location  3008   3009                         # Only remember positioned locations.  3010   3011                         if latitude is not None and longitude is not None:  3012                             event_locations[location] = latitude, longitude  3013   3014                     # Record events according to location.  3015   3016                     if not events_by_location.has_key(location):  3017                         events_by_location[location] = []  3018   3019                     events_by_location[location].append(event)  3020   3021                 # Get the map image URL.  3022   3023                 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request)  3024   3025                 # Start of map view output.  3026   3027                 map_identifier = "map-%s" % self.getIdentifier()  3028                 append(fmt.div(on=1, css_class="event-map", id=map_identifier))  3029   3030                 append(fmt.table(on=1))  3031   3032                 append(fmt.table_row(on=1))  3033                 append(self.writeMapTableHeading())  3034                 append(fmt.table_row(on=0))  3035   3036                 append(fmt.table_row(on=1))  3037                 append(fmt.table_cell(on=1))  3038   3039                 append(fmt.div(on=1, css_class="event-map-container"))  3040                 append(fmt.image(map_image_url))  3041                 append(fmt.number_list(on=1))  3042   3043                 # Events with no location are unpositioned.  3044   3045                 if events_by_location.has_key(None):  3046                     unpositioned_events = events_by_location[None]  3047                     del events_by_location[None]  3048                 else:  3049                     unpositioned_events = []  3050   3051                 # Events whose location is unpositioned are themselves considered  3052                 # unpositioned.  3053   3054                 for location in set(events_by_location.keys()).difference(event_locations.keys()):  3055                     unpositioned_events += events_by_location[location]  3056   3057                 # Sort the locations before traversing them.  3058   3059                 event_locations = event_locations.items()  3060                 event_locations.sort()  3061   3062                 # Show the events in the map.  3063   3064                 for location, (latitude, longitude) in event_locations:  3065                     events = events_by_location[location]  3066   3067                     # Skip unpositioned locations and locations outside the map.  3068   3069                     if latitude is None or longitude is None or \  3070                         latitude < map_bottom_left_latitude or \  3071                         longitude < map_bottom_left_longitude or \  3072                         latitude > map_top_right_latitude or \  3073                         longitude > map_top_right_longitude:  3074   3075                         unpositioned_events += events  3076                         continue  3077   3078                     # Get the position and dimensions of the map marker.  3079                     # NOTE: Use one degree as the marker size.  3080   3081                     marker_x, marker_y = getPositionForCentrePoint(  3082                         getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude,  3083                             map_x_scale, map_y_scale),  3084                         map_x_scale, map_y_scale)  3085   3086                     # Add the map marker.  3087   3088                     append(self.writeMapMarker(marker_x, marker_y, map_x_scale, map_y_scale, location, events))  3089   3090                 append(fmt.number_list(on=0))  3091                 append(fmt.div(on=0))  3092                 append(fmt.table_cell(on=0))  3093                 append(fmt.table_row(on=0))  3094   3095                 # Write unpositioned events.  3096   3097                 if unpositioned_events:  3098                     unpositioned_identifier = "unpositioned-%s" % self.getIdentifier()  3099   3100                     append(fmt.table_row(on=1, css_class="event-map-unpositioned",  3101                         id=unpositioned_identifier))  3102                     append(fmt.table_cell(on=1))  3103   3104                     append(fmt.heading(on=1, depth=2))  3105                     append(fmt.text(_("Events not shown on the map")))  3106                     append(fmt.heading(on=0, depth=2))  3107   3108                     # Show and hide controls.  3109   3110                     append(fmt.div(on=1, css_class="event-map-show-control"))  3111                     append(fmt.anchorlink(on=1, name=unpositioned_identifier))  3112                     append(fmt.text(_("Show unpositioned events")))  3113                     append(fmt.anchorlink(on=0))  3114                     append(fmt.div(on=0))  3115   3116                     append(fmt.div(on=1, css_class="event-map-hide-control"))  3117                     append(fmt.anchorlink(on=1, name=map_identifier))  3118                     append(fmt.text(_("Hide unpositioned events")))  3119                     append(fmt.anchorlink(on=0))  3120                     append(fmt.div(on=0))  3121   3122                     append(self.writeMapEventSummaries(unpositioned_events))  3123   3124                 # End of map view output.  3125   3126                 append(fmt.table_cell(on=0))  3127                 append(fmt.table_row(on=0))  3128                 append(fmt.table(on=0))  3129                 append(fmt.div(on=0))  3130   3131         # Output a list.  3132   3133         elif self.mode == "list":  3134   3135             # Start of list view output.  3136   3137             append(fmt.bullet_list(on=1, attr={"class" : "event-listings"}))  3138   3139             # Output a list.  3140   3141             for period in self.first.until(self.last):  3142   3143                 append(fmt.listitem(on=1, attr={"class" : "event-listings-period"}))  3144                 append(fmt.div(on=1, attr={"class" : "event-listings-heading"}))  3145   3146                 # Either write a date heading or produce links for navigable  3147                 # calendars.  3148   3149                 append(self.writeDateHeading(period))  3150   3151                 append(fmt.div(on=0))  3152   3153                 append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"}))  3154   3155                 # Show the events in order.  3156   3157                 events_in_period = getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period))  3158                 events_in_period.sort(sort_start_first)  3159   3160                 for event in events_in_period:  3161                     event_page = event.getPage()  3162                     event_details = event.getDetails()  3163                     event_summary = event.getSummary(self.parent_name)  3164   3165                     append(fmt.listitem(on=1, attr={"class" : "event-listing"}))  3166   3167                     # Link to the page using the summary.  3168   3169                     append(fmt.paragraph(on=1))  3170                     append(event.linkToEvent(request, event_summary))  3171                     append(fmt.paragraph(on=0))  3172   3173                     # Start and end dates.  3174   3175                     append(fmt.paragraph(on=1))  3176                     append(fmt.span(on=1))  3177                     append(fmt.text(str(event_details["start"])))  3178                     append(fmt.span(on=0))  3179                     append(fmt.text(" - "))  3180                     append(fmt.span(on=1))  3181                     append(fmt.text(str(event_details["end"])))  3182                     append(fmt.span(on=0))  3183                     append(fmt.paragraph(on=0))  3184   3185                     # Location.  3186   3187                     if event_details.has_key("location"):  3188                         append(fmt.paragraph(on=1))  3189                         append(event_page.formatText(event_details["location"], fmt))  3190                         append(fmt.paragraph(on=1))  3191   3192                     # Topics.  3193   3194                     if event_details.has_key("topics") or event_details.has_key("categories"):  3195                         append(fmt.bullet_list(on=1, attr={"class" : "event-topics"}))  3196   3197                         for topic in event_details.get("topics") or event_details.get("categories") or []:  3198                             append(fmt.listitem(on=1))  3199                             append(event_page.formatText(topic, fmt))  3200                             append(fmt.listitem(on=0))  3201   3202                         append(fmt.bullet_list(on=0))  3203   3204                     append(fmt.listitem(on=0))  3205   3206                 append(fmt.bullet_list(on=0))  3207   3208             # End of list view output.  3209   3210             append(fmt.bullet_list(on=0))  3211   3212         # Output a month calendar. This shows month-by-month data.  3213   3214         elif self.mode == "calendar":  3215   3216             # Visit all months in the requested range, or across known events.  3217   3218             for month in self.first.months_until(self.last):  3219   3220                 # Output a month.  3221   3222                 append(fmt.table(on=1, attrs={"tableclass" : "event-month"}))  3223   3224                 # Either write a month heading or produce links for navigable  3225                 # calendars.  3226   3227                 append(self.writeMonthTableHeading(month))  3228   3229                 # Weekday headings.  3230   3231                 append(self.writeWeekdayHeadings())  3232   3233                 # Process the days of the month.  3234   3235                 start_weekday, number_of_days = month.month_properties()  3236   3237                 # The start weekday is the weekday of day number 1.  3238                 # Find the first day of the week, counting from below zero, if  3239                 # necessary, in order to land on the first day of the month as  3240                 # day number 1.  3241   3242                 first_day = 1 - start_weekday  3243   3244                 while first_day <= number_of_days:  3245   3246                     # Find events in this week and determine how to mark them on the  3247                     # calendar.  3248   3249                     week_start = month.as_date(max(first_day, 1))  3250                     week_end = month.as_date(min(first_day + 6, number_of_days))  3251   3252                     full_coverage, week_slots = getCoverage(  3253                         getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end)))  3254   3255                     # Output a week, starting with the day numbers.  3256   3257                     append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage))  3258   3259                     # Either generate empty days...  3260   3261                     if not week_slots:  3262                         append(self.writeEmptyWeek(first_day, number_of_days, month))  3263   3264                     # Or generate each set of scheduled events...  3265   3266                     else:  3267                         append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots))  3268   3269                     # Process the next week...  3270   3271                     first_day += 7  3272   3273                 # End of month.  3274   3275                 append(fmt.table(on=0))  3276   3277         # Output a day view.  3278   3279         elif self.mode == "day":  3280   3281             # Visit all days in the requested range, or across known events.  3282   3283             for date in self.first.days_until(self.last):  3284   3285                 append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"}))  3286   3287                 full_coverage, day_slots = getCoverage(  3288                     getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime")  3289   3290                 # Work out how many columns the day title will need.  3291                 # Include spacers after the scale and each event column.  3292   3293                 colspan = sum(map(len, day_slots.values())) * 2 + 2  3294   3295                 append(self.writeDayTableHeading(date, colspan))  3296   3297                 # Either generate empty days...  3298   3299                 if not day_slots:  3300                     append(self.writeEmptyDay(date))  3301   3302                 # Or generate each set of scheduled events...  3303   3304                 else:  3305                     append(self.writeDaySlots(date, full_coverage, day_slots))  3306   3307                 # End of day.  3308   3309                 append(fmt.table(on=0))  3310   3311         # Output view controls.  3312   3313         append(fmt.div(on=1, css_class="event-controls"))  3314         append(self.writeViewControls())  3315         append(fmt.div(on=0))  3316   3317         # Close the calendar region.  3318   3319         append(fmt.div(on=0))  3320   3321         # Add any scripts.  3322   3323         if isinstance(fmt, request.html_formatter.__class__):  3324             append(self.update_script)  3325   3326         return ''.join(output)  3327   3328     update_script = """\  3329 <script type="text/javascript">  3330 function replaceCalendar(name, url) {  3331     var calendar = document.getElementById(name);  3332   3333     if (calendar == null) {  3334         return true;  3335     }  3336   3337     var xmlhttp = new XMLHttpRequest();  3338     xmlhttp.open("GET", url, false);  3339     xmlhttp.send(null);  3340   3341     var newCalendar = xmlhttp.responseText;  3342   3343     if (newCalendar != null) {  3344         calendar.innerHTML = newCalendar;  3345         return false;  3346     }  3347   3348     return true;  3349 }  3350 </script>  3351 """  3352   3353 # Event selection from request parameters.  3354   3355 def getEventsUsingParameters(category_names, search_pattern, remote_sources,  3356     calendar_start, calendar_end, resolution, request):  3357   3358     "Get the events according to the resolution of the calendar."  3359   3360     if search_pattern:  3361         results         = getPagesForSearch(search_pattern, request)  3362     else:  3363         results         = []  3364   3365     results            += getAllCategoryPages(category_names, request)  3366     pages               = getPagesFromResults(results, request)  3367     events              = getEventsFromResources(getEventPages(pages))  3368     events             += getEventsFromResources(getEventResources(remote_sources, calendar_start, calendar_end, request))  3369     all_shown_events    = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end))  3370     earliest, latest    = getEventLimits(all_shown_events)  3371   3372     # Get a concrete period of time.  3373   3374     first, last = getConcretePeriod(calendar_start, calendar_end, earliest, latest, resolution)  3375   3376     return all_shown_events, first, last  3377   3378 # Event-only formatting.  3379   3380 def formatEvent(event, request, fmt, write=None):  3381   3382     """  3383     Format the given 'event' using the 'request' and formatter 'fmt'. If the  3384     'write' parameter is specified, use it to write output.  3385     """  3386   3387     details = event.getDetails()  3388     raw_details = event.getRawDetails()  3389     write = write or request.write  3390   3391     if details.has_key("fragment"):  3392         write(fmt.anchordef(details["fragment"]))  3393   3394     # Promote any title to a heading above the event details.  3395   3396     if raw_details.has_key("title"):  3397         write(formatText(raw_details["title"], request, fmt))  3398     elif details.has_key("title"):  3399         write(fmt.heading(on=1, depth=1))  3400         write(fmt.text(details["title"]))  3401         write(fmt.heading(on=0, depth=1))  3402   3403     # Produce a definition list for the rest of the details.  3404   3405     write(fmt.definition_list(on=1))  3406   3407     for term in event.all_terms:  3408         if term == "title":  3409             continue  3410   3411         raw_value = raw_details.get(term)  3412         value = details.get(term)  3413   3414         if raw_value or value:  3415             write(fmt.definition_term(on=1))  3416             write(fmt.text(term))  3417             write(fmt.definition_term(on=0))  3418             write(fmt.definition_desc(on=1))  3419   3420             # Try and use the raw details, if available.  3421   3422             if raw_value:  3423                 write(formatText(raw_value, request, fmt))  3424   3425             # Otherwise, format the processed details.  3426   3427             else:  3428                 if term in event.list_terms:  3429                     write(", ".join([formatText(str(v), request, fmt) for v in value]))  3430                 else:  3431                     write(fmt.text(str(value)))  3432   3433             write(fmt.definition_desc(on=0))  3434   3435     write(fmt.definition_list(on=0))  3436   3437 def formatEventsForOutputType(events, request, mimetype, parent=None, descriptions=None, latest_timestamp=None, write=None):  3438   3439     """  3440     Format the given 'events' using the 'request' for the given 'mimetype'.  3441   3442     The optional 'parent' indicates the "natural" parent page of the events. Any  3443     event pages residing beneath the parent page will have their names  3444     reproduced as relative to the parent page.  3445   3446     The optional 'descriptions' indicates the nature of any description given  3447     for events in the output resource.  3448   3449     The optional 'latest_timestamp' indicates the timestamp of the latest edit  3450     of the page or event collection.  3451   3452     If the 'write' parameter is specified, use it to write output.  3453     """  3454   3455     write = write or request.write  3456   3457     # Start the collection.  3458   3459     if mimetype == "text/calendar" and vCalendar is not None:  3460         write = vCalendar.iterwrite(write=write).write  3461         write("BEGIN", {}, "VCALENDAR")  3462         write("PRODID", {}, "-//MoinMoin//EventAggregatorSummary")  3463         write("VERSION", {}, "2.0")  3464   3465     elif mimetype == "application/rss+xml":  3466   3467         # Using the page name and the page URL in the title, link and  3468         # description.  3469   3470         path_info = getPathInfo(request)  3471   3472         write('<rss version="2.0">\r\n')  3473         write('<channel>\r\n')  3474         write('<title>%s</title>\r\n' % path_info[1:])  3475         write('<link>%s%s</link>\r\n' % (request.getBaseURL(), path_info))  3476         write('<description>Events published on %s%s</description>\r\n' % (request.getBaseURL(), path_info))  3477   3478         if latest_timestamp is not None:  3479             write('<lastBuildDate>%s</lastBuildDate>\r\n' % latest_timestamp.as_HTTP_datetime_string())  3480    3481         # Sort the events by start date, reversed.  3482   3483         ordered_events = getOrderedEvents(events)  3484         ordered_events.reverse()  3485         events = ordered_events  3486   3487     elif mimetype == "text/html":  3488         write('<html>')  3489         write('<body>')  3490   3491     # Output the collection one by one.  3492   3493     for event in events:  3494         formatEventForOutputType(event, request, mimetype, parent, descriptions)  3495   3496     # End the collection.  3497   3498     if mimetype == "text/calendar" and vCalendar is not None:  3499         write("END", {}, "VCALENDAR")  3500   3501     elif mimetype == "application/rss+xml":  3502         write('</channel>\r\n')  3503         write('</rss>\r\n')  3504   3505     elif mimetype == "text/html":  3506         write('</body>')  3507         write('</html>')  3508   3509 def formatEventForOutputType(event, request, mimetype, parent=None, descriptions=None, write=None):  3510   3511     """  3512     Format the given 'event' using the 'request' for the given 'mimetype'.  3513   3514     The optional 'parent' indicates the "natural" parent page of the events. Any  3515     event pages residing beneath the parent page will have their names  3516     reproduced as relative to the parent page.  3517   3518     The optional 'descriptions' indicates the nature of any description given  3519     for events in the output resource.  3520   3521     If the 'write' parameter is specified, use it to write output.  3522     """  3523   3524     write = write or request.write  3525     event_details = event.getDetails()  3526     event_metadata = event.getMetadata()  3527   3528     if mimetype == "text/calendar" and vCalendar is not None:  3529   3530         # NOTE: A custom formatter making attributes for links and plain  3531         # NOTE: text for values could be employed here.  3532   3533         write = vCalendar.iterwrite(write=write).write  3534   3535         # Get the summary details.  3536   3537         event_summary = event.getSummary(parent)  3538         link = event.getEventURL()  3539   3540         # Output the event details.  3541   3542         write("BEGIN", {}, "VEVENT")  3543         write("UID", {}, link)  3544         write("URL", {}, link)  3545         write("DTSTAMP", {}, "%04d%02d%02dT%02d%02d%02dZ" % event_metadata["created"].as_tuple()[:6])  3546         write("LAST-MODIFIED", {}, "%04d%02d%02dT%02d%02d%02dZ" % event_metadata["last-modified"].as_tuple()[:6])  3547         write("SEQUENCE", {}, "%d" % event_metadata["sequence"])  3548   3549         start = event_details["start"]  3550         end = event_details["end"]  3551   3552         if isinstance(start, DateTime):  3553             params, value = get_calendar_datetime(start)  3554         else:  3555             params, value = {"VALUE" : "DATE"}, "%04d%02d%02d" % start.as_date().as_tuple()  3556         write("DTSTART", params, value)  3557   3558         if isinstance(end, DateTime):  3559             params, value = get_calendar_datetime(end)  3560         else:  3561             params, value = {"VALUE" : "DATE"}, "%04d%02d%02d" % end.next_day().as_date().as_tuple()  3562         write("DTEND", params, value)  3563   3564         write("SUMMARY", {}, event_summary)  3565   3566         # Optional details.  3567   3568         if event_details.get("topics") or event_details.get("categories"):  3569             write("CATEGORIES", {}, event_details.get("topics") or event_details.get("categories"))  3570         if event_details.has_key("location"):  3571             write("LOCATION", {}, event_details["location"])  3572         if event_details.has_key("geo"):  3573             write("GEO", {}, tuple([str(ref.to_degrees()) for ref in event_details["geo"]]))  3574   3575         write("END", {}, "VEVENT")  3576   3577     elif mimetype == "application/rss+xml":  3578   3579         event_page = event.getPage()  3580         event_details = event.getDetails()  3581   3582         # Get a parser and formatter for the formatting of some attributes.  3583   3584         fmt = request.html_formatter  3585   3586         # Get the summary details.  3587   3588         event_summary = event.getSummary(parent)  3589         link = event.getEventURL()  3590   3591         write('<item>\r\n')  3592         write('<title>%s</title>\r\n' % escape(event_summary))  3593         write('<link>%s</link>\r\n' % link)  3594   3595         # Write a description according to the preferred source of  3596         # descriptions.  3597   3598         if descriptions == "page":  3599             description = event_details.get("description", "")  3600         else:  3601             description = event_metadata["last-comment"]  3602   3603         write('<description>%s</description>\r\n' %  3604             fmt.text(event_page.formatText(description, fmt)))  3605   3606         for topic in event_details.get("topics") or event_details.get("categories") or []:  3607             write('<category>%s</category>\r\n' %  3608                 fmt.text(event_page.formatText(topic, fmt)))  3609   3610         write('<pubDate>%s</pubDate>\r\n' % event_metadata["created"].as_HTTP_datetime_string())  3611         write('<guid>%s#%s</guid>\r\n' % (link, event_metadata["sequence"]))  3612         write('</item>\r\n')  3613   3614     elif mimetype == "text/html":  3615         fmt = request.html_formatter  3616         fmt.setPage(request.page)  3617         formatEvent(event, request, fmt, write=write)  3618   3619 # iCalendar format helper functions.  3620   3621 def get_calendar_datetime(datetime):  3622   3623     """  3624     Write to the given 'request' the 'datetime' using appropriate time zone  3625     information.  3626     """  3627   3628     utc_datetime = datetime.to_utc()  3629     if utc_datetime:  3630         return {"VALUE" : "DATE-TIME"}, "%04d%02d%02dT%02d%02d%02dZ" % utc_datetime.padded().as_tuple()[:-1]  3631     else:  3632         zone = datetime.time_zone()  3633         params = {"VALUE" : "DATE-TIME"}  3634         if zone:  3635             params["TZID"] = zone  3636         return params, "%04d%02d%02dT%02d%02d%02d" % datetime.padded().as_tuple()[:-1]  3637   3638 # vim: tabstop=4 expandtab shiftwidth=4