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 getConcretePeriod(calendar_start, calendar_end, earliest, latest): 425 426 """ 427 From the requested 'calendar_start' and 'calendar_end', which may be None, 428 indicating that no restriction is imposed on the period for each of the 429 boundaries, use the 'earliest' and 'latest' event months to define a 430 specific period of interest. 431 """ 432 433 # Define the period as starting with any specified start month or the 434 # earliest event known, ending with any specified end month or the latest 435 # event known. 436 437 first = calendar_start or earliest 438 last = calendar_end or latest 439 440 # If there is no range of months to show, perhaps because there are no 441 # events in the requested period, and there was no start or end month 442 # specified, show only the month indicated by the start or end of the 443 # requested period. If all events were to be shown but none were found show 444 # the current month. 445 446 if first is None: 447 first = last or getCurrentMonth() 448 if last is None: 449 last = first or getCurrentMonth() 450 451 # Permit "expiring" periods (where the start date approaches the end date). 452 453 return min(first, last), last 454 455 def getCoverage(start, end, events): 456 457 """ 458 Within the period defined by the 'start' and 'end' dates, determine the 459 coverage of the days in the period by the given 'events', returning a set of 460 covered days, along with a list of slots, where each slot contains a tuple 461 of the form (set of covered days, events). 462 """ 463 464 all_events = [] 465 full_coverage = set() 466 467 # Get event details. 468 469 for event in events: 470 event_page, event_details = event 471 472 # Test for the event in the period. 473 474 if event_details["start"] <= end and event_details["end"] >= start: 475 476 # Find the coverage of this period for the event. 477 478 event_start = max(event_details["start"], start) 479 event_end = min(event_details["end"], end) 480 event_coverage = set(daterange(event_start, event_end)) 481 482 # Update the overall coverage. 483 484 full_coverage.update(event_coverage) 485 486 # Try and fit the event into the events list. 487 488 for i, (coverage, covered_events) in enumerate(all_events): 489 490 # Where the event does not overlap with the current 491 # element, add it alongside existing events. 492 493 if not coverage.intersection(event_coverage): 494 covered_events.append(event) 495 all_events[i] = coverage.union(event_coverage), covered_events 496 break 497 498 # Make a new element in the list if the event cannot be 499 # marked alongside existing events. 500 501 else: 502 all_events.append((event_coverage, [event])) 503 504 return full_coverage, all_events 505 506 # User interface functions. 507 508 def getParameter(request, name): 509 return request.form.get(name, [None])[0] 510 511 def getParameterMonth(arg): 512 n = None 513 514 if arg.startswith("current"): 515 date = getCurrentMonth() 516 if len(arg) > 8: 517 n = int(arg[7:]) 518 519 elif arg.startswith("yearstart"): 520 date = (getCurrentYear(), 1) 521 if len(arg) > 10: 522 n = int(arg[9:]) 523 524 elif arg.startswith("yearend"): 525 date = (getCurrentYear(), 12) 526 if len(arg) > 8: 527 n = int(arg[7:]) 528 529 else: 530 date = getMonth(arg) 531 532 if n is not None: 533 date = monthupdate(date, n) 534 535 return date 536 537 def getFormMonth(request, calendar_name, argname): 538 if calendar_name is None: 539 calendar_prefix = argname 540 else: 541 calendar_prefix = "%s-%s" % (calendar_name, argname) 542 543 arg = getParameter(request, calendar_prefix) 544 if arg is not None: 545 return getParameterMonth(arg) 546 else: 547 return None 548 549 def getFormMonthPair(request, yeararg, montharg): 550 year = getParameter(request, yeararg) 551 month = getParameter(request, montharg) 552 if year and month: 553 return (int(year), int(month)) 554 else: 555 return None 556 557 def getPrettyPageName(page): 558 559 "Return a nicely formatted title/name for the given 'page'." 560 561 return page.split_title(force=1).replace("_", " ").replace("/", u" ? ") 562 563 def getMonthLabel(month): 564 565 "Return an unlocalised label for the given 'month'." 566 567 return month_labels[month - 1] # zero-based labels 568 569 def getDayLabel(weekday): 570 571 "Return an unlocalised label for the given 'weekday'." 572 573 return weekday_labels[weekday] 574 575 # vim: tabstop=4 expandtab shiftwidth=4