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 import calendar 14 import datetime 15 import re 16 17 __version__ = "0.1" 18 19 # Date labels. 20 21 month_labels = ["January", "February", "March", "April", "May", "June", 22 "July", "August", "September", "October", "November", "December"] 23 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 24 25 # Regular expressions where MoinMoin does not provide the required support. 26 27 category_regexp = None 28 definition_list_regexp = re.compile(ur'^\s+(?P<term>.*?)::\s(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 29 date_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})', re.UNICODE) 30 month_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})', re.UNICODE) 31 verbatim_regexp = re.compile(ur'(?:' 32 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 33 ur'|' 34 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 35 ur'|' 36 ur'`(?P<monospace>.*?)`' 37 ur'|' 38 ur'{{{(?P<preformatted>.*?)}}}' 39 ur')', re.UNICODE) 40 41 # Utility functions. 42 43 def isMoin15(): 44 return version.release.startswith("1.5.") 45 46 def getCategoryPattern(request): 47 global category_regexp 48 49 try: 50 return request.cfg.cache.page_category_regexact 51 except AttributeError: 52 53 # Use regular expression from MoinMoin 1.7.1 otherwise. 54 55 if category_regexp is None: 56 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 57 return category_regexp 58 59 # Action support functions. 60 61 def getCategories(request): 62 63 """ 64 From the AdvancedSearch macro, return a list of category page names using 65 the given 'request'. 66 """ 67 68 # This will return all pages with "Category" in the title. 69 70 cat_filter = getCategoryPattern(request).search 71 return request.rootpage.getPageList(filter=cat_filter) 72 73 def getCategoryMapping(category_pagenames, request): 74 75 """ 76 For the given 'category_pagenames' return a list of tuples of the form 77 (category name, category page name) using the given 'request'. 78 """ 79 80 cat_pattern = getCategoryPattern(request) 81 mapping = [] 82 for pagename in category_pagenames: 83 name = cat_pattern.match(pagename).group("key") 84 if name != "Category": 85 mapping.append((name, pagename)) 86 mapping.sort() 87 return mapping 88 89 # The main activity functions. 90 91 def getPages(pagename, request): 92 93 "Return the links minus category links for 'pagename' using the 'request'." 94 95 query = search.QueryParser().parse_query('category:%s' % pagename) 96 if isMoin15(): 97 results = search.searchPages(request, query) 98 results.sortByPagename() 99 else: 100 results = search.searchPages(request, query, "page_name") 101 102 cat_pattern = getCategoryPattern(request) 103 pages = [] 104 for page in results.hits: 105 if not cat_pattern.match(page.page_name): 106 pages.append(page) 107 return pages 108 109 def getEventDetails(page): 110 111 "Return a dictionary of event details from the given 'page'." 112 113 event_details = {} 114 115 if page.pi["format"] == "wiki": 116 for match in definition_list_regexp.finditer(page.body): 117 118 # Permit case-insensitive list terms. 119 120 term = match.group("term").lower() 121 desc = match.group("desc") 122 123 # Special value type handling. 124 125 # Dates. 126 127 if term in ("start", "end"): 128 desc = getDate(desc) 129 130 # Lists. 131 132 elif term in ("topics",): 133 desc = [value.strip() for value in desc.split(",")] 134 135 # Labels which may well be quoted. 136 # NOTE: Re-implementing support for verbatim text and linking 137 # NOTE: avoidance. 138 139 elif term in ("title", "summary"): 140 desc = "".join([s for s in verbatim_regexp.split(desc) if s is not None]) 141 142 if desc is not None: 143 event_details[term] = desc 144 145 return event_details 146 147 def getEventSummary(event_page, event_details): 148 149 """ 150 Return either the given title or summary of the event described by the given 151 'event_page', according to the given 'event_details', or return the pretty 152 version of the page name. 153 """ 154 155 if event_details.has_key("title"): 156 return event_details["title"] 157 elif event_details.has_key("summary"): 158 return event_details["summary"] 159 else: 160 return getPrettyPageName(event_page) 161 162 def getDate(s): 163 164 "Parse the string 's', extracting and returning a date string." 165 166 m = date_regexp.search(s) 167 if m: 168 return tuple(map(int, m.groups())) 169 else: 170 return None 171 172 def getMonth(s): 173 174 "Parse the string 's', extracting and returning a month string." 175 176 m = month_regexp.search(s) 177 if m: 178 return tuple(map(int, m.groups())) 179 else: 180 return None 181 182 def getCurrentMonth(): 183 184 "Return the current month as a (year, month) tuple." 185 186 today = datetime.date.today() 187 return (today.year, today.month) 188 189 def getCurrentYear(): 190 191 "Return the current year." 192 193 today = datetime.date.today() 194 return today.year 195 196 def monthupdate(date, n): 197 198 "Return 'date' updated by 'n' months." 199 200 if n < 0: 201 fn = prevmonth 202 else: 203 fn = nextmonth 204 205 i = 0 206 while i < abs(n): 207 date = fn(date) 208 i += 1 209 210 return date 211 212 def daterange(first, last, step=1): 213 214 """ 215 Get the range of dates starting at 'first' and ending on 'last', using the 216 specified 'step'. 217 """ 218 219 results = [] 220 221 months_only = len(first) == 2 222 start_year = first[0] 223 end_year = last[0] 224 225 for year in range(start_year, end_year + step, step): 226 if step == 1 and year < end_year: 227 end_month = 12 228 elif step == -1 and year > end_year: 229 end_month = 1 230 else: 231 end_month = last[1] 232 233 if step == 1 and year > start_year: 234 start_month = 1 235 elif step == -1 and year < start_year: 236 start_month = 12 237 else: 238 start_month = first[1] 239 240 for month in range(start_month, end_month + step, step): 241 if months_only: 242 results.append((year, month)) 243 else: 244 if step == 1 and month < end_month: 245 _wd, end_day = calendar.monthrange(year, month) 246 elif step == -1 and month > end_month: 247 end_day = 1 248 else: 249 end_day = last[2] 250 251 if step == 1 and month > start_month: 252 start_day = 1 253 elif step == -1 and month < start_month: 254 _wd, start_day = calendar.monthrange(year, month) 255 else: 256 start_day = first[2] 257 258 for day in range(start_day, end_day + step, step): 259 results.append((year, month, day)) 260 261 return results 262 263 def nextdate(date): 264 265 "Return the date following the given 'date'." 266 267 year, month, day = date 268 _wd, end_day = calendar.monthrange(year, month) 269 if day == end_day: 270 if month == 12: 271 return (year + 1, 1, 1) 272 else: 273 return (year, month + 1, 1) 274 else: 275 return (year, month, day + 1) 276 277 def prevdate(date): 278 279 "Return the date preceding the given 'date'." 280 281 year, month, day = date 282 if day == 1: 283 if month == 1: 284 return (year - 1, 12, 31) 285 else: 286 _wd, end_day = calendar.monthrange(year, month - 1) 287 return (year, month - 1, end_day) 288 else: 289 return (year, month, day - 1) 290 291 def nextmonth(date): 292 293 "Return the (year, month) tuple following 'date'." 294 295 year, month = date 296 if month == 12: 297 return (year + 1, 1) 298 else: 299 return year, month + 1 300 301 def prevmonth(date): 302 303 "Return the (year, month) tuple preceding 'date'." 304 305 year, month = date 306 if month == 1: 307 return (year - 1, 12) 308 else: 309 return year, month - 1 310 311 def span(start, end): 312 313 "Return the difference between 'start' and 'end'." 314 315 return end[0] - start[0], end[1] - start[1] 316 317 def getEvents(request, category_names, calendar_start=None, calendar_end=None): 318 319 """ 320 Using the 'request', generate a list of events found on pages belonging to 321 the specified 'category_names', using the optional 'calendar_start' and 322 'calendar_end' month tuples of the form (year, month) to indicate a window 323 of interest. 324 325 Return a list of events, a dictionary mapping months to event lists (within 326 the window of interest), a list of all events within the window of interest, 327 the earliest month of an event within the window of interest, and the latest 328 month of an event within the window of interest. 329 """ 330 331 # Re-order the window, if appropriate. 332 333 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 334 calendar_start, calendar_end = calendar_end, calendar_start 335 336 events = [] 337 shown_events = {} 338 all_shown_events = [] 339 processed_pages = set() 340 341 earliest = None 342 latest = None 343 344 for category_name in category_names: 345 346 # Get the pages and page names in the category. 347 348 pages_in_category = getPages(category_name, request) 349 350 # Visit each page in the category. 351 352 for page_in_category in pages_in_category: 353 pagename = page_in_category.page_name 354 355 # Only process each page once. 356 357 if pagename in processed_pages: 358 continue 359 else: 360 processed_pages.add(pagename) 361 362 # Get a real page, not a result page. 363 364 real_page_in_category = Page(request, pagename) 365 event_details = getEventDetails(real_page_in_category) 366 367 # Define the event as the page together with its details. 368 369 event = (real_page_in_category, event_details) 370 events.append(event) 371 372 # Test for the suitability of the event. 373 374 if event_details.has_key("start") and event_details.has_key("end"): 375 376 start_month = event_details["start"][:2] 377 end_month = event_details["end"][:2] 378 379 # Compare the months of the dates to the requested calendar 380 # window, if any. 381 382 if (calendar_start is None or end_month >= calendar_start) and \ 383 (calendar_end is None or start_month <= calendar_end): 384 385 all_shown_events.append(event) 386 387 if earliest is None or start_month < earliest: 388 earliest = start_month 389 if latest is None or end_month > latest: 390 latest = end_month 391 392 # Store the event in the month-specific dictionary. 393 394 first = max(start_month, calendar_start or start_month) 395 last = min(end_month, calendar_end or end_month) 396 397 for event_month in daterange(first, last): 398 if not shown_events.has_key(event_month): 399 shown_events[event_month] = [] 400 shown_events[event_month].append(event) 401 402 return events, shown_events, all_shown_events, earliest, latest 403 404 def getConcretePeriod(calendar_start, calendar_end, earliest, latest): 405 406 """ 407 From the requested 'calendar_start' and 'calendar_end', which may be None, 408 indicating that no restriction is imposed on the period for each of the 409 boundaries, use the 'earliest' and 'latest' event months to define a 410 specific period of interest. 411 """ 412 413 # Define the period as starting with any specified start month or the 414 # earliest event known, ending with any specified end month or the latest 415 # event known. 416 417 first = calendar_start or earliest 418 last = calendar_end or latest 419 420 # If there is no range of months to show, perhaps because there are no 421 # events in the requested period, and there was no start or end month 422 # specified, show only the month indicated by the start or end of the 423 # requested period. If all events were to be shown but none were found show 424 # the current month. 425 426 if first is None: 427 first = last or getCurrentMonth() 428 if last is None: 429 last = first or getCurrentMonth() 430 431 # Permit "expiring" periods (where the start date approaches the end date). 432 433 return min(first, last), last 434 435 def getCoverage(start, end, events): 436 437 """ 438 Within the period defined by the 'start' and 'end' dates, determine the 439 coverage of the days in the period by the given 'events', returning a set of 440 covered days, along with a list of slots, where each slot contains a tuple 441 of the form (set of covered days, events). 442 """ 443 444 all_events = [] 445 full_coverage = set() 446 447 # Get event details. 448 449 for event in events: 450 event_page, event_details = event 451 452 # Test for the event in the period. 453 454 if event_details["start"] <= end and event_details["end"] >= start: 455 456 # Find the coverage of this period for the event. 457 458 event_start = max(event_details["start"], start) 459 event_end = min(event_details["end"], end) 460 event_coverage = set(daterange(event_start, event_end)) 461 462 # Update the overall coverage. 463 464 full_coverage.update(event_coverage) 465 466 # Try and fit the event into the events list. 467 468 for i, (coverage, covered_events) in enumerate(all_events): 469 470 # Where the event does not overlap with the current 471 # element, add it alongside existing events. 472 473 if not coverage.intersection(event_coverage): 474 covered_events.append(event) 475 all_events[i] = coverage.union(event_coverage), covered_events 476 break 477 478 # Make a new element in the list if the event cannot be 479 # marked alongside existing events. 480 481 else: 482 all_events.append((event_coverage, [event])) 483 484 return full_coverage, all_events 485 486 # User interface functions. 487 488 def getParameterMonth(arg): 489 n = None 490 491 if arg.startswith("current"): 492 date = getCurrentMonth() 493 if len(arg) > 8: 494 n = int(arg[7:]) 495 496 elif arg.startswith("yearstart"): 497 date = (getCurrentYear(), 1) 498 if len(arg) > 10: 499 n = int(arg[9:]) 500 501 elif arg.startswith("yearend"): 502 date = (getCurrentYear(), 12) 503 if len(arg) > 8: 504 n = int(arg[7:]) 505 506 else: 507 date = getMonth(arg) 508 509 if n is not None: 510 date = monthupdate(date, n) 511 512 return date 513 514 def getFormMonth(request, calendar_name, argname): 515 if calendar_name is None: 516 calendar_prefix = argname 517 else: 518 calendar_prefix = "%s-%s" % (calendar_name, argname) 519 520 arg = request.form.get(calendar_prefix, [None])[0] 521 if arg is not None: 522 return getParameterMonth(arg) 523 else: 524 return None 525 526 def getPrettyPageName(page): 527 528 "Return a nicely formatted title/name for the given 'page'." 529 530 return page.split_title(force=1).replace("_", " ").replace("/", u" ? ") 531 532 def getMonthLabel(month): 533 534 "Return an unlocalised label for the given 'month'." 535 536 return month_labels[month - 1] # zero-based labels 537 538 def getDayLabel(weekday): 539 540 "Return an unlocalised label for the given 'weekday'." 541 542 return weekday_labels[weekday] 543 544 # vim: tabstop=4 expandtab shiftwidth=4