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