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