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