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