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 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, start_times, end_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, start_times, end_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, start_times, end_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(start_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(end_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-region", 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 append(fmt.div(on=1, css_class="event-display")) 1691 1692 # Output a table. 1693 1694 if self.mode == "table": 1695 1696 # Start of table view output. 1697 1698 append(fmt.table(on=1, attrs={"tableclass" : "event-table", "summary" : _("A table of events")})) 1699 1700 append(fmt.table_row(on=1)) 1701 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1702 append(fmt.text(_("Event dates"))) 1703 append(fmt.table_cell(on=0)) 1704 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1705 append(fmt.text(_("Event location"))) 1706 append(fmt.table_cell(on=0)) 1707 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1708 append(fmt.text(_("Event details"))) 1709 append(fmt.table_cell(on=0)) 1710 append(fmt.table_row(on=0)) 1711 1712 # Show the events in order. 1713 1714 all_shown_events.sort(sort_start_first) 1715 1716 for event in all_shown_events: 1717 event_page = event.getPage() 1718 event_summary = event.getSummary(self.parent_name) 1719 event_details = event.getDetails() 1720 1721 # Prepare CSS classes with category-related styling. 1722 1723 css_classes = ["event-table-details"] 1724 1725 for topic in event_details.get("topics") or event_details.get("categories") or []: 1726 1727 # Filter the category text to avoid illegal characters. 1728 1729 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 1730 1731 attrs = {"class" : " ".join(css_classes)} 1732 1733 append(fmt.table_row(on=1)) 1734 1735 # Start and end dates. 1736 1737 append(fmt.table_cell(on=1, attrs=attrs)) 1738 append(fmt.span(on=1)) 1739 append(fmt.text(str(event_details["start"]))) 1740 append(fmt.span(on=0)) 1741 1742 if event_details["start"] != event_details["end"]: 1743 append(fmt.text(" - ")) 1744 append(fmt.span(on=1)) 1745 append(fmt.text(str(event_details["end"]))) 1746 append(fmt.span(on=0)) 1747 1748 append(fmt.table_cell(on=0)) 1749 1750 # Location. 1751 1752 append(fmt.table_cell(on=1, attrs=attrs)) 1753 1754 if event_details.has_key("location"): 1755 append(event_page.formatText(event_details["location"], fmt)) 1756 1757 append(fmt.table_cell(on=0)) 1758 1759 # Link to the page using the summary. 1760 1761 append(fmt.table_cell(on=1, attrs=attrs)) 1762 append(event.linkToEvent(request, event_summary)) 1763 append(fmt.table_cell(on=0)) 1764 1765 append(fmt.table_row(on=0)) 1766 1767 # End of table view output. 1768 1769 append(fmt.table(on=0)) 1770 1771 # Output a map view. 1772 1773 elif self.mode == "map": 1774 1775 # Special dictionary pages. 1776 1777 maps_page = getMapsPage(request) 1778 locations_page = getLocationsPage(request) 1779 1780 map_image = None 1781 1782 # Get the maps and locations. 1783 1784 maps = getWikiDict(maps_page, request) 1785 locations = getWikiDict(locations_page, request) 1786 1787 # Get the map image definition. 1788 1789 if maps is not None and self.map_name: 1790 try: 1791 map_details = maps[self.map_name].split() 1792 1793 map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \ 1794 map(getMapReference, map_details[:4]) 1795 map_width, map_height = map(int, map_details[4:6]) 1796 map_image = map_details[6] 1797 1798 map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees() 1799 map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees() 1800 1801 except (KeyError, ValueError): 1802 pass 1803 1804 # Report errors. 1805 1806 if maps is None: 1807 append(self.showDictError( 1808 _("You do not have read access to the maps page:"), 1809 maps_page)) 1810 1811 elif not self.map_name: 1812 append(self.showDictError( 1813 _("Please specify a valid map name corresponding to an entry on the following page:"), 1814 maps_page)) 1815 1816 elif map_image is None: 1817 append(self.showDictError( 1818 _("Please specify a valid entry for %s on the following page:") % self.map_name, 1819 maps_page)) 1820 1821 elif locations is None: 1822 append(self.showDictError( 1823 _("You do not have read access to the locations page:"), 1824 locations_page)) 1825 1826 # Attempt to show the map. 1827 1828 else: 1829 1830 # Get events by position. 1831 1832 events_by_location = {} 1833 event_locations = {} 1834 1835 for event in all_shown_events: 1836 event_details = event.getDetails() 1837 1838 location = event_details.get("location") 1839 geo = event_details.get("geo") 1840 1841 # Make a temporary location if an explicit position is given 1842 # but not a location name. 1843 1844 if not location and geo: 1845 location = "%s %s" % tuple(geo) 1846 1847 # Map the location to a position. 1848 1849 if location is not None and not event_locations.has_key(location): 1850 1851 # Get any explicit position of an event. 1852 1853 if geo: 1854 latitude, longitude = geo 1855 1856 # Or look up the position of a location using the locations 1857 # page. 1858 1859 else: 1860 latitude, longitude = Location(location, locations).getPosition() 1861 1862 # Use a normalised location if necessary. 1863 1864 if latitude is None and longitude is None: 1865 normalised_location = getNormalisedLocation(location) 1866 if normalised_location is not None: 1867 latitude, longitude = getLocationPosition(normalised_location, locations) 1868 if latitude is not None and longitude is not None: 1869 location = normalised_location 1870 1871 # Only remember positioned locations. 1872 1873 if latitude is not None and longitude is not None: 1874 event_locations[location] = latitude, longitude 1875 1876 # Record events according to location. 1877 1878 if not events_by_location.has_key(location): 1879 events_by_location[location] = [] 1880 1881 events_by_location[location].append(event) 1882 1883 # Get the map image URL. 1884 1885 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request) 1886 1887 # Start of map view output. 1888 1889 map_identifier = "map-%s" % self.getIdentifier() 1890 append(fmt.div(on=1, css_class="event-map", id=map_identifier)) 1891 1892 append(self.writeCalendarNavigation()) 1893 1894 append(fmt.table(on=1, attrs={"summary" : _("A map showing events")})) 1895 1896 append(self.writeMapTableHeading()) 1897 1898 append(fmt.table_row(on=1)) 1899 append(fmt.table_cell(on=1)) 1900 1901 append(fmt.div(on=1, css_class="event-map-container")) 1902 append(fmt.image(map_image_url)) 1903 append(fmt.number_list(on=1)) 1904 1905 # Events with no location are unpositioned. 1906 1907 if events_by_location.has_key(None): 1908 unpositioned_events = events_by_location[None] 1909 del events_by_location[None] 1910 else: 1911 unpositioned_events = [] 1912 1913 # Events whose location is unpositioned are themselves considered 1914 # unpositioned. 1915 1916 for location in set(events_by_location.keys()).difference(event_locations.keys()): 1917 unpositioned_events += events_by_location[location] 1918 1919 # Sort the locations before traversing them. 1920 1921 event_locations = event_locations.items() 1922 event_locations.sort() 1923 1924 # Show the events in the map. 1925 1926 for location, (latitude, longitude) in event_locations: 1927 events = events_by_location[location] 1928 1929 # Skip unpositioned locations and locations outside the map. 1930 1931 if latitude is None or longitude is None or \ 1932 latitude < map_bottom_left_latitude or \ 1933 longitude < map_bottom_left_longitude or \ 1934 latitude > map_top_right_latitude or \ 1935 longitude > map_top_right_longitude: 1936 1937 unpositioned_events += events 1938 continue 1939 1940 # Get the position and dimensions of the map marker. 1941 # NOTE: Use one degree as the marker size. 1942 1943 marker_x, marker_y = getPositionForCentrePoint( 1944 getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude, 1945 map_x_scale, map_y_scale), 1946 map_x_scale, map_y_scale) 1947 1948 # Add the map marker. 1949 1950 append(self.writeMapMarker(marker_x, marker_y, map_x_scale, map_y_scale, location, events)) 1951 1952 append(fmt.number_list(on=0)) 1953 append(fmt.div(on=0)) 1954 append(fmt.table_cell(on=0)) 1955 append(fmt.table_row(on=0)) 1956 1957 # Write unpositioned events. 1958 1959 if unpositioned_events: 1960 unpositioned_identifier = "unpositioned-%s" % self.getIdentifier() 1961 1962 append(fmt.table_row(on=1, css_class="event-map-unpositioned", 1963 id=unpositioned_identifier)) 1964 append(fmt.table_cell(on=1)) 1965 1966 append(fmt.heading(on=1, depth=2)) 1967 append(fmt.text(_("Events not shown on the map"))) 1968 append(fmt.heading(on=0, depth=2)) 1969 1970 # Show and hide controls. 1971 1972 append(fmt.div(on=1, css_class="event-map-show-control")) 1973 append(fmt.anchorlink(on=1, name=unpositioned_identifier)) 1974 append(fmt.text(_("Show unpositioned events"))) 1975 append(fmt.anchorlink(on=0)) 1976 append(fmt.div(on=0)) 1977 1978 append(fmt.div(on=1, css_class="event-map-hide-control")) 1979 append(fmt.anchorlink(on=1, name=map_identifier)) 1980 append(fmt.text(_("Hide unpositioned events"))) 1981 append(fmt.anchorlink(on=0)) 1982 append(fmt.div(on=0)) 1983 1984 append(self.writeMapEventSummaries(unpositioned_events)) 1985 1986 # End of map view output. 1987 1988 append(fmt.table_cell(on=0)) 1989 append(fmt.table_row(on=0)) 1990 append(fmt.table(on=0)) 1991 append(fmt.div(on=0)) 1992 1993 # Output a list. 1994 1995 elif self.mode == "list": 1996 1997 # Start of list view output. 1998 1999 append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 2000 2001 # Output a list. 2002 # NOTE: Make the heading depth configurable. 2003 2004 for period in self.first.until(self.last): 2005 2006 append(fmt.listitem(on=1, attr={"class" : "event-listings-period"})) 2007 append(fmt.heading(on=1, depth=2, attr={"class" : "event-listings-heading"})) 2008 2009 # Either write a date heading or produce links for navigable 2010 # calendars. 2011 2012 append(self.writeDateHeading(period)) 2013 2014 append(fmt.heading(on=0, depth=2)) 2015 2016 append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"})) 2017 2018 # Show the events in order. 2019 2020 events_in_period = getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period)) 2021 events_in_period.sort(sort_start_first) 2022 2023 for event in events_in_period: 2024 event_page = event.getPage() 2025 event_details = event.getDetails() 2026 event_summary = event.getSummary(self.parent_name) 2027 2028 append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 2029 2030 # Link to the page using the summary. 2031 2032 append(fmt.paragraph(on=1)) 2033 append(event.linkToEvent(request, event_summary)) 2034 append(fmt.paragraph(on=0)) 2035 2036 # Start and end dates. 2037 2038 append(fmt.paragraph(on=1)) 2039 append(fmt.span(on=1)) 2040 append(fmt.text(str(event_details["start"]))) 2041 append(fmt.span(on=0)) 2042 append(fmt.text(" - ")) 2043 append(fmt.span(on=1)) 2044 append(fmt.text(str(event_details["end"]))) 2045 append(fmt.span(on=0)) 2046 append(fmt.paragraph(on=0)) 2047 2048 # Location. 2049 2050 if event_details.has_key("location"): 2051 append(fmt.paragraph(on=1)) 2052 append(event_page.formatText(event_details["location"], fmt)) 2053 append(fmt.paragraph(on=1)) 2054 2055 # Topics. 2056 2057 if event_details.has_key("topics") or event_details.has_key("categories"): 2058 append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 2059 2060 for topic in event_details.get("topics") or event_details.get("categories") or []: 2061 append(fmt.listitem(on=1)) 2062 append(event_page.formatText(topic, fmt)) 2063 append(fmt.listitem(on=0)) 2064 2065 append(fmt.bullet_list(on=0)) 2066 2067 append(fmt.listitem(on=0)) 2068 2069 append(fmt.bullet_list(on=0)) 2070 2071 # End of list view output. 2072 2073 append(fmt.bullet_list(on=0)) 2074 2075 # Output a month calendar. This shows month-by-month data. 2076 2077 elif self.mode == "calendar": 2078 2079 # Visit all months in the requested range, or across known events. 2080 2081 for month in self.first.months_until(self.last): 2082 2083 append(fmt.div(on=1, css_class="event-calendar")) 2084 append(self.writeCalendarNavigation()) 2085 2086 # Output a month. 2087 2088 append(fmt.table(on=1, attrs={"tableclass" : "event-month", "summary" : _("A table showing a calendar month")})) 2089 2090 # Either write a month heading or produce links for navigable 2091 # calendars. 2092 2093 append(self.writeMonthTableHeading(month)) 2094 2095 # Weekday headings. 2096 2097 append(self.writeWeekdayHeadings()) 2098 2099 # Process the days of the month. 2100 2101 start_weekday, number_of_days = month.month_properties() 2102 2103 # The start weekday is the weekday of day number 1. 2104 # Find the first day of the week, counting from below zero, if 2105 # necessary, in order to land on the first day of the month as 2106 # day number 1. 2107 2108 first_day = 1 - start_weekday 2109 2110 while first_day <= number_of_days: 2111 2112 # Find events in this week and determine how to mark them on the 2113 # calendar. 2114 2115 week_start = month.as_date(max(first_day, 1)) 2116 week_end = month.as_date(min(first_day + 6, number_of_days)) 2117 2118 full_coverage, week_slots = getCoverage( 2119 getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end))) 2120 2121 # Make a new table region. 2122 # NOTE: Moin opens a "tbody" element in the table method. 2123 2124 append(fmt.rawHTML("</tbody>")) 2125 append(fmt.rawHTML("<tbody>")) 2126 2127 # Output a week, starting with the day numbers. 2128 2129 append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 2130 2131 # Either generate empty days... 2132 2133 if not week_slots: 2134 append(self.writeEmptyWeek(first_day, number_of_days, month)) 2135 2136 # Or generate each set of scheduled events... 2137 2138 else: 2139 append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 2140 2141 # Process the next week... 2142 2143 first_day += 7 2144 2145 # End of month. 2146 # NOTE: Moin closes a "tbody" element in the table method. 2147 2148 append(fmt.table(on=0)) 2149 append(fmt.div(on=0)) 2150 2151 # Output a day view. 2152 2153 elif self.mode == "day": 2154 2155 # Visit all days in the requested range, or across known events. 2156 2157 for date in self.first.days_until(self.last): 2158 2159 append(fmt.div(on=1, css_class="event-calendar")) 2160 append(self.writeCalendarNavigation()) 2161 2162 append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day", "summary" : _("A table showing a calendar day")})) 2163 2164 full_coverage, day_slots = getCoverage( 2165 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 2166 2167 # Work out how many columns the day title will need. 2168 # Include spacers after the scale and each event column. 2169 2170 colspan = sum(map(len, day_slots.values())) * 2 + 2 2171 2172 append(self.writeDayTableHeading(date, colspan)) 2173 2174 # Either generate empty days... 2175 2176 if not day_slots: 2177 append(self.writeEmptyDay(date)) 2178 2179 # Or generate each set of scheduled events... 2180 2181 else: 2182 append(self.writeDaySlots(date, full_coverage, day_slots)) 2183 2184 # End of day. 2185 2186 append(fmt.table(on=0)) 2187 append(fmt.div(on=0)) 2188 2189 append(fmt.div(on=0)) # end of event-display 2190 2191 # Output view controls. 2192 2193 append(fmt.div(on=1, css_class="event-controls")) 2194 append(self.writeViewControls()) 2195 append(fmt.div(on=0)) 2196 2197 # Close the calendar region. 2198 2199 append(fmt.div(on=0)) 2200 2201 # Add any scripts. 2202 2203 if isinstance(fmt, request.html_formatter.__class__): 2204 append(self.update_script) 2205 2206 return ''.join(output) 2207 2208 update_script = """\ 2209 <script type="text/javascript"> 2210 function replaceCalendar(name, url) { 2211 var calendar = document.getElementById(name); 2212 2213 if (calendar == null) { 2214 return true; 2215 } 2216 2217 var xmlhttp = new XMLHttpRequest(); 2218 xmlhttp.open("GET", url, false); 2219 xmlhttp.send(null); 2220 2221 var newCalendar = xmlhttp.responseText; 2222 2223 if (newCalendar != null) { 2224 calendar.innerHTML = newCalendar; 2225 return false; 2226 } 2227 2228 return true; 2229 } 2230 </script> 2231 """ 2232 2233 # vim: tabstop=4 expandtab shiftwidth=4