1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator library 4 5 @copyright: 2008, 2009 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 MoinMoin.Page import Page 12 from MoinMoin import search, version 13 from MoinMoin import wikiutil 14 import calendar 15 import datetime 16 import time 17 import re 18 19 __version__ = "0.4" 20 21 # Date labels. 22 23 month_labels = ["January", "February", "March", "April", "May", "June", 24 "July", "August", "September", "October", "November", "December"] 25 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 26 27 # Regular expressions where MoinMoin does not provide the required support. 28 29 category_regexp = None 30 31 # Page parsing. 32 33 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?)::\s)(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 34 category_membership_regexp = re.compile(ur"^\s*((Category\S+)(\s+Category\S+)*)\s*$", re.MULTILINE | re.UNICODE) 35 36 # Value parsing. 37 38 date_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})', re.UNICODE) 39 month_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})', re.UNICODE) 40 verbatim_regexp = re.compile(ur'(?:' 41 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 42 ur'|' 43 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 44 ur'|' 45 ur'`(?P<monospace>.*?)`' 46 ur'|' 47 ur'{{{(?P<preformatted>.*?)}}}' 48 ur')', re.UNICODE) 49 50 # Utility functions. 51 52 def isMoin15(): 53 return version.release.startswith("1.5.") 54 55 def getCategoryPattern(request): 56 global category_regexp 57 58 try: 59 return request.cfg.cache.page_category_regexact 60 except AttributeError: 61 62 # Use regular expression from MoinMoin 1.7.1 otherwise. 63 64 if category_regexp is None: 65 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 66 return category_regexp 67 68 # Action support functions. 69 70 def getCategories(request): 71 72 """ 73 From the AdvancedSearch macro, return a list of category page names using 74 the given 'request'. 75 """ 76 77 # This will return all pages with "Category" in the title. 78 79 cat_filter = getCategoryPattern(request).search 80 return request.rootpage.getPageList(filter=cat_filter) 81 82 def getCategoryMapping(category_pagenames, request): 83 84 """ 85 For the given 'category_pagenames' return a list of tuples of the form 86 (category name, category page name) using the given 'request'. 87 """ 88 89 cat_pattern = getCategoryPattern(request) 90 mapping = [] 91 for pagename in category_pagenames: 92 name = cat_pattern.match(pagename).group("key") 93 if name != "Category": 94 mapping.append((name, pagename)) 95 mapping.sort() 96 return mapping 97 98 def getPageRevision(page): 99 100 # From Page.edit_info... 101 102 if hasattr(page, "editlog_entry"): 103 line = page.editlog_entry() 104 else: 105 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 106 107 timestamp = line.ed_time_usecs 108 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 109 return {"timestamp" : time.gmtime(mtime), "comment" : line.comment} 110 111 def getHTTPTimeString(tmtuple): 112 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( 113 weekday_labels[tmtuple.tm_wday], 114 tmtuple.tm_mday, 115 month_labels[tmtuple.tm_mon -1], # zero-based labels 116 tmtuple.tm_year, 117 tmtuple.tm_hour, 118 tmtuple.tm_min, 119 tmtuple.tm_sec 120 ) 121 122 # The main activity functions. 123 124 def getPages(pagename, request): 125 126 "Return the links minus category links for 'pagename' using the 'request'." 127 128 query = search.QueryParser().parse_query('category:%s' % pagename) 129 if isMoin15(): 130 results = search.searchPages(request, query) 131 results.sortByPagename() 132 else: 133 results = search.searchPages(request, query, "page_name") 134 135 cat_pattern = getCategoryPattern(request) 136 pages = [] 137 for page in results.hits: 138 if not cat_pattern.match(page.page_name): 139 pages.append(page) 140 return pages 141 142 def getSimpleWikiText(text): 143 144 """ 145 Return the plain text representation of the given 'text' which may employ 146 certain Wiki syntax features, such as those providing verbatim or monospaced 147 text. 148 """ 149 150 # NOTE: Re-implementing support for verbatim text and linking avoidance. 151 152 return "".join([s for s in verbatim_regexp.split(text) if s is not None]) 153 154 def getEncodedWikiText(text): 155 156 "Encode the given 'text' in a verbatim representation." 157 158 return "<<Verbatim(%s)>>" % text 159 160 def getFormat(page): 161 162 "Get the format used on 'page'." 163 164 if isMoin15(): 165 return "wiki" # page.pi_format 166 else: 167 return page.pi["format"] 168 169 def getEventDetails(page): 170 171 "Return a dictionary of event details from the given 'page'." 172 173 event_details = {} 174 175 if getFormat(page) == "wiki": 176 for match in definition_list_regexp.finditer(page.get_raw_body()): 177 178 # Skip commented-out items. 179 180 if match.group("optcomment"): 181 continue 182 183 # Permit case-insensitive list terms. 184 185 term = match.group("term").lower() 186 desc = match.group("desc") 187 188 # Special value type handling. 189 190 # Dates. 191 192 if term in ("start", "end"): 193 desc = getDate(desc) 194 195 # Lists (whose elements may be quoted). 196 197 elif term in ("topics", "categories"): 198 desc = [getSimpleWikiText(value.strip()) for value in desc.split(",")] 199 200 # Labels which may well be quoted. 201 202 elif term in ("title", "summary", "description"): 203 desc = getSimpleWikiText(desc) 204 205 if desc is not None: 206 event_details[term] = desc 207 208 return event_details 209 210 def setEventDetails(body, event_details): 211 212 """ 213 Set the event details in the given page 'body' using the 'event_details' 214 dictionary, returning the new body text. 215 """ 216 217 new_body_parts = [] 218 end_of_last_match = 0 219 220 for match in definition_list_regexp.finditer(body): 221 222 # Add preceding text to the new body. 223 224 new_body_parts.append(body[end_of_last_match:match.start()]) 225 end_of_last_match = match.end() 226 227 # Get the matching regions, adding the term to the new body. 228 229 new_body_parts.append(match.group("wholeterm")) 230 231 # Permit case-insensitive list terms. 232 233 term = match.group("term").lower() 234 desc = match.group("desc") 235 236 # Special value type handling. 237 238 if event_details.has_key(term): 239 240 # Dates. 241 242 if term in ("start", "end"): 243 desc = desc.replace("YYYY-MM-DD", event_details[term]) 244 245 # Lists (whose elements may be quoted). 246 247 elif term in ("topics", "categories"): 248 desc = ", ".join(getEncodedWikiText(event_details[term])) 249 250 # Labels which may well be quoted. 251 252 elif term in ("title", "summary"): 253 desc = getEncodedWikiText(event_details[term]) 254 255 # Text which need not be quoted, but it will be Wiki text. 256 257 elif term in ("description",): 258 desc = event_details[term] 259 260 new_body_parts.append(desc) 261 262 else: 263 new_body_parts.append(body[end_of_last_match:]) 264 265 return "".join(new_body_parts) 266 267 def setCategoryMembership(body, category_names): 268 269 """ 270 Set the category membership in the given page 'body' using the specified 271 'category_names' and returning the new body text. 272 """ 273 274 match = category_membership_regexp.search(body) 275 if match: 276 return "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 277 else: 278 return body 279 280 def getEventSummary(event_page, event_details, event_parent=None): 281 282 """ 283 Return either the given title or summary of the event described by the given 284 'event_page', according to the given 'event_details', or return the pretty 285 version of the page name. 286 287 If the optional 'event_parent' is specified, any page beneath the given 288 'event_parent' page in the page hierarchy will omit this parent information 289 if its name is used as the summary. 290 """ 291 292 if event_details.has_key("title"): 293 return event_details["title"] 294 elif event_details.has_key("summary"): 295 return event_details["summary"] 296 else: 297 # If appropriate, remove the parent details and "/" character. 298 299 title = event_page.page_name 300 301 if event_parent is not None and title.startswith(event_parent): 302 title = title[len(event_parent.rstrip("/")) + 1:] 303 304 return getPrettyTitle(title) 305 306 def getDate(s): 307 308 "Parse the string 's', extracting and returning a date string." 309 310 m = date_regexp.search(s) 311 if m: 312 return tuple(map(int, m.groups())) 313 else: 314 return None 315 316 def getMonth(s): 317 318 "Parse the string 's', extracting and returning a month string." 319 320 m = month_regexp.search(s) 321 if m: 322 return tuple(map(int, m.groups())) 323 else: 324 return None 325 326 def getCurrentMonth(): 327 328 "Return the current month as a (year, month) tuple." 329 330 today = datetime.date.today() 331 return (today.year, today.month) 332 333 def getCurrentYear(): 334 335 "Return the current year." 336 337 today = datetime.date.today() 338 return today.year 339 340 def monthupdate(date, n): 341 342 "Return 'date' updated by 'n' months." 343 344 if n < 0: 345 fn = prevmonth 346 else: 347 fn = nextmonth 348 349 i = 0 350 while i < abs(n): 351 date = fn(date) 352 i += 1 353 354 return date 355 356 def daterange(first, last, step=1): 357 358 """ 359 Get the range of dates starting at 'first' and ending on 'last', using the 360 specified 'step'. 361 """ 362 363 results = [] 364 365 months_only = len(first) == 2 366 start_year = first[0] 367 end_year = last[0] 368 369 for year in range(start_year, end_year + step, step): 370 if step == 1 and year < end_year: 371 end_month = 12 372 elif step == -1 and year > end_year: 373 end_month = 1 374 else: 375 end_month = last[1] 376 377 if step == 1 and year > start_year: 378 start_month = 1 379 elif step == -1 and year < start_year: 380 start_month = 12 381 else: 382 start_month = first[1] 383 384 for month in range(start_month, end_month + step, step): 385 if months_only: 386 results.append((year, month)) 387 else: 388 if step == 1 and month < end_month: 389 _wd, end_day = calendar.monthrange(year, month) 390 elif step == -1 and month > end_month: 391 end_day = 1 392 else: 393 end_day = last[2] 394 395 if step == 1 and month > start_month: 396 start_day = 1 397 elif step == -1 and month < start_month: 398 _wd, start_day = calendar.monthrange(year, month) 399 else: 400 start_day = first[2] 401 402 for day in range(start_day, end_day + step, step): 403 results.append((year, month, day)) 404 405 return results 406 407 def nextdate(date): 408 409 "Return the date following the given 'date'." 410 411 year, month, day = date 412 _wd, end_day = calendar.monthrange(year, month) 413 if day == end_day: 414 if month == 12: 415 return (year + 1, 1, 1) 416 else: 417 return (year, month + 1, 1) 418 else: 419 return (year, month, day + 1) 420 421 def prevdate(date): 422 423 "Return the date preceding the given 'date'." 424 425 year, month, day = date 426 if day == 1: 427 if month == 1: 428 return (year - 1, 12, 31) 429 else: 430 _wd, end_day = calendar.monthrange(year, month - 1) 431 return (year, month - 1, end_day) 432 else: 433 return (year, month, day - 1) 434 435 def nextmonth(date): 436 437 "Return the (year, month) tuple following 'date'." 438 439 year, month = date 440 if month == 12: 441 return (year + 1, 1) 442 else: 443 return year, month + 1 444 445 def prevmonth(date): 446 447 "Return the (year, month) tuple preceding 'date'." 448 449 year, month = date 450 if month == 1: 451 return (year - 1, 12) 452 else: 453 return year, month - 1 454 455 def span(start, end): 456 457 "Return the difference between 'start' and 'end'." 458 459 return end[0] - start[0], end[1] - start[1] 460 461 def getEvents(request, category_names, calendar_start=None, calendar_end=None): 462 463 """ 464 Using the 'request', generate a list of events found on pages belonging to 465 the specified 'category_names', using the optional 'calendar_start' and 466 'calendar_end' month tuples of the form (year, month) to indicate a window 467 of interest. 468 469 Return a list of events, a dictionary mapping months to event lists (within 470 the window of interest), a list of all events within the window of interest, 471 the earliest month of an event within the window of interest, and the latest 472 month of an event within the window of interest. 473 """ 474 475 # Re-order the window, if appropriate. 476 477 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 478 calendar_start, calendar_end = calendar_end, calendar_start 479 480 events = [] 481 shown_events = {} 482 all_shown_events = [] 483 processed_pages = set() 484 485 earliest = None 486 latest = None 487 488 for category_name in category_names: 489 490 # Get the pages and page names in the category. 491 492 pages_in_category = getPages(category_name, request) 493 494 # Visit each page in the category. 495 496 for page_in_category in pages_in_category: 497 pagename = page_in_category.page_name 498 499 # Only process each page once. 500 501 if pagename in processed_pages: 502 continue 503 else: 504 processed_pages.add(pagename) 505 506 # Get a real page, not a result page. 507 508 real_page_in_category = Page(request, pagename) 509 event_details = getEventDetails(real_page_in_category) 510 511 # Define the event as the page together with its details. 512 513 event = (real_page_in_category, event_details) 514 events.append(event) 515 516 # Test for the suitability of the event. 517 518 if event_details.has_key("start") and event_details.has_key("end"): 519 520 start_month = event_details["start"][:2] 521 end_month = event_details["end"][:2] 522 523 # Compare the months of the dates to the requested calendar 524 # window, if any. 525 526 if (calendar_start is None or end_month >= calendar_start) and \ 527 (calendar_end is None or start_month <= calendar_end): 528 529 all_shown_events.append(event) 530 531 if earliest is None or start_month < earliest: 532 earliest = start_month 533 if latest is None or end_month > latest: 534 latest = end_month 535 536 # Store the event in the month-specific dictionary. 537 538 first = max(start_month, calendar_start or start_month) 539 last = min(end_month, calendar_end or end_month) 540 541 for event_month in daterange(first, last): 542 if not shown_events.has_key(event_month): 543 shown_events[event_month] = [] 544 shown_events[event_month].append(event) 545 546 return events, shown_events, all_shown_events, earliest, latest 547 548 def setEventTimestamps(request, events): 549 550 """ 551 Using 'request', set timestamp details in the details dictionary of each of 552 the 'events': a list of the form (event_page, event_details). 553 554 Retutn the latest timestamp found. 555 """ 556 557 latest = None 558 559 for event_page, event_details in events: 560 561 # Get the initial revision of the page. 562 563 revisions = event_page.getRevList() 564 event_page_initial = Page(request, event_page.page_name, rev=revisions[-1]) 565 566 # Get the created and last modified times. 567 568 initial_revision = getPageRevision(event_page_initial) 569 event_details["created"] = initial_revision["timestamp"] 570 latest_revision = getPageRevision(event_page) 571 event_details["last-modified"] = latest_revision["timestamp"] 572 event_details["sequence"] = len(revisions) - 1 573 event_details["last-comment"] = latest_revision["comment"] 574 575 if latest is None or latest < event_details["last-modified"]: 576 latest = event_details["last-modified"] 577 578 return latest 579 580 def compareEvents(event1, event2): 581 582 """ 583 Compare 'event1' and 'event2' by start and end date, where both parameters 584 are of the following form: 585 586 (event_page, event_details) 587 """ 588 589 event_page1, event_details1 = event1 590 event_page2, event_details2 = event2 591 return cmp( 592 (event_details1["start"], event_details1["end"]), 593 (event_details2["start"], event_details2["end"]) 594 ) 595 596 def getOrderedEvents(events): 597 598 """ 599 Return a list with the given 'events' ordered according to their start and 600 end dates. Each list element must be of the following form: 601 602 (event_page, event_details) 603 """ 604 605 ordered_events = events[:] 606 ordered_events.sort(compareEvents) 607 return ordered_events 608 609 def getConcretePeriod(calendar_start, calendar_end, earliest, latest): 610 611 """ 612 From the requested 'calendar_start' and 'calendar_end', which may be None, 613 indicating that no restriction is imposed on the period for each of the 614 boundaries, use the 'earliest' and 'latest' event months to define a 615 specific period of interest. 616 """ 617 618 # Define the period as starting with any specified start month or the 619 # earliest event known, ending with any specified end month or the latest 620 # event known. 621 622 first = calendar_start or earliest 623 last = calendar_end or latest 624 625 # If there is no range of months to show, perhaps because there are no 626 # events in the requested period, and there was no start or end month 627 # specified, show only the month indicated by the start or end of the 628 # requested period. If all events were to be shown but none were found show 629 # the current month. 630 631 if first is None: 632 first = last or getCurrentMonth() 633 if last is None: 634 last = first or getCurrentMonth() 635 636 # Permit "expiring" periods (where the start date approaches the end date). 637 638 return min(first, last), last 639 640 def getCoverage(start, end, events): 641 642 """ 643 Within the period defined by the 'start' and 'end' dates, determine the 644 coverage of the days in the period by the given 'events', returning a set of 645 covered days, along with a list of slots, where each slot contains a tuple 646 of the form (set of covered days, events). 647 """ 648 649 all_events = [] 650 full_coverage = set() 651 652 # Get event details. 653 654 for event in events: 655 event_page, event_details = event 656 657 # Test for the event in the period. 658 659 if event_details["start"] <= end and event_details["end"] >= start: 660 661 # Find the coverage of this period for the event. 662 663 event_start = max(event_details["start"], start) 664 event_end = min(event_details["end"], end) 665 event_coverage = set(daterange(event_start, event_end)) 666 667 # Update the overall coverage. 668 669 full_coverage.update(event_coverage) 670 671 # Try and fit the event into the events list. 672 673 for i, (coverage, covered_events) in enumerate(all_events): 674 675 # Where the event does not overlap with the current 676 # element, add it alongside existing events. 677 678 if not coverage.intersection(event_coverage): 679 covered_events.append(event) 680 all_events[i] = coverage.union(event_coverage), covered_events 681 break 682 683 # Make a new element in the list if the event cannot be 684 # marked alongside existing events. 685 686 else: 687 all_events.append((event_coverage, [event])) 688 689 return full_coverage, all_events 690 691 # User interface functions. 692 693 def getParameter(request, name, default=None): 694 return request.form.get(name, [default])[0] 695 696 def getQualifiedParameter(request, calendar_name, argname, default=None): 697 argname = getQualifiedParameterName(calendar_name, argname) 698 return getParameter(request, argname, default) 699 700 def getQualifiedParameterName(calendar_name, argname): 701 if calendar_name is None: 702 return argname 703 else: 704 return "%s-%s" % (calendar_name, argname) 705 706 def getParameterMonth(arg): 707 n = None 708 709 if arg.startswith("current"): 710 date = getCurrentMonth() 711 if len(arg) > 8: 712 n = int(arg[7:]) 713 714 elif arg.startswith("yearstart"): 715 date = (getCurrentYear(), 1) 716 if len(arg) > 10: 717 n = int(arg[9:]) 718 719 elif arg.startswith("yearend"): 720 date = (getCurrentYear(), 12) 721 if len(arg) > 8: 722 n = int(arg[7:]) 723 724 else: 725 date = getMonth(arg) 726 727 if n is not None: 728 date = monthupdate(date, n) 729 730 return date 731 732 def getFormMonth(request, calendar_name, argname): 733 arg = getQualifiedParameter(request, calendar_name, argname) 734 if arg is not None: 735 return getParameterMonth(arg) 736 else: 737 return None 738 739 def getFormMonthPair(request, yeararg, montharg): 740 year = getParameter(request, yeararg) 741 month = getParameter(request, montharg) 742 if year and month: 743 return (int(year), int(month)) 744 else: 745 return None 746 747 def getPrettyPageName(page): 748 749 "Return a nicely formatted title/name for the given 'page'." 750 751 if isMoin15(): 752 title = page.split_title(page.request, force=1) 753 else: 754 title = page.split_title(force=1) 755 756 return getPrettyTitle(title) 757 758 def getPrettyTitle(title): 759 760 "Return a nicely formatted version of the given 'title'." 761 762 return title.replace("_", " ").replace("/", u" ? ") 763 764 def getMonthLabel(month): 765 766 "Return an unlocalised label for the given 'month'." 767 768 return month_labels[month - 1] # zero-based labels 769 770 def getDayLabel(weekday): 771 772 "Return an unlocalised label for the given 'weekday'." 773 774 return weekday_labels[weekday] 775 776 def linkToPage(request, page, text, query_string=None): 777 778 """ 779 Using 'request', return a link to 'page' with the given link 'text' and 780 optional 'query_string'. 781 """ 782 783 text = wikiutil.escape(text) 784 785 if isMoin15(): 786 url = wikiutil.quoteWikinameURL(page.page_name) 787 if query_string is not None: 788 url = "%s?%s" % (url, query_string) 789 return wikiutil.link_tag(request, url, text, getattr(page, "formatter", None)) 790 else: 791 return page.link_to_raw(request, text, query_string) 792 793 def getPageURL(request, page): 794 795 "Using 'request', return the URL of 'page'." 796 797 if isMoin15(): 798 return request.getQualifiedURL(page.url(request)) 799 else: 800 return request.getQualifiedURL(page.url(request, relative=0)) 801 802 # vim: tabstop=4 expandtab shiftwidth=4