EventAggregator

macros/EventAggregator.py

48:8d69ccf101db
2009-06-07 Paul Boddie Fixed form HTML and added title and category value retention across submissions. Simplified the date processing, producing errors for obvious fault conditions.
     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                         # Make a link to a new event action.   362    363                         new_event_link = "action=EventAggregatorNewEvent&start-day=%d&start-month=%d&start-year=%d" % (   364                             day, month, year)   365    366                         # Output the day number.   367    368                         output.append(fmt.div(on=1))   369                         output.append(fmt.span(on=1, css_class="event-day-number"))   370                         output.append(linkToPage(request, page, unicode(day), new_event_link))   371                         output.append(fmt.span(on=0))   372                         output.append(fmt.div(on=0))   373    374                         # End of day.   375    376                         output.append(fmt.table_cell(on=0))   377    378                 # End of day numbers.   379    380                 output.append(fmt.table_row(on=0))   381    382                 # Either generate empty days...   383    384                 if not week_events:   385                     output.append(fmt.table_row(on=1))   386    387                     for weekday in range(0, 7):   388                         day = first_day + weekday   389                         date = (year, month, day)   390    391                         # Output out-of-month days.   392    393                         if day < 1 or day > number_of_days:   394                             output.append(fmt.table_cell(on=1,   395                                 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"}))   396                             output.append(fmt.table_cell(on=0))   397    398                         # Output empty days.   399    400                         else:   401                             output.append(fmt.table_cell(on=1,   402                                 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"}))   403    404                     output.append(fmt.table_row(on=0))   405    406                 # Or visit each set of scheduled events...   407    408                 else:   409                     for coverage, events in week_events:   410    411                         # Output each set.   412    413                         output.append(fmt.table_row(on=1))   414    415                         # Then, output day details.   416    417                         for weekday in range(0, 7):   418                             day = first_day + weekday   419                             date = (year, month, day)   420    421                             # Skip out-of-month days.   422    423                             if day < 1 or day > number_of_days:   424                                 output.append(fmt.table_cell(on=1,   425                                     attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"}))   426                                 output.append(fmt.table_cell(on=0))   427                                 continue   428    429                             # Output the day.   430    431                             if date not in coverage:   432                                 output.append(fmt.table_cell(on=1,   433                                     attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"}))   434    435                             # Get event details for the current day.   436    437                             for event_page, event_details in events:   438                                 if not (event_details["start"] <= date <= event_details["end"]):   439                                     continue   440    441                                 # Get basic properties of the event.   442    443                                 starts_today = event_details["start"] == date   444                                 ends_today = event_details["end"] == date   445                                 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details)   446    447                                 # Generate a colour for the event.   448    449                                 bg = getColour(event_page.page_name)   450                                 fg = getBlackOrWhite(bg)   451                                 style = ("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg))   452    453                                 # Determine if the event name should be shown.   454    455                                 start_of_period = starts_today or weekday == 0 or day == 1   456    457                                 if name_usage == "daily" or start_of_period:   458                                     hide_text = 0   459                                 else:   460                                     hide_text = 1   461    462                                 # Output start of day gap and determine whether   463                                 # any event content should be explicitly output   464                                 # for this day.   465    466                                 if starts_today:   467    468                                     # Single day events...   469    470                                     if ends_today:   471                                         colspan = 3   472                                         event_day_type = "event-day-single"   473    474                                     # Events starting today...   475    476                                     else:   477                                         output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap"}))   478                                         output.append(fmt.table_cell(on=0))   479    480                                         # Calculate the span of this cell.   481                                         # Events whose names appear on every day...   482    483                                         if name_usage == "daily":   484                                             colspan = 2   485                                             event_day_type = "event-day-starting"   486    487                                         # Events whose names appear once per week...   488    489                                         else:   490                                             if event_details["end"] <= week_end:   491                                                 event_length = event_details["end"][2] - day + 1   492                                                 colspan = (event_length - 2) * 3 + 4   493                                             else:   494                                                 event_length = week_end[2] - day + 1   495                                                 colspan = (event_length - 1) * 3 + 2   496    497                                             event_day_type = "event-day-multiple"   498    499                                 # Events continuing from a previous week...   500    501                                 elif start_of_period:   502    503                                     # End of continuing event...   504    505                                     if ends_today:   506                                         colspan = 2   507                                         event_day_type = "event-day-ending"   508    509                                     # Events continuing for at least one more day...   510    511                                     else:   512    513                                         # Calculate the span of this cell.   514                                         # Events whose names appear on every day...   515    516                                         if name_usage == "daily":   517                                             colspan = 3   518                                             event_day_type = "event-day-full"   519    520                                         # Events whose names appear once per week...   521    522                                         else:   523                                             if event_details["end"] <= week_end:   524                                                 event_length = event_details["end"][2] - day + 1   525                                                 colspan = (event_length - 1) * 3 + 2   526                                             else:   527                                                 event_length = week_end[2] - day + 1   528                                                 colspan = event_length * 3   529    530                                             event_day_type = "event-day-multiple"   531    532                                 # Continuing events whose names appear on every day...   533    534                                 elif name_usage == "daily":   535                                     if ends_today:   536                                         colspan = 2   537                                         event_day_type = "event-day-ending"   538                                     else:   539                                         colspan = 3   540                                         event_day_type = "event-day-full"   541    542                                 # Continuing events whose names appear once per week...   543    544                                 else:   545                                     colspan = None   546    547                                 # Output the main content only if it is not   548                                 # continuing from a previous day.   549    550                                 if colspan is not None:   551    552                                     # Colour the cell for continuing events.   553    554                                     attrs={   555                                         "class" : "event-day-content event-day-busy %s" % event_day_type,   556                                         "colspan" : str(colspan)   557                                         }   558    559                                     if not (starts_today and ends_today):   560                                         attrs["style"] = style   561    562                                     output.append(fmt.table_cell(on=1, attrs=attrs))   563    564                                     # Output the event.   565    566                                     if starts_today and ends_today or not hide_text:   567    568                                         output.append(fmt.div(on=1, css_class="event-summary-box"))   569                                         output.append(fmt.div(on=1, css_class="event-summary", style=style))   570                                         output.append(linkToPage(request, event_page, event_summary))   571                                         output.append(fmt.div(on=0))   572    573                                         # Add a pop-up element for long summaries.   574    575                                         output.append(fmt.div(on=1, css_class="event-summary-popup", style=style))   576                                         output.append(linkToPage(request, event_page, event_summary))   577                                         output.append(fmt.div(on=0))   578    579                                         output.append(fmt.div(on=0))   580    581                                     # Output end of day content.   582    583                                     output.append(fmt.div(on=0))   584    585                                 # Output end of day gap.   586    587                                 if ends_today and not starts_today:   588                                     output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap"}))   589                                     output.append(fmt.table_cell(on=0))   590    591                             # End of day.   592    593                             output.append(fmt.table_cell(on=0))   594    595                         # End of set.   596    597                         output.append(fmt.table_row(on=0))   598    599                         # Add a spacer.   600    601                         output.append(fmt.table_row(on=1))   602    603                         for weekday in range(0, 7):   604                             day = first_day + weekday   605                             css_classes = "event-day-spacer"   606    607                             # Skip out-of-month days.   608    609                             if day < 1 or day > number_of_days:   610                                 css_classes += " event-day-excluded"   611    612                             output.append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"}))   613                             output.append(fmt.table_cell(on=0))   614    615                         output.append(fmt.table_row(on=0))   616    617                 # Process the next week...   618    619                 first_day += 7   620    621             # End of month.   622    623             output.append(fmt.table(on=0))   624    625         # Or output a summary view...   626    627         elif mode == "list":   628    629             # Output a list.   630    631             output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"}))   632             output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"}))   633    634             # Either write a month heading or produce links for navigable   635             # calendars.   636    637             output.append(view.writeMonthHeading(year, month))   638    639             output.append(fmt.div(on=0))   640    641             output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"}))   642    643             # Get the events in order.   644    645             ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get((year, month), []))   646    647             # Show the events in order.   648    649             for event_page, event_details in ordered_events:   650                 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details)   651    652                 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"}))   653    654                 # Link to the page using the summary.   655    656                 output.append(fmt.paragraph(on=1))   657                 output.append(linkToPage(request, event_page, event_summary))   658                 output.append(fmt.paragraph(on=0))   659    660                 # Start and end dates.   661    662                 output.append(fmt.paragraph(on=1))   663                 output.append(fmt.span(on=1))   664                 output.append(fmt.text("%04d-%02d-%02d" % event_details["start"]))   665                 output.append(fmt.span(on=0))   666                 output.append(fmt.text(" - "))   667                 output.append(fmt.span(on=1))   668                 output.append(fmt.text("%04d-%02d-%02d" % event_details["end"]))   669                 output.append(fmt.span(on=0))   670                 output.append(fmt.paragraph(on=0))   671    672                 # Location.   673    674                 if event_details.has_key("location"):   675                     output.append(fmt.paragraph(on=1))   676                     output.append(fmt.text(event_details["location"]))   677                     output.append(fmt.paragraph(on=1))   678    679                 # Topics.   680    681                 if event_details.has_key("topics") or event_details.has_key("categories"):   682                     output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"}))   683    684                     for topic in event_details.get("topics") or event_details.get("categories"):   685                         output.append(fmt.listitem(on=1))   686                         output.append(fmt.text(topic))   687                         output.append(fmt.listitem(on=0))   688    689                     output.append(fmt.bullet_list(on=0))   690    691                 output.append(fmt.listitem(on=0))   692    693             output.append(fmt.bullet_list(on=0))   694    695     # Output top-level information.   696    697     # End of list view output.   698    699     if mode == "list":   700         output.append(fmt.bullet_list(on=0))   701    702     return ''.join(output)   703    704 # vim: tabstop=4 expandtab shiftwidth=4