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