1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator Macro 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 wikiutil, search, version 13 import calendar 14 import re 15 16 try: 17 set 18 except NameError: 19 from sets import Set as set 20 21 __version__ = "0.1" 22 23 Dependencies = ['pages'] 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 32 # Date labels. 33 34 month_labels = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] 35 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 36 37 # Utility functions. 38 39 def isMoin15(): 40 return version.release.startswith("1.5.") 41 42 def getCategoryPattern(request): 43 global category_regexp 44 45 try: 46 return request.cfg.cache.page_category_regexact 47 except AttributeError: 48 49 # Use regular expression from MoinMoin 1.7.1 otherwise. 50 51 if category_regexp is None: 52 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 53 return category_regexp 54 55 # The main activity functions. 56 57 def getPages(pagename, request): 58 59 "Return the links minus category links for 'pagename' using the 'request'." 60 61 query = search.QueryParser().parse_query('category:%s' % pagename) 62 if isMoin15(): 63 results = search.searchPages(request, query) 64 results.sortByPagename() 65 else: 66 results = search.searchPages(request, query, "page_name") 67 68 cat_pattern = getCategoryPattern(request) 69 pages = [] 70 for page in results.hits: 71 if not cat_pattern.match(page.page_name): 72 pages.append(page) 73 return pages 74 75 def getPrettyPageName(page): 76 77 "Return a nicely formatted title/name for the given 'page'." 78 79 return page.split_title(force=1).replace("_", " ").replace("/", u" ? ") 80 81 def getEventDetails(page): 82 83 "Return a dictionary of event details from the given 'page'." 84 85 event_details = {} 86 87 if page.pi["format"] == "wiki": 88 for match in definition_list_regexp.finditer(page.body): 89 # Permit case-insensitive list terms. 90 term = match.group("term").lower() 91 desc = match.group("desc") 92 if term in ("start", "end"): 93 desc = getDate(desc) 94 if desc is not None: 95 event_details[term] = desc 96 97 return event_details 98 99 def getDate(s): 100 101 "Parse the string 's', extracting and returning a date string." 102 103 m = date_regexp.search(s) 104 if m: 105 return tuple(map(int, m.groups())) 106 else: 107 return None 108 109 def getMonth(s): 110 111 "Parse the string 's', extracting and returning a month string." 112 113 m = month_regexp.search(s) 114 if m: 115 return tuple(map(int, m.groups())) 116 else: 117 return None 118 119 def daterange(first, last): 120 results = [] 121 122 months_only = len(first) == 2 123 start_year = first[0] 124 end_year = last[0] 125 126 for year in range(start_year, end_year + 1): 127 if year < end_year: 128 end_month = 12 129 else: 130 end_month = last[1] 131 132 if year > start_year: 133 start_month = 1 134 else: 135 start_month = first[1] 136 137 for month in range(start_month, end_month + 1): 138 if months_only: 139 results.append((year, month)) 140 else: 141 if month < end_month: 142 _wd, end_day = calendar.monthrange(year, month) 143 else: 144 end_day = last[2] 145 146 if month > start_month: 147 start_day = 1 148 else: 149 start_day = first[2] 150 151 for day in range(start_day, end_day + 1): 152 results.append((year, month, day)) 153 154 return results 155 156 def getColour(s): 157 colour = [0, 0, 0] 158 digit = 0 159 for c in s: 160 colour[digit] += ord(c) 161 colour[digit] = colour[digit] % 256 162 digit += 1 163 digit = digit % 3 164 return tuple(colour) 165 166 def getBlackOrWhite(colour): 167 if sum(colour) / 3.0 > 127: 168 return (0, 0, 0) 169 else: 170 return (255, 255, 255) 171 172 def execute(macro, args): 173 174 """ 175 Execute the 'macro' with the given 'args': an optional list of selected 176 category names (categories whose pages are to be shown), together with 177 optional named arguments of the following forms: 178 179 start=YYYY-MM shows event details starting from the specified month 180 end=YYYY-MM shows event details ending at the specified month 181 182 mode=calendar shows a calendar view of events 183 mode=list shows a list of events by month 184 185 names=daily shows the name of an event on every day of that event 186 names=weekly shows the name of an event once per week 187 """ 188 189 request = macro.request 190 fmt = macro.formatter 191 page = fmt.page 192 _ = request.getText 193 194 # Interpret the arguments. 195 196 try: 197 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 198 except AttributeError: 199 parsed_args = args.split(",") 200 201 parsed_args = [arg for arg in parsed_args if arg] 202 203 # Get special arguments. 204 205 category_names = [] 206 calendar_start = None 207 calendar_end = None 208 mode = "calendar" 209 name_usage = "daily" 210 211 for arg in parsed_args: 212 if arg.startswith("start="): 213 calendar_start = getMonth(arg[6:]) 214 elif arg.startswith("end="): 215 calendar_end = getMonth(arg[4:]) 216 elif arg.startswith("mode="): 217 mode = arg[5:] 218 elif arg.startswith("names="): 219 name_usage = arg[6:] 220 else: 221 category_names.append(arg) 222 223 # Generate a list of events found on pages belonging to the specified 224 # categories, as found in the macro arguments. 225 226 events = [] 227 shown_events = {} 228 229 earliest = None 230 latest = None 231 232 for category_name in category_names: 233 234 # Get the pages and page names in the category. 235 236 pages_in_category = getPages(category_name, request) 237 238 # Visit each page in the category. 239 240 for page_in_category in pages_in_category: 241 pagename = page_in_category.page_name 242 243 # Get a real page, not a result page. 244 245 real_page_in_category = Page(request, pagename) 246 event_details = getEventDetails(real_page_in_category) 247 248 # Define the event as the page together with its details. 249 250 event = (real_page_in_category, event_details) 251 events.append(event) 252 253 # Test for the suitability of the event. 254 255 if event_details.has_key("start") and event_details.has_key("end"): 256 257 start_month = event_details["start"][:2] 258 end_month = event_details["end"][:2] 259 260 # Compare the months of the dates to the requested calendar 261 # window, if any. 262 263 if (calendar_start is None or end_month >= calendar_start) and \ 264 (calendar_end is None or start_month <= calendar_end): 265 266 if earliest is None or start_month < earliest: 267 earliest = start_month 268 if latest is None or end_month > latest: 269 latest = end_month 270 271 # Store the event in the month-specific dictionary. 272 273 first = max(start_month, calendar_start or start_month) 274 last = min(end_month, calendar_end or end_month) 275 276 for event_month in daterange(first, last): 277 if not shown_events.has_key(event_month): 278 shown_events[event_month] = [] 279 shown_events[event_month].append(event) 280 281 # Make a calendar. 282 283 output = [] 284 285 if mode == "list": 286 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 287 288 # Visit all months in the requested range, or across known events. 289 290 first = calendar_start or earliest 291 last = calendar_end or latest 292 293 for year, month in daterange(first, last): 294 295 # Either output a calendar view... 296 297 if mode == "calendar": 298 299 # Output a month. 300 301 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 302 303 output.append(fmt.table_row(on=1)) 304 output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "7"})) 305 output.append(fmt.span(on=1)) 306 output.append(fmt.text(_(month_labels[month - 1]))) # zero-based labels 307 output.append(fmt.span(on=0)) 308 output.append(fmt.text(" ")) 309 output.append(fmt.span(on=1)) 310 output.append(fmt.text(year)) 311 output.append(fmt.span(on=0)) 312 output.append(fmt.table_cell(on=0)) 313 output.append(fmt.table_row(on=0)) 314 315 # Weekday headings. 316 317 output.append(fmt.table_row(on=1)) 318 319 for weekday in range(0, 7): 320 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading"})) 321 output.append(fmt.text(_(weekday_labels[weekday]))) 322 output.append(fmt.table_cell(on=0)) 323 324 output.append(fmt.table_row(on=0)) 325 326 # Process the days of the month. 327 328 start_weekday, number_of_days = calendar.monthrange(year, month) 329 330 # The start weekday is the weekday of day number 1. 331 # Find the first day of the week, counting from below zero, if 332 # necessary, in order to land on the first day of the month as 333 # day number 1. 334 335 first_day = 1 - start_weekday 336 337 while first_day <= number_of_days: 338 339 # Find events in this week and determine how to mark them on the 340 # calendar. 341 342 week_events = [] 343 week_coverage = set() 344 345 week_start = (year, month, max(first_day, 1)) 346 week_end = (year, month, min(first_day + 6, number_of_days)) 347 348 # Get event details. 349 350 for event in shown_events.get((year, month), []): 351 event_page, event_details = event 352 353 # Test for the event in the current week. 354 355 if event_details["start"] <= week_end and event_details["end"] >= week_start: 356 357 # Find the coverage of this week for the event. 358 359 event_start = max(event_details["start"], week_start) 360 event_end = min(event_details["end"], week_end) 361 event_coverage = set(daterange(event_start, event_end)) 362 363 # Update the overall coverage. 364 365 week_coverage.update(event_coverage) 366 367 # Try and fit the event into the events list. 368 369 for i, (coverage, events) in enumerate(week_events): 370 371 # Where the event does not overlap with the current 372 # element, add it alongside existing events. 373 374 if not coverage.intersection(event_coverage): 375 events.append(event) 376 week_events[i] = coverage.union(event_coverage), events 377 break 378 379 # Make a new element in the list if the event cannot be 380 # marked alongside existing events. 381 382 else: 383 week_events.append((event_coverage, [event])) 384 385 # Output a week, starting with the day numbers. 386 387 output.append(fmt.table_row(on=1)) 388 389 for weekday in range(0, 7): 390 day = first_day + weekday 391 date = (year, month, day) 392 393 # Output out-of-month days. 394 395 if day < 1 or day > number_of_days: 396 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-excluded"})) 397 output.append(fmt.table_cell(on=0)) 398 399 # Output normal days. 400 401 else: 402 if date in week_coverage: 403 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-busy"})) 404 else: 405 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-empty"})) 406 407 output.append(fmt.div(on=1)) 408 output.append(fmt.span(on=1, css_class="event-day-number")) 409 output.append(fmt.text(day)) 410 output.append(fmt.span(on=0)) 411 output.append(fmt.div(on=0)) 412 413 # End of day. 414 415 output.append(fmt.table_cell(on=0)) 416 417 # End of day numbers. 418 419 output.append(fmt.table_row(on=0)) 420 421 # Visit each set of scheduled events. 422 423 for coverage, events in week_events: 424 425 # Output each set. 426 427 output.append(fmt.table_row(on=1)) 428 429 # Then, output day details. 430 431 for weekday in range(0, 7): 432 day = first_day + weekday 433 date = (year, month, day) 434 435 # Skip out-of-month days. 436 437 if day < 1 or day > number_of_days: 438 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-excluded"})) 439 output.append(fmt.table_cell(on=0)) 440 continue 441 442 # Output the day. 443 444 if date in coverage: 445 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-busy"})) 446 else: 447 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-empty"})) 448 449 # Get event details for the current day. 450 451 for event_page, event_details in events: 452 if not (event_details["start"] <= date <= event_details["end"]): 453 continue 454 455 # Get a pretty version of the page name. 456 457 pretty_pagename = getPrettyPageName(event_page) 458 459 # Generate a colour for the event. 460 461 bg = getColour(event_page.page_name) 462 fg = getBlackOrWhite(bg) 463 464 css_classes = ["event-summary"] 465 466 if event_details["start"] == date: 467 css_classes.append("event-starts") 468 start_of_event = 1 469 else: 470 start_of_event = 0 471 472 if event_details["end"] == date: 473 css_classes.append("event-ends") 474 475 # Output the event. 476 477 if name_usage == "daily" or start_of_event or weekday == 0 or day == 1: 478 hide_text = 0 479 else: 480 hide_text = 1 481 482 output.append(fmt.div(on=1, css_class=(" ".join(css_classes)), 483 style=("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg)))) 484 485 if not hide_text: 486 output.append(event_page.link_to_raw(request, wikiutil.escape(pretty_pagename))) 487 488 output.append(fmt.div(on=0)) 489 490 # End of day. 491 492 output.append(fmt.table_cell(on=0)) 493 494 # End of set. 495 496 output.append(fmt.table_row(on=0)) 497 498 # Process the next week... 499 500 first_day += 7 501 502 # End of month. 503 504 output.append(fmt.table(on=0)) 505 506 # Or output a summary view... 507 508 elif mode == "list": 509 510 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"})) 511 output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"})) 512 output.append(fmt.span(on=1)) 513 output.append(fmt.text(_(month_labels[month - 1]))) # zero-based labels 514 output.append(fmt.span(on=0)) 515 output.append(fmt.text(" ")) 516 output.append(fmt.span(on=1)) 517 output.append(fmt.text(year)) 518 output.append(fmt.span(on=0)) 519 output.append(fmt.div(on=0)) 520 521 output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"})) 522 523 for event_page, event_details in shown_events.get((year, month), []): 524 525 # Get a pretty version of the page name. 526 527 pretty_pagename = getPrettyPageName(event_page) 528 529 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 530 531 # Link to the page using the pretty name. 532 533 output.append(event_page.link_to_raw(request, wikiutil.escape(pretty_pagename))) 534 535 # Add the event details. 536 537 output.append(fmt.definition_list(on=1, attr={"class" : "event-details"})) 538 539 for key, value in event_details.items(): 540 output.append(fmt.definition_term(on=1)) 541 output.append(fmt.text(key)) 542 output.append(fmt.definition_term(on=0)) 543 output.append(fmt.definition_desc(on=1)) 544 output.append(fmt.text(value)) 545 output.append(fmt.definition_desc(on=0)) 546 547 output.append(fmt.definition_list(on=0)) 548 output.append(fmt.listitem(on=0)) 549 550 output.append(fmt.bullet_list(on=0)) 551 552 if mode == "list": 553 output.append(fmt.bullet_list(on=0)) 554 555 return ''.join(output) 556 557 # vim: tabstop=4 expandtab shiftwidth=4