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