EventAggregator

macros/EventAggregator.py

51:c8ca5d3a4587
2009-10-02 Paul Boddie Added a table view to the macro. Added styles to support the table view and to colour events for special topics. Updated the documentation. Updated the release information.
     1 # -*- coding: iso-8859-1 -*-     2 """     3     MoinMoin - EventAggregator Macro     4      5     @copyright: 2008, 2009 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 MoinMoin import wikiutil    12 import EventAggregatorSupport    13 import calendar    14     15 linkToPage = EventAggregatorSupport.linkToPage    16     17 try:    18     set    19 except NameError:    20     from sets import Set as set    21     22 Dependencies = ['pages']    23     24 # Abstractions.    25     26 class View:    27     28     "A view of the event calendar."    29     30     def __init__(self, page, calendar_name, first, last):    31     32         """    33         Initialise the view with the current 'page', a 'calendar_name' (which    34         may be None), and the 'first' and 'last' months.    35         """    36     37         self.page = page    38         self.calendar_name = calendar_name    39     40         if self.calendar_name is not None:    41     42             # Store the view parameters.    43     44             span = EventAggregatorSupport.span(first, last)    45             self.number_of_months = span[0] * 12 + span[1] + 1    46     47             self.previous_month_start = EventAggregatorSupport.prevmonth(first)    48             self.next_month_start = EventAggregatorSupport.nextmonth(first)    49             self.previous_month_end = EventAggregatorSupport.prevmonth(last)    50             self.next_month_end = EventAggregatorSupport.nextmonth(last)    51     52             self.previous_set_start = EventAggregatorSupport.monthupdate(first, -self.number_of_months)    53             self.next_set_start = EventAggregatorSupport.monthupdate(first, self.number_of_months)    54             self.previous_set_end = EventAggregatorSupport.monthupdate(last, -self.number_of_months)    55             self.next_set_end = EventAggregatorSupport.monthupdate(last, self.number_of_months)    56     57     def getMonthQueryString(self, argname, month):    58         if month is not None:    59             return "%s-%s=%04d-%02d" % ((self.calendar_name, argname) + month)    60         else:    61             return ""    62     63     def writeMonthHeading(self, year, month):    64         page = self.page    65         request = page.request    66         fmt = page.formatter    67         _ = request.getText    68     69         output = []    70     71         month_label = _(EventAggregatorSupport.getMonthLabel(month))    72     73         # Prepare navigation links.    74     75         if self.calendar_name is not None:    76             calendar_name = self.calendar_name    77     78             # Links to the previous set of months and to a calendar shifted    79             # back one month.    80     81             previous_set_link = "%s&%s" % (    82                 self.getMonthQueryString("start", self.previous_set_start),    83                 self.getMonthQueryString("end", self.previous_set_end)    84                 )    85             previous_month_link = "%s&%s" % (    86                 self.getMonthQueryString("start", self.previous_month_start),    87                 self.getMonthQueryString("end", self.previous_month_end)    88                 )    89     90             # Links to the next set of months and to a calendar shifted    91             # forward one month.    92     93             next_set_link = "%s&%s" % (    94                 self.getMonthQueryString("start", self.next_set_start),    95                 self.getMonthQueryString("end", self.next_set_end)    96                 )    97             next_month_link = "%s&%s" % (    98                 self.getMonthQueryString("start", self.next_month_start),    99                 self.getMonthQueryString("end", self.next_month_end)   100                 )   101    102             # A link leading to this month being at the top of the calendar.   103    104             full_month_label = "%s %s" % (month_label, year)   105             end_month = EventAggregatorSupport.monthupdate((year, month), self.number_of_months - 1)   106    107             month_link = "%s&%s" % (   108                 self.getMonthQueryString("start", (year, month)),   109                 self.getMonthQueryString("end", end_month)   110                 )   111    112             output.append(fmt.span(on=1, css_class="previous-month"))   113             output.append(linkToPage(request, page, "<<", previous_set_link))   114             output.append(fmt.text(" "))   115             output.append(linkToPage(request, page, "<", previous_month_link))   116             output.append(fmt.span(on=0))   117    118             output.append(fmt.span(on=1, css_class="next-month"))   119             output.append(linkToPage(request, page, ">", next_month_link))   120             output.append(fmt.text(" "))   121             output.append(linkToPage(request, page, ">>", next_set_link))   122             output.append(fmt.span(on=0))   123    124             output.append(linkToPage(request, page, full_month_label, month_link))   125    126         else:   127             output.append(fmt.span(on=1))   128             output.append(fmt.text(month_label))   129             output.append(fmt.span(on=0))   130             output.append(fmt.text(" "))   131             output.append(fmt.span(on=1))   132             output.append(fmt.text(unicode(year)))   133             output.append(fmt.span(on=0))   134    135         return "".join(output)   136    137 # HTML-related functions.   138    139 def getColour(s):   140     colour = [0, 0, 0]   141     digit = 0   142     for c in s:   143         colour[digit] += ord(c)   144         colour[digit] = colour[digit] % 256   145         digit += 1   146         digit = digit % 3   147     return tuple(colour)   148    149 def getBlackOrWhite(colour):   150     if sum(colour) / 3.0 > 127:   151         return (0, 0, 0)   152     else:   153         return (255, 255, 255)   154    155 def getMonthActionQueryString(argname, month):   156     if month is not None:   157         return "%s=%04d-%02d" % ((argname,) + month)   158     else:   159         return ""   160    161 # Macro functions.   162    163 def execute(macro, args):   164    165     """   166     Execute the 'macro' with the given 'args': an optional list of selected   167     category names (categories whose pages are to be shown), together with   168     optional named arguments of the following forms:   169    170       start=YYYY-MM     shows event details starting from the specified month   171       start=current-N   shows event details relative to the current month   172       end=YYYY-MM       shows event details ending at the specified month   173       end=current+N     shows event details relative to the current month   174    175       mode=calendar     shows a calendar view of events   176       mode=list         shows a list of events by month   177       mode=ics          provides iCalendar data for the events   178    179       names=daily       shows the name of an event on every day of that event   180       names=weekly      shows the name of an event once per week   181    182       calendar=NAME     uses the given NAME to provide request parameters which   183                         can be used to control the calendar view   184     """   185    186     request = macro.request   187     fmt = macro.formatter   188     page = fmt.page   189     _ = request.getText   190    191     # Interpret the arguments.   192    193     try:   194         parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or []   195     except AttributeError:   196         parsed_args = args.split(",")   197    198     parsed_args = [arg for arg in parsed_args if arg]   199    200     # Get special arguments.   201    202     category_names = []   203     calendar_start = None   204     calendar_end = None   205     mode = "calendar"   206     name_usage = "weekly"   207     calendar_name = None   208    209     for arg in parsed_args:   210         if arg.startswith("start="):   211             calendar_start = EventAggregatorSupport.getParameterMonth(arg[6:])   212    213         elif arg.startswith("end="):   214             calendar_end = EventAggregatorSupport.getParameterMonth(arg[4:])   215    216         elif arg.startswith("mode="):   217             mode = arg[5:]   218    219         elif arg.startswith("names="):   220             name_usage = arg[6:]   221    222         elif arg.startswith("calendar="):   223             calendar_name = arg[9:]   224    225         else:   226             category_names.append(arg)   227    228     # Find request parameters to override settings.   229    230     if calendar_name is not None:   231         calendar_start = EventAggregatorSupport.getFormMonth(request, calendar_name, "start") or calendar_start   232         calendar_end = EventAggregatorSupport.getFormMonth(request, calendar_name, "end") or calendar_end   233    234     # Get the events.   235    236     events, shown_events, all_shown_events, earliest, latest = \   237         EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end)   238    239     # Get a concrete period of time.   240    241     first, last = EventAggregatorSupport.getConcretePeriod(calendar_start, calendar_end, earliest, latest)   242    243     # Define a view of the calendar, retaining useful navigational information.   244    245     view = View(page, calendar_name, first, last)   246    247     # Make a calendar.   248    249     output = []   250    251     # Output download controls.   252    253     download_all_link = "action=EventAggregatorSummary&doit=1&%s" % (   254         "&".join([("category=%s" % name) for name in category_names])   255         )   256     download_link = download_all_link + ("&%s&%s" % (   257         getMonthActionQueryString("start", calendar_start),   258         getMonthActionQueryString("end", calendar_end)   259         ))   260     subscribe_all_link = download_all_link + "&format=RSS"   261     subscribe_link = download_link + "&format=RSS"   262    263     output.append(fmt.div(on=1, css_class="event-controls"))   264     output.append(fmt.span(on=1, css_class="event-download"))   265     output.append(linkToPage(request, page, _("Download this view"), download_link))   266     output.append(fmt.span(on=0))   267     output.append(fmt.span(on=1, css_class="event-download"))   268     output.append(linkToPage(request, page, _("Download this calendar"), download_all_link))   269     output.append(fmt.span(on=0))   270     output.append(fmt.span(on=1, css_class="event-download"))   271     output.append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link))   272     output.append(fmt.span(on=0))   273     output.append(fmt.span(on=1, css_class="event-download"))   274     output.append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link))   275     output.append(fmt.span(on=0))   276     output.append(fmt.div(on=0))   277    278     # Output top-level information.   279    280     # Start of list view output.   281    282     if mode == "list":   283         output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"}))   284    285     # Start of table view output.   286    287     elif mode == "table":   288    289         # Output a table.   290    291         output.append(fmt.table(on=1, attrs={"tableclass" : "event-table"}))   292    293         output.append(fmt.table_row(on=1))   294         output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"}))   295         output.append(fmt.text(_("Event dates")))   296         output.append(fmt.table_cell(on=0))   297         output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"}))   298         output.append(fmt.text(_("Event location")))   299         output.append(fmt.table_cell(on=0))   300         output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"}))   301         output.append(fmt.text(_("Event details")))   302         output.append(fmt.table_cell(on=0))   303         output.append(fmt.table_row(on=0))   304    305     # Visit all months in the requested range, or across known events.   306    307     for year, month in EventAggregatorSupport.daterange(first, last):   308    309         # Either output a calendar view...   310    311         if mode == "calendar":   312    313             # Output a month.   314    315             output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"}))   316    317             output.append(fmt.table_row(on=1))   318             output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"}))   319    320             # Either write a month heading or produce links for navigable   321             # calendars.   322    323             output.append(view.writeMonthHeading(year, month))   324    325             output.append(fmt.table_cell(on=0))   326             output.append(fmt.table_row(on=0))   327    328             # Weekday headings.   329    330             output.append(fmt.table_row(on=1))   331    332             for weekday in range(0, 7):   333                 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"}))   334                 output.append(fmt.text(_(EventAggregatorSupport.getDayLabel(weekday))))   335                 output.append(fmt.table_cell(on=0))   336    337             output.append(fmt.table_row(on=0))   338    339             # Process the days of the month.   340    341             start_weekday, number_of_days = calendar.monthrange(year, month)   342    343             # The start weekday is the weekday of day number 1.   344             # Find the first day of the week, counting from below zero, if   345             # necessary, in order to land on the first day of the month as   346             # day number 1.   347    348             first_day = 1 - start_weekday   349    350             while first_day <= number_of_days:   351    352                 # Find events in this week and determine how to mark them on the   353                 # calendar.   354    355                 week_start = (year, month, max(first_day, 1))   356                 week_end = (year, month, min(first_day + 6, number_of_days))   357    358                 week_coverage, week_events = EventAggregatorSupport.getCoverage(   359                     week_start, week_end, shown_events.get((year, month), []))   360    361                 # Output a week, starting with the day numbers.   362    363                 output.append(fmt.table_row(on=1))   364    365                 for weekday in range(0, 7):   366                     day = first_day + weekday   367                     date = (year, month, day)   368    369                     # Output out-of-month days.   370    371                     if day < 1 or day > number_of_days:   372                         output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"}))   373                         output.append(fmt.table_cell(on=0))   374    375                     # Output normal days.   376    377                     else:   378                         if date in week_coverage:   379                             output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-busy", "colspan" : "3"}))   380                         else:   381                             output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-empty", "colspan" : "3"}))   382    383                         # Make a link to a new event action.   384    385                         new_event_link = "action=EventAggregatorNewEvent&start-day=%d&start-month=%d&start-year=%d" % (   386                             day, month, year)   387    388                         # Output the day number.   389    390                         output.append(fmt.div(on=1))   391                         output.append(fmt.span(on=1, css_class="event-day-number"))   392                         output.append(linkToPage(request, page, unicode(day), new_event_link))   393                         output.append(fmt.span(on=0))   394                         output.append(fmt.div(on=0))   395    396                         # End of day.   397    398                         output.append(fmt.table_cell(on=0))   399    400                 # End of day numbers.   401    402                 output.append(fmt.table_row(on=0))   403    404                 # Either generate empty days...   405    406                 if not week_events:   407                     output.append(fmt.table_row(on=1))   408    409                     for weekday in range(0, 7):   410                         day = first_day + weekday   411                         date = (year, month, day)   412    413                         # Output out-of-month days.   414    415                         if day < 1 or day > number_of_days:   416                             output.append(fmt.table_cell(on=1,   417                                 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"}))   418                             output.append(fmt.table_cell(on=0))   419    420                         # Output empty days.   421    422                         else:   423                             output.append(fmt.table_cell(on=1,   424                                 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"}))   425    426                     output.append(fmt.table_row(on=0))   427    428                 # Or visit each set of scheduled events...   429    430                 else:   431                     for coverage, events in week_events:   432    433                         # Output each set.   434    435                         output.append(fmt.table_row(on=1))   436    437                         # Then, output day details.   438    439                         for weekday in range(0, 7):   440                             day = first_day + weekday   441                             date = (year, month, day)   442    443                             # Skip out-of-month days.   444    445                             if day < 1 or day > number_of_days:   446                                 output.append(fmt.table_cell(on=1,   447                                     attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"}))   448                                 output.append(fmt.table_cell(on=0))   449                                 continue   450    451                             # Output the day.   452    453                             if date not in coverage:   454                                 output.append(fmt.table_cell(on=1,   455                                     attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"}))   456    457                             # Get event details for the current day.   458    459                             for event_page, event_details in events:   460                                 if not (event_details["start"] <= date <= event_details["end"]):   461                                     continue   462    463                                 # Get basic properties of the event.   464    465                                 starts_today = event_details["start"] == date   466                                 ends_today = event_details["end"] == date   467                                 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details)   468    469                                 # Generate a colour for the event.   470    471                                 bg = getColour(event_page.page_name)   472                                 fg = getBlackOrWhite(bg)   473                                 style = ("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg))   474    475                                 # Determine if the event name should be shown.   476    477                                 start_of_period = starts_today or weekday == 0 or day == 1   478    479                                 if name_usage == "daily" or start_of_period:   480                                     hide_text = 0   481                                 else:   482                                     hide_text = 1   483    484                                 # Output start of day gap and determine whether   485                                 # any event content should be explicitly output   486                                 # for this day.   487    488                                 if starts_today:   489    490                                     # Single day events...   491    492                                     if ends_today:   493                                         colspan = 3   494                                         event_day_type = "event-day-single"   495    496                                     # Events starting today...   497    498                                     else:   499                                         output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap"}))   500                                         output.append(fmt.table_cell(on=0))   501    502                                         # Calculate the span of this cell.   503                                         # Events whose names appear on every day...   504    505                                         if name_usage == "daily":   506                                             colspan = 2   507                                             event_day_type = "event-day-starting"   508    509                                         # Events whose names appear once per week...   510    511                                         else:   512                                             if event_details["end"] <= week_end:   513                                                 event_length = event_details["end"][2] - day + 1   514                                                 colspan = (event_length - 2) * 3 + 4   515                                             else:   516                                                 event_length = week_end[2] - day + 1   517                                                 colspan = (event_length - 1) * 3 + 2   518    519                                             event_day_type = "event-day-multiple"   520    521                                 # Events continuing from a previous week...   522    523                                 elif start_of_period:   524    525                                     # End of continuing event...   526    527                                     if ends_today:   528                                         colspan = 2   529                                         event_day_type = "event-day-ending"   530    531                                     # Events continuing for at least one more day...   532    533                                     else:   534    535                                         # Calculate the span of this cell.   536                                         # Events whose names appear on every day...   537    538                                         if name_usage == "daily":   539                                             colspan = 3   540                                             event_day_type = "event-day-full"   541    542                                         # Events whose names appear once per week...   543    544                                         else:   545                                             if event_details["end"] <= week_end:   546                                                 event_length = event_details["end"][2] - day + 1   547                                                 colspan = (event_length - 1) * 3 + 2   548                                             else:   549                                                 event_length = week_end[2] - day + 1   550                                                 colspan = event_length * 3   551    552                                             event_day_type = "event-day-multiple"   553    554                                 # Continuing events whose names appear on every day...   555    556                                 elif name_usage == "daily":   557                                     if ends_today:   558                                         colspan = 2   559                                         event_day_type = "event-day-ending"   560                                     else:   561                                         colspan = 3   562                                         event_day_type = "event-day-full"   563    564                                 # Continuing events whose names appear once per week...   565    566                                 else:   567                                     colspan = None   568    569                                 # Output the main content only if it is not   570                                 # continuing from a previous day.   571    572                                 if colspan is not None:   573    574                                     # Colour the cell for continuing events.   575    576                                     attrs={   577                                         "class" : "event-day-content event-day-busy %s" % event_day_type,   578                                         "colspan" : str(colspan)   579                                         }   580    581                                     if not (starts_today and ends_today):   582                                         attrs["style"] = style   583    584                                     output.append(fmt.table_cell(on=1, attrs=attrs))   585    586                                     # Output the event.   587    588                                     if starts_today and ends_today or not hide_text:   589    590                                         output.append(fmt.div(on=1, css_class="event-summary-box"))   591                                         output.append(fmt.div(on=1, css_class="event-summary", style=style))   592                                         output.append(linkToPage(request, event_page, event_summary))   593                                         output.append(fmt.div(on=0))   594    595                                         # Add a pop-up element for long summaries.   596    597                                         output.append(fmt.div(on=1, css_class="event-summary-popup", style=style))   598                                         output.append(linkToPage(request, event_page, event_summary))   599                                         output.append(fmt.div(on=0))   600    601                                         output.append(fmt.div(on=0))   602    603                                     # Output end of day content.   604    605                                     output.append(fmt.div(on=0))   606    607                                 # Output end of day gap.   608    609                                 if ends_today and not starts_today:   610                                     output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap"}))   611                                     output.append(fmt.table_cell(on=0))   612    613                             # End of day.   614    615                             output.append(fmt.table_cell(on=0))   616    617                         # End of set.   618    619                         output.append(fmt.table_row(on=0))   620    621                         # Add a spacer.   622    623                         output.append(fmt.table_row(on=1))   624    625                         for weekday in range(0, 7):   626                             day = first_day + weekday   627                             css_classes = "event-day-spacer"   628    629                             # Skip out-of-month days.   630    631                             if day < 1 or day > number_of_days:   632                                 css_classes += " event-day-excluded"   633    634                             output.append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"}))   635                             output.append(fmt.table_cell(on=0))   636    637                         output.append(fmt.table_row(on=0))   638    639                 # Process the next week...   640    641                 first_day += 7   642    643             # End of month.   644    645             output.append(fmt.table(on=0))   646    647         # Or output a summary view...   648    649         elif mode == "list":   650    651             # Output a list.   652    653             output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"}))   654             output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"}))   655    656             # Either write a month heading or produce links for navigable   657             # calendars.   658    659             output.append(view.writeMonthHeading(year, month))   660    661             output.append(fmt.div(on=0))   662    663             output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"}))   664    665             # Get the events in order.   666    667             ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get((year, month), []))   668    669             # Show the events in order.   670    671             for event_page, event_details in ordered_events:   672                 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details)   673    674                 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"}))   675    676                 # Link to the page using the summary.   677    678                 output.append(fmt.paragraph(on=1))   679                 output.append(linkToPage(request, event_page, event_summary))   680                 output.append(fmt.paragraph(on=0))   681    682                 # Start and end dates.   683    684                 output.append(fmt.paragraph(on=1))   685                 output.append(fmt.span(on=1))   686                 output.append(fmt.text("%04d-%02d-%02d" % event_details["start"]))   687                 output.append(fmt.span(on=0))   688                 output.append(fmt.text(" - "))   689                 output.append(fmt.span(on=1))   690                 output.append(fmt.text("%04d-%02d-%02d" % event_details["end"]))   691                 output.append(fmt.span(on=0))   692                 output.append(fmt.paragraph(on=0))   693    694                 # Location.   695    696                 if event_details.has_key("location"):   697                     output.append(fmt.paragraph(on=1))   698                     output.append(fmt.text(event_details["location"]))   699                     output.append(fmt.paragraph(on=1))   700    701                 # Topics.   702    703                 if event_details.has_key("topics") or event_details.has_key("categories"):   704                     output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"}))   705    706                     for topic in event_details.get("topics") or event_details.get("categories"):   707                         output.append(fmt.listitem(on=1))   708                         output.append(fmt.text(topic))   709                         output.append(fmt.listitem(on=0))   710    711                     output.append(fmt.bullet_list(on=0))   712    713                 output.append(fmt.listitem(on=0))   714    715             output.append(fmt.bullet_list(on=0))   716    717         # Or output a table of events...   718    719         elif mode == "table":   720    721             # Get the events in order.   722    723             ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get((year, month), []))   724    725             # Show the events in order.   726    727             for event_page, event_details in ordered_events:   728                 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details)   729    730                 # Prepare CSS classes with category-related styling.   731    732                 css_classes = ["event-table-details"]   733    734                 for topic in event_details.get("topics") or event_details.get("categories"):   735    736                     # Filter the category text to avoid illegal characters.   737    738                     css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic)))   739    740                 attrs = {"class" : " ".join(css_classes)}   741    742                 output.append(fmt.table_row(on=1))   743    744                 # Start and end dates.   745    746                 output.append(fmt.table_cell(on=1, attrs=attrs))   747                 output.append(fmt.span(on=1))   748                 output.append(fmt.text("%04d-%02d-%02d" % event_details["start"]))   749                 output.append(fmt.span(on=0))   750                 output.append(fmt.text(" - "))   751                 output.append(fmt.span(on=1))   752                 output.append(fmt.text("%04d-%02d-%02d" % event_details["end"]))   753                 output.append(fmt.span(on=0))   754                 output.append(fmt.table_cell(on=0))   755    756                 # Location.   757    758                 output.append(fmt.table_cell(on=1, attrs=attrs))   759    760                 if event_details.has_key("location"):   761                     output.append(fmt.text(event_details["location"]))   762    763                 output.append(fmt.table_cell(on=0))   764    765                 # Link to the page using the summary.   766    767                 output.append(fmt.table_cell(on=1, attrs=attrs))   768                 output.append(linkToPage(request, event_page, event_summary))   769                 output.append(fmt.table_cell(on=0))   770    771                 output.append(fmt.table_row(on=0))   772    773     # Output top-level information.   774    775     # End of list view output.   776    777     if mode == "list":   778         output.append(fmt.bullet_list(on=0))   779    780     # End of table view output.   781    782     elif mode == "table":   783         output.append(fmt.table(on=0))   784    785     return ''.join(output)   786    787 # vim: tabstop=4 expandtab shiftwidth=4