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