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