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