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=True) 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 1381 for location in locations: 1382 1383 # Visit each coverage span, presenting the events in the span. 1384 1385 for events in day_slots[location]: 1386 event = self.getActiveEvent(period, events) 1387 if event is not None: 1388 if not rowspans.has_key(event): 1389 rowspans[event] = 1 1390 else: 1391 rowspans[event] += 1 1392 day_row.append((location, event)) 1393 1394 day_rows.append((period, day_row, start_times, end_times)) 1395 1396 # Output the locations. 1397 1398 append(fmt.table_row(on=1)) 1399 1400 # Add a spacer. 1401 1402 append(self.writeDaySpacer(colspan=2, cls="location")) 1403 1404 for location in locations: 1405 1406 # Add spacers to the column spans. 1407 1408 columns = len(day_slots[location]) * 2 - 1 1409 append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)})) 1410 append(fmt.text(location or "")) 1411 append(fmt.table_cell(on=0)) 1412 1413 # Add a trailing spacer. 1414 1415 append(self.writeDaySpacer(cls="location")) 1416 1417 append(fmt.table_row(on=0)) 1418 1419 # Output the periods with event details. 1420 1421 last_period = period = None 1422 events_written = set() 1423 1424 for period, day_row, start_times, end_times in day_rows: 1425 1426 # Write a heading describing the time. 1427 1428 append(fmt.table_row(on=1)) 1429 1430 # Show times only for distinct periods. 1431 1432 if not last_period or period.start != last_period.start: 1433 append(self.writeDayScaleHeading(start_times)) 1434 else: 1435 append(self.writeDayScaleHeading([])) 1436 1437 append(self.writeDaySpacer()) 1438 1439 # Visit each slot corresponding to a location (or no location). 1440 1441 for location, event in day_row: 1442 1443 # Output each location slot's contribution. 1444 1445 if event is None or event not in events_written: 1446 append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) 1447 if event is not None: 1448 events_written.add(event) 1449 1450 # Add a trailing spacer. 1451 1452 append(self.writeDaySpacer()) 1453 1454 append(fmt.table_row(on=0)) 1455 1456 last_period = period 1457 1458 # Write a final time heading if the last period ends in the current day. 1459 1460 if period is not None: 1461 if period.end == date: 1462 append(fmt.table_row(on=1)) 1463 append(self.writeDayScaleHeading(end_times)) 1464 1465 for slot in day_row: 1466 append(self.writeDaySpacer()) 1467 append(self.writeEmptyDaySlot()) 1468 1469 append(fmt.table_row(on=0)) 1470 1471 return "".join(output) 1472 1473 def writeDayScaleHeading(self, times): 1474 page = self.page 1475 fmt = page.request.formatter 1476 1477 output = [] 1478 append = output.append 1479 1480 append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"})) 1481 1482 first = 1 1483 for t in times: 1484 if isinstance(t, DateTime): 1485 if not first: 1486 append(fmt.linebreak(0)) 1487 append(fmt.text(t.time_string())) 1488 first = 0 1489 1490 append(fmt.table_cell(on=0)) 1491 1492 return "".join(output) 1493 1494 def getActiveEvent(self, period, events): 1495 for event in events: 1496 if period not in event: 1497 continue 1498 return event 1499 else: 1500 return None 1501 1502 def writeDaySlot(self, period, event, rowspan): 1503 page = self.page 1504 fmt = page.request.formatter 1505 1506 output = [] 1507 append = output.append 1508 1509 if event is not None: 1510 event_summary = event.getSummary(self.parent_name) 1511 style = self.getEventStyle(event_summary) 1512 1513 append(fmt.table_cell(on=1, attrs={ 1514 "class" : "event-timespan-content event-timespan-busy", 1515 "style" : style, 1516 "rowspan" : str(rowspan) 1517 })) 1518 append(self.writeEventSummaryBox(event)) 1519 append(fmt.table_cell(on=0)) 1520 else: 1521 append(self.writeEmptyDaySlot()) 1522 1523 return "".join(output) 1524 1525 def writeEmptyDaySlot(self): 1526 page = self.page 1527 fmt = page.request.formatter 1528 1529 output = [] 1530 append = output.append 1531 1532 append(fmt.table_cell(on=1, 1533 attrs={"class" : "event-timespan-content event-timespan-empty"})) 1534 append(fmt.table_cell(on=0)) 1535 1536 return "".join(output) 1537 1538 def writeDaySpacer(self, colspan=1, cls="timespan"): 1539 page = self.page 1540 fmt = page.request.formatter 1541 1542 output = [] 1543 append = output.append 1544 1545 append(fmt.table_cell(on=1, attrs={ 1546 "class" : "event-%s-spacer" % cls, 1547 "colspan" : str(colspan)})) 1548 append(fmt.table_cell(on=0)) 1549 return "".join(output) 1550 1551 # Map layout methods. 1552 1553 def writeMapTableHeading(self): 1554 page = self.page 1555 fmt = page.request.formatter 1556 1557 output = [] 1558 append = output.append 1559 1560 # Using a caption for accessibility reasons. 1561 1562 append(fmt.rawHTML('<caption class="event-map-heading">')) 1563 append(self.writeMapHeading()) 1564 append(fmt.rawHTML("</caption>")) 1565 1566 return "".join(output) 1567 1568 def showDictError(self, text, pagename): 1569 page = self.page 1570 request = page.request 1571 fmt = request.formatter 1572 1573 output = [] 1574 append = output.append 1575 1576 append(fmt.div(on=1, attrs={"class" : "event-aggregator-error"})) 1577 append(fmt.paragraph(on=1)) 1578 append(fmt.text(text)) 1579 append(fmt.paragraph(on=0)) 1580 append(fmt.paragraph(on=1)) 1581 append(linkToPage(request, Page(request, pagename), pagename)) 1582 append(fmt.paragraph(on=0)) 1583 1584 return "".join(output) 1585 1586 def writeMapMarker(self, marker_x, marker_y, map_x_scale, map_y_scale, location, events): 1587 1588 "Put a marker on the map." 1589 1590 page = self.page 1591 request = page.request 1592 fmt = request.formatter 1593 1594 output = [] 1595 append = output.append 1596 1597 append(fmt.listitem(on=1, css_class="event-map-label")) 1598 1599 # Have a positioned marker for the print mode. 1600 1601 append(fmt.div(on=1, css_class="event-map-label-only", 1602 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1603 marker_x, marker_y, map_x_scale, map_y_scale)) 1604 append(fmt.div(on=0)) 1605 1606 # Have a marker containing a pop-up when using the screen mode, 1607 # providing a normal block when using the print mode. 1608 1609 location_text = to_plain_text(location, request) 1610 label_target = "%s-maplabel-%s" % (self.getIdentifier(), location_text) 1611 1612 append(fmt.div(on=1, css_class="event-map-label", id=label_target, 1613 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1614 marker_x, marker_y, map_x_scale, map_y_scale)) 1615 1616 label_target_url = page.url(request, anchor=label_target, relative=True) 1617 append(fmt.url(1, label_target_url, "event-map-label-link")) 1618 append(fmt.span(1)) 1619 append(fmt.text(location_text)) 1620 append(fmt.span(0)) 1621 append(fmt.url(0)) 1622 1623 append(fmt.div(on=1, css_class="event-map-details")) 1624 append(fmt.div(on=1, css_class="event-map-shadow")) 1625 append(fmt.div(on=1, css_class="event-map-location")) 1626 1627 # The location may have been given as formatted text, but this will not 1628 # be usable in a heading, so it must be first converted to plain text. 1629 1630 append(fmt.heading(on=1, depth=2)) 1631 append(fmt.text(location_text)) 1632 append(fmt.heading(on=0, depth=2)) 1633 1634 append(self.writeMapEventSummaries(events)) 1635 1636 append(fmt.div(on=0)) 1637 append(fmt.div(on=0)) 1638 append(fmt.div(on=0)) 1639 append(fmt.div(on=0)) 1640 append(fmt.listitem(on=0)) 1641 1642 return "".join(output) 1643 1644 def writeMapEventSummaries(self, events): 1645 1646 "Write summaries of the given 'events' for the map." 1647 1648 page = self.page 1649 request = page.request 1650 fmt = request.formatter 1651 1652 # Sort the events by date. 1653 1654 events.sort(sort_start_first) 1655 1656 # Write out a self-contained list of events. 1657 1658 output = [] 1659 append = output.append 1660 1661 append(fmt.bullet_list(on=1, attr={"class" : "event-map-location-events"})) 1662 1663 for event in events: 1664 1665 # Get the event details. 1666 1667 event_summary = event.getSummary(self.parent_name) 1668 start, end = event.as_limits() 1669 event_period = self._getCalendarPeriod( 1670 start and self.getFullDateLabel(start), 1671 end and self.getFullDateLabel(end), 1672 "") 1673 1674 append(fmt.listitem(on=1)) 1675 1676 # Link to the page using the summary. 1677 1678 append(event.linkToEvent(request, event_summary)) 1679 1680 # Add the event period. 1681 1682 append(fmt.text(" ")) 1683 append(fmt.span(on=1, css_class="event-map-period")) 1684 append(fmt.text(event_period)) 1685 append(fmt.span(on=0)) 1686 1687 append(fmt.listitem(on=0)) 1688 1689 append(fmt.bullet_list(on=0)) 1690 1691 return "".join(output) 1692 1693 def render(self, all_shown_events): 1694 1695 """ 1696 Render the view, returning the rendered representation as a string. 1697 The view will show a list of 'all_shown_events'. 1698 """ 1699 1700 page = self.page 1701 request = page.request 1702 fmt = request.formatter 1703 _ = request.getText 1704 1705 # Make a calendar. 1706 1707 output = [] 1708 append = output.append 1709 1710 append(fmt.div(on=1, css_class="event-calendar-region", id=("EventAggregator-%s" % self.getIdentifier()))) 1711 1712 # Output download controls. 1713 1714 append(fmt.div(on=1, css_class="event-controls")) 1715 append(self.writeDownloadControls()) 1716 append(fmt.div(on=0)) 1717 1718 append(fmt.div(on=1, css_class="event-display")) 1719 1720 # Output a table. 1721 1722 if self.mode == "table": 1723 1724 # Start of table view output. 1725 1726 append(fmt.table(on=1, attrs={"tableclass" : "event-table", "summary" : _("A table of events")})) 1727 1728 append(fmt.table_row(on=1)) 1729 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1730 append(fmt.text(_("Event dates"))) 1731 append(fmt.table_cell(on=0)) 1732 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1733 append(fmt.text(_("Event location"))) 1734 append(fmt.table_cell(on=0)) 1735 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1736 append(fmt.text(_("Event details"))) 1737 append(fmt.table_cell(on=0)) 1738 append(fmt.table_row(on=0)) 1739 1740 # Show the events in order. 1741 1742 all_shown_events.sort(sort_start_first) 1743 1744 for event in all_shown_events: 1745 event_page = event.getPage() 1746 event_summary = event.getSummary(self.parent_name) 1747 event_details = event.getDetails() 1748 1749 # Prepare CSS classes with category-related styling. 1750 1751 css_classes = ["event-table-details"] 1752 1753 for topic in event_details.get("topics") or event_details.get("categories") or []: 1754 1755 # Filter the category text to avoid illegal characters. 1756 1757 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 1758 1759 attrs = {"class" : " ".join(css_classes)} 1760 1761 append(fmt.table_row(on=1)) 1762 1763 # Start and end dates. 1764 1765 append(fmt.table_cell(on=1, attrs=attrs)) 1766 append(fmt.span(on=1)) 1767 append(fmt.text(str(event_details["start"]))) 1768 append(fmt.span(on=0)) 1769 1770 if event_details["start"] != event_details["end"]: 1771 append(fmt.text(" - ")) 1772 append(fmt.span(on=1)) 1773 append(fmt.text(str(event_details["end"]))) 1774 append(fmt.span(on=0)) 1775 1776 append(fmt.table_cell(on=0)) 1777 1778 # Location. 1779 1780 append(fmt.table_cell(on=1, attrs=attrs)) 1781 1782 if event_details.has_key("location"): 1783 append(event_page.formatText(event_details["location"], fmt)) 1784 1785 append(fmt.table_cell(on=0)) 1786 1787 # Link to the page using the summary. 1788 1789 append(fmt.table_cell(on=1, attrs=attrs)) 1790 append(event.linkToEvent(request, event_summary)) 1791 append(fmt.table_cell(on=0)) 1792 1793 append(fmt.table_row(on=0)) 1794 1795 # End of table view output. 1796 1797 append(fmt.table(on=0)) 1798 1799 # Output a map view. 1800 1801 elif self.mode == "map": 1802 1803 # Special dictionary pages. 1804 1805 maps_page = getMapsPage(request) 1806 locations_page = getLocationsPage(request) 1807 1808 map_image = None 1809 1810 # Get the maps and locations. 1811 1812 maps = getWikiDict(maps_page, request) 1813 locations = getWikiDict(locations_page, request) 1814 1815 # Get the map image definition. 1816 1817 if maps is not None and self.map_name: 1818 try: 1819 map_details = maps[self.map_name].split() 1820 1821 map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \ 1822 map(getMapReference, map_details[:4]) 1823 map_width, map_height = map(int, map_details[4:6]) 1824 map_image = map_details[6] 1825 1826 map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees() 1827 map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees() 1828 1829 except (KeyError, ValueError): 1830 pass 1831 1832 # Report errors. 1833 1834 if maps is None: 1835 append(self.showDictError( 1836 _("You do not have read access to the maps page:"), 1837 maps_page)) 1838 1839 elif not self.map_name: 1840 append(self.showDictError( 1841 _("Please specify a valid map name corresponding to an entry on the following page:"), 1842 maps_page)) 1843 1844 elif map_image is None: 1845 append(self.showDictError( 1846 _("Please specify a valid entry for %s on the following page:") % self.map_name, 1847 maps_page)) 1848 1849 elif locations is None: 1850 append(self.showDictError( 1851 _("You do not have read access to the locations page:"), 1852 locations_page)) 1853 1854 # Attempt to show the map. 1855 1856 else: 1857 1858 # Get events by position. 1859 1860 events_by_location = {} 1861 event_locations = {} 1862 1863 for event in all_shown_events: 1864 event_details = event.getDetails() 1865 1866 location = event_details.get("location") 1867 geo = event_details.get("geo") 1868 1869 # Make a temporary location if an explicit position is given 1870 # but not a location name. 1871 1872 if not location and geo: 1873 location = "%s %s" % tuple(geo) 1874 1875 # Map the location to a position. 1876 1877 if location is not None and not event_locations.has_key(location): 1878 1879 # Get any explicit position of an event. 1880 1881 if geo: 1882 latitude, longitude = geo 1883 1884 # Or look up the position of a location using the locations 1885 # page. 1886 1887 else: 1888 latitude, longitude = Location(location, locations).getPosition() 1889 1890 # Use a normalised location if necessary. 1891 1892 if latitude is None and longitude is None: 1893 normalised_location = getNormalisedLocation(location) 1894 if normalised_location is not None: 1895 latitude, longitude = getLocationPosition(normalised_location, locations) 1896 if latitude is not None and longitude is not None: 1897 location = normalised_location 1898 1899 # Only remember positioned locations. 1900 1901 if latitude is not None and longitude is not None: 1902 event_locations[location] = latitude, longitude 1903 1904 # Record events according to location. 1905 1906 if not events_by_location.has_key(location): 1907 events_by_location[location] = [] 1908 1909 events_by_location[location].append(event) 1910 1911 # Get the map image URL. 1912 1913 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request) 1914 1915 # Start of map view output. 1916 1917 map_identifier = "map-%s" % self.getIdentifier() 1918 append(fmt.div(on=1, css_class="event-map", id=map_identifier)) 1919 1920 append(self.writeCalendarNavigation()) 1921 1922 append(fmt.table(on=1, attrs={"summary" : _("A map showing events")})) 1923 1924 append(self.writeMapTableHeading()) 1925 1926 append(fmt.table_row(on=1)) 1927 append(fmt.table_cell(on=1)) 1928 1929 append(fmt.div(on=1, css_class="event-map-container")) 1930 append(fmt.image(map_image_url)) 1931 append(fmt.number_list(on=1)) 1932 1933 # Events with no location are unpositioned. 1934 1935 if events_by_location.has_key(None): 1936 unpositioned_events = events_by_location[None] 1937 del events_by_location[None] 1938 else: 1939 unpositioned_events = [] 1940 1941 # Events whose location is unpositioned are themselves considered 1942 # unpositioned. 1943 1944 for location in set(events_by_location.keys()).difference(event_locations.keys()): 1945 unpositioned_events += events_by_location[location] 1946 1947 # Sort the locations before traversing them. 1948 1949 event_locations = event_locations.items() 1950 event_locations.sort() 1951 1952 # Show the events in the map. 1953 1954 for location, (latitude, longitude) in event_locations: 1955 events = events_by_location[location] 1956 1957 # Skip unpositioned locations and locations outside the map. 1958 1959 if latitude is None or longitude is None or \ 1960 latitude < map_bottom_left_latitude or \ 1961 longitude < map_bottom_left_longitude or \ 1962 latitude > map_top_right_latitude or \ 1963 longitude > map_top_right_longitude: 1964 1965 unpositioned_events += events 1966 continue 1967 1968 # Get the position and dimensions of the map marker. 1969 # NOTE: Use one degree as the marker size. 1970 1971 marker_x, marker_y = getPositionForCentrePoint( 1972 getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude, 1973 map_x_scale, map_y_scale), 1974 map_x_scale, map_y_scale) 1975 1976 # Add the map marker. 1977 1978 append(self.writeMapMarker(marker_x, marker_y, map_x_scale, map_y_scale, location, events)) 1979 1980 append(fmt.number_list(on=0)) 1981 append(fmt.div(on=0)) 1982 append(fmt.table_cell(on=0)) 1983 append(fmt.table_row(on=0)) 1984 1985 # Write unpositioned events. 1986 1987 if unpositioned_events: 1988 unpositioned_identifier = "unpositioned-%s" % self.getIdentifier() 1989 1990 append(fmt.table_row(on=1, css_class="event-map-unpositioned", 1991 id=unpositioned_identifier)) 1992 append(fmt.table_cell(on=1)) 1993 1994 append(fmt.heading(on=1, depth=2)) 1995 append(fmt.text(_("Events not shown on the map"))) 1996 append(fmt.heading(on=0, depth=2)) 1997 1998 # Show and hide controls. 1999 2000 append(fmt.div(on=1, css_class="event-map-show-control")) 2001 append(fmt.anchorlink(on=1, name=unpositioned_identifier)) 2002 append(fmt.text(_("Show unpositioned events"))) 2003 append(fmt.anchorlink(on=0)) 2004 append(fmt.div(on=0)) 2005 2006 append(fmt.div(on=1, css_class="event-map-hide-control")) 2007 append(fmt.anchorlink(on=1, name=map_identifier)) 2008 append(fmt.text(_("Hide unpositioned events"))) 2009 append(fmt.anchorlink(on=0)) 2010 append(fmt.div(on=0)) 2011 2012 append(self.writeMapEventSummaries(unpositioned_events)) 2013 2014 # End of map view output. 2015 2016 append(fmt.table_cell(on=0)) 2017 append(fmt.table_row(on=0)) 2018 append(fmt.table(on=0)) 2019 append(fmt.div(on=0)) 2020 2021 # Output a list. 2022 2023 elif self.mode == "list": 2024 2025 # Start of list view output. 2026 2027 append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 2028 2029 # Output a list. 2030 # NOTE: Make the heading depth configurable. 2031 2032 for period in self.first.until(self.last): 2033 2034 append(fmt.listitem(on=1, attr={"class" : "event-listings-period"})) 2035 append(fmt.heading(on=1, depth=2, attr={"class" : "event-listings-heading"})) 2036 2037 # Either write a date heading or produce links for navigable 2038 # calendars. 2039 2040 append(self.writeDateHeading(period)) 2041 2042 append(fmt.heading(on=0, depth=2)) 2043 2044 append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"})) 2045 2046 # Show the events in order. 2047 2048 events_in_period = getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period)) 2049 events_in_period.sort(sort_start_first) 2050 2051 for event in events_in_period: 2052 event_page = event.getPage() 2053 event_details = event.getDetails() 2054 event_summary = event.getSummary(self.parent_name) 2055 2056 append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 2057 2058 # Link to the page using the summary. 2059 2060 append(fmt.paragraph(on=1)) 2061 append(event.linkToEvent(request, event_summary)) 2062 append(fmt.paragraph(on=0)) 2063 2064 # Start and end dates. 2065 2066 append(fmt.paragraph(on=1)) 2067 append(fmt.span(on=1)) 2068 append(fmt.text(str(event_details["start"]))) 2069 append(fmt.span(on=0)) 2070 append(fmt.text(" - ")) 2071 append(fmt.span(on=1)) 2072 append(fmt.text(str(event_details["end"]))) 2073 append(fmt.span(on=0)) 2074 append(fmt.paragraph(on=0)) 2075 2076 # Location. 2077 2078 if event_details.has_key("location"): 2079 append(fmt.paragraph(on=1)) 2080 append(event_page.formatText(event_details["location"], fmt)) 2081 append(fmt.paragraph(on=1)) 2082 2083 # Topics. 2084 2085 if event_details.has_key("topics") or event_details.has_key("categories"): 2086 append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 2087 2088 for topic in event_details.get("topics") or event_details.get("categories") or []: 2089 append(fmt.listitem(on=1)) 2090 append(event_page.formatText(topic, fmt)) 2091 append(fmt.listitem(on=0)) 2092 2093 append(fmt.bullet_list(on=0)) 2094 2095 append(fmt.listitem(on=0)) 2096 2097 append(fmt.bullet_list(on=0)) 2098 2099 # End of list view output. 2100 2101 append(fmt.bullet_list(on=0)) 2102 2103 # Output a month calendar. This shows month-by-month data. 2104 2105 elif self.mode == "calendar": 2106 2107 # Visit all months in the requested range, or across known events. 2108 2109 for month in self.first.months_until(self.last): 2110 2111 append(fmt.div(on=1, css_class="event-calendar")) 2112 append(self.writeCalendarNavigation()) 2113 2114 # Output a month. 2115 2116 append(fmt.table(on=1, attrs={"tableclass" : "event-month", "summary" : _("A table showing a calendar month")})) 2117 2118 # Either write a month heading or produce links for navigable 2119 # calendars. 2120 2121 append(self.writeMonthTableHeading(month)) 2122 2123 # Weekday headings. 2124 2125 append(self.writeWeekdayHeadings()) 2126 2127 # Process the days of the month. 2128 2129 start_weekday, number_of_days = month.month_properties() 2130 2131 # The start weekday is the weekday of day number 1. 2132 # Find the first day of the week, counting from below zero, if 2133 # necessary, in order to land on the first day of the month as 2134 # day number 1. 2135 2136 first_day = 1 - start_weekday 2137 2138 while first_day <= number_of_days: 2139 2140 # Find events in this week and determine how to mark them on the 2141 # calendar. 2142 2143 week_start = month.as_date(max(first_day, 1)) 2144 week_end = month.as_date(min(first_day + 6, number_of_days)) 2145 2146 full_coverage, week_slots = getCoverage( 2147 getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end))) 2148 2149 # Make a new table region. 2150 # NOTE: Moin opens a "tbody" element in the table method. 2151 2152 append(fmt.rawHTML("</tbody>")) 2153 append(fmt.rawHTML("<tbody>")) 2154 2155 # Output a week, starting with the day numbers. 2156 2157 append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 2158 2159 # Either generate empty days... 2160 2161 if not week_slots: 2162 append(self.writeEmptyWeek(first_day, number_of_days, month)) 2163 2164 # Or generate each set of scheduled events... 2165 2166 else: 2167 append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 2168 2169 # Process the next week... 2170 2171 first_day += 7 2172 2173 # End of month. 2174 # NOTE: Moin closes a "tbody" element in the table method. 2175 2176 append(fmt.table(on=0)) 2177 append(fmt.div(on=0)) 2178 2179 # Output a day view. 2180 2181 elif self.mode == "day": 2182 2183 # Visit all days in the requested range, or across known events. 2184 2185 for date in self.first.days_until(self.last): 2186 2187 append(fmt.div(on=1, css_class="event-calendar")) 2188 append(self.writeCalendarNavigation()) 2189 2190 append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day", "summary" : _("A table showing a calendar day")})) 2191 2192 full_coverage, day_slots = getCoverage( 2193 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 2194 2195 # Work out how many columns the day title will need. 2196 # Include spacers after the scale and each event column. 2197 2198 colspan = sum(map(len, day_slots.values())) * 2 + 2 2199 2200 append(self.writeDayTableHeading(date, colspan)) 2201 2202 # Either generate empty days... 2203 2204 if not day_slots: 2205 append(self.writeEmptyDay(date)) 2206 2207 # Or generate each set of scheduled events... 2208 2209 else: 2210 append(self.writeDaySlots(date, full_coverage, day_slots)) 2211 2212 # End of day. 2213 2214 append(fmt.table(on=0)) 2215 append(fmt.div(on=0)) 2216 2217 append(fmt.div(on=0)) # end of event-display 2218 2219 # Output view controls. 2220 2221 append(fmt.div(on=1, css_class="event-controls")) 2222 append(self.writeViewControls()) 2223 append(fmt.div(on=0)) 2224 2225 # Close the calendar region. 2226 2227 append(fmt.div(on=0)) 2228 2229 # Add any scripts. 2230 2231 if isinstance(fmt, request.html_formatter.__class__): 2232 append(self.update_script) 2233 2234 return ''.join(output) 2235 2236 update_script = """\ 2237 <script type="text/javascript"> 2238 function replaceCalendar(name, url) { 2239 var calendar = document.getElementById(name); 2240 2241 if (calendar == null) { 2242 return true; 2243 } 2244 2245 var xmlhttp = new XMLHttpRequest(); 2246 xmlhttp.open("GET", url, false); 2247 xmlhttp.send(null); 2248 2249 var newCalendar = xmlhttp.responseText; 2250 2251 if (newCalendar != null) { 2252 calendar.innerHTML = newCalendar; 2253 return false; 2254 } 2255 2256 return true; 2257 } 2258 </script> 2259 """ 2260 2261 # vim: tabstop=4 expandtab shiftwidth=4