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 import wikiutil 12 import EventAggregatorSupport 13 import calendar 14 15 linkToPage = EventAggregatorSupport.linkToPage 16 17 try: 18 set 19 except NameError: 20 from sets import Set as set 21 22 Dependencies = ['pages'] 23 24 # HTML-related functions. 25 26 def getColour(s): 27 colour = [0, 0, 0] 28 digit = 0 29 for c in s: 30 colour[digit] += ord(c) 31 colour[digit] = colour[digit] % 256 32 digit += 1 33 digit = digit % 3 34 return tuple(colour) 35 36 def getBlackOrWhite(colour): 37 if sum(colour) / 3.0 > 127: 38 return (0, 0, 0) 39 else: 40 return (255, 255, 255) 41 42 def getMonthActionQueryString(argname, month): 43 if month is not None: 44 return "%s=%04d-%02d" % ((argname,) + month) 45 else: 46 return "" 47 48 def getMonthQueryString(calendar_name, argname, month): 49 if month is not None: 50 return "%s-%s=%04d-%02d" % ((calendar_name, argname) + month) 51 else: 52 return "" 53 54 # Macro functions. 55 56 def execute(macro, args): 57 58 """ 59 Execute the 'macro' with the given 'args': an optional list of selected 60 category names (categories whose pages are to be shown), together with 61 optional named arguments of the following forms: 62 63 start=YYYY-MM shows event details starting from the specified month 64 start=current-N shows event details relative to the current month 65 end=YYYY-MM shows event details ending at the specified month 66 end=current+N shows event details relative to the current month 67 68 mode=calendar shows a calendar view of events 69 mode=list shows a list of events by month 70 mode=ics provides iCalendar data for the events 71 72 names=daily shows the name of an event on every day of that event 73 names=weekly shows the name of an event once per week 74 75 calendar=NAME uses the given NAME to provide request parameters which 76 can be used to control the calendar view 77 """ 78 79 request = macro.request 80 fmt = macro.formatter 81 page = fmt.page 82 _ = request.getText 83 84 # Interpret the arguments. 85 86 try: 87 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 88 except AttributeError: 89 parsed_args = args.split(",") 90 91 parsed_args = [arg for arg in parsed_args if arg] 92 93 # Get special arguments. 94 95 category_names = [] 96 calendar_start = None 97 calendar_end = None 98 mode = "calendar" 99 name_usage = "weekly" 100 calendar_name = None 101 102 for arg in parsed_args: 103 if arg.startswith("start="): 104 calendar_start = EventAggregatorSupport.getParameterMonth(arg[6:]) 105 106 elif arg.startswith("end="): 107 calendar_end = EventAggregatorSupport.getParameterMonth(arg[4:]) 108 109 elif arg.startswith("mode="): 110 mode = arg[5:] 111 112 elif arg.startswith("names="): 113 name_usage = arg[6:] 114 115 elif arg.startswith("calendar="): 116 calendar_name = arg[9:] 117 118 else: 119 category_names.append(arg) 120 121 # Find request parameters to override settings. 122 123 if calendar_name is not None: 124 calendar_start = EventAggregatorSupport.getFormMonth(request, calendar_name, "start") or calendar_start 125 calendar_end = EventAggregatorSupport.getFormMonth(request, calendar_name, "end") or calendar_end 126 127 # Get the events. 128 129 events, shown_events, all_shown_events, earliest, latest = \ 130 EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end) 131 132 # Get a concrete period of time. 133 134 first, last = EventAggregatorSupport.getConcretePeriod(calendar_start, calendar_end, earliest, latest) 135 136 # Define some useful navigation months. 137 138 if calendar_name is not None: 139 span = EventAggregatorSupport.span(first, last) 140 number_of_months = span[0] * 12 + span[1] + 1 141 142 previous_month_start = EventAggregatorSupport.prevmonth(first) 143 next_month_start = EventAggregatorSupport.nextmonth(first) 144 previous_month_end = EventAggregatorSupport.prevmonth(last) 145 next_month_end = EventAggregatorSupport.nextmonth(last) 146 147 previous_set_start = EventAggregatorSupport.monthupdate(first, -number_of_months) 148 next_set_start = EventAggregatorSupport.monthupdate(first, number_of_months) 149 previous_set_end = EventAggregatorSupport.monthupdate(last, -number_of_months) 150 next_set_end = EventAggregatorSupport.monthupdate(last, number_of_months) 151 152 # Make a calendar. 153 154 output = [] 155 156 # Output download controls. 157 158 download_all_link = "action=EventAggregatorSummary&doit=1&%s" % ( 159 "&".join([("category=%s" % name) for name in category_names]) 160 ) 161 download_link = download_all_link + ("&%s&%s" % ( 162 getMonthActionQueryString("start", calendar_start), 163 getMonthActionQueryString("end", calendar_end) 164 )) 165 subscribe_all_link = download_all_link + "&format=RSS" 166 subscribe_link = download_link + "&format=RSS" 167 168 output.append(fmt.div(on=1, css_class="event-controls")) 169 output.append(fmt.span(on=1, css_class="event-download")) 170 output.append(linkToPage(request, page, _("Download this view"), download_link)) 171 output.append(fmt.span(on=0)) 172 output.append(fmt.span(on=1, css_class="event-download")) 173 output.append(linkToPage(request, page, _("Download this calendar"), download_all_link)) 174 output.append(fmt.span(on=0)) 175 output.append(fmt.span(on=1, css_class="event-download")) 176 output.append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link)) 177 output.append(fmt.span(on=0)) 178 output.append(fmt.span(on=1, css_class="event-download")) 179 output.append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link)) 180 output.append(fmt.span(on=0)) 181 output.append(fmt.div(on=0)) 182 183 # Output top-level information. 184 185 if mode == "list": 186 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 187 188 # Visit all months in the requested range, or across known events. 189 190 for year, month in EventAggregatorSupport.daterange(first, last): 191 192 # Either output a calendar view... 193 194 if mode == "calendar": 195 196 # Output a month. 197 198 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 199 200 output.append(fmt.table_row(on=1)) 201 output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"})) 202 203 # Either write a month heading or produce a link for navigable 204 # calendars. 205 206 month_label = _(EventAggregatorSupport.getMonthLabel(month)) 207 208 if calendar_name is not None: 209 210 # Links to the previous set of months and to a calendar shifted 211 # back one month. 212 213 previous_set_link = "%s&%s" % ( 214 getMonthQueryString(calendar_name, "start", previous_set_start), 215 getMonthQueryString(calendar_name, "end", previous_set_end) 216 ) 217 previous_month_link = "%s&%s" % ( 218 getMonthQueryString(calendar_name, "start", previous_month_start), 219 getMonthQueryString(calendar_name, "end", previous_month_end) 220 ) 221 222 output.append(fmt.span(on=1, css_class="previous-month")) 223 output.append(linkToPage(request, page, "<<", previous_set_link)) 224 output.append(fmt.text(" ")) 225 output.append(linkToPage(request, page, "<", previous_month_link)) 226 output.append(fmt.span(on=0)) 227 228 # Links to the next set of months and to a calendar shifted 229 # forward one month. 230 231 next_set_link = "%s&%s" % ( 232 getMonthQueryString(calendar_name, "start", next_set_start), 233 getMonthQueryString(calendar_name, "end", next_set_end) 234 ) 235 next_month_link = "%s&%s" % ( 236 getMonthQueryString(calendar_name, "start", next_month_start), 237 getMonthQueryString(calendar_name, "end", next_month_end) 238 ) 239 240 output.append(fmt.span(on=1, css_class="next-month")) 241 output.append(linkToPage(request, page, ">", next_month_link)) 242 output.append(fmt.text(" ")) 243 output.append(linkToPage(request, page, ">>", next_set_link)) 244 output.append(fmt.span(on=0)) 245 246 # A link leading to this month being at the top of the calendar. 247 248 full_month_label = "%s %s" % (month_label, year) 249 end_month = EventAggregatorSupport.monthupdate((year, month), number_of_months - 1) 250 251 month_link = "%s&%s" % ( 252 getMonthQueryString(calendar_name, "start", (year, month)), 253 getMonthQueryString(calendar_name, "end", end_month) 254 ) 255 output.append(linkToPage(request, page, full_month_label, month_link)) 256 257 else: 258 output.append(fmt.span(on=1)) 259 output.append(fmt.text(month_label)) 260 output.append(fmt.span(on=0)) 261 output.append(fmt.text(" ")) 262 output.append(fmt.span(on=1)) 263 output.append(fmt.text(unicode(year))) 264 output.append(fmt.span(on=0)) 265 266 output.append(fmt.table_cell(on=0)) 267 output.append(fmt.table_row(on=0)) 268 269 # Weekday headings. 270 271 output.append(fmt.table_row(on=1)) 272 273 for weekday in range(0, 7): 274 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"})) 275 output.append(fmt.text(_(EventAggregatorSupport.getDayLabel(weekday)))) 276 output.append(fmt.table_cell(on=0)) 277 278 output.append(fmt.table_row(on=0)) 279 280 # Process the days of the month. 281 282 start_weekday, number_of_days = calendar.monthrange(year, month) 283 284 # The start weekday is the weekday of day number 1. 285 # Find the first day of the week, counting from below zero, if 286 # necessary, in order to land on the first day of the month as 287 # day number 1. 288 289 first_day = 1 - start_weekday 290 291 while first_day <= number_of_days: 292 293 # Find events in this week and determine how to mark them on the 294 # calendar. 295 296 week_start = (year, month, max(first_day, 1)) 297 week_end = (year, month, min(first_day + 6, number_of_days)) 298 299 week_coverage, week_events = EventAggregatorSupport.getCoverage( 300 week_start, week_end, shown_events.get((year, month), [])) 301 302 # Output a week, starting with the day numbers. 303 304 output.append(fmt.table_row(on=1)) 305 306 for weekday in range(0, 7): 307 day = first_day + weekday 308 date = (year, month, day) 309 310 # Output out-of-month days. 311 312 if day < 1 or day > number_of_days: 313 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"})) 314 output.append(fmt.table_cell(on=0)) 315 316 # Output normal days. 317 318 else: 319 if date in week_coverage: 320 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-busy", "colspan" : "3"})) 321 else: 322 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-empty", "colspan" : "3"})) 323 324 output.append(fmt.div(on=1)) 325 output.append(fmt.span(on=1, css_class="event-day-number")) 326 output.append(fmt.text(unicode(day))) 327 output.append(fmt.span(on=0)) 328 output.append(fmt.div(on=0)) 329 330 # End of day. 331 332 output.append(fmt.table_cell(on=0)) 333 334 # End of day numbers. 335 336 output.append(fmt.table_row(on=0)) 337 338 # Either generate empty days... 339 340 if not week_events: 341 output.append(fmt.table_row(on=1)) 342 343 for weekday in range(0, 7): 344 day = first_day + weekday 345 date = (year, month, day) 346 347 # Output out-of-month days. 348 349 if day < 1 or day > number_of_days: 350 output.append(fmt.table_cell(on=1, 351 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 352 output.append(fmt.table_cell(on=0)) 353 354 # Output empty days. 355 356 else: 357 output.append(fmt.table_cell(on=1, 358 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 359 360 output.append(fmt.table_row(on=0)) 361 362 # Or visit each set of scheduled events... 363 364 else: 365 for coverage, events in week_events: 366 367 # Output each set. 368 369 output.append(fmt.table_row(on=1)) 370 371 # Then, output day details. 372 373 for weekday in range(0, 7): 374 day = first_day + weekday 375 date = (year, month, day) 376 377 # Skip out-of-month days. 378 379 if day < 1 or day > number_of_days: 380 output.append(fmt.table_cell(on=1, 381 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 382 output.append(fmt.table_cell(on=0)) 383 continue 384 385 # Output the day. 386 387 if date not in coverage: 388 output.append(fmt.table_cell(on=1, 389 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 390 391 # Get event details for the current day. 392 393 for event_page, event_details in events: 394 if not (event_details["start"] <= date <= event_details["end"]): 395 continue 396 397 # Get basic properties of the event. 398 399 starts_today = event_details["start"] == date 400 ends_today = event_details["end"] == date 401 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details) 402 403 # Generate a colour for the event. 404 405 bg = getColour(event_page.page_name) 406 fg = getBlackOrWhite(bg) 407 style = ("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg)) 408 hidden_style = ("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + bg)) 409 410 # Determine if the event name should be shown. 411 412 start_of_period = starts_today or weekday == 0 or day == 1 413 414 if name_usage == "daily" or start_of_period: 415 hide_text = 0 416 else: 417 hide_text = 1 418 419 # Output start of day gap and determine whether 420 # any event content should be explicitly output 421 # for this day. 422 423 if starts_today: 424 425 # Single day events... 426 427 if ends_today: 428 colspan = 3 429 event_day_type = "event-day-single" 430 431 # Events starting today... 432 433 else: 434 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap"})) 435 output.append(fmt.table_cell(on=0)) 436 437 # Calculate the span of this cell. 438 # Events whose names appear on every day... 439 440 if name_usage == "daily": 441 colspan = 2 442 event_day_type = "event-day-starting" 443 444 # Events whose names appear once per week... 445 446 else: 447 if event_details["end"] <= week_end: 448 event_length = event_details["end"][2] - day + 1 449 colspan = (event_length - 2) * 3 + 4 450 else: 451 event_length = week_end[2] - day + 1 452 colspan = (event_length - 1) * 3 + 2 453 454 event_day_type = "event-day-multiple" 455 456 # Events continuing from a previous week... 457 458 elif start_of_period: 459 460 # End of continuing event... 461 462 if ends_today: 463 colspan = 2 464 event_day_type = "event-day-ending" 465 466 # Events continuing for at least one more day... 467 468 else: 469 470 # Calculate the span of this cell. 471 # Events whose names appear on every day... 472 473 if name_usage == "daily": 474 colspan = 3 475 event_day_type = "event-day-full" 476 477 # Events whose names appear once per week... 478 479 else: 480 if event_details["end"] <= week_end: 481 event_length = event_details["end"][2] - day + 1 482 colspan = (event_length - 1) * 3 + 2 483 else: 484 event_length = week_end[2] - day + 1 485 colspan = event_length * 3 486 487 event_day_type = "event-day-multiple" 488 489 # Continuing events whose names appear on every day... 490 491 elif name_usage == "daily": 492 if ends_today: 493 colspan = 2 494 event_day_type = "event-day-ending" 495 else: 496 colspan = 3 497 event_day_type = "event-day-full" 498 499 # Continuing events whose names appear once per week... 500 501 else: 502 colspan = None 503 504 # Output the main content only if it is not 505 # continuing from a previous day. 506 507 if colspan is not None: 508 509 # Colour the cell for continuing events. 510 511 attrs={ 512 "class" : "event-day-content event-day-busy %s" % event_day_type, 513 "colspan" : str(colspan) 514 } 515 516 if not (starts_today and ends_today): 517 attrs["style"] = hide_text and hidden_style or style 518 519 output.append(fmt.table_cell(on=1, attrs=attrs)) 520 521 # Output the event. 522 523 if starts_today and ends_today or not hide_text: 524 525 output.append(fmt.div(on=1, css_class="event-summary-box")) 526 output.append(fmt.div(on=1, css_class="event-summary", style=style)) 527 output.append(linkToPage(request, event_page, event_summary)) 528 output.append(fmt.div(on=0)) 529 530 # Add a pop-up element for long summaries. 531 532 output.append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 533 output.append(linkToPage(request, event_page, event_summary)) 534 output.append(fmt.div(on=0)) 535 536 output.append(fmt.div(on=0)) 537 538 # Output end of day content. 539 540 output.append(fmt.div(on=0)) 541 542 # Output end of day gap. 543 544 if ends_today and not starts_today: 545 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap"})) 546 output.append(fmt.table_cell(on=0)) 547 548 # End of day. 549 550 output.append(fmt.table_cell(on=0)) 551 552 # End of set. 553 554 output.append(fmt.table_row(on=0)) 555 556 # Add a spacer. 557 558 output.append(fmt.table_row(on=1)) 559 560 for weekday in range(0, 7): 561 day = first_day + weekday 562 css_classes = "event-day-spacer" 563 564 # Skip out-of-month days. 565 566 if day < 1 or day > number_of_days: 567 css_classes += " event-day-excluded" 568 569 output.append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"})) 570 output.append(fmt.table_cell(on=0)) 571 572 output.append(fmt.table_row(on=0)) 573 574 # Process the next week... 575 576 first_day += 7 577 578 # End of month. 579 580 output.append(fmt.table(on=0)) 581 582 # Or output a summary view... 583 584 elif mode == "list": 585 586 month_label = _(EventAggregatorSupport.getMonthLabel(month)) 587 588 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"})) 589 output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"})) 590 output.append(fmt.span(on=1)) 591 output.append(fmt.text(month_label)) 592 output.append(fmt.span(on=0)) 593 output.append(fmt.text(" ")) 594 output.append(fmt.span(on=1)) 595 output.append(fmt.text(unicode(year))) 596 output.append(fmt.span(on=0)) 597 output.append(fmt.div(on=0)) 598 599 output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"})) 600 601 # Get the events in order. 602 603 ordered_events = EventAggregatorSummary.getOrderedEvents(shown_events.get((year, month), [])) 604 605 # Show the events in order. 606 607 for event_page, event_details in ordered_events: 608 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details) 609 610 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 611 612 # Link to the page using the summary. 613 614 output.append(fmt.paragraph(on=1)) 615 output.append(linkToPage(request, event_page, event_summary)) 616 output.append(fmt.paragraph(on=0)) 617 618 # Start and end dates. 619 620 output.append(fmt.paragraph(on=1)) 621 output.append(fmt.span(on=1)) 622 output.append(fmt.text("%04d-%02d-%02d" % event_details["start"])) 623 output.append(fmt.span(on=0)) 624 output.append(fmt.text(" - ")) 625 output.append(fmt.span(on=1)) 626 output.append(fmt.text("%04d-%02d-%02d" % event_details["end"])) 627 output.append(fmt.span(on=0)) 628 output.append(fmt.paragraph(on=0)) 629 630 # Location. 631 632 if event_details.has_key("location"): 633 output.append(fmt.paragraph(on=1)) 634 output.append(fmt.text(event_details["location"])) 635 output.append(fmt.paragraph(on=1)) 636 637 # Topics. 638 639 if event_details.has_key("topics") or event_details.has_key("categories"): 640 output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 641 642 for topic in event_details.get("topics") or event_details.get("categories"): 643 output.append(fmt.listitem(on=1)) 644 output.append(fmt.text(topic)) 645 output.append(fmt.listitem(on=0)) 646 647 output.append(fmt.bullet_list(on=0)) 648 649 output.append(fmt.listitem(on=0)) 650 651 output.append(fmt.bullet_list(on=0)) 652 653 # Output top-level information. 654 655 # End of list view output. 656 657 if mode == "list": 658 output.append(fmt.bullet_list(on=0)) 659 660 return ''.join(output) 661 662 # vim: tabstop=4 expandtab shiftwidth=4