EventAggregator

EventAggregatorSupport.py

47:1cbad5c4b7f0
2009-06-06 Paul Boddie Moved event body manipulation to the EventAggregatorSupport module, employing an approach similar to that used when retrieving event details from page bodies. Changed the definition list regular expression to support recognition of values in commented-out regions, meaning that such regions must now be recognised and discarded when retrieving event details from pages. Changed the parameter values sent by the macro in links from day numbers in calendar views. Introduced better behaviour when interpreting request parameters in the EventAggregatorNewEvent action, providing the current year as the default start year. Removed the redirect for cancelled page edits since it gives the wrong impression about whether a page has been created (which has already happened by the time the user gets to edit the page). Added documentation for the EventAggregatorNewEvent action. Updated documentation for the EventAggregator macro. Updated the release information. Added package-related files.
     1 # -*- coding: iso-8859-1 -*-     2 """     3     MoinMoin - EventAggregator library     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.Page import Page    12 from MoinMoin import search, version    13 from MoinMoin import wikiutil    14 import calendar    15 import datetime    16 import time    17 import re    18     19 __version__ = "0.3"    20     21 # Date labels.    22     23 month_labels = ["January", "February", "March", "April", "May", "June",    24     "July", "August", "September", "October", "November", "December"]    25 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]    26     27 # Regular expressions where MoinMoin does not provide the required support.    28     29 category_regexp = None    30     31 # Page parsing.    32     33 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?)::\s)(?P<desc>.*?)$', re.UNICODE | re.MULTILINE)    34 category_membership_regexp = re.compile(ur"^\s*((Category\S+)(\s+Category\S+)*)\s*$", re.MULTILINE | re.UNICODE)    35     36 # Value parsing.    37     38 date_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})', re.UNICODE)    39 month_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})', re.UNICODE)    40 verbatim_regexp = re.compile(ur'(?:'    41     ur'<<Verbatim\((?P<verbatim>.*?)\)>>'    42     ur'|'    43     ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]'    44     ur'|'    45     ur'`(?P<monospace>.*?)`'    46     ur'|'    47     ur'{{{(?P<preformatted>.*?)}}}'    48     ur')', re.UNICODE)    49     50 # Utility functions.    51     52 def isMoin15():    53     return version.release.startswith("1.5.")    54     55 def getCategoryPattern(request):    56     global category_regexp    57     58     try:    59         return request.cfg.cache.page_category_regexact    60     except AttributeError:    61     62         # Use regular expression from MoinMoin 1.7.1 otherwise.    63     64         if category_regexp is None:    65             category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE)    66         return category_regexp    67     68 # Action support functions.    69     70 def getCategories(request):    71     72     """    73     From the AdvancedSearch macro, return a list of category page names using    74     the given 'request'.    75     """    76     77     # This will return all pages with "Category" in the title.    78     79     cat_filter = getCategoryPattern(request).search    80     return request.rootpage.getPageList(filter=cat_filter)    81     82 def getCategoryMapping(category_pagenames, request):    83     84     """    85     For the given 'category_pagenames' return a list of tuples of the form    86     (category name, category page name) using the given 'request'.    87     """    88     89     cat_pattern = getCategoryPattern(request)    90     mapping = []    91     for pagename in category_pagenames:    92         name = cat_pattern.match(pagename).group("key")    93         if name != "Category":    94             mapping.append((name, pagename))    95     mapping.sort()    96     return mapping    97     98 def getPageRevision(page):    99    100     # From Page.edit_info...   101    102     if hasattr(page, "editlog_entry"):   103         line = page.editlog_entry()   104     else:   105         line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x   106    107     timestamp = line.ed_time_usecs   108     mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x   109     return {"timestamp" : time.gmtime(mtime), "comment" : line.comment}   110    111 def getHTTPTimeString(tmtuple):   112     return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (   113         weekday_labels[tmtuple.tm_wday],   114         tmtuple.tm_mday,   115         month_labels[tmtuple.tm_mon -1], # zero-based labels   116         tmtuple.tm_year,   117         tmtuple.tm_hour,   118         tmtuple.tm_min,   119         tmtuple.tm_sec   120         )   121    122 # The main activity functions.   123    124 def getPages(pagename, request):   125    126     "Return the links minus category links for 'pagename' using the 'request'."   127    128     query = search.QueryParser().parse_query('category:%s' % pagename)   129     if isMoin15():   130         results = search.searchPages(request, query)   131         results.sortByPagename()   132     else:   133         results = search.searchPages(request, query, "page_name")   134    135     cat_pattern = getCategoryPattern(request)   136     pages = []   137     for page in results.hits:   138         if not cat_pattern.match(page.page_name):   139             pages.append(page)   140     return pages   141    142 def getSimpleWikiText(text):   143    144     """   145     Return the plain text representation of the given 'text' which may employ   146     certain Wiki syntax features, such as those providing verbatim or monospaced   147     text.   148     """   149    150     # NOTE: Re-implementing support for verbatim text and linking avoidance.   151    152     return "".join([s for s in verbatim_regexp.split(text) if s is not None])   153    154 def getEncodedWikiText(text):   155    156     "Encode the given 'text' in a verbatim representation."   157    158     return "<<Verbatim(%s)>>" % text   159    160 def getFormat(page):   161    162     "Get the format used on 'page'."   163    164     if isMoin15():   165         return "wiki" # page.pi_format   166     else:   167         return page.pi["format"]   168    169 def getEventDetails(page):   170    171     "Return a dictionary of event details from the given 'page'."   172    173     event_details = {}   174    175     if getFormat(page) == "wiki":   176         for match in definition_list_regexp.finditer(page.get_raw_body()):   177    178             # Skip commented-out items.   179    180             if match.group("optcomment"):   181                 continue   182    183             # Permit case-insensitive list terms.   184    185             term = match.group("term").lower()   186             desc = match.group("desc")   187    188             # Special value type handling.   189    190             # Dates.   191    192             if term in ("start", "end"):   193                 desc = getDate(desc)   194    195             # Lists (whose elements may be quoted).   196    197             elif term in ("topics", "categories"):   198                 desc = [getSimpleWikiText(value.strip()) for value in desc.split(",")]   199    200             # Labels which may well be quoted.   201    202             elif term in ("title", "summary", "description"):   203                 desc = getSimpleWikiText(desc)   204    205             if desc is not None:   206                 event_details[term] = desc   207    208     return event_details   209    210 def setEventDetails(body, event_details):   211    212     """   213     Set the event details in the given page 'body' using the 'event_details'   214     dictionary, returning the new body text.   215     """   216    217     new_body_parts = []   218     end_of_last_match = 0   219    220     for match in definition_list_regexp.finditer(body):   221    222         # Add preceding text to the new body.   223    224         new_body_parts.append(body[end_of_last_match:match.start()])   225         end_of_last_match = match.end()   226    227         # Get the matching regions, adding the term to the new body.   228    229         new_body_parts.append(match.group("wholeterm"))   230    231         # Permit case-insensitive list terms.   232    233         term = match.group("term").lower()   234         desc = match.group("desc")   235    236         # Special value type handling.   237    238         if event_details.has_key(term):   239    240             # Dates.   241    242             if term in ("start", "end"):   243                 desc = desc.replace("YYYY-MM-DD", event_details[term])   244    245             # Lists (whose elements may be quoted).   246    247             elif term in ("topics", "categories"):   248                 desc = ", ".join(getEncodedWikiText(event_details[term]))   249    250             # Labels which may well be quoted.   251    252             elif term in ("title", "summary", "description"):   253                 desc = getEncodedWikiText(event_details[term])   254    255         new_body_parts.append(desc)   256    257     else:   258         new_body_parts.append(body[end_of_last_match:])   259    260     return "".join(new_body_parts)   261    262 def setCategoryMembership(body, category_names):   263    264     """   265     Set the category membership in the given page 'body' using the specified   266     'category_names' and returning the new body text.   267     """   268    269     match = category_membership_regexp.search(body)   270     if match:   271         return "".join([body[:match.start()], " ".join(category_names), body[match.end():]])   272     else:   273         return body   274    275 def getEventSummary(event_page, event_details):   276    277     """   278     Return either the given title or summary of the event described by the given   279     'event_page', according to the given 'event_details', or return the pretty   280     version of the page name.   281     """   282    283     if event_details.has_key("title"):   284         return event_details["title"]   285     elif event_details.has_key("summary"):   286         return event_details["summary"]   287     else:   288         return getPrettyPageName(event_page)   289    290 def getDate(s):   291    292     "Parse the string 's', extracting and returning a date string."   293    294     m = date_regexp.search(s)   295     if m:   296         return tuple(map(int, m.groups()))   297     else:   298         return None   299    300 def getMonth(s):   301    302     "Parse the string 's', extracting and returning a month string."   303    304     m = month_regexp.search(s)   305     if m:   306         return tuple(map(int, m.groups()))   307     else:   308         return None   309    310 def getCurrentMonth():   311    312     "Return the current month as a (year, month) tuple."   313    314     today = datetime.date.today()   315     return (today.year, today.month)   316    317 def getCurrentYear():   318    319     "Return the current year."   320    321     today = datetime.date.today()   322     return today.year   323    324 def monthupdate(date, n):   325    326     "Return 'date' updated by 'n' months."   327    328     if n < 0:   329         fn = prevmonth   330     else:   331         fn = nextmonth   332    333     i = 0   334     while i < abs(n):   335         date = fn(date)   336         i += 1   337            338     return date   339    340 def daterange(first, last, step=1):   341    342     """   343     Get the range of dates starting at 'first' and ending on 'last', using the   344     specified 'step'.   345     """   346    347     results = []   348    349     months_only = len(first) == 2   350     start_year = first[0]   351     end_year = last[0]   352    353     for year in range(start_year, end_year + step, step):   354         if step == 1 and year < end_year:   355             end_month = 12   356         elif step == -1 and year > end_year:   357             end_month = 1   358         else:   359             end_month = last[1]   360    361         if step == 1 and year > start_year:   362             start_month = 1   363         elif step == -1 and year < start_year:   364             start_month = 12   365         else:   366             start_month = first[1]   367    368         for month in range(start_month, end_month + step, step):   369             if months_only:   370                 results.append((year, month))   371             else:   372                 if step == 1 and month < end_month:   373                     _wd, end_day = calendar.monthrange(year, month)   374                 elif step == -1 and month > end_month:   375                     end_day = 1   376                 else:   377                     end_day = last[2]   378    379                 if step == 1 and month > start_month:   380                     start_day = 1   381                 elif step == -1 and month < start_month:   382                     _wd, start_day = calendar.monthrange(year, month)   383                 else:   384                     start_day = first[2]   385    386                 for day in range(start_day, end_day + step, step):   387                     results.append((year, month, day))   388    389     return results   390    391 def nextdate(date):   392    393     "Return the date following the given 'date'."   394    395     year, month, day = date   396     _wd, end_day = calendar.monthrange(year, month)   397     if day == end_day:   398         if month == 12:   399             return (year + 1, 1, 1)   400         else:   401             return (year, month + 1, 1)   402     else:   403         return (year, month, day + 1)   404    405 def prevdate(date):   406    407     "Return the date preceding the given 'date'."   408    409     year, month, day = date   410     if day == 1:   411         if month == 1:   412             return (year - 1, 12, 31)   413         else:   414             _wd, end_day = calendar.monthrange(year, month - 1)   415             return (year, month - 1, end_day)   416     else:   417         return (year, month, day - 1)   418    419 def nextmonth(date):   420    421     "Return the (year, month) tuple following 'date'."   422    423     year, month = date   424     if month == 12:   425         return (year + 1, 1)   426     else:   427         return year, month + 1   428    429 def prevmonth(date):   430    431     "Return the (year, month) tuple preceding 'date'."   432    433     year, month = date   434     if month == 1:   435         return (year - 1, 12)   436     else:   437         return year, month - 1   438    439 def span(start, end):   440    441     "Return the difference between 'start' and 'end'."   442    443     return end[0] - start[0], end[1] - start[1]   444    445 def getEvents(request, category_names, calendar_start=None, calendar_end=None):   446    447     """   448     Using the 'request', generate a list of events found on pages belonging to   449     the specified 'category_names', using the optional 'calendar_start' and   450     'calendar_end' month tuples of the form (year, month) to indicate a window   451     of interest.   452    453     Return a list of events, a dictionary mapping months to event lists (within   454     the window of interest), a list of all events within the window of interest,   455     the earliest month of an event within the window of interest, and the latest   456     month of an event within the window of interest.   457     """   458    459     # Re-order the window, if appropriate.   460    461     if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end:   462         calendar_start, calendar_end = calendar_end, calendar_start   463    464     events = []   465     shown_events = {}   466     all_shown_events = []   467     processed_pages = set()   468    469     earliest = None   470     latest = None   471    472     for category_name in category_names:   473    474         # Get the pages and page names in the category.   475    476         pages_in_category = getPages(category_name, request)   477    478         # Visit each page in the category.   479    480         for page_in_category in pages_in_category:   481             pagename = page_in_category.page_name   482    483             # Only process each page once.   484    485             if pagename in processed_pages:   486                 continue   487             else:   488                 processed_pages.add(pagename)   489    490             # Get a real page, not a result page.   491    492             real_page_in_category = Page(request, pagename)   493             event_details = getEventDetails(real_page_in_category)   494    495             # Define the event as the page together with its details.   496    497             event = (real_page_in_category, event_details)   498             events.append(event)   499    500             # Test for the suitability of the event.   501    502             if event_details.has_key("start") and event_details.has_key("end"):   503    504                 start_month = event_details["start"][:2]   505                 end_month = event_details["end"][:2]   506    507                 # Compare the months of the dates to the requested calendar   508                 # window, if any.   509    510                 if (calendar_start is None or end_month >= calendar_start) and \   511                     (calendar_end is None or start_month <= calendar_end):   512    513                     all_shown_events.append(event)   514    515                     if earliest is None or start_month < earliest:   516                         earliest = start_month   517                     if latest is None or end_month > latest:   518                         latest = end_month   519    520                     # Store the event in the month-specific dictionary.   521    522                     first = max(start_month, calendar_start or start_month)   523                     last = min(end_month, calendar_end or end_month)   524    525                     for event_month in daterange(first, last):   526                         if not shown_events.has_key(event_month):   527                             shown_events[event_month] = []   528                         shown_events[event_month].append(event)   529    530     return events, shown_events, all_shown_events, earliest, latest   531    532 def setEventTimestamps(request, events):   533    534     """   535     Using 'request', set timestamp details in the details dictionary of each of   536     the 'events': a list of the form (event_page, event_details).   537    538     Retutn the latest timestamp found.   539     """   540    541     latest = None   542    543     for event_page, event_details in events:   544    545         # Get the initial revision of the page.   546    547         revisions = event_page.getRevList()   548         event_page_initial = Page(request, event_page.page_name, rev=revisions[-1])   549    550         # Get the created and last modified times.   551    552         initial_revision = getPageRevision(event_page_initial)   553         event_details["created"] = initial_revision["timestamp"]   554         latest_revision = getPageRevision(event_page)   555         event_details["last-modified"] = latest_revision["timestamp"]   556         event_details["sequence"] = len(revisions) - 1   557         event_details["last-comment"] = latest_revision["comment"]   558    559         if latest is None or latest < event_details["last-modified"]:   560             latest = event_details["last-modified"]   561    562     return latest   563    564 def compareEvents(event1, event2):   565    566     """   567     Compare 'event1' and 'event2' by start and end date, where both parameters   568     are of the following form:   569    570     (event_page, event_details)   571     """   572    573     event_page1, event_details1 = event1   574     event_page2, event_details2 = event2   575     return cmp(   576         (event_details1["start"], event_details1["end"]),   577         (event_details2["start"], event_details2["end"])   578         )   579    580 def getOrderedEvents(events):   581    582     """   583     Return a list with the given 'events' ordered according to their start and   584     end dates. Each list element must be of the following form:   585    586     (event_page, event_details)   587     """   588    589     ordered_events = events[:]   590     ordered_events.sort(compareEvents)   591     return ordered_events   592    593 def getConcretePeriod(calendar_start, calendar_end, earliest, latest):   594    595     """   596     From the requested 'calendar_start' and 'calendar_end', which may be None,   597     indicating that no restriction is imposed on the period for each of the   598     boundaries, use the 'earliest' and 'latest' event months to define a   599     specific period of interest.   600     """   601    602     # Define the period as starting with any specified start month or the   603     # earliest event known, ending with any specified end month or the latest   604     # event known.   605    606     first = calendar_start or earliest   607     last = calendar_end or latest   608    609     # If there is no range of months to show, perhaps because there are no   610     # events in the requested period, and there was no start or end month   611     # specified, show only the month indicated by the start or end of the   612     # requested period. If all events were to be shown but none were found show   613     # the current month.   614    615     if first is None:   616         first = last or getCurrentMonth()   617     if last is None:   618         last = first or getCurrentMonth()   619    620     # Permit "expiring" periods (where the start date approaches the end date).   621    622     return min(first, last), last   623    624 def getCoverage(start, end, events):   625    626     """   627     Within the period defined by the 'start' and 'end' dates, determine the   628     coverage of the days in the period by the given 'events', returning a set of   629     covered days, along with a list of slots, where each slot contains a tuple   630     of the form (set of covered days, events).   631     """   632    633     all_events = []   634     full_coverage = set()   635    636     # Get event details.   637    638     for event in events:   639         event_page, event_details = event   640    641         # Test for the event in the period.   642    643         if event_details["start"] <= end and event_details["end"] >= start:   644    645             # Find the coverage of this period for the event.   646    647             event_start = max(event_details["start"], start)   648             event_end = min(event_details["end"], end)   649             event_coverage = set(daterange(event_start, event_end))   650    651             # Update the overall coverage.   652    653             full_coverage.update(event_coverage)   654    655             # Try and fit the event into the events list.   656    657             for i, (coverage, covered_events) in enumerate(all_events):   658    659                 # Where the event does not overlap with the current   660                 # element, add it alongside existing events.   661    662                 if not coverage.intersection(event_coverage):   663                     covered_events.append(event)   664                     all_events[i] = coverage.union(event_coverage), covered_events   665                     break   666    667             # Make a new element in the list if the event cannot be   668             # marked alongside existing events.   669    670             else:   671                 all_events.append((event_coverage, [event]))   672    673     return full_coverage, all_events   674    675 # User interface functions.   676    677 def getParameter(request, name):   678     return request.form.get(name, [None])[0]   679    680 def getParameterMonth(arg):   681     n = None   682    683     if arg.startswith("current"):   684         date = getCurrentMonth()   685         if len(arg) > 8:   686             n = int(arg[7:])   687    688     elif arg.startswith("yearstart"):   689         date = (getCurrentYear(), 1)   690         if len(arg) > 10:   691             n = int(arg[9:])   692    693     elif arg.startswith("yearend"):   694         date = (getCurrentYear(), 12)   695         if len(arg) > 8:   696             n = int(arg[7:])   697    698     else:   699         date = getMonth(arg)   700    701     if n is not None:   702         date = monthupdate(date, n)   703    704     return date   705    706 def getFormMonth(request, calendar_name, argname):   707     if calendar_name is None:   708         calendar_prefix = argname   709     else:   710         calendar_prefix = "%s-%s" % (calendar_name, argname)   711    712     arg = getParameter(request, calendar_prefix)   713     if arg is not None:   714         return getParameterMonth(arg)   715     else:   716         return None   717    718 def getFormMonthPair(request, yeararg, montharg):   719     year = getParameter(request, yeararg)   720     month = getParameter(request, montharg)   721     if year and month:   722         return (int(year), int(month))   723     else:   724         return None   725    726 def getPrettyPageName(page):   727    728     "Return a nicely formatted title/name for the given 'page'."   729    730     if isMoin15():   731         title = page.split_title(page.request, force=1)   732     else:   733         title = page.split_title(force=1)   734    735     return title.replace("_", " ").replace("/", u" ? ")   736    737 def getMonthLabel(month):   738    739     "Return an unlocalised label for the given 'month'."   740    741     return month_labels[month - 1] # zero-based labels   742    743 def getDayLabel(weekday):   744    745     "Return an unlocalised label for the given 'weekday'."   746    747     return weekday_labels[weekday]   748    749 def linkToPage(request, page, text, query_string=None):   750    751     """   752     Using 'request', return a link to 'page' with the given link 'text' and   753     optional 'query_string'.   754     """   755    756     text = wikiutil.escape(text)   757    758     if isMoin15():   759         url = wikiutil.quoteWikinameURL(page.page_name)   760         if query_string is not None:   761             url = "%s?%s" % (url, query_string)   762         return wikiutil.link_tag(request, url, text, getattr(page, "formatter", None))   763     else:   764         return page.link_to_raw(request, text, query_string)   765    766 def getPageURL(request, page):   767    768     "Using 'request', return the URL of 'page'."   769    770     if isMoin15():   771         return request.getQualifiedURL(page.url(request))   772     else:   773         return request.getQualifiedURL(page.url(request, relative=0))   774    775 # vim: tabstop=4 expandtab shiftwidth=4