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