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 14 from MoinSupport import * 15 16 from email.utils import parsedate 17 import re 18 19 try: 20 set 21 except NameError: 22 from sets import Set as set 23 24 # Page parsing. 25 26 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 27 category_membership_regexp = re.compile(ur"^\s*(?:(Category\S+)(?:\s+(Category\S+))*)\s*$", re.MULTILINE | re.UNICODE) 28 29 # Event parsing from page texts. 30 31 def parseEvents(text, event_page, fragment=None): 32 33 """ 34 Parse events in the given 'text', returning a list of event objects for the 35 given 'event_page'. An optional 'fragment' can be specified to indicate a 36 specific region of the event page. 37 38 If the optional 'fragment' identifier is provided, the first heading may 39 also be used to provide an event summary/title. 40 """ 41 42 template_details = {} 43 if fragment: 44 template_details["fragment"] = fragment 45 46 details = {} 47 details.update(template_details) 48 raw_details = {} 49 50 # Obtain a heading, if requested. 51 52 if fragment: 53 for level, title, (start, end) in getHeadings(text): 54 raw_details["title"] = text[start:end] 55 details["title"] = getSimpleWikiText(title.strip()) 56 break 57 58 # Start populating events. 59 60 events = [Event(event_page, details, raw_details)] 61 62 # Get any default raw details to modify. 63 64 raw_details = events[-1].getRawDetails() 65 66 for match in definition_list_regexp.finditer(text): 67 68 # Skip commented-out items. 69 70 if match.group("optcomment"): 71 continue 72 73 # Permit case-insensitive list terms. 74 75 term = match.group("term").lower() 76 raw_desc = match.group("desc") 77 78 # Special value type handling. 79 80 # Dates. 81 82 if term in Event.date_terms: 83 desc = getDateTime(raw_desc) 84 85 # Lists (whose elements may be quoted). 86 87 elif term in Event.list_terms: 88 desc = map(getSimpleWikiText, to_list(raw_desc, ",")) 89 90 # Position details. 91 92 elif term == "geo": 93 try: 94 desc = map(getMapReference, to_list(raw_desc, None)) 95 if len(desc) != 2: 96 continue 97 except (KeyError, ValueError): 98 continue 99 100 # Labels which may well be quoted. 101 102 elif term in Event.title_terms: 103 desc = getSimpleWikiText(raw_desc.strip()) 104 105 # Plain Wiki text terms. 106 107 elif term in Event.other_terms: 108 desc = raw_desc.strip() 109 110 else: 111 desc = raw_desc 112 113 if desc is not None: 114 115 # Handle apparent duplicates by creating a new set of 116 # details. 117 118 if details.has_key(term): 119 120 # Make a new event. 121 122 details = {} 123 details.update(template_details) 124 raw_details = {} 125 events.append(Event(event_page, details, raw_details)) 126 raw_details = events[-1].getRawDetails() 127 128 details[term] = desc 129 raw_details[term] = raw_desc 130 131 return events 132 133 # Event resources providing collections of events. 134 135 class EventResource: 136 137 "A resource providing event information." 138 139 def __init__(self, url): 140 self.url = url 141 142 def getPageURL(self): 143 144 "Return the URL of this page." 145 146 return self.url 147 148 def getFormat(self): 149 150 "Get the format used by this resource." 151 152 return "plain" 153 154 def getMetadata(self): 155 156 """ 157 Return a dictionary containing items describing the page's "created" 158 time, "last-modified" time, "sequence" (or revision number) and the 159 "last-comment" made about the last edit. 160 """ 161 162 return {} 163 164 def getEvents(self): 165 166 "Return a list of events from this resource." 167 168 return [] 169 170 def linkToPage(self, request, text, query_string=None, anchor=None): 171 172 """ 173 Using 'request', return a link to this page with the given link 'text' 174 and optional 'query_string' and 'anchor'. 175 """ 176 177 return linkToResource(self.url, request, text, query_string, anchor) 178 179 # Formatting-related functions. 180 181 def formatText(self, text, fmt): 182 183 """ 184 Format the given 'text' using the specified formatter 'fmt'. 185 """ 186 187 # Assume plain text which is then formatted appropriately. 188 189 return fmt.text(text) 190 191 class EventCalendar(EventResource): 192 193 "An iCalendar resource." 194 195 def __init__(self, url, calendar, metadata): 196 EventResource.__init__(self, url) 197 self.calendar = calendar 198 self.metadata = metadata 199 self.events = None 200 201 if not self.metadata.has_key("created") and self.metadata.has_key("date"): 202 self.metadata["created"] = DateTime(parsedate(self.metadata["date"])[:7]) 203 204 if self.metadata.has_key("last-modified") and not isinstance(self.metadata["last-modified"], DateTime): 205 self.metadata["last-modified"] = DateTime(parsedate(self.metadata["last-modified"])[:7]) 206 207 def getMetadata(self): 208 209 """ 210 Return a dictionary containing items describing the page's "created" 211 time, "last-modified" time, "sequence" (or revision number) and the 212 "last-comment" made about the last edit. 213 """ 214 215 return self.metadata 216 217 def getEvents(self): 218 219 "Return a list of events from this resource." 220 221 if self.events is None: 222 self.events = [] 223 224 _calendar, _empty, calendar = self.calendar 225 226 for objtype, attrs, obj in calendar: 227 228 # Read events. 229 230 if objtype == "VEVENT": 231 details = {} 232 233 for property, attrs, value in obj: 234 235 # Convert dates. 236 237 if property in ("DTSTART", "DTEND", "CREATED", "DTSTAMP", "LAST-MODIFIED"): 238 if property in ("DTSTART", "DTEND"): 239 property = property[2:] 240 if attrs.get("VALUE") == "DATE": 241 value = getDateFromCalendar(value) 242 if value and property == "END": 243 value = value.previous_day() 244 else: 245 value = getDateTimeFromCalendar(value) 246 247 # Convert numeric data. 248 249 elif property == "SEQUENCE": 250 value = int(value) 251 252 # Convert lists. 253 254 elif property == "CATEGORIES": 255 if not isinstance(value, list): 256 value = to_list(value, ",") 257 258 # Convert positions (using decimal values). 259 260 elif property == "GEO": 261 try: 262 value = map(getMapReferenceFromDecimal, to_list(value, ";")) 263 if len(value) != 2: 264 continue 265 except (KeyError, ValueError): 266 continue 267 268 # Accept other textual data as it is. 269 270 elif property in ("LOCATION", "SUMMARY", "URL"): 271 value = value or None 272 273 # Ignore other properties. 274 275 else: 276 continue 277 278 property = property.lower() 279 details[property] = value 280 281 self.events.append(CalendarEvent(self, details)) 282 283 return self.events 284 285 class EventPage: 286 287 "An event page acting as an event resource." 288 289 def __init__(self, page): 290 self.page = page 291 self.events = None 292 self.body = None 293 self.categories = None 294 self.metadata = None 295 296 def copyPage(self, page): 297 298 "Copy the body of the given 'page'." 299 300 self.body = page.getBody() 301 302 def getPageURL(self): 303 304 "Return the URL of this page." 305 306 return getPageURL(self.page) 307 308 def getFormat(self): 309 310 "Get the format used on this page." 311 312 return getFormat(self.page) 313 314 def getMetadata(self): 315 316 """ 317 Return a dictionary containing items describing the page's "created" 318 time, "last-modified" time, "sequence" (or revision number) and the 319 "last-comment" made about the last edit. 320 """ 321 322 if self.metadata is None: 323 self.metadata = getMetadata(self.page) 324 return self.metadata 325 326 def getRevisions(self): 327 328 "Return a list of page revisions." 329 330 return self.page.getRevList() 331 332 def getPageRevision(self): 333 334 "Return the revision details dictionary for this page." 335 336 return getPageRevision(self.page) 337 338 def getPageName(self): 339 340 "Return the page name." 341 342 return self.page.page_name 343 344 def getPrettyPageName(self): 345 346 "Return a nicely formatted title/name for this page." 347 348 return getPrettyPageName(self.page) 349 350 def getBody(self): 351 352 "Get the current page body." 353 354 if self.body is None: 355 self.body = self.page.get_raw_body() 356 return self.body 357 358 def getEvents(self): 359 360 "Return a list of events from this page." 361 362 if self.events is None: 363 self.events = [] 364 if self.getFormat() == "wiki": 365 for format, attributes, region in getFragments(self.getBody(), True): 366 self.events += parseEvents(region, self, attributes.get("fragment")) 367 368 return self.events 369 370 def setEvents(self, events): 371 372 "Set the given 'events' on this page." 373 374 self.events = events 375 376 def getCategoryMembership(self): 377 378 "Get the category names from this page." 379 380 if self.categories is None: 381 body = self.getBody() 382 match = category_membership_regexp.search(body) 383 self.categories = match and [x for x in match.groups() if x] or [] 384 385 return self.categories 386 387 def setCategoryMembership(self, category_names): 388 389 """ 390 Set the category membership for the page using the specified 391 'category_names'. 392 """ 393 394 self.categories = category_names 395 396 def flushEventDetails(self): 397 398 "Flush the current event details to this page's body text." 399 400 new_body_parts = [] 401 end_of_last_match = 0 402 body = self.getBody() 403 404 events = iter(self.getEvents()) 405 406 event = events.next() 407 event_details = event.getDetails() 408 replaced_terms = set() 409 410 for match in definition_list_regexp.finditer(body): 411 412 # Permit case-insensitive list terms. 413 414 term = match.group("term").lower() 415 desc = match.group("desc") 416 417 # Check that the term has not already been substituted. If so, 418 # get the next event. 419 420 if term in replaced_terms: 421 try: 422 event = events.next() 423 424 # No more events. 425 426 except StopIteration: 427 break 428 429 event_details = event.getDetails() 430 replaced_terms = set() 431 432 # Add preceding text to the new body. 433 434 new_body_parts.append(body[end_of_last_match:match.start()]) 435 436 # Get the matching regions, adding the term to the new body. 437 438 new_body_parts.append(match.group("wholeterm")) 439 440 # Special value type handling. 441 442 if event_details.has_key(term): 443 444 # Dates. 445 446 if term in event.date_terms: 447 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 448 449 # Lists (whose elements may be quoted). 450 451 elif term in event.list_terms: 452 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 453 454 # Labels which must be quoted. 455 456 elif term in event.title_terms: 457 desc = getEncodedWikiText(event_details[term]) 458 459 # Position details. 460 461 elif term == "geo": 462 desc = " ".join(map(str, event_details[term])) 463 464 # Text which need not be quoted, but it will be Wiki text. 465 466 elif term in event.other_terms: 467 desc = event_details[term] 468 469 replaced_terms.add(term) 470 471 # Add the replaced value. 472 473 new_body_parts.append(desc) 474 475 # Remember where in the page has been processed. 476 477 end_of_last_match = match.end() 478 479 # Write the rest of the page. 480 481 new_body_parts.append(body[end_of_last_match:]) 482 483 self.body = "".join(new_body_parts) 484 485 def flushCategoryMembership(self): 486 487 "Flush the category membership to the page body." 488 489 body = self.getBody() 490 category_names = self.getCategoryMembership() 491 match = category_membership_regexp.search(body) 492 493 if match: 494 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 495 496 def saveChanges(self): 497 498 "Save changes to the event." 499 500 self.flushEventDetails() 501 self.flushCategoryMembership() 502 self.page.saveText(self.getBody(), 0) 503 504 def linkToPage(self, request, text, query_string=None, anchor=None): 505 506 """ 507 Using 'request', return a link to this page with the given link 'text' 508 and optional 'query_string' and 'anchor'. 509 """ 510 511 return linkToPage(request, self.page, text, query_string, anchor) 512 513 # Formatting-related functions. 514 515 def getParserClass(self, format): 516 517 """ 518 Return a parser class for the given 'format', returning a plain text 519 parser if no parser can be found for the specified 'format'. 520 """ 521 522 return getParserClass(self.page.request, format) 523 524 def formatText(self, text, fmt): 525 526 """ 527 Format the given 'text' using the specified formatter 'fmt'. 528 """ 529 530 fmt.page = page = self.page 531 request = page.request 532 533 parser_cls = self.getParserClass(self.getFormat()) 534 return formatText(text, request, fmt, parser_cls) 535 536 # Event details. 537 538 class Event(ActsAsTimespan): 539 540 "A description of an event." 541 542 title_terms = "title", "summary" 543 date_terms = "start", "end" 544 list_terms = "topics", "categories" 545 other_terms = "description", "location", "link" 546 geo_terms = "geo", 547 all_terms = title_terms + date_terms + list_terms + other_terms + geo_terms 548 549 def __init__(self, page, details, raw_details=None): 550 self.page = page 551 self.details = details 552 self.raw_details = raw_details or {} 553 554 # Permit omission of the end of the event by duplicating the start. 555 556 if self.details.has_key("start") and not self.details.get("end"): 557 end = self.details["start"] 558 559 # Make any end time refer to the day instead. 560 561 if isinstance(end, DateTime): 562 end = end.as_date() 563 564 self.details["end"] = end 565 566 def __repr__(self): 567 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 568 569 def __hash__(self): 570 571 """ 572 Return a dictionary hash, avoiding mistaken equality of events in some 573 situations (notably membership tests) by including the URL as well as 574 the summary. 575 """ 576 577 return hash(self.getSummary() + self.getEventURL()) 578 579 def getPage(self): 580 581 "Return the page describing this event." 582 583 return self.page 584 585 def setPage(self, page): 586 587 "Set the 'page' describing this event." 588 589 self.page = page 590 591 def getEventURL(self): 592 593 "Return the URL of this event." 594 595 fragment = self.details.get("fragment") 596 return self.page.getPageURL() + (fragment and "#" + fragment or "") 597 598 def linkToEvent(self, request, text, query_string=None): 599 600 """ 601 Using 'request', return a link to this event with the given link 'text' 602 and optional 'query_string'. 603 """ 604 605 return self.page.linkToPage(request, text, query_string, self.details.get("fragment")) 606 607 def getMetadata(self): 608 609 """ 610 Return a dictionary containing items describing the event's "created" 611 time, "last-modified" time, "sequence" (or revision number) and the 612 "last-comment" made about the last edit. 613 """ 614 615 # Delegate this to the page. 616 617 return self.page.getMetadata() 618 619 def getSummary(self, event_parent=None): 620 621 """ 622 Return either the given title or summary of the event according to the 623 event details, or a summary made from using the pretty version of the 624 page name. 625 626 If the optional 'event_parent' is specified, any page beneath the given 627 'event_parent' page in the page hierarchy will omit this parent information 628 if its name is used as the summary. 629 """ 630 631 event_details = self.details 632 633 if event_details.has_key("title"): 634 return event_details["title"] 635 elif event_details.has_key("summary"): 636 return event_details["summary"] 637 else: 638 # If appropriate, remove the parent details and "/" character. 639 640 title = self.page.getPageName() 641 642 if event_parent and title.startswith(event_parent): 643 title = title[len(event_parent.rstrip("/")) + 1:] 644 645 return getPrettyTitle(title) 646 647 def getDetails(self): 648 649 "Return the details for this event." 650 651 return self.details 652 653 def setDetails(self, event_details): 654 655 "Set the 'event_details' for this event." 656 657 self.details = event_details 658 659 def getRawDetails(self): 660 661 "Return the details for this event as they were written in a page." 662 663 return self.raw_details 664 665 # Timespan-related methods. 666 667 def __contains__(self, other): 668 return self == other 669 670 def __eq__(self, other): 671 if isinstance(other, Event): 672 return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other) 673 else: 674 return self._cmp(other) == 0 675 676 def __ne__(self, other): 677 return not self.__eq__(other) 678 679 def __lt__(self, other): 680 return self._cmp(other) == -1 681 682 def __le__(self, other): 683 return self._cmp(other) in (-1, 0) 684 685 def __gt__(self, other): 686 return self._cmp(other) == 1 687 688 def __ge__(self, other): 689 return self._cmp(other) in (0, 1) 690 691 def _cmp(self, other): 692 693 "Compare this event to an 'other' event purely by their timespans." 694 695 if isinstance(other, Event): 696 return cmp(self.as_timespan(), other.as_timespan()) 697 else: 698 return cmp(self.as_timespan(), other) 699 700 def as_timespan(self): 701 details = self.details 702 if details.has_key("start") and details.has_key("end"): 703 return Timespan(details["start"], details["end"]) 704 else: 705 return None 706 707 def as_limits(self): 708 ts = self.as_timespan() 709 return ts and ts.as_limits() 710 711 class CalendarEvent(Event): 712 713 "An event from a remote calendar." 714 715 def getEventURL(self): 716 717 """ 718 Return the URL of this event, fixing any misinterpreted or incorrectly 719 formatted value in the event definition or returning the resource URL in 720 the absence of any URL in the event details. 721 """ 722 723 return self.details.get("url") and \ 724 self.valueToString(self.details["url"]) or \ 725 self.page.getPageURL() 726 727 def getSummary(self, event_parent=None): 728 729 """ 730 Return the event summary, fixing any misinterpreted or incorrectly 731 formatted value in the event definition. 732 """ 733 734 return self.valueToString(self.details["summary"]) 735 736 def valueToString(self, value): 737 738 "Return the given 'value' converted to a string." 739 740 if isinstance(value, list): 741 return ",".join(value) 742 elif isinstance(value, tuple): 743 return ";".join(value) 744 else: 745 return value 746 747 def linkToEvent(self, request, text, query_string=None, anchor=None): 748 749 """ 750 Using 'request', return a link to this event with the given link 'text' 751 and optional 'query_string' and 'anchor'. 752 """ 753 754 return linkToResource(self.getEventURL(), request, text, query_string, anchor) 755 756 def getMetadata(self): 757 758 """ 759 Return a dictionary containing items describing the event's "created" 760 time, "last-modified" time, "sequence" (or revision number) and the 761 "last-comment" made about the last edit. 762 """ 763 764 metadata = self.page.getMetadata() 765 766 return { 767 "created" : self.details.get("created") or self.details.get("dtstamp") or metadata["created"], 768 "last-modified" : self.details.get("last-modified") or self.details.get("dtstamp") or metadata["last-modified"], 769 "sequence" : self.details.get("sequence") or 0, 770 "last-comment" : "" 771 } 772 773 # vim: tabstop=4 expandtab shiftwidth=4