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