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