EventAggregator

EventAggregatorSupport.py

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