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