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