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