1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator user interface library 4 5 @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from DateSupport import Date 10 from EventAggregatorSupport.Filter import getCalendarPeriod, getEventsInPeriod, \ 11 getCoverage, getCoverageScale 12 from EventAggregatorSupport.Locations import getMapsPage, getLocationsPage, Location 13 14 from GeneralSupport import sort_none_first 15 from LocationSupport import getMapReference, getNormalisedLocation, \ 16 getPositionForCentrePoint, getPositionForReference 17 from MoinDateSupport import getFullDateLabel, getFullMonthLabel 18 from MoinSupport import * 19 from ViewSupport import getColour, getBlackOrWhite 20 21 from MoinMoin.Page import Page 22 from MoinMoin.action import AttachFile 23 from MoinMoin import wikiutil 24 25 try: 26 set 27 except NameError: 28 from sets import Set as set 29 30 # Utility functions. 31 32 def to_plain_text(s, request): 33 34 "Convert 's' to plain text." 35 36 fmt = getFormatterClass(request, "plain")(request) 37 fmt.setPage(request.page) 38 return formatText(s, request, fmt) 39 40 def getLocationPosition(location, locations): 41 42 """ 43 Attempt to return the position of the given 'location' using the 'locations' 44 dictionary provided. If no position can be found, return a latitude of None 45 and a longitude of None. 46 """ 47 48 latitude, longitude = None, None 49 50 if location is not None: 51 try: 52 latitude, longitude = map(getMapReference, locations[location].split()) 53 except (KeyError, ValueError): 54 pass 55 56 return latitude, longitude 57 58 # Event sorting. 59 60 def sort_start_first(x, y): 61 x_ts = x.as_limits() 62 if x_ts is not None: 63 x_start, x_end = x_ts 64 y_ts = y.as_limits() 65 if y_ts is not None: 66 y_start, y_end = y_ts 67 start_order = cmp(x_start, y_start) 68 if start_order == 0: 69 return cmp(x_end, y_end) 70 else: 71 return start_order 72 return 0 73 74 # User interface abstractions. 75 76 class View: 77 78 "A view of the event calendar." 79 80 def __init__(self, page, calendar_name, 81 raw_calendar_start, raw_calendar_end, 82 original_calendar_start, original_calendar_end, 83 calendar_start, calendar_end, 84 wider_calendar_start, wider_calendar_end, 85 first, last, category_names, remote_sources, search_pattern, template_name, 86 parent_name, mode, raw_resolution, resolution, name_usage, map_name): 87 88 """ 89 Initialise the view with the current 'page', a 'calendar_name' (which 90 may be None), the 'raw_calendar_start' and 'raw_calendar_end' (which 91 are the actual start and end values provided by the request), the 92 calculated 'original_calendar_start' and 'original_calendar_end' (which 93 are the result of calculating the calendar's limits from the raw start 94 and end values), the requested, calculated 'calendar_start' and 95 'calendar_end' (which may involve different start and end values due to 96 navigation in the user interface), and the requested 97 'wider_calendar_start' and 'wider_calendar_end' (which indicate a wider 98 view used when navigating out of the day view), along with the 'first' 99 and 'last' months of event coverage. 100 101 The additional 'category_names', 'remote_sources', 'search_pattern', 102 'template_name', 'parent_name' and 'mode' parameters are used to 103 configure the links employed by the view. 104 105 The 'raw_resolution' is used to parameterise download links, whereas the 106 'resolution' affects the view for certain modes and is also used to 107 parameterise links. 108 109 The 'name_usage' parameter controls how names are shown on calendar mode 110 events, such as how often labels are repeated. 111 112 The 'map_name' parameter provides the name of a map to be used in the 113 map mode. 114 """ 115 116 self.page = page 117 self.calendar_name = calendar_name 118 self.raw_calendar_start = raw_calendar_start 119 self.raw_calendar_end = raw_calendar_end 120 self.original_calendar_start = original_calendar_start 121 self.original_calendar_end = original_calendar_end 122 self.calendar_start = calendar_start 123 self.calendar_end = calendar_end 124 self.wider_calendar_start = wider_calendar_start 125 self.wider_calendar_end = wider_calendar_end 126 self.template_name = template_name 127 self.parent_name = parent_name 128 self.mode = mode 129 self.raw_resolution = raw_resolution 130 self.resolution = resolution 131 self.name_usage = name_usage 132 self.map_name = map_name 133 134 # Search-related parameters for links. 135 136 self.category_name_parameters = "&".join([("category=%s" % name) for name in category_names]) 137 self.remote_source_parameters = "&".join([("source=%s" % source) for source in remote_sources]) 138 self.search_pattern = search_pattern 139 140 # Calculate the duration in terms of the highest common unit of time. 141 142 self.first = first 143 self.last = last 144 self.initDuration() 145 146 self.showing_everything = not self.calendar_start and not self.calendar_end 147 148 if not self.showing_everything: 149 150 # Store the view parameters. 151 152 self.previous_start = first.previous() 153 self.next_start = first.next() 154 self.previous_end = last.previous() 155 self.next_end = last.next() 156 157 self.previous_set_start = first.update(-self.duration) 158 self.next_set_start = first.update(self.duration) 159 self.previous_set_end = last.update(-self.duration) 160 self.next_set_end = last.update(self.duration) 161 162 def initDuration(self): 163 164 "Limit the duration of the calendar to prevent excessive output." 165 166 request = self.page.request 167 168 self.duration = abs(self.last - self.first) + 1 169 170 # Limit to the specified number of units. 171 172 limit = int(getattr(request.cfg, "event_aggregator_max_duration", 31)) 173 174 if self.duration > limit: 175 if isinstance(self.first, Date): 176 self.last = self.first.day_update(limit - 1) 177 else: 178 self.last = self.first.month_update(limit - 1) 179 180 self.calendar_start = self.calendar_start or self.first 181 self.calendar_end = self.calendar_end or self.last 182 self.duration = limit 183 184 def getIdentifier(self): 185 186 "Return a unique identifier to be used to refer to this view." 187 188 # NOTE: Nasty hack to get a unique identifier if no name is given. 189 190 return self.calendar_name or str(id(self)) 191 192 def getQualifiedParameterName(self, argname): 193 194 "Return the 'argname' qualified using the calendar name." 195 196 if self.calendar_name: 197 return getQualifiedParameterName(self.calendar_name, argname) 198 else: 199 return argname 200 201 def getDateQueryString(self, argname, date, prefix=1): 202 203 """ 204 Return a query string fragment for the given 'argname', referring to the 205 month given by the specified 'year_month' object, appropriate for this 206 calendar. 207 208 If 'prefix' is specified and set to a false value, the parameters in the 209 query string will not be calendar-specific, but could be used with the 210 summary action. 211 """ 212 213 suffixes = ["year", "month", "day"] 214 215 if date is not None: 216 args = [] 217 for suffix, value in zip(suffixes, date.as_tuple()): 218 suffixed_argname = "%s-%s" % (argname, suffix) 219 if prefix: 220 suffixed_argname = self.getQualifiedParameterName(suffixed_argname) 221 args.append("%s=%s" % (suffixed_argname, value)) 222 return "&".join(args) 223 else: 224 return "" 225 226 def getRawDateQueryString(self, argname, date, prefix=1): 227 228 """ 229 Return a query string fragment for the given 'argname', referring to the 230 date given by the specified 'date' value, appropriate for this 231 calendar. 232 233 If 'prefix' is specified and set to a false value, the parameters in the 234 query string will not be calendar-specific, but could be used with the 235 summary action. 236 """ 237 238 if date is not None: 239 if prefix: 240 argname = self.getQualifiedParameterName(argname) 241 return "%s=%s" % (argname, wikiutil.url_quote(date)) 242 else: 243 return "" 244 245 def getNavigationLink(self, start, end, mode=None, resolution=None, wider_start=None, wider_end=None): 246 247 """ 248 Return a query string fragment for navigation to a view showing months 249 from 'start' to 'end' inclusive, with the optional 'mode' indicating the 250 view style and the optional 'resolution' indicating the resolution of a 251 view, if configurable. 252 253 If the 'wider_start' and 'wider_end' arguments are given, parameters 254 indicating a wider calendar view (when returning from a day view, for 255 example) will be included in the link. 256 """ 257 258 return "%s&%s&%s=%s&%s=%s&%s&%s" % ( 259 self.getRawDateQueryString("start", start), 260 self.getRawDateQueryString("end", end), 261 self.getQualifiedParameterName("mode"), mode or self.mode, 262 self.getQualifiedParameterName("resolution"), resolution or self.resolution, 263 self.getRawDateQueryString("wider-start", wider_start), 264 self.getRawDateQueryString("wider-end", wider_end), 265 ) 266 267 def getUpdateLink(self, start, end, mode=None, resolution=None, wider_start=None, wider_end=None): 268 269 """ 270 Return a query string fragment for navigation to a view showing months 271 from 'start' to 'end' inclusive, with the optional 'mode' indicating the 272 view style and the optional 'resolution' indicating the resolution of a 273 view, if configurable. This link differs from the conventional 274 navigation link in that it is sufficient to activate the update action 275 and produce an updated region of the page without needing to locate and 276 process the page or any macro invocation. 277 278 If the 'wider_start' and 'wider_end' arguments are given, parameters 279 indicating a wider calendar view (when returning from a day view, for 280 example) will be included in the link. 281 """ 282 283 parameters = [ 284 self.getRawDateQueryString("start", start, 0), 285 self.getRawDateQueryString("end", end, 0), 286 self.category_name_parameters, 287 self.remote_source_parameters, 288 self.getRawDateQueryString("wider-start", wider_start, 0), 289 self.getRawDateQueryString("wider-end", wider_end, 0), 290 ] 291 292 pairs = [ 293 ("calendar", self.calendar_name or ""), 294 ("calendarstart", self.raw_calendar_start or ""), 295 ("calendarend", self.raw_calendar_end or ""), 296 ("mode", mode or self.mode), 297 ("resolution", resolution or self.resolution), 298 ("raw-resolution", self.raw_resolution), 299 ("parent", self.parent_name or ""), 300 ("template", self.template_name or ""), 301 ("names", self.name_usage), 302 ("map", self.map_name or ""), 303 ("search", self.search_pattern or ""), 304 ] 305 306 url = self.page.url(self.page.request, 307 "action=EventAggregatorUpdate&%s" % ( 308 "&".join([("%s=%s" % (key, wikiutil.url_quote(value))) for (key, value) in pairs] + parameters) 309 ), relative=False) 310 311 return "return replaceCalendar('EventAggregator-%s', '%s')" % (self.getIdentifier(), url) 312 313 def getNewEventLink(self, start): 314 315 """ 316 Return a query string activating the new event form, incorporating the 317 calendar parameters, specialising the form for the given 'start' date or 318 month. 319 """ 320 321 if start is not None: 322 details = start.as_tuple() 323 pairs = zip(["start-year=%d", "start-month=%d", "start-day=%d"], details) 324 args = [(param % value) for (param, value) in pairs] 325 args = "&".join(args) 326 else: 327 args = "" 328 329 # Prepare navigation details for the calendar shown with the new event 330 # form. 331 332 navigation_link = self.getNavigationLink( 333 self.calendar_start, self.calendar_end 334 ) 335 336 return "action=EventAggregatorNewEvent%s%s&template=%s&parent=%s&%s" % ( 337 args and "&%s" % args, 338 self.category_name_parameters and "&%s" % self.category_name_parameters, 339 self.template_name, self.parent_name or "", 340 navigation_link) 341 342 def getFullDateLabel(self, date): 343 return getFullDateLabel(self.page.request, date) 344 345 def getFullMonthLabel(self, year_month): 346 return getFullMonthLabel(self.page.request, year_month) 347 348 def getFullLabel(self, arg, resolution): 349 return resolution == "date" and self.getFullDateLabel(arg) or self.getFullMonthLabel(arg) 350 351 def _getCalendarPeriod(self, start_label, end_label, default_label): 352 353 """ 354 Return a label describing a calendar period in terms of the given 355 'start_label' and 'end_label', with the 'default_label' being used where 356 the supplied start and end labels fail to produce a meaningful label. 357 """ 358 359 output = [] 360 append = output.append 361 362 if start_label: 363 append(start_label) 364 if end_label and start_label != end_label: 365 if output: 366 append(" - ") 367 append(end_label) 368 return "".join(output) or default_label 369 370 def getCalendarPeriod(self): 371 372 "Return the period description for the shown calendar." 373 374 _ = self.page.request.getText 375 return self._getCalendarPeriod( 376 self.calendar_start and self.getFullLabel(self.calendar_start, self.resolution), 377 self.calendar_end and self.getFullLabel(self.calendar_end, self.resolution), 378 _("All events") 379 ) 380 381 def getOriginalCalendarPeriod(self): 382 383 "Return the period description for the originally specified calendar." 384 385 _ = self.page.request.getText 386 return self._getCalendarPeriod( 387 self.original_calendar_start and self.getFullLabel(self.original_calendar_start, self.raw_resolution), 388 self.original_calendar_end and self.getFullLabel(self.original_calendar_end, self.raw_resolution), 389 _("All events") 390 ) 391 392 def getRawCalendarPeriod(self): 393 394 "Return the raw period description for the calendar." 395 396 _ = self.page.request.getText 397 return self._getCalendarPeriod( 398 self.raw_calendar_start, 399 self.raw_calendar_end, 400 _("No period specified") 401 ) 402 403 def writeDownloadControls(self): 404 405 """ 406 Return a representation of the download controls, featuring links for 407 view, calendar and customised downloads and subscriptions. 408 """ 409 410 page = self.page 411 request = page.request 412 fmt = request.formatter 413 _ = request.getText 414 415 output = [] 416 append = output.append 417 418 # The full URL is needed for webcal links. 419 420 full_url = "%s/%s" % (request.getBaseURL().rstrip("/"), getPathInfo(request).lstrip("/")) 421 422 # Generate the links. 423 424 download_dialogue_link = "action=EventAggregatorSummary&parent=%s&search=%s%s%s" % ( 425 self.parent_name or "", 426 self.search_pattern or "", 427 self.category_name_parameters and "&%s" % self.category_name_parameters, 428 self.remote_source_parameters and "&%s" % self.remote_source_parameters 429 ) 430 download_all_link = download_dialogue_link + "&doit=1" 431 download_link = download_all_link + ("&%s&%s" % ( 432 self.getDateQueryString("start", self.calendar_start, prefix=0), 433 self.getDateQueryString("end", self.calendar_end, prefix=0) 434 )) 435 436 # The entire calendar download uses the originally specified resolution 437 # of the calendar as does the dialogue. The other link uses the current 438 # resolution. 439 440 download_dialogue_link += "&resolution=%s" % self.raw_resolution 441 download_all_link += "&resolution=%s" % self.raw_resolution 442 download_link += "&resolution=%s" % self.resolution 443 444 # Subscription links just explicitly select the RSS format. 445 446 subscribe_dialogue_link = download_dialogue_link + "&format=RSS" 447 subscribe_all_link = download_all_link + "&format=RSS" 448 subscribe_link = download_link + "&format=RSS" 449 450 # Adjust the "download all" and "subscribe all" links if the calendar 451 # has an inherent period associated with it. 452 453 period_limits = [] 454 455 if self.raw_calendar_start: 456 period_limits.append("&%s" % 457 self.getRawDateQueryString("start", self.raw_calendar_start, prefix=0) 458 ) 459 if self.raw_calendar_end: 460 period_limits.append("&%s" % 461 self.getRawDateQueryString("end", self.raw_calendar_end, prefix=0) 462 ) 463 464 period_limits = "".join(period_limits) 465 466 download_dialogue_link += period_limits 467 download_all_link += period_limits 468 subscribe_dialogue_link += period_limits 469 subscribe_all_link += period_limits 470 471 # Pop-up descriptions of the downloadable calendars. 472 473 shown_calendar_period = self.getCalendarPeriod() 474 original_calendar_period = self.getOriginalCalendarPeriod() 475 raw_calendar_period = self.getRawCalendarPeriod() 476 477 # Write the controls. 478 479 # Download controls. 480 481 controls_target = "%s-controls" % self.getIdentifier() 482 483 append(fmt.div(on=1, css_class="event-download-controls", id=controls_target)) 484 485 download_target = "%s-download" % self.getIdentifier() 486 487 append(fmt.span(on=1, css_class="event-download", id=download_target)) 488 append(linkToPage(request, page, _("Download..."), anchor=download_target)) 489 append(fmt.div(on=1, css_class="event-download-popup")) 490 491 append(fmt.div(on=1, css_class="event-download-item")) 492 append(fmt.span(on=1, css_class="event-download-types")) 493 append(fmt.span(on=1, css_class="event-download-webcal")) 494 append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_link)) 495 append(fmt.span(on=0)) 496 append(fmt.span(on=1, css_class="event-download-http")) 497 append(linkToPage(request, page, _("http"), download_link, title=_("Download this view in the browser"))) 498 append(fmt.span(on=0)) 499 append(fmt.span(on=0)) # end types 500 append(fmt.span(on=1, css_class="event-download-label")) 501 append(fmt.text(_("Download this view"))) 502 append(fmt.span(on=0)) # end label 503 append(fmt.span(on=1, css_class="event-download-period")) 504 append(fmt.text(shown_calendar_period)) 505 append(fmt.span(on=0)) 506 append(fmt.div(on=0)) 507 508 append(fmt.div(on=1, css_class="event-download-item")) 509 append(fmt.span(on=1, css_class="event-download-types")) 510 append(fmt.span(on=1, css_class="event-download-webcal")) 511 append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_all_link)) 512 append(fmt.span(on=0)) 513 append(fmt.span(on=1, css_class="event-download-http")) 514 append(linkToPage(request, page, _("http"), download_all_link, title=_("Download this calendar in the browser"))) 515 append(fmt.span(on=0)) 516 append(fmt.span(on=0)) # end types 517 append(fmt.span(on=1, css_class="event-download-label")) 518 append(fmt.text(_("Download this calendar"))) 519 append(fmt.span(on=0)) # end label 520 append(fmt.span(on=1, css_class="event-download-period")) 521 append(fmt.text(original_calendar_period)) 522 append(fmt.span(on=0)) 523 append(fmt.span(on=1, css_class="event-download-period-raw")) 524 append(fmt.text(raw_calendar_period)) 525 append(fmt.span(on=0)) 526 append(fmt.div(on=0)) 527 528 append(fmt.div(on=1, css_class="event-download-item")) 529 append(fmt.span(on=1, css_class="event-download-link")) 530 append(linkToPage(request, page, _("Edit download options..."), download_dialogue_link)) 531 append(fmt.span(on=0)) # end label 532 append(fmt.div(on=0)) 533 534 append(fmt.div(on=1, css_class="event-download-item focus-only")) 535 append(fmt.span(on=1, css_class="event-download-link")) 536 append(linkToPage(request, page, _("Cancel"), anchor=controls_target)) 537 append(fmt.span(on=0)) # end label 538 append(fmt.div(on=0)) 539 540 append(fmt.div(on=0)) # end of pop-up 541 append(fmt.span(on=0)) # end of download 542 543 # Subscription controls. 544 545 subscribe_target = "%s-subscribe" % self.getIdentifier() 546 547 append(fmt.span(on=1, css_class="event-download", id=subscribe_target)) 548 append(linkToPage(request, page, _("Subscribe..."), anchor=subscribe_target)) 549 append(fmt.div(on=1, css_class="event-download-popup")) 550 551 append(fmt.div(on=1, css_class="event-download-item")) 552 append(fmt.span(on=1, css_class="event-download-label")) 553 append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link)) 554 append(fmt.span(on=0)) # end label 555 append(fmt.span(on=1, css_class="event-download-period")) 556 append(fmt.text(shown_calendar_period)) 557 append(fmt.span(on=0)) 558 append(fmt.div(on=0)) 559 560 append(fmt.div(on=1, css_class="event-download-item")) 561 append(fmt.span(on=1, css_class="event-download-label")) 562 append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link)) 563 append(fmt.span(on=0)) # end label 564 append(fmt.span(on=1, css_class="event-download-period")) 565 append(fmt.text(original_calendar_period)) 566 append(fmt.span(on=0)) 567 append(fmt.span(on=1, css_class="event-download-period-raw")) 568 append(fmt.text(raw_calendar_period)) 569 append(fmt.span(on=0)) 570 append(fmt.div(on=0)) 571 572 append(fmt.div(on=1, css_class="event-download-item")) 573 append(fmt.span(on=1, css_class="event-download-link")) 574 append(linkToPage(request, page, _("Edit subscription options..."), subscribe_dialogue_link)) 575 append(fmt.span(on=0)) # end label 576 append(fmt.div(on=0)) 577 578 append(fmt.div(on=1, css_class="event-download-item focus-only")) 579 append(fmt.span(on=1, css_class="event-download-link")) 580 append(linkToPage(request, page, _("Cancel"), anchor=controls_target)) 581 append(fmt.span(on=0)) # end label 582 append(fmt.div(on=0)) 583 584 append(fmt.div(on=0)) # end of pop-up 585 append(fmt.span(on=0)) # end of download 586 587 append(fmt.div(on=0)) # end of controls 588 589 return "".join(output) 590 591 def writeViewControls(self): 592 593 """ 594 Return a representation of the view mode controls, permitting viewing of 595 aggregated events in calendar, list or table form. 596 """ 597 598 page = self.page 599 request = page.request 600 fmt = request.formatter 601 _ = request.getText 602 603 output = [] 604 append = output.append 605 606 # For day view links to other views, the wider view parameters should 607 # be used in order to be able to return to those other views. 608 609 specific_start = self.calendar_start 610 specific_end = self.calendar_end 611 612 multiday = self.resolution == "date" and len(specific_start.days_until(specific_end)) > 1 613 614 start = self.wider_calendar_start or self.original_calendar_start and specific_start 615 end = self.wider_calendar_end or self.original_calendar_end and specific_end 616 617 help_page = Page(request, "HelpOnEventAggregator") 618 619 calendar_link = self.getNavigationLink(start and start.as_month(), end and end.as_month(), "calendar", "month") 620 calendar_update_link = self.getUpdateLink(start and start.as_month(), end and end.as_month(), "calendar", "month") 621 list_link = self.getNavigationLink(start, end, "list", "month") 622 list_update_link = self.getUpdateLink(start, end, "list", "month") 623 table_link = self.getNavigationLink(start, end, "table", "month") 624 table_update_link = self.getUpdateLink(start, end, "table", "month") 625 map_link = self.getNavigationLink(start, end, "map", "month") 626 map_update_link = self.getUpdateLink(start, end, "map", "month") 627 628 # Specific links permit date-level navigation. 629 630 specific_day_link = self.getNavigationLink(specific_start, specific_end, "day", wider_start=start, wider_end=end) 631 specific_day_update_link = self.getUpdateLink(specific_start, specific_end, "day", wider_start=start, wider_end=end) 632 specific_list_link = self.getNavigationLink(specific_start, specific_end, "list", wider_start=start, wider_end=end) 633 specific_list_update_link = self.getUpdateLink(specific_start, specific_end, "list", wider_start=start, wider_end=end) 634 specific_table_link = self.getNavigationLink(specific_start, specific_end, "table", wider_start=start, wider_end=end) 635 specific_table_update_link = self.getUpdateLink(specific_start, specific_end, "table", wider_start=start, wider_end=end) 636 specific_map_link = self.getNavigationLink(specific_start, specific_end, "map", wider_start=start, wider_end=end) 637 specific_map_update_link = self.getUpdateLink(specific_start, specific_end, "map", wider_start=start, wider_end=end) 638 639 new_event_link = self.getNewEventLink(start) 640 641 # Write the controls. 642 643 append(fmt.div(on=1, css_class="event-view-controls")) 644 645 append(fmt.span(on=1, css_class="event-view")) 646 append(linkToPage(request, help_page, _("Help"))) 647 append(fmt.span(on=0)) 648 649 append(fmt.span(on=1, css_class="event-view")) 650 append(linkToPage(request, page, _("New event"), new_event_link)) 651 append(fmt.span(on=0)) 652 653 if self.mode != "calendar": 654 view_label = self.resolution == "date" and \ 655 (multiday and _("View days in calendar") or _("View day in calendar")) or \ 656 _("View as calendar") 657 append(fmt.span(on=1, css_class="event-view")) 658 append(linkToPage(request, page, view_label, calendar_link, onclick=calendar_update_link)) 659 append(fmt.span(on=0)) 660 661 if self.resolution == "date" and self.mode != "day": 662 view_label = multiday and _("View days as calendar") or _("View day as calendar") 663 append(fmt.span(on=1, css_class="event-view")) 664 append(linkToPage(request, page, view_label, specific_day_link, onclick=specific_day_update_link)) 665 append(fmt.span(on=0)) 666 667 if self.resolution != "date" and self.mode != "list" or self.resolution == "date": 668 view_label = self.resolution == "date" and \ 669 (multiday and _("View days in list") or _("View day in list")) or \ 670 _("View as list") 671 append(fmt.span(on=1, css_class="event-view")) 672 append(linkToPage(request, page, view_label, list_link, onclick=list_update_link)) 673 append(fmt.span(on=0)) 674 675 if self.resolution == "date" and self.mode != "list": 676 view_label = multiday and _("View days as list") or _("View day as list") 677 append(fmt.span(on=1, css_class="event-view")) 678 append(linkToPage(request, page, view_label, specific_list_link, onclick=specific_list_update_link)) 679 append(fmt.span(on=0)) 680 681 if self.resolution != "date" and self.mode != "table" or self.resolution == "date": 682 view_label = self.resolution == "date" and \ 683 (multiday and _("View days in table") or _("View day in table")) or \ 684 _("View as table") 685 append(fmt.span(on=1, css_class="event-view")) 686 append(linkToPage(request, page, view_label, table_link, onclick=table_update_link)) 687 append(fmt.span(on=0)) 688 689 if self.resolution == "date" and self.mode != "table": 690 view_label = multiday and _("View days as table") or _("View day as table") 691 append(fmt.span(on=1, css_class="event-view")) 692 append(linkToPage(request, page, view_label, specific_table_link, onclick=specific_table_update_link)) 693 append(fmt.span(on=0)) 694 695 if self.map_name: 696 if self.resolution != "date" and self.mode != "map" or self.resolution == "date": 697 view_label = self.resolution == "date" and \ 698 (multiday and _("View days in map") or _("View day in map")) or \ 699 _("View as map") 700 append(fmt.span(on=1, css_class="event-view")) 701 append(linkToPage(request, page, view_label, map_link, onclick=map_update_link)) 702 append(fmt.span(on=0)) 703 704 if self.resolution == "date" and self.mode != "map": 705 view_label = multiday and _("View days as map") or _("View day as map") 706 append(fmt.span(on=1, css_class="event-view")) 707 append(linkToPage(request, page, view_label, specific_map_link, onclick=specific_map_update_link)) 708 append(fmt.span(on=0)) 709 710 append(fmt.div(on=0)) 711 712 return "".join(output) 713 714 def writeMapHeading(self): 715 716 """ 717 Return the calendar heading for the current calendar, providing links 718 permitting navigation to other periods. 719 """ 720 721 label = self.getCalendarPeriod() 722 723 if self.raw_calendar_start is None or self.raw_calendar_end is None: 724 fmt = self.page.request.formatter 725 output = [] 726 append = output.append 727 append(fmt.span(on=1)) 728 append(fmt.text(label)) 729 append(fmt.span(on=0)) 730 return "".join(output) 731 else: 732 return self._writeCalendarHeading(label, self.calendar_start, self.calendar_end) 733 734 def writeDateHeading(self, date): 735 if isinstance(date, Date): 736 return self.writeDayHeading(date) 737 else: 738 return self.writeMonthHeading(date) 739 740 def writeMonthHeading(self, year_month): 741 742 """ 743 Return the calendar heading for the given 'year_month' (a Month object) 744 providing links permitting navigation to other months. 745 """ 746 747 full_month_label = self.getFullMonthLabel(year_month) 748 end_month = year_month.update(self.duration - 1) 749 return self._writeCalendarHeading(full_month_label, year_month, end_month) 750 751 def writeDayHeading(self, date): 752 753 """ 754 Return the calendar heading for the given 'date' (a Date object) 755 providing links permitting navigation to other dates. 756 """ 757 758 full_date_label = self.getFullDateLabel(date) 759 end_date = date.update(self.duration - 1) 760 return self._writeCalendarHeading(full_date_label, date, end_date) 761 762 def writeCalendarNavigation(self): 763 764 "Return navigation links for a calendar." 765 766 if self.showing_everything: 767 return "" 768 769 page = self.page 770 request = page.request 771 fmt = request.formatter 772 _ = request.getText 773 774 output = [] 775 append = output.append 776 777 # Links to the previous set of months and to a calendar shifted 778 # back one month. 779 780 previous_set_link = self.getNavigationLink( 781 self.previous_set_start, self.previous_set_end 782 ) 783 previous_link = self.getNavigationLink( 784 self.previous_start, self.previous_end 785 ) 786 previous_set_update_link = self.getUpdateLink( 787 self.previous_set_start, self.previous_set_end 788 ) 789 previous_update_link = self.getUpdateLink( 790 self.previous_start, self.previous_end 791 ) 792 793 # Links to the next set of months and to a calendar shifted 794 # forward one month. 795 796 next_set_link = self.getNavigationLink( 797 self.next_set_start, self.next_set_end 798 ) 799 next_link = self.getNavigationLink( 800 self.next_start, self.next_end 801 ) 802 next_set_update_link = self.getUpdateLink( 803 self.next_set_start, self.next_set_end 804 ) 805 next_update_link = self.getUpdateLink( 806 self.next_start, self.next_end 807 ) 808 809 append(fmt.div(on=1, css_class="event-calendar-navigation")) 810 811 append(fmt.span(on=1, css_class="previous")) 812 append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link, title=_("Previous set"))) 813 append(fmt.text(" ")) 814 append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link, title=_("Previous"))) 815 append(fmt.span(on=0)) 816 817 append(fmt.span(on=1, css_class="next")) 818 append(linkToPage(request, page, ">", next_link, onclick=next_update_link, title=_("Next"))) 819 append(fmt.text(" ")) 820 append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link, title=_("Next set"))) 821 append(fmt.span(on=0)) 822 823 append(fmt.div(on=0)) 824 825 return "".join(output) 826 827 def _writeCalendarHeading(self, label, start, end): 828 829 """ 830 Write a calendar heading providing links permitting navigation to other 831 periods, using the given 'label' along with the 'start' and 'end' dates 832 to provide a link to a particular period. 833 """ 834 835 page = self.page 836 request = page.request 837 fmt = request.formatter 838 _ = request.getText 839 840 output = [] 841 append = output.append 842 843 if not self.showing_everything: 844 845 # A link leading to this date being at the top of the calendar. 846 847 date_link = self.getNavigationLink(start, end) 848 date_update_link = self.getUpdateLink(start, end) 849 850 append(linkToPage(request, page, label, date_link, onclick=date_update_link, title=_("Show this period first"))) 851 852 else: 853 append(fmt.text(label)) 854 855 return "".join(output) 856 857 def writeDayNumberHeading(self, date, busy): 858 859 """ 860 Return a link for the given 'date' which will activate the new event 861 action for the given day. If 'busy' is given as a true value, the 862 heading will be marked as busy. 863 """ 864 865 page = self.page 866 request = page.request 867 fmt = request.formatter 868 _ = request.getText 869 870 output = [] 871 append = output.append 872 873 year, month, day = date.as_tuple() 874 new_event_link = self.getNewEventLink(date) 875 876 # Prepare a link to the day view for this day. 877 878 day_view_link = self.getNavigationLink(date, date, "day", "date", self.calendar_start, self.calendar_end) 879 day_view_update_link = self.getUpdateLink(date, date, "day", "date", self.calendar_start, self.calendar_end) 880 881 # Output the heading. 882 883 day_target = "%s-day-%d" % (self.getIdentifier(), day) 884 day_menu_target = "%s-menu" % day_target 885 886 today_attr = date == getCurrentDate() and "event-day-current" or "" 887 888 append(fmt.rawHTML("<th class='event-day-heading event-day-%s %s' colspan='3' axis='day' id='%s'>" % ( 889 busy and "busy" or "empty", escattr(today_attr), escattr(day_target)))) 890 891 # Output the number and pop-up menu. 892 893 append(fmt.div(on=1, css_class="event-day-box", id=day_menu_target)) 894 895 append(fmt.span(on=1, css_class="event-day-number-popup")) 896 append(fmt.span(on=1, css_class="event-day-number-link")) 897 append(linkToPage(request, page, _("View day"), day_view_link, onclick=day_view_update_link)) 898 append(fmt.span(on=0)) 899 append(fmt.span(on=1, css_class="event-day-number-link")) 900 append(linkToPage(request, page, _("New event on this day"), new_event_link)) 901 append(fmt.span(on=0)) 902 append(fmt.span(on=0)) 903 904 # Link the number to the day view. 905 906 append(fmt.span(on=1, css_class="event-day-number")) 907 append(linkToPage(request, page, unicode(day), anchor=day_menu_target, title=_("View day options"))) 908 append(fmt.span(on=0)) 909 910 append(fmt.div(on=0)) 911 912 # End of heading. 913 914 append(fmt.rawHTML("</th>")) 915 916 return "".join(output) 917 918 # Common layout methods. 919 920 def getEventStyle(self, colour_seed): 921 922 "Generate colour style information using the given 'colour_seed'." 923 924 bg = getColour(colour_seed) 925 fg = getBlackOrWhite(bg) 926 return "background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg) 927 928 def writeEventSummaryBox(self, event): 929 930 "Return an event summary box linking to the given 'event'." 931 932 page = self.page 933 request = page.request 934 fmt = request.formatter 935 936 output = [] 937 append = output.append 938 939 event_details = event.getDetails() 940 event_summary = event.getSummary(self.parent_name) 941 942 is_ambiguous = event.as_timespan().ambiguous() 943 style = self.getEventStyle(event_summary) 944 945 # The event box contains the summary, alongside 946 # other elements. 947 948 append(fmt.div(on=1, css_class="event-summary-box")) 949 append(fmt.div(on=1, css_class="event-summary", style=style)) 950 951 if is_ambiguous: 952 append(fmt.icon("/!\\")) 953 954 append(event.linkToEvent(request, event_summary)) 955 append(fmt.div(on=0)) 956 957 # Add a pop-up element for long summaries. 958 959 append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 960 961 if is_ambiguous: 962 append(fmt.icon("/!\\")) 963 964 append(event.linkToEvent(request, event_summary)) 965 append(fmt.div(on=0)) 966 967 append(fmt.div(on=0)) 968 969 return "".join(output) 970 971 # Calendar layout methods. 972 973 def writeMonthTableHeading(self, year_month): 974 page = self.page 975 fmt = page.request.formatter 976 977 output = [] 978 append = output.append 979 980 # Using a caption for accessibility reasons. 981 982 append(fmt.rawHTML('<caption class="event-month-heading">')) 983 append(self.writeMonthHeading(year_month)) 984 append(fmt.rawHTML("</caption>")) 985 986 return "".join(output) 987 988 def writeWeekdayHeadings(self): 989 page = self.page 990 request = page.request 991 fmt = request.formatter 992 _ = request.getText 993 994 output = [] 995 append = output.append 996 997 append(fmt.table_row(on=1)) 998 999 for weekday in range(0, 7): 1000 append(fmt.rawHTML(u"<th class='event-weekday-heading' colspan='3' abbr='%s' scope='row'>" % 1001 escattr(_(getVerboseDayLabel(weekday))))) 1002 append(fmt.text(_(getDayLabel(weekday)))) 1003 append(fmt.rawHTML("</th>")) 1004 1005 append(fmt.table_row(on=0)) 1006 return "".join(output) 1007 1008 def writeDayNumbers(self, first_day, number_of_days, month, coverage): 1009 page = self.page 1010 fmt = page.request.formatter 1011 1012 output = [] 1013 append = output.append 1014 1015 append(fmt.table_row(on=1)) 1016 1017 for weekday in range(0, 7): 1018 day = first_day + weekday 1019 date = month.as_date(day) 1020 1021 # Output out-of-month days. 1022 1023 if day < 1 or day > number_of_days: 1024 append(fmt.table_cell(on=1, 1025 attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"})) 1026 append(fmt.table_cell(on=0)) 1027 1028 # Output normal days. 1029 1030 else: 1031 # Output the day heading, making a link to a new event 1032 # action. 1033 1034 append(self.writeDayNumberHeading(date, date in coverage)) 1035 1036 # End of day numbers. 1037 1038 append(fmt.table_row(on=0)) 1039 return "".join(output) 1040 1041 def writeEmptyWeek(self, first_day, number_of_days, month): 1042 page = self.page 1043 fmt = page.request.formatter 1044 1045 output = [] 1046 append = output.append 1047 1048 append(fmt.table_row(on=1)) 1049 1050 for weekday in range(0, 7): 1051 day = first_day + weekday 1052 date = month.as_date(day) 1053 1054 today_attr = date == getCurrentDate() and "event-day-current" or "" 1055 1056 # Output out-of-month days. 1057 1058 if day < 1 or day > number_of_days: 1059 append(fmt.table_cell(on=1, 1060 attrs={"class" : "event-day-content event-day-excluded %s" % today_attr, "colspan" : "3"})) 1061 append(fmt.table_cell(on=0)) 1062 1063 # Output empty days. 1064 1065 else: 1066 append(fmt.table_cell(on=1, 1067 attrs={"class" : "event-day-content event-day-empty %s" % today_attr, "colspan" : "3"})) 1068 1069 append(fmt.table_row(on=0)) 1070 return "".join(output) 1071 1072 def writeWeekSlots(self, first_day, number_of_days, month, week_end, week_slots): 1073 output = [] 1074 append = output.append 1075 1076 locations = week_slots.keys() 1077 locations.sort(sort_none_first) 1078 1079 # Visit each slot corresponding to a location (or no location). 1080 1081 for location in locations: 1082 1083 # Visit each coverage span, presenting the events in the span. 1084 1085 for events in week_slots[location]: 1086 1087 # Output each set. 1088 1089 append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events)) 1090 1091 # Add a spacer. 1092 1093 append(self.writeWeekSpacer(first_day, number_of_days, month)) 1094 1095 return "".join(output) 1096 1097 def writeWeekSlot(self, first_day, number_of_days, month, week_end, events): 1098 page = self.page 1099 request = page.request 1100 fmt = request.formatter 1101 1102 output = [] 1103 append = output.append 1104 1105 append(fmt.table_row(on=1)) 1106 1107 # Then, output day details. 1108 1109 for weekday in range(0, 7): 1110 day = first_day + weekday 1111 date = month.as_date(day) 1112 1113 # Skip out-of-month days. 1114 1115 if day < 1 or day > number_of_days: 1116 append(fmt.table_cell(on=1, 1117 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 1118 append(fmt.table_cell(on=0)) 1119 continue 1120 1121 # Output the day. 1122 # Where a day does not contain an event, a single cell is used. 1123 # Otherwise, multiple cells are used to provide space before, during 1124 # and after events. 1125 1126 day_target = "%s-day-%d" % (self.getIdentifier(), day) 1127 today_attr = date == getCurrentDate() and "event-day-current" or "" 1128 1129 if date not in events: 1130 append(fmt.rawHTML(u"<td class='event-day-content event-day-empty %s' colspan='3' headers='%s'>" % ( 1131 escattr(today_attr), escattr(day_target)))) 1132 1133 # Get event details for the current day. 1134 1135 for event in events: 1136 event_details = event.getDetails() 1137 1138 if date not in event: 1139 continue 1140 1141 # Get basic properties of the event. 1142 1143 starts_today = event_details["start"] == date 1144 ends_today = event_details["end"] == date 1145 event_summary = event.getSummary(self.parent_name) 1146 1147 style = self.getEventStyle(event_summary) 1148 1149 # Determine if the event name should be shown. 1150 1151 start_of_period = starts_today or weekday == 0 or day == 1 1152 1153 if self.name_usage == "daily" or start_of_period: 1154 hide_text = 0 1155 else: 1156 hide_text = 1 1157 1158 # Output start of day gap and determine whether 1159 # any event content should be explicitly output 1160 # for this day. 1161 1162 if starts_today: 1163 1164 # Single day events... 1165 1166 if ends_today: 1167 colspan = 3 1168 event_day_type = "event-day-single" 1169 1170 # Events starting today... 1171 1172 else: 1173 append(fmt.rawHTML(u"<td class='event-day-start-gap %s' headers='%s'>" % ( 1174 escattr(today_attr), escattr(day_target)))) 1175 append(fmt.table_cell(on=0)) 1176 1177 # Calculate the span of this cell. 1178 # Events whose names appear on every day... 1179 1180 if self.name_usage == "daily": 1181 colspan = 2 1182 event_day_type = "event-day-starting" 1183 1184 # Events whose names appear once per week... 1185 1186 else: 1187 if event_details["end"] <= week_end: 1188 event_length = event_details["end"].day() - day + 1 1189 colspan = (event_length - 2) * 3 + 4 1190 else: 1191 event_length = week_end.day() - day + 1 1192 colspan = (event_length - 1) * 3 + 2 1193 1194 event_day_type = "event-day-multiple" 1195 1196 # Events continuing from a previous week... 1197 1198 elif start_of_period: 1199 1200 # End of continuing event... 1201 1202 if ends_today: 1203 colspan = 2 1204 event_day_type = "event-day-ending" 1205 1206 # Events continuing for at least one more day... 1207 1208 else: 1209 1210 # Calculate the span of this cell. 1211 # Events whose names appear on every day... 1212 1213 if self.name_usage == "daily": 1214 colspan = 3 1215 event_day_type = "event-day-full" 1216 1217 # Events whose names appear once per week... 1218 1219 else: 1220 if event_details["end"] <= week_end: 1221 event_length = event_details["end"].day() - day + 1 1222 colspan = (event_length - 1) * 3 + 2 1223 else: 1224 event_length = week_end.day() - day + 1 1225 colspan = event_length * 3 1226 1227 event_day_type = "event-day-multiple" 1228 1229 # Continuing events whose names appear on every day... 1230 1231 elif self.name_usage == "daily": 1232 if ends_today: 1233 colspan = 2 1234 event_day_type = "event-day-ending" 1235 else: 1236 colspan = 3 1237 event_day_type = "event-day-full" 1238 1239 # Continuing events whose names appear once per week... 1240 1241 else: 1242 colspan = None 1243 1244 # Output the main content only if it is not 1245 # continuing from a previous day. 1246 1247 if colspan is not None: 1248 1249 # Colour the cell for continuing events. 1250 1251 attrs={ 1252 "class" : escattr("event-day-content event-day-busy %s %s" % (event_day_type, today_attr)), 1253 "colspan" : str(colspan), 1254 "headers" : escattr(day_target), 1255 } 1256 1257 if not (starts_today and ends_today): 1258 attrs["style"] = style 1259 1260 append(fmt.rawHTML(u"<td class='%(class)s' colspan='%(colspan)s' headers='%(headers)s'>" % attrs)) 1261 1262 # Output the event. 1263 1264 if starts_today and ends_today or not hide_text: 1265 append(self.writeEventSummaryBox(event)) 1266 1267 append(fmt.table_cell(on=0)) 1268 1269 # Output end of day gap. 1270 1271 if ends_today and not starts_today: 1272 append(fmt.rawHTML("<td class='event-day-end-gap %s' headers='%s'>" % (escattr(today_attr), escattr(day_target)))) 1273 append(fmt.table_cell(on=0)) 1274 1275 # End of set. 1276 1277 append(fmt.table_row(on=0)) 1278 return "".join(output) 1279 1280 def writeWeekSpacer(self, first_day, number_of_days, month): 1281 page = self.page 1282 fmt = page.request.formatter 1283 1284 output = [] 1285 append = output.append 1286 1287 append(fmt.table_row(on=1)) 1288 1289 for weekday in range(0, 7): 1290 day = first_day + weekday 1291 date = month.as_date(day) 1292 today_attr = date == getCurrentDate() and "event-day-current" or "" 1293 1294 css_classes = "event-day-spacer %s" % today_attr 1295 1296 # Skip out-of-month days. 1297 1298 if day < 1 or day > number_of_days: 1299 css_classes += " event-day-excluded" 1300 1301 append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"})) 1302 append(fmt.table_cell(on=0)) 1303 1304 append(fmt.table_row(on=0)) 1305 return "".join(output) 1306 1307 # Day layout methods. 1308 1309 def writeDayTableHeading(self, date, colspan=1): 1310 page = self.page 1311 fmt = page.request.formatter 1312 1313 output = [] 1314 append = output.append 1315 1316 # Using a caption for accessibility reasons. 1317 1318 append(fmt.rawHTML('<caption class="event-full-day-heading">')) 1319 append(self.writeDayHeading(date)) 1320 append(fmt.rawHTML("</caption>")) 1321 1322 return "".join(output) 1323 1324 def writeEmptyDay(self, date): 1325 page = self.page 1326 fmt = page.request.formatter 1327 1328 output = [] 1329 append = output.append 1330 1331 append(fmt.table_row(on=1)) 1332 1333 append(fmt.table_cell(on=1, 1334 attrs={"class" : "event-day-content event-day-empty"})) 1335 1336 append(fmt.table_row(on=0)) 1337 return "".join(output) 1338 1339 def writeDaySlots(self, date, full_coverage, day_slots): 1340 1341 """ 1342 Given a 'date', non-empty 'full_coverage' for the day concerned, and a 1343 non-empty mapping of 'day_slots' (from locations to event collections), 1344 output the day slots for the day. 1345 """ 1346 1347 page = self.page 1348 fmt = page.request.formatter 1349 1350 output = [] 1351 append = output.append 1352 1353 locations = day_slots.keys() 1354 locations.sort(sort_none_first) 1355 1356 # Traverse the time scale of the full coverage, visiting each slot to 1357 # determine whether it provides content for each period. 1358 1359 scale = getCoverageScale(full_coverage) 1360 1361 # Define a mapping of events to rowspans. 1362 1363 rowspans = {} 1364 1365 # Populate each period with event details, recording how many periods 1366 # each event populates. 1367 1368 day_rows = [] 1369 1370 for period, limit, start_times, end_times in scale: 1371 1372 # Ignore timespans before this day. 1373 1374 if period != date: 1375 continue 1376 1377 # Visit each slot corresponding to a location (or no location). 1378 1379 day_row = [] 1380 have_event = False 1381 1382 for location in locations: 1383 1384 # Visit each coverage span, presenting the events in the span. 1385 1386 for events in day_slots[location]: 1387 event = self.getActiveEvent(period, events) 1388 if event is not None: 1389 if not rowspans.has_key(event): 1390 rowspans[event] = 1 1391 else: 1392 rowspans[event] += 1 1393 day_row.append((location, event)) 1394 have_event = have_event or event 1395 1396 start, end = period.as_limits() 1397 1398 # Only retain non-instant periods or periods having events. 1399 1400 if have_event or start != end: 1401 day_rows.append((period, day_row, start_times, end_times)) 1402 1403 # Output the locations. 1404 1405 append(fmt.table_row(on=1)) 1406 1407 # Add a spacer. 1408 1409 append(self.writeDaySpacer(colspan=2, cls="location")) 1410 1411 for location in locations: 1412 1413 # Add spacers to the column spans. 1414 1415 columns = len(day_slots[location]) * 2 - 1 1416 append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)})) 1417 append(fmt.text(location or "")) 1418 append(fmt.table_cell(on=0)) 1419 1420 # Add a trailing spacer. 1421 1422 append(self.writeDaySpacer(cls="location")) 1423 1424 append(fmt.table_row(on=0)) 1425 1426 # Output the periods with event details. 1427 1428 last_period = period = None 1429 events_written = set() 1430 1431 for period, day_row, start_times, end_times in day_rows: 1432 1433 # Write a heading describing the time. 1434 1435 append(fmt.table_row(on=1)) 1436 1437 # Show times only for distinct periods. 1438 1439 if not last_period or period.start != last_period.start: 1440 append(self.writeDayScaleHeading(start_times)) 1441 else: 1442 append(self.writeDayScaleHeading([])) 1443 1444 append(self.writeDaySpacer()) 1445 1446 # Visit each slot corresponding to a location (or no location). 1447 1448 for location, event in day_row: 1449 1450 # Output each location slot's contribution. 1451 1452 if event is None or event not in events_written: 1453 append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) 1454 if event is not None: 1455 events_written.add(event) 1456 1457 # Add a trailing spacer. 1458 1459 append(self.writeDaySpacer()) 1460 1461 append(fmt.table_row(on=0)) 1462 1463 last_period = period 1464 1465 # Write a final time heading if the last period ends in the current day. 1466 1467 if period is not None: 1468 if period.end == date: 1469 append(fmt.table_row(on=1)) 1470 append(self.writeDayScaleHeading(end_times)) 1471 1472 for slot in day_row: 1473 append(self.writeDaySpacer()) 1474 append(self.writeEmptyDaySlot()) 1475 1476 append(fmt.table_row(on=0)) 1477 1478 return "".join(output) 1479 1480 def writeDayScaleHeading(self, times): 1481 page = self.page 1482 fmt = page.request.formatter 1483 1484 output = [] 1485 append = output.append 1486 1487 append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"})) 1488 1489 first = 1 1490 for t in times: 1491 if isinstance(t, DateTime): 1492 if not first: 1493 append(fmt.linebreak(0)) 1494 append(fmt.text(t.time_string())) 1495 first = 0 1496 1497 append(fmt.table_cell(on=0)) 1498 1499 return "".join(output) 1500 1501 def getActiveEvent(self, period, events): 1502 for event in events: 1503 if period not in event: 1504 continue 1505 return event 1506 else: 1507 return None 1508 1509 def writeDaySlot(self, period, event, rowspan): 1510 page = self.page 1511 fmt = page.request.formatter 1512 1513 output = [] 1514 append = output.append 1515 1516 if event is not None: 1517 event_summary = event.getSummary(self.parent_name) 1518 style = self.getEventStyle(event_summary) 1519 1520 append(fmt.table_cell(on=1, attrs={ 1521 "class" : "event-timespan-content event-timespan-busy", 1522 "style" : style, 1523 "rowspan" : str(rowspan) 1524 })) 1525 append(self.writeEventSummaryBox(event)) 1526 append(fmt.table_cell(on=0)) 1527 else: 1528 append(self.writeEmptyDaySlot()) 1529 1530 return "".join(output) 1531 1532 def writeEmptyDaySlot(self): 1533 page = self.page 1534 fmt = page.request.formatter 1535 1536 output = [] 1537 append = output.append 1538 1539 append(fmt.table_cell(on=1, 1540 attrs={"class" : "event-timespan-content event-timespan-empty"})) 1541 append(fmt.table_cell(on=0)) 1542 1543 return "".join(output) 1544 1545 def writeDaySpacer(self, colspan=1, cls="timespan"): 1546 page = self.page 1547 fmt = page.request.formatter 1548 1549 output = [] 1550 append = output.append 1551 1552 append(fmt.table_cell(on=1, attrs={ 1553 "class" : "event-%s-spacer" % cls, 1554 "colspan" : str(colspan)})) 1555 append(fmt.table_cell(on=0)) 1556 return "".join(output) 1557 1558 # Map layout methods. 1559 1560 def writeMapTableHeading(self): 1561 page = self.page 1562 fmt = page.request.formatter 1563 1564 output = [] 1565 append = output.append 1566 1567 # Using a caption for accessibility reasons. 1568 1569 append(fmt.rawHTML('<caption class="event-map-heading">')) 1570 append(self.writeMapHeading()) 1571 append(fmt.rawHTML("</caption>")) 1572 1573 return "".join(output) 1574 1575 def showDictError(self, text, pagename): 1576 page = self.page 1577 request = page.request 1578 fmt = request.formatter 1579 1580 output = [] 1581 append = output.append 1582 1583 append(fmt.div(on=1, attrs={"class" : "event-aggregator-error"})) 1584 append(fmt.paragraph(on=1)) 1585 append(fmt.text(text)) 1586 append(fmt.paragraph(on=0)) 1587 append(fmt.paragraph(on=1)) 1588 append(linkToPage(request, Page(request, pagename), pagename)) 1589 append(fmt.paragraph(on=0)) 1590 1591 return "".join(output) 1592 1593 def writeMapMarker(self, marker_x, marker_y, map_x_scale, map_y_scale, location, events): 1594 1595 "Put a marker on the map." 1596 1597 page = self.page 1598 request = page.request 1599 fmt = request.formatter 1600 1601 output = [] 1602 append = output.append 1603 1604 append(fmt.listitem(on=1, css_class="event-map-label")) 1605 1606 # Have a positioned marker for the print mode. 1607 1608 append(fmt.div(on=1, css_class="event-map-label-only", 1609 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1610 marker_x, marker_y, map_x_scale, map_y_scale)) 1611 append(fmt.div(on=0)) 1612 1613 # Have a marker containing a pop-up when using the screen mode, 1614 # providing a normal block when using the print mode. 1615 1616 location_text = to_plain_text(location, request) 1617 label_target = "%s-maplabel-%s" % (self.getIdentifier(), location_text) 1618 1619 append(fmt.div(on=1, css_class="event-map-label", id=label_target, 1620 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1621 marker_x, marker_y, map_x_scale, map_y_scale)) 1622 1623 label_target_url = page.url(request, anchor=label_target, relative=True) 1624 append(fmt.url(1, label_target_url, "event-map-label-link")) 1625 append(fmt.span(1)) 1626 append(fmt.text(location_text)) 1627 append(fmt.span(0)) 1628 append(fmt.url(0)) 1629 1630 append(fmt.div(on=1, css_class="event-map-details")) 1631 append(fmt.div(on=1, css_class="event-map-shadow")) 1632 append(fmt.div(on=1, css_class="event-map-location")) 1633 1634 # The location may have been given as formatted text, but this will not 1635 # be usable in a heading, so it must be first converted to plain text. 1636 1637 append(fmt.heading(on=1, depth=2)) 1638 append(fmt.text(location_text)) 1639 append(fmt.heading(on=0, depth=2)) 1640 1641 append(self.writeMapEventSummaries(events)) 1642 1643 append(fmt.div(on=0)) 1644 append(fmt.div(on=0)) 1645 append(fmt.div(on=0)) 1646 append(fmt.div(on=0)) 1647 append(fmt.listitem(on=0)) 1648 1649 return "".join(output) 1650 1651 def writeMapEventSummaries(self, events): 1652 1653 "Write summaries of the given 'events' for the map." 1654 1655 page = self.page 1656 request = page.request 1657 fmt = request.formatter 1658 1659 # Sort the events by date. 1660 1661 events.sort(sort_start_first) 1662 1663 # Write out a self-contained list of events. 1664 1665 output = [] 1666 append = output.append 1667 1668 append(fmt.bullet_list(on=1, attr={"class" : "event-map-location-events"})) 1669 1670 for event in events: 1671 1672 # Get the event details. 1673 1674 event_summary = event.getSummary(self.parent_name) 1675 start, end = event.as_limits() 1676 event_period = self._getCalendarPeriod( 1677 start and self.getFullDateLabel(start), 1678 end and self.getFullDateLabel(end), 1679 "") 1680 1681 append(fmt.listitem(on=1)) 1682 1683 # Link to the page using the summary. 1684 1685 append(event.linkToEvent(request, event_summary)) 1686 1687 # Add the event period. 1688 1689 append(fmt.text(" ")) 1690 append(fmt.span(on=1, css_class="event-map-period")) 1691 append(fmt.text(event_period)) 1692 append(fmt.span(on=0)) 1693 1694 append(fmt.listitem(on=0)) 1695 1696 append(fmt.bullet_list(on=0)) 1697 1698 return "".join(output) 1699 1700 def render(self, all_shown_events): 1701 1702 """ 1703 Render the view, returning the rendered representation as a string. 1704 The view will show a list of 'all_shown_events'. 1705 """ 1706 1707 page = self.page 1708 request = page.request 1709 fmt = request.formatter 1710 _ = request.getText 1711 1712 # Make a calendar. 1713 1714 output = [] 1715 append = output.append 1716 1717 append(fmt.div(on=1, css_class="event-calendar-region", id=("EventAggregator-%s" % self.getIdentifier()))) 1718 1719 # Output download controls. 1720 1721 append(fmt.div(on=1, css_class="event-controls")) 1722 append(self.writeDownloadControls()) 1723 append(fmt.div(on=0)) 1724 1725 append(fmt.div(on=1, css_class="event-display")) 1726 1727 # Output a table. 1728 1729 if self.mode == "table": 1730 1731 # Start of table view output. 1732 1733 append(fmt.table(on=1, attrs={"tableclass" : "event-table", "summary" : _("A table of events")})) 1734 1735 append(fmt.table_row(on=1)) 1736 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1737 append(fmt.text(_("Event dates"))) 1738 append(fmt.table_cell(on=0)) 1739 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1740 append(fmt.text(_("Event location"))) 1741 append(fmt.table_cell(on=0)) 1742 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1743 append(fmt.text(_("Event details"))) 1744 append(fmt.table_cell(on=0)) 1745 append(fmt.table_row(on=0)) 1746 1747 # Show the events in order. 1748 1749 all_shown_events.sort(sort_start_first) 1750 1751 for event in all_shown_events: 1752 event_page = event.getPage() 1753 event_summary = event.getSummary(self.parent_name) 1754 event_details = event.getDetails() 1755 1756 # Prepare CSS classes with category-related styling. 1757 1758 css_classes = ["event-table-details"] 1759 1760 for topic in event_details.get("topics") or event_details.get("categories") or []: 1761 1762 # Filter the category text to avoid illegal characters. 1763 1764 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 1765 1766 attrs = {"class" : " ".join(css_classes)} 1767 1768 append(fmt.table_row(on=1)) 1769 1770 # Start and end dates. 1771 1772 append(fmt.table_cell(on=1, attrs=attrs)) 1773 append(fmt.span(on=1)) 1774 append(fmt.text(str(event_details["start"]))) 1775 append(fmt.span(on=0)) 1776 1777 if event_details["start"] != event_details["end"]: 1778 append(fmt.text(" - ")) 1779 append(fmt.span(on=1)) 1780 append(fmt.text(str(event_details["end"]))) 1781 append(fmt.span(on=0)) 1782 1783 append(fmt.table_cell(on=0)) 1784 1785 # Location. 1786 1787 append(fmt.table_cell(on=1, attrs=attrs)) 1788 1789 if event_details.has_key("location"): 1790 append(event_page.formatText(event_details["location"], fmt)) 1791 1792 append(fmt.table_cell(on=0)) 1793 1794 # Link to the page using the summary. 1795 1796 append(fmt.table_cell(on=1, attrs=attrs)) 1797 append(event.linkToEvent(request, event_summary)) 1798 append(fmt.table_cell(on=0)) 1799 1800 append(fmt.table_row(on=0)) 1801 1802 # End of table view output. 1803 1804 append(fmt.table(on=0)) 1805 1806 # Output a map view. 1807 1808 elif self.mode == "map": 1809 1810 # Special dictionary pages. 1811 1812 maps_page = getMapsPage(request) 1813 locations_page = getLocationsPage(request) 1814 1815 map_image = None 1816 1817 # Get the maps and locations. 1818 1819 maps = getWikiDict(maps_page, request) 1820 locations = getWikiDict(locations_page, request) 1821 1822 # Get the map image definition. 1823 1824 if maps is not None and self.map_name: 1825 try: 1826 map_details = maps[self.map_name].split() 1827 1828 map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \ 1829 map(getMapReference, map_details[:4]) 1830 map_width, map_height = map(int, map_details[4:6]) 1831 map_image = map_details[6] 1832 1833 map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees() 1834 map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees() 1835 1836 except (KeyError, ValueError): 1837 pass 1838 1839 # Report errors. 1840 1841 if maps is None: 1842 append(self.showDictError( 1843 _("You do not have read access to the maps page:"), 1844 maps_page)) 1845 1846 elif not self.map_name: 1847 append(self.showDictError( 1848 _("Please specify a valid map name corresponding to an entry on the following page:"), 1849 maps_page)) 1850 1851 elif map_image is None: 1852 append(self.showDictError( 1853 _("Please specify a valid entry for %s on the following page:") % self.map_name, 1854 maps_page)) 1855 1856 elif locations is None: 1857 append(self.showDictError( 1858 _("You do not have read access to the locations page:"), 1859 locations_page)) 1860 1861 # Attempt to show the map. 1862 1863 else: 1864 1865 # Get events by position. 1866 1867 events_by_location = {} 1868 event_locations = {} 1869 1870 for event in all_shown_events: 1871 event_details = event.getDetails() 1872 1873 location = event_details.get("location") 1874 geo = event_details.get("geo") 1875 1876 # Make a temporary location if an explicit position is given 1877 # but not a location name. 1878 1879 if not location and geo: 1880 location = "%s %s" % tuple(geo) 1881 1882 # Map the location to a position. 1883 1884 if location is not None and not event_locations.has_key(location): 1885 1886 # Get any explicit position of an event. 1887 1888 if geo: 1889 latitude, longitude = geo 1890 1891 # Or look up the position of a location using the locations 1892 # page. 1893 1894 else: 1895 latitude, longitude = Location(location, locations).getPosition() 1896 1897 # Use a normalised location if necessary. 1898 1899 if latitude is None and longitude is None: 1900 normalised_location = getNormalisedLocation(location) 1901 if normalised_location is not None: 1902 latitude, longitude = getLocationPosition(normalised_location, locations) 1903 if latitude is not None and longitude is not None: 1904 location = normalised_location 1905 1906 # Only remember positioned locations. 1907 1908 if latitude is not None and longitude is not None: 1909 event_locations[location] = latitude, longitude 1910 1911 # Record events according to location. 1912 1913 if not events_by_location.has_key(location): 1914 events_by_location[location] = [] 1915 1916 events_by_location[location].append(event) 1917 1918 # Get the map image URL. 1919 1920 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request) 1921 1922 # Start of map view output. 1923 1924 map_identifier = "map-%s" % self.getIdentifier() 1925 append(fmt.div(on=1, css_class="event-map", id=map_identifier)) 1926 1927 append(self.writeCalendarNavigation()) 1928 1929 append(fmt.table(on=1, attrs={"summary" : _("A map showing events")})) 1930 1931 append(self.writeMapTableHeading()) 1932 1933 append(fmt.table_row(on=1)) 1934 append(fmt.table_cell(on=1)) 1935 1936 append(fmt.div(on=1, css_class="event-map-container")) 1937 append(fmt.image(map_image_url)) 1938 append(fmt.number_list(on=1)) 1939 1940 # Events with no location are unpositioned. 1941 1942 if events_by_location.has_key(None): 1943 unpositioned_events = events_by_location[None] 1944 del events_by_location[None] 1945 else: 1946 unpositioned_events = [] 1947 1948 # Events whose location is unpositioned are themselves considered 1949 # unpositioned. 1950 1951 for location in set(events_by_location.keys()).difference(event_locations.keys()): 1952 unpositioned_events += events_by_location[location] 1953 1954 # Sort the locations before traversing them. 1955 1956 event_locations = event_locations.items() 1957 event_locations.sort() 1958 1959 # Show the events in the map. 1960 1961 for location, (latitude, longitude) in event_locations: 1962 events = events_by_location[location] 1963 1964 # Skip unpositioned locations and locations outside the map. 1965 1966 if latitude is None or longitude is None or \ 1967 latitude < map_bottom_left_latitude or \ 1968 longitude < map_bottom_left_longitude or \ 1969 latitude > map_top_right_latitude or \ 1970 longitude > map_top_right_longitude: 1971 1972 unpositioned_events += events 1973 continue 1974 1975 # Get the position and dimensions of the map marker. 1976 # NOTE: Use one degree as the marker size. 1977 1978 marker_x, marker_y = getPositionForCentrePoint( 1979 getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude, 1980 map_x_scale, map_y_scale), 1981 map_x_scale, map_y_scale) 1982 1983 # Add the map marker. 1984 1985 append(self.writeMapMarker(marker_x, marker_y, map_x_scale, map_y_scale, location, events)) 1986 1987 append(fmt.number_list(on=0)) 1988 append(fmt.div(on=0)) 1989 append(fmt.table_cell(on=0)) 1990 append(fmt.table_row(on=0)) 1991 1992 # Write unpositioned events. 1993 1994 if unpositioned_events: 1995 unpositioned_identifier = "unpositioned-%s" % self.getIdentifier() 1996 1997 append(fmt.table_row(on=1, css_class="event-map-unpositioned", 1998 id=unpositioned_identifier)) 1999 append(fmt.table_cell(on=1)) 2000 2001 append(fmt.heading(on=1, depth=2)) 2002 append(fmt.text(_("Events not shown on the map"))) 2003 append(fmt.heading(on=0, depth=2)) 2004 2005 # Show and hide controls. 2006 2007 append(fmt.div(on=1, css_class="event-map-show-control")) 2008 append(fmt.anchorlink(on=1, name=unpositioned_identifier)) 2009 append(fmt.text(_("Show unpositioned events"))) 2010 append(fmt.anchorlink(on=0)) 2011 append(fmt.div(on=0)) 2012 2013 append(fmt.div(on=1, css_class="event-map-hide-control")) 2014 append(fmt.anchorlink(on=1, name=map_identifier)) 2015 append(fmt.text(_("Hide unpositioned events"))) 2016 append(fmt.anchorlink(on=0)) 2017 append(fmt.div(on=0)) 2018 2019 append(self.writeMapEventSummaries(unpositioned_events)) 2020 2021 # End of map view output. 2022 2023 append(fmt.table_cell(on=0)) 2024 append(fmt.table_row(on=0)) 2025 append(fmt.table(on=0)) 2026 append(fmt.div(on=0)) 2027 2028 # Output a list. 2029 2030 elif self.mode == "list": 2031 2032 # Start of list view output. 2033 2034 append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 2035 2036 # Output a list. 2037 # NOTE: Make the heading depth configurable. 2038 2039 for period in self.first.until(self.last): 2040 2041 append(fmt.listitem(on=1, attr={"class" : "event-listings-period"})) 2042 append(fmt.heading(on=1, depth=2, attr={"class" : "event-listings-heading"})) 2043 2044 # Either write a date heading or produce links for navigable 2045 # calendars. 2046 2047 append(self.writeDateHeading(period)) 2048 2049 append(fmt.heading(on=0, depth=2)) 2050 2051 append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"})) 2052 2053 # Show the events in order. 2054 2055 events_in_period = getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period)) 2056 events_in_period.sort(sort_start_first) 2057 2058 for event in events_in_period: 2059 event_page = event.getPage() 2060 event_details = event.getDetails() 2061 event_summary = event.getSummary(self.parent_name) 2062 2063 append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 2064 2065 # Link to the page using the summary. 2066 2067 append(fmt.paragraph(on=1)) 2068 append(event.linkToEvent(request, event_summary)) 2069 append(fmt.paragraph(on=0)) 2070 2071 # Start and end dates. 2072 2073 append(fmt.paragraph(on=1)) 2074 append(fmt.span(on=1)) 2075 append(fmt.text(str(event_details["start"]))) 2076 append(fmt.span(on=0)) 2077 append(fmt.text(" - ")) 2078 append(fmt.span(on=1)) 2079 append(fmt.text(str(event_details["end"]))) 2080 append(fmt.span(on=0)) 2081 append(fmt.paragraph(on=0)) 2082 2083 # Location. 2084 2085 if event_details.has_key("location"): 2086 append(fmt.paragraph(on=1)) 2087 append(event_page.formatText(event_details["location"], fmt)) 2088 append(fmt.paragraph(on=1)) 2089 2090 # Topics. 2091 2092 if event_details.has_key("topics") or event_details.has_key("categories"): 2093 append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 2094 2095 for topic in event_details.get("topics") or event_details.get("categories") or []: 2096 append(fmt.listitem(on=1)) 2097 append(event_page.formatText(topic, fmt)) 2098 append(fmt.listitem(on=0)) 2099 2100 append(fmt.bullet_list(on=0)) 2101 2102 append(fmt.listitem(on=0)) 2103 2104 append(fmt.bullet_list(on=0)) 2105 2106 # End of list view output. 2107 2108 append(fmt.bullet_list(on=0)) 2109 2110 # Output a month calendar. This shows month-by-month data. 2111 2112 elif self.mode == "calendar": 2113 2114 # Visit all months in the requested range, or across known events. 2115 2116 for month in self.first.months_until(self.last): 2117 2118 append(fmt.div(on=1, css_class="event-calendar")) 2119 append(self.writeCalendarNavigation()) 2120 2121 # Output a month. 2122 2123 append(fmt.table(on=1, attrs={"tableclass" : "event-month", "summary" : _("A table showing a calendar month")})) 2124 2125 # Either write a month heading or produce links for navigable 2126 # calendars. 2127 2128 append(self.writeMonthTableHeading(month)) 2129 2130 # Weekday headings. 2131 2132 append(self.writeWeekdayHeadings()) 2133 2134 # Process the days of the month. 2135 2136 start_weekday, number_of_days = month.month_properties() 2137 2138 # The start weekday is the weekday of day number 1. 2139 # Find the first day of the week, counting from below zero, if 2140 # necessary, in order to land on the first day of the month as 2141 # day number 1. 2142 2143 first_day = 1 - start_weekday 2144 2145 while first_day <= number_of_days: 2146 2147 # Find events in this week and determine how to mark them on the 2148 # calendar. 2149 2150 week_start = month.as_date(max(first_day, 1)) 2151 week_end = month.as_date(min(first_day + 6, number_of_days)) 2152 2153 full_coverage, week_slots = getCoverage( 2154 getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end))) 2155 2156 # Make a new table region. 2157 # NOTE: Moin opens a "tbody" element in the table method. 2158 2159 append(fmt.rawHTML("</tbody>")) 2160 append(fmt.rawHTML("<tbody>")) 2161 2162 # Output a week, starting with the day numbers. 2163 2164 append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 2165 2166 # Either generate empty days... 2167 2168 if not week_slots: 2169 append(self.writeEmptyWeek(first_day, number_of_days, month)) 2170 2171 # Or generate each set of scheduled events... 2172 2173 else: 2174 append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 2175 2176 # Process the next week... 2177 2178 first_day += 7 2179 2180 # End of month. 2181 # NOTE: Moin closes a "tbody" element in the table method. 2182 2183 append(fmt.table(on=0)) 2184 append(fmt.div(on=0)) 2185 2186 # Output a day view. 2187 2188 elif self.mode == "day": 2189 2190 # Visit all days in the requested range, or across known events. 2191 2192 for date in self.first.days_until(self.last): 2193 2194 append(fmt.div(on=1, css_class="event-calendar")) 2195 append(self.writeCalendarNavigation()) 2196 2197 append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day", "summary" : _("A table showing a calendar day")})) 2198 2199 full_coverage, day_slots = getCoverage( 2200 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 2201 2202 # Work out how many columns the day title will need. 2203 # Include spacers after the scale and each event column. 2204 2205 colspan = sum(map(len, day_slots.values())) * 2 + 2 2206 2207 append(self.writeDayTableHeading(date, colspan)) 2208 2209 # Either generate empty days... 2210 2211 if not day_slots: 2212 append(self.writeEmptyDay(date)) 2213 2214 # Or generate each set of scheduled events... 2215 2216 else: 2217 append(self.writeDaySlots(date, full_coverage, day_slots)) 2218 2219 # End of day. 2220 2221 append(fmt.table(on=0)) 2222 append(fmt.div(on=0)) 2223 2224 append(fmt.div(on=0)) # end of event-display 2225 2226 # Output view controls. 2227 2228 append(fmt.div(on=1, css_class="event-controls")) 2229 append(self.writeViewControls()) 2230 append(fmt.div(on=0)) 2231 2232 # Close the calendar region. 2233 2234 append(fmt.div(on=0)) 2235 2236 # Add any scripts. 2237 2238 if isinstance(fmt, request.html_formatter.__class__): 2239 append(self.update_script) 2240 2241 return ''.join(output) 2242 2243 update_script = """\ 2244 <script type="text/javascript"> 2245 function replaceCalendar(name, url) { 2246 var calendar = document.getElementById(name); 2247 2248 if (calendar == null) { 2249 return true; 2250 } 2251 2252 var xmlhttp = new XMLHttpRequest(); 2253 xmlhttp.open("GET", url, false); 2254 xmlhttp.send(null); 2255 2256 var newCalendar = xmlhttp.responseText; 2257 2258 if (newCalendar != null) { 2259 calendar.innerHTML = newCalendar; 2260 return false; 2261 } 2262 2263 return true; 2264 } 2265 </script> 2266 """ 2267 2268 # vim: tabstop=4 expandtab shiftwidth=4