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