# HG changeset patch # User Paul Boddie # Date 1311440404 -7200 # Node ID a0c1bd36b025f8c5cb0dcf9c4682e9d755b9fe6c # Parent 23288a853ef1a4b282c81cc7773612e1f4a1e1bd Integrated event sources with the summary action and the links produced by the macro to invoke the action. Changed the metadata acquisition mechanism to populate calendar events with specific event metadata whilst leaving Wiki events to get metadata from their containing pages. Events are now queried for such metadata instead of pages. Changed the date representation for metadata to use the DateTime class, moving the HTTP textual representation function to become a method of DateTime. Fixed text formatting for Wiki events. Added encoding detection for remote events. diff -r 23288a853ef1 -r a0c1bd36b025 EventAggregatorSupport.py --- a/EventAggregatorSupport.py Sat Jul 23 15:59:27 2011 +0200 +++ b/EventAggregatorSupport.py Sat Jul 23 19:00:04 2011 +0200 @@ -94,6 +94,11 @@ date_icalendar_regexp = re.compile(date_icalendar_regexp_str, re.UNICODE) datetime_icalendar_regexp = re.compile(datetime_icalendar_regexp_str, re.UNICODE) +# Content type parsing. + +encoding_regexp_str = ur'charset=(?P[-A-Za-z0-9]+)' +encoding_regexp = re.compile(encoding_regexp_str) + # Simple content parsing. verbatim_regexp = re.compile(ur'(?:' @@ -123,6 +128,13 @@ category_regexp = re.compile(u'^%s$' % ur'(?PCategory(?P(?!Template)\S+))', re.UNICODE) return category_regexp +def getContentEncoding(content_type): + m = encoding_regexp.search(content_type) + if m: + return m.group("encoding") + else: + return None + def int_or_none(x): if x is None: return x @@ -306,17 +318,6 @@ # Textual representations. -def getHTTPTimeString(tmtuple): - return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( - getDayLabel(tmtuple.tm_wday), - tmtuple.tm_mday, - getMonthLabel(tmtuple.tm_mon), - tmtuple.tm_year, - tmtuple.tm_hour, - tmtuple.tm_min, - tmtuple.tm_sec - ) - def getSimpleWikiText(text): """ @@ -407,7 +408,9 @@ mtime = 0 comment = "" - return {"timestamp" : time.gmtime(mtime), "comment" : comment} + # Leave the time zone empty. + + return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} # Category discovery and searching. @@ -516,10 +519,20 @@ def getFormat(self): - "Get the format used on this page." + "Get the format used by this resource." return "plain" + def getMetadata(self): + + """ + Return a dictionary containing items describing the page's "created" + time, "last-modified" time, "sequence" (or revision number) and the + "last-comment" made about the last edit. + """ + + return {} + def getEvents(self): "Return a list of events from this resource." @@ -533,19 +546,7 @@ and optional 'query_string'. """ - url = self.url - - if query_string: - query_string = wikiutil.makeQueryString(query_string) - url = "%s?%s" % (url, query_string) - - formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter - - output = [] - output.append(formatter.url(1, url)) - output.append(formatter.text(text)) - output.append(formatter.url(0)) - return "".join(output) + return linkToResource(self.url, request, text, query_string) # Formatting-related functions. @@ -589,24 +590,38 @@ # Convert dates. - if property in ("DTSTART", "DTEND"): - property = property[2:].lower() + if property in ("DTSTART", "DTEND", "CREATED", "DTSTAMP", "LAST-MODIFIED"): + if property in ("DTSTART", "DTEND"): + property = property[2:] if attrs.get("VALUE") == "DATE": value = getDateFromCalendar(value) else: value = getDateTimeFromCalendar(value) + # Convert numeric data. + + elif property == "SEQUENCE": + value = int(value) + + # Convert lists. + + elif property == "CATEGORIES": + value = [v.strip() for v in value.split(",") if v.strip()] + # Accept other textual data as it is. - elif property in ("CATEGORIES", "LOCATION", "SUMMARY"): - property = property.lower() + elif property in ("LOCATION", "SUMMARY", "URL"): + pass + + # Ignore other properties. else: continue + property = property.lower() details[property] = value - self.events.append(Event(self, details)) + self.events.append(CalendarEvent(self, details)) return self.events @@ -619,6 +634,7 @@ self.events = None self.body = None self.categories = None + self.metadata = None def copyPage(self, page): @@ -638,6 +654,35 @@ return self.page.pi["format"] + def getMetadata(self): + + """ + Return a dictionary containing items describing the page's "created" + time, "last-modified" time, "sequence" (or revision number) and the + "last-comment" made about the last edit. + """ + + request = self.page.request + + # Get the initial revision of the page. + + revisions = self.getRevisions() + event_page_initial = Page(request, self.getPageName(), rev=revisions[-1]) + + # Get the created and last modified times. + + initial_revision = getPageRevision(event_page_initial) + + if self.metadata is None: + self.metadata = {} + self.metadata["created"] = initial_revision["timestamp"] + latest_revision = self.getPageRevision() + self.metadata["last-modified"] = latest_revision["timestamp"] + self.metadata["sequence"] = len(revisions) - 1 + self.metadata["last-comment"] = latest_revision["comment"] + + return self.metadata + def getRevisions(self): "Return a list of page revisions." @@ -883,6 +928,8 @@ 'fmt'. """ + fmt.page = self.page + # Suppress line anchors. parser_cls = self.getParserClass(request, self.getFormat()) @@ -932,6 +979,33 @@ self.page = page + def getEventURL(self, request): + + "Using 'request', return the URL of this event." + + return self.page.getPageURL(request) + + def linkToEvent(self, request, text, query_string=None): + + """ + Using 'request', return a link to this event with the given link 'text' + and optional 'query_string'. + """ + + return self.page.linkToPage(request, text, query_string) + + def getMetadata(self): + + """ + Return a dictionary containing items describing the event's "created" + time, "last-modified" time, "sequence" (or revision number) and the + "last-comment" made about the last edit. + """ + + # Delegate this to the page. + + return self.page.getMetadata() + def getSummary(self, event_parent=None): """ @@ -994,6 +1068,40 @@ ts = self.as_timespan() return ts and ts.as_limits() +class CalendarEvent(Event): + + "An event from a remote calendar." + + def getEventURL(self, request): + + "Using 'request', return the URL of this event." + + return self.details.get("url") or self.page.getPageURL(request) + + def linkToEvent(self, request, text, query_string=None): + + """ + Using 'request', return a link to this event with the given link 'text' + and optional 'query_string'. + """ + + return linkToResource(self.getEventURL(request), request, text, query_string) + + def getMetadata(self): + + """ + Return a dictionary containing items describing the event's "created" + time, "last-modified" time, "sequence" (or revision number) and the + "last-comment" made about the last edit. + """ + + return { + "created" : self.details.get("created") or self.details["dtstamp"], + "last-modified" : self.details.get("last-modified") or self.details["dtstamp"], + "sequence" : self.details.get("sequence") or 0, + "last-comment" : "" + } + # Obtaining event containers and events from such containers. def getEventPages(pages): @@ -1004,6 +1112,19 @@ return map(EventPage, pages) +def getAllEventSources(request): + + "Return all event sources defined in the Wiki using the 'request'." + + sources_page = getattr(request.cfg, "event_aggregator_sources_page", "EventSourcesDict") + + # Remote sources are accessed via dictionary page definitions. + + if request.user.may.read(sources_page): + return request.dicts.dict(sources_page) + else: + return {} + def getEventResources(sources, calendar_start, calendar_end, request): """ @@ -1012,13 +1133,8 @@ and the 'request' to access configuration settings in the Wiki. """ - sources_page = getattr(request.cfg, "event_aggregator_sources_page", "EventSourcesDict") - - # Remote sources are accessed via dictionary page definitions. - - if request.user.may.read(sources_page): - sources_dict = request.dicts.dict(sources_page) - else: + sources_dict = getAllEventSources(request) + if not sources_dict: return [] # Use dates for the calendar limits. @@ -1061,9 +1177,12 @@ f = urllib.urlopen(url) - # NOTE: Should look at the metadata first. - - uf = codecs.getreader("utf-8")(f) + if f.headers.has_key("content-type"): + encoding = getContentEncoding(f.headers["content-type"]) + else: + encoding = None + + uf = codecs.getreader(encoding or "utf-8")(f) try: resources.append(resource_cls(url, parser(uf))) @@ -1151,21 +1270,10 @@ for event in events: event_details = event.getDetails() - event_page = event.getPage() - - # Get the initial revision of the page. - - revisions = event_page.getRevisions() - event_page_initial = Page(request, event_page.getPageName(), rev=revisions[-1]) - - # Get the created and last modified times. - - initial_revision = getPageRevision(event_page_initial) - event_details["created"] = initial_revision["timestamp"] - latest_revision = event_page.getPageRevision() - event_details["last-modified"] = latest_revision["timestamp"] - event_details["sequence"] = len(revisions) - 1 - event_details["last-comment"] = latest_revision["comment"] + + # Populate the details with event metadata. + + event_details.update(event.getMetadata()) if latest is None or latest < event_details["last-modified"]: latest = event_details["last-modified"] @@ -1638,6 +1746,15 @@ else: return "" + def as_HTTP_datetime_string(self): + weekday = calendar.weekday(*self.data[:3]) + return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (( + getDayLabel(weekday), + self.data[2], + getMonthLabel(self.data[1]), + self.data[0] + ) + tuple(self.data[3:6])) + def as_datetime(self): return self @@ -2434,6 +2551,25 @@ text = wikiutil.escape(text) return page.link_to_raw(request, text, query_string) +def linkToResource(url, request, text, query_string=None): + + """ + Using 'request', return a link to 'url' with the given link 'text' and + optional 'query_string'. + """ + + if query_string: + query_string = wikiutil.makeQueryString(query_string) + url = "%s?%s" % (url, query_string) + + formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter + + output = [] + output.append(formatter.url(1, url)) + output.append(formatter.text(text)) + output.append(formatter.url(0)) + return "".join(output) + def getFullPageName(parent, title): """ diff -r 23288a853ef1 -r a0c1bd36b025 actions/EventAggregatorSummary.py --- a/actions/EventAggregatorSummary.py Sat Jul 23 15:59:27 2011 +0200 +++ b/actions/EventAggregatorSummary.py Sat Jul 23 19:00:04 2011 +0200 @@ -42,6 +42,16 @@ category_list.append('' % ( escattr(category_pagename), selected, escape(category_name))) + sources_list = [] + sources = form.get("source", []) + + for source_name in getAllEventSources(request).keys(): + + selected = self._get_selected_for_list(source_name, sources) + + sources_list.append('' % ( + escattr(source_name), selected, escape(source_name))) + # Initialise month lists and defaults. start_month_list, end_month_list = self.get_month_lists() @@ -90,6 +100,8 @@ "buttons_html" : buttons_html, "category_label" : escape(_("Categories")), "category_list" : "\n".join(category_list), + "sources_label" : escape(_("Sources")), + "sources_list" : "\n".join(sources_list), "start_month_list" : "\n".join(start_month_list), "start_label" : escape(_("Start day (optional), month and year")), "start_day_default" : escattr(start_day_default), @@ -127,6 +139,14 @@ + + + + + + @@ -200,13 +220,14 @@ _ = self._ form = self.get_form() - # If no category names exist in the request, an error message is - # returned. + # If no category names or sources exist in the request, an error message + # is returned. category_names = form.get("category", []) + sources = form.get("source", []) - if not category_names: - return 0, _("No categories specified.") + if not (category_names or sources): + return 0, _("No categories or sources specified.") write_resource(self.request) return 1, None @@ -239,6 +260,7 @@ form = get_form(request) category_names = form.get("category", []) + remote_sources = form.get("source", []) format = form.get("format", ["iCalendar"])[0] descriptions = form.get("descriptions", ["page"])[0] parent = form.get("parent", [""])[0] @@ -268,10 +290,11 @@ # Determine the period and get the events involved. - event_pages = getPagesFromResults(getAllCategoryPages(category_names, request), request) - events = getEventsFromPages(event_pages) - all_shown_events = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end)) - latest_timestamp = setEventTimestamps(request, all_shown_events) + pages = getPagesFromResults(getAllCategoryPages(category_names, request), request) + events = getEventsFromResources(getEventPages(pages)) + events += getEventsFromResources(getEventResources(remote_sources, calendar_start, calendar_end, request)) + all_shown_events = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end)) + latest_timestamp = setEventTimestamps(request, all_shown_events) # Output summary data... @@ -292,7 +315,7 @@ # Define the last modified time. if latest_timestamp is not None: - headers.append("Last-Modified: %s" % getHTTPTimeString(latest_timestamp)) + headers.append("Last-Modified: %s" % latest_timestamp.as_HTTP_datetime_string()) send_headers(headers) @@ -304,7 +327,6 @@ request.write("VERSION:2.0\r\n") for event in all_shown_events: - event_page = event.getPage() event_details = event.getDetails() # NOTE: A custom formatter making attributes for links and plain @@ -313,15 +335,15 @@ # Get the summary details. event_summary = event.getSummary(parent) - link = event_page.getPageURL(request) + link = event.getEventURL(request) # Output the event details. request.write("BEGIN:VEVENT\r\n") request.write("UID:%s\r\n" % link) request.write("URL:%s\r\n" % link) - request.write("DTSTAMP:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_details["created"][:6]) - request.write("LAST-MODIFIED:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_details["last-modified"][:6]) + request.write("DTSTAMP:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_details["created"].as_tuple()[:6]) + request.write("LAST-MODIFIED:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_details["last-modified"].as_tuple()[:6]) request.write("SEQUENCE:%d\r\n" % event_details["sequence"]) start = event_details["start"] @@ -374,7 +396,7 @@ request.write('Events published on %s%s\r\n' % (request.getBaseURL(), path_info)) if latest_timestamp is not None: - request.write('%s\r\n' % getHTTPTimeString(latest_timestamp)) + request.write('%s\r\n' % latest_timestamp.as_HTTP_datetime_string()) # Sort all_shown_events by start date, reversed. @@ -387,13 +409,12 @@ # Get a parser and formatter for the formatting of some attributes. - parser_cls = getParserClass(request, event_page.getFormat()) - fmt = getFormatter(request, "text/html", event_page.page) + fmt = request.html_formatter # Get the summary details. event_summary = event.getSummary(parent) - link = event_page.getPageURL(request) + link = event.getEventURL(request) request.write('\r\n') request.write('%s\r\n' % wikiutil.escape(event_summary)) @@ -408,13 +429,13 @@ description = event_details["last-comment"] request.write('%s\r\n' % - fmt.text(formatText(description, request, fmt, parser_cls))) + fmt.text(event_page.formatText(description, request, fmt))) for topic in event_details.get("topics") or event_details.get("categories") or []: request.write('%s\r\n' % - fmt.text(formatText(topic, request, fmt, parser_cls))) + fmt.text(event_page.formatText(topic, request, fmt))) - request.write('%s\r\n' % getHTTPTimeString(event_details["created"])) + request.write('%s\r\n' % event_details["created"].as_HTTP_datetime_string()) request.write('%s#%s\r\n' % (link, event_details["sequence"])) request.write('\r\n') diff -r 23288a853ef1 -r a0c1bd36b025 macros/EventAggregator.py --- a/macros/EventAggregator.py Sat Jul 23 15:59:27 2011 +0200 +++ b/macros/EventAggregator.py Sat Jul 23 19:00:04 2011 +0200 @@ -24,8 +24,8 @@ def __init__(self, page, calendar_name, raw_calendar_start, raw_calendar_end, original_calendar_start, original_calendar_end, calendar_start, calendar_end, - first, last, category_names, template_name, parent_name, mode, resolution, - name_usage, map_name): + first, last, category_names, remote_sources, template_name, parent_name, mode, + resolution, name_usage, map_name): """ Initialise the view with the current 'page', a 'calendar_name' (which @@ -38,8 +38,9 @@ navigation in the user interface), along with the 'first' and 'last' months of event coverage. - The additional 'category_names', 'template_name', 'parent_name' and - 'mode' parameters are used to configure the links employed by the view. + The additional 'category_names', 'remote_sources', 'template_name', + 'parent_name' and 'mode' parameters are used to configure the links + employed by the view. The 'resolution' affects the view for certain modes and is also used to parameterise links. @@ -67,6 +68,7 @@ self.map_name = map_name self.category_name_parameters = "&".join([("category=%s" % name) for name in category_names]) + self.remote_source_parameters = "&".join([("source=%s" % source) for source in remote_sources]) # Calculate the duration in terms of the highest common unit of time. @@ -250,10 +252,11 @@ # Generate the links. - download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s&%s" % ( + download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s&%s&%s" % ( self.parent_name or "", self.resolution, - self.category_name_parameters + self.category_name_parameters, + self.remote_source_parameters ) download_all_link = download_dialogue_link + "&doit=1" download_link = download_all_link + ("&%s&%s" % ( @@ -603,7 +606,6 @@ output = [] - event_page = event.getPage() event_details = event.getDetails() event_summary = event.getSummary(self.parent_name) @@ -619,7 +621,7 @@ if is_ambiguous: output.append(fmt.icon("/!\\")) - output.append(event_page.linkToPage(request, event_summary)) + output.append(event.linkToEvent(request, event_summary)) output.append(fmt.div(on=0)) # Add a pop-up element for long summaries. @@ -629,7 +631,7 @@ if is_ambiguous: output.append(fmt.icon("/!\\")) - output.append(event_page.linkToPage(request, event_summary)) + output.append(event.linkToEvent(request, event_summary)) output.append(fmt.div(on=0)) output.append(fmt.div(on=0)) @@ -782,7 +784,6 @@ # Get event details for the current day. for event in events: - event_page = event.getPage() event_details = event.getDetails() if date not in event: @@ -1221,7 +1222,6 @@ # Get the event details. - event_page = event.getPage() event_summary = event.getSummary(self.parent_name) start, end = event.as_limits() event_period = self._getCalendarPeriod( @@ -1233,7 +1233,7 @@ # Link to the page using the summary. - output.append(event_page.linkToPage(request, event_summary)) + output.append(event.linkToEvent(request, event_summary)) # Add the event period. @@ -1413,8 +1413,8 @@ view = View(page, calendar_name, raw_calendar_start, raw_calendar_end, original_calendar_start, original_calendar_end, calendar_start, calendar_end, - first, last, category_names, template_name, parent_name, mode, resolution, - name_usage, map_name) + first, last, category_names, remote_sources, template_name, parent_name, + mode, resolution, name_usage, map_name) # Make a calendar. @@ -1496,7 +1496,7 @@ # Link to the page using the summary. output.append(fmt.table_cell(on=1, attrs=attrs)) - output.append(event_page.linkToPage(request, event_summary)) + output.append(event.linkToEvent(request, event_summary)) output.append(fmt.table_cell(on=0)) output.append(fmt.table_row(on=0)) @@ -1770,7 +1770,7 @@ # Link to the page using the summary. output.append(fmt.paragraph(on=1)) - output.append(event_page.linkToPage(request, event_summary)) + output.append(event.linkToEvent(request, event_summary)) output.append(fmt.paragraph(on=0)) # Start and end dates.