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