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 mode=ics provides iCalendar data for the events 185 186 names=daily shows the name of an event on every day of that event 187 names=weekly shows the name of an event once per week 188 """ 189 190 request = macro.request 191 fmt = macro.formatter 192 page = fmt.page 193 _ = request.getText 194 195 # Interpret the arguments. 196 197 try: 198 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 199 except AttributeError: 200 parsed_args = args.split(",") 201 202 parsed_args = [arg for arg in parsed_args if arg] 203 204 # Get special arguments. 205 206 category_names = [] 207 calendar_start = None 208 calendar_end = None 209 mode = "calendar" 210 name_usage = "daily" 211 212 for arg in parsed_args: 213 if arg.startswith("start="): 214 calendar_start = getMonth(arg[6:]) 215 elif arg.startswith("end="): 216 calendar_end = getMonth(arg[4:]) 217 elif arg.startswith("mode="): 218 mode = arg[5:] 219 elif arg.startswith("names="): 220 name_usage = arg[6:] 221 else: 222 category_names.append(arg) 223 224 # Generate a list of events found on pages belonging to the specified 225 # categories, as found in the macro arguments. 226 227 events = [] 228 shown_events = {} 229 all_shown_events = [] 230 231 earliest = None 232 latest = None 233 234 for category_name in category_names: 235 236 # Get the pages and page names in the category. 237 238 pages_in_category = getPages(category_name, request) 239 240 # Visit each page in the category. 241 242 for page_in_category in pages_in_category: 243 pagename = page_in_category.page_name 244 245 # Get a real page, not a result page. 246 247 real_page_in_category = Page(request, pagename) 248 event_details = getEventDetails(real_page_in_category) 249 250 # Define the event as the page together with its details. 251 252 event = (real_page_in_category, event_details) 253 events.append(event) 254 255 # Test for the suitability of the event. 256 257 if event_details.has_key("start") and event_details.has_key("end"): 258 259 start_month = event_details["start"][:2] 260 end_month = event_details["end"][:2] 261 262 # Compare the months of the dates to the requested calendar 263 # window, if any. 264 265 if (calendar_start is None or end_month >= calendar_start) and \ 266 (calendar_end is None or start_month <= calendar_end): 267 268 if earliest is None or start_month < earliest: 269 earliest = start_month 270 if latest is None or end_month > latest: 271 latest = end_month 272 273 # Store the event in the month-specific dictionary. 274 275 first = max(start_month, calendar_start or start_month) 276 last = min(end_month, calendar_end or end_month) 277 278 for event_month in daterange(first, last): 279 if not shown_events.has_key(event_month): 280 shown_events[event_month] = [] 281 shown_events[event_month].append(event) 282 all_shown_events.append(event) 283 284 # Make a calendar. 285 286 output = [] 287 288 # Output top-level information. 289 290 if mode == "list": 291 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 292 293 # Visit all months in the requested range, or across known events. 294 295 first = calendar_start or earliest 296 last = calendar_end or latest 297 298 for year, month in daterange(first, last): 299 300 # Either output a calendar view... 301 302 if mode == "calendar": 303 304 # Output a month. 305 306 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 307 308 output.append(fmt.table_row(on=1)) 309 output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "7"})) 310 output.append(fmt.span(on=1)) 311 output.append(fmt.text(_(month_labels[month - 1]))) # zero-based labels 312 output.append(fmt.span(on=0)) 313 output.append(fmt.text(" ")) 314 output.append(fmt.span(on=1)) 315 output.append(fmt.text(year)) 316 output.append(fmt.span(on=0)) 317 output.append(fmt.table_cell(on=0)) 318 output.append(fmt.table_row(on=0)) 319 320 # Weekday headings. 321 322 output.append(fmt.table_row(on=1)) 323 324 for weekday in range(0, 7): 325 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading"})) 326 output.append(fmt.text(_(weekday_labels[weekday]))) 327 output.append(fmt.table_cell(on=0)) 328 329 output.append(fmt.table_row(on=0)) 330 331 # Process the days of the month. 332 333 start_weekday, number_of_days = calendar.monthrange(year, month) 334 335 # The start weekday is the weekday of day number 1. 336 # Find the first day of the week, counting from below zero, if 337 # necessary, in order to land on the first day of the month as 338 # day number 1. 339 340 first_day = 1 - start_weekday 341 342 while first_day <= number_of_days: 343 344 # Find events in this week and determine how to mark them on the 345 # calendar. 346 347 week_events = [] 348 week_coverage = set() 349 350 week_start = (year, month, max(first_day, 1)) 351 week_end = (year, month, min(first_day + 6, number_of_days)) 352 353 # Get event details. 354 355 for event in shown_events.get((year, month), []): 356 event_page, event_details = event 357 358 # Test for the event in the current week. 359 360 if event_details["start"] <= week_end and event_details["end"] >= week_start: 361 362 # Find the coverage of this week for the event. 363 364 event_start = max(event_details["start"], week_start) 365 event_end = min(event_details["end"], week_end) 366 event_coverage = set(daterange(event_start, event_end)) 367 368 # Update the overall coverage. 369 370 week_coverage.update(event_coverage) 371 372 # Try and fit the event into the events list. 373 374 for i, (coverage, events) in enumerate(week_events): 375 376 # Where the event does not overlap with the current 377 # element, add it alongside existing events. 378 379 if not coverage.intersection(event_coverage): 380 events.append(event) 381 week_events[i] = coverage.union(event_coverage), events 382 break 383 384 # Make a new element in the list if the event cannot be 385 # marked alongside existing events. 386 387 else: 388 week_events.append((event_coverage, [event])) 389 390 # Output a week, starting with the day numbers. 391 392 output.append(fmt.table_row(on=1)) 393 394 for weekday in range(0, 7): 395 day = first_day + weekday 396 date = (year, month, day) 397 398 # Output out-of-month days. 399 400 if day < 1 or day > number_of_days: 401 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-excluded"})) 402 output.append(fmt.table_cell(on=0)) 403 404 # Output normal days. 405 406 else: 407 if date in week_coverage: 408 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-busy"})) 409 else: 410 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-empty"})) 411 412 output.append(fmt.div(on=1)) 413 output.append(fmt.span(on=1, css_class="event-day-number")) 414 output.append(fmt.text(day)) 415 output.append(fmt.span(on=0)) 416 output.append(fmt.div(on=0)) 417 418 # End of day. 419 420 output.append(fmt.table_cell(on=0)) 421 422 # End of day numbers. 423 424 output.append(fmt.table_row(on=0)) 425 426 # Either generate empty days... 427 428 if not week_events: 429 output.append(fmt.table_row(on=1)) 430 431 for weekday in range(0, 7): 432 day = first_day + weekday 433 date = (year, month, day) 434 435 # Output 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 441 # Output empty days. 442 443 else: 444 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-empty"})) 445 446 output.append(fmt.table_row(on=0)) 447 448 # Or visit each set of scheduled events... 449 450 else: 451 for coverage, events in week_events: 452 453 # Output each set. 454 455 output.append(fmt.table_row(on=1)) 456 457 # Then, output day details. 458 459 for weekday in range(0, 7): 460 day = first_day + weekday 461 date = (year, month, day) 462 463 # Skip out-of-month days. 464 465 if day < 1 or day > number_of_days: 466 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-excluded"})) 467 output.append(fmt.table_cell(on=0)) 468 continue 469 470 # Output the day. 471 472 if date in coverage: 473 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-busy"})) 474 else: 475 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-empty"})) 476 477 # Get event details for the current day. 478 479 for event_page, event_details in events: 480 if not (event_details["start"] <= date <= event_details["end"]): 481 continue 482 483 # Get a pretty version of the page name. 484 485 pretty_pagename = getPrettyPageName(event_page) 486 487 # Generate a colour for the event. 488 489 bg = getColour(event_page.page_name) 490 fg = getBlackOrWhite(bg) 491 492 css_classes = ["event-summary"] 493 494 if event_details["start"] == date: 495 css_classes.append("event-starts") 496 start_of_event = 1 497 else: 498 start_of_event = 0 499 500 if event_details["end"] == date: 501 css_classes.append("event-ends") 502 503 # Output the event. 504 505 if name_usage == "daily" or start_of_event or weekday == 0 or day == 1: 506 hide_text = 0 507 else: 508 hide_text = 1 509 510 output.append(fmt.div(on=1, css_class=(" ".join(css_classes)), 511 style=("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg)))) 512 513 if not hide_text: 514 output.append(event_page.link_to_raw(request, wikiutil.escape(pretty_pagename))) 515 516 output.append(fmt.div(on=0)) 517 518 # End of day. 519 520 output.append(fmt.table_cell(on=0)) 521 522 # End of set. 523 524 output.append(fmt.table_row(on=0)) 525 526 # Process the next week... 527 528 first_day += 7 529 530 # End of month. 531 532 output.append(fmt.table(on=0)) 533 534 # Or output a summary view... 535 536 elif mode == "list": 537 538 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"})) 539 output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"})) 540 output.append(fmt.span(on=1)) 541 output.append(fmt.text(_(month_labels[month - 1]))) # zero-based labels 542 output.append(fmt.span(on=0)) 543 output.append(fmt.text(" ")) 544 output.append(fmt.span(on=1)) 545 output.append(fmt.text(year)) 546 output.append(fmt.span(on=0)) 547 output.append(fmt.div(on=0)) 548 549 output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"})) 550 551 for event_page, event_details in shown_events.get((year, month), []): 552 553 # Get a pretty version of the page name. 554 555 pretty_pagename = getPrettyPageName(event_page) 556 557 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 558 559 # Link to the page using the pretty name. 560 561 output.append(event_page.link_to_raw(request, wikiutil.escape(pretty_pagename))) 562 563 # Add the event details. 564 565 output.append(fmt.definition_list(on=1, attr={"class" : "event-details"})) 566 567 for key, value in event_details.items(): 568 output.append(fmt.definition_term(on=1)) 569 output.append(fmt.text(key)) 570 output.append(fmt.definition_term(on=0)) 571 output.append(fmt.definition_desc(on=1)) 572 output.append(fmt.text(value)) 573 output.append(fmt.definition_desc(on=0)) 574 575 output.append(fmt.definition_list(on=0)) 576 output.append(fmt.listitem(on=0)) 577 578 output.append(fmt.bullet_list(on=0)) 579 580 # Output top-level information. 581 582 # Output iCalendar data... 583 584 if mode == "ics": 585 586 # Output the calendar details as preformatted text. 587 588 output.append(fmt.preformatted(on=1)) 589 output.append(fmt.text("BEGIN:VCALENDAR")) 590 output.append(fmt.linebreak()) 591 output.append(fmt.text("VERSION:1.0")) 592 output.append(fmt.linebreak()) 593 594 for event_page, event_details in all_shown_events: 595 596 # Get a pretty version of the page name. 597 598 pretty_pagename = getPrettyPageName(event_page) 599 600 # Output the event details. 601 602 output.append(fmt.text("BEGIN:VEVENT")) 603 output.append(fmt.linebreak()) 604 output.append(fmt.text("SUMMARY:%s" % pretty_pagename)) 605 output.append(fmt.linebreak()) 606 output.append(fmt.text("URL:%s" % request.getQualifiedURL(event_page.url(request)))) 607 output.append(fmt.linebreak()) 608 output.append(fmt.text("DTSTART:%04d%02d%02d" % event_details["start"])) 609 output.append(fmt.linebreak()) 610 output.append(fmt.text("DTEND:%04d%02d%02d" % event_details["end"])) 611 output.append(fmt.linebreak()) 612 output.append(fmt.text("END:VEVENT")) 613 output.append(fmt.linebreak()) 614 615 output.append(fmt.text("END:VCALENDAR")) 616 output.append(fmt.linebreak()) 617 output.append(fmt.preformatted(on=0)) 618 619 # End of list view output. 620 621 elif mode == "list": 622 output.append(fmt.bullet_list(on=0)) 623 624 return ''.join(output) 625 626 # vim: tabstop=4 expandtab shiftwidth=4