EventAggregator

EventAggregatorSupport/View.py

370:7ac25d2de0ac
2013-06-17 Paul Boddie Fixed table start output.
     1 # -*- coding: iso-8859-1 -*-     2 """     3     MoinMoin - EventAggregator user interface library     4      5     @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk>     6     @license: GNU GPL (v2 or later), see COPYING.txt for details.     7 """     8      9 from EventAggregatorSupport.Filter import getCalendarPeriod, getEventsInPeriod, \    10                                           getCoverage, getCoverageScale    11 from EventAggregatorSupport.Locations import getMapsPage, getLocationsPage, Location    12     13 from GeneralSupport import sort_none_first    14 from LocationSupport import getMapReference, getNormalisedLocation, \    15                             getPositionForCentrePoint, getPositionForReference    16 from MoinDateSupport import getFullDateLabel, getFullMonthLabel    17 from MoinSupport import *    18 from ViewSupport import getColour, getBlackOrWhite    19     20 from MoinMoin.Page import Page    21 from MoinMoin.action import AttachFile    22 from MoinMoin import wikiutil    23     24 try:    25     set    26 except NameError:    27     from sets import Set as set    28     29 # Utility functions.    30     31 def to_plain_text(s, request):    32     33     "Convert 's' to plain text."    34     35     fmt = getFormatterClass(request, "plain")(request)    36     fmt.setPage(request.page)    37     return formatText(s, request, fmt)    38     39 def getLocationPosition(location, locations):    40     41     """    42     Attempt to return the position of the given 'location' using the 'locations'    43     dictionary provided. If no position can be found, return a latitude of None    44     and a longitude of None.    45     """    46     47     latitude, longitude = None, None    48     49     if location is not None:    50         try:    51             latitude, longitude = map(getMapReference, locations[location].split())    52         except (KeyError, ValueError):    53             pass    54     55     return latitude, longitude    56     57 # Event sorting.    58     59 def sort_start_first(x, y):    60     x_ts = x.as_limits()    61     if x_ts is not None:    62         x_start, x_end = x_ts    63         y_ts = y.as_limits()    64         if y_ts is not None:    65             y_start, y_end = y_ts    66             start_order = cmp(x_start, y_start)    67             if start_order == 0:    68                 return cmp(x_end, y_end)    69             else:    70                 return start_order    71     return 0    72     73 # User interface abstractions.    74     75 class View:    76     77     "A view of the event calendar."    78     79     def __init__(self, page, calendar_name,    80         raw_calendar_start, raw_calendar_end,    81         original_calendar_start, original_calendar_end,    82         calendar_start, calendar_end,    83         wider_calendar_start, wider_calendar_end,    84         first, last, category_names, remote_sources, search_pattern, template_name,    85         parent_name, mode, raw_resolution, resolution, name_usage, map_name):    86     87         """    88         Initialise the view with the current 'page', a 'calendar_name' (which    89         may be None), the 'raw_calendar_start' and 'raw_calendar_end' (which    90         are the actual start and end values provided by the request), the    91         calculated 'original_calendar_start' and 'original_calendar_end' (which    92         are the result of calculating the calendar's limits from the raw start    93         and end values), the requested, calculated 'calendar_start' and    94         'calendar_end' (which may involve different start and end values due to    95         navigation in the user interface), and the requested    96         'wider_calendar_start' and 'wider_calendar_end' (which indicate a wider    97         view used when navigating out of the day view), along with the 'first'    98         and 'last' months of event coverage.    99    100         The additional 'category_names', 'remote_sources', 'search_pattern',   101         'template_name', 'parent_name' and 'mode' parameters are used to   102         configure the links employed by the view.   103    104         The 'raw_resolution' is used to parameterise download links, whereas the   105         'resolution' affects the view for certain modes and is also used to   106         parameterise links.   107    108         The 'name_usage' parameter controls how names are shown on calendar mode   109         events, such as how often labels are repeated.   110    111         The 'map_name' parameter provides the name of a map to be used in the   112         map mode.   113         """   114    115         self.page = page   116         self.calendar_name = calendar_name   117         self.raw_calendar_start = raw_calendar_start   118         self.raw_calendar_end = raw_calendar_end   119         self.original_calendar_start = original_calendar_start   120         self.original_calendar_end = original_calendar_end   121         self.calendar_start = calendar_start   122         self.calendar_end = calendar_end   123         self.wider_calendar_start = wider_calendar_start   124         self.wider_calendar_end = wider_calendar_end   125         self.template_name = template_name   126         self.parent_name = parent_name   127         self.mode = mode   128         self.raw_resolution = raw_resolution   129         self.resolution = resolution   130         self.name_usage = name_usage   131         self.map_name = map_name   132    133         # Search-related parameters for links.   134    135         self.category_name_parameters = "&".join([("category=%s" % name) for name in category_names])   136         self.remote_source_parameters = "&".join([("source=%s" % source) for source in remote_sources])   137         self.search_pattern = search_pattern   138    139         # Calculate the duration in terms of the highest common unit of time.   140    141         self.first = first   142         self.last = last   143         self.duration = abs(last - first) + 1   144    145         if self.calendar_name:   146    147             # Store the view parameters.   148    149             self.previous_start = first.previous()   150             self.next_start = first.next()   151             self.previous_end = last.previous()   152             self.next_end = last.next()   153    154             self.previous_set_start = first.update(-self.duration)   155             self.next_set_start = first.update(self.duration)   156             self.previous_set_end = last.update(-self.duration)   157             self.next_set_end = last.update(self.duration)   158    159     def getIdentifier(self):   160    161         "Return a unique identifier to be used to refer to this view."   162    163         # NOTE: Nasty hack to get a unique identifier if no name is given.   164    165         return self.calendar_name or str(id(self))   166    167     def getQualifiedParameterName(self, argname):   168    169         "Return the 'argname' qualified using the calendar name."   170    171         return getQualifiedParameterName(self.calendar_name, argname)   172    173     def getDateQueryString(self, argname, date, prefix=1):   174    175         """   176         Return a query string fragment for the given 'argname', referring to the   177         month given by the specified 'year_month' object, appropriate for this   178         calendar.   179    180         If 'prefix' is specified and set to a false value, the parameters in the   181         query string will not be calendar-specific, but could be used with the   182         summary action.   183         """   184    185         suffixes = ["year", "month", "day"]   186    187         if date is not None:   188             args = []   189             for suffix, value in zip(suffixes, date.as_tuple()):   190                 suffixed_argname = "%s-%s" % (argname, suffix)   191                 if prefix:   192                     suffixed_argname = self.getQualifiedParameterName(suffixed_argname)   193                 args.append("%s=%s" % (suffixed_argname, value))   194             return "&".join(args)   195         else:   196             return ""   197    198     def getRawDateQueryString(self, argname, date, prefix=1):   199    200         """   201         Return a query string fragment for the given 'argname', referring to the   202         date given by the specified 'date' value, appropriate for this   203         calendar.   204    205         If 'prefix' is specified and set to a false value, the parameters in the   206         query string will not be calendar-specific, but could be used with the   207         summary action.   208         """   209    210         if date is not None:   211             if prefix:   212                 argname = self.getQualifiedParameterName(argname)   213             return "%s=%s" % (argname, wikiutil.url_quote(date))   214         else:   215             return ""   216    217     def getNavigationLink(self, start, end, mode=None, resolution=None, wider_start=None, wider_end=None):   218    219         """   220         Return a query string fragment for navigation to a view showing months   221         from 'start' to 'end' inclusive, with the optional 'mode' indicating the   222         view style and the optional 'resolution' indicating the resolution of a   223         view, if configurable.   224    225         If the 'wider_start' and 'wider_end' arguments are given, parameters   226         indicating a wider calendar view (when returning from a day view, for   227         example) will be included in the link.   228         """   229    230         return "%s&%s&%s=%s&%s=%s&%s&%s" % (   231             self.getRawDateQueryString("start", start),   232             self.getRawDateQueryString("end", end),   233             self.getQualifiedParameterName("mode"), mode or self.mode,   234             self.getQualifiedParameterName("resolution"), resolution or self.resolution,   235             self.getRawDateQueryString("wider-start", wider_start),   236             self.getRawDateQueryString("wider-end", wider_end),   237             )   238    239     def getUpdateLink(self, start, end, mode=None, resolution=None, wider_start=None, wider_end=None):   240    241         """   242         Return a query string fragment for navigation to a view showing months   243         from 'start' to 'end' inclusive, with the optional 'mode' indicating the   244         view style and the optional 'resolution' indicating the resolution of a   245         view, if configurable. This link differs from the conventional   246         navigation link in that it is sufficient to activate the update action   247         and produce an updated region of the page without needing to locate and   248         process the page or any macro invocation.   249    250         If the 'wider_start' and 'wider_end' arguments are given, parameters   251         indicating a wider calendar view (when returning from a day view, for   252         example) will be included in the link.   253         """   254    255         parameters = [   256             self.getRawDateQueryString("start", start, 0),   257             self.getRawDateQueryString("end", end, 0),   258             self.category_name_parameters,   259             self.remote_source_parameters,   260             self.getRawDateQueryString("wider-start", wider_start, 0),   261             self.getRawDateQueryString("wider-end", wider_end, 0),   262             ]   263    264         pairs = [   265             ("calendar", self.calendar_name or ""),   266             ("calendarstart", self.raw_calendar_start or ""),   267             ("calendarend", self.raw_calendar_end or ""),   268             ("mode", mode or self.mode),   269             ("resolution", resolution or self.resolution),   270             ("raw-resolution", self.raw_resolution),   271             ("parent", self.parent_name or ""),   272             ("template", self.template_name or ""),   273             ("names", self.name_usage),   274             ("map", self.map_name or ""),   275             ("search", self.search_pattern or ""),   276             ]   277    278         url = self.page.url(self.page.request,   279             "action=EventAggregatorUpdate&%s" % (   280                 "&".join([("%s=%s" % (key, wikiutil.url_quote(value))) for (key, value) in pairs] + parameters)   281             ), relative=True)   282    283         return "return replaceCalendar('EventAggregator-%s', '%s')" % (self.getIdentifier(), url)   284    285     def getNewEventLink(self, start):   286    287         """   288         Return a query string activating the new event form, incorporating the   289         calendar parameters, specialising the form for the given 'start' date or   290         month.   291         """   292    293         if start is not None:   294             details = start.as_tuple()   295             pairs = zip(["start-year=%d", "start-month=%d", "start-day=%d"], details)   296             args = [(param % value) for (param, value) in pairs]   297             args = "&".join(args)   298         else:   299             args = ""   300    301         # Prepare navigation details for the calendar shown with the new event   302         # form.   303    304         navigation_link = self.getNavigationLink(   305             self.calendar_start, self.calendar_end   306             )   307    308         return "action=EventAggregatorNewEvent%s%s&template=%s&parent=%s&%s" % (   309             args and "&%s" % args,   310             self.category_name_parameters and "&%s" % self.category_name_parameters,   311             self.template_name, self.parent_name or "",   312             navigation_link)   313    314     def getFullDateLabel(self, date):   315         return getFullDateLabel(self.page.request, date)   316    317     def getFullMonthLabel(self, year_month):   318         return getFullMonthLabel(self.page.request, year_month)   319    320     def getFullLabel(self, arg, resolution):   321         return resolution == "date" and self.getFullDateLabel(arg) or self.getFullMonthLabel(arg)   322    323     def _getCalendarPeriod(self, start_label, end_label, default_label):   324    325         """   326         Return a label describing a calendar period in terms of the given   327         'start_label' and 'end_label', with the 'default_label' being used where   328         the supplied start and end labels fail to produce a meaningful label.   329         """   330    331         output = []   332         append = output.append   333    334         if start_label:   335             append(start_label)   336         if end_label and start_label != end_label:   337             if output:   338                 append(" - ")   339             append(end_label)   340         return "".join(output) or default_label   341    342     def getCalendarPeriod(self):   343    344         "Return the period description for the shown calendar."   345    346         _ = self.page.request.getText   347         return self._getCalendarPeriod(   348             self.calendar_start and self.getFullLabel(self.calendar_start, self.resolution),   349             self.calendar_end and self.getFullLabel(self.calendar_end, self.resolution),   350             _("All events")   351             )   352    353     def getOriginalCalendarPeriod(self):   354    355         "Return the period description for the originally specified calendar."   356    357         _ = self.page.request.getText   358         return self._getCalendarPeriod(   359             self.original_calendar_start and self.getFullLabel(self.original_calendar_start, self.raw_resolution),   360             self.original_calendar_end and self.getFullLabel(self.original_calendar_end, self.raw_resolution),   361             _("All events")   362             )   363    364     def getRawCalendarPeriod(self):   365    366         "Return the raw period description for the calendar."   367    368         _ = self.page.request.getText   369         return self._getCalendarPeriod(   370             self.raw_calendar_start,   371             self.raw_calendar_end,   372              _("No period specified")   373             )   374    375     def writeDownloadControls(self):   376    377         """   378         Return a representation of the download controls, featuring links for   379         view, calendar and customised downloads and subscriptions.   380         """   381    382         page = self.page   383         request = page.request   384         fmt = request.formatter   385         _ = request.getText   386    387         output = []   388         append = output.append   389    390         # The full URL is needed for webcal links.   391    392         full_url = "%s%s" % (request.getBaseURL(), getPathInfo(request))   393    394         # Generate the links.   395    396         download_dialogue_link = "action=EventAggregatorSummary&parent=%s&search=%s%s%s" % (   397             self.parent_name or "",   398             self.search_pattern or "",   399             self.category_name_parameters and "&%s" % self.category_name_parameters,   400             self.remote_source_parameters and "&%s" % self.remote_source_parameters   401             )   402         download_all_link = download_dialogue_link + "&doit=1"   403         download_link = download_all_link + ("&%s&%s" % (   404             self.getDateQueryString("start", self.calendar_start, prefix=0),   405             self.getDateQueryString("end", self.calendar_end, prefix=0)   406             ))   407    408         # The entire calendar download uses the originally specified resolution   409         # of the calendar as does the dialogue. The other link uses the current   410         # resolution.   411    412         download_dialogue_link += "&resolution=%s" % self.raw_resolution   413         download_all_link += "&resolution=%s" % self.raw_resolution   414         download_link += "&resolution=%s" % self.resolution   415    416         # Subscription links just explicitly select the RSS format.   417    418         subscribe_dialogue_link = download_dialogue_link + "&format=RSS"   419         subscribe_all_link = download_all_link + "&format=RSS"   420         subscribe_link = download_link + "&format=RSS"   421    422         # Adjust the "download all" and "subscribe all" links if the calendar   423         # has an inherent period associated with it.   424    425         period_limits = []   426    427         if self.raw_calendar_start:   428             period_limits.append("&%s" %   429                 self.getRawDateQueryString("start", self.raw_calendar_start, prefix=0)   430                 )   431         if self.raw_calendar_end:   432             period_limits.append("&%s" %   433                 self.getRawDateQueryString("end", self.raw_calendar_end, prefix=0)   434                 )   435    436         period_limits = "".join(period_limits)   437    438         download_dialogue_link += period_limits   439         download_all_link += period_limits   440         subscribe_dialogue_link += period_limits   441         subscribe_all_link += period_limits   442    443         # Pop-up descriptions of the downloadable calendars.   444    445         shown_calendar_period = self.getCalendarPeriod()   446         original_calendar_period = self.getOriginalCalendarPeriod()   447         raw_calendar_period = self.getRawCalendarPeriod()   448    449         # Write the controls.   450    451         # Download controls.   452    453         append(fmt.div(on=1, css_class="event-download-controls"))   454    455         append(fmt.span(on=1, css_class="event-download"))   456         append(linkToPage(request, page, _("Download..."), download_dialogue_link, title=_("Edit download options...")))   457         append(fmt.div(on=1, css_class="event-download-popup"))   458    459         append(fmt.div(on=1, css_class="event-download-item"))   460         append(fmt.span(on=1, css_class="event-download-types"))   461         append(fmt.span(on=1, css_class="event-download-webcal"))   462         append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_link))   463         append(fmt.span(on=0))   464         append(fmt.span(on=1, css_class="event-download-http"))   465         append(linkToPage(request, page, _("http"), download_link, title=_("Download this view in the browser")))   466         append(fmt.span(on=0))   467         append(fmt.span(on=0)) # end types   468         append(fmt.span(on=1, css_class="event-download-label"))   469         append(fmt.text(_("Download this view")))   470         append(fmt.span(on=0)) # end label   471         append(fmt.span(on=1, css_class="event-download-period"))   472         append(fmt.text(shown_calendar_period))   473         append(fmt.span(on=0))   474         append(fmt.div(on=0))   475    476         append(fmt.div(on=1, css_class="event-download-item"))   477         append(fmt.span(on=1, css_class="event-download-types"))   478         append(fmt.span(on=1, css_class="event-download-webcal"))   479         append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_all_link))   480         append(fmt.span(on=0))   481         append(fmt.span(on=1, css_class="event-download-http"))   482         append(linkToPage(request, page, _("http"), download_all_link, title=_("Download this calendar in the browser")))   483         append(fmt.span(on=0))   484         append(fmt.span(on=0)) # end types   485         append(fmt.span(on=1, css_class="event-download-label"))   486         append(fmt.text(_("Download this calendar")))   487         append(fmt.span(on=0)) # end label   488         append(fmt.span(on=1, css_class="event-download-period"))   489         append(fmt.text(original_calendar_period))   490         append(fmt.span(on=0))   491         append(fmt.span(on=1, css_class="event-download-period-raw"))   492         append(fmt.text(raw_calendar_period))   493         append(fmt.span(on=0))   494         append(fmt.div(on=0))   495    496         append(fmt.div(on=1, css_class="event-download-item"))   497         append(fmt.span(on=1, css_class="event-download-link"))   498         append(linkToPage(request, page, _("Edit download options..."), download_dialogue_link))   499         append(fmt.span(on=0)) # end label   500         append(fmt.div(on=0))   501    502         append(fmt.div(on=0)) # end of pop-up   503         append(fmt.span(on=0)) # end of download   504    505         # Subscription controls.   506    507         append(fmt.span(on=1, css_class="event-download"))   508         append(linkToPage(request, page, _("Subscribe..."), subscribe_dialogue_link, title=_("Edit subscription options...")))   509         append(fmt.div(on=1, css_class="event-download-popup"))   510    511         append(fmt.div(on=1, css_class="event-download-item"))   512         append(fmt.span(on=1, css_class="event-download-label"))   513         append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link))   514         append(fmt.span(on=0)) # end label   515         append(fmt.span(on=1, css_class="event-download-period"))   516         append(fmt.text(shown_calendar_period))   517         append(fmt.span(on=0))   518         append(fmt.div(on=0))   519    520         append(fmt.div(on=1, css_class="event-download-item"))   521         append(fmt.span(on=1, css_class="event-download-label"))   522         append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link))   523         append(fmt.span(on=0)) # end label   524         append(fmt.span(on=1, css_class="event-download-period"))   525         append(fmt.text(original_calendar_period))   526         append(fmt.span(on=0))   527         append(fmt.span(on=1, css_class="event-download-period-raw"))   528         append(fmt.text(raw_calendar_period))   529         append(fmt.span(on=0))   530         append(fmt.div(on=0))   531    532         append(fmt.div(on=1, css_class="event-download-item"))   533         append(fmt.span(on=1, css_class="event-download-link"))   534         append(linkToPage(request, page, _("Edit subscription options..."), subscribe_dialogue_link))   535         append(fmt.span(on=0)) # end label   536         append(fmt.div(on=0))   537    538         append(fmt.div(on=0)) # end of pop-up   539         append(fmt.span(on=0)) # end of download   540    541         append(fmt.div(on=0)) # end of controls   542    543         return "".join(output)   544    545     def writeViewControls(self):   546    547         """   548         Return a representation of the view mode controls, permitting viewing of   549         aggregated events in calendar, list or table form.   550         """   551    552         page = self.page   553         request = page.request   554         fmt = request.formatter   555         _ = request.getText   556    557         output = []   558         append = output.append   559    560         # For day view links to other views, the wider view parameters should   561         # be used in order to be able to return to those other views.   562    563         specific_start = self.calendar_start   564         specific_end = self.calendar_end   565    566         multiday = self.resolution == "date" and len(specific_start.days_until(specific_end)) > 1   567    568         start = self.wider_calendar_start or self.original_calendar_start and specific_start   569         end = self.wider_calendar_end or self.original_calendar_end and specific_end   570    571         help_page = Page(request, "HelpOnEventAggregator")   572    573         calendar_link = self.getNavigationLink(start and start.as_month(), end and end.as_month(), "calendar", "month")   574         calendar_update_link = self.getUpdateLink(start and start.as_month(), end and end.as_month(), "calendar", "month")   575         list_link = self.getNavigationLink(start, end, "list", "month")   576         list_update_link = self.getUpdateLink(start, end, "list", "month")   577         table_link = self.getNavigationLink(start, end, "table", "month")   578         table_update_link = self.getUpdateLink(start, end, "table", "month")   579         map_link = self.getNavigationLink(start, end, "map", "month")   580         map_update_link = self.getUpdateLink(start, end, "map", "month")   581    582         # Specific links permit date-level navigation.   583    584         specific_day_link = self.getNavigationLink(specific_start, specific_end, "day", wider_start=start, wider_end=end)   585         specific_day_update_link = self.getUpdateLink(specific_start, specific_end, "day", wider_start=start, wider_end=end)   586         specific_list_link = self.getNavigationLink(specific_start, specific_end, "list", wider_start=start, wider_end=end)   587         specific_list_update_link = self.getUpdateLink(specific_start, specific_end, "list", wider_start=start, wider_end=end)   588         specific_table_link = self.getNavigationLink(specific_start, specific_end, "table", wider_start=start, wider_end=end)   589         specific_table_update_link = self.getUpdateLink(specific_start, specific_end, "table", wider_start=start, wider_end=end)   590         specific_map_link = self.getNavigationLink(specific_start, specific_end, "map", wider_start=start, wider_end=end)   591         specific_map_update_link = self.getUpdateLink(specific_start, specific_end, "map", wider_start=start, wider_end=end)   592    593         new_event_link = self.getNewEventLink(start)   594    595         # Write the controls.   596    597         append(fmt.div(on=1, css_class="event-view-controls"))   598    599         append(fmt.span(on=1, css_class="event-view"))   600         append(linkToPage(request, help_page, _("Help")))   601         append(fmt.span(on=0))   602    603         append(fmt.span(on=1, css_class="event-view"))   604         append(linkToPage(request, page, _("New event"), new_event_link))   605         append(fmt.span(on=0))   606    607         if self.mode != "calendar":   608             view_label = self.resolution == "date" and \   609                 (multiday and _("View days in calendar") or _("View day in calendar")) or \   610                 _("View as calendar")   611             append(fmt.span(on=1, css_class="event-view"))   612             append(linkToPage(request, page, view_label, calendar_link, onclick=calendar_update_link))   613             append(fmt.span(on=0))   614    615         if self.resolution == "date" and self.mode != "day":   616             view_label = multiday and _("View days as calendar") or _("View day as calendar")   617             append(fmt.span(on=1, css_class="event-view"))   618             append(linkToPage(request, page, view_label, specific_day_link, onclick=specific_day_update_link))   619             append(fmt.span(on=0))   620    621         if self.resolution != "date" and self.mode != "list" or self.resolution == "date":   622             view_label = self.resolution == "date" and \   623                 (multiday and _("View days in list") or _("View day in list")) or \   624                 _("View as list")   625             append(fmt.span(on=1, css_class="event-view"))   626             append(linkToPage(request, page, view_label, list_link, onclick=list_update_link))   627             append(fmt.span(on=0))   628    629         if self.resolution == "date" and self.mode != "list":   630             view_label = multiday and _("View days as list") or _("View day as list")   631             append(fmt.span(on=1, css_class="event-view"))   632             append(linkToPage(request, page, view_label, specific_list_link, onclick=specific_list_update_link))   633             append(fmt.span(on=0))   634    635         if self.resolution != "date" and self.mode != "table" or self.resolution == "date":   636             view_label = self.resolution == "date" and \   637                 (multiday and _("View days in table") or _("View day in table")) or \   638                 _("View as table")   639             append(fmt.span(on=1, css_class="event-view"))   640             append(linkToPage(request, page, view_label, table_link, onclick=table_update_link))   641             append(fmt.span(on=0))   642    643         if self.resolution == "date" and self.mode != "table":   644             view_label = multiday and _("View days as table") or _("View day as table")   645             append(fmt.span(on=1, css_class="event-view"))   646             append(linkToPage(request, page, view_label, specific_table_link, onclick=specific_table_update_link))   647             append(fmt.span(on=0))   648    649         if self.map_name:   650             if self.resolution != "date" and self.mode != "map" or self.resolution == "date":   651                 view_label = self.resolution == "date" and \   652                     (multiday and _("View days in map") or _("View day in map")) or \   653                     _("View as map")   654                 append(fmt.span(on=1, css_class="event-view"))   655                 append(linkToPage(request, page, view_label, map_link, onclick=map_update_link))   656                 append(fmt.span(on=0))   657    658             if self.resolution == "date" and self.mode != "map":   659                 view_label = multiday and _("View days as map") or _("View day as map")   660                 append(fmt.span(on=1, css_class="event-view"))   661                 append(linkToPage(request, page, view_label, specific_map_link, onclick=specific_map_update_link))   662                 append(fmt.span(on=0))   663    664         append(fmt.div(on=0))   665    666         return "".join(output)   667    668     def writeMapHeading(self):   669    670         """   671         Return the calendar heading for the current calendar, providing links   672         permitting navigation to other periods.   673         """   674    675         label = self.getCalendarPeriod()   676    677         if self.raw_calendar_start is None or self.raw_calendar_end is None:   678             fmt = self.page.request.formatter   679             output = []   680             append = output.append   681             append(fmt.span(on=1))   682             append(fmt.text(label))   683             append(fmt.span(on=0))   684             return "".join(output)   685         else:   686             return self._writeCalendarHeading(label, self.calendar_start, self.calendar_end)   687    688     def writeDateHeading(self, date):   689         if isinstance(date, Date):   690             return self.writeDayHeading(date)   691         else:   692             return self.writeMonthHeading(date)   693    694     def writeMonthHeading(self, year_month):   695    696         """   697         Return the calendar heading for the given 'year_month' (a Month object)   698         providing links permitting navigation to other months.   699         """   700    701         full_month_label = self.getFullMonthLabel(year_month)   702         end_month = year_month.update(self.duration - 1)   703         return self._writeCalendarHeading(full_month_label, year_month, end_month)   704    705     def writeDayHeading(self, date):   706    707         """   708         Return the calendar heading for the given 'date' (a Date object)   709         providing links permitting navigation to other dates.   710         """   711    712         full_date_label = self.getFullDateLabel(date)   713         end_date = date.update(self.duration - 1)   714         return self._writeCalendarHeading(full_date_label, date, end_date)   715    716     def _writeCalendarHeading(self, label, start, end):   717    718         """   719         Write a calendar heading providing links permitting navigation to other   720         periods, using the given 'label' along with the 'start' and 'end' dates   721         to provide a link to a particular period.   722         """   723    724         page = self.page   725         request = page.request   726         fmt = request.formatter   727         _ = request.getText   728    729         output = []   730         append = output.append   731    732         # Prepare navigation links.   733    734         if self.calendar_name:   735             calendar_name = self.calendar_name   736    737             # Links to the previous set of months and to a calendar shifted   738             # back one month.   739    740             previous_set_link = self.getNavigationLink(   741                 self.previous_set_start, self.previous_set_end   742                 )   743             previous_link = self.getNavigationLink(   744                 self.previous_start, self.previous_end   745                 )   746             previous_set_update_link = self.getUpdateLink(   747                 self.previous_set_start, self.previous_set_end   748                 )   749             previous_update_link = self.getUpdateLink(   750                 self.previous_start, self.previous_end   751                 )   752    753             # Links to the next set of months and to a calendar shifted   754             # forward one month.   755    756             next_set_link = self.getNavigationLink(   757                 self.next_set_start, self.next_set_end   758                 )   759             next_link = self.getNavigationLink(   760                 self.next_start, self.next_end   761                 )   762             next_set_update_link = self.getUpdateLink(   763                 self.next_set_start, self.next_set_end   764                 )   765             next_update_link = self.getUpdateLink(   766                 self.next_start, self.next_end   767                 )   768    769             # A link leading to this date being at the top of the calendar.   770    771             date_link = self.getNavigationLink(start, end)   772             date_update_link = self.getUpdateLink(start, end)   773    774             append(fmt.span(on=1, css_class="previous"))   775             append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link, title=_("Previous set")))   776             append(fmt.text(" "))   777             append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link, title=_("Previous")))   778             append(fmt.span(on=0))   779    780             append(fmt.span(on=1, css_class="next"))   781             append(linkToPage(request, page, ">", next_link, onclick=next_update_link, title=_("Next")))   782             append(fmt.text(" "))   783             append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link, title=_("Next set")))   784             append(fmt.span(on=0))   785    786             append(linkToPage(request, page, label, date_link, onclick=date_update_link, title=_("Show this period first")))   787    788         else:   789             append(fmt.span(on=1))   790             append(fmt.text(label))   791             append(fmt.span(on=0))   792    793         return "".join(output)   794    795     def writeDayNumberHeading(self, date, busy):   796    797         """   798         Return a link for the given 'date' which will activate the new event   799         action for the given day. If 'busy' is given as a true value, the   800         heading will be marked as busy.   801         """   802    803         page = self.page   804         request = page.request   805         fmt = request.formatter   806         _ = request.getText   807    808         output = []   809         append = output.append   810    811         year, month, day = date.as_tuple()   812         new_event_link = self.getNewEventLink(date)   813    814         # Prepare a link to the day view for this day.   815    816         day_view_link = self.getNavigationLink(date, date, "day", "date", self.calendar_start, self.calendar_end)   817         day_view_update_link = self.getUpdateLink(date, date, "day", "date", self.calendar_start, self.calendar_end)   818    819         # Output the heading class.   820    821         today_attr = date == getCurrentDate() and "event-day-current" or ""   822    823         append(   824             fmt.table_cell(on=1, attrs={   825                 "class" : "event-day-heading event-day-%s %s" % (busy and "busy" or "empty", today_attr),   826                 "colspan" : "3"   827                 }))   828    829         # Output the number and pop-up menu.   830    831         append(fmt.div(on=1, css_class="event-day-box"))   832    833         append(fmt.span(on=1, css_class="event-day-number-popup"))   834         append(fmt.span(on=1, css_class="event-day-number-link"))   835         append(linkToPage(request, page, _("View day"), day_view_link, onclick=day_view_update_link))   836         append(fmt.span(on=0))   837         append(fmt.span(on=1, css_class="event-day-number-link"))   838         append(linkToPage(request, page, _("New event"), new_event_link))   839         append(fmt.span(on=0))   840         append(fmt.span(on=0))   841    842         # Link the number to the day view.   843    844         append(fmt.span(on=1, css_class="event-day-number"))   845         append(linkToPage(request, page, unicode(day), day_view_link, onclick=day_view_update_link, title=_("View day")))   846         append(fmt.span(on=0))   847    848         append(fmt.div(on=0))   849    850         # End of heading.   851    852         append(fmt.table_cell(on=0))   853    854         return "".join(output)   855    856     # Common layout methods.   857    858     def getEventStyle(self, colour_seed):   859    860         "Generate colour style information using the given 'colour_seed'."   861    862         bg = getColour(colour_seed)   863         fg = getBlackOrWhite(bg)   864         return "background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg)   865    866     def writeEventSummaryBox(self, event):   867    868         "Return an event summary box linking to the given 'event'."   869    870         page = self.page   871         request = page.request   872         fmt = request.formatter   873    874         output = []   875         append = output.append   876    877         event_details = event.getDetails()   878         event_summary = event.getSummary(self.parent_name)   879    880         is_ambiguous = event.as_timespan().ambiguous()   881         style = self.getEventStyle(event_summary)   882    883         # The event box contains the summary, alongside   884         # other elements.   885    886         append(fmt.div(on=1, css_class="event-summary-box"))   887         append(fmt.div(on=1, css_class="event-summary", style=style))   888    889         if is_ambiguous:   890             append(fmt.icon("/!\\"))   891    892         append(event.linkToEvent(request, event_summary))   893         append(fmt.div(on=0))   894    895         # Add a pop-up element for long summaries.   896    897         append(fmt.div(on=1, css_class="event-summary-popup", style=style))   898    899         if is_ambiguous:   900             append(fmt.icon("/!\\"))   901    902         append(event.linkToEvent(request, event_summary))   903         append(fmt.div(on=0))   904    905         append(fmt.div(on=0))   906    907         return "".join(output)   908    909     # Calendar layout methods.   910    911     def writeMonthTableHeading(self, year_month):   912         page = self.page   913         fmt = page.request.formatter   914    915         output = []   916         append = output.append   917    918         # Using a caption for accessibility reasons.   919    920         append(fmt.rawHTML('<caption class="event-month-heading">'))   921         append(self.writeMonthHeading(year_month))   922         append(fmt.rawHTML("</caption>"))   923    924         return "".join(output)   925    926     def writeWeekdayHeadings(self):   927         page = self.page   928         request = page.request   929         fmt = request.formatter   930         _ = request.getText   931    932         output = []   933         append = output.append   934    935         append(fmt.table_row(on=1))   936    937         for weekday in range(0, 7):   938             append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"}))   939             append(fmt.text(_(getDayLabel(weekday))))   940             append(fmt.table_cell(on=0))   941    942         append(fmt.table_row(on=0))   943         return "".join(output)   944    945     def writeDayNumbers(self, first_day, number_of_days, month, coverage):   946         page = self.page   947         fmt = page.request.formatter   948    949         output = []   950         append = output.append   951    952         append(fmt.table_row(on=1))   953    954         for weekday in range(0, 7):   955             day = first_day + weekday   956             date = month.as_date(day)   957    958             # Output out-of-month days.   959    960             if day < 1 or day > number_of_days:   961                 append(fmt.table_cell(on=1,   962                     attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"}))   963                 append(fmt.table_cell(on=0))   964    965             # Output normal days.   966    967             else:   968                 # Output the day heading, making a link to a new event   969                 # action.   970    971                 append(self.writeDayNumberHeading(date, date in coverage))   972    973         # End of day numbers.   974    975         append(fmt.table_row(on=0))   976         return "".join(output)   977    978     def writeEmptyWeek(self, first_day, number_of_days, month):   979         page = self.page   980         fmt = page.request.formatter   981    982         output = []   983         append = output.append   984    985         append(fmt.table_row(on=1))   986    987         for weekday in range(0, 7):   988             day = first_day + weekday   989             date = month.as_date(day)   990    991             today_attr = date == getCurrentDate() and "event-day-current" or ""   992    993             # Output out-of-month days.   994    995             if day < 1 or day > number_of_days:   996                 append(fmt.table_cell(on=1,   997                     attrs={"class" : "event-day-content event-day-excluded %s" % today_attr, "colspan" : "3"}))   998                 append(fmt.table_cell(on=0))   999   1000             # Output empty days.  1001   1002             else:  1003                 append(fmt.table_cell(on=1,  1004                     attrs={"class" : "event-day-content event-day-empty %s" % today_attr, "colspan" : "3"}))  1005   1006         append(fmt.table_row(on=0))  1007         return "".join(output)  1008   1009     def writeWeekSlots(self, first_day, number_of_days, month, week_end, week_slots):  1010         output = []  1011         append = output.append  1012   1013         locations = week_slots.keys()  1014         locations.sort(sort_none_first)  1015   1016         # Visit each slot corresponding to a location (or no location).  1017   1018         for location in locations:  1019   1020             # Visit each coverage span, presenting the events in the span.  1021   1022             for events in week_slots[location]:  1023   1024                 # Output each set.  1025   1026                 append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events))  1027   1028                 # Add a spacer.  1029   1030                 append(self.writeWeekSpacer(first_day, number_of_days, month))  1031   1032         return "".join(output)  1033   1034     def writeWeekSlot(self, first_day, number_of_days, month, week_end, events):  1035         page = self.page  1036         request = page.request  1037         fmt = request.formatter  1038   1039         output = []  1040         append = output.append  1041   1042         append(fmt.table_row(on=1))  1043   1044         # Then, output day details.  1045   1046         for weekday in range(0, 7):  1047             day = first_day + weekday  1048             date = month.as_date(day)  1049   1050             # Skip out-of-month days.  1051   1052             if day < 1 or day > number_of_days:  1053                 append(fmt.table_cell(on=1,  1054                     attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"}))  1055                 append(fmt.table_cell(on=0))  1056                 continue  1057   1058             # Output the day.  1059             # Where a day does not contain an event, a single cell is used.  1060             # Otherwise, multiple cells are used to provide space before, during  1061             # and after events.  1062   1063             today_attr = date == getCurrentDate() and "event-day-current" or ""  1064   1065             if date not in events:  1066                 append(fmt.table_cell(on=1,  1067                     attrs={"class" : "event-day-content event-day-empty %s" % today_attr, "colspan" : "3"}))  1068   1069             # Get event details for the current day.  1070   1071             for event in events:  1072                 event_details = event.getDetails()  1073   1074                 if date not in event:  1075                     continue  1076   1077                 # Get basic properties of the event.  1078   1079                 starts_today = event_details["start"] == date  1080                 ends_today = event_details["end"] == date  1081                 event_summary = event.getSummary(self.parent_name)  1082   1083                 style = self.getEventStyle(event_summary)  1084   1085                 # Determine if the event name should be shown.  1086   1087                 start_of_period = starts_today or weekday == 0 or day == 1  1088   1089                 if self.name_usage == "daily" or start_of_period:  1090                     hide_text = 0  1091                 else:  1092                     hide_text = 1  1093   1094                 # Output start of day gap and determine whether  1095                 # any event content should be explicitly output  1096                 # for this day.  1097   1098                 if starts_today:  1099   1100                     # Single day events...  1101   1102                     if ends_today:  1103                         colspan = 3  1104                         event_day_type = "event-day-single"  1105   1106                     # Events starting today...  1107   1108                     else:  1109                         append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap %s" % today_attr}))  1110                         append(fmt.table_cell(on=0))  1111   1112                         # Calculate the span of this cell.  1113                         # Events whose names appear on every day...  1114   1115                         if self.name_usage == "daily":  1116                             colspan = 2  1117                             event_day_type = "event-day-starting"  1118   1119                         # Events whose names appear once per week...  1120   1121                         else:  1122                             if event_details["end"] <= week_end:  1123                                 event_length = event_details["end"].day() - day + 1  1124                                 colspan = (event_length - 2) * 3 + 4  1125                             else:  1126                                 event_length = week_end.day() - day + 1  1127                                 colspan = (event_length - 1) * 3 + 2  1128   1129                             event_day_type = "event-day-multiple"  1130   1131                 # Events continuing from a previous week...  1132   1133                 elif start_of_period:  1134   1135                     # End of continuing event...  1136   1137                     if ends_today:  1138                         colspan = 2  1139                         event_day_type = "event-day-ending"  1140   1141                     # Events continuing for at least one more day...  1142   1143                     else:  1144   1145                         # Calculate the span of this cell.  1146                         # Events whose names appear on every day...  1147   1148                         if self.name_usage == "daily":  1149                             colspan = 3  1150                             event_day_type = "event-day-full"  1151   1152                         # Events whose names appear once per week...  1153   1154                         else:  1155                             if event_details["end"] <= week_end:  1156                                 event_length = event_details["end"].day() - day + 1  1157                                 colspan = (event_length - 1) * 3 + 2  1158                             else:  1159                                 event_length = week_end.day() - day + 1  1160                                 colspan = event_length * 3  1161   1162                             event_day_type = "event-day-multiple"  1163   1164                 # Continuing events whose names appear on every day...  1165   1166                 elif self.name_usage == "daily":  1167                     if ends_today:  1168                         colspan = 2  1169                         event_day_type = "event-day-ending"  1170                     else:  1171                         colspan = 3  1172                         event_day_type = "event-day-full"  1173   1174                 # Continuing events whose names appear once per week...  1175   1176                 else:  1177                     colspan = None  1178   1179                 # Output the main content only if it is not  1180                 # continuing from a previous day.  1181   1182                 if colspan is not None:  1183   1184                     # Colour the cell for continuing events.  1185   1186                     attrs={  1187                         "class" : "event-day-content event-day-busy %s %s" % (event_day_type, today_attr),  1188                         "colspan" : str(colspan)  1189                         }  1190   1191                     if not (starts_today and ends_today):  1192                         attrs["style"] = style  1193   1194                     append(fmt.table_cell(on=1, attrs=attrs))  1195   1196                     # Output the event.  1197   1198                     if starts_today and ends_today or not hide_text:  1199                         append(self.writeEventSummaryBox(event))  1200   1201                     append(fmt.table_cell(on=0))  1202   1203                 # Output end of day gap.  1204   1205                 if ends_today and not starts_today:  1206                     append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap %s" % today_attr}))  1207                     append(fmt.table_cell(on=0))  1208   1209         # End of set.  1210   1211         append(fmt.table_row(on=0))  1212         return "".join(output)  1213   1214     def writeWeekSpacer(self, first_day, number_of_days, month):  1215         page = self.page  1216         fmt = page.request.formatter  1217   1218         output = []  1219         append = output.append  1220   1221         append(fmt.table_row(on=1))  1222   1223         for weekday in range(0, 7):  1224             day = first_day + weekday  1225             date = month.as_date(day)  1226             today_attr = date == getCurrentDate() and "event-day-current" or ""  1227   1228             css_classes = "event-day-spacer %s" % today_attr  1229   1230             # Skip out-of-month days.  1231   1232             if day < 1 or day > number_of_days:  1233                 css_classes += " event-day-excluded"  1234   1235             append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"}))  1236             append(fmt.table_cell(on=0))  1237   1238         append(fmt.table_row(on=0))  1239         return "".join(output)  1240   1241     # Day layout methods.  1242   1243     def writeDayTableHeading(self, date, colspan=1):  1244         page = self.page  1245         fmt = page.request.formatter  1246   1247         output = []  1248         append = output.append  1249   1250         # Using a caption for accessibility reasons.  1251   1252         append(fmt.rawHTML('<caption class="event-full-day-heading">'))  1253         append(self.writeDayHeading(date))  1254         append(fmt.rawHTML("</caption>"))  1255   1256         return "".join(output)  1257   1258     def writeEmptyDay(self, date):  1259         page = self.page  1260         fmt = page.request.formatter  1261   1262         output = []  1263         append = output.append  1264   1265         append(fmt.table_row(on=1))  1266   1267         append(fmt.table_cell(on=1,  1268             attrs={"class" : "event-day-content event-day-empty"}))  1269   1270         append(fmt.table_row(on=0))  1271         return "".join(output)  1272   1273     def writeDaySlots(self, date, full_coverage, day_slots):  1274   1275         """  1276         Given a 'date', non-empty 'full_coverage' for the day concerned, and a  1277         non-empty mapping of 'day_slots' (from locations to event collections),  1278         output the day slots for the day.  1279         """  1280   1281         page = self.page  1282         fmt = page.request.formatter  1283   1284         output = []  1285         append = output.append  1286   1287         locations = day_slots.keys()  1288         locations.sort(sort_none_first)  1289   1290         # Traverse the time scale of the full coverage, visiting each slot to  1291         # determine whether it provides content for each period.  1292   1293         scale = getCoverageScale(full_coverage)  1294   1295         # Define a mapping of events to rowspans.  1296   1297         rowspans = {}  1298   1299         # Populate each period with event details, recording how many periods  1300         # each event populates.  1301   1302         day_rows = []  1303   1304         for period, limit, times in scale:  1305   1306             # Ignore timespans before this day.  1307   1308             if period != date:  1309                 continue  1310   1311             # Visit each slot corresponding to a location (or no location).  1312   1313             day_row = []  1314   1315             for location in locations:  1316   1317                 # Visit each coverage span, presenting the events in the span.  1318   1319                 for events in day_slots[location]:  1320                     event = self.getActiveEvent(period, events)  1321                     if event is not None:  1322                         if not rowspans.has_key(event):  1323                             rowspans[event] = 1  1324                         else:  1325                             rowspans[event] += 1  1326                     day_row.append((location, event))  1327   1328             day_rows.append((period, day_row, times))  1329   1330         # Output the locations.  1331   1332         append(fmt.table_row(on=1))  1333   1334         # Add a spacer.  1335   1336         append(self.writeDaySpacer(colspan=2, cls="location"))  1337   1338         for location in locations:  1339   1340             # Add spacers to the column spans.  1341   1342             columns = len(day_slots[location]) * 2 - 1  1343             append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)}))  1344             append(fmt.text(location or ""))  1345             append(fmt.table_cell(on=0))  1346   1347             # Add a trailing spacer.  1348   1349             append(self.writeDaySpacer(cls="location"))  1350   1351         append(fmt.table_row(on=0))  1352   1353         # Output the periods with event details.  1354   1355         last_period = period = None  1356         events_written = set()  1357   1358         for period, day_row, times in day_rows:  1359   1360             # Write a heading describing the time.  1361   1362             append(fmt.table_row(on=1))  1363   1364             # Show times only for distinct periods.  1365   1366             if not last_period or period.start != last_period.start:  1367                 append(self.writeDayScaleHeading(times))  1368             else:  1369                 append(self.writeDayScaleHeading([]))  1370   1371             append(self.writeDaySpacer())  1372   1373             # Visit each slot corresponding to a location (or no location).  1374   1375             for location, event in day_row:  1376   1377                 # Output each location slot's contribution.  1378   1379                 if event is None or event not in events_written:  1380                     append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event]))  1381                     if event is not None:  1382                         events_written.add(event)  1383   1384                 # Add a trailing spacer.  1385   1386                 append(self.writeDaySpacer())  1387   1388             append(fmt.table_row(on=0))  1389   1390             last_period = period  1391   1392         # Write a final time heading if the last period ends in the current day.  1393   1394         if period is not None:  1395             if period.end == date:  1396                 append(fmt.table_row(on=1))  1397                 append(self.writeDayScaleHeading(times))  1398   1399                 for slot in day_row:  1400                     append(self.writeDaySpacer())  1401                     append(self.writeEmptyDaySlot())  1402   1403                 append(fmt.table_row(on=0))  1404   1405         return "".join(output)  1406   1407     def writeDayScaleHeading(self, times):  1408         page = self.page  1409         fmt = page.request.formatter  1410   1411         output = []  1412         append = output.append  1413   1414         append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"}))  1415   1416         first = 1  1417         for t in times:  1418             if isinstance(t, DateTime):  1419                 if not first:  1420                     append(fmt.linebreak(0))  1421                 append(fmt.text(t.time_string()))  1422             first = 0  1423   1424         append(fmt.table_cell(on=0))  1425   1426         return "".join(output)  1427   1428     def getActiveEvent(self, period, events):  1429         for event in events:  1430             if period not in event:  1431                 continue  1432             return event  1433         else:  1434             return None  1435   1436     def writeDaySlot(self, period, event, rowspan):  1437         page = self.page  1438         fmt = page.request.formatter  1439   1440         output = []  1441         append = output.append  1442   1443         if event is not None:  1444             event_summary = event.getSummary(self.parent_name)  1445             style = self.getEventStyle(event_summary)  1446   1447             append(fmt.table_cell(on=1, attrs={  1448                 "class" : "event-timespan-content event-timespan-busy",  1449                 "style" : style,  1450                 "rowspan" : str(rowspan)  1451                 }))  1452             append(self.writeEventSummaryBox(event))  1453             append(fmt.table_cell(on=0))  1454         else:  1455             append(self.writeEmptyDaySlot())  1456   1457         return "".join(output)  1458   1459     def writeEmptyDaySlot(self):  1460         page = self.page  1461         fmt = page.request.formatter  1462   1463         output = []  1464         append = output.append  1465   1466         append(fmt.table_cell(on=1,  1467             attrs={"class" : "event-timespan-content event-timespan-empty"}))  1468         append(fmt.table_cell(on=0))  1469   1470         return "".join(output)  1471   1472     def writeDaySpacer(self, colspan=1, cls="timespan"):  1473         page = self.page  1474         fmt = page.request.formatter  1475   1476         output = []  1477         append = output.append  1478   1479         append(fmt.table_cell(on=1, attrs={  1480             "class" : "event-%s-spacer" % cls,  1481             "colspan" : str(colspan)}))  1482         append(fmt.table_cell(on=0))  1483         return "".join(output)  1484   1485     # Map layout methods.  1486   1487     def writeMapTableHeading(self):  1488         page = self.page  1489         fmt = page.request.formatter  1490   1491         output = []  1492         append = output.append  1493   1494         # Using a caption for accessibility reasons.  1495   1496         append(fmt.rawHTML('<caption class="event-map-heading">'))  1497         append(self.writeMapHeading())  1498         append(fmt.rawHTML("</caption>"))  1499   1500         return "".join(output)  1501   1502     def showDictError(self, text, pagename):  1503         page = self.page  1504         request = page.request  1505         fmt = request.formatter  1506   1507         output = []  1508         append = output.append  1509   1510         append(fmt.div(on=1, attrs={"class" : "event-aggregator-error"}))  1511         append(fmt.paragraph(on=1))  1512         append(fmt.text(text))  1513         append(fmt.paragraph(on=0))  1514         append(fmt.paragraph(on=1))  1515         append(linkToPage(request, Page(request, pagename), pagename))  1516         append(fmt.paragraph(on=0))  1517   1518         return "".join(output)  1519   1520     def writeMapMarker(self, marker_x, marker_y, map_x_scale, map_y_scale, location, events):  1521   1522         "Put a marker on the map."  1523   1524         page = self.page  1525         request = page.request  1526         fmt = request.formatter  1527   1528         output = []  1529         append = output.append  1530   1531         append(fmt.listitem(on=1, css_class="event-map-label"))  1532   1533         # Have a positioned marker for the print mode.  1534   1535         append(fmt.div(on=1, css_class="event-map-label-only",  1536             style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % (  1537                 marker_x, marker_y, map_x_scale, map_y_scale))  1538         append(fmt.div(on=0))  1539   1540         # Have a marker containing a pop-up when using the screen mode,  1541         # providing a normal block when using the print mode.  1542   1543         append(fmt.div(on=1, css_class="event-map-label",  1544             style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % (  1545                 marker_x, marker_y, map_x_scale, map_y_scale))  1546         append(fmt.div(on=1, css_class="event-map-details"))  1547         append(fmt.div(on=1, css_class="event-map-shadow"))  1548         append(fmt.div(on=1, css_class="event-map-location"))  1549   1550         # The location may have been given as formatted text, but this will not  1551         # be usable in a heading, so it must be first converted to plain text.  1552   1553         append(fmt.heading(on=1, depth=2))  1554         append(fmt.text(to_plain_text(location, request)))  1555         append(fmt.heading(on=0, depth=2))  1556   1557         append(self.writeMapEventSummaries(events))  1558   1559         append(fmt.div(on=0))  1560         append(fmt.div(on=0))  1561         append(fmt.div(on=0))  1562         append(fmt.div(on=0))  1563         append(fmt.listitem(on=0))  1564   1565         return "".join(output)  1566   1567     def writeMapEventSummaries(self, events):  1568   1569         "Write summaries of the given 'events' for the map."  1570   1571         page = self.page  1572         request = page.request  1573         fmt = request.formatter  1574   1575         # Sort the events by date.  1576   1577         events.sort(sort_start_first)  1578   1579         # Write out a self-contained list of events.  1580   1581         output = []  1582         append = output.append  1583   1584         append(fmt.bullet_list(on=1, attr={"class" : "event-map-location-events"}))  1585   1586         for event in events:  1587   1588             # Get the event details.  1589   1590             event_summary = event.getSummary(self.parent_name)  1591             start, end = event.as_limits()  1592             event_period = self._getCalendarPeriod(  1593                 start and self.getFullDateLabel(start),  1594                 end and self.getFullDateLabel(end),  1595                 "")  1596   1597             append(fmt.listitem(on=1))  1598   1599             # Link to the page using the summary.  1600   1601             append(event.linkToEvent(request, event_summary))  1602   1603             # Add the event period.  1604   1605             append(fmt.text(" "))  1606             append(fmt.span(on=1, css_class="event-map-period"))  1607             append(fmt.text(event_period))  1608             append(fmt.span(on=0))  1609   1610             append(fmt.listitem(on=0))  1611   1612         append(fmt.bullet_list(on=0))  1613   1614         return "".join(output)  1615   1616     def render(self, all_shown_events):  1617   1618         """  1619         Render the view, returning the rendered representation as a string.  1620         The view will show a list of 'all_shown_events'.  1621         """  1622   1623         page = self.page  1624         request = page.request  1625         fmt = request.formatter  1626         _ = request.getText  1627   1628         # Make a calendar.  1629   1630         output = []  1631         append = output.append  1632   1633         append(fmt.div(on=1, css_class="event-calendar", id=("EventAggregator-%s" % self.getIdentifier())))  1634   1635         # Output download controls.  1636   1637         append(fmt.div(on=1, css_class="event-controls"))  1638         append(self.writeDownloadControls())  1639         append(fmt.div(on=0))  1640   1641         # Output a table.  1642   1643         if self.mode == "table":  1644   1645             # Start of table view output.  1646   1647             append(fmt.table(on=1, attrs={"tableclass" : "event-table", "summary" : _("A table of events")}))  1648   1649             append(fmt.table_row(on=1))  1650             append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"}))  1651             append(fmt.text(_("Event dates")))  1652             append(fmt.table_cell(on=0))  1653             append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"}))  1654             append(fmt.text(_("Event location")))  1655             append(fmt.table_cell(on=0))  1656             append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"}))  1657             append(fmt.text(_("Event details")))  1658             append(fmt.table_cell(on=0))  1659             append(fmt.table_row(on=0))  1660   1661             # Show the events in order.  1662   1663             all_shown_events.sort(sort_start_first)  1664   1665             for event in all_shown_events:  1666                 event_page = event.getPage()  1667                 event_summary = event.getSummary(self.parent_name)  1668                 event_details = event.getDetails()  1669   1670                 # Prepare CSS classes with category-related styling.  1671   1672                 css_classes = ["event-table-details"]  1673   1674                 for topic in event_details.get("topics") or event_details.get("categories") or []:  1675   1676                     # Filter the category text to avoid illegal characters.  1677   1678                     css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic)))  1679   1680                 attrs = {"class" : " ".join(css_classes)}  1681   1682                 append(fmt.table_row(on=1))  1683   1684                 # Start and end dates.  1685   1686                 append(fmt.table_cell(on=1, attrs=attrs))  1687                 append(fmt.span(on=1))  1688                 append(fmt.text(str(event_details["start"])))  1689                 append(fmt.span(on=0))  1690   1691                 if event_details["start"] != event_details["end"]:  1692                     append(fmt.text(" - "))  1693                     append(fmt.span(on=1))  1694                     append(fmt.text(str(event_details["end"])))  1695                     append(fmt.span(on=0))  1696   1697                 append(fmt.table_cell(on=0))  1698   1699                 # Location.  1700   1701                 append(fmt.table_cell(on=1, attrs=attrs))  1702   1703                 if event_details.has_key("location"):  1704                     append(event_page.formatText(event_details["location"], fmt))  1705   1706                 append(fmt.table_cell(on=0))  1707   1708                 # Link to the page using the summary.  1709   1710                 append(fmt.table_cell(on=1, attrs=attrs))  1711                 append(event.linkToEvent(request, event_summary))  1712                 append(fmt.table_cell(on=0))  1713   1714                 append(fmt.table_row(on=0))  1715   1716             # End of table view output.  1717   1718             append(fmt.table(on=0))  1719   1720         # Output a map view.  1721   1722         elif self.mode == "map":  1723   1724             # Special dictionary pages.  1725   1726             maps_page = getMapsPage(request)  1727             locations_page = getLocationsPage(request)  1728   1729             map_image = None  1730   1731             # Get the maps and locations.  1732   1733             maps = getWikiDict(maps_page, request)  1734             locations = getWikiDict(locations_page, request)  1735   1736             # Get the map image definition.  1737   1738             if maps is not None and self.map_name:  1739                 try:  1740                     map_details = maps[self.map_name].split()  1741   1742                     map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \  1743                         map(getMapReference, map_details[:4])  1744                     map_width, map_height = map(int, map_details[4:6])  1745                     map_image = map_details[6]  1746   1747                     map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees()  1748                     map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees()  1749   1750                 except (KeyError, ValueError):  1751                     pass  1752   1753             # Report errors.  1754   1755             if maps is None:  1756                 append(self.showDictError(  1757                     _("You do not have read access to the maps page:"),  1758                     maps_page))  1759   1760             elif not self.map_name:  1761                 append(self.showDictError(  1762                     _("Please specify a valid map name corresponding to an entry on the following page:"),  1763                     maps_page))  1764   1765             elif map_image is None:  1766                 append(self.showDictError(  1767                     _("Please specify a valid entry for %s on the following page:") % self.map_name,  1768                     maps_page))  1769   1770             elif locations is None:  1771                 append(self.showDictError(  1772                     _("You do not have read access to the locations page:"),  1773                     locations_page))  1774   1775             # Attempt to show the map.  1776   1777             else:  1778   1779                 # Get events by position.  1780   1781                 events_by_location = {}  1782                 event_locations = {}  1783   1784                 for event in all_shown_events:  1785                     event_details = event.getDetails()  1786   1787                     location = event_details.get("location")  1788                     geo = event_details.get("geo")  1789   1790                     # Make a temporary location if an explicit position is given  1791                     # but not a location name.  1792   1793                     if not location and geo:  1794                         location = "%s %s" % tuple(geo)  1795   1796                     # Map the location to a position.  1797   1798                     if location is not None and not event_locations.has_key(location):  1799   1800                         # Get any explicit position of an event.  1801   1802                         if geo:  1803                             latitude, longitude = geo  1804   1805                         # Or look up the position of a location using the locations  1806                         # page.  1807   1808                         else:  1809                             latitude, longitude = Location(location, locations).getPosition()  1810   1811                         # Use a normalised location if necessary.  1812   1813                         if latitude is None and longitude is None:  1814                             normalised_location = getNormalisedLocation(location)  1815                             if normalised_location is not None:  1816                                 latitude, longitude = getLocationPosition(normalised_location, locations)  1817                                 if latitude is not None and longitude is not None:  1818                                     location = normalised_location  1819   1820                         # Only remember positioned locations.  1821   1822                         if latitude is not None and longitude is not None:  1823                             event_locations[location] = latitude, longitude  1824   1825                     # Record events according to location.  1826   1827                     if not events_by_location.has_key(location):  1828                         events_by_location[location] = []  1829   1830                     events_by_location[location].append(event)  1831   1832                 # Get the map image URL.  1833   1834                 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request)  1835   1836                 # Start of map view output.  1837   1838                 map_identifier = "map-%s" % self.getIdentifier()  1839                 append(fmt.div(on=1, css_class="event-map", id=map_identifier))  1840   1841                 append(fmt.table(on=1, attrs={"summary" : _("A map showing events")}))  1842   1843                 append(self.writeMapTableHeading())  1844   1845                 append(fmt.table_row(on=1))  1846                 append(fmt.table_cell(on=1))  1847   1848                 append(fmt.div(on=1, css_class="event-map-container"))  1849                 append(fmt.image(map_image_url))  1850                 append(fmt.number_list(on=1))  1851   1852                 # Events with no location are unpositioned.  1853   1854                 if events_by_location.has_key(None):  1855                     unpositioned_events = events_by_location[None]  1856                     del events_by_location[None]  1857                 else:  1858                     unpositioned_events = []  1859   1860                 # Events whose location is unpositioned are themselves considered  1861                 # unpositioned.  1862   1863                 for location in set(events_by_location.keys()).difference(event_locations.keys()):  1864                     unpositioned_events += events_by_location[location]  1865   1866                 # Sort the locations before traversing them.  1867   1868                 event_locations = event_locations.items()  1869                 event_locations.sort()  1870   1871                 # Show the events in the map.  1872   1873                 for location, (latitude, longitude) in event_locations:  1874                     events = events_by_location[location]  1875   1876                     # Skip unpositioned locations and locations outside the map.  1877   1878                     if latitude is None or longitude is None or \  1879                         latitude < map_bottom_left_latitude or \  1880                         longitude < map_bottom_left_longitude or \  1881                         latitude > map_top_right_latitude or \  1882                         longitude > map_top_right_longitude:  1883   1884                         unpositioned_events += events  1885                         continue  1886   1887                     # Get the position and dimensions of the map marker.  1888                     # NOTE: Use one degree as the marker size.  1889   1890                     marker_x, marker_y = getPositionForCentrePoint(  1891                         getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude,  1892                             map_x_scale, map_y_scale),  1893                         map_x_scale, map_y_scale)  1894   1895                     # Add the map marker.  1896   1897                     append(self.writeMapMarker(marker_x, marker_y, map_x_scale, map_y_scale, location, events))  1898   1899                 append(fmt.number_list(on=0))  1900                 append(fmt.div(on=0))  1901                 append(fmt.table_cell(on=0))  1902                 append(fmt.table_row(on=0))  1903   1904                 # Write unpositioned events.  1905   1906                 if unpositioned_events:  1907                     unpositioned_identifier = "unpositioned-%s" % self.getIdentifier()  1908   1909                     append(fmt.table_row(on=1, css_class="event-map-unpositioned",  1910                         id=unpositioned_identifier))  1911                     append(fmt.table_cell(on=1))  1912   1913                     append(fmt.heading(on=1, depth=2))  1914                     append(fmt.text(_("Events not shown on the map")))  1915                     append(fmt.heading(on=0, depth=2))  1916   1917                     # Show and hide controls.  1918   1919                     append(fmt.div(on=1, css_class="event-map-show-control"))  1920                     append(fmt.anchorlink(on=1, name=unpositioned_identifier))  1921                     append(fmt.text(_("Show unpositioned events")))  1922                     append(fmt.anchorlink(on=0))  1923                     append(fmt.div(on=0))  1924   1925                     append(fmt.div(on=1, css_class="event-map-hide-control"))  1926                     append(fmt.anchorlink(on=1, name=map_identifier))  1927                     append(fmt.text(_("Hide unpositioned events")))  1928                     append(fmt.anchorlink(on=0))  1929                     append(fmt.div(on=0))  1930   1931                     append(self.writeMapEventSummaries(unpositioned_events))  1932   1933                 # End of map view output.  1934   1935                 append(fmt.table_cell(on=0))  1936                 append(fmt.table_row(on=0))  1937                 append(fmt.table(on=0))  1938                 append(fmt.div(on=0))  1939   1940         # Output a list.  1941   1942         elif self.mode == "list":  1943   1944             # Start of list view output.  1945   1946             append(fmt.bullet_list(on=1, attr={"class" : "event-listings"}))  1947   1948             # Output a list.  1949             # NOTE: Make the heading depth configurable.  1950   1951             for period in self.first.until(self.last):  1952   1953                 append(fmt.listitem(on=1, attr={"class" : "event-listings-period"}))  1954                 append(fmt.heading(on=1, depth=2, attr={"class" : "event-listings-heading"}))  1955   1956                 # Either write a date heading or produce links for navigable  1957                 # calendars.  1958   1959                 append(self.writeDateHeading(period))  1960   1961                 append(fmt.heading(on=0, depth=2))  1962   1963                 append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"}))  1964   1965                 # Show the events in order.  1966   1967                 events_in_period = getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period))  1968                 events_in_period.sort(sort_start_first)  1969   1970                 for event in events_in_period:  1971                     event_page = event.getPage()  1972                     event_details = event.getDetails()  1973                     event_summary = event.getSummary(self.parent_name)  1974   1975                     append(fmt.listitem(on=1, attr={"class" : "event-listing"}))  1976   1977                     # Link to the page using the summary.  1978   1979                     append(fmt.paragraph(on=1))  1980                     append(event.linkToEvent(request, event_summary))  1981                     append(fmt.paragraph(on=0))  1982   1983                     # Start and end dates.  1984   1985                     append(fmt.paragraph(on=1))  1986                     append(fmt.span(on=1))  1987                     append(fmt.text(str(event_details["start"])))  1988                     append(fmt.span(on=0))  1989                     append(fmt.text(" - "))  1990                     append(fmt.span(on=1))  1991                     append(fmt.text(str(event_details["end"])))  1992                     append(fmt.span(on=0))  1993                     append(fmt.paragraph(on=0))  1994   1995                     # Location.  1996   1997                     if event_details.has_key("location"):  1998                         append(fmt.paragraph(on=1))  1999                         append(event_page.formatText(event_details["location"], fmt))  2000                         append(fmt.paragraph(on=1))  2001   2002                     # Topics.  2003   2004                     if event_details.has_key("topics") or event_details.has_key("categories"):  2005                         append(fmt.bullet_list(on=1, attr={"class" : "event-topics"}))  2006   2007                         for topic in event_details.get("topics") or event_details.get("categories") or []:  2008                             append(fmt.listitem(on=1))  2009                             append(event_page.formatText(topic, fmt))  2010                             append(fmt.listitem(on=0))  2011   2012                         append(fmt.bullet_list(on=0))  2013   2014                     append(fmt.listitem(on=0))  2015   2016                 append(fmt.bullet_list(on=0))  2017   2018             # End of list view output.  2019   2020             append(fmt.bullet_list(on=0))  2021   2022         # Output a month calendar. This shows month-by-month data.  2023   2024         elif self.mode == "calendar":  2025   2026             # Visit all months in the requested range, or across known events.  2027   2028             for month in self.first.months_until(self.last):  2029   2030                 # Output a month.  2031   2032                 append(fmt.table(on=1, attrs={"tableclass" : "event-month", "summary" : _("A table showing a calendar month")}))  2033   2034                 # Either write a month heading or produce links for navigable  2035                 # calendars.  2036   2037                 append(self.writeMonthTableHeading(month))  2038   2039                 # Weekday headings.  2040   2041                 append(self.writeWeekdayHeadings())  2042   2043                 # Process the days of the month.  2044   2045                 start_weekday, number_of_days = month.month_properties()  2046   2047                 # The start weekday is the weekday of day number 1.  2048                 # Find the first day of the week, counting from below zero, if  2049                 # necessary, in order to land on the first day of the month as  2050                 # day number 1.  2051   2052                 first_day = 1 - start_weekday  2053   2054                 while first_day <= number_of_days:  2055   2056                     # Find events in this week and determine how to mark them on the  2057                     # calendar.  2058   2059                     week_start = month.as_date(max(first_day, 1))  2060                     week_end = month.as_date(min(first_day + 6, number_of_days))  2061   2062                     full_coverage, week_slots = getCoverage(  2063                         getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end)))  2064   2065                     # Make a new table region.  2066                     # NOTE: Moin opens a "tbody" element in the table method.  2067   2068                     append(fmt.rawHTML("</tbody>"))  2069                     append(fmt.rawHTML("<tbody>"))  2070   2071                     # Output a week, starting with the day numbers.  2072   2073                     append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage))  2074   2075                     # Either generate empty days...  2076   2077                     if not week_slots:  2078                         append(self.writeEmptyWeek(first_day, number_of_days, month))  2079   2080                     # Or generate each set of scheduled events...  2081   2082                     else:  2083                         append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots))  2084   2085                     # Process the next week...  2086   2087                     first_day += 7  2088   2089                 # End of month.  2090                 # NOTE: Moin closes a "tbody" element in the table method.  2091   2092                 append(fmt.table(on=0))  2093   2094         # Output a day view.  2095   2096         elif self.mode == "day":  2097   2098             # Visit all days in the requested range, or across known events.  2099   2100             for date in self.first.days_until(self.last):  2101   2102                 append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day", "summary" : _("A table showing a calendar day")}))  2103   2104                 full_coverage, day_slots = getCoverage(  2105                     getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime")  2106   2107                 # Work out how many columns the day title will need.  2108                 # Include spacers after the scale and each event column.  2109   2110                 colspan = sum(map(len, day_slots.values())) * 2 + 2  2111   2112                 append(self.writeDayTableHeading(date, colspan))  2113   2114                 # Either generate empty days...  2115   2116                 if not day_slots:  2117                     append(self.writeEmptyDay(date))  2118   2119                 # Or generate each set of scheduled events...  2120   2121                 else:  2122                     append(self.writeDaySlots(date, full_coverage, day_slots))  2123   2124                 # End of day.  2125   2126                 append(fmt.table(on=0))  2127   2128         # Output view controls.  2129   2130         append(fmt.div(on=1, css_class="event-controls"))  2131         append(self.writeViewControls())  2132         append(fmt.div(on=0))  2133   2134         # Close the calendar region.  2135   2136         append(fmt.div(on=0))  2137   2138         # Add any scripts.  2139   2140         if isinstance(fmt, request.html_formatter.__class__):  2141             append(self.update_script)  2142   2143         return ''.join(output)  2144   2145     update_script = """\  2146 <script type="text/javascript">  2147 function replaceCalendar(name, url) {  2148     var calendar = document.getElementById(name);  2149   2150     if (calendar == null) {  2151         return true;  2152     }  2153   2154     var xmlhttp = new XMLHttpRequest();  2155     xmlhttp.open("GET", url, false);  2156     xmlhttp.send(null);  2157   2158     var newCalendar = xmlhttp.responseText;  2159   2160     if (newCalendar != null) {  2161         calendar.innerHTML = newCalendar;  2162         return false;  2163     }  2164   2165     return true;  2166 }  2167 </script>  2168 """  2169   2170 # vim: tabstop=4 expandtab shiftwidth=4