1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/EventAggregatorSupport/Types.py Tue Apr 30 23:49:32 2013 +0200
1.3 @@ -0,0 +1,720 @@
1.4 +# -*- coding: iso-8859-1 -*-
1.5 +"""
1.6 + MoinMoin - EventAggregator object types
1.7 +
1.8 + @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
1.9 + @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
1.10 + 2005-2008 MoinMoin:ThomasWaldmann.
1.11 + @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.12 +"""
1.13 +
1.14 +from GeneralSupport import to_list
1.15 +from LocationSupport import getMapReference
1.16 +from MoinSupport import *
1.17 +
1.18 +import re
1.19 +
1.20 +try:
1.21 + set
1.22 +except NameError:
1.23 + from sets import Set as set
1.24 +
1.25 +# Page parsing.
1.26 +
1.27 +definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE)
1.28 +category_membership_regexp = re.compile(ur"^\s*(?:(Category\S+)(?:\s+(Category\S+))*)\s*$", re.MULTILINE | re.UNICODE)
1.29 +
1.30 +# Event parsing from page texts.
1.31 +
1.32 +def parseEvents(text, event_page, fragment=None):
1.33 +
1.34 + """
1.35 + Parse events in the given 'text', returning a list of event objects for the
1.36 + given 'event_page'. An optional 'fragment' can be specified to indicate a
1.37 + specific region of the event page.
1.38 +
1.39 + If the optional 'fragment' identifier is provided, the first heading may
1.40 + also be used to provide an event summary/title.
1.41 + """
1.42 +
1.43 + template_details = {}
1.44 + if fragment:
1.45 + template_details["fragment"] = fragment
1.46 +
1.47 + details = {}
1.48 + details.update(template_details)
1.49 + raw_details = {}
1.50 +
1.51 + # Obtain a heading, if requested.
1.52 +
1.53 + if fragment:
1.54 + for level, title, (start, end) in getHeadings(text):
1.55 + raw_details["title"] = text[start:end]
1.56 + details["title"] = getSimpleWikiText(title.strip())
1.57 + break
1.58 +
1.59 + # Start populating events.
1.60 +
1.61 + events = [Event(event_page, details, raw_details)]
1.62 +
1.63 + for match in definition_list_regexp.finditer(text):
1.64 +
1.65 + # Skip commented-out items.
1.66 +
1.67 + if match.group("optcomment"):
1.68 + continue
1.69 +
1.70 + # Permit case-insensitive list terms.
1.71 +
1.72 + term = match.group("term").lower()
1.73 + raw_desc = match.group("desc")
1.74 +
1.75 + # Special value type handling.
1.76 +
1.77 + # Dates.
1.78 +
1.79 + if term in Event.date_terms:
1.80 + desc = getDateTime(raw_desc)
1.81 +
1.82 + # Lists (whose elements may be quoted).
1.83 +
1.84 + elif term in Event.list_terms:
1.85 + desc = map(getSimpleWikiText, to_list(raw_desc, ","))
1.86 +
1.87 + # Position details.
1.88 +
1.89 + elif term == "geo":
1.90 + try:
1.91 + desc = map(getMapReference, to_list(raw_desc, None))
1.92 + if len(desc) != 2:
1.93 + continue
1.94 + except (KeyError, ValueError):
1.95 + continue
1.96 +
1.97 + # Labels which may well be quoted.
1.98 +
1.99 + elif term in Event.title_terms:
1.100 + desc = getSimpleWikiText(raw_desc.strip())
1.101 +
1.102 + # Plain Wiki text terms.
1.103 +
1.104 + elif term in Event.other_terms:
1.105 + desc = raw_desc.strip()
1.106 +
1.107 + else:
1.108 + desc = raw_desc
1.109 +
1.110 + if desc is not None:
1.111 +
1.112 + # Handle apparent duplicates by creating a new set of
1.113 + # details.
1.114 +
1.115 + if details.has_key(term):
1.116 +
1.117 + # Make a new event.
1.118 +
1.119 + details = {}
1.120 + details.update(template_details)
1.121 + raw_details = {}
1.122 + events.append(Event(event_page, details, raw_details))
1.123 +
1.124 + details[term] = desc
1.125 + raw_details[term] = raw_desc
1.126 +
1.127 + return events
1.128 +
1.129 +# Event resources providing collections of events.
1.130 +
1.131 +class EventResource:
1.132 +
1.133 + "A resource providing event information."
1.134 +
1.135 + def __init__(self, url):
1.136 + self.url = url
1.137 +
1.138 + def getPageURL(self):
1.139 +
1.140 + "Return the URL of this page."
1.141 +
1.142 + return self.url
1.143 +
1.144 + def getFormat(self):
1.145 +
1.146 + "Get the format used by this resource."
1.147 +
1.148 + return "plain"
1.149 +
1.150 + def getMetadata(self):
1.151 +
1.152 + """
1.153 + Return a dictionary containing items describing the page's "created"
1.154 + time, "last-modified" time, "sequence" (or revision number) and the
1.155 + "last-comment" made about the last edit.
1.156 + """
1.157 +
1.158 + return {}
1.159 +
1.160 + def getEvents(self):
1.161 +
1.162 + "Return a list of events from this resource."
1.163 +
1.164 + return []
1.165 +
1.166 + def linkToPage(self, request, text, query_string=None, anchor=None):
1.167 +
1.168 + """
1.169 + Using 'request', return a link to this page with the given link 'text'
1.170 + and optional 'query_string' and 'anchor'.
1.171 + """
1.172 +
1.173 + return linkToResource(self.url, request, text, query_string, anchor)
1.174 +
1.175 + # Formatting-related functions.
1.176 +
1.177 + def formatText(self, text, fmt):
1.178 +
1.179 + """
1.180 + Format the given 'text' using the specified formatter 'fmt'.
1.181 + """
1.182 +
1.183 + # Assume plain text which is then formatted appropriately.
1.184 +
1.185 + return fmt.text(text)
1.186 +
1.187 +class EventCalendar(EventResource):
1.188 +
1.189 + "An iCalendar resource."
1.190 +
1.191 + def __init__(self, url, calendar):
1.192 + EventResource.__init__(self, url)
1.193 + self.calendar = calendar
1.194 + self.events = None
1.195 +
1.196 + def getEvents(self):
1.197 +
1.198 + "Return a list of events from this resource."
1.199 +
1.200 + if self.events is None:
1.201 + self.events = []
1.202 +
1.203 + _calendar, _empty, calendar = self.calendar
1.204 +
1.205 + for objtype, attrs, obj in calendar:
1.206 +
1.207 + # Read events.
1.208 +
1.209 + if objtype == "VEVENT":
1.210 + details = {}
1.211 +
1.212 + for property, attrs, value in obj:
1.213 +
1.214 + # Convert dates.
1.215 +
1.216 + if property in ("DTSTART", "DTEND", "CREATED", "DTSTAMP", "LAST-MODIFIED"):
1.217 + if property in ("DTSTART", "DTEND"):
1.218 + property = property[2:]
1.219 + if attrs.get("VALUE") == "DATE":
1.220 + value = getDateFromCalendar(value)
1.221 + if value and property == "END":
1.222 + value = value.previous_day()
1.223 + else:
1.224 + value = getDateTimeFromCalendar(value)
1.225 +
1.226 + # Convert numeric data.
1.227 +
1.228 + elif property == "SEQUENCE":
1.229 + value = int(value)
1.230 +
1.231 + # Convert lists.
1.232 +
1.233 + elif property == "CATEGORIES":
1.234 + value = to_list(value, ",")
1.235 +
1.236 + # Convert positions (using decimal values).
1.237 +
1.238 + elif property == "GEO":
1.239 + try:
1.240 + value = map(getMapReferenceFromDecimal, to_list(value, ";"))
1.241 + if len(value) != 2:
1.242 + continue
1.243 + except (KeyError, ValueError):
1.244 + continue
1.245 +
1.246 + # Accept other textual data as it is.
1.247 +
1.248 + elif property in ("LOCATION", "SUMMARY", "URL"):
1.249 + value = value or None
1.250 +
1.251 + # Ignore other properties.
1.252 +
1.253 + else:
1.254 + continue
1.255 +
1.256 + property = property.lower()
1.257 + details[property] = value
1.258 +
1.259 + self.events.append(CalendarEvent(self, details))
1.260 +
1.261 + return self.events
1.262 +
1.263 +class EventPage:
1.264 +
1.265 + "An event page acting as an event resource."
1.266 +
1.267 + def __init__(self, page):
1.268 + self.page = page
1.269 + self.events = None
1.270 + self.body = None
1.271 + self.categories = None
1.272 + self.metadata = None
1.273 +
1.274 + def copyPage(self, page):
1.275 +
1.276 + "Copy the body of the given 'page'."
1.277 +
1.278 + self.body = page.getBody()
1.279 +
1.280 + def getPageURL(self):
1.281 +
1.282 + "Return the URL of this page."
1.283 +
1.284 + return getPageURL(self.page)
1.285 +
1.286 + def getFormat(self):
1.287 +
1.288 + "Get the format used on this page."
1.289 +
1.290 + return getFormat(self.page)
1.291 +
1.292 + def getMetadata(self):
1.293 +
1.294 + """
1.295 + Return a dictionary containing items describing the page's "created"
1.296 + time, "last-modified" time, "sequence" (or revision number) and the
1.297 + "last-comment" made about the last edit.
1.298 + """
1.299 +
1.300 + if self.metadata is None:
1.301 + self.metadata = getMetadata(self.page)
1.302 + return self.metadata
1.303 +
1.304 + def getRevisions(self):
1.305 +
1.306 + "Return a list of page revisions."
1.307 +
1.308 + return self.page.getRevList()
1.309 +
1.310 + def getPageRevision(self):
1.311 +
1.312 + "Return the revision details dictionary for this page."
1.313 +
1.314 + return getPageRevision(self.page)
1.315 +
1.316 + def getPageName(self):
1.317 +
1.318 + "Return the page name."
1.319 +
1.320 + return self.page.page_name
1.321 +
1.322 + def getPrettyPageName(self):
1.323 +
1.324 + "Return a nicely formatted title/name for this page."
1.325 +
1.326 + return getPrettyPageName(self.page)
1.327 +
1.328 + def getBody(self):
1.329 +
1.330 + "Get the current page body."
1.331 +
1.332 + if self.body is None:
1.333 + self.body = self.page.get_raw_body()
1.334 + return self.body
1.335 +
1.336 + def getEvents(self):
1.337 +
1.338 + "Return a list of events from this page."
1.339 +
1.340 + if self.events is None:
1.341 + self.events = []
1.342 + if self.getFormat() == "wiki":
1.343 + for format, attributes, region in getFragments(self.getBody(), True):
1.344 + self.events += parseEvents(region, self, attributes.get("fragment"))
1.345 +
1.346 + return self.events
1.347 +
1.348 + def setEvents(self, events):
1.349 +
1.350 + "Set the given 'events' on this page."
1.351 +
1.352 + self.events = events
1.353 +
1.354 + def getCategoryMembership(self):
1.355 +
1.356 + "Get the category names from this page."
1.357 +
1.358 + if self.categories is None:
1.359 + body = self.getBody()
1.360 + match = category_membership_regexp.search(body)
1.361 + self.categories = match and [x for x in match.groups() if x] or []
1.362 +
1.363 + return self.categories
1.364 +
1.365 + def setCategoryMembership(self, category_names):
1.366 +
1.367 + """
1.368 + Set the category membership for the page using the specified
1.369 + 'category_names'.
1.370 + """
1.371 +
1.372 + self.categories = category_names
1.373 +
1.374 + def flushEventDetails(self):
1.375 +
1.376 + "Flush the current event details to this page's body text."
1.377 +
1.378 + new_body_parts = []
1.379 + end_of_last_match = 0
1.380 + body = self.getBody()
1.381 +
1.382 + events = iter(self.getEvents())
1.383 +
1.384 + event = events.next()
1.385 + event_details = event.getDetails()
1.386 + replaced_terms = set()
1.387 +
1.388 + for match in definition_list_regexp.finditer(body):
1.389 +
1.390 + # Permit case-insensitive list terms.
1.391 +
1.392 + term = match.group("term").lower()
1.393 + desc = match.group("desc")
1.394 +
1.395 + # Check that the term has not already been substituted. If so,
1.396 + # get the next event.
1.397 +
1.398 + if term in replaced_terms:
1.399 + try:
1.400 + event = events.next()
1.401 +
1.402 + # No more events.
1.403 +
1.404 + except StopIteration:
1.405 + break
1.406 +
1.407 + event_details = event.getDetails()
1.408 + replaced_terms = set()
1.409 +
1.410 + # Add preceding text to the new body.
1.411 +
1.412 + new_body_parts.append(body[end_of_last_match:match.start()])
1.413 +
1.414 + # Get the matching regions, adding the term to the new body.
1.415 +
1.416 + new_body_parts.append(match.group("wholeterm"))
1.417 +
1.418 + # Special value type handling.
1.419 +
1.420 + if event_details.has_key(term):
1.421 +
1.422 + # Dates.
1.423 +
1.424 + if term in event.date_terms:
1.425 + desc = desc.replace("YYYY-MM-DD", str(event_details[term]))
1.426 +
1.427 + # Lists (whose elements may be quoted).
1.428 +
1.429 + elif term in event.list_terms:
1.430 + desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]])
1.431 +
1.432 + # Labels which must be quoted.
1.433 +
1.434 + elif term in event.title_terms:
1.435 + desc = getEncodedWikiText(event_details[term])
1.436 +
1.437 + # Position details.
1.438 +
1.439 + elif term == "geo":
1.440 + desc = " ".join(map(str, event_details[term]))
1.441 +
1.442 + # Text which need not be quoted, but it will be Wiki text.
1.443 +
1.444 + elif term in event.other_terms:
1.445 + desc = event_details[term]
1.446 +
1.447 + replaced_terms.add(term)
1.448 +
1.449 + # Add the replaced value.
1.450 +
1.451 + new_body_parts.append(desc)
1.452 +
1.453 + # Remember where in the page has been processed.
1.454 +
1.455 + end_of_last_match = match.end()
1.456 +
1.457 + # Write the rest of the page.
1.458 +
1.459 + new_body_parts.append(body[end_of_last_match:])
1.460 +
1.461 + self.body = "".join(new_body_parts)
1.462 +
1.463 + def flushCategoryMembership(self):
1.464 +
1.465 + "Flush the category membership to the page body."
1.466 +
1.467 + body = self.getBody()
1.468 + category_names = self.getCategoryMembership()
1.469 + match = category_membership_regexp.search(body)
1.470 +
1.471 + if match:
1.472 + self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]])
1.473 +
1.474 + def saveChanges(self):
1.475 +
1.476 + "Save changes to the event."
1.477 +
1.478 + self.flushEventDetails()
1.479 + self.flushCategoryMembership()
1.480 + self.page.saveText(self.getBody(), 0)
1.481 +
1.482 + def linkToPage(self, request, text, query_string=None, anchor=None):
1.483 +
1.484 + """
1.485 + Using 'request', return a link to this page with the given link 'text'
1.486 + and optional 'query_string' and 'anchor'.
1.487 + """
1.488 +
1.489 + return linkToPage(request, self.page, text, query_string, anchor)
1.490 +
1.491 + # Formatting-related functions.
1.492 +
1.493 + def getParserClass(self, format):
1.494 +
1.495 + """
1.496 + Return a parser class for the given 'format', returning a plain text
1.497 + parser if no parser can be found for the specified 'format'.
1.498 + """
1.499 +
1.500 + return getParserClass(self.page.request, format)
1.501 +
1.502 + def formatText(self, text, fmt):
1.503 +
1.504 + """
1.505 + Format the given 'text' using the specified formatter 'fmt'.
1.506 + """
1.507 +
1.508 + fmt.page = page = self.page
1.509 + request = page.request
1.510 +
1.511 + parser_cls = self.getParserClass(self.getFormat())
1.512 + return formatText(text, request, fmt, parser_cls)
1.513 +
1.514 +# Event details.
1.515 +
1.516 +class Event(ActsAsTimespan):
1.517 +
1.518 + "A description of an event."
1.519 +
1.520 + title_terms = "title", "summary"
1.521 + date_terms = "start", "end"
1.522 + list_terms = "topics", "categories"
1.523 + other_terms = "description", "location", "link"
1.524 + geo_terms = "geo",
1.525 + all_terms = title_terms + date_terms + list_terms + other_terms + geo_terms
1.526 +
1.527 + def __init__(self, page, details, raw_details=None):
1.528 + self.page = page
1.529 + self.details = details
1.530 + self.raw_details = raw_details
1.531 +
1.532 + # Permit omission of the end of the event by duplicating the start.
1.533 +
1.534 + if self.details.has_key("start") and not self.details.get("end"):
1.535 + end = self.details["start"]
1.536 +
1.537 + # Make any end time refer to the day instead.
1.538 +
1.539 + if isinstance(end, DateTime):
1.540 + end = end.as_date()
1.541 +
1.542 + self.details["end"] = end
1.543 +
1.544 + def __repr__(self):
1.545 + return "<Event %r %r>" % (self.getSummary(), self.as_limits())
1.546 +
1.547 + def __hash__(self):
1.548 +
1.549 + """
1.550 + Return a dictionary hash, avoiding mistaken equality of events in some
1.551 + situations (notably membership tests) by including the URL as well as
1.552 + the summary.
1.553 + """
1.554 +
1.555 + return hash(self.getSummary() + self.getEventURL())
1.556 +
1.557 + def getPage(self):
1.558 +
1.559 + "Return the page describing this event."
1.560 +
1.561 + return self.page
1.562 +
1.563 + def setPage(self, page):
1.564 +
1.565 + "Set the 'page' describing this event."
1.566 +
1.567 + self.page = page
1.568 +
1.569 + def getEventURL(self):
1.570 +
1.571 + "Return the URL of this event."
1.572 +
1.573 + fragment = self.details.get("fragment")
1.574 + return self.page.getPageURL() + (fragment and "#" + fragment or "")
1.575 +
1.576 + def linkToEvent(self, request, text, query_string=None):
1.577 +
1.578 + """
1.579 + Using 'request', return a link to this event with the given link 'text'
1.580 + and optional 'query_string'.
1.581 + """
1.582 +
1.583 + return self.page.linkToPage(request, text, query_string, self.details.get("fragment"))
1.584 +
1.585 + def getMetadata(self):
1.586 +
1.587 + """
1.588 + Return a dictionary containing items describing the event's "created"
1.589 + time, "last-modified" time, "sequence" (or revision number) and the
1.590 + "last-comment" made about the last edit.
1.591 + """
1.592 +
1.593 + # Delegate this to the page.
1.594 +
1.595 + return self.page.getMetadata()
1.596 +
1.597 + def getSummary(self, event_parent=None):
1.598 +
1.599 + """
1.600 + Return either the given title or summary of the event according to the
1.601 + event details, or a summary made from using the pretty version of the
1.602 + page name.
1.603 +
1.604 + If the optional 'event_parent' is specified, any page beneath the given
1.605 + 'event_parent' page in the page hierarchy will omit this parent information
1.606 + if its name is used as the summary.
1.607 + """
1.608 +
1.609 + event_details = self.details
1.610 +
1.611 + if event_details.has_key("title"):
1.612 + return event_details["title"]
1.613 + elif event_details.has_key("summary"):
1.614 + return event_details["summary"]
1.615 + else:
1.616 + # If appropriate, remove the parent details and "/" character.
1.617 +
1.618 + title = self.page.getPageName()
1.619 +
1.620 + if event_parent and title.startswith(event_parent):
1.621 + title = title[len(event_parent.rstrip("/")) + 1:]
1.622 +
1.623 + return getPrettyTitle(title)
1.624 +
1.625 + def getDetails(self):
1.626 +
1.627 + "Return the details for this event."
1.628 +
1.629 + return self.details
1.630 +
1.631 + def setDetails(self, event_details):
1.632 +
1.633 + "Set the 'event_details' for this event."
1.634 +
1.635 + self.details = event_details
1.636 +
1.637 + def getRawDetails(self):
1.638 +
1.639 + "Return the details for this event as they were written in a page."
1.640 +
1.641 + return self.raw_details
1.642 +
1.643 + # Timespan-related methods.
1.644 +
1.645 + def __contains__(self, other):
1.646 + return self == other
1.647 +
1.648 + def __eq__(self, other):
1.649 + if isinstance(other, Event):
1.650 + return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other)
1.651 + else:
1.652 + return self._cmp(other) == 0
1.653 +
1.654 + def __ne__(self, other):
1.655 + return not self.__eq__(other)
1.656 +
1.657 + def __lt__(self, other):
1.658 + return self._cmp(other) == -1
1.659 +
1.660 + def __le__(self, other):
1.661 + return self._cmp(other) in (-1, 0)
1.662 +
1.663 + def __gt__(self, other):
1.664 + return self._cmp(other) == 1
1.665 +
1.666 + def __ge__(self, other):
1.667 + return self._cmp(other) in (0, 1)
1.668 +
1.669 + def _cmp(self, other):
1.670 +
1.671 + "Compare this event to an 'other' event purely by their timespans."
1.672 +
1.673 + if isinstance(other, Event):
1.674 + return cmp(self.as_timespan(), other.as_timespan())
1.675 + else:
1.676 + return cmp(self.as_timespan(), other)
1.677 +
1.678 + def as_timespan(self):
1.679 + details = self.details
1.680 + if details.has_key("start") and details.has_key("end"):
1.681 + return Timespan(details["start"], details["end"])
1.682 + else:
1.683 + return None
1.684 +
1.685 + def as_limits(self):
1.686 + ts = self.as_timespan()
1.687 + return ts and ts.as_limits()
1.688 +
1.689 +class CalendarEvent(Event):
1.690 +
1.691 + "An event from a remote calendar."
1.692 +
1.693 + def getEventURL(self):
1.694 +
1.695 + "Return the URL of this event."
1.696 +
1.697 + return self.details.get("url") or self.page.getPageURL()
1.698 +
1.699 + def linkToEvent(self, request, text, query_string=None, anchor=None):
1.700 +
1.701 + """
1.702 + Using 'request', return a link to this event with the given link 'text'
1.703 + and optional 'query_string' and 'anchor'.
1.704 + """
1.705 +
1.706 + return linkToResource(self.getEventURL(), request, text, query_string, anchor)
1.707 +
1.708 + def getMetadata(self):
1.709 +
1.710 + """
1.711 + Return a dictionary containing items describing the event's "created"
1.712 + time, "last-modified" time, "sequence" (or revision number) and the
1.713 + "last-comment" made about the last edit.
1.714 + """
1.715 +
1.716 + return {
1.717 + "created" : self.details.get("created") or self.details["dtstamp"],
1.718 + "last-modified" : self.details.get("last-modified") or self.details["dtstamp"],
1.719 + "sequence" : self.details.get("sequence") or 0,
1.720 + "last-comment" : ""
1.721 + }
1.722 +
1.723 +# vim: tabstop=4 expandtab shiftwidth=4