1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator object types 4 5 @copyright: 2008, 2009, 2010, 2011, 2012, 2013 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 value = to_list(value, ",") 256 257 # Convert positions (using decimal values). 258 259 elif property == "GEO": 260 try: 261 value = map(getMapReferenceFromDecimal, to_list(value, ";")) 262 if len(value) != 2: 263 continue 264 except (KeyError, ValueError): 265 continue 266 267 # Accept other textual data as it is. 268 269 elif property in ("LOCATION", "SUMMARY", "URL"): 270 value = value or None 271 272 # Ignore other properties. 273 274 else: 275 continue 276 277 property = property.lower() 278 details[property] = value 279 280 self.events.append(CalendarEvent(self, details)) 281 282 return self.events 283 284 class EventPage: 285 286 "An event page acting as an event resource." 287 288 def __init__(self, page): 289 self.page = page 290 self.events = None 291 self.body = None 292 self.categories = None 293 self.metadata = None 294 295 def copyPage(self, page): 296 297 "Copy the body of the given 'page'." 298 299 self.body = page.getBody() 300 301 def getPageURL(self): 302 303 "Return the URL of this page." 304 305 return getPageURL(self.page) 306 307 def getFormat(self): 308 309 "Get the format used on this page." 310 311 return getFormat(self.page) 312 313 def getMetadata(self): 314 315 """ 316 Return a dictionary containing items describing the page's "created" 317 time, "last-modified" time, "sequence" (or revision number) and the 318 "last-comment" made about the last edit. 319 """ 320 321 if self.metadata is None: 322 self.metadata = getMetadata(self.page) 323 return self.metadata 324 325 def getRevisions(self): 326 327 "Return a list of page revisions." 328 329 return self.page.getRevList() 330 331 def getPageRevision(self): 332 333 "Return the revision details dictionary for this page." 334 335 return getPageRevision(self.page) 336 337 def getPageName(self): 338 339 "Return the page name." 340 341 return self.page.page_name 342 343 def getPrettyPageName(self): 344 345 "Return a nicely formatted title/name for this page." 346 347 return getPrettyPageName(self.page) 348 349 def getBody(self): 350 351 "Get the current page body." 352 353 if self.body is None: 354 self.body = self.page.get_raw_body() 355 return self.body 356 357 def getEvents(self): 358 359 "Return a list of events from this page." 360 361 if self.events is None: 362 self.events = [] 363 if self.getFormat() == "wiki": 364 for format, attributes, region in getFragments(self.getBody(), True): 365 self.events += parseEvents(region, self, attributes.get("fragment")) 366 367 return self.events 368 369 def setEvents(self, events): 370 371 "Set the given 'events' on this page." 372 373 self.events = events 374 375 def getCategoryMembership(self): 376 377 "Get the category names from this page." 378 379 if self.categories is None: 380 body = self.getBody() 381 match = category_membership_regexp.search(body) 382 self.categories = match and [x for x in match.groups() if x] or [] 383 384 return self.categories 385 386 def setCategoryMembership(self, category_names): 387 388 """ 389 Set the category membership for the page using the specified 390 'category_names'. 391 """ 392 393 self.categories = category_names 394 395 def flushEventDetails(self): 396 397 "Flush the current event details to this page's body text." 398 399 new_body_parts = [] 400 end_of_last_match = 0 401 body = self.getBody() 402 403 events = iter(self.getEvents()) 404 405 event = events.next() 406 event_details = event.getDetails() 407 replaced_terms = set() 408 409 for match in definition_list_regexp.finditer(body): 410 411 # Permit case-insensitive list terms. 412 413 term = match.group("term").lower() 414 desc = match.group("desc") 415 416 # Check that the term has not already been substituted. If so, 417 # get the next event. 418 419 if term in replaced_terms: 420 try: 421 event = events.next() 422 423 # No more events. 424 425 except StopIteration: 426 break 427 428 event_details = event.getDetails() 429 replaced_terms = set() 430 431 # Add preceding text to the new body. 432 433 new_body_parts.append(body[end_of_last_match:match.start()]) 434 435 # Get the matching regions, adding the term to the new body. 436 437 new_body_parts.append(match.group("wholeterm")) 438 439 # Special value type handling. 440 441 if event_details.has_key(term): 442 443 # Dates. 444 445 if term in event.date_terms: 446 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 447 448 # Lists (whose elements may be quoted). 449 450 elif term in event.list_terms: 451 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 452 453 # Labels which must be quoted. 454 455 elif term in event.title_terms: 456 desc = getEncodedWikiText(event_details[term]) 457 458 # Position details. 459 460 elif term == "geo": 461 desc = " ".join(map(str, event_details[term])) 462 463 # Text which need not be quoted, but it will be Wiki text. 464 465 elif term in event.other_terms: 466 desc = event_details[term] 467 468 replaced_terms.add(term) 469 470 # Add the replaced value. 471 472 new_body_parts.append(desc) 473 474 # Remember where in the page has been processed. 475 476 end_of_last_match = match.end() 477 478 # Write the rest of the page. 479 480 new_body_parts.append(body[end_of_last_match:]) 481 482 self.body = "".join(new_body_parts) 483 484 def flushCategoryMembership(self): 485 486 "Flush the category membership to the page body." 487 488 body = self.getBody() 489 category_names = self.getCategoryMembership() 490 match = category_membership_regexp.search(body) 491 492 if match: 493 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 494 495 def saveChanges(self): 496 497 "Save changes to the event." 498 499 self.flushEventDetails() 500 self.flushCategoryMembership() 501 self.page.saveText(self.getBody(), 0) 502 503 def linkToPage(self, request, text, query_string=None, anchor=None): 504 505 """ 506 Using 'request', return a link to this page with the given link 'text' 507 and optional 'query_string' and 'anchor'. 508 """ 509 510 return linkToPage(request, self.page, text, query_string, anchor) 511 512 # Formatting-related functions. 513 514 def getParserClass(self, format): 515 516 """ 517 Return a parser class for the given 'format', returning a plain text 518 parser if no parser can be found for the specified 'format'. 519 """ 520 521 return getParserClass(self.page.request, format) 522 523 def formatText(self, text, fmt): 524 525 """ 526 Format the given 'text' using the specified formatter 'fmt'. 527 """ 528 529 fmt.page = page = self.page 530 request = page.request 531 532 parser_cls = self.getParserClass(self.getFormat()) 533 return formatText(text, request, fmt, parser_cls) 534 535 # Event details. 536 537 class Event(ActsAsTimespan): 538 539 "A description of an event." 540 541 title_terms = "title", "summary" 542 date_terms = "start", "end" 543 list_terms = "topics", "categories" 544 other_terms = "description", "location", "link" 545 geo_terms = "geo", 546 all_terms = title_terms + date_terms + list_terms + other_terms + geo_terms 547 548 def __init__(self, page, details, raw_details=None): 549 self.page = page 550 self.details = details 551 self.raw_details = raw_details or {} 552 553 # Permit omission of the end of the event by duplicating the start. 554 555 if self.details.has_key("start") and not self.details.get("end"): 556 end = self.details["start"] 557 558 # Make any end time refer to the day instead. 559 560 if isinstance(end, DateTime): 561 end = end.as_date() 562 563 self.details["end"] = end 564 565 def __repr__(self): 566 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 567 568 def __hash__(self): 569 570 """ 571 Return a dictionary hash, avoiding mistaken equality of events in some 572 situations (notably membership tests) by including the URL as well as 573 the summary. 574 """ 575 576 return hash(self.getSummary() + self.getEventURL()) 577 578 def getPage(self): 579 580 "Return the page describing this event." 581 582 return self.page 583 584 def setPage(self, page): 585 586 "Set the 'page' describing this event." 587 588 self.page = page 589 590 def getEventURL(self): 591 592 "Return the URL of this event." 593 594 fragment = self.details.get("fragment") 595 return self.page.getPageURL() + (fragment and "#" + fragment or "") 596 597 def linkToEvent(self, request, text, query_string=None): 598 599 """ 600 Using 'request', return a link to this event with the given link 'text' 601 and optional 'query_string'. 602 """ 603 604 return self.page.linkToPage(request, text, query_string, self.details.get("fragment")) 605 606 def getMetadata(self): 607 608 """ 609 Return a dictionary containing items describing the event's "created" 610 time, "last-modified" time, "sequence" (or revision number) and the 611 "last-comment" made about the last edit. 612 """ 613 614 # Delegate this to the page. 615 616 return self.page.getMetadata() 617 618 def getSummary(self, event_parent=None): 619 620 """ 621 Return either the given title or summary of the event according to the 622 event details, or a summary made from using the pretty version of the 623 page name. 624 625 If the optional 'event_parent' is specified, any page beneath the given 626 'event_parent' page in the page hierarchy will omit this parent information 627 if its name is used as the summary. 628 """ 629 630 event_details = self.details 631 632 if event_details.has_key("title"): 633 return event_details["title"] 634 elif event_details.has_key("summary"): 635 return event_details["summary"] 636 else: 637 # If appropriate, remove the parent details and "/" character. 638 639 title = self.page.getPageName() 640 641 if event_parent and title.startswith(event_parent): 642 title = title[len(event_parent.rstrip("/")) + 1:] 643 644 return getPrettyTitle(title) 645 646 def getDetails(self): 647 648 "Return the details for this event." 649 650 return self.details 651 652 def setDetails(self, event_details): 653 654 "Set the 'event_details' for this event." 655 656 self.details = event_details 657 658 def getRawDetails(self): 659 660 "Return the details for this event as they were written in a page." 661 662 return self.raw_details 663 664 # Timespan-related methods. 665 666 def __contains__(self, other): 667 return self == other 668 669 def __eq__(self, other): 670 if isinstance(other, Event): 671 return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other) 672 else: 673 return self._cmp(other) == 0 674 675 def __ne__(self, other): 676 return not self.__eq__(other) 677 678 def __lt__(self, other): 679 return self._cmp(other) == -1 680 681 def __le__(self, other): 682 return self._cmp(other) in (-1, 0) 683 684 def __gt__(self, other): 685 return self._cmp(other) == 1 686 687 def __ge__(self, other): 688 return self._cmp(other) in (0, 1) 689 690 def _cmp(self, other): 691 692 "Compare this event to an 'other' event purely by their timespans." 693 694 if isinstance(other, Event): 695 return cmp(self.as_timespan(), other.as_timespan()) 696 else: 697 return cmp(self.as_timespan(), other) 698 699 def as_timespan(self): 700 details = self.details 701 if details.has_key("start") and details.has_key("end"): 702 return Timespan(details["start"], details["end"]) 703 else: 704 return None 705 706 def as_limits(self): 707 ts = self.as_timespan() 708 return ts and ts.as_limits() 709 710 class CalendarEvent(Event): 711 712 "An event from a remote calendar." 713 714 def getEventURL(self): 715 716 """ 717 Return the URL of this event, fixing any misinterpreted or incorrectly 718 formatted value in the event definition or returning the resource URL in 719 the absence of any URL in the event details. 720 """ 721 722 return self.details.get("url") and \ 723 self.valueToString(self.details["url"]) or \ 724 self.page.getPageURL() 725 726 def getSummary(self, event_parent=None): 727 728 """ 729 Return the event summary, fixing any misinterpreted or incorrectly 730 formatted value in the event definition. 731 """ 732 733 return self.valueToString(self.details["summary"]) 734 735 def valueToString(self, value): 736 737 "Return the given 'value' converted to a string." 738 739 if isinstance(value, list): 740 return ",".join(value) 741 elif isinstance(value, tuple): 742 return ";".join(value) 743 else: 744 return value 745 746 def linkToEvent(self, request, text, query_string=None, anchor=None): 747 748 """ 749 Using 'request', return a link to this event with the given link 'text' 750 and optional 'query_string' and 'anchor'. 751 """ 752 753 return linkToResource(self.getEventURL(), request, text, query_string, anchor) 754 755 def getMetadata(self): 756 757 """ 758 Return a dictionary containing items describing the event's "created" 759 time, "last-modified" time, "sequence" (or revision number) and the 760 "last-comment" made about the last edit. 761 """ 762 763 metadata = self.page.getMetadata() 764 765 return { 766 "created" : self.details.get("created") or self.details.get("dtstamp") or metadata["created"], 767 "last-modified" : self.details.get("last-modified") or self.details.get("dtstamp") or metadata["last-modified"], 768 "sequence" : self.details.get("sequence") or 0, 769 "last-comment" : "" 770 } 771 772 # vim: tabstop=4 expandtab shiftwidth=4