1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator object types 4 5 @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 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 DateSupport import DateTime 12 from GeneralSupport import to_list 13 from LocationSupport import getMapReference, getMapReferenceFromDecimal 14 from MoinSupport import * 15 import vCalendar 16 17 from codecs import getreader 18 from email.utils import parsedate 19 import re 20 21 try: 22 from cStringIO import StringIO 23 except ImportError: 24 from StringIO import StringIO 25 26 try: 27 set 28 except NameError: 29 from sets import Set as set 30 31 # Import libxml2dom for xCalendar parsing. 32 33 try: 34 import libxml2dom 35 except ImportError: 36 libxml2dom = None 37 38 # Page parsing. 39 40 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 41 category_membership_regexp = re.compile(ur"^\s*(?:(Category\S+)(?:\s+(Category\S+))*)\s*$", re.MULTILINE | re.UNICODE) 42 43 # Event parsing from page texts. 44 45 def parseEventsInPage(text, page, fragment=None): 46 47 """ 48 Parse events in the given 'text' from the given 'page'. 49 """ 50 51 # Calendar-format pages are parsed directly by the iCalendar parser. 52 53 if page.getFormat() == "calendar": 54 return parseEventsInCalendar(text) 55 56 # xCalendar-format pages are parsed directly by the iCalendar parser. 57 58 elif page.getFormat() == "xcalendar": 59 return parseEventsInXMLCalendar(text) 60 61 # Wiki-format pages are parsed region-by-region using the special markup. 62 63 elif page.getFormat() == "wiki": 64 65 # Where a page contains events, potentially in regions, identify the page 66 # regions and obtain the events within them. 67 68 events = [] 69 for format, attributes, region in getFragments(text, True): 70 if format == "calendar": 71 events += parseEventsInCalendar(region) 72 else: 73 events += parseEvents(region, page, attributes.get("fragment") or fragment) 74 return events 75 76 # Unsupported format pages return no events. 77 78 else: 79 return [] 80 81 def parseEventsInCalendar(text): 82 83 """ 84 Parse events in iCalendar format from the given 'text'. 85 """ 86 87 # Fill the StringIO with encoded plain string data. 88 89 encoding = "utf-8" 90 calendar = parseEventsInCalendarFromResource(StringIO(text.encode(encoding)), encoding) 91 return calendar.getEvents() 92 93 def parseEventsInXMLCalendar(text): 94 95 """ 96 Parse events in xCalendar format from the given 'text'. 97 """ 98 99 # Fill the StringIO with encoded plain string data. 100 101 encoding = "utf-8" 102 calendar = parseEventsInXMLCalendarFromResource(StringIO(text.encode(encoding)), encoding) 103 return calendar.getEvents() 104 105 def parseEventsInCalendarFromResource(f, encoding=None, url=None, metadata=None): 106 107 """ 108 Parse events in iCalendar format from the given file-like object 'f', with 109 content having any specified 'encoding' and being described by the given 110 'url' and 'metadata'. 111 """ 112 113 # Read Unicode from the resource. 114 115 uf = getreader(encoding or "utf-8")(f) 116 try: 117 return EventCalendar(url or "", vCalendar.parse(uf), metadata or {}) 118 finally: 119 uf.close() 120 121 def parseEventsInXMLCalendarFromResource(f, encoding=None, url=None, metadata=None): 122 123 """ 124 Parse events in xCalendar format from the given file-like object 'f', with 125 content having any specified 'encoding' and being described by the given 126 'url' and 'metadata'. 127 """ 128 129 if libxml2dom is not None: 130 return EventXMLCalendar(url or "", libxml2dom.parse(f), metadata or {}) 131 else: 132 return None 133 134 def parseEvents(text, event_page, fragment=None): 135 136 """ 137 Parse events in the given 'text', returning a list of event objects for the 138 given 'event_page'. An optional 'fragment' can be specified to indicate a 139 specific region of the event page. 140 141 If the optional 'fragment' identifier is provided, the first heading may 142 also be used to provide an event summary/title. 143 """ 144 145 template_details = {} 146 if fragment: 147 template_details["fragment"] = fragment 148 149 details = {} 150 details.update(template_details) 151 raw_details = {} 152 153 # Obtain a heading, if requested. 154 155 if fragment: 156 for level, title, (start, end) in getHeadings(text): 157 raw_details["title"] = text[start:end] 158 details["title"] = getSimpleWikiText(title.strip()) 159 break 160 161 # Start populating events. 162 163 events = [Event(event_page, details, raw_details)] 164 165 # Get any default raw details to modify. 166 167 raw_details = events[-1].getRawDetails() 168 169 for match in definition_list_regexp.finditer(text): 170 171 # Skip commented-out items. 172 173 if match.group("optcomment"): 174 continue 175 176 # Permit case-insensitive list terms. 177 178 term = match.group("term").lower() 179 raw_desc = match.group("desc") 180 181 # Special value type handling. 182 183 # Dates. 184 185 if term in Event.date_terms: 186 desc = getDateTime(raw_desc) 187 188 # Lists (whose elements may be quoted). 189 190 elif term in Event.list_terms: 191 desc = map(getSimpleWikiText, to_list(raw_desc, ",")) 192 193 # Position details. 194 195 elif term == "geo": 196 try: 197 desc = map(getMapReference, to_list(raw_desc, None)) 198 if len(desc) != 2: 199 continue 200 except (KeyError, ValueError): 201 continue 202 203 # Labels which may well be quoted. 204 205 elif term in Event.title_terms: 206 desc = getSimpleWikiText(raw_desc.strip()) 207 208 # Plain Wiki text terms. 209 210 elif term in Event.other_terms: 211 desc = raw_desc.strip() 212 213 else: 214 desc = raw_desc 215 216 if desc is not None: 217 218 # Handle apparent duplicates by creating a new set of 219 # details. 220 221 if details.has_key(term): 222 223 # Make a new event. 224 225 details = {} 226 details.update(template_details) 227 raw_details = {} 228 events.append(Event(event_page, details, raw_details)) 229 raw_details = events[-1].getRawDetails() 230 231 details[term] = desc 232 raw_details[term] = raw_desc 233 234 return events 235 236 # Event resources providing collections of events. 237 238 class EventResource: 239 240 "A resource providing event information." 241 242 def __init__(self, url): 243 self.url = url 244 245 def getPageURL(self): 246 247 "Return the URL of this page." 248 249 return self.url 250 251 def getFormat(self): 252 253 "Get the format used by this resource." 254 255 return "plain" 256 257 def getMetadata(self): 258 259 """ 260 Return a dictionary containing items describing the page's "created" 261 time, "last-modified" time, "sequence" (or revision number) and the 262 "last-comment" made about the last edit. 263 """ 264 265 return {} 266 267 def getEvents(self): 268 269 "Return a list of events from this resource." 270 271 return [] 272 273 def linkToPage(self, request, text, query_string=None, anchor=None): 274 275 """ 276 Using 'request', return a link to this page with the given link 'text' 277 and optional 'query_string' and 'anchor'. 278 """ 279 280 return linkToResource(self.url, request, text, query_string, anchor) 281 282 # Formatting-related functions. 283 284 def formatText(self, text, fmt): 285 286 """ 287 Format the given 'text' using the specified formatter 'fmt'. 288 """ 289 290 # Assume plain text which is then formatted appropriately. 291 292 return fmt.text(text) 293 294 class EventCalendarResource(EventResource): 295 296 "A generic calendar resource." 297 298 def __init__(self, url, metadata): 299 EventResource.__init__(self, url) 300 self.metadata = metadata 301 self.events = None 302 303 if not self.metadata.has_key("created") and self.metadata.has_key("date"): 304 self.metadata["created"] = DateTime(parsedate(self.metadata["date"])[:7]) 305 306 if self.metadata.has_key("last-modified") and not isinstance(self.metadata["last-modified"], DateTime): 307 self.metadata["last-modified"] = DateTime(parsedate(self.metadata["last-modified"])[:7]) 308 309 def getMetadata(self): 310 311 """ 312 Return a dictionary containing items describing the page's "created" 313 time, "last-modified" time, "sequence" (or revision number) and the 314 "last-comment" made about the last edit. 315 """ 316 317 return self.metadata 318 319 class EventCalendar(EventCalendarResource): 320 321 "An iCalendar resource." 322 323 def __init__(self, url, calendar, metadata): 324 EventCalendarResource.__init__(self, url, metadata) 325 self.calendar = calendar 326 327 def getMetadata(self): 328 329 """ 330 Return a dictionary containing items describing the page's "created" 331 time, "last-modified" time, "sequence" (or revision number) and the 332 "last-comment" made about the last edit. 333 """ 334 335 return self.metadata 336 337 def getEvents(self): 338 339 "Return a list of events from this resource." 340 341 if self.events is None: 342 self.events = [] 343 344 _calendar, _empty, calendar = self.calendar 345 346 for objtype, attrs, obj in calendar: 347 348 # Read events. 349 350 if objtype == "VEVENT": 351 details = {} 352 353 for property, attrs, value in obj: 354 355 # Convert dates. 356 357 if property in ("DTSTART", "DTEND", "CREATED", "DTSTAMP", "LAST-MODIFIED"): 358 if property in ("DTSTART", "DTEND"): 359 property = property[2:] 360 if attrs.get("VALUE") == "DATE": 361 value = getDateFromCalendar(value) 362 if value and property == "END": 363 value = value.previous_day() 364 else: 365 value = getDateTimeFromCalendar(value) 366 367 # Convert numeric data. 368 369 elif property == "SEQUENCE": 370 value = int(value) 371 372 # Convert lists. 373 374 elif property == "CATEGORIES": 375 if not isinstance(value, list): 376 value = to_list(value, ",") 377 378 # Convert positions (using decimal values). 379 380 elif property == "GEO": 381 try: 382 value = map(getMapReferenceFromDecimal, to_list(value, ";")) 383 if len(value) != 2: 384 continue 385 except (KeyError, ValueError): 386 continue 387 388 # Accept other textual data as it is. 389 390 elif property in ("LOCATION", "SUMMARY", "URL"): 391 value = value or None 392 393 # Ignore other properties. 394 395 else: 396 continue 397 398 property = property.lower() 399 details[property] = value 400 401 self.events.append(CalendarEvent(self, details)) 402 403 return self.events 404 405 class EventXMLCalendar(EventCalendarResource): 406 407 "An xCalendar resource." 408 409 XCAL = {"xcal" : "urn:ietf:params:xml:ns:icalendar-2.0"} 410 411 # See: http://tools.ietf.org/html/draft-daboo-et-al-icalendar-in-xml-11#section-3.4 412 413 properties = [ 414 ("summary", "xcal:properties/xcal:summary", "getText"), 415 ("location", "xcal:properties/xcal:location", "getText"), 416 ("start", "xcal:properties/xcal:dtstart", "getDateTime"), 417 ("end", "xcal:properties/xcal:dtend", "getDateTime"), 418 ("created", "xcal:properties/xcal:created", "getDateTime"), 419 ("dtstamp", "xcal:properties/xcal:dtstamp", "getDateTime"), 420 ("last-modified", "xcal:properties/xcal:last-modified", "getDateTime"), 421 ("sequence", "xcal:properties/xcal:sequence", "getInteger"), 422 ("categories", "xcal:properties/xcal:categories", "getCollection"), 423 ("geo", "xcal:properties/xcal:geo", "getGeo"), 424 ("url", "xcal:properties/xcal:url", "getURI"), 425 ] 426 427 def __init__(self, url, doc, metadata): 428 EventCalendarResource.__init__(self, url, metadata) 429 self.doc = doc 430 431 def getEvents(self): 432 433 "Return a list of events from this resource." 434 435 if self.events is None: 436 self.events = [] 437 438 for event in self.doc.xpath("//xcal:vevent", namespaces=self.XCAL): 439 details = {} 440 441 for property, path, converter in self.properties: 442 values = event.xpath(path, namespaces=self.XCAL) 443 444 try: 445 value = getattr(self, converter)(property, values) 446 details[property] = value 447 except (IndexError, ValueError): 448 pass 449 450 self.events.append(CalendarEvent(self, details)) 451 452 return self.events 453 454 def _getValue(self, values, type): 455 for element in values[0].xpath("xcal:%s" % type, namespaces=self.XCAL): 456 return element.textContent 457 else: 458 return None 459 460 def getText(self, property, values): 461 return self._getValue(values, "text") 462 463 def getDateTime(self, property, values): 464 element = values[0] 465 for dtelement in element.xpath("xcal:date-time|xcal:date", namespaces=self.XCAL): 466 dt = getDateTimeFromISO8601(dtelement.textContent) 467 break 468 else: 469 return None 470 471 tzid = self._getValue(element.xpath("xcal:parameters", namespaces=self.XCAL), "tzid") 472 if tzid and isinstance(dt, DateTime): 473 zone = "/".join(tzid.rsplit("/", 2)[-2:]) 474 dt.set_time_zone(zone) 475 476 if dtelement.localName == "date" and property == "end": 477 dt = dt.previous_day() 478 479 return dt 480 481 def getInteger(self, property, values): 482 value = self._getValue(values, "integer") 483 if value is not None: 484 return int(value) 485 else: 486 return None 487 488 def getCollection(self, property, values): 489 return [n.textContent for n in values[0].xpath("xcal:text", namespaces=self.XCAL)] 490 491 def getGeo(self, property, values): 492 geo = [None, None] 493 494 for geoelement in values[0].xpath("xcal:latitude|xcal:longitude", namespaces=self.XCAL): 495 value = geoelement.textContent 496 if geoelement.localName == "latitude": 497 geo[0] = value 498 else: 499 geo[1] = value 500 501 if None not in geo: 502 return map(getMapReferenceFromDecimal, geo) 503 else: 504 return None 505 506 def getURI(self, property, values): 507 return self._getValue(values, "uri") 508 509 class EventPage: 510 511 "An event page acting as an event resource." 512 513 def __init__(self, page): 514 self.page = page 515 self.events = None 516 self.body = None 517 self.categories = None 518 self.metadata = None 519 520 def copyPage(self, page): 521 522 "Copy the body of the given 'page'." 523 524 self.body = page.getBody() 525 526 def getPageURL(self): 527 528 "Return the URL of this page." 529 530 return getPageURL(self.page) 531 532 def getFormat(self): 533 534 "Get the format used on this page." 535 536 return getFormat(self.page) 537 538 def getMetadata(self): 539 540 """ 541 Return a dictionary containing items describing the page's "created" 542 time, "last-modified" time, "sequence" (or revision number) and the 543 "last-comment" made about the last edit. 544 """ 545 546 if self.metadata is None: 547 self.metadata = getMetadata(self.page) 548 return self.metadata 549 550 def getRevisions(self): 551 552 "Return a list of page revisions." 553 554 return self.page.getRevList() 555 556 def getPageRevision(self): 557 558 "Return the revision details dictionary for this page." 559 560 return getPageRevision(self.page) 561 562 def getPageName(self): 563 564 "Return the page name." 565 566 return self.page.page_name 567 568 def getPrettyPageName(self): 569 570 "Return a nicely formatted title/name for this page." 571 572 return getPrettyPageName(self.page) 573 574 def getBody(self): 575 576 "Get the current page body." 577 578 if self.body is None: 579 self.body = self.page.get_raw_body() 580 return self.body 581 582 def getEvents(self): 583 584 "Return a list of events from this page." 585 586 if self.events is None: 587 self.events = parseEventsInPage(self.page.data, self) 588 589 return self.events 590 591 def setEvents(self, events): 592 593 "Set the given 'events' on this page." 594 595 self.events = events 596 597 def getCategoryMembership(self): 598 599 "Get the category names from this page." 600 601 if self.categories is None: 602 body = self.getBody() 603 match = category_membership_regexp.search(body) 604 self.categories = match and [x for x in match.groups() if x] or [] 605 606 return self.categories 607 608 def setCategoryMembership(self, category_names): 609 610 """ 611 Set the category membership for the page using the specified 612 'category_names'. 613 """ 614 615 self.categories = category_names 616 617 def flushEventDetails(self): 618 619 "Flush the current event details to this page's body text." 620 621 new_body_parts = [] 622 end_of_last_match = 0 623 body = self.getBody() 624 625 events = iter(self.getEvents()) 626 627 event = events.next() 628 event_details = event.getDetails() 629 replaced_terms = set() 630 631 for match in definition_list_regexp.finditer(body): 632 633 # Permit case-insensitive list terms. 634 635 term = match.group("term").lower() 636 desc = match.group("desc") 637 638 # Check that the term has not already been substituted. If so, 639 # get the next event. 640 641 if term in replaced_terms: 642 try: 643 event = events.next() 644 645 # No more events. 646 647 except StopIteration: 648 break 649 650 event_details = event.getDetails() 651 replaced_terms = set() 652 653 # Add preceding text to the new body. 654 655 new_body_parts.append(body[end_of_last_match:match.start()]) 656 657 # Get the matching regions, adding the term to the new body. 658 659 new_body_parts.append(match.group("wholeterm")) 660 661 # Special value type handling. 662 663 if event_details.has_key(term): 664 665 # Dates. 666 667 if term in event.date_terms: 668 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 669 670 # Lists (whose elements may be quoted). 671 672 elif term in event.list_terms: 673 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 674 675 # Labels which must be quoted. 676 677 elif term in event.title_terms: 678 desc = getEncodedWikiText(event_details[term]) 679 680 # Position details. 681 682 elif term == "geo": 683 desc = " ".join(map(str, event_details[term])) 684 685 # Text which need not be quoted, but it will be Wiki text. 686 687 elif term in event.other_terms: 688 desc = event_details[term] 689 690 replaced_terms.add(term) 691 692 # Add the replaced value. 693 694 new_body_parts.append(desc) 695 696 # Remember where in the page has been processed. 697 698 end_of_last_match = match.end() 699 700 # Write the rest of the page. 701 702 new_body_parts.append(body[end_of_last_match:]) 703 704 self.body = "".join(new_body_parts) 705 706 def flushCategoryMembership(self): 707 708 "Flush the category membership to the page body." 709 710 body = self.getBody() 711 category_names = self.getCategoryMembership() 712 match = category_membership_regexp.search(body) 713 714 if match: 715 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 716 717 def saveChanges(self): 718 719 "Save changes to the event." 720 721 self.flushEventDetails() 722 self.flushCategoryMembership() 723 self.page.saveText(self.getBody(), 0) 724 725 def linkToPage(self, request, text, query_string=None, anchor=None): 726 727 """ 728 Using 'request', return a link to this page with the given link 'text' 729 and optional 'query_string' and 'anchor'. 730 """ 731 732 return linkToPage(request, self.page, text, query_string, anchor) 733 734 # Formatting-related functions. 735 736 def getParserClass(self, format): 737 738 """ 739 Return a parser class for the given 'format', returning a plain text 740 parser if no parser can be found for the specified 'format'. 741 """ 742 743 return getParserClass(self.page.request, format) 744 745 def formatText(self, text, fmt): 746 747 """ 748 Format the given 'text' using the specified formatter 'fmt'. 749 """ 750 751 fmt.page = page = self.page 752 request = page.request 753 754 if self.getFormat() == "calendar": 755 parser_cls = RawParser 756 else: 757 parser_cls = self.getParserClass(self.getFormat()) 758 759 return formatText(text, request, fmt, parser_cls) 760 761 # Event details. 762 763 class Event(ActsAsTimespan): 764 765 "A description of an event." 766 767 title_terms = "title", "summary" 768 date_terms = "start", "end" 769 list_terms = "topics", "categories" 770 other_terms = "description", "location", "link" 771 geo_terms = "geo", 772 all_terms = title_terms + date_terms + list_terms + other_terms + geo_terms 773 774 def __init__(self, page, details, raw_details=None): 775 self.page = page 776 self.details = details 777 self.raw_details = raw_details or {} 778 779 # Permit omission of the end of the event by duplicating the start. 780 781 if self.details.has_key("start") and not self.details.get("end"): 782 end = self.details["start"] 783 784 # Make any end time refer to the day instead. 785 786 if isinstance(end, DateTime): 787 end = end.as_date() 788 789 self.details["end"] = end 790 791 def __repr__(self): 792 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 793 794 def __hash__(self): 795 796 """ 797 Return a dictionary hash, avoiding mistaken equality of events in some 798 situations (notably membership tests) by including the URL as well as 799 the summary. 800 """ 801 802 return hash(self.getSummary() + self.getEventURL()) 803 804 def getPage(self): 805 806 "Return the page describing this event." 807 808 return self.page 809 810 def setPage(self, page): 811 812 "Set the 'page' describing this event." 813 814 self.page = page 815 816 def getEventURL(self): 817 818 "Return the URL of this event." 819 820 fragment = self.details.get("fragment") 821 return self.page.getPageURL() + (fragment and "#" + fragment or "") 822 823 def linkToEvent(self, request, text, query_string=None): 824 825 """ 826 Using 'request', return a link to this event with the given link 'text' 827 and optional 'query_string'. 828 """ 829 830 return self.page.linkToPage(request, text, query_string, self.details.get("fragment")) 831 832 def getMetadata(self): 833 834 """ 835 Return a dictionary containing items describing the event's "created" 836 time, "last-modified" time, "sequence" (or revision number) and the 837 "last-comment" made about the last edit. 838 """ 839 840 # Delegate this to the page. 841 842 return self.page.getMetadata() 843 844 def getSummary(self, event_parent=None): 845 846 """ 847 Return either the given title or summary of the event according to the 848 event details, or a summary made from using the pretty version of the 849 page name. 850 851 If the optional 'event_parent' is specified, any page beneath the given 852 'event_parent' page in the page hierarchy will omit this parent information 853 if its name is used as the summary. 854 """ 855 856 event_details = self.details 857 858 if event_details.has_key("title"): 859 return event_details["title"] 860 elif event_details.has_key("summary"): 861 return event_details["summary"] 862 else: 863 # If appropriate, remove the parent details and "/" character. 864 865 title = self.page.getPageName() 866 867 if event_parent and title.startswith(event_parent): 868 title = title[len(event_parent.rstrip("/")) + 1:] 869 870 return getPrettyTitle(title) 871 872 def getDetails(self): 873 874 "Return the details for this event." 875 876 return self.details 877 878 def setDetails(self, event_details): 879 880 "Set the 'event_details' for this event." 881 882 self.details = event_details 883 884 def getRawDetails(self): 885 886 "Return the details for this event as they were written in a page." 887 888 return self.raw_details 889 890 # Timespan-related methods. 891 892 def __contains__(self, other): 893 return self == other 894 895 def __eq__(self, other): 896 if isinstance(other, Event): 897 return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other) 898 else: 899 return self._cmp(other) == 0 900 901 def __ne__(self, other): 902 return not self.__eq__(other) 903 904 def __lt__(self, other): 905 return self._cmp(other) == -1 906 907 def __le__(self, other): 908 return self._cmp(other) in (-1, 0) 909 910 def __gt__(self, other): 911 return self._cmp(other) == 1 912 913 def __ge__(self, other): 914 return self._cmp(other) in (0, 1) 915 916 def _cmp(self, other): 917 918 "Compare this event to an 'other' event purely by their timespans." 919 920 if isinstance(other, Event): 921 return cmp(self.as_timespan(), other.as_timespan()) 922 else: 923 return cmp(self.as_timespan(), other) 924 925 def as_timespan(self): 926 details = self.details 927 if details.has_key("start") and details.has_key("end"): 928 return Timespan(details["start"], details["end"]) 929 else: 930 return None 931 932 def as_limits(self): 933 ts = self.as_timespan() 934 return ts and ts.as_limits() 935 936 class CalendarEvent(Event): 937 938 "An event from a remote calendar." 939 940 def getEventURL(self): 941 942 """ 943 Return the URL of this event, fixing any misinterpreted or incorrectly 944 formatted value in the event definition or returning the resource URL in 945 the absence of any URL in the event details. 946 """ 947 948 return self.details.get("url") and \ 949 self.valueToString(self.details["url"]) or \ 950 self.page.getPageURL() 951 952 def getSummary(self, event_parent=None): 953 954 """ 955 Return the event summary, fixing any misinterpreted or incorrectly 956 formatted value in the event definition. 957 """ 958 959 return self.valueToString(self.details["summary"]) 960 961 def valueToString(self, value): 962 963 "Return the given 'value' converted to a string." 964 965 if isinstance(value, list): 966 return ",".join(value) 967 elif isinstance(value, tuple): 968 return ";".join(value) 969 else: 970 return value 971 972 def linkToEvent(self, request, text, query_string=None, anchor=None): 973 974 """ 975 Using 'request', return a link to this event with the given link 'text' 976 and optional 'query_string' and 'anchor'. 977 """ 978 979 return linkToResource(self.getEventURL(), request, text, query_string, anchor) 980 981 def getMetadata(self): 982 983 """ 984 Return a dictionary containing items describing the event's "created" 985 time, "last-modified" time, "sequence" (or revision number) and the 986 "last-comment" made about the last edit. 987 """ 988 989 metadata = self.page.getMetadata() 990 991 return { 992 "created" : self.details.get("created") or self.details.get("dtstamp") or metadata["created"], 993 "last-modified" : self.details.get("last-modified") or self.details.get("dtstamp") or metadata["last-modified"], 994 "sequence" : self.details.get("sequence") or 0, 995 "last-comment" : "" 996 } 997 998 # vim: tabstop=4 expandtab shiftwidth=4