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