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