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