1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator Macro 4 5 @copyright: 2008, 2009, 2010, 2011 by Paul Boddie <paul@boddie.org.uk> 6 @copyright: 2000-2004 Juergen Hermann <jh@web.de>, 7 2005-2008 MoinMoin:ThomasWaldmann. 8 @license: GNU GPL (v2 or later), see COPYING.txt for details. 9 """ 10 11 from MoinMoin import wikiutil 12 import EventAggregatorSupport 13 import calendar 14 15 linkToPage = EventAggregatorSupport.linkToPage 16 17 Dependencies = ['pages'] 18 19 # Abstractions. 20 21 class View: 22 23 "A view of the event calendar." 24 25 def __init__(self, page, calendar_name, raw_calendar_start, raw_calendar_end, 26 original_calendar_start, original_calendar_end, calendar_start, calendar_end, 27 first, last, category_names, template_name, parent_name, mode, name_usage): 28 29 """ 30 Initialise the view with the current 'page', a 'calendar_name' (which 31 may be None), the 'raw_calendar_start' and 'raw_calendar_end' (which 32 are the actual start and end values provided by the request), the 33 calculated 'original_calendar_start' and 'original_calendar_end' (which 34 are the result of calculating the calendar's limits from the raw start 35 and end values), and the requested, calculated 'calendar_start' and 36 'calendar_end' (which may involve different start and end values due to 37 navigation in the user interface), along with the 'first' and 'last' 38 months of event coverage. 39 40 The additional 'category_names', 'template_name', 'parent_name' and 41 'mode' parameters are used to configure the links employed by the view. 42 43 The 'name_usage' parameter controls how names are shown on calendar mode 44 events, such as how often labels are repeated. 45 """ 46 47 self.page = page 48 self.calendar_name = calendar_name 49 self.raw_calendar_start = raw_calendar_start 50 self.raw_calendar_end = raw_calendar_end 51 self.original_calendar_start = original_calendar_start 52 self.original_calendar_end = original_calendar_end 53 self.calendar_start = calendar_start 54 self.calendar_end = calendar_end 55 self.template_name = template_name 56 self.parent_name = parent_name 57 self.mode = mode 58 self.name_usage = name_usage 59 60 self.category_name_parameters = "&".join([("category=%s" % name) for name in category_names]) 61 62 if self.calendar_name is not None: 63 64 # Store the view parameters. 65 66 self.number_of_months = (last - first).months() + 1 67 68 self.previous_month_start = first.previous_month() 69 self.next_month_start = first.next_month() 70 self.previous_month_end = last.previous_month() 71 self.next_month_end = last.next_month() 72 73 self.previous_set_start = first.month_update(-self.number_of_months) 74 self.next_set_start = first.month_update(self.number_of_months) 75 self.previous_set_end = last.month_update(-self.number_of_months) 76 self.next_set_end = last.month_update(self.number_of_months) 77 78 def getQualifiedParameterName(self, argname): 79 80 "Return the 'argname' qualified using the calendar name." 81 82 return EventAggregatorSupport.getQualifiedParameterName(self.calendar_name, argname) 83 84 def getDateQueryString(self, argname, date, prefix=1): 85 86 """ 87 Return a query string fragment for the given 'argname', referring to the 88 month given by the specified 'year_month' object, appropriate for this 89 calendar. 90 91 If 'prefix' is specified and set to a false value, the parameters in the 92 query string will not be calendar-specific, but could be used with the 93 summary action. 94 """ 95 96 suffixes = ["year", "month", "day"] 97 98 if date is not None: 99 args = [] 100 for suffix, value in zip(suffixes, date.as_tuple()): 101 suffixed_argname = "%s-%s" % (argname, suffix) 102 if prefix: 103 suffixed_argname = self.getQualifiedParameterName(suffixed_argname) 104 args.append("%s=%s" % (suffixed_argname, value)) 105 return "&".join(args) 106 else: 107 return "" 108 109 def getRawDateQueryString(self, argname, date, prefix=1): 110 111 """ 112 Return a query string fragment for the given 'argname', referring to the 113 date given by the specified 'date' value, appropriate for this 114 calendar. 115 116 If 'prefix' is specified and set to a false value, the parameters in the 117 query string will not be calendar-specific, but could be used with the 118 summary action. 119 """ 120 121 if date is not None: 122 if prefix: 123 argname = self.getQualifiedParameterName(argname) 124 return "%s=%s" % (argname, date) 125 else: 126 return "" 127 128 def getNavigationLink(self, start, end, mode=None): 129 130 """ 131 Return a query string fragment for navigation to a view showing months 132 from 'start' to 'end' inclusive, with the optional 'mode' indicating the 133 view style. 134 """ 135 136 return "%s&%s&%s=%s" % ( 137 self.getRawDateQueryString("start", start), 138 self.getRawDateQueryString("end", end), 139 self.getQualifiedParameterName("mode"), mode or self.mode 140 ) 141 142 def getFullDateLabel(self, date): 143 page = self.page 144 request = page.request 145 return EventAggregatorSupport.getFullDateLabel(request, date) 146 147 def getFullMonthLabel(self, year_month): 148 page = self.page 149 request = page.request 150 return EventAggregatorSupport.getFullMonthLabel(request, year_month) 151 152 def writeDownloadControls(self): 153 154 """ 155 Return a representation of the download controls, featuring links for 156 view, calendar and customised downloads and subscriptions. 157 """ 158 159 page = self.page 160 request = page.request 161 fmt = page.formatter 162 _ = request.getText 163 164 output = [] 165 166 # Generate the links. 167 168 download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s&%s" % ( 169 self.parent_name or "", 170 self.mode == "day" and "date" or "month", 171 self.category_name_parameters 172 ) 173 download_all_link = download_dialogue_link + "&doit=1" 174 download_link = download_all_link + ("&%s&%s" % ( 175 self.getDateQueryString("start", self.calendar_start, prefix=0), 176 self.getDateQueryString("end", self.calendar_end, prefix=0) 177 )) 178 179 # Subscription links just explicitly select the RSS format. 180 181 subscribe_dialogue_link = download_dialogue_link + "&format=RSS" 182 subscribe_all_link = download_all_link + "&format=RSS" 183 subscribe_link = download_link + "&format=RSS" 184 185 # Adjust the "download all" and "subscribe all" links if the calendar 186 # has an inherent period associated with it. 187 188 period_limits = [] 189 190 if self.raw_calendar_start: 191 period_limits.append("&%s" % 192 self.getRawDateQueryString("start", self.raw_calendar_start, prefix=0) 193 ) 194 if self.raw_calendar_end: 195 period_limits.append("&%s" % 196 self.getRawDateQueryString("end", self.raw_calendar_end, prefix=0) 197 ) 198 199 period_limits = "".join(period_limits) 200 201 download_dialogue_link += period_limits 202 download_all_link += period_limits 203 subscribe_dialogue_link += period_limits 204 subscribe_all_link += period_limits 205 206 # Pop-up descriptions of the downloadable calendars. 207 208 get_label = self.mode == "day" and self.getFullDateLabel or self.getFullMonthLabel 209 210 calendar_period = "%s - %s" % ( 211 get_label(self.calendar_start), 212 get_label(self.calendar_end) 213 ) 214 original_calendar_period = "%s - %s" % ( 215 get_label(self.original_calendar_start), 216 get_label(self.original_calendar_end) 217 ) 218 raw_calendar_period = "%s - %s" % (self.raw_calendar_start, self.raw_calendar_end) 219 220 # Write the controls. 221 222 # Download controls. 223 224 output.append(fmt.div(on=1, css_class="event-download-controls")) 225 output.append(fmt.span(on=1, css_class="event-download")) 226 output.append(linkToPage(request, page, _("Download this view"), download_link)) 227 output.append(fmt.span(on=1, css_class="event-download-popup")) 228 output.append(fmt.text(calendar_period)) 229 output.append(fmt.span(on=0)) 230 output.append(fmt.span(on=0)) 231 232 output.append(fmt.span(on=1, css_class="event-download")) 233 output.append(linkToPage(request, page, _("Download this calendar"), download_all_link)) 234 output.append(fmt.span(on=1, css_class="event-download-popup")) 235 output.append(fmt.span(on=1, css_class="event-download-period")) 236 output.append(fmt.text(original_calendar_period)) 237 output.append(fmt.span(on=0)) 238 output.append(fmt.span(on=1, css_class="event-download-period-raw")) 239 output.append(fmt.text(raw_calendar_period)) 240 output.append(fmt.span(on=0)) 241 output.append(fmt.span(on=0)) 242 output.append(fmt.span(on=0)) 243 244 output.append(fmt.span(on=1, css_class="event-download")) 245 output.append(linkToPage(request, page, _("Download..."), download_dialogue_link)) 246 output.append(fmt.span(on=1, css_class="event-download-popup")) 247 output.append(fmt.text(_("Edit download options"))) 248 output.append(fmt.span(on=0)) 249 output.append(fmt.span(on=0)) 250 251 # Subscription controls. 252 253 output.append(fmt.span(on=1, css_class="event-download")) 254 output.append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link)) 255 output.append(fmt.span(on=1, css_class="event-download-popup")) 256 output.append(fmt.text(calendar_period)) 257 output.append(fmt.span(on=0)) 258 output.append(fmt.span(on=0)) 259 260 output.append(fmt.span(on=1, css_class="event-download")) 261 output.append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link)) 262 output.append(fmt.span(on=1, css_class="event-download-popup")) 263 output.append(fmt.span(on=1, css_class="event-download-period")) 264 output.append(fmt.text(original_calendar_period)) 265 output.append(fmt.span(on=0)) 266 output.append(fmt.span(on=1, css_class="event-download-period-raw")) 267 output.append(fmt.text(raw_calendar_period)) 268 output.append(fmt.span(on=0)) 269 output.append(fmt.span(on=0)) 270 output.append(fmt.span(on=0)) 271 272 output.append(fmt.span(on=1, css_class="event-download")) 273 output.append(linkToPage(request, page, _("Subscribe..."), subscribe_dialogue_link)) 274 output.append(fmt.span(on=1, css_class="event-download-popup")) 275 output.append(fmt.text(_("Edit subscription options"))) 276 output.append(fmt.span(on=0)) 277 output.append(fmt.span(on=0)) 278 output.append(fmt.div(on=0)) 279 280 return "".join(output) 281 282 def writeViewControls(self): 283 284 """ 285 Return a representation of the view mode controls, permitting viewing of 286 aggregated events in calendar, list or table form. 287 """ 288 289 page = self.page 290 request = page.request 291 fmt = page.formatter 292 _ = request.getText 293 294 output = [] 295 296 calendar_link = self.getNavigationLink( 297 self.calendar_start, self.calendar_end, "calendar" 298 ) 299 list_link = self.getNavigationLink( 300 self.calendar_start, self.calendar_end, "list" 301 ) 302 table_link = self.getNavigationLink( 303 self.calendar_start, self.calendar_end, "table" 304 ) 305 306 # Write the controls. 307 308 output.append(fmt.div(on=1, css_class="event-view-controls")) 309 output.append(fmt.span(on=1, css_class="event-view")) 310 output.append(linkToPage(request, page, _("View as calendar"), calendar_link)) 311 output.append(fmt.span(on=0)) 312 output.append(fmt.span(on=1, css_class="event-view")) 313 output.append(linkToPage(request, page, _("View as list"), list_link)) 314 output.append(fmt.span(on=0)) 315 output.append(fmt.span(on=1, css_class="event-view")) 316 output.append(linkToPage(request, page, _("View as table"), table_link)) 317 output.append(fmt.span(on=0)) 318 output.append(fmt.div(on=0)) 319 320 return "".join(output) 321 322 def writeMonthHeading(self, year_month): 323 324 """ 325 Return the calendar heading for the given 'year_month' (a Month object) 326 providing links permitting navigation to other months. 327 """ 328 329 page = self.page 330 request = page.request 331 fmt = page.formatter 332 _ = request.getText 333 full_month_label = self.getFullMonthLabel(year_month) 334 335 output = [] 336 337 # Prepare navigation links. 338 339 if self.calendar_name is not None: 340 calendar_name = self.calendar_name 341 342 # Links to the previous set of months and to a calendar shifted 343 # back one month. 344 345 previous_set_link = self.getNavigationLink( 346 self.previous_set_start, self.previous_set_end 347 ) 348 previous_month_link = self.getNavigationLink( 349 self.previous_month_start, self.previous_month_end 350 ) 351 352 # Links to the next set of months and to a calendar shifted 353 # forward one month. 354 355 next_set_link = self.getNavigationLink( 356 self.next_set_start, self.next_set_end 357 ) 358 next_month_link = self.getNavigationLink( 359 self.next_month_start, self.next_month_end 360 ) 361 362 # A link leading to this month being at the top of the calendar. 363 364 end_month = year_month.month_update(self.number_of_months - 1) 365 366 month_link = self.getNavigationLink(year_month, end_month) 367 368 output.append(fmt.span(on=1, css_class="previous-month")) 369 output.append(linkToPage(request, page, "<<", previous_set_link)) 370 output.append(fmt.text(" ")) 371 output.append(linkToPage(request, page, "<", previous_month_link)) 372 output.append(fmt.span(on=0)) 373 374 output.append(fmt.span(on=1, css_class="next-month")) 375 output.append(linkToPage(request, page, ">", next_month_link)) 376 output.append(fmt.text(" ")) 377 output.append(linkToPage(request, page, ">>", next_set_link)) 378 output.append(fmt.span(on=0)) 379 380 output.append(linkToPage(request, page, full_month_label, month_link)) 381 382 else: 383 output.append(fmt.span(on=1)) 384 output.append(fmt.text(full_month_label)) 385 output.append(fmt.span(on=0)) 386 387 return "".join(output) 388 389 def 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(_(EventAggregatorSupport.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(EventAggregatorSupport.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-weekday-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(EventAggregatorSupport.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 = EventAggregatorSupport.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): 1008 page = self.page 1009 fmt = page.formatter 1010 1011 output = [] 1012 output.append(fmt.table_cell(on=1, attrs={"class" : "event-timespan-spacer"})) 1013 output.append(fmt.table_cell(on=0)) 1014 return "".join(output) 1015 1016 # HTML-related functions. 1017 1018 def getColour(s): 1019 colour = [0, 0, 0] 1020 digit = 0 1021 for c in s: 1022 colour[digit] += ord(c) 1023 colour[digit] = colour[digit] % 256 1024 digit += 1 1025 digit = digit % 3 1026 return tuple(colour) 1027 1028 def getBlackOrWhite(colour): 1029 if sum(colour) / 3.0 > 127: 1030 return (0, 0, 0) 1031 else: 1032 return (255, 255, 255) 1033 1034 # Macro functions. 1035 1036 def execute(macro, args): 1037 1038 """ 1039 Execute the 'macro' with the given 'args': an optional list of selected 1040 category names (categories whose pages are to be shown), together with 1041 optional named arguments of the following forms: 1042 1043 start=YYYY-MM shows event details starting from the specified month 1044 start=YYYY-MM-DD shows event details starting from the specified day 1045 start=current-N shows event details relative to the current month 1046 (or relative to the current day in "day" mode) 1047 end=YYYY-MM shows event details ending at the specified month 1048 end=YYYY-MM-DD shows event details ending on the specified day 1049 end=current+N shows event details relative to the current month 1050 (or relative to the current day in "day" mode) 1051 1052 mode=calendar shows a calendar view of events 1053 mode=day shows a calendar day view of events 1054 mode=list shows a list of events by month 1055 mode=table shows a table of events 1056 1057 names=daily shows the name of an event on every day of that event 1058 names=weekly shows the name of an event once per week 1059 1060 calendar=NAME uses the given NAME to provide request parameters which 1061 can be used to control the calendar view 1062 1063 template=PAGE uses the given PAGE as the default template for new 1064 events (or the default template from the configuration 1065 if not specified) 1066 1067 parent=PAGE uses the given PAGE as the parent of any new event page 1068 """ 1069 1070 request = macro.request 1071 fmt = macro.formatter 1072 page = fmt.page 1073 _ = request.getText 1074 1075 # Interpret the arguments. 1076 1077 try: 1078 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 1079 except AttributeError: 1080 parsed_args = args.split(",") 1081 1082 parsed_args = [arg for arg in parsed_args if arg] 1083 1084 # Get special arguments. 1085 1086 category_names = [] 1087 raw_calendar_start = None 1088 raw_calendar_end = None 1089 calendar_start = None 1090 calendar_end = None 1091 mode = None 1092 name_usage = "weekly" 1093 calendar_name = None 1094 template_name = getattr(request.cfg, "event_aggregator_new_event_template", "EventTemplate") 1095 parent_name = None 1096 1097 for arg in parsed_args: 1098 if arg.startswith("start="): 1099 raw_calendar_start = arg[6:] 1100 1101 elif arg.startswith("end="): 1102 raw_calendar_end = arg[4:] 1103 1104 elif arg.startswith("mode="): 1105 mode = arg[5:] 1106 1107 elif arg.startswith("names="): 1108 name_usage = arg[6:] 1109 1110 elif arg.startswith("calendar="): 1111 calendar_name = arg[9:] 1112 1113 elif arg.startswith("template="): 1114 template_name = arg[9:] 1115 1116 elif arg.startswith("parent="): 1117 parent_name = arg[7:] 1118 1119 else: 1120 category_names.append(arg) 1121 1122 # Find request parameters to override settings. 1123 1124 mode = EventAggregatorSupport.getQualifiedParameter(request, calendar_name, "mode", mode or "calendar") 1125 1126 # Different modes require different levels of precision. 1127 1128 if mode == "day": 1129 get_date = EventAggregatorSupport.getParameterDate 1130 get_form_date = EventAggregatorSupport.getFormDate 1131 else: 1132 get_date = EventAggregatorSupport.getParameterMonth 1133 get_form_date = EventAggregatorSupport.getFormMonth 1134 1135 # Determine the limits of the calendar. 1136 1137 original_calendar_start = calendar_start = get_date(raw_calendar_start) 1138 original_calendar_end = calendar_end = get_date(raw_calendar_end) 1139 1140 if calendar_name is not None: 1141 calendar_start = get_form_date(request, calendar_name, "start") or calendar_start 1142 calendar_end = get_form_date(request, calendar_name, "end") or calendar_end 1143 1144 # Get the events according to the resolution of the calendar. 1145 1146 events, shown_events, all_shown_events, earliest, latest = \ 1147 EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end, 1148 mode == "day" and "date" or "month") 1149 1150 # Get a concrete period of time. 1151 1152 first, last = EventAggregatorSupport.getConcretePeriod(calendar_start, calendar_end, earliest, latest) 1153 1154 # Define a view of the calendar, retaining useful navigational information. 1155 1156 view = View(page, calendar_name, raw_calendar_start, raw_calendar_end, 1157 original_calendar_start, original_calendar_end, calendar_start, calendar_end, 1158 first, last, category_names, template_name, parent_name, mode, name_usage) 1159 1160 # Make a calendar. 1161 1162 output = [] 1163 1164 # Output download controls. 1165 1166 output.append(fmt.div(on=1, css_class="event-controls")) 1167 output.append(view.writeDownloadControls()) 1168 output.append(fmt.div(on=0)) 1169 1170 # Output a table. 1171 1172 if mode == "table": 1173 1174 # Start of table view output. 1175 1176 output.append(fmt.table(on=1, attrs={"tableclass" : "event-table"})) 1177 1178 output.append(fmt.table_row(on=1)) 1179 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1180 output.append(fmt.text(_("Event dates"))) 1181 output.append(fmt.table_cell(on=0)) 1182 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1183 output.append(fmt.text(_("Event location"))) 1184 output.append(fmt.table_cell(on=0)) 1185 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1186 output.append(fmt.text(_("Event details"))) 1187 output.append(fmt.table_cell(on=0)) 1188 output.append(fmt.table_row(on=0)) 1189 1190 # Get the events in order. 1191 1192 ordered_events = EventAggregatorSupport.getOrderedEvents(all_shown_events) 1193 1194 # Show the events in order. 1195 1196 for event in ordered_events: 1197 event_page = event.getPage() 1198 event_summary = event.getSummary(parent_name) 1199 event_details = event.getDetails() 1200 1201 # Prepare CSS classes with category-related styling. 1202 1203 css_classes = ["event-table-details"] 1204 1205 for topic in event_details.get("topics") or event_details.get("categories") or []: 1206 1207 # Filter the category text to avoid illegal characters. 1208 1209 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 1210 1211 attrs = {"class" : " ".join(css_classes)} 1212 1213 output.append(fmt.table_row(on=1)) 1214 1215 # Start and end dates. 1216 1217 output.append(fmt.table_cell(on=1, attrs=attrs)) 1218 output.append(fmt.span(on=1)) 1219 output.append(fmt.text(str(event_details["start"]))) 1220 output.append(fmt.span(on=0)) 1221 1222 if event_details["start"] != event_details["end"]: 1223 output.append(fmt.text(" - ")) 1224 output.append(fmt.span(on=1)) 1225 output.append(fmt.text(str(event_details["end"]))) 1226 output.append(fmt.span(on=0)) 1227 1228 output.append(fmt.table_cell(on=0)) 1229 1230 # Location. 1231 1232 output.append(fmt.table_cell(on=1, attrs=attrs)) 1233 1234 if event_details.has_key("location"): 1235 output.append(fmt.text(event_details["location"])) 1236 1237 output.append(fmt.table_cell(on=0)) 1238 1239 # Link to the page using the summary. 1240 1241 output.append(fmt.table_cell(on=1, attrs=attrs)) 1242 output.append(event_page.linkToPage(request, event_summary)) 1243 output.append(fmt.table_cell(on=0)) 1244 1245 output.append(fmt.table_row(on=0)) 1246 1247 # End of table view output. 1248 1249 output.append(fmt.table(on=0)) 1250 1251 # Output a list or month calendar. 1252 1253 elif mode in ("list", "calendar"): 1254 1255 # Output top-level information. 1256 1257 # Start of list view output. 1258 1259 if mode == "list": 1260 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 1261 1262 # Visit all months in the requested range, or across known events. 1263 1264 for month in first.months_until(last): 1265 1266 # Either output a calendar view... 1267 1268 if mode == "calendar": 1269 1270 # Output a month. 1271 1272 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 1273 1274 # Either write a month heading or produce links for navigable 1275 # calendars. 1276 1277 output.append(view.writeMonthTableHeading(month)) 1278 1279 # Weekday headings. 1280 1281 output.append(view.writeWeekdayHeadings()) 1282 1283 # Process the days of the month. 1284 1285 start_weekday, number_of_days = month.month_properties() 1286 1287 # The start weekday is the weekday of day number 1. 1288 # Find the first day of the week, counting from below zero, if 1289 # necessary, in order to land on the first day of the month as 1290 # day number 1. 1291 1292 first_day = 1 - start_weekday 1293 1294 while first_day <= number_of_days: 1295 1296 # Find events in this week and determine how to mark them on the 1297 # calendar. 1298 1299 week_start = month.as_date(max(first_day, 1)) 1300 week_end = month.as_date(min(first_day + 6, number_of_days)) 1301 1302 full_coverage, week_slots = EventAggregatorSupport.getCoverage( 1303 week_start, week_end, shown_events.get(month, [])) 1304 1305 # Output a week, starting with the day numbers. 1306 1307 output.append(view.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 1308 1309 # Either generate empty days... 1310 1311 if not week_slots: 1312 output.append(view.writeEmptyWeek(first_day, number_of_days)) 1313 1314 # Or generate each set of scheduled events... 1315 1316 else: 1317 output.append(view.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 1318 1319 # Process the next week... 1320 1321 first_day += 7 1322 1323 # End of month. 1324 1325 output.append(fmt.table(on=0)) 1326 1327 # Or output a summary view... 1328 1329 elif mode == "list": 1330 1331 # Output a list. 1332 1333 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"})) 1334 output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"})) 1335 1336 # Either write a month heading or produce links for navigable 1337 # calendars. 1338 1339 output.append(view.writeMonthHeading(month)) 1340 1341 output.append(fmt.div(on=0)) 1342 1343 output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"})) 1344 1345 # Get the events in order. 1346 1347 ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get(month, [])) 1348 1349 # Show the events in order. 1350 1351 for event in ordered_events: 1352 event_page = event.getPage() 1353 event_details = event.getDetails() 1354 event_summary = event.getSummary(parent_name) 1355 1356 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 1357 1358 # Link to the page using the summary. 1359 1360 output.append(fmt.paragraph(on=1)) 1361 output.append(event_page.linkToPage(request, event_summary)) 1362 output.append(fmt.paragraph(on=0)) 1363 1364 # Start and end dates. 1365 1366 output.append(fmt.paragraph(on=1)) 1367 output.append(fmt.span(on=1)) 1368 output.append(fmt.text(str(event_details["start"]))) 1369 output.append(fmt.span(on=0)) 1370 output.append(fmt.text(" - ")) 1371 output.append(fmt.span(on=1)) 1372 output.append(fmt.text(str(event_details["end"]))) 1373 output.append(fmt.span(on=0)) 1374 output.append(fmt.paragraph(on=0)) 1375 1376 # Location. 1377 1378 if event_details.has_key("location"): 1379 output.append(fmt.paragraph(on=1)) 1380 output.append(fmt.text(event_details["location"])) 1381 output.append(fmt.paragraph(on=1)) 1382 1383 # Topics. 1384 1385 if event_details.has_key("topics") or event_details.has_key("categories"): 1386 output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 1387 1388 for topic in event_details.get("topics") or event_details.get("categories") or []: 1389 output.append(fmt.listitem(on=1)) 1390 output.append(fmt.text(topic)) 1391 output.append(fmt.listitem(on=0)) 1392 1393 output.append(fmt.bullet_list(on=0)) 1394 1395 output.append(fmt.listitem(on=0)) 1396 1397 output.append(fmt.bullet_list(on=0)) 1398 1399 # Output top-level information. 1400 1401 # End of list view output. 1402 1403 if mode == "list": 1404 output.append(fmt.bullet_list(on=0)) 1405 1406 # Output a day view. 1407 1408 elif mode == "day": 1409 1410 # Visit all days in the requested range, or across known events. 1411 1412 for date in first.days_until(last): 1413 1414 output.append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"})) 1415 1416 full_coverage, day_slots = EventAggregatorSupport.getCoverage( 1417 date, date, shown_events.get(date, []), "datetime") 1418 1419 # Work out how many columns the day title will need. 1420 # Includer spacers before each event column. 1421 1422 colspan = sum(map(len, day_slots.values())) * 2 + 1 1423 1424 output.append(view.writeDayHeading(date, colspan)) 1425 1426 # Either generate empty days... 1427 1428 if not day_slots: 1429 output.append(view.writeEmptyDay(date)) 1430 1431 # Or generate each set of scheduled events... 1432 1433 else: 1434 output.append(view.writeDaySlots(date, full_coverage, day_slots)) 1435 1436 # End of day. 1437 1438 output.append(fmt.table(on=0)) 1439 1440 # Output view controls. 1441 1442 output.append(fmt.div(on=1, css_class="event-controls")) 1443 output.append(view.writeViewControls()) 1444 output.append(fmt.div(on=0)) 1445 1446 return ''.join(output) 1447 1448 # vim: tabstop=4 expandtab shiftwidth=4