1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator Macro 4 5 @copyright: 2008, 2009, 2010, 2011 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 Dependencies = ['pages'] 18 19 # Abstractions. 20 21 class View: 22 23 "A view of the event calendar." 24 25 def __init__(self, page, calendar_name, raw_calendar_start, raw_calendar_end, 26 original_calendar_start, original_calendar_end, calendar_start, calendar_end, 27 first, last, category_names, template_name, parent_name, mode, name_usage): 28 29 """ 30 Initialise the view with the current 'page', a 'calendar_name' (which 31 may be None), the 'raw_calendar_start' and 'raw_calendar_end' (which 32 are the actual start and end values provided by the request), the 33 calculated 'original_calendar_start' and 'original_calendar_end' (which 34 are the result of calculating the calendar's limits from the raw start 35 and end values), and the requested, calculated 'calendar_start' and 36 'calendar_end' (which may involve different start and end values due to 37 navigation in the user interface), along with the 'first' and 'last' 38 months of event coverage. 39 40 The additional 'category_names', 'template_name', 'parent_name' and 41 'mode' parameters are used to configure the links employed by the view. 42 43 The 'name_usage' parameter controls how names are shown on calendar mode 44 events, such as how often labels are repeated. 45 """ 46 47 self.page = page 48 self.calendar_name = calendar_name 49 self.raw_calendar_start = raw_calendar_start 50 self.raw_calendar_end = raw_calendar_end 51 self.original_calendar_start = original_calendar_start 52 self.original_calendar_end = original_calendar_end 53 self.calendar_start = calendar_start 54 self.calendar_end = calendar_end 55 self.template_name = template_name 56 self.parent_name = parent_name 57 self.mode = mode 58 self.name_usage = name_usage 59 60 self.category_name_parameters = "&".join([("category=%s" % name) for name in category_names]) 61 62 if self.calendar_name is not None: 63 64 # Store the view parameters. 65 66 self.number_of_months = (last - first).months() + 1 67 68 self.previous_month_start = first.previous_month() 69 self.next_month_start = first.next_month() 70 self.previous_month_end = last.previous_month() 71 self.next_month_end = last.next_month() 72 73 self.previous_set_start = first.month_update(-self.number_of_months) 74 self.next_set_start = first.month_update(self.number_of_months) 75 self.previous_set_end = last.month_update(-self.number_of_months) 76 self.next_set_end = last.month_update(self.number_of_months) 77 78 def getQualifiedParameterName(self, argname): 79 80 "Return the 'argname' qualified using the calendar name." 81 82 return EventAggregatorSupport.getQualifiedParameterName(self.calendar_name, argname) 83 84 def getDateQueryString(self, argname, date, prefix=1): 85 86 """ 87 Return a query string fragment for the given 'argname', referring to the 88 month given by the specified 'year_month' object, appropriate for this 89 calendar. 90 91 If 'prefix' is specified and set to a false value, the parameters in the 92 query string will not be calendar-specific, but could be used with the 93 summary action. 94 """ 95 96 suffixes = ["year", "month", "day"] 97 98 if date is not None: 99 args = [] 100 for suffix, value in zip(suffixes, date.as_tuple()): 101 suffixed_argname = "%s-%s" % (argname, suffix) 102 if prefix: 103 suffixed_argname = self.getQualifiedParameterName(suffixed_argname) 104 args.append("%s=%s" % (suffixed_argname, value)) 105 return "&".join(args) 106 else: 107 return "" 108 109 def getRawDateQueryString(self, argname, date, prefix=1): 110 111 """ 112 Return a query string fragment for the given 'argname', referring to the 113 date given by the specified 'date' value, appropriate for this 114 calendar. 115 116 If 'prefix' is specified and set to a false value, the parameters in the 117 query string will not be calendar-specific, but could be used with the 118 summary action. 119 """ 120 121 if date is not None: 122 if prefix: 123 argname = self.getQualifiedParameterName(argname) 124 return "%s=%s" % (argname, date) 125 else: 126 return "" 127 128 def getNavigationLink(self, start, end, mode=None): 129 130 """ 131 Return a query string fragment for navigation to a view showing months 132 from 'start' to 'end' inclusive, with the optional 'mode' indicating the 133 view style. 134 """ 135 136 return "%s&%s&%s=%s" % ( 137 self.getRawDateQueryString("start", start), 138 self.getRawDateQueryString("end", end), 139 self.getQualifiedParameterName("mode"), mode or self.mode 140 ) 141 142 def getFullDateLabel(self, date): 143 page = self.page 144 request = page.request 145 return EventAggregatorSupport.getFullDateLabel(request, date) 146 147 def getFullMonthLabel(self, year_month): 148 page = self.page 149 request = page.request 150 return EventAggregatorSupport.getFullMonthLabel(request, year_month) 151 152 def writeDownloadControls(self): 153 page = self.page 154 request = page.request 155 fmt = page.formatter 156 _ = request.getText 157 158 output = [] 159 160 # Generate the links. 161 162 download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s&%s" % ( 163 self.parent_name or "", 164 self.mode == "day" and "date" or "month", 165 self.category_name_parameters 166 ) 167 download_all_link = download_dialogue_link + "&doit=1" 168 download_link = download_all_link + ("&%s&%s" % ( 169 self.getDateQueryString("start", self.calendar_start, prefix=0), 170 self.getDateQueryString("end", self.calendar_end, prefix=0) 171 )) 172 173 # Subscription links just explicitly select the RSS format. 174 175 subscribe_dialogue_link = download_dialogue_link + "&format=RSS" 176 subscribe_all_link = download_all_link + "&format=RSS" 177 subscribe_link = download_link + "&format=RSS" 178 179 # Adjust the "download all" and "subscribe all" links if the calendar 180 # has an inherent period associated with it. 181 182 period_limits = [] 183 184 if self.raw_calendar_start: 185 period_limits.append("&%s" % 186 self.getRawDateQueryString("start", self.raw_calendar_start, prefix=0) 187 ) 188 if self.raw_calendar_end: 189 period_limits.append("&%s" % 190 self.getRawDateQueryString("end", self.raw_calendar_end, prefix=0) 191 ) 192 193 period_limits = "".join(period_limits) 194 195 download_dialogue_link += period_limits 196 download_all_link += period_limits 197 subscribe_dialogue_link += period_limits 198 subscribe_all_link += period_limits 199 200 # Pop-up descriptions of the downloadable calendars. 201 202 get_label = self.mode == "day" and self.getFullDateLabel or self.getFullMonthLabel 203 204 calendar_period = "%s - %s" % ( 205 get_label(self.calendar_start), 206 get_label(self.calendar_end) 207 ) 208 original_calendar_period = "%s - %s" % ( 209 get_label(self.original_calendar_start), 210 get_label(self.original_calendar_end) 211 ) 212 raw_calendar_period = "%s - %s" % (self.raw_calendar_start, self.raw_calendar_end) 213 214 # Write the controls. 215 216 # Download controls. 217 218 output.append(fmt.div(on=1, css_class="event-download-controls")) 219 output.append(fmt.span(on=1, css_class="event-download")) 220 output.append(linkToPage(request, page, _("Download this view"), download_link)) 221 output.append(fmt.span(on=1, css_class="event-download-popup")) 222 output.append(fmt.text(calendar_period)) 223 output.append(fmt.span(on=0)) 224 output.append(fmt.span(on=0)) 225 226 output.append(fmt.span(on=1, css_class="event-download")) 227 output.append(linkToPage(request, page, _("Download this calendar"), download_all_link)) 228 output.append(fmt.span(on=1, css_class="event-download-popup")) 229 output.append(fmt.span(on=1, css_class="event-download-period")) 230 output.append(fmt.text(original_calendar_period)) 231 output.append(fmt.span(on=0)) 232 output.append(fmt.span(on=1, css_class="event-download-period-raw")) 233 output.append(fmt.text(raw_calendar_period)) 234 output.append(fmt.span(on=0)) 235 output.append(fmt.span(on=0)) 236 output.append(fmt.span(on=0)) 237 238 output.append(fmt.span(on=1, css_class="event-download")) 239 output.append(linkToPage(request, page, _("Download..."), download_dialogue_link)) 240 output.append(fmt.span(on=1, css_class="event-download-popup")) 241 output.append(fmt.text(_("Edit download options"))) 242 output.append(fmt.span(on=0)) 243 output.append(fmt.span(on=0)) 244 245 # Subscription controls. 246 247 output.append(fmt.span(on=1, css_class="event-download")) 248 output.append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link)) 249 output.append(fmt.span(on=1, css_class="event-download-popup")) 250 output.append(fmt.text(calendar_period)) 251 output.append(fmt.span(on=0)) 252 output.append(fmt.span(on=0)) 253 254 output.append(fmt.span(on=1, css_class="event-download")) 255 output.append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link)) 256 output.append(fmt.span(on=1, css_class="event-download-popup")) 257 output.append(fmt.span(on=1, css_class="event-download-period")) 258 output.append(fmt.text(original_calendar_period)) 259 output.append(fmt.span(on=0)) 260 output.append(fmt.span(on=1, css_class="event-download-period-raw")) 261 output.append(fmt.text(raw_calendar_period)) 262 output.append(fmt.span(on=0)) 263 output.append(fmt.span(on=0)) 264 output.append(fmt.span(on=0)) 265 266 output.append(fmt.span(on=1, css_class="event-download")) 267 output.append(linkToPage(request, page, _("Subscribe..."), subscribe_dialogue_link)) 268 output.append(fmt.span(on=1, css_class="event-download-popup")) 269 output.append(fmt.text(_("Edit subscription options"))) 270 output.append(fmt.span(on=0)) 271 output.append(fmt.span(on=0)) 272 output.append(fmt.div(on=0)) 273 274 return "".join(output) 275 276 def writeViewControls(self): 277 page = self.page 278 request = page.request 279 fmt = page.formatter 280 _ = request.getText 281 282 output = [] 283 284 calendar_link = self.getNavigationLink( 285 self.calendar_start, self.calendar_end, "calendar" 286 ) 287 list_link = self.getNavigationLink( 288 self.calendar_start, self.calendar_end, "list" 289 ) 290 table_link = self.getNavigationLink( 291 self.calendar_start, self.calendar_end, "table" 292 ) 293 294 # Write the controls. 295 296 output.append(fmt.div(on=1, css_class="event-view-controls")) 297 output.append(fmt.span(on=1, css_class="event-view")) 298 output.append(linkToPage(request, page, _("View as calendar"), calendar_link)) 299 output.append(fmt.span(on=0)) 300 output.append(fmt.span(on=1, css_class="event-view")) 301 output.append(linkToPage(request, page, _("View as list"), list_link)) 302 output.append(fmt.span(on=0)) 303 output.append(fmt.span(on=1, css_class="event-view")) 304 output.append(linkToPage(request, page, _("View as table"), table_link)) 305 output.append(fmt.span(on=0)) 306 output.append(fmt.div(on=0)) 307 308 return "".join(output) 309 310 def writeMonthHeading(self, year_month): 311 page = self.page 312 request = page.request 313 fmt = page.formatter 314 _ = request.getText 315 full_month_label = self.getFullMonthLabel(year_month) 316 317 output = [] 318 319 # Prepare navigation links. 320 321 if self.calendar_name is not None: 322 calendar_name = self.calendar_name 323 324 # Links to the previous set of months and to a calendar shifted 325 # back one month. 326 327 previous_set_link = self.getNavigationLink( 328 self.previous_set_start, self.previous_set_end 329 ) 330 previous_month_link = self.getNavigationLink( 331 self.previous_month_start, self.previous_month_end 332 ) 333 334 # Links to the next set of months and to a calendar shifted 335 # forward one month. 336 337 next_set_link = self.getNavigationLink( 338 self.next_set_start, self.next_set_end 339 ) 340 next_month_link = self.getNavigationLink( 341 self.next_month_start, self.next_month_end 342 ) 343 344 # A link leading to this month being at the top of the calendar. 345 346 end_month = year_month.month_update(self.number_of_months - 1) 347 348 month_link = self.getNavigationLink(year_month, end_month) 349 350 output.append(fmt.span(on=1, css_class="previous-month")) 351 output.append(linkToPage(request, page, "<<", previous_set_link)) 352 output.append(fmt.text(" ")) 353 output.append(linkToPage(request, page, "<", previous_month_link)) 354 output.append(fmt.span(on=0)) 355 356 output.append(fmt.span(on=1, css_class="next-month")) 357 output.append(linkToPage(request, page, ">", next_month_link)) 358 output.append(fmt.text(" ")) 359 output.append(linkToPage(request, page, ">>", next_set_link)) 360 output.append(fmt.span(on=0)) 361 362 output.append(linkToPage(request, page, full_month_label, month_link)) 363 364 else: 365 output.append(fmt.span(on=1)) 366 output.append(fmt.text(full_month_label)) 367 output.append(fmt.span(on=0)) 368 369 return "".join(output) 370 371 def writeDayNumberLinked(self, date): 372 page = self.page 373 request = page.request 374 fmt = page.formatter 375 _ = request.getText 376 377 year, month, day = date.as_tuple() 378 output = [] 379 380 # Prepare navigation details for the calendar shown with the new event 381 # form. 382 383 navigation_link = self.getNavigationLink( 384 self.calendar_start, self.calendar_end, self.mode 385 ) 386 387 # Prepare the link to the new event form, incorporating the above 388 # calendar parameters. 389 390 new_event_link = "action=EventAggregatorNewEvent&start-day=%d&start-month=%d&start-year=%d" \ 391 "&%s&template=%s&parent=%s&%s" % ( 392 day, month, year, self.category_name_parameters, self.template_name, self.parent_name or "", 393 navigation_link) 394 395 output.append(fmt.div(on=1)) 396 output.append(fmt.span(on=1, css_class="event-day-number")) 397 output.append(linkToPage(request, page, unicode(day), new_event_link)) 398 output.append(fmt.span(on=0)) 399 output.append(fmt.div(on=0)) 400 401 return "".join(output) 402 403 # Common layout methods. 404 405 def getEventStyle(self, colour_seed): 406 407 "Generate colour style information using the given 'colour_seed'." 408 409 bg = getColour(colour_seed) 410 fg = getBlackOrWhite(bg) 411 return "background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg) 412 413 def writeEventSummaryBox(self, event): 414 415 "Return an event summary box linking to the given 'event'." 416 417 page = self.page 418 request = page.request 419 fmt = page.formatter 420 421 output = [] 422 423 event_page = event.getPage() 424 event_details = event.getDetails() 425 event_summary = event.getSummary(self.parent_name) 426 427 is_ambiguous = event_details["start"].ambiguous() or event_details["end"].ambiguous() 428 style = self.getEventStyle(event_summary) 429 430 # The event box contains the summary, alongside 431 # other elements. 432 433 output.append(fmt.div(on=1, css_class="event-summary-box")) 434 output.append(fmt.div(on=1, css_class="event-summary", style=style)) 435 436 if is_ambiguous: 437 output.append(fmt.icon("/!\\")) 438 439 output.append(event_page.linkToPage(request, event_summary)) 440 output.append(fmt.div(on=0)) 441 442 # Add a pop-up element for long summaries. 443 444 output.append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 445 446 if is_ambiguous: 447 output.append(fmt.icon("/!\\")) 448 449 output.append(event_page.linkToPage(request, event_summary)) 450 output.append(fmt.div(on=0)) 451 452 output.append(fmt.div(on=0)) 453 454 return "".join(output) 455 456 # Calendar layout methods. 457 458 def writeMonthTableHeading(self, year_month): 459 page = self.page 460 fmt = page.formatter 461 462 output = [] 463 output.append(fmt.table_row(on=1)) 464 output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"})) 465 466 output.append(self.writeMonthHeading(year_month)) 467 468 output.append(fmt.table_cell(on=0)) 469 output.append(fmt.table_row(on=0)) 470 471 return "".join(output) 472 473 def writeWeekdayHeadings(self): 474 page = self.page 475 request = page.request 476 fmt = page.formatter 477 _ = request.getText 478 479 output = [] 480 output.append(fmt.table_row(on=1)) 481 482 for weekday in range(0, 7): 483 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"})) 484 output.append(fmt.text(_(EventAggregatorSupport.getDayLabel(weekday)))) 485 output.append(fmt.table_cell(on=0)) 486 487 output.append(fmt.table_row(on=0)) 488 return "".join(output) 489 490 def writeDayNumbers(self, first_day, number_of_days, month, coverage): 491 page = self.page 492 fmt = page.formatter 493 494 output = [] 495 output.append(fmt.table_row(on=1)) 496 497 for weekday in range(0, 7): 498 day = first_day + weekday 499 date = month.as_date(day) 500 501 # Output out-of-month days. 502 503 if day < 1 or day > number_of_days: 504 output.append(fmt.table_cell(on=1, 505 attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"})) 506 output.append(fmt.table_cell(on=0)) 507 508 # Output normal days. 509 510 else: 511 if date in coverage: 512 output.append(fmt.table_cell(on=1, 513 attrs={"class" : "event-day-heading event-day-busy", "colspan" : "3"})) 514 else: 515 output.append(fmt.table_cell(on=1, 516 attrs={"class" : "event-day-heading event-day-empty", "colspan" : "3"})) 517 518 # Output the day number, making a link to a new event 519 # action. 520 521 output.append(self.writeDayNumberLinked(date)) 522 523 # End of day. 524 525 output.append(fmt.table_cell(on=0)) 526 527 # End of day numbers. 528 529 output.append(fmt.table_row(on=0)) 530 return "".join(output) 531 532 def writeEmptyWeek(self, first_day, number_of_days): 533 page = self.page 534 fmt = page.formatter 535 536 output = [] 537 output.append(fmt.table_row(on=1)) 538 539 for weekday in range(0, 7): 540 day = first_day + weekday 541 542 # Output out-of-month days. 543 544 if day < 1 or day > number_of_days: 545 output.append(fmt.table_cell(on=1, 546 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 547 output.append(fmt.table_cell(on=0)) 548 549 # Output empty days. 550 551 else: 552 output.append(fmt.table_cell(on=1, 553 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 554 555 output.append(fmt.table_row(on=0)) 556 return "".join(output) 557 558 def writeWeekSlots(self, first_day, number_of_days, month, week_end, week_slots): 559 output = [] 560 561 locations = week_slots.keys() 562 locations.sort(EventAggregatorSupport.sort_none_first) 563 564 # Visit each slot corresponding to a location (or no location). 565 566 for location in locations: 567 568 # Visit each coverage span, presenting the events in the span. 569 570 for events in week_slots[location]: 571 572 # Output each set. 573 574 output.append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events)) 575 576 # Add a spacer. 577 578 output.append(self.writeWeekSpacer(first_day, number_of_days)) 579 580 return "".join(output) 581 582 def writeWeekSlot(self, first_day, number_of_days, month, week_end, events): 583 page = self.page 584 request = page.request 585 fmt = page.formatter 586 587 output = [] 588 output.append(fmt.table_row(on=1)) 589 590 # Then, output day details. 591 592 for weekday in range(0, 7): 593 day = first_day + weekday 594 date = month.as_date(day) 595 596 # Skip out-of-month days. 597 598 if day < 1 or day > number_of_days: 599 output.append(fmt.table_cell(on=1, 600 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 601 output.append(fmt.table_cell(on=0)) 602 continue 603 604 # Output the day. 605 606 if date not in events: 607 output.append(fmt.table_cell(on=1, 608 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 609 610 # Get event details for the current day. 611 612 for event in events: 613 event_page = event.getPage() 614 event_details = event.getDetails() 615 616 if date not in event: 617 continue 618 619 # Get basic properties of the event. 620 621 starts_today = event_details["start"] == date 622 ends_today = event_details["end"] == date 623 event_summary = event.getSummary(self.parent_name) 624 625 style = self.getEventStyle(event_summary) 626 627 # Determine if the event name should be shown. 628 629 start_of_period = starts_today or weekday == 0 or day == 1 630 631 if self.name_usage == "daily" or start_of_period: 632 hide_text = 0 633 else: 634 hide_text = 1 635 636 # Output start of day gap and determine whether 637 # any event content should be explicitly output 638 # for this day. 639 640 if starts_today: 641 642 # Single day events... 643 644 if ends_today: 645 colspan = 3 646 event_day_type = "event-day-single" 647 648 # Events starting today... 649 650 else: 651 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap"})) 652 output.append(fmt.table_cell(on=0)) 653 654 # Calculate the span of this cell. 655 # Events whose names appear on every day... 656 657 if self.name_usage == "daily": 658 colspan = 2 659 event_day_type = "event-day-starting" 660 661 # Events whose names appear once per week... 662 663 else: 664 if event_details["end"] <= week_end: 665 event_length = event_details["end"].day() - day + 1 666 colspan = (event_length - 2) * 3 + 4 667 else: 668 event_length = week_end.day() - day + 1 669 colspan = (event_length - 1) * 3 + 2 670 671 event_day_type = "event-day-multiple" 672 673 # Events continuing from a previous week... 674 675 elif start_of_period: 676 677 # End of continuing event... 678 679 if ends_today: 680 colspan = 2 681 event_day_type = "event-day-ending" 682 683 # Events continuing for at least one more day... 684 685 else: 686 687 # Calculate the span of this cell. 688 # Events whose names appear on every day... 689 690 if self.name_usage == "daily": 691 colspan = 3 692 event_day_type = "event-day-full" 693 694 # Events whose names appear once per week... 695 696 else: 697 if event_details["end"] <= week_end: 698 event_length = event_details["end"].day() - day + 1 699 colspan = (event_length - 1) * 3 + 2 700 else: 701 event_length = week_end.day() - day + 1 702 colspan = event_length * 3 703 704 event_day_type = "event-day-multiple" 705 706 # Continuing events whose names appear on every day... 707 708 elif self.name_usage == "daily": 709 if ends_today: 710 colspan = 2 711 event_day_type = "event-day-ending" 712 else: 713 colspan = 3 714 event_day_type = "event-day-full" 715 716 # Continuing events whose names appear once per week... 717 718 else: 719 colspan = None 720 721 # Output the main content only if it is not 722 # continuing from a previous day. 723 724 if colspan is not None: 725 726 # Colour the cell for continuing events. 727 728 attrs={ 729 "class" : "event-day-content event-day-busy %s" % event_day_type, 730 "colspan" : str(colspan) 731 } 732 733 if not (starts_today and ends_today): 734 attrs["style"] = style 735 736 output.append(fmt.table_cell(on=1, attrs=attrs)) 737 738 # Output the event. 739 740 if starts_today and ends_today or not hide_text: 741 output.append(self.writeEventSummaryBox(event)) 742 743 output.append(fmt.table_cell(on=0)) 744 745 # Output end of day gap. 746 747 if ends_today and not starts_today: 748 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap"})) 749 output.append(fmt.table_cell(on=0)) 750 751 # End of set. 752 753 output.append(fmt.table_row(on=0)) 754 return "".join(output) 755 756 def writeWeekSpacer(self, first_day, number_of_days): 757 page = self.page 758 fmt = page.formatter 759 760 output = [] 761 output.append(fmt.table_row(on=1)) 762 763 for weekday in range(0, 7): 764 day = first_day + weekday 765 css_classes = "event-day-spacer" 766 767 # Skip out-of-month days. 768 769 if day < 1 or day > number_of_days: 770 css_classes += " event-day-excluded" 771 772 output.append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"})) 773 output.append(fmt.table_cell(on=0)) 774 775 output.append(fmt.table_row(on=0)) 776 return "".join(output) 777 778 # Day layout methods. 779 780 def writeDayHeading(self, date, colspan=1): 781 page = self.page 782 request = page.request 783 fmt = page.formatter 784 _ = request.getText 785 full_date_label = self.getFullDateLabel(date) 786 787 output = [] 788 output.append(fmt.table_row(on=1)) 789 790 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : str(colspan)})) 791 output.append(fmt.text(full_date_label)) 792 output.append(fmt.table_cell(on=0)) 793 794 output.append(fmt.table_row(on=0)) 795 return "".join(output) 796 797 def writeEmptyDay(self, date): 798 page = self.page 799 fmt = page.formatter 800 801 output = [] 802 output.append(fmt.table_row(on=1)) 803 804 output.append(fmt.table_cell(on=1, 805 attrs={"class" : "event-day-content event-day-empty"})) 806 807 output.append(fmt.table_row(on=0)) 808 return "".join(output) 809 810 def writeDaySlots(self, date, full_coverage, day_slots): 811 page = self.page 812 fmt = page.formatter 813 814 output = [] 815 816 locations = day_slots.keys() 817 locations.sort(EventAggregatorSupport.sort_none_first) 818 819 # Traverse the time scale of the full coverage, visiting each slot to 820 # determine whether it provides content for each period. 821 822 scale = EventAggregatorSupport.getCoverageScale(full_coverage) 823 824 # Define a mapping of events to rowspans. 825 826 rowspans = {} 827 828 # Populate each period with event details, recording how many periods 829 # each event populates. 830 831 day_rows = [] 832 833 for period in scale: 834 835 # Ignore timespans before this day. 836 837 if period != date: 838 continue 839 840 # Visit each slot corresponding to a location (or no location). 841 842 day_row = [] 843 844 for location in locations: 845 846 # Visit each coverage span, presenting the events in the span. 847 848 for events in day_slots[location]: 849 event = self.getActiveEvent(period, events) 850 if event is not None: 851 if not rowspans.has_key(event): 852 rowspans[event] = 1 853 else: 854 rowspans[event] += 1 855 day_row.append((location, event)) 856 857 day_rows.append((period, day_row)) 858 859 # Output the periods with event details. 860 861 period = None 862 events_written = set() 863 864 for period, day_row in day_rows: 865 866 # Write an empty heading for the start of the day where the first 867 # applicable timespan starts before this day. 868 869 if period.start < date: 870 output.append(fmt.table_row(on=1)) 871 output.append(self.writeDayScaleHeading("")) 872 873 # Otherwise, write a heading describing the time. 874 875 else: 876 output.append(fmt.table_row(on=1)) 877 output.append(self.writeDayScaleHeading(period.start.time_string())) 878 879 # Visit each slot corresponding to a location (or no location). 880 881 for location, event in day_row: 882 883 # Add a spacer. 884 885 output.append(self.writeDaySpacer()) 886 887 # Output each location slot's contribution. 888 889 if event is None or event not in events_written: 890 output.append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) 891 if event is not None: 892 events_written.add(event) 893 894 output.append(fmt.table_row(on=0)) 895 896 # Write a final time heading if the last period ends in the current day. 897 898 if period is not None: 899 if period.end == date: 900 output.append(fmt.table_row(on=1)) 901 output.append(self.writeDayScaleHeading(period.end.time_string())) 902 903 for slot in day_row: 904 output.append(self.writeDaySpacer()) 905 output.append(self.writeEmptyDaySlot()) 906 907 output.append(fmt.table_row(on=0)) 908 909 return "".join(output) 910 911 def writeDayScaleHeading(self, heading): 912 page = self.page 913 fmt = page.formatter 914 915 output = [] 916 output.append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"})) 917 output.append(fmt.text(heading)) 918 output.append(fmt.table_cell(on=0)) 919 920 return "".join(output) 921 922 def getActiveEvent(self, period, events): 923 for event in events: 924 if period not in event: 925 continue 926 return event 927 else: 928 return None 929 930 def writeDaySlot(self, period, event, rowspan): 931 page = self.page 932 fmt = page.formatter 933 934 output = [] 935 936 if event is not None: 937 event_summary = event.getSummary(self.parent_name) 938 style = self.getEventStyle(event_summary) 939 940 output.append(fmt.table_cell(on=1, attrs={ 941 "class" : "event-timespan-content event-timespan-busy", 942 "style" : style, 943 "rowspan" : str(rowspan) 944 })) 945 output.append(self.writeEventSummaryBox(event)) 946 output.append(fmt.table_cell(on=0)) 947 else: 948 output.append(self.writeEmptyDaySlot()) 949 950 return "".join(output) 951 952 def writeEmptyDaySlot(self): 953 page = self.page 954 fmt = page.formatter 955 956 output = [] 957 958 output.append(fmt.table_cell(on=1, 959 attrs={"class" : "event-timespan-content event-timespan-empty"})) 960 output.append(fmt.table_cell(on=0)) 961 962 return "".join(output) 963 964 def writeDaySpacer(self): 965 page = self.page 966 fmt = page.formatter 967 968 output = [] 969 output.append(fmt.table_cell(on=1, attrs={"class" : "event-timespan-spacer"})) 970 output.append(fmt.table_cell(on=0)) 971 return "".join(output) 972 973 # HTML-related functions. 974 975 def getColour(s): 976 colour = [0, 0, 0] 977 digit = 0 978 for c in s: 979 colour[digit] += ord(c) 980 colour[digit] = colour[digit] % 256 981 digit += 1 982 digit = digit % 3 983 return tuple(colour) 984 985 def getBlackOrWhite(colour): 986 if sum(colour) / 3.0 > 127: 987 return (0, 0, 0) 988 else: 989 return (255, 255, 255) 990 991 # Macro functions. 992 993 def execute(macro, args): 994 995 """ 996 Execute the 'macro' with the given 'args': an optional list of selected 997 category names (categories whose pages are to be shown), together with 998 optional named arguments of the following forms: 999 1000 start=YYYY-MM shows event details starting from the specified month 1001 start=YYYY-MM-DD shows event details starting from the specified day 1002 start=current-N shows event details relative to the current month 1003 (or relative to the current day in "day" mode) 1004 end=YYYY-MM shows event details ending at the specified month 1005 end=YYYY-MM-DD shows event details ending on the specified day 1006 end=current+N shows event details relative to the current month 1007 (or relative to the current day in "day" mode) 1008 1009 mode=calendar shows a calendar view of events 1010 mode=day shows a calendar day view of events 1011 mode=list shows a list of events by month 1012 mode=table shows a table of events 1013 1014 names=daily shows the name of an event on every day of that event 1015 names=weekly shows the name of an event once per week 1016 1017 calendar=NAME uses the given NAME to provide request parameters which 1018 can be used to control the calendar view 1019 1020 template=PAGE uses the given PAGE as the default template for new 1021 events (or the default template from the configuration 1022 if not specified) 1023 1024 parent=PAGE uses the given PAGE as the parent of any new event page 1025 """ 1026 1027 request = macro.request 1028 fmt = macro.formatter 1029 page = fmt.page 1030 _ = request.getText 1031 1032 # Interpret the arguments. 1033 1034 try: 1035 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 1036 except AttributeError: 1037 parsed_args = args.split(",") 1038 1039 parsed_args = [arg for arg in parsed_args if arg] 1040 1041 # Get special arguments. 1042 1043 category_names = [] 1044 raw_calendar_start = None 1045 raw_calendar_end = None 1046 calendar_start = None 1047 calendar_end = None 1048 mode = None 1049 name_usage = "weekly" 1050 calendar_name = None 1051 template_name = getattr(request.cfg, "event_aggregator_new_event_template", "EventTemplate") 1052 parent_name = None 1053 1054 for arg in parsed_args: 1055 if arg.startswith("start="): 1056 raw_calendar_start = arg[6:] 1057 1058 elif arg.startswith("end="): 1059 raw_calendar_end = arg[4:] 1060 1061 elif arg.startswith("mode="): 1062 mode = arg[5:] 1063 1064 elif arg.startswith("names="): 1065 name_usage = arg[6:] 1066 1067 elif arg.startswith("calendar="): 1068 calendar_name = arg[9:] 1069 1070 elif arg.startswith("template="): 1071 template_name = arg[9:] 1072 1073 elif arg.startswith("parent="): 1074 parent_name = arg[7:] 1075 1076 else: 1077 category_names.append(arg) 1078 1079 # Find request parameters to override settings. 1080 1081 mode = EventAggregatorSupport.getQualifiedParameter(request, calendar_name, "mode", mode or "calendar") 1082 1083 # Different modes require different levels of precision. 1084 1085 if mode == "day": 1086 get_date = EventAggregatorSupport.getParameterDate 1087 get_form_date = EventAggregatorSupport.getFormDate 1088 else: 1089 get_date = EventAggregatorSupport.getParameterMonth 1090 get_form_date = EventAggregatorSupport.getFormMonth 1091 1092 # Determine the limits of the calendar. 1093 1094 original_calendar_start = calendar_start = get_date(raw_calendar_start) 1095 original_calendar_end = calendar_end = get_date(raw_calendar_end) 1096 1097 if calendar_name is not None: 1098 calendar_start = get_form_date(request, calendar_name, "start") or calendar_start 1099 calendar_end = get_form_date(request, calendar_name, "end") or calendar_end 1100 1101 # Get the events according to the resolution of the calendar. 1102 1103 events, shown_events, all_shown_events, earliest, latest = \ 1104 EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end, 1105 mode == "day" and "date" or "month") 1106 1107 # Get a concrete period of time. 1108 1109 first, last = EventAggregatorSupport.getConcretePeriod(calendar_start, calendar_end, earliest, latest) 1110 1111 # Define a view of the calendar, retaining useful navigational information. 1112 1113 view = View(page, calendar_name, raw_calendar_start, raw_calendar_end, 1114 original_calendar_start, original_calendar_end, calendar_start, calendar_end, 1115 first, last, category_names, template_name, parent_name, mode, name_usage) 1116 1117 # Make a calendar. 1118 1119 output = [] 1120 1121 # Output download controls. 1122 1123 output.append(fmt.div(on=1, css_class="event-controls")) 1124 output.append(view.writeDownloadControls()) 1125 output.append(fmt.div(on=0)) 1126 1127 # Output a table. 1128 1129 if mode == "table": 1130 1131 # Start of table view output. 1132 1133 output.append(fmt.table(on=1, attrs={"tableclass" : "event-table"})) 1134 1135 output.append(fmt.table_row(on=1)) 1136 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1137 output.append(fmt.text(_("Event dates"))) 1138 output.append(fmt.table_cell(on=0)) 1139 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1140 output.append(fmt.text(_("Event location"))) 1141 output.append(fmt.table_cell(on=0)) 1142 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1143 output.append(fmt.text(_("Event details"))) 1144 output.append(fmt.table_cell(on=0)) 1145 output.append(fmt.table_row(on=0)) 1146 1147 # Get the events in order. 1148 1149 ordered_events = EventAggregatorSupport.getOrderedEvents(all_shown_events) 1150 1151 # Show the events in order. 1152 1153 for event in ordered_events: 1154 event_page = event.getPage() 1155 event_summary = event.getSummary(parent_name) 1156 event_details = event.getDetails() 1157 1158 # Prepare CSS classes with category-related styling. 1159 1160 css_classes = ["event-table-details"] 1161 1162 for topic in event_details.get("topics") or event_details.get("categories") or []: 1163 1164 # Filter the category text to avoid illegal characters. 1165 1166 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 1167 1168 attrs = {"class" : " ".join(css_classes)} 1169 1170 output.append(fmt.table_row(on=1)) 1171 1172 # Start and end dates. 1173 1174 output.append(fmt.table_cell(on=1, attrs=attrs)) 1175 output.append(fmt.span(on=1)) 1176 output.append(fmt.text(str(event_details["start"]))) 1177 output.append(fmt.span(on=0)) 1178 1179 if event_details["start"] != event_details["end"]: 1180 output.append(fmt.text(" - ")) 1181 output.append(fmt.span(on=1)) 1182 output.append(fmt.text(str(event_details["end"]))) 1183 output.append(fmt.span(on=0)) 1184 1185 output.append(fmt.table_cell(on=0)) 1186 1187 # Location. 1188 1189 output.append(fmt.table_cell(on=1, attrs=attrs)) 1190 1191 if event_details.has_key("location"): 1192 output.append(fmt.text(event_details["location"])) 1193 1194 output.append(fmt.table_cell(on=0)) 1195 1196 # Link to the page using the summary. 1197 1198 output.append(fmt.table_cell(on=1, attrs=attrs)) 1199 output.append(event_page.linkToPage(request, event_summary)) 1200 output.append(fmt.table_cell(on=0)) 1201 1202 output.append(fmt.table_row(on=0)) 1203 1204 # End of table view output. 1205 1206 output.append(fmt.table(on=0)) 1207 1208 # Output a list or month calendar. 1209 1210 elif mode in ("list", "calendar"): 1211 1212 # Output top-level information. 1213 1214 # Start of list view output. 1215 1216 if mode == "list": 1217 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 1218 1219 # Visit all months in the requested range, or across known events. 1220 1221 for month in first.months_until(last): 1222 1223 # Either output a calendar view... 1224 1225 if mode == "calendar": 1226 1227 # Output a month. 1228 1229 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 1230 1231 # Either write a month heading or produce links for navigable 1232 # calendars. 1233 1234 output.append(view.writeMonthTableHeading(month)) 1235 1236 # Weekday headings. 1237 1238 output.append(view.writeWeekdayHeadings()) 1239 1240 # Process the days of the month. 1241 1242 start_weekday, number_of_days = month.month_properties() 1243 1244 # The start weekday is the weekday of day number 1. 1245 # Find the first day of the week, counting from below zero, if 1246 # necessary, in order to land on the first day of the month as 1247 # day number 1. 1248 1249 first_day = 1 - start_weekday 1250 1251 while first_day <= number_of_days: 1252 1253 # Find events in this week and determine how to mark them on the 1254 # calendar. 1255 1256 week_start = month.as_date(max(first_day, 1)) 1257 week_end = month.as_date(min(first_day + 6, number_of_days)) 1258 1259 full_coverage, week_slots = EventAggregatorSupport.getCoverage( 1260 week_start, week_end, shown_events.get(month, [])) 1261 1262 # Output a week, starting with the day numbers. 1263 1264 output.append(view.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 1265 1266 # Either generate empty days... 1267 1268 if not week_slots: 1269 output.append(view.writeEmptyWeek(first_day, number_of_days)) 1270 1271 # Or generate each set of scheduled events... 1272 1273 else: 1274 output.append(view.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 1275 1276 # Process the next week... 1277 1278 first_day += 7 1279 1280 # End of month. 1281 1282 output.append(fmt.table(on=0)) 1283 1284 # Or output a summary view... 1285 1286 elif mode == "list": 1287 1288 # Output a list. 1289 1290 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"})) 1291 output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"})) 1292 1293 # Either write a month heading or produce links for navigable 1294 # calendars. 1295 1296 output.append(view.writeMonthHeading(month)) 1297 1298 output.append(fmt.div(on=0)) 1299 1300 output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"})) 1301 1302 # Get the events in order. 1303 1304 ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get(month, [])) 1305 1306 # Show the events in order. 1307 1308 for event in ordered_events: 1309 event_page = event.getPage() 1310 event_details = event.getDetails() 1311 event_summary = event.getSummary(parent_name) 1312 1313 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 1314 1315 # Link to the page using the summary. 1316 1317 output.append(fmt.paragraph(on=1)) 1318 output.append(event_page.linkToPage(request, event_summary)) 1319 output.append(fmt.paragraph(on=0)) 1320 1321 # Start and end dates. 1322 1323 output.append(fmt.paragraph(on=1)) 1324 output.append(fmt.span(on=1)) 1325 output.append(fmt.text(str(event_details["start"]))) 1326 output.append(fmt.span(on=0)) 1327 output.append(fmt.text(" - ")) 1328 output.append(fmt.span(on=1)) 1329 output.append(fmt.text(str(event_details["end"]))) 1330 output.append(fmt.span(on=0)) 1331 output.append(fmt.paragraph(on=0)) 1332 1333 # Location. 1334 1335 if event_details.has_key("location"): 1336 output.append(fmt.paragraph(on=1)) 1337 output.append(fmt.text(event_details["location"])) 1338 output.append(fmt.paragraph(on=1)) 1339 1340 # Topics. 1341 1342 if event_details.has_key("topics") or event_details.has_key("categories"): 1343 output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 1344 1345 for topic in event_details.get("topics") or event_details.get("categories") or []: 1346 output.append(fmt.listitem(on=1)) 1347 output.append(fmt.text(topic)) 1348 output.append(fmt.listitem(on=0)) 1349 1350 output.append(fmt.bullet_list(on=0)) 1351 1352 output.append(fmt.listitem(on=0)) 1353 1354 output.append(fmt.bullet_list(on=0)) 1355 1356 # Output top-level information. 1357 1358 # End of list view output. 1359 1360 if mode == "list": 1361 output.append(fmt.bullet_list(on=0)) 1362 1363 # Output a day view. 1364 1365 elif mode == "day": 1366 1367 # Visit all days in the requested range, or across known events. 1368 1369 for date in first.days_until(last): 1370 1371 output.append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"})) 1372 1373 full_coverage, day_slots = EventAggregatorSupport.getCoverage( 1374 date, date, shown_events.get(date, [])) 1375 1376 # Work out how many columns the day title will need. 1377 # Includer spacers before each event column. 1378 1379 colspan = sum(map(len, day_slots.values())) * 2 + 1 1380 1381 output.append(view.writeDayHeading(date, colspan)) 1382 1383 # Either generate empty days... 1384 1385 if not day_slots: 1386 output.append(view.writeEmptyDay(date)) 1387 1388 # Or generate each set of scheduled events... 1389 1390 else: 1391 output.append(view.writeDaySlots(date, full_coverage, day_slots)) 1392 1393 # End of day. 1394 1395 output.append(fmt.table(on=0)) 1396 1397 # Output view controls. 1398 1399 output.append(fmt.div(on=1, css_class="event-controls")) 1400 output.append(view.writeViewControls()) 1401 output.append(fmt.div(on=0)) 1402 1403 return ''.join(output) 1404 1405 # vim: tabstop=4 expandtab shiftwidth=4