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.as_timespan().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 855 """ 856 Given a 'date', non-empty 'full_coverage' for the day concerned, and a 857 non-empty mapping of 'day_slots' (from locations to event collections), 858 output the day slots for the day. 859 """ 860 861 page = self.page 862 fmt = page.formatter 863 864 output = [] 865 866 locations = day_slots.keys() 867 locations.sort(sort_none_first) 868 869 # Traverse the time scale of the full coverage, visiting each slot to 870 # determine whether it provides content for each period. 871 872 scale = getCoverageScale(full_coverage) 873 874 # Define a mapping of events to rowspans. 875 876 rowspans = {} 877 878 # Populate each period with event details, recording how many periods 879 # each event populates. 880 881 day_rows = [] 882 883 for period in scale: 884 885 # Ignore timespans before this day. 886 887 if period != date: 888 continue 889 890 # Visit each slot corresponding to a location (or no location). 891 892 day_row = [] 893 894 for location in locations: 895 896 # Visit each coverage span, presenting the events in the span. 897 898 for events in day_slots[location]: 899 event = self.getActiveEvent(period, events) 900 if event is not None: 901 if not rowspans.has_key(event): 902 rowspans[event] = 1 903 else: 904 rowspans[event] += 1 905 day_row.append((location, event)) 906 907 day_rows.append((period, day_row)) 908 909 # Output the locations. 910 911 output.append(fmt.table_row(on=1)) 912 913 # Add a spacer. 914 915 output.append(self.writeDaySpacer(colspan=2, cls="location")) 916 917 for location in locations: 918 919 # Add spacers to the column spans. 920 921 columns = len(day_slots[location]) * 2 - 1 922 output.append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)})) 923 output.append(fmt.text(location)) 924 output.append(fmt.table_cell(on=0)) 925 926 # Add a trailing spacer. 927 928 output.append(self.writeDaySpacer(cls="location")) 929 930 output.append(fmt.table_row(on=0)) 931 932 # Output the periods with event details. 933 934 period = None 935 events_written = set() 936 937 for period, day_row in day_rows: 938 939 # Write an empty heading for the start of the day where the first 940 # applicable timespan starts before this day. 941 942 if period.start < date: 943 output.append(fmt.table_row(on=1)) 944 output.append(self.writeDayScaleHeading("")) 945 946 # Otherwise, write a heading describing the time. 947 948 else: 949 output.append(fmt.table_row(on=1)) 950 output.append(self.writeDayScaleHeading(period.start.time_string())) 951 952 output.append(self.writeDaySpacer()) 953 954 # Visit each slot corresponding to a location (or no location). 955 956 for location, event in day_row: 957 958 # Output each location slot's contribution. 959 960 if event is None or event not in events_written: 961 output.append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) 962 if event is not None: 963 events_written.add(event) 964 965 # Add a trailing spacer. 966 967 output.append(self.writeDaySpacer()) 968 969 output.append(fmt.table_row(on=0)) 970 971 # Write a final time heading if the last period ends in the current day. 972 973 if period is not None: 974 if period.end == date: 975 output.append(fmt.table_row(on=1)) 976 output.append(self.writeDayScaleHeading(period.end.time_string())) 977 978 for slot in day_row: 979 output.append(self.writeDaySpacer()) 980 output.append(self.writeEmptyDaySlot()) 981 982 output.append(fmt.table_row(on=0)) 983 984 return "".join(output) 985 986 def writeDayScaleHeading(self, heading): 987 page = self.page 988 fmt = page.formatter 989 990 output = [] 991 output.append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"})) 992 output.append(fmt.text(heading)) 993 output.append(fmt.table_cell(on=0)) 994 995 return "".join(output) 996 997 def getActiveEvent(self, period, events): 998 for event in events: 999 if period not in event: 1000 continue 1001 return event 1002 else: 1003 return None 1004 1005 def writeDaySlot(self, period, event, rowspan): 1006 page = self.page 1007 fmt = page.formatter 1008 1009 output = [] 1010 1011 if event is not None: 1012 event_summary = event.getSummary(self.parent_name) 1013 style = self.getEventStyle(event_summary) 1014 1015 output.append(fmt.table_cell(on=1, attrs={ 1016 "class" : "event-timespan-content event-timespan-busy", 1017 "style" : style, 1018 "rowspan" : str(rowspan) 1019 })) 1020 output.append(self.writeEventSummaryBox(event)) 1021 output.append(fmt.table_cell(on=0)) 1022 else: 1023 output.append(self.writeEmptyDaySlot()) 1024 1025 return "".join(output) 1026 1027 def writeEmptyDaySlot(self): 1028 page = self.page 1029 fmt = page.formatter 1030 1031 output = [] 1032 1033 output.append(fmt.table_cell(on=1, 1034 attrs={"class" : "event-timespan-content event-timespan-empty"})) 1035 output.append(fmt.table_cell(on=0)) 1036 1037 return "".join(output) 1038 1039 def writeDaySpacer(self, colspan=1, cls="timespan"): 1040 page = self.page 1041 fmt = page.formatter 1042 1043 output = [] 1044 output.append(fmt.table_cell(on=1, attrs={ 1045 "class" : "event-%s-spacer" % cls, 1046 "colspan" : str(colspan)})) 1047 output.append(fmt.table_cell(on=0)) 1048 return "".join(output) 1049 1050 # HTML-related functions. 1051 1052 def getColour(s): 1053 colour = [0, 0, 0] 1054 digit = 0 1055 for c in s: 1056 colour[digit] += ord(c) 1057 colour[digit] = colour[digit] % 256 1058 digit += 1 1059 digit = digit % 3 1060 return tuple(colour) 1061 1062 def getBlackOrWhite(colour): 1063 if sum(colour) / 3.0 > 127: 1064 return (0, 0, 0) 1065 else: 1066 return (255, 255, 255) 1067 1068 # Macro functions. 1069 1070 def execute(macro, args): 1071 1072 """ 1073 Execute the 'macro' with the given 'args': an optional list of selected 1074 category names (categories whose pages are to be shown), together with 1075 optional named arguments of the following forms: 1076 1077 start=YYYY-MM shows event details starting from the specified month 1078 start=YYYY-MM-DD shows event details starting from the specified day 1079 start=current-N shows event details relative to the current month 1080 (or relative to the current day in "day" mode) 1081 end=YYYY-MM shows event details ending at the specified month 1082 end=YYYY-MM-DD shows event details ending on the specified day 1083 end=current+N shows event details relative to the current month 1084 (or relative to the current day in "day" mode) 1085 1086 mode=calendar shows a calendar view of events 1087 mode=day shows a calendar day view of events 1088 mode=list shows a list of events by month 1089 mode=table shows a table of events 1090 1091 names=daily shows the name of an event on every day of that event 1092 names=weekly shows the name of an event once per week 1093 1094 calendar=NAME uses the given NAME to provide request parameters which 1095 can be used to control the calendar view 1096 1097 template=PAGE uses the given PAGE as the default template for new 1098 events (or the default template from the configuration 1099 if not specified) 1100 1101 parent=PAGE uses the given PAGE as the parent of any new event page 1102 """ 1103 1104 request = macro.request 1105 fmt = macro.formatter 1106 page = fmt.page 1107 _ = request.getText 1108 1109 # Interpret the arguments. 1110 1111 try: 1112 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 1113 except AttributeError: 1114 parsed_args = args.split(",") 1115 1116 parsed_args = [arg for arg in parsed_args if arg] 1117 1118 # Get special arguments. 1119 1120 category_names = [] 1121 raw_calendar_start = None 1122 raw_calendar_end = None 1123 calendar_start = None 1124 calendar_end = None 1125 mode = None 1126 name_usage = "weekly" 1127 calendar_name = None 1128 template_name = getattr(request.cfg, "event_aggregator_new_event_template", "EventTemplate") 1129 parent_name = None 1130 1131 for arg in parsed_args: 1132 if arg.startswith("start="): 1133 raw_calendar_start = arg[6:] 1134 1135 elif arg.startswith("end="): 1136 raw_calendar_end = arg[4:] 1137 1138 elif arg.startswith("mode="): 1139 mode = arg[5:] 1140 1141 elif arg.startswith("names="): 1142 name_usage = arg[6:] 1143 1144 elif arg.startswith("calendar="): 1145 calendar_name = arg[9:] 1146 1147 elif arg.startswith("template="): 1148 template_name = arg[9:] 1149 1150 elif arg.startswith("parent="): 1151 parent_name = arg[7:] 1152 1153 else: 1154 category_names.append(arg) 1155 1156 # Find request parameters to override settings. 1157 1158 mode = getQualifiedParameter(request, calendar_name, "mode", mode or "calendar") 1159 1160 # Different modes require different levels of precision. 1161 1162 if mode == "day": 1163 get_date = getParameterDate 1164 get_form_date = getFormDate 1165 else: 1166 get_date = getParameterMonth 1167 get_form_date = getFormMonth 1168 1169 # Determine the limits of the calendar. 1170 1171 original_calendar_start = calendar_start = get_date(raw_calendar_start) 1172 original_calendar_end = calendar_end = get_date(raw_calendar_end) 1173 1174 calendar_start = get_form_date(request, calendar_name, "start") or calendar_start 1175 calendar_end = get_form_date(request, calendar_name, "end") or calendar_end 1176 1177 # Get the events according to the resolution of the calendar. 1178 1179 event_pages = getPagesFromResults(getAllCategoryPages(category_names, request), request) 1180 events = getEventsFromPages(event_pages) 1181 all_shown_events = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end)) 1182 earliest, latest = getEventLimits(all_shown_events) 1183 1184 # Get a concrete period of time. 1185 1186 first, last = getConcretePeriod(calendar_start, calendar_end, earliest, latest) 1187 1188 # Define a view of the calendar, retaining useful navigational information. 1189 1190 view = View(page, calendar_name, raw_calendar_start, raw_calendar_end, 1191 original_calendar_start, original_calendar_end, calendar_start, calendar_end, 1192 first, last, category_names, template_name, parent_name, mode, name_usage) 1193 1194 # Make a calendar. 1195 1196 output = [] 1197 1198 output.append(fmt.div(on=1, css_class="event-calendar")) 1199 1200 # Output download controls. 1201 1202 output.append(fmt.div(on=1, css_class="event-controls")) 1203 output.append(view.writeDownloadControls()) 1204 output.append(fmt.div(on=0)) 1205 1206 # Output a table. 1207 1208 if mode == "table": 1209 1210 # Start of table view output. 1211 1212 output.append(fmt.table(on=1, attrs={"tableclass" : "event-table"})) 1213 1214 output.append(fmt.table_row(on=1)) 1215 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1216 output.append(fmt.text(_("Event dates"))) 1217 output.append(fmt.table_cell(on=0)) 1218 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1219 output.append(fmt.text(_("Event location"))) 1220 output.append(fmt.table_cell(on=0)) 1221 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1222 output.append(fmt.text(_("Event details"))) 1223 output.append(fmt.table_cell(on=0)) 1224 output.append(fmt.table_row(on=0)) 1225 1226 # Show the events in order. 1227 1228 for event in all_shown_events: 1229 event_page = event.getPage() 1230 event_summary = event.getSummary(parent_name) 1231 event_details = event.getDetails() 1232 1233 # Prepare CSS classes with category-related styling. 1234 1235 css_classes = ["event-table-details"] 1236 1237 for topic in event_details.get("topics") or event_details.get("categories") or []: 1238 1239 # Filter the category text to avoid illegal characters. 1240 1241 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 1242 1243 attrs = {"class" : " ".join(css_classes)} 1244 1245 output.append(fmt.table_row(on=1)) 1246 1247 # Start and end dates. 1248 1249 output.append(fmt.table_cell(on=1, attrs=attrs)) 1250 output.append(fmt.span(on=1)) 1251 output.append(fmt.text(str(event_details["start"]))) 1252 output.append(fmt.span(on=0)) 1253 1254 if event_details["start"] != event_details["end"]: 1255 output.append(fmt.text(" - ")) 1256 output.append(fmt.span(on=1)) 1257 output.append(fmt.text(str(event_details["end"]))) 1258 output.append(fmt.span(on=0)) 1259 1260 output.append(fmt.table_cell(on=0)) 1261 1262 # Location. 1263 1264 output.append(fmt.table_cell(on=1, attrs=attrs)) 1265 1266 if event_details.has_key("location"): 1267 output.append(fmt.text(event_details["location"])) 1268 1269 output.append(fmt.table_cell(on=0)) 1270 1271 # Link to the page using the summary. 1272 1273 output.append(fmt.table_cell(on=1, attrs=attrs)) 1274 output.append(event_page.linkToPage(request, event_summary)) 1275 output.append(fmt.table_cell(on=0)) 1276 1277 output.append(fmt.table_row(on=0)) 1278 1279 # End of table view output. 1280 1281 output.append(fmt.table(on=0)) 1282 1283 # Output a list or month calendar. 1284 1285 elif mode in ("list", "calendar"): 1286 1287 # Output top-level information. 1288 1289 # Start of list view output. 1290 1291 if mode == "list": 1292 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 1293 1294 # Visit all months in the requested range, or across known events. 1295 1296 for month in first.months_until(last): 1297 1298 # Either output a calendar view... 1299 1300 if mode == "calendar": 1301 1302 # Output a month. 1303 1304 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 1305 1306 # Either write a month heading or produce links for navigable 1307 # calendars. 1308 1309 output.append(view.writeMonthTableHeading(month)) 1310 1311 # Weekday headings. 1312 1313 output.append(view.writeWeekdayHeadings()) 1314 1315 # Process the days of the month. 1316 1317 start_weekday, number_of_days = month.month_properties() 1318 1319 # The start weekday is the weekday of day number 1. 1320 # Find the first day of the week, counting from below zero, if 1321 # necessary, in order to land on the first day of the month as 1322 # day number 1. 1323 1324 first_day = 1 - start_weekday 1325 1326 while first_day <= number_of_days: 1327 1328 # Find events in this week and determine how to mark them on the 1329 # calendar. 1330 1331 week_start = month.as_date(max(first_day, 1)) 1332 week_end = month.as_date(min(first_day + 6, number_of_days)) 1333 1334 full_coverage, week_slots = getCoverage( 1335 getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end))) 1336 1337 # Output a week, starting with the day numbers. 1338 1339 output.append(view.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 1340 1341 # Either generate empty days... 1342 1343 if not week_slots: 1344 output.append(view.writeEmptyWeek(first_day, number_of_days)) 1345 1346 # Or generate each set of scheduled events... 1347 1348 else: 1349 output.append(view.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 1350 1351 # Process the next week... 1352 1353 first_day += 7 1354 1355 # End of month. 1356 1357 output.append(fmt.table(on=0)) 1358 1359 # Or output a summary view... 1360 1361 elif mode == "list": 1362 1363 # Output a list. 1364 1365 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"})) 1366 output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"})) 1367 1368 # Either write a month heading or produce links for navigable 1369 # calendars. 1370 1371 output.append(view.writeMonthHeading(month)) 1372 1373 output.append(fmt.div(on=0)) 1374 1375 output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"})) 1376 1377 # Show the events in order. 1378 1379 for event in getEventsInPeriod(all_shown_events, getCalendarPeriod(month, month)): 1380 event_page = event.getPage() 1381 event_details = event.getDetails() 1382 event_summary = event.getSummary(parent_name) 1383 1384 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 1385 1386 # Link to the page using the summary. 1387 1388 output.append(fmt.paragraph(on=1)) 1389 output.append(event_page.linkToPage(request, event_summary)) 1390 output.append(fmt.paragraph(on=0)) 1391 1392 # Start and end dates. 1393 1394 output.append(fmt.paragraph(on=1)) 1395 output.append(fmt.span(on=1)) 1396 output.append(fmt.text(str(event_details["start"]))) 1397 output.append(fmt.span(on=0)) 1398 output.append(fmt.text(" - ")) 1399 output.append(fmt.span(on=1)) 1400 output.append(fmt.text(str(event_details["end"]))) 1401 output.append(fmt.span(on=0)) 1402 output.append(fmt.paragraph(on=0)) 1403 1404 # Location. 1405 1406 if event_details.has_key("location"): 1407 output.append(fmt.paragraph(on=1)) 1408 output.append(fmt.text(event_details["location"])) 1409 output.append(fmt.paragraph(on=1)) 1410 1411 # Topics. 1412 1413 if event_details.has_key("topics") or event_details.has_key("categories"): 1414 output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 1415 1416 for topic in event_details.get("topics") or event_details.get("categories") or []: 1417 output.append(fmt.listitem(on=1)) 1418 output.append(fmt.text(topic)) 1419 output.append(fmt.listitem(on=0)) 1420 1421 output.append(fmt.bullet_list(on=0)) 1422 1423 output.append(fmt.listitem(on=0)) 1424 1425 output.append(fmt.bullet_list(on=0)) 1426 1427 # Output top-level information. 1428 1429 # End of list view output. 1430 1431 if mode == "list": 1432 output.append(fmt.bullet_list(on=0)) 1433 1434 # Output a day view. 1435 1436 elif mode == "day": 1437 1438 # Visit all days in the requested range, or across known events. 1439 1440 for date in first.days_until(last): 1441 1442 output.append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"})) 1443 1444 full_coverage, day_slots = getCoverage( 1445 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 1446 1447 # Work out how many columns the day title will need. 1448 # Include spacers after the scale and each event column. 1449 1450 colspan = sum(map(len, day_slots.values())) * 2 + 2 1451 1452 output.append(view.writeDayHeading(date, colspan)) 1453 1454 # Either generate empty days... 1455 1456 if not day_slots: 1457 output.append(view.writeEmptyDay(date)) 1458 1459 # Or generate each set of scheduled events... 1460 1461 else: 1462 output.append(view.writeDaySlots(date, full_coverage, day_slots)) 1463 1464 # End of day. 1465 1466 output.append(fmt.table(on=0)) 1467 1468 # Output view controls. 1469 1470 output.append(fmt.div(on=1, css_class="event-controls")) 1471 output.append(view.writeViewControls()) 1472 output.append(fmt.div(on=0)) 1473 1474 # Close the calendar region. 1475 1476 output.append(fmt.div(on=0)) 1477 1478 return ''.join(output) 1479 1480 # vim: tabstop=4 expandtab shiftwidth=4