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 monthupdate(date, n): 124 125 "Return 'date' updated by 'n' months." 126 127 if n < 0: 128 fn = prevmonth 129 else: 130 fn = nextmonth 131 132 i = 0 133 while i < abs(n): 134 date = fn(date) 135 i += 1 136 137 return date 138 139 def daterange(first, last, step=1): 140 141 """ 142 Get the range of dates starting at 'first' and ending on 'last', using the 143 specified 'step'. 144 """ 145 146 results = [] 147 148 months_only = len(first) == 2 149 start_year = first[0] 150 end_year = last[0] 151 152 for year in range(start_year, end_year + step, step): 153 if step == 1 and year < end_year: 154 end_month = 12 155 elif step == -1 and year > end_year: 156 end_month = 1 157 else: 158 end_month = last[1] 159 160 if step == 1 and year > start_year: 161 start_month = 1 162 elif step == -1 and year < start_year: 163 start_month = 12 164 else: 165 start_month = first[1] 166 167 for month in range(start_month, end_month + step, step): 168 if months_only: 169 results.append((year, month)) 170 else: 171 if step == 1 and month < end_month: 172 _wd, end_day = calendar.monthrange(year, month) 173 elif step == -1 and month > end_month: 174 end_day = 1 175 else: 176 end_day = last[2] 177 178 if step == 1 and month > start_month: 179 start_day = 1 180 elif step == -1 and month < start_month: 181 _wd, start_day = calendar.monthrange(year, month) 182 else: 183 start_day = first[2] 184 185 for day in range(start_day, end_day + step, step): 186 results.append((year, month, day)) 187 188 return results 189 190 def nextdate(date): 191 192 "Return the date following the given 'date'." 193 194 year, month, day = date 195 _wd, end_day = calendar.monthrange(year, month) 196 if day == end_day: 197 if month == 12: 198 return (year + 1, 1, 1) 199 else: 200 return (year, month + 1, 1) 201 else: 202 return (year, month, day + 1) 203 204 def prevdate(date): 205 206 "Return the date preceding the given 'date'." 207 208 year, month, day = date 209 if day == 1: 210 if month == 1: 211 return (year - 1, 12, 31) 212 else: 213 _wd, end_day = calendar.monthrange(year, month - 1) 214 return (year, month - 1, end_day) 215 else: 216 return (year, month, day - 1) 217 218 def nextmonth(date): 219 220 "Return the (year, month) tuple following 'date'." 221 222 year, month = date 223 if month == 12: 224 return (year + 1, 1) 225 else: 226 return year, month + 1 227 228 def prevmonth(date): 229 230 "Return the (year, month) tuple preceding 'date'." 231 232 year, month = date 233 if month == 1: 234 return (year - 1, 12) 235 else: 236 return year, month - 1 237 238 def span(start, end): 239 240 "Return the difference between 'start' and 'end'." 241 242 return end[0] - start[0], end[1] - start[1] 243 244 def getEvents(request, category_names, calendar_start=None, calendar_end=None): 245 246 """ 247 Using the 'request', generate a list of events found on pages belonging to 248 the specified 'category_names', using the optional 'calendar_start' and 249 'calendar_end' month tuples of the form (year, month) to indicate a window 250 of interest. 251 252 Return a list of events, a dictionary mapping months to event lists (within 253 the window of interest), a list of all events within the window of interest, 254 the earliest month of an event within the window of interest, and the latest 255 month of an event within the window of interest. 256 """ 257 258 # Re-order the window, if appropriate. 259 260 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 261 calendar_start, calendar_end = calendar_end, calendar_start 262 263 events = [] 264 shown_events = {} 265 all_shown_events = [] 266 267 earliest = None 268 latest = None 269 270 for category_name in category_names: 271 272 # Get the pages and page names in the category. 273 274 pages_in_category = getPages(category_name, request) 275 276 # Visit each page in the category. 277 278 for page_in_category in pages_in_category: 279 pagename = page_in_category.page_name 280 281 # Get a real page, not a result page. 282 283 real_page_in_category = Page(request, pagename) 284 event_details = getEventDetails(real_page_in_category) 285 286 # Define the event as the page together with its details. 287 288 event = (real_page_in_category, event_details) 289 events.append(event) 290 291 # Test for the suitability of the event. 292 293 if event_details.has_key("start") and event_details.has_key("end"): 294 295 start_month = event_details["start"][:2] 296 end_month = event_details["end"][:2] 297 298 # Compare the months of the dates to the requested calendar 299 # window, if any. 300 301 if (calendar_start is None or end_month >= calendar_start) and \ 302 (calendar_end is None or start_month <= calendar_end): 303 304 all_shown_events.append(event) 305 306 if earliest is None or start_month < earliest: 307 earliest = start_month 308 if latest is None or end_month > latest: 309 latest = end_month 310 311 # Store the event in the month-specific dictionary. 312 313 first = max(start_month, calendar_start or start_month) 314 last = min(end_month, calendar_end or end_month) 315 316 for event_month in daterange(first, last): 317 if not shown_events.has_key(event_month): 318 shown_events[event_month] = [] 319 shown_events[event_month].append(event) 320 321 return events, shown_events, all_shown_events, earliest, latest 322 323 def getConcretePeriod(calendar_start, calendar_end, earliest, latest): 324 325 """ 326 From the requested 'calendar_start' and 'calendar_end', which may be None, 327 indicating that no restriction is imposed on the period for each of the 328 boundaries, use the 'earliest' and 'latest' event months to define a 329 specific period of interest. 330 """ 331 332 # Define the period as starting with any specified start month or the 333 # earliest event known, ending with any specified end month or the latest 334 # event known. 335 336 first = calendar_start or earliest 337 last = calendar_end or latest 338 339 # If there is no range of months to show, perhaps because there are no 340 # events in the requested period, and there was no start or end month 341 # specified, show only the month indicated by the start or end of the 342 # requested period. If all events were to be shown but none were found show 343 # the current month. 344 345 if first is None: 346 first = last or getCurrentMonth() 347 if last is None: 348 last = first or getCurrentMonth() 349 350 # Permit "expiring" periods (where the start date approaches the end date). 351 352 return min(first, last), last 353 354 def getCoverage(start, end, events): 355 356 """ 357 Within the period defined by the 'start' and 'end' dates, determine the 358 coverage of the days in the period by the given 'events', returning a set of 359 covered days, along with a list of slots, where each slot contains a tuple 360 of the form (set of covered days, events). 361 """ 362 363 all_events = [] 364 full_coverage = set() 365 366 # Get event details. 367 368 for event in events: 369 event_page, event_details = event 370 371 # Test for the event in the period. 372 373 if event_details["start"] <= end and event_details["end"] >= start: 374 375 # Find the coverage of this period for the event. 376 377 event_start = max(event_details["start"], start) 378 event_end = min(event_details["end"], end) 379 event_coverage = set(daterange(event_start, event_end)) 380 381 # Update the overall coverage. 382 383 full_coverage.update(event_coverage) 384 385 # Try and fit the event into the events list. 386 387 for i, (coverage, covered_events) in enumerate(all_events): 388 389 # Where the event does not overlap with the current 390 # element, add it alongside existing events. 391 392 if not coverage.intersection(event_coverage): 393 covered_events.append(event) 394 all_events[i] = coverage.union(event_coverage), covered_events 395 break 396 397 # Make a new element in the list if the event cannot be 398 # marked alongside existing events. 399 400 else: 401 all_events.append((event_coverage, [event])) 402 403 return full_coverage, all_events 404 405 # vim: tabstop=4 expandtab shiftwidth=4