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.3" 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", "description"): 253 desc = getEncodedWikiText(event_details[term]) 254 255 new_body_parts.append(desc) 256 257 else: 258 new_body_parts.append(body[end_of_last_match:]) 259 260 return "".join(new_body_parts) 261 262 def setCategoryMembership(body, category_names): 263 264 """ 265 Set the category membership in the given page 'body' using the specified 266 'category_names' and returning the new body text. 267 """ 268 269 match = category_membership_regexp.search(body) 270 if match: 271 return "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 272 else: 273 return body 274 275 def getEventSummary(event_page, event_details): 276 277 """ 278 Return either the given title or summary of the event described by the given 279 'event_page', according to the given 'event_details', or return the pretty 280 version of the page name. 281 """ 282 283 if event_details.has_key("title"): 284 return event_details["title"] 285 elif event_details.has_key("summary"): 286 return event_details["summary"] 287 else: 288 return getPrettyPageName(event_page) 289 290 def getDate(s): 291 292 "Parse the string 's', extracting and returning a date string." 293 294 m = date_regexp.search(s) 295 if m: 296 return tuple(map(int, m.groups())) 297 else: 298 return None 299 300 def getMonth(s): 301 302 "Parse the string 's', extracting and returning a month string." 303 304 m = month_regexp.search(s) 305 if m: 306 return tuple(map(int, m.groups())) 307 else: 308 return None 309 310 def getCurrentMonth(): 311 312 "Return the current month as a (year, month) tuple." 313 314 today = datetime.date.today() 315 return (today.year, today.month) 316 317 def getCurrentYear(): 318 319 "Return the current year." 320 321 today = datetime.date.today() 322 return today.year 323 324 def monthupdate(date, n): 325 326 "Return 'date' updated by 'n' months." 327 328 if n < 0: 329 fn = prevmonth 330 else: 331 fn = nextmonth 332 333 i = 0 334 while i < abs(n): 335 date = fn(date) 336 i += 1 337 338 return date 339 340 def daterange(first, last, step=1): 341 342 """ 343 Get the range of dates starting at 'first' and ending on 'last', using the 344 specified 'step'. 345 """ 346 347 results = [] 348 349 months_only = len(first) == 2 350 start_year = first[0] 351 end_year = last[0] 352 353 for year in range(start_year, end_year + step, step): 354 if step == 1 and year < end_year: 355 end_month = 12 356 elif step == -1 and year > end_year: 357 end_month = 1 358 else: 359 end_month = last[1] 360 361 if step == 1 and year > start_year: 362 start_month = 1 363 elif step == -1 and year < start_year: 364 start_month = 12 365 else: 366 start_month = first[1] 367 368 for month in range(start_month, end_month + step, step): 369 if months_only: 370 results.append((year, month)) 371 else: 372 if step == 1 and month < end_month: 373 _wd, end_day = calendar.monthrange(year, month) 374 elif step == -1 and month > end_month: 375 end_day = 1 376 else: 377 end_day = last[2] 378 379 if step == 1 and month > start_month: 380 start_day = 1 381 elif step == -1 and month < start_month: 382 _wd, start_day = calendar.monthrange(year, month) 383 else: 384 start_day = first[2] 385 386 for day in range(start_day, end_day + step, step): 387 results.append((year, month, day)) 388 389 return results 390 391 def nextdate(date): 392 393 "Return the date following the given 'date'." 394 395 year, month, day = date 396 _wd, end_day = calendar.monthrange(year, month) 397 if day == end_day: 398 if month == 12: 399 return (year + 1, 1, 1) 400 else: 401 return (year, month + 1, 1) 402 else: 403 return (year, month, day + 1) 404 405 def prevdate(date): 406 407 "Return the date preceding the given 'date'." 408 409 year, month, day = date 410 if day == 1: 411 if month == 1: 412 return (year - 1, 12, 31) 413 else: 414 _wd, end_day = calendar.monthrange(year, month - 1) 415 return (year, month - 1, end_day) 416 else: 417 return (year, month, day - 1) 418 419 def nextmonth(date): 420 421 "Return the (year, month) tuple following 'date'." 422 423 year, month = date 424 if month == 12: 425 return (year + 1, 1) 426 else: 427 return year, month + 1 428 429 def prevmonth(date): 430 431 "Return the (year, month) tuple preceding 'date'." 432 433 year, month = date 434 if month == 1: 435 return (year - 1, 12) 436 else: 437 return year, month - 1 438 439 def span(start, end): 440 441 "Return the difference between 'start' and 'end'." 442 443 return end[0] - start[0], end[1] - start[1] 444 445 def getEvents(request, category_names, calendar_start=None, calendar_end=None): 446 447 """ 448 Using the 'request', generate a list of events found on pages belonging to 449 the specified 'category_names', using the optional 'calendar_start' and 450 'calendar_end' month tuples of the form (year, month) to indicate a window 451 of interest. 452 453 Return a list of events, a dictionary mapping months to event lists (within 454 the window of interest), a list of all events within the window of interest, 455 the earliest month of an event within the window of interest, and the latest 456 month of an event within the window of interest. 457 """ 458 459 # Re-order the window, if appropriate. 460 461 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 462 calendar_start, calendar_end = calendar_end, calendar_start 463 464 events = [] 465 shown_events = {} 466 all_shown_events = [] 467 processed_pages = set() 468 469 earliest = None 470 latest = None 471 472 for category_name in category_names: 473 474 # Get the pages and page names in the category. 475 476 pages_in_category = getPages(category_name, request) 477 478 # Visit each page in the category. 479 480 for page_in_category in pages_in_category: 481 pagename = page_in_category.page_name 482 483 # Only process each page once. 484 485 if pagename in processed_pages: 486 continue 487 else: 488 processed_pages.add(pagename) 489 490 # Get a real page, not a result page. 491 492 real_page_in_category = Page(request, pagename) 493 event_details = getEventDetails(real_page_in_category) 494 495 # Define the event as the page together with its details. 496 497 event = (real_page_in_category, event_details) 498 events.append(event) 499 500 # Test for the suitability of the event. 501 502 if event_details.has_key("start") and event_details.has_key("end"): 503 504 start_month = event_details["start"][:2] 505 end_month = event_details["end"][:2] 506 507 # Compare the months of the dates to the requested calendar 508 # window, if any. 509 510 if (calendar_start is None or end_month >= calendar_start) and \ 511 (calendar_end is None or start_month <= calendar_end): 512 513 all_shown_events.append(event) 514 515 if earliest is None or start_month < earliest: 516 earliest = start_month 517 if latest is None or end_month > latest: 518 latest = end_month 519 520 # Store the event in the month-specific dictionary. 521 522 first = max(start_month, calendar_start or start_month) 523 last = min(end_month, calendar_end or end_month) 524 525 for event_month in daterange(first, last): 526 if not shown_events.has_key(event_month): 527 shown_events[event_month] = [] 528 shown_events[event_month].append(event) 529 530 return events, shown_events, all_shown_events, earliest, latest 531 532 def setEventTimestamps(request, events): 533 534 """ 535 Using 'request', set timestamp details in the details dictionary of each of 536 the 'events': a list of the form (event_page, event_details). 537 538 Retutn the latest timestamp found. 539 """ 540 541 latest = None 542 543 for event_page, event_details in events: 544 545 # Get the initial revision of the page. 546 547 revisions = event_page.getRevList() 548 event_page_initial = Page(request, event_page.page_name, rev=revisions[-1]) 549 550 # Get the created and last modified times. 551 552 initial_revision = getPageRevision(event_page_initial) 553 event_details["created"] = initial_revision["timestamp"] 554 latest_revision = getPageRevision(event_page) 555 event_details["last-modified"] = latest_revision["timestamp"] 556 event_details["sequence"] = len(revisions) - 1 557 event_details["last-comment"] = latest_revision["comment"] 558 559 if latest is None or latest < event_details["last-modified"]: 560 latest = event_details["last-modified"] 561 562 return latest 563 564 def compareEvents(event1, event2): 565 566 """ 567 Compare 'event1' and 'event2' by start and end date, where both parameters 568 are of the following form: 569 570 (event_page, event_details) 571 """ 572 573 event_page1, event_details1 = event1 574 event_page2, event_details2 = event2 575 return cmp( 576 (event_details1["start"], event_details1["end"]), 577 (event_details2["start"], event_details2["end"]) 578 ) 579 580 def getOrderedEvents(events): 581 582 """ 583 Return a list with the given 'events' ordered according to their start and 584 end dates. Each list element must be of the following form: 585 586 (event_page, event_details) 587 """ 588 589 ordered_events = events[:] 590 ordered_events.sort(compareEvents) 591 return ordered_events 592 593 def getConcretePeriod(calendar_start, calendar_end, earliest, latest): 594 595 """ 596 From the requested 'calendar_start' and 'calendar_end', which may be None, 597 indicating that no restriction is imposed on the period for each of the 598 boundaries, use the 'earliest' and 'latest' event months to define a 599 specific period of interest. 600 """ 601 602 # Define the period as starting with any specified start month or the 603 # earliest event known, ending with any specified end month or the latest 604 # event known. 605 606 first = calendar_start or earliest 607 last = calendar_end or latest 608 609 # If there is no range of months to show, perhaps because there are no 610 # events in the requested period, and there was no start or end month 611 # specified, show only the month indicated by the start or end of the 612 # requested period. If all events were to be shown but none were found show 613 # the current month. 614 615 if first is None: 616 first = last or getCurrentMonth() 617 if last is None: 618 last = first or getCurrentMonth() 619 620 # Permit "expiring" periods (where the start date approaches the end date). 621 622 return min(first, last), last 623 624 def getCoverage(start, end, events): 625 626 """ 627 Within the period defined by the 'start' and 'end' dates, determine the 628 coverage of the days in the period by the given 'events', returning a set of 629 covered days, along with a list of slots, where each slot contains a tuple 630 of the form (set of covered days, events). 631 """ 632 633 all_events = [] 634 full_coverage = set() 635 636 # Get event details. 637 638 for event in events: 639 event_page, event_details = event 640 641 # Test for the event in the period. 642 643 if event_details["start"] <= end and event_details["end"] >= start: 644 645 # Find the coverage of this period for the event. 646 647 event_start = max(event_details["start"], start) 648 event_end = min(event_details["end"], end) 649 event_coverage = set(daterange(event_start, event_end)) 650 651 # Update the overall coverage. 652 653 full_coverage.update(event_coverage) 654 655 # Try and fit the event into the events list. 656 657 for i, (coverage, covered_events) in enumerate(all_events): 658 659 # Where the event does not overlap with the current 660 # element, add it alongside existing events. 661 662 if not coverage.intersection(event_coverage): 663 covered_events.append(event) 664 all_events[i] = coverage.union(event_coverage), covered_events 665 break 666 667 # Make a new element in the list if the event cannot be 668 # marked alongside existing events. 669 670 else: 671 all_events.append((event_coverage, [event])) 672 673 return full_coverage, all_events 674 675 # User interface functions. 676 677 def getParameter(request, name): 678 return request.form.get(name, [None])[0] 679 680 def getParameterMonth(arg): 681 n = None 682 683 if arg.startswith("current"): 684 date = getCurrentMonth() 685 if len(arg) > 8: 686 n = int(arg[7:]) 687 688 elif arg.startswith("yearstart"): 689 date = (getCurrentYear(), 1) 690 if len(arg) > 10: 691 n = int(arg[9:]) 692 693 elif arg.startswith("yearend"): 694 date = (getCurrentYear(), 12) 695 if len(arg) > 8: 696 n = int(arg[7:]) 697 698 else: 699 date = getMonth(arg) 700 701 if n is not None: 702 date = monthupdate(date, n) 703 704 return date 705 706 def getFormMonth(request, calendar_name, argname): 707 if calendar_name is None: 708 calendar_prefix = argname 709 else: 710 calendar_prefix = "%s-%s" % (calendar_name, argname) 711 712 arg = getParameter(request, calendar_prefix) 713 if arg is not None: 714 return getParameterMonth(arg) 715 else: 716 return None 717 718 def getFormMonthPair(request, yeararg, montharg): 719 year = getParameter(request, yeararg) 720 month = getParameter(request, montharg) 721 if year and month: 722 return (int(year), int(month)) 723 else: 724 return None 725 726 def getPrettyPageName(page): 727 728 "Return a nicely formatted title/name for the given 'page'." 729 730 if isMoin15(): 731 title = page.split_title(page.request, force=1) 732 else: 733 title = page.split_title(force=1) 734 735 return title.replace("_", " ").replace("/", u" ? ") 736 737 def getMonthLabel(month): 738 739 "Return an unlocalised label for the given 'month'." 740 741 return month_labels[month - 1] # zero-based labels 742 743 def getDayLabel(weekday): 744 745 "Return an unlocalised label for the given 'weekday'." 746 747 return weekday_labels[weekday] 748 749 def linkToPage(request, page, text, query_string=None): 750 751 """ 752 Using 'request', return a link to 'page' with the given link 'text' and 753 optional 'query_string'. 754 """ 755 756 text = wikiutil.escape(text) 757 758 if isMoin15(): 759 url = wikiutil.quoteWikinameURL(page.page_name) 760 if query_string is not None: 761 url = "%s?%s" % (url, query_string) 762 return wikiutil.link_tag(request, url, text, getattr(page, "formatter", None)) 763 else: 764 return page.link_to_raw(request, text, query_string) 765 766 def getPageURL(request, page): 767 768 "Using 'request', return the URL of 'page'." 769 770 if isMoin15(): 771 return request.getQualifiedURL(page.url(request)) 772 else: 773 return request.getQualifiedURL(page.url(request, relative=0)) 774 775 # vim: tabstop=4 expandtab shiftwidth=4