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"): 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 ] 529 530 def __init__(self, url, doc, metadata): 531 EventCalendarResource.__init__(self, url, metadata) 532 self.doc = doc 533 self.properties = self.getProperties(self.calendar_properties, doc) 534 535 def getMethod(self): 536 return self.properties.get("method") 537 538 def getEvents(self): 539 540 "Return a list of events from this resource." 541 542 if self.events is None: 543 self.events = [] 544 545 for event in self.doc.xpath("//xcal:vevent", namespaces=self.XCAL): 546 details = self.getProperties(self.event_properties, event) 547 self.events.append(CalendarEvent(self, details)) 548 549 return self.events 550 551 # Retrieval methods. 552 553 def getProperties(self, definitions, root): 554 details = {} 555 556 for property, path, converter in definitions: 557 values = root.xpath(path, namespaces=self.XCAL) 558 559 try: 560 value = getattr(self, converter)(property, values) 561 details[property] = value 562 except (IndexError, ValueError), exc: 563 pass 564 565 return details 566 567 # Parsing methods. 568 569 def _getValue(self, values, type): 570 if values: 571 for element in values[0].xpath("xcal:%s" % type, namespaces=self.XCAL): 572 return element.textContent 573 return None 574 575 def getText(self, property, values): 576 return self._getValue(values, "text") 577 578 def getDateTime(self, property, values): 579 element = values[0] 580 for dtelement in element.xpath("xcal:date-time|xcal:date", namespaces=self.XCAL): 581 dt = getDateTimeFromISO8601(dtelement.textContent) 582 break 583 else: 584 return None 585 586 tzid = self._getValue(element.xpath("xcal:parameters", namespaces=self.XCAL), "tzid") 587 if tzid and isinstance(dt, DateTime): 588 zone = "/".join(tzid.rsplit("/", 2)[-2:]) 589 dt.set_time_zone(zone) 590 591 if dtelement.localName == "date" and property == "end": 592 dt = dt.previous_day() 593 594 return dt 595 596 def getInteger(self, property, values): 597 value = self._getValue(values, "integer") 598 if value is not None: 599 return int(value) 600 else: 601 return None 602 603 def getCollection(self, property, values): 604 return [n.textContent for n in values[0].xpath("xcal:text", namespaces=self.XCAL)] 605 606 def getGeo(self, property, values): 607 geo = [None, None] 608 609 for geoelement in values[0].xpath("xcal:latitude|xcal:longitude", namespaces=self.XCAL): 610 value = geoelement.textContent 611 if geoelement.localName == "latitude": 612 geo[0] = value 613 else: 614 geo[1] = value 615 616 if None not in geo: 617 return map(getMapReferenceFromDecimal, geo) 618 else: 619 return None 620 621 def getURI(self, property, values): 622 return self._getValue(values, "uri") 623 624 class EventPage: 625 626 "An event page acting as an event resource." 627 628 def __init__(self, page): 629 self.page = page 630 self.events = None 631 self.body = None 632 self.categories = None 633 self.metadata = None 634 635 def copyPage(self, page): 636 637 "Copy the body of the given 'page'." 638 639 self.body = page.getBody() 640 641 def getPageURL(self): 642 643 "Return the URL of this page." 644 645 return getPageURL(self.page) 646 647 def getFormat(self): 648 649 "Get the format used on this page." 650 651 return getFormat(self.page) 652 653 def getMetadata(self): 654 655 """ 656 Return a dictionary containing items describing the page's "created" 657 time, "last-modified" time, "sequence" (or revision number) and the 658 "last-comment" made about the last edit. 659 """ 660 661 if self.metadata is None: 662 self.metadata = getMetadata(self.page) 663 return self.metadata 664 665 def getRevisions(self): 666 667 "Return a list of page revisions." 668 669 return self.page.getRevList() 670 671 def getPageRevision(self): 672 673 "Return the revision details dictionary for this page." 674 675 return getPageRevision(self.page) 676 677 def getPageName(self): 678 679 "Return the page name." 680 681 return self.page.page_name 682 683 def getPrettyPageName(self): 684 685 "Return a nicely formatted title/name for this page." 686 687 return getPrettyPageName(self.page) 688 689 def getBody(self): 690 691 "Get the current page body." 692 693 if self.body is None: 694 self.body = self.page.get_raw_body() 695 return self.body 696 697 def getEvents(self): 698 699 "Return a list of events from this page." 700 701 if self.events is None: 702 self.events = parseEventsInPage(self.page.data, self).getEvents() 703 704 return self.events 705 706 def setEvents(self, events): 707 708 "Set the given 'events' on this page." 709 710 self.events = events 711 712 def getCategoryMembership(self): 713 714 "Get the category names from this page." 715 716 if self.categories is None: 717 body = self.getBody() 718 match = category_membership_regexp.search(body) 719 self.categories = match and [x for x in match.groups() if x] or [] 720 721 return self.categories 722 723 def setCategoryMembership(self, category_names): 724 725 """ 726 Set the category membership for the page using the specified 727 'category_names'. 728 """ 729 730 self.categories = category_names 731 732 def flushEventDetails(self): 733 734 "Flush the current event details to this page's body text." 735 736 new_body_parts = [] 737 end_of_last_match = 0 738 body = self.getBody() 739 740 events = iter(self.getEvents()) 741 742 event = events.next() 743 event_details = event.getDetails() 744 replaced_terms = set() 745 746 for match in definition_list_regexp.finditer(body): 747 748 # Permit case-insensitive list terms. 749 750 term = match.group("term").lower() 751 desc = match.group("desc") 752 753 # Check that the term has not already been substituted. If so, 754 # get the next event. 755 756 if term in replaced_terms: 757 try: 758 event = events.next() 759 760 # No more events. 761 762 except StopIteration: 763 break 764 765 event_details = event.getDetails() 766 replaced_terms = set() 767 768 # Add preceding text to the new body. 769 770 new_body_parts.append(body[end_of_last_match:match.start()]) 771 772 # Get the matching regions, adding the term to the new body. 773 774 new_body_parts.append(match.group("wholeterm")) 775 776 # Special value type handling. 777 778 if event_details.has_key(term): 779 780 # Dates. 781 782 if term in event.date_terms: 783 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 784 785 # Lists (whose elements may be quoted). 786 787 elif term in event.list_terms: 788 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 789 790 # Labels which must be quoted. 791 792 elif term in event.title_terms: 793 desc = getEncodedWikiText(event_details[term]) 794 795 # Position details. 796 797 elif term == "geo": 798 desc = " ".join(map(str, event_details[term])) 799 800 # Text which need not be quoted, but it will be Wiki text. 801 802 elif term in event.other_terms: 803 desc = event_details[term] 804 805 replaced_terms.add(term) 806 807 # Add the replaced value. 808 809 new_body_parts.append(desc) 810 811 # Remember where in the page has been processed. 812 813 end_of_last_match = match.end() 814 815 # Write the rest of the page. 816 817 new_body_parts.append(body[end_of_last_match:]) 818 819 self.body = "".join(new_body_parts) 820 821 def flushCategoryMembership(self): 822 823 "Flush the category membership to the page body." 824 825 body = self.getBody() 826 category_names = self.getCategoryMembership() 827 match = category_membership_regexp.search(body) 828 829 if match: 830 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 831 832 def saveChanges(self): 833 834 "Save changes to the event." 835 836 self.flushEventDetails() 837 self.flushCategoryMembership() 838 self.page.saveText(self.getBody(), 0) 839 840 def linkToPage(self, request, text, query_string=None, anchor=None): 841 842 """ 843 Using 'request', return a link to this page with the given link 'text' 844 and optional 'query_string' and 'anchor'. 845 """ 846 847 return linkToPage(request, self.page, text, query_string, anchor) 848 849 # Formatting-related functions. 850 851 def getParserClass(self, format): 852 853 """ 854 Return a parser class for the given 'format', returning a plain text 855 parser if no parser can be found for the specified 'format'. 856 """ 857 858 return getParserClass(self.page.request, format) 859 860 def formatText(self, text, fmt): 861 862 """ 863 Format the given 'text' using the specified formatter 'fmt'. 864 """ 865 866 fmt.page = page = self.page 867 request = page.request 868 869 if self.getFormat() == "calendar": 870 parser_cls = RawParser 871 else: 872 parser_cls = self.getParserClass(self.getFormat()) 873 874 return formatText(text, request, fmt, parser_cls) 875 876 # Event details. 877 878 class Event(ActsAsTimespan): 879 880 "A description of an event." 881 882 title_terms = "title", "summary" 883 date_terms = "start", "end" 884 list_terms = "topics", "categories" 885 other_terms = "description", "location", "link" 886 geo_terms = "geo", 887 all_terms = title_terms + date_terms + list_terms + other_terms + geo_terms 888 889 def __init__(self, page, details, raw_details=None): 890 self.page = page 891 self.details = details 892 self.raw_details = raw_details or {} 893 894 # Permit omission of the end of the event by duplicating the start. 895 896 if self.details.has_key("start") and not self.details.get("end"): 897 end = self.details["start"] 898 899 # Make any end time refer to the day instead. 900 901 if isinstance(end, DateTime): 902 end = end.as_date() 903 904 self.details["end"] = end 905 906 def __repr__(self): 907 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 908 909 def __hash__(self): 910 911 """ 912 Return a dictionary hash, avoiding mistaken equality of events in some 913 situations (notably membership tests) by including the URL as well as 914 the summary. 915 """ 916 917 return hash(self.getSummary() + self.getEventURL()) 918 919 def getPage(self): 920 921 "Return the page describing this event." 922 923 return self.page 924 925 def setPage(self, page): 926 927 "Set the 'page' describing this event." 928 929 self.page = page 930 931 def getEventURL(self): 932 933 "Return the URL of this event." 934 935 fragment = self.details.get("fragment") 936 return self.page.getPageURL() + (fragment and "#" + fragment or "") 937 938 def linkToEvent(self, request, text, query_string=None): 939 940 """ 941 Using 'request', return a link to this event with the given link 'text' 942 and optional 'query_string'. 943 """ 944 945 return self.page.linkToPage(request, text, query_string, self.details.get("fragment")) 946 947 def getMetadata(self): 948 949 """ 950 Return a dictionary containing items describing the event's "created" 951 time, "last-modified" time, "sequence" (or revision number) and the 952 "last-comment" made about the last edit. 953 """ 954 955 # Delegate this to the page. 956 957 return self.page.getMetadata() 958 959 def getSummary(self, event_parent=None): 960 961 """ 962 Return either the given title or summary of the event according to the 963 event details, or a summary made from using the pretty version of the 964 page name. 965 966 If the optional 'event_parent' is specified, any page beneath the given 967 'event_parent' page in the page hierarchy will omit this parent information 968 if its name is used as the summary. 969 """ 970 971 event_details = self.details 972 973 if event_details.has_key("title"): 974 return event_details["title"] 975 elif event_details.has_key("summary"): 976 return event_details["summary"] 977 else: 978 # If appropriate, remove the parent details and "/" character. 979 980 title = self.page.getPageName() 981 982 if event_parent and title.startswith(event_parent): 983 title = title[len(event_parent.rstrip("/")) + 1:] 984 985 return getPrettyTitle(title) 986 987 def getDetails(self): 988 989 "Return the details for this event." 990 991 return self.details 992 993 def setDetails(self, event_details): 994 995 "Set the 'event_details' for this event." 996 997 self.details = event_details 998 999 def getRawDetails(self): 1000 1001 "Return the details for this event as they were written in a page." 1002 1003 return self.raw_details 1004 1005 # Timespan-related methods. 1006 1007 def __contains__(self, other): 1008 return self == other 1009 1010 def __eq__(self, other): 1011 if isinstance(other, Event): 1012 return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other) 1013 else: 1014 return self._cmp(other) == 0 1015 1016 def __ne__(self, other): 1017 return not self.__eq__(other) 1018 1019 def __lt__(self, other): 1020 return self._cmp(other) == -1 1021 1022 def __le__(self, other): 1023 return self._cmp(other) in (-1, 0) 1024 1025 def __gt__(self, other): 1026 return self._cmp(other) == 1 1027 1028 def __ge__(self, other): 1029 return self._cmp(other) in (0, 1) 1030 1031 def _cmp(self, other): 1032 1033 "Compare this event to an 'other' event purely by their timespans." 1034 1035 if isinstance(other, Event): 1036 return cmp(self.as_timespan(), other.as_timespan()) 1037 else: 1038 return cmp(self.as_timespan(), other) 1039 1040 def as_timespan(self): 1041 details = self.details 1042 if details.has_key("start") and details.has_key("end"): 1043 return Timespan(details["start"], details["end"]) 1044 else: 1045 return None 1046 1047 def as_limits(self): 1048 ts = self.as_timespan() 1049 return ts and ts.as_limits() 1050 1051 class CalendarEvent(Event): 1052 1053 "An event from a remote calendar." 1054 1055 def getEventURL(self): 1056 1057 """ 1058 Return the URL of this event, fixing any misinterpreted or incorrectly 1059 formatted value in the event definition or returning the resource URL in 1060 the absence of any URL in the event details. 1061 """ 1062 1063 # NOTE: Redirect empty URLs to an action showing the resource details. 1064 1065 return self.details.get("url") and \ 1066 self.valueToString(self.details["url"]) or \ 1067 self.page.getPageURL() 1068 1069 def getSummary(self, event_parent=None): 1070 1071 """ 1072 Return the event summary, fixing any misinterpreted or incorrectly 1073 formatted value in the event definition. 1074 """ 1075 1076 return self.valueToString(self.details["summary"]) 1077 1078 def valueToString(self, value): 1079 1080 "Return the given 'value' converted to a string." 1081 1082 if isinstance(value, list): 1083 return ",".join(value) 1084 elif isinstance(value, tuple): 1085 return ";".join(value) 1086 else: 1087 return value 1088 1089 def linkToEvent(self, request, text, query_string=None, anchor=None): 1090 1091 """ 1092 Using 'request', return a link to this event with the given link 'text' 1093 and optional 'query_string' and 'anchor'. 1094 """ 1095 1096 return linkToResource(self.getEventURL(), request, text, query_string, anchor) 1097 1098 def getMetadata(self): 1099 1100 """ 1101 Return a dictionary containing items describing the event's "created" 1102 time, "last-modified" time, "sequence" (or revision number) and the 1103 "last-comment" made about the last edit. 1104 """ 1105 1106 metadata = self.page.getMetadata() 1107 1108 return { 1109 "created" : self.details.get("created") or self.details.get("dtstamp") or metadata["created"], 1110 "last-modified" : self.details.get("last-modified") or self.details.get("dtstamp") or metadata["last-modified"], 1111 "sequence" : self.details.get("sequence") or 0, 1112 "last-comment" : "" 1113 } 1114 1115 # vim: tabstop=4 expandtab shiftwidth=4