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