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 getMethod(self): 356 357 "Return the iTIP-related method associated with this resource." 358 359 return None 360 361 def getEvents(self): 362 363 "Return a list of events from this resource." 364 365 return self.events or [] 366 367 def linkToPage(self, request, text, query_string=None, anchor=None): 368 369 """ 370 Using 'request', return a link to this page with the given link 'text' 371 and optional 'query_string' and 'anchor'. 372 """ 373 374 return linkToResource(self.url, request, text, query_string, anchor) 375 376 # Formatting-related functions. 377 378 def formatText(self, text, fmt): 379 380 """ 381 Format the given 'text' using the specified formatter 'fmt'. 382 """ 383 384 # Assume plain text which is then formatted appropriately. 385 386 return fmt.text(text) 387 388 class EventResourceCollection(EventResource): 389 390 "A collection of resources." 391 392 def __init__(self, resources=None): 393 self.resources = resources or [] 394 395 def append(self, resource): 396 self.resources.append(resource) 397 398 def getEvents(self): 399 events = [] 400 for resource in self.resources: 401 events += resource.getEvents() 402 return events 403 404 class EventCalendarResource(EventResource): 405 406 "A generic calendar resource." 407 408 def __init__(self, url, metadata): 409 EventResource.__init__(self, url, metadata) 410 411 if not self.metadata.has_key("created") and self.metadata.has_key("date"): 412 self.metadata["created"] = DateTime(parsedate(self.metadata["date"])[:7]) 413 414 if self.metadata.has_key("last-modified") and not isinstance(self.metadata["last-modified"], DateTime): 415 self.metadata["last-modified"] = DateTime(parsedate(self.metadata["last-modified"])[:7]) 416 417 class EventCalendar(EventCalendarResource): 418 419 "An iCalendar resource." 420 421 calendar_properties = ("METHOD",) 422 423 def __init__(self, url, calendar, metadata): 424 EventCalendarResource.__init__(self, url, metadata) 425 self.calendar = calendar 426 self.properties = {} 427 428 def getMethod(self): 429 return self.properties.get("METHOD") 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 _calendar, _empty, calendar = self.calendar 439 440 for objtype, attrs, obj in calendar: 441 442 # Read events. 443 444 if objtype == "VEVENT": 445 details = {} 446 447 for property, attrs, value in obj: 448 449 # Convert dates. 450 451 if property in ("DTSTART", "DTEND", "CREATED", "DTSTAMP", "LAST-MODIFIED"): 452 if property in ("DTSTART", "DTEND"): 453 property = property[2:] 454 if attrs.get("VALUE") == "DATE": 455 value = getDateFromCalendar(value) 456 if value and property == "END": 457 value = value.previous_day() 458 else: 459 value = getDateTimeFromCalendar(value) 460 461 # Convert numeric data. 462 463 elif property == "SEQUENCE": 464 value = int(value) 465 466 # Convert lists. 467 468 elif property == "CATEGORIES": 469 if not isinstance(value, list): 470 value = to_list(value, ",") 471 472 # Convert positions (using decimal values). 473 474 elif property == "GEO": 475 try: 476 value = map(getMapReferenceFromDecimal, to_list(value, ";")) 477 if len(value) != 2: 478 continue 479 except (KeyError, ValueError): 480 continue 481 482 # Accept other textual data as it is. 483 484 elif property in ("LOCATION", "SUMMARY", "URL", "UID"): 485 value = value or None 486 487 # Ignore other properties. 488 489 else: 490 continue 491 492 property = property.lower() 493 details[property] = value 494 495 self.events.append(CalendarEvent(self, details)) 496 497 # Obtain calendar-level information. 498 499 elif objtype in self.calendar_properties: 500 self.properties[objtype] = obj 501 502 return self.events 503 504 class EventXMLCalendar(EventCalendarResource): 505 506 "An xCalendar resource." 507 508 XCAL = {"xcal" : "urn:ietf:params:xml:ns:icalendar-2.0"} 509 510 # See: http://tools.ietf.org/html/draft-daboo-et-al-icalendar-in-xml-11#section-3.4 511 512 calendar_properties = [ 513 ("method", "xcal:properties/xcal:method", "getText"), 514 ] 515 516 event_properties = [ 517 ("summary", "xcal:properties/xcal:summary", "getText"), 518 ("location", "xcal:properties/xcal:location", "getText"), 519 ("start", "xcal:properties/xcal:dtstart", "getDateTime"), 520 ("end", "xcal:properties/xcal:dtend", "getDateTime"), 521 ("created", "xcal:properties/xcal:created", "getDateTime"), 522 ("dtstamp", "xcal:properties/xcal:dtstamp", "getDateTime"), 523 ("last-modified", "xcal:properties/xcal:last-modified", "getDateTime"), 524 ("sequence", "xcal:properties/xcal:sequence", "getInteger"), 525 ("categories", "xcal:properties/xcal:categories", "getCollection"), 526 ("geo", "xcal:properties/xcal:geo", "getGeo"), 527 ("url", "xcal:properties/xcal:url", "getURI"), 528 ("uid", "xcal:properties/xcal:uid", "getText"), 529 ] 530 531 def __init__(self, url, doc, metadata): 532 EventCalendarResource.__init__(self, url, metadata) 533 self.doc = doc 534 self.properties = self.getProperties(self.calendar_properties, doc) 535 536 def getMethod(self): 537 return self.properties.get("method") 538 539 def getEvents(self): 540 541 "Return a list of events from this resource." 542 543 if self.events is None: 544 self.events = [] 545 546 for event in self.doc.xpath("//xcal:vevent", namespaces=self.XCAL): 547 details = self.getProperties(self.event_properties, event) 548 self.events.append(CalendarEvent(self, details)) 549 550 return self.events 551 552 # Retrieval methods. 553 554 def getProperties(self, definitions, root): 555 details = {} 556 557 for property, path, converter in definitions: 558 values = root.xpath(path, namespaces=self.XCAL) 559 560 try: 561 value = getattr(self, converter)(property, values) 562 details[property] = value 563 except (IndexError, ValueError), exc: 564 pass 565 566 return details 567 568 # Parsing methods. 569 570 def _getValue(self, values, type): 571 if values: 572 for element in values[0].xpath("xcal:%s" % type, namespaces=self.XCAL): 573 return element.textContent 574 return None 575 576 def getText(self, property, values): 577 return self._getValue(values, "text") 578 579 def getDateTime(self, property, values): 580 element = values[0] 581 for dtelement in element.xpath("xcal:date-time|xcal:date", namespaces=self.XCAL): 582 dt = getDateTimeFromISO8601(dtelement.textContent) 583 break 584 else: 585 return None 586 587 tzid = self._getValue(element.xpath("xcal:parameters", namespaces=self.XCAL), "tzid") 588 if tzid and isinstance(dt, DateTime): 589 zone = "/".join(tzid.rsplit("/", 2)[-2:]) 590 dt.set_time_zone(zone) 591 592 if dtelement.localName == "date" and property == "end": 593 dt = dt.previous_day() 594 595 return dt 596 597 def getInteger(self, property, values): 598 value = self._getValue(values, "integer") 599 if value is not None: 600 return int(value) 601 else: 602 return None 603 604 def getCollection(self, property, values): 605 return [n.textContent for n in values[0].xpath("xcal:text", namespaces=self.XCAL)] 606 607 def getGeo(self, property, values): 608 geo = [None, None] 609 610 for geoelement in values[0].xpath("xcal:latitude|xcal:longitude", namespaces=self.XCAL): 611 value = geoelement.textContent 612 if geoelement.localName == "latitude": 613 geo[0] = value 614 else: 615 geo[1] = value 616 617 if None not in geo: 618 return map(getMapReferenceFromDecimal, geo) 619 else: 620 return None 621 622 def getURI(self, property, values): 623 return self._getValue(values, "uri") 624 625 class EventPage: 626 627 "An event page acting as an event resource." 628 629 def __init__(self, page): 630 self.page = page 631 self.events = None 632 self.body = None 633 self.categories = None 634 self.metadata = None 635 636 def copyPage(self, page): 637 638 "Copy the body of the given 'page'." 639 640 self.body = page.getBody() 641 642 def getPageURL(self): 643 644 "Return the URL of this page." 645 646 return getPageURL(self.page) 647 648 def getFormat(self): 649 650 "Get the format used on this page." 651 652 return getFormat(self.page) 653 654 def getMetadata(self): 655 656 """ 657 Return a dictionary containing items describing the page's "created" 658 time, "last-modified" time, "sequence" (or revision number) and the 659 "last-comment" made about the last edit. 660 """ 661 662 if self.metadata is None: 663 self.metadata = getMetadata(self.page) 664 return self.metadata 665 666 def getRevisions(self): 667 668 "Return a list of page revisions." 669 670 return self.page.getRevList() 671 672 def getPageRevision(self): 673 674 "Return the revision details dictionary for this page." 675 676 return getPageRevision(self.page) 677 678 def getPageName(self): 679 680 "Return the page name." 681 682 return self.page.page_name 683 684 def getPrettyPageName(self): 685 686 "Return a nicely formatted title/name for this page." 687 688 return getPrettyPageName(self.page) 689 690 def getBody(self): 691 692 "Get the current page body." 693 694 if self.body is None: 695 self.body = self.page.get_raw_body() 696 return self.body 697 698 def getEvents(self): 699 700 "Return a list of events from this page." 701 702 if self.events is None: 703 self.events = parseEventsInPage(self.page.data, self).getEvents() 704 705 return self.events 706 707 def setEvents(self, events): 708 709 "Set the given 'events' on this page." 710 711 self.events = events 712 713 def getCategoryMembership(self): 714 715 "Get the category names from this page." 716 717 if self.categories is None: 718 body = self.getBody() 719 match = category_membership_regexp.search(body) 720 self.categories = match and [x for x in match.groups() if x] or [] 721 722 return self.categories 723 724 def setCategoryMembership(self, category_names): 725 726 """ 727 Set the category membership for the page using the specified 728 'category_names'. 729 """ 730 731 self.categories = category_names 732 733 def flushEventDetails(self): 734 735 "Flush the current event details to this page's body text." 736 737 new_body_parts = [] 738 end_of_last_match = 0 739 body = self.getBody() 740 741 events = iter(self.getEvents()) 742 743 event = events.next() 744 event_details = event.getDetails() 745 replaced_terms = set() 746 747 for match in definition_list_regexp.finditer(body): 748 749 # Permit case-insensitive list terms. 750 751 term = match.group("term").lower() 752 desc = match.group("desc") 753 754 # Check that the term has not already been substituted. If so, 755 # get the next event. 756 757 if term in replaced_terms: 758 try: 759 event = events.next() 760 761 # No more events. 762 763 except StopIteration: 764 break 765 766 event_details = event.getDetails() 767 replaced_terms = set() 768 769 # Add preceding text to the new body. 770 771 new_body_parts.append(body[end_of_last_match:match.start()]) 772 773 # Get the matching regions, adding the term to the new body. 774 775 new_body_parts.append(match.group("wholeterm")) 776 777 # Special value type handling. 778 779 if event_details.has_key(term): 780 781 # Dates. 782 783 if term in event.date_terms: 784 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 785 786 # Lists (whose elements may be quoted). 787 788 elif term in event.list_terms: 789 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 790 791 # Labels which must be quoted. 792 793 elif term in event.title_terms: 794 desc = getEncodedWikiText(event_details[term]) 795 796 # Position details. 797 798 elif term == "geo": 799 desc = " ".join(map(str, event_details[term])) 800 801 # Text which need not be quoted, but it will be Wiki text. 802 803 elif term in event.other_terms: 804 desc = event_details[term] 805 806 replaced_terms.add(term) 807 808 # Add the replaced value. 809 810 new_body_parts.append(desc) 811 812 # Remember where in the page has been processed. 813 814 end_of_last_match = match.end() 815 816 # Write the rest of the page. 817 818 new_body_parts.append(body[end_of_last_match:]) 819 820 self.body = "".join(new_body_parts) 821 822 def flushCategoryMembership(self): 823 824 "Flush the category membership to the page body." 825 826 body = self.getBody() 827 category_names = self.getCategoryMembership() 828 match = category_membership_regexp.search(body) 829 830 if match: 831 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 832 833 def saveChanges(self): 834 835 "Save changes to the event." 836 837 self.flushEventDetails() 838 self.flushCategoryMembership() 839 self.page.saveText(self.getBody(), 0) 840 841 def linkToPage(self, request, text, query_string=None, anchor=None): 842 843 """ 844 Using 'request', return a link to this page with the given link 'text' 845 and optional 'query_string' and 'anchor'. 846 """ 847 848 return linkToPage(request, self.page, text, query_string, anchor) 849 850 # Formatting-related functions. 851 852 def getParserClass(self, format): 853 854 """ 855 Return a parser class for the given 'format', returning a plain text 856 parser if no parser can be found for the specified 'format'. 857 """ 858 859 return getParserClass(self.page.request, format) 860 861 def formatText(self, text, fmt): 862 863 """ 864 Format the given 'text' using the specified formatter 'fmt'. 865 """ 866 867 fmt.page = page = self.page 868 request = page.request 869 870 if self.getFormat() == "calendar": 871 parser_cls = RawParser 872 else: 873 parser_cls = self.getParserClass(self.getFormat()) 874 875 return formatText(text, request, fmt, parser_cls) 876 877 # Event details. 878 879 class Event(ActsAsTimespan): 880 881 "A description of an event." 882 883 title_terms = "title", "summary" 884 date_terms = "start", "end" 885 list_terms = "topics", "categories" 886 other_terms = "description", "location", "link" 887 geo_terms = "geo", 888 all_terms = title_terms + date_terms + list_terms + other_terms + geo_terms 889 890 def __init__(self, page, details, raw_details=None): 891 self.page = page 892 self.details = details 893 self.raw_details = raw_details or {} 894 895 # Permit omission of the end of the event by duplicating the start. 896 897 if self.details.has_key("start") and not self.details.get("end"): 898 end = self.details["start"] 899 900 # Make any end time refer to the day instead. 901 902 if isinstance(end, DateTime): 903 end = end.as_date() 904 905 self.details["end"] = end 906 907 def __repr__(self): 908 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 909 910 def __hash__(self): 911 912 """ 913 Return a dictionary hash, avoiding mistaken equality of events in some 914 situations (notably membership tests) by including the URL as well as 915 the summary. 916 """ 917 918 return hash(self.getSummary() + self.getEventURL()) 919 920 def getPage(self): 921 922 "Return the page describing this event." 923 924 return self.page 925 926 def setPage(self, page): 927 928 "Set the 'page' describing this event." 929 930 self.page = page 931 932 def getEventUID(self): 933 934 "Return the UID of this event." 935 936 return self.details.get("uid") or self.getEventURL() 937 938 def getSequence(self): 939 940 "Return the sequence number of this event." 941 942 return self.details.get("sequence") or 0 943 944 def getEventURL(self): 945 946 "Return the URL of this event." 947 948 fragment = self.details.get("fragment") 949 return self.page.getPageURL() + (fragment and "#" + fragment or "") 950 951 def linkToEvent(self, request, text, query_string=None): 952 953 """ 954 Using 'request', return a link to this event with the given link 'text' 955 and optional 'query_string'. 956 """ 957 958 return self.page.linkToPage(request, text, query_string, self.details.get("fragment")) 959 960 def getMetadata(self): 961 962 """ 963 Return a dictionary containing items describing the event's "created" 964 time, "last-modified" time, "sequence" (or revision number) and the 965 "last-comment" made about the last edit. 966 """ 967 968 # Delegate this to the page. 969 970 return self.page.getMetadata() 971 972 def getSummary(self, event_parent=None): 973 974 """ 975 Return either the given title or summary of the event according to the 976 event details, or a summary made from using the pretty version of the 977 page name. 978 979 If the optional 'event_parent' is specified, any page beneath the given 980 'event_parent' page in the page hierarchy will omit this parent information 981 if its name is used as the summary. 982 """ 983 984 event_details = self.details 985 986 if event_details.has_key("title"): 987 return event_details["title"] 988 elif event_details.has_key("summary"): 989 return event_details["summary"] 990 else: 991 # If appropriate, remove the parent details and "/" character. 992 993 title = self.page.getPageName() 994 995 if event_parent and title.startswith(event_parent): 996 title = title[len(event_parent.rstrip("/")) + 1:] 997 998 return getPrettyTitle(title) 999 1000 def getDetails(self): 1001 1002 "Return the details for this event." 1003 1004 return self.details 1005 1006 def setDetails(self, event_details): 1007 1008 "Set the 'event_details' for this event." 1009 1010 self.details = event_details 1011 1012 def getRawDetails(self): 1013 1014 "Return the details for this event as they were written in a page." 1015 1016 return self.raw_details 1017 1018 # Timespan-related methods. 1019 1020 def __contains__(self, other): 1021 return self == other 1022 1023 def __eq__(self, other): 1024 if isinstance(other, Event): 1025 return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other) 1026 else: 1027 return self._cmp(other) == 0 1028 1029 def __ne__(self, other): 1030 return not self.__eq__(other) 1031 1032 def __lt__(self, other): 1033 return self._cmp(other) == -1 1034 1035 def __le__(self, other): 1036 return self._cmp(other) in (-1, 0) 1037 1038 def __gt__(self, other): 1039 return self._cmp(other) == 1 1040 1041 def __ge__(self, other): 1042 return self._cmp(other) in (0, 1) 1043 1044 def _cmp(self, other): 1045 1046 "Compare this event to an 'other' event purely by their timespans." 1047 1048 if isinstance(other, Event): 1049 return cmp(self.as_timespan(), other.as_timespan()) 1050 else: 1051 return cmp(self.as_timespan(), other) 1052 1053 def as_timespan(self): 1054 details = self.details 1055 if details.has_key("start") and details.has_key("end"): 1056 return Timespan(details["start"], details["end"]) 1057 else: 1058 return None 1059 1060 def as_limits(self): 1061 ts = self.as_timespan() 1062 return ts and ts.as_limits() 1063 1064 class CalendarEvent(Event): 1065 1066 "An event from a remote calendar." 1067 1068 def getEventURL(self): 1069 1070 """ 1071 Return the URL of this event, fixing any misinterpreted or incorrectly 1072 formatted value in the event definition or returning the resource URL in 1073 the absence of any URL in the event details. 1074 """ 1075 1076 # NOTE: Redirect empty URLs to an action showing the resource details. 1077 1078 return self.details.get("url") and \ 1079 self.valueToString(self.details["url"]) or \ 1080 self.page.getPageURL() 1081 1082 def getSummary(self, event_parent=None): 1083 1084 """ 1085 Return the event summary, fixing any misinterpreted or incorrectly 1086 formatted value in the event definition. 1087 """ 1088 1089 return self.valueToString(self.details["summary"]) 1090 1091 def valueToString(self, value): 1092 1093 "Return the given 'value' converted to a string." 1094 1095 if isinstance(value, list): 1096 return ",".join(value) 1097 elif isinstance(value, tuple): 1098 return ";".join(value) 1099 else: 1100 return value 1101 1102 def linkToEvent(self, request, text, query_string=None, anchor=None): 1103 1104 """ 1105 Using 'request', return a link to this event with the given link 'text' 1106 and optional 'query_string' and 'anchor'. 1107 """ 1108 1109 return linkToResource(self.getEventURL(), request, text, query_string, anchor) 1110 1111 def getMetadata(self): 1112 1113 """ 1114 Return a dictionary containing items describing the event's "created" 1115 time, "last-modified" time, "sequence" (or revision number) and the 1116 "last-comment" made about the last edit. 1117 """ 1118 1119 metadata = self.page.getMetadata() 1120 1121 return { 1122 "created" : self.details.get("created") or self.details.get("dtstamp") or metadata["created"], 1123 "last-modified" : self.details.get("last-modified") or self.details.get("dtstamp") or metadata["last-modified"], 1124 "sequence" : self.getSequence(), 1125 "last-comment" : "" 1126 } 1127 1128 # vim: tabstop=4 expandtab shiftwidth=4