# HG changeset patch # User Paul Boddie # Date 1237769533 -3600 # Node ID 8d656956624276d184597f99fec04120ab74434e # Parent 242a07db6caf46c83560595f4ed24f3342d4860b Made iCalendar output part of a new action, introducing a common library for the action and the macro. Fixed iCalendar output, introducing a nextdate common function to properly support DTEND. Added library and action installation scripts. Updated the documentation. Made event topics a list in each event's details dictionary. diff -r 242a07db6caf -r 8d6569566242 EventAggregatorSupport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/EventAggregatorSupport.py Mon Mar 23 01:52:13 2009 +0100 @@ -0,0 +1,237 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - EventAggregator library + + @copyright: 2008, 2009 by Paul Boddie + @copyright: 2000-2004 Juergen Hermann , + 2005-2008 MoinMoin:ThomasWaldmann. + @license: GNU GPL (v2 or later), see COPYING.txt for details. +""" + +from MoinMoin.Page import Page +from MoinMoin import search, version +import calendar +import re + +__version__ = "0.1" + +# Regular expressions where MoinMoin does not provide the required support. + +category_regexp = None +definition_list_regexp = re.compile(ur'^\s+(?P.*?)::\s(?P.*?)$', re.UNICODE | re.MULTILINE) +date_regexp = re.compile(ur'(?P[0-9]{4})-(?P[0-9]{2})-(?P[0-9]{2})', re.UNICODE) +month_regexp = re.compile(ur'(?P[0-9]{4})-(?P[0-9]{2})', re.UNICODE) + +# Utility functions. + +def isMoin15(): + return version.release.startswith("1.5.") + +def getCategoryPattern(request): + global category_regexp + + try: + return request.cfg.cache.page_category_regexact + except AttributeError: + + # Use regular expression from MoinMoin 1.7.1 otherwise. + + if category_regexp is None: + category_regexp = re.compile(u'^%s$' % ur'(?PCategory(?P(?!Template)\S+))', re.UNICODE) + return category_regexp + +# The main activity functions. + +def getPages(pagename, request): + + "Return the links minus category links for 'pagename' using the 'request'." + + query = search.QueryParser().parse_query('category:%s' % pagename) + if isMoin15(): + results = search.searchPages(request, query) + results.sortByPagename() + else: + results = search.searchPages(request, query, "page_name") + + cat_pattern = getCategoryPattern(request) + pages = [] + for page in results.hits: + if not cat_pattern.match(page.page_name): + pages.append(page) + return pages + +def getPrettyPageName(page): + + "Return a nicely formatted title/name for the given 'page'." + + return page.split_title(force=1).replace("_", " ").replace("/", u" » ") + +def getEventDetails(page): + + "Return a dictionary of event details from the given 'page'." + + event_details = {} + + if page.pi["format"] == "wiki": + for match in definition_list_regexp.finditer(page.body): + + # Permit case-insensitive list terms. + + term = match.group("term").lower() + desc = match.group("desc") + + # Special value type handling. + + if term in ("start", "end"): + desc = getDate(desc) + elif term in ("topics",): + desc = [value.strip() for value in desc.split(",")] + + if desc is not None: + event_details[term] = desc + + return event_details + +def getDate(s): + + "Parse the string 's', extracting and returning a date string." + + m = date_regexp.search(s) + if m: + return tuple(map(int, m.groups())) + else: + return None + +def getMonth(s): + + "Parse the string 's', extracting and returning a month string." + + m = month_regexp.search(s) + if m: + return tuple(map(int, m.groups())) + else: + return None + +def daterange(first, last): + results = [] + + months_only = len(first) == 2 + start_year = first[0] + end_year = last[0] + + for year in range(start_year, end_year + 1): + if year < end_year: + end_month = 12 + else: + end_month = last[1] + + if year > start_year: + start_month = 1 + else: + start_month = first[1] + + for month in range(start_month, end_month + 1): + if months_only: + results.append((year, month)) + else: + if month < end_month: + _wd, end_day = calendar.monthrange(year, month) + else: + end_day = last[2] + + if month > start_month: + start_day = 1 + else: + start_day = first[2] + + for day in range(start_day, end_day + 1): + results.append((year, month, day)) + + return results + +def nextdate(date): + year, month, day = date + _wd, end_day = calendar.monthrange(year, month) + if day == end_day: + if month == 12: + return (year + 1, 1, 1) + else: + return (year, month + 1, 1) + else: + return (year, month, day + 1) + +def getEvents(request, category_names, calendar_start=None, calendar_end=None): + + """ + Using the 'request', generate a list of events found on pages belonging to + the specified 'category_names', using the optional 'calendar_start' and + 'calendar_end' month tuples of the form (year, month) to indicate a window + of interest. + + Return a list of events, a dictionary mapping months to event lists (within + the window of interest), a list of all events within the window of interest, + the earliest month of an event within the window of interest, and the latest + month of an event within the window of interest. + """ + + events = [] + shown_events = {} + all_shown_events = [] + + earliest = None + latest = None + + for category_name in category_names: + + # Get the pages and page names in the category. + + pages_in_category = getPages(category_name, request) + + # Visit each page in the category. + + for page_in_category in pages_in_category: + pagename = page_in_category.page_name + + # Get a real page, not a result page. + + real_page_in_category = Page(request, pagename) + event_details = getEventDetails(real_page_in_category) + + # Define the event as the page together with its details. + + event = (real_page_in_category, event_details) + events.append(event) + + # Test for the suitability of the event. + + if event_details.has_key("start") and event_details.has_key("end"): + + start_month = event_details["start"][:2] + end_month = event_details["end"][:2] + + # Compare the months of the dates to the requested calendar + # window, if any. + + if (calendar_start is None or end_month >= calendar_start) and \ + (calendar_end is None or start_month <= calendar_end): + + all_shown_events.append(event) + + if earliest is None or start_month < earliest: + earliest = start_month + if latest is None or end_month > latest: + latest = end_month + + # Store the event in the month-specific dictionary. + + first = max(start_month, calendar_start or start_month) + last = min(end_month, calendar_end or end_month) + + for event_month in daterange(first, last): + if not shown_events.has_key(event_month): + shown_events[event_month] = [] + shown_events[event_month].append(event) + + return events, shown_events, all_shown_events, earliest, latest + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 242a07db6caf -r 8d6569566242 README.txt --- a/README.txt Sun Mar 22 22:46:53 2009 +0100 +++ b/README.txt Mon Mar 23 01:52:13 2009 +0100 @@ -8,9 +8,23 @@ the details of each event, colouring each event period in a specially generated colour. +The EventAggregatorSummary action can be used to provide an iCalendar summary +of event data based on pages belonging to specific categories, as described +above. The category, start and end parameters are read directly from the +request as URL or form parameters. + Installation ------------ +To install the support library, consider using the setup.py script provided: + + python setup.py install + +You may wish to indicate a specific prefix if MoinMoin is not installed in the +traditional location: + + python setup.py install --prefix=path-to-moin-prefix + To install the macro in a Wiki, consider using the instmacros script provided: ./instmacros path-to-wiki @@ -18,6 +32,37 @@ On non-UNIX platforms, it is necessary to manually copy the contents of the macros directory in this distribution into the macros directory of your Wiki. +It is highly recommended that the tables and listings be styled according to +the stylesheet provided, and you can use this file as a starting point for +your own modifications. To install the stylesheet, consider using the +insttheme script provided: + + ./insttheme path-to-wiki theme-name + +Again, on non-UNIX platforms, it is necessary to manually copy the files. In +this case, just copy the contents of the css directory into the css directory +of themes which will support styling of event calendars and listings. + +To activate the styles provided by the stylesheet in the css directory, you +will need to edit the screen.css file in each affected theme's css directory, +adding the following before any style rules: + + /* Event aggregation... */ + + @import "event-aggregator.css"; + +This ensures that the styles are made available to the browser. + +To install the action in a Wiki, consider using the instactions script provided: + + ./instactions path-to-wiki + +On non-UNIX platforms, it is necessary to manually copy the contents of the +actions directory in this distribution into the actions directory of your Wiki. + +Using the Macro +--------------- + It should now be possible to edit pages and use the macro as follows. For MoinMoin 1.5: @@ -38,26 +83,21 @@ <> -It is highly recommended that the tables and listings be styled according to -the stylesheet provided, and you can use this file as a starting point for -your own modifications. To install the stylesheet, consider using the -insttheme script provided: +Using the Action +---------------- - ./insttheme path-to-wiki theme-name +To obtain an iCalendar summary, a collection of parameters can be specified in +the URL of any Wiki page. For example: + + http://example.com/moin/FrontPage?action=EventAggregatorSummary&category=CategoryEvents -Again, on non-UNIX platforms, it is necessary to manually copy the files. In -this case, just copy the contents of the css directory into the css directory -of themes which will support styling of event calendars and listings. +This should produce an iCalendar resource in response. By specifying 'start' +and 'end' parameters, a restricted view can be obtained. For example: -To activate the styles provided by the stylesheet in the css directory, you -will need to edit the screen.css file in each affected theme's css directory, -adding the following before any style rules: + http://example.com/moin/FrontPage?action=EventAggregatorSummary&category=CategoryEvents&start=2009-06&end=2009-07 - /* Event aggregation... */ - - @import "event-aggregator.css"; - -This ensures that the styles are made available to the browser. +This would restrict the initial query to events occurring in the months of +June 2009 ('2009-06') and July 2009 ('2009-07'). Recommended Software -------------------- @@ -84,9 +124,10 @@ Contact, Copyright and Licence Information ------------------------------------------ -See the following Web page for more information about this work: +See the following Web pages for more information about this work: http://moinmo.in/MacroMarket/EventAggregator +http://moinmo.in/ActionMarket/EventAggregator The author can be contacted at the following e-mail address: @@ -98,9 +139,9 @@ Release Procedures ------------------ -Update the EventAggregator.py __version__ attribute. +Update the EventAggregatorSupport.py __version__ attribute. Change the version number and package filename/directory in the documentation. Update the release notes (see above). Tag, export. Archive, upload. -Update the MacroMarket (see above for the URL). +Update the MacroMarket and ActionMarket (see above for the URLs). diff -r 242a07db6caf -r 8d6569566242 actions/EventAggregatorSummary.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/actions/EventAggregatorSummary.py Mon Mar 23 01:52:13 2009 +0100 @@ -0,0 +1,70 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - EventAggregatorSummary Action + + @copyright: 2008, 2009 by Paul Boddie + @copyright: 2000-2004 Juergen Hermann , + 2005-2008 MoinMoin:ThomasWaldmann. + @license: GNU GPL (v2 or later), see COPYING.txt for details. +""" + +from MoinMoin import config +import EventAggregatorSupport + +Dependencies = ['pages'] + +# Action function. + +def execute(pagename, request): + + """ + For the given 'pagename' and 'request', write an iCalendar summary of the + event data found in the categories specified via the "category" request + parameter, using the "start" and "end" parameters (if specified). Multiple + "category" parameters can be specified. + """ + + category_names = request.form.get("category", []) + + if request.form.has_key("start"): + calendar_start = EventAggregatorSupport.getMonth(request.form["start"][0]) + else: + calendar_start = None + + if request.form.has_key("end"): + calendar_end = EventAggregatorSupport.getMonth(request.form["end"][0]) + else: + calendar_end = None + + events, shown_events, all_shown_events, earliest, latest = \ + EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end) + + # Output iCalendar data... + + request.emit_http_headers(["Content-Type: text/calendar; charset=%s" % config.charset]) + + request.write("BEGIN:VCALENDAR\r\n") + request.write("PRODID:-//MoinMoin//EventAggregatorSummary\r\n") + request.write("VERSION:2.0\r\n") + + for event_page, event_details in all_shown_events: + + # Get a pretty version of the page name. + + pretty_pagename = EventAggregatorSupport.getPrettyPageName(event_page) + + # Output the event details. + + request.write("BEGIN:VEVENT\r\n") + request.write("SUMMARY:%s\r\n" % pretty_pagename) + request.write("UID:%s\r\n" % request.getQualifiedURL(event_page.url(request))) + request.write("URL:%s\r\n" % request.getQualifiedURL(event_page.url(request))) + request.write("DTSTART;VALUE=DATE:%04d%02d%02d\r\n" % event_details["start"]) + request.write("DTEND;VALUE=DATE:%04d%02d%02d\r\n" % EventAggregatorSupport.nextdate(event_details["end"])) + if event_details.has_key("topics"): + request.write("CATEGORIES:%s\r\n" % ",".join(event_details["topics"])) + request.write("END:VEVENT\r\n") + + request.write("END:VCALENDAR\r\n") + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 242a07db6caf -r 8d6569566242 instactions --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/instactions Mon Mar 23 01:52:13 2009 +0100 @@ -0,0 +1,13 @@ +#!/bin/bash + +if [[ ! $1 ]] ; then + echo "Please specify a directory such as ../mysite/wiki or /tmp/mysite/wiki." + echo "This should be the root of your Wiki installation and contain the data" + echo "directory." + echo + echo "You may wish to uncomment and modify the chown command in this script." + exit +fi + +cp actions/*.py $1/data/plugin/action/ +#chown www-data: $1/data/plugin/action/*.py diff -r 242a07db6caf -r 8d6569566242 macros/EventAggregator.py --- a/macros/EventAggregator.py Sun Mar 22 22:46:53 2009 +0100 +++ b/macros/EventAggregator.py Mon Mar 23 01:52:13 2009 +0100 @@ -8,150 +8,23 @@ @license: GNU GPL (v2 or later), see COPYING.txt for details. """ -from MoinMoin.Page import Page -from MoinMoin import wikiutil, search, version +from MoinMoin import wikiutil +import EventAggregatorSupport import calendar -import re try: set except NameError: from sets import Set as set -__version__ = "0.1" - Dependencies = ['pages'] -# Regular expressions where MoinMoin does not provide the required support. - -category_regexp = None -definition_list_regexp = re.compile(ur'^\s+(?P.*?)::\s(?P.*?)$', re.UNICODE | re.MULTILINE) -date_regexp = re.compile(ur'(?P[0-9]{4})-(?P[0-9]{2})-(?P[0-9]{2})', re.UNICODE) -month_regexp = re.compile(ur'(?P[0-9]{4})-(?P[0-9]{2})', re.UNICODE) - # Date labels. month_labels = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] -# Utility functions. - -def isMoin15(): - return version.release.startswith("1.5.") - -def getCategoryPattern(request): - global category_regexp - - try: - return request.cfg.cache.page_category_regexact - except AttributeError: - - # Use regular expression from MoinMoin 1.7.1 otherwise. - - if category_regexp is None: - category_regexp = re.compile(u'^%s$' % ur'(?PCategory(?P(?!Template)\S+))', re.UNICODE) - return category_regexp - -# The main activity functions. - -def getPages(pagename, request): - - "Return the links minus category links for 'pagename' using the 'request'." - - query = search.QueryParser().parse_query('category:%s' % pagename) - if isMoin15(): - results = search.searchPages(request, query) - results.sortByPagename() - else: - results = search.searchPages(request, query, "page_name") - - cat_pattern = getCategoryPattern(request) - pages = [] - for page in results.hits: - if not cat_pattern.match(page.page_name): - pages.append(page) - return pages - -def getPrettyPageName(page): - - "Return a nicely formatted title/name for the given 'page'." - - return page.split_title(force=1).replace("_", " ").replace("/", u" » ") - -def getEventDetails(page): - - "Return a dictionary of event details from the given 'page'." - - event_details = {} - - if page.pi["format"] == "wiki": - for match in definition_list_regexp.finditer(page.body): - # Permit case-insensitive list terms. - term = match.group("term").lower() - desc = match.group("desc") - if term in ("start", "end"): - desc = getDate(desc) - if desc is not None: - event_details[term] = desc - - return event_details - -def getDate(s): - - "Parse the string 's', extracting and returning a date string." - - m = date_regexp.search(s) - if m: - return tuple(map(int, m.groups())) - else: - return None - -def getMonth(s): - - "Parse the string 's', extracting and returning a month string." - - m = month_regexp.search(s) - if m: - return tuple(map(int, m.groups())) - else: - return None - -def daterange(first, last): - results = [] - - months_only = len(first) == 2 - start_year = first[0] - end_year = last[0] - - for year in range(start_year, end_year + 1): - if year < end_year: - end_month = 12 - else: - end_month = last[1] - - if year > start_year: - start_month = 1 - else: - start_month = first[1] - - for month in range(start_month, end_month + 1): - if months_only: - results.append((year, month)) - else: - if month < end_month: - _wd, end_day = calendar.monthrange(year, month) - else: - end_day = last[2] - - if month > start_month: - start_day = 1 - else: - start_day = first[2] - - for day in range(start_day, end_day + 1): - results.append((year, month, day)) - - return results +# HTML-related functions. def getColour(s): colour = [0, 0, 0] @@ -169,6 +42,8 @@ else: return (255, 255, 255) +# Macro function. + def execute(macro, args): """ @@ -211,9 +86,9 @@ for arg in parsed_args: if arg.startswith("start="): - calendar_start = getMonth(arg[6:]) + calendar_start = EventAggregatorSupport.getMonth(arg[6:]) elif arg.startswith("end="): - calendar_end = getMonth(arg[4:]) + calendar_end = EventAggregatorSupport.getMonth(arg[4:]) elif arg.startswith("mode="): mode = arg[5:] elif arg.startswith("names="): @@ -221,65 +96,8 @@ else: category_names.append(arg) - # Generate a list of events found on pages belonging to the specified - # categories, as found in the macro arguments. - - events = [] - shown_events = {} - all_shown_events = [] - - earliest = None - latest = None - - for category_name in category_names: - - # Get the pages and page names in the category. - - pages_in_category = getPages(category_name, request) - - # Visit each page in the category. - - for page_in_category in pages_in_category: - pagename = page_in_category.page_name - - # Get a real page, not a result page. - - real_page_in_category = Page(request, pagename) - event_details = getEventDetails(real_page_in_category) - - # Define the event as the page together with its details. - - event = (real_page_in_category, event_details) - events.append(event) - - # Test for the suitability of the event. - - if event_details.has_key("start") and event_details.has_key("end"): - - start_month = event_details["start"][:2] - end_month = event_details["end"][:2] - - # Compare the months of the dates to the requested calendar - # window, if any. - - if (calendar_start is None or end_month >= calendar_start) and \ - (calendar_end is None or start_month <= calendar_end): - - if earliest is None or start_month < earliest: - earliest = start_month - if latest is None or end_month > latest: - latest = end_month - - # Store the event in the month-specific dictionary. - - first = max(start_month, calendar_start or start_month) - last = min(end_month, calendar_end or end_month) - - for event_month in daterange(first, last): - if not shown_events.has_key(event_month): - shown_events[event_month] = [] - shown_events[event_month].append(event) - all_shown_events.append(event) + events, shown_events, all_shown_events, earliest, latest = \ + EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end) # Make a calendar. @@ -295,7 +113,7 @@ first = calendar_start or earliest last = calendar_end or latest - for year, month in daterange(first, last): + for year, month in EventAggregatorSupport.daterange(first, last): # Either output a calendar view... @@ -363,7 +181,7 @@ event_start = max(event_details["start"], week_start) event_end = min(event_details["end"], week_end) - event_coverage = set(daterange(event_start, event_end)) + event_coverage = set(EventAggregatorSupport.daterange(event_start, event_end)) # Update the overall coverage. @@ -482,7 +300,7 @@ # Get a pretty version of the page name. - pretty_pagename = getPrettyPageName(event_page) + pretty_pagename = EventAggregatorSupport.getPrettyPageName(event_page) # Generate a colour for the event. @@ -552,7 +370,7 @@ # Get a pretty version of the page name. - pretty_pagename = getPrettyPageName(event_page) + pretty_pagename = EventAggregatorSupport.getPrettyPageName(event_page) output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) @@ -579,46 +397,9 @@ # Output top-level information. - # Output iCalendar data... - - if mode == "ics": - - # Output the calendar details as preformatted text. - - output.append(fmt.preformatted(on=1)) - output.append(fmt.text("BEGIN:VCALENDAR")) - output.append(fmt.linebreak()) - output.append(fmt.text("VERSION:1.0")) - output.append(fmt.linebreak()) - - for event_page, event_details in all_shown_events: - - # Get a pretty version of the page name. - - pretty_pagename = getPrettyPageName(event_page) - - # Output the event details. - - output.append(fmt.text("BEGIN:VEVENT")) - output.append(fmt.linebreak()) - output.append(fmt.text("SUMMARY:%s" % pretty_pagename)) - output.append(fmt.linebreak()) - output.append(fmt.text("URL:%s" % request.getQualifiedURL(event_page.url(request)))) - output.append(fmt.linebreak()) - output.append(fmt.text("DTSTART:%04d%02d%02d" % event_details["start"])) - output.append(fmt.linebreak()) - output.append(fmt.text("DTEND:%04d%02d%02d" % event_details["end"])) - output.append(fmt.linebreak()) - output.append(fmt.text("END:VEVENT")) - output.append(fmt.linebreak()) - - output.append(fmt.text("END:VCALENDAR")) - output.append(fmt.linebreak()) - output.append(fmt.preformatted(on=0)) - # End of list view output. - elif mode == "list": + if mode == "list": output.append(fmt.bullet_list(on=0)) return ''.join(output) diff -r 242a07db6caf -r 8d6569566242 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Mon Mar 23 01:52:13 2009 +0100 @@ -0,0 +1,15 @@ +#! /usr/bin/env python + +from distutils.core import setup + +import EventAggregatorSupport + +setup( + name = "EventAggregator", + description = "Aggregate event data and display it in an event calendar (or summarise it in an iCalendar resource)", + author = "Paul Boddie", + author_email = "paul@boddie.org.uk", + url = "http://moinmo.in/MacroMarket/EventAggregator", + version = EventAggregatorSupport.__version__, + py_modules = ["EventAggregatorSupport"] + )