EventAggregator

macros/EventAggregator.py

44:5582d8b9d3ed
2009-04-05 Paul Boddie Added RSS notes.
     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     if mode == "list":   281         output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"}))   282    283     # Visit all months in the requested range, or across known events.   284    285     for year, month in EventAggregatorSupport.daterange(first, last):   286    287         # Either output a calendar view...   288    289         if mode == "calendar":   290    291             # Output a month.   292    293             output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"}))   294    295             output.append(fmt.table_row(on=1))   296             output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"}))   297    298             # Either write a month heading or produce links for navigable   299             # calendars.   300    301             output.append(view.writeMonthHeading(year, month))   302    303             output.append(fmt.table_cell(on=0))   304             output.append(fmt.table_row(on=0))   305    306             # Weekday headings.   307    308             output.append(fmt.table_row(on=1))   309    310             for weekday in range(0, 7):   311                 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"}))   312                 output.append(fmt.text(_(EventAggregatorSupport.getDayLabel(weekday))))   313                 output.append(fmt.table_cell(on=0))   314    315             output.append(fmt.table_row(on=0))   316    317             # Process the days of the month.   318    319             start_weekday, number_of_days = calendar.monthrange(year, month)   320    321             # The start weekday is the weekday of day number 1.   322             # Find the first day of the week, counting from below zero, if   323             # necessary, in order to land on the first day of the month as   324             # day number 1.   325    326             first_day = 1 - start_weekday   327    328             while first_day <= number_of_days:   329    330                 # Find events in this week and determine how to mark them on the   331                 # calendar.   332    333                 week_start = (year, month, max(first_day, 1))   334                 week_end = (year, month, min(first_day + 6, number_of_days))   335    336                 week_coverage, week_events = EventAggregatorSupport.getCoverage(   337                     week_start, week_end, shown_events.get((year, month), []))   338    339                 # Output a week, starting with the day numbers.   340    341                 output.append(fmt.table_row(on=1))   342    343                 for weekday in range(0, 7):   344                     day = first_day + weekday   345                     date = (year, month, day)   346    347                     # Output out-of-month days.   348    349                     if day < 1 or day > number_of_days:   350                         output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"}))   351                         output.append(fmt.table_cell(on=0))   352    353                     # Output normal days.   354    355                     else:   356                         if date in week_coverage:   357                             output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-busy", "colspan" : "3"}))   358                         else:   359                             output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-empty", "colspan" : "3"}))   360    361                         output.append(fmt.div(on=1))   362                         output.append(fmt.span(on=1, css_class="event-day-number"))   363                         output.append(fmt.text(unicode(day)))   364                         output.append(fmt.span(on=0))   365                         output.append(fmt.div(on=0))   366    367                         # End of day.   368    369                         output.append(fmt.table_cell(on=0))   370    371                 # End of day numbers.   372    373                 output.append(fmt.table_row(on=0))   374    375                 # Either generate empty days...   376    377                 if not week_events:   378                     output.append(fmt.table_row(on=1))   379    380                     for weekday in range(0, 7):   381                         day = first_day + weekday   382                         date = (year, month, day)   383    384                         # Output out-of-month days.   385    386                         if day < 1 or day > number_of_days:   387                             output.append(fmt.table_cell(on=1,   388                                 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"}))   389                             output.append(fmt.table_cell(on=0))   390    391                         # Output empty days.   392    393                         else:   394                             output.append(fmt.table_cell(on=1,   395                                 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"}))   396    397                     output.append(fmt.table_row(on=0))   398    399                 # Or visit each set of scheduled events...   400    401                 else:   402                     for coverage, events in week_events:   403    404                         # Output each set.   405    406                         output.append(fmt.table_row(on=1))   407    408                         # Then, output day details.   409    410                         for weekday in range(0, 7):   411                             day = first_day + weekday   412                             date = (year, month, day)   413    414                             # Skip out-of-month days.   415    416                             if day < 1 or day > number_of_days:   417                                 output.append(fmt.table_cell(on=1,   418                                     attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"}))   419                                 output.append(fmt.table_cell(on=0))   420                                 continue   421    422                             # Output the day.   423    424                             if date not in coverage:   425                                 output.append(fmt.table_cell(on=1,   426                                     attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"}))   427    428                             # Get event details for the current day.   429    430                             for event_page, event_details in events:   431                                 if not (event_details["start"] <= date <= event_details["end"]):   432                                     continue   433    434                                 # Get basic properties of the event.   435    436                                 starts_today = event_details["start"] == date   437                                 ends_today = event_details["end"] == date   438                                 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details)   439    440                                 # Generate a colour for the event.   441    442                                 bg = getColour(event_page.page_name)   443                                 fg = getBlackOrWhite(bg)   444                                 style = ("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg))   445    446                                 # Determine if the event name should be shown.   447    448                                 start_of_period = starts_today or weekday == 0 or day == 1   449    450                                 if name_usage == "daily" or start_of_period:   451                                     hide_text = 0   452                                 else:   453                                     hide_text = 1   454    455                                 # Output start of day gap and determine whether   456                                 # any event content should be explicitly output   457                                 # for this day.   458    459                                 if starts_today:   460    461                                     # Single day events...   462    463                                     if ends_today:   464                                         colspan = 3   465                                         event_day_type = "event-day-single"   466    467                                     # Events starting today...   468    469                                     else:   470                                         output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap"}))   471                                         output.append(fmt.table_cell(on=0))   472    473                                         # Calculate the span of this cell.   474                                         # Events whose names appear on every day...   475    476                                         if name_usage == "daily":   477                                             colspan = 2   478                                             event_day_type = "event-day-starting"   479    480                                         # Events whose names appear once per week...   481    482                                         else:   483                                             if event_details["end"] <= week_end:   484                                                 event_length = event_details["end"][2] - day + 1   485                                                 colspan = (event_length - 2) * 3 + 4   486                                             else:   487                                                 event_length = week_end[2] - day + 1   488                                                 colspan = (event_length - 1) * 3 + 2   489    490                                             event_day_type = "event-day-multiple"   491    492                                 # Events continuing from a previous week...   493    494                                 elif start_of_period:   495    496                                     # End of continuing event...   497    498                                     if ends_today:   499                                         colspan = 2   500                                         event_day_type = "event-day-ending"   501    502                                     # Events continuing for at least one more day...   503    504                                     else:   505    506                                         # Calculate the span of this cell.   507                                         # Events whose names appear on every day...   508    509                                         if name_usage == "daily":   510                                             colspan = 3   511                                             event_day_type = "event-day-full"   512    513                                         # Events whose names appear once per week...   514    515                                         else:   516                                             if event_details["end"] <= week_end:   517                                                 event_length = event_details["end"][2] - day + 1   518                                                 colspan = (event_length - 1) * 3 + 2   519                                             else:   520                                                 event_length = week_end[2] - day + 1   521                                                 colspan = event_length * 3   522    523                                             event_day_type = "event-day-multiple"   524    525                                 # Continuing events whose names appear on every day...   526    527                                 elif name_usage == "daily":   528                                     if ends_today:   529                                         colspan = 2   530                                         event_day_type = "event-day-ending"   531                                     else:   532                                         colspan = 3   533                                         event_day_type = "event-day-full"   534    535                                 # Continuing events whose names appear once per week...   536    537                                 else:   538                                     colspan = None   539    540                                 # Output the main content only if it is not   541                                 # continuing from a previous day.   542    543                                 if colspan is not None:   544    545                                     # Colour the cell for continuing events.   546    547                                     attrs={   548                                         "class" : "event-day-content event-day-busy %s" % event_day_type,   549                                         "colspan" : str(colspan)   550                                         }   551    552                                     if not (starts_today and ends_today):   553                                         attrs["style"] = style   554    555                                     output.append(fmt.table_cell(on=1, attrs=attrs))   556    557                                     # Output the event.   558    559                                     if starts_today and ends_today or not hide_text:   560    561                                         output.append(fmt.div(on=1, css_class="event-summary-box"))   562                                         output.append(fmt.div(on=1, css_class="event-summary", style=style))   563                                         output.append(linkToPage(request, event_page, event_summary))   564                                         output.append(fmt.div(on=0))   565    566                                         # Add a pop-up element for long summaries.   567    568                                         output.append(fmt.div(on=1, css_class="event-summary-popup", style=style))   569                                         output.append(linkToPage(request, event_page, event_summary))   570                                         output.append(fmt.div(on=0))   571    572                                         output.append(fmt.div(on=0))   573    574                                     # Output end of day content.   575    576                                     output.append(fmt.div(on=0))   577    578                                 # Output end of day gap.   579    580                                 if ends_today and not starts_today:   581                                     output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap"}))   582                                     output.append(fmt.table_cell(on=0))   583    584                             # End of day.   585    586                             output.append(fmt.table_cell(on=0))   587    588                         # End of set.   589    590                         output.append(fmt.table_row(on=0))   591    592                         # Add a spacer.   593    594                         output.append(fmt.table_row(on=1))   595    596                         for weekday in range(0, 7):   597                             day = first_day + weekday   598                             css_classes = "event-day-spacer"   599    600                             # Skip out-of-month days.   601    602                             if day < 1 or day > number_of_days:   603                                 css_classes += " event-day-excluded"   604    605                             output.append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"}))   606                             output.append(fmt.table_cell(on=0))   607    608                         output.append(fmt.table_row(on=0))   609    610                 # Process the next week...   611    612                 first_day += 7   613    614             # End of month.   615    616             output.append(fmt.table(on=0))   617    618         # Or output a summary view...   619    620         elif mode == "list":   621    622             # Output a list.   623    624             output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"}))   625             output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"}))   626    627             # Either write a month heading or produce links for navigable   628             # calendars.   629    630             output.append(view.writeMonthHeading(year, month))   631    632             output.append(fmt.div(on=0))   633    634             output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"}))   635    636             # Get the events in order.   637    638             ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get((year, month), []))   639    640             # Show the events in order.   641    642             for event_page, event_details in ordered_events:   643                 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details)   644    645                 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"}))   646    647                 # Link to the page using the summary.   648    649                 output.append(fmt.paragraph(on=1))   650                 output.append(linkToPage(request, event_page, event_summary))   651                 output.append(fmt.paragraph(on=0))   652    653                 # Start and end dates.   654    655                 output.append(fmt.paragraph(on=1))   656                 output.append(fmt.span(on=1))   657                 output.append(fmt.text("%04d-%02d-%02d" % event_details["start"]))   658                 output.append(fmt.span(on=0))   659                 output.append(fmt.text(" - "))   660                 output.append(fmt.span(on=1))   661                 output.append(fmt.text("%04d-%02d-%02d" % event_details["end"]))   662                 output.append(fmt.span(on=0))   663                 output.append(fmt.paragraph(on=0))   664    665                 # Location.   666    667                 if event_details.has_key("location"):   668                     output.append(fmt.paragraph(on=1))   669                     output.append(fmt.text(event_details["location"]))   670                     output.append(fmt.paragraph(on=1))   671    672                 # Topics.   673    674                 if event_details.has_key("topics") or event_details.has_key("categories"):   675                     output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"}))   676    677                     for topic in event_details.get("topics") or event_details.get("categories"):   678                         output.append(fmt.listitem(on=1))   679                         output.append(fmt.text(topic))   680                         output.append(fmt.listitem(on=0))   681    682                     output.append(fmt.bullet_list(on=0))   683    684                 output.append(fmt.listitem(on=0))   685    686             output.append(fmt.bullet_list(on=0))   687    688     # Output top-level information.   689    690     # End of list view output.   691    692     if mode == "list":   693         output.append(fmt.bullet_list(on=0))   694    695     return ''.join(output)   696    697 # vim: tabstop=4 expandtab shiftwidth=4