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 "Return the URL of this event." 712 713 return self.details.get("url") or self.page.getPageURL() 714 715 def linkToEvent(self, request, text, query_string=None, anchor=None): 716 717 """ 718 Using 'request', return a link to this event with the given link 'text' 719 and optional 'query_string' and 'anchor'. 720 """ 721 722 return linkToResource(self.getEventURL(), request, text, query_string, anchor) 723 724 def getMetadata(self): 725 726 """ 727 Return a dictionary containing items describing the event's "created" 728 time, "last-modified" time, "sequence" (or revision number) and the 729 "last-comment" made about the last edit. 730 """ 731 732 metadata = self.page.getMetadata() 733 734 return { 735 "created" : self.details.get("created") or self.details.get("dtstamp") or metadata["created"], 736 "last-modified" : self.details.get("last-modified") or self.details.get("dtstamp") or metadata["last-modified"], 737 "sequence" : self.details.get("sequence") or 0, 738 "last-comment" : "" 739 } 740 741 # vim: tabstop=4 expandtab shiftwidth=4