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