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..."), "#%s" % 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")) 507 append(fmt.span(on=1, css_class="event-download-link")) 508 append(linkToPage(request, page, _("Cancel"), "#%s" % 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..."), "#%s" % 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")) 551 append(fmt.span(on=1, css_class="event-download-link")) 552 append(linkToPage(request, page, _("Cancel"), "#%s" % 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 _writeCalendarHeading(self, label, start, end): 735 736 """ 737 Write a calendar heading providing links permitting navigation to other 738 periods, using the given 'label' along with the 'start' and 'end' dates 739 to provide a link to a particular period. 740 """ 741 742 page = self.page 743 request = page.request 744 fmt = request.formatter 745 _ = request.getText 746 747 output = [] 748 append = output.append 749 750 # Prepare navigation links. 751 752 if self.calendar_name: 753 calendar_name = self.calendar_name 754 755 # Links to the previous set of months and to a calendar shifted 756 # back one month. 757 758 previous_set_link = self.getNavigationLink( 759 self.previous_set_start, self.previous_set_end 760 ) 761 previous_link = self.getNavigationLink( 762 self.previous_start, self.previous_end 763 ) 764 previous_set_update_link = self.getUpdateLink( 765 self.previous_set_start, self.previous_set_end 766 ) 767 previous_update_link = self.getUpdateLink( 768 self.previous_start, self.previous_end 769 ) 770 771 # Links to the next set of months and to a calendar shifted 772 # forward one month. 773 774 next_set_link = self.getNavigationLink( 775 self.next_set_start, self.next_set_end 776 ) 777 next_link = self.getNavigationLink( 778 self.next_start, self.next_end 779 ) 780 next_set_update_link = self.getUpdateLink( 781 self.next_set_start, self.next_set_end 782 ) 783 next_update_link = self.getUpdateLink( 784 self.next_start, self.next_end 785 ) 786 787 # A link leading to this date being at the top of the calendar. 788 789 date_link = self.getNavigationLink(start, end) 790 date_update_link = self.getUpdateLink(start, end) 791 792 append(fmt.span(on=1, css_class="previous")) 793 append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link, title=_("Previous set"))) 794 append(fmt.text(" ")) 795 append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link, title=_("Previous"))) 796 append(fmt.span(on=0)) 797 798 append(fmt.span(on=1, css_class="next")) 799 append(linkToPage(request, page, ">", next_link, onclick=next_update_link, title=_("Next"))) 800 append(fmt.text(" ")) 801 append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link, title=_("Next set"))) 802 append(fmt.span(on=0)) 803 804 append(linkToPage(request, page, label, date_link, onclick=date_update_link, title=_("Show this period first"))) 805 806 else: 807 append(fmt.span(on=1)) 808 append(fmt.text(label)) 809 append(fmt.span(on=0)) 810 811 return "".join(output) 812 813 def writeDayNumberHeading(self, date, busy): 814 815 """ 816 Return a link for the given 'date' which will activate the new event 817 action for the given day. If 'busy' is given as a true value, the 818 heading will be marked as busy. 819 """ 820 821 page = self.page 822 request = page.request 823 fmt = request.formatter 824 _ = request.getText 825 826 output = [] 827 append = output.append 828 829 year, month, day = date.as_tuple() 830 new_event_link = self.getNewEventLink(date) 831 832 # Prepare a link to the day view for this day. 833 834 day_view_link = self.getNavigationLink(date, date, "day", "date", self.calendar_start, self.calendar_end) 835 day_view_update_link = self.getUpdateLink(date, date, "day", "date", self.calendar_start, self.calendar_end) 836 837 # Output the heading class. 838 839 today_attr = date == getCurrentDate() and "event-day-current" or "" 840 841 append( 842 fmt.table_cell(on=1, attrs={ 843 "class" : "event-day-heading event-day-%s %s" % (busy and "busy" or "empty", today_attr), 844 "colspan" : "3" 845 })) 846 847 # Output the number and pop-up menu. 848 849 append(fmt.div(on=1, css_class="event-day-box")) 850 851 append(fmt.span(on=1, css_class="event-day-number-popup")) 852 append(fmt.span(on=1, css_class="event-day-number-link")) 853 append(linkToPage(request, page, _("View day"), day_view_link, onclick=day_view_update_link)) 854 append(fmt.span(on=0)) 855 append(fmt.span(on=1, css_class="event-day-number-link")) 856 append(linkToPage(request, page, _("New event"), new_event_link)) 857 append(fmt.span(on=0)) 858 append(fmt.span(on=0)) 859 860 # Link the number to the day view. 861 862 append(fmt.span(on=1, css_class="event-day-number")) 863 append(linkToPage(request, page, unicode(day), day_view_link, onclick=day_view_update_link, title=_("View day"))) 864 append(fmt.span(on=0)) 865 866 append(fmt.div(on=0)) 867 868 # End of heading. 869 870 append(fmt.table_cell(on=0)) 871 872 return "".join(output) 873 874 # Common layout methods. 875 876 def getEventStyle(self, colour_seed): 877 878 "Generate colour style information using the given 'colour_seed'." 879 880 bg = getColour(colour_seed) 881 fg = getBlackOrWhite(bg) 882 return "background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg) 883 884 def writeEventSummaryBox(self, event): 885 886 "Return an event summary box linking to the given 'event'." 887 888 page = self.page 889 request = page.request 890 fmt = request.formatter 891 892 output = [] 893 append = output.append 894 895 event_details = event.getDetails() 896 event_summary = event.getSummary(self.parent_name) 897 898 is_ambiguous = event.as_timespan().ambiguous() 899 style = self.getEventStyle(event_summary) 900 901 # The event box contains the summary, alongside 902 # other elements. 903 904 append(fmt.div(on=1, css_class="event-summary-box")) 905 append(fmt.div(on=1, css_class="event-summary", style=style)) 906 907 if is_ambiguous: 908 append(fmt.icon("/!\\")) 909 910 append(event.linkToEvent(request, event_summary)) 911 append(fmt.div(on=0)) 912 913 # Add a pop-up element for long summaries. 914 915 append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 916 917 if is_ambiguous: 918 append(fmt.icon("/!\\")) 919 920 append(event.linkToEvent(request, event_summary)) 921 append(fmt.div(on=0)) 922 923 append(fmt.div(on=0)) 924 925 return "".join(output) 926 927 # Calendar layout methods. 928 929 def writeMonthTableHeading(self, year_month): 930 page = self.page 931 fmt = page.request.formatter 932 933 output = [] 934 append = output.append 935 936 # Using a caption for accessibility reasons. 937 938 append(fmt.rawHTML('<caption class="event-month-heading">')) 939 append(self.writeMonthHeading(year_month)) 940 append(fmt.rawHTML("</caption>")) 941 942 return "".join(output) 943 944 def writeWeekdayHeadings(self): 945 page = self.page 946 request = page.request 947 fmt = request.formatter 948 _ = request.getText 949 950 output = [] 951 append = output.append 952 953 append(fmt.table_row(on=1)) 954 955 for weekday in range(0, 7): 956 append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"})) 957 append(fmt.text(_(getDayLabel(weekday)))) 958 append(fmt.table_cell(on=0)) 959 960 append(fmt.table_row(on=0)) 961 return "".join(output) 962 963 def writeDayNumbers(self, first_day, number_of_days, month, coverage): 964 page = self.page 965 fmt = page.request.formatter 966 967 output = [] 968 append = output.append 969 970 append(fmt.table_row(on=1)) 971 972 for weekday in range(0, 7): 973 day = first_day + weekday 974 date = month.as_date(day) 975 976 # Output out-of-month days. 977 978 if day < 1 or day > number_of_days: 979 append(fmt.table_cell(on=1, 980 attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"})) 981 append(fmt.table_cell(on=0)) 982 983 # Output normal days. 984 985 else: 986 # Output the day heading, making a link to a new event 987 # action. 988 989 append(self.writeDayNumberHeading(date, date in coverage)) 990 991 # End of day numbers. 992 993 append(fmt.table_row(on=0)) 994 return "".join(output) 995 996 def writeEmptyWeek(self, first_day, number_of_days, month): 997 page = self.page 998 fmt = page.request.formatter 999 1000 output = [] 1001 append = output.append 1002 1003 append(fmt.table_row(on=1)) 1004 1005 for weekday in range(0, 7): 1006 day = first_day + weekday 1007 date = month.as_date(day) 1008 1009 today_attr = date == getCurrentDate() and "event-day-current" or "" 1010 1011 # Output out-of-month days. 1012 1013 if day < 1 or day > number_of_days: 1014 append(fmt.table_cell(on=1, 1015 attrs={"class" : "event-day-content event-day-excluded %s" % today_attr, "colspan" : "3"})) 1016 append(fmt.table_cell(on=0)) 1017 1018 # Output empty days. 1019 1020 else: 1021 append(fmt.table_cell(on=1, 1022 attrs={"class" : "event-day-content event-day-empty %s" % today_attr, "colspan" : "3"})) 1023 1024 append(fmt.table_row(on=0)) 1025 return "".join(output) 1026 1027 def writeWeekSlots(self, first_day, number_of_days, month, week_end, week_slots): 1028 output = [] 1029 append = output.append 1030 1031 locations = week_slots.keys() 1032 locations.sort(sort_none_first) 1033 1034 # Visit each slot corresponding to a location (or no location). 1035 1036 for location in locations: 1037 1038 # Visit each coverage span, presenting the events in the span. 1039 1040 for events in week_slots[location]: 1041 1042 # Output each set. 1043 1044 append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events)) 1045 1046 # Add a spacer. 1047 1048 append(self.writeWeekSpacer(first_day, number_of_days, month)) 1049 1050 return "".join(output) 1051 1052 def writeWeekSlot(self, first_day, number_of_days, month, week_end, events): 1053 page = self.page 1054 request = page.request 1055 fmt = request.formatter 1056 1057 output = [] 1058 append = output.append 1059 1060 append(fmt.table_row(on=1)) 1061 1062 # Then, output day details. 1063 1064 for weekday in range(0, 7): 1065 day = first_day + weekday 1066 date = month.as_date(day) 1067 1068 # Skip out-of-month days. 1069 1070 if day < 1 or day > number_of_days: 1071 append(fmt.table_cell(on=1, 1072 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 1073 append(fmt.table_cell(on=0)) 1074 continue 1075 1076 # Output the day. 1077 # Where a day does not contain an event, a single cell is used. 1078 # Otherwise, multiple cells are used to provide space before, during 1079 # and after events. 1080 1081 today_attr = date == getCurrentDate() and "event-day-current" or "" 1082 1083 if date not in events: 1084 append(fmt.table_cell(on=1, 1085 attrs={"class" : "event-day-content event-day-empty %s" % today_attr, "colspan" : "3"})) 1086 1087 # Get event details for the current day. 1088 1089 for event in events: 1090 event_details = event.getDetails() 1091 1092 if date not in event: 1093 continue 1094 1095 # Get basic properties of the event. 1096 1097 starts_today = event_details["start"] == date 1098 ends_today = event_details["end"] == date 1099 event_summary = event.getSummary(self.parent_name) 1100 1101 style = self.getEventStyle(event_summary) 1102 1103 # Determine if the event name should be shown. 1104 1105 start_of_period = starts_today or weekday == 0 or day == 1 1106 1107 if self.name_usage == "daily" or start_of_period: 1108 hide_text = 0 1109 else: 1110 hide_text = 1 1111 1112 # Output start of day gap and determine whether 1113 # any event content should be explicitly output 1114 # for this day. 1115 1116 if starts_today: 1117 1118 # Single day events... 1119 1120 if ends_today: 1121 colspan = 3 1122 event_day_type = "event-day-single" 1123 1124 # Events starting today... 1125 1126 else: 1127 append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap %s" % today_attr})) 1128 append(fmt.table_cell(on=0)) 1129 1130 # Calculate the span of this cell. 1131 # Events whose names appear on every day... 1132 1133 if self.name_usage == "daily": 1134 colspan = 2 1135 event_day_type = "event-day-starting" 1136 1137 # Events whose names appear once per week... 1138 1139 else: 1140 if event_details["end"] <= week_end: 1141 event_length = event_details["end"].day() - day + 1 1142 colspan = (event_length - 2) * 3 + 4 1143 else: 1144 event_length = week_end.day() - day + 1 1145 colspan = (event_length - 1) * 3 + 2 1146 1147 event_day_type = "event-day-multiple" 1148 1149 # Events continuing from a previous week... 1150 1151 elif start_of_period: 1152 1153 # End of continuing event... 1154 1155 if ends_today: 1156 colspan = 2 1157 event_day_type = "event-day-ending" 1158 1159 # Events continuing for at least one more day... 1160 1161 else: 1162 1163 # Calculate the span of this cell. 1164 # Events whose names appear on every day... 1165 1166 if self.name_usage == "daily": 1167 colspan = 3 1168 event_day_type = "event-day-full" 1169 1170 # Events whose names appear once per week... 1171 1172 else: 1173 if event_details["end"] <= week_end: 1174 event_length = event_details["end"].day() - day + 1 1175 colspan = (event_length - 1) * 3 + 2 1176 else: 1177 event_length = week_end.day() - day + 1 1178 colspan = event_length * 3 1179 1180 event_day_type = "event-day-multiple" 1181 1182 # Continuing events whose names appear on every day... 1183 1184 elif self.name_usage == "daily": 1185 if ends_today: 1186 colspan = 2 1187 event_day_type = "event-day-ending" 1188 else: 1189 colspan = 3 1190 event_day_type = "event-day-full" 1191 1192 # Continuing events whose names appear once per week... 1193 1194 else: 1195 colspan = None 1196 1197 # Output the main content only if it is not 1198 # continuing from a previous day. 1199 1200 if colspan is not None: 1201 1202 # Colour the cell for continuing events. 1203 1204 attrs={ 1205 "class" : "event-day-content event-day-busy %s %s" % (event_day_type, today_attr), 1206 "colspan" : str(colspan) 1207 } 1208 1209 if not (starts_today and ends_today): 1210 attrs["style"] = style 1211 1212 append(fmt.table_cell(on=1, attrs=attrs)) 1213 1214 # Output the event. 1215 1216 if starts_today and ends_today or not hide_text: 1217 append(self.writeEventSummaryBox(event)) 1218 1219 append(fmt.table_cell(on=0)) 1220 1221 # Output end of day gap. 1222 1223 if ends_today and not starts_today: 1224 append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap %s" % today_attr})) 1225 append(fmt.table_cell(on=0)) 1226 1227 # End of set. 1228 1229 append(fmt.table_row(on=0)) 1230 return "".join(output) 1231 1232 def writeWeekSpacer(self, first_day, number_of_days, month): 1233 page = self.page 1234 fmt = page.request.formatter 1235 1236 output = [] 1237 append = output.append 1238 1239 append(fmt.table_row(on=1)) 1240 1241 for weekday in range(0, 7): 1242 day = first_day + weekday 1243 date = month.as_date(day) 1244 today_attr = date == getCurrentDate() and "event-day-current" or "" 1245 1246 css_classes = "event-day-spacer %s" % today_attr 1247 1248 # Skip out-of-month days. 1249 1250 if day < 1 or day > number_of_days: 1251 css_classes += " event-day-excluded" 1252 1253 append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"})) 1254 append(fmt.table_cell(on=0)) 1255 1256 append(fmt.table_row(on=0)) 1257 return "".join(output) 1258 1259 # Day layout methods. 1260 1261 def writeDayTableHeading(self, date, colspan=1): 1262 page = self.page 1263 fmt = page.request.formatter 1264 1265 output = [] 1266 append = output.append 1267 1268 # Using a caption for accessibility reasons. 1269 1270 append(fmt.rawHTML('<caption class="event-full-day-heading">')) 1271 append(self.writeDayHeading(date)) 1272 append(fmt.rawHTML("</caption>")) 1273 1274 return "".join(output) 1275 1276 def writeEmptyDay(self, date): 1277 page = self.page 1278 fmt = page.request.formatter 1279 1280 output = [] 1281 append = output.append 1282 1283 append(fmt.table_row(on=1)) 1284 1285 append(fmt.table_cell(on=1, 1286 attrs={"class" : "event-day-content event-day-empty"})) 1287 1288 append(fmt.table_row(on=0)) 1289 return "".join(output) 1290 1291 def writeDaySlots(self, date, full_coverage, day_slots): 1292 1293 """ 1294 Given a 'date', non-empty 'full_coverage' for the day concerned, and a 1295 non-empty mapping of 'day_slots' (from locations to event collections), 1296 output the day slots for the day. 1297 """ 1298 1299 page = self.page 1300 fmt = page.request.formatter 1301 1302 output = [] 1303 append = output.append 1304 1305 locations = day_slots.keys() 1306 locations.sort(sort_none_first) 1307 1308 # Traverse the time scale of the full coverage, visiting each slot to 1309 # determine whether it provides content for each period. 1310 1311 scale = getCoverageScale(full_coverage) 1312 1313 # Define a mapping of events to rowspans. 1314 1315 rowspans = {} 1316 1317 # Populate each period with event details, recording how many periods 1318 # each event populates. 1319 1320 day_rows = [] 1321 1322 for period, limit, times in scale: 1323 1324 # Ignore timespans before this day. 1325 1326 if period != date: 1327 continue 1328 1329 # Visit each slot corresponding to a location (or no location). 1330 1331 day_row = [] 1332 1333 for location in locations: 1334 1335 # Visit each coverage span, presenting the events in the span. 1336 1337 for events in day_slots[location]: 1338 event = self.getActiveEvent(period, events) 1339 if event is not None: 1340 if not rowspans.has_key(event): 1341 rowspans[event] = 1 1342 else: 1343 rowspans[event] += 1 1344 day_row.append((location, event)) 1345 1346 day_rows.append((period, day_row, times)) 1347 1348 # Output the locations. 1349 1350 append(fmt.table_row(on=1)) 1351 1352 # Add a spacer. 1353 1354 append(self.writeDaySpacer(colspan=2, cls="location")) 1355 1356 for location in locations: 1357 1358 # Add spacers to the column spans. 1359 1360 columns = len(day_slots[location]) * 2 - 1 1361 append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)})) 1362 append(fmt.text(location or "")) 1363 append(fmt.table_cell(on=0)) 1364 1365 # Add a trailing spacer. 1366 1367 append(self.writeDaySpacer(cls="location")) 1368 1369 append(fmt.table_row(on=0)) 1370 1371 # Output the periods with event details. 1372 1373 last_period = period = None 1374 events_written = set() 1375 1376 for period, day_row, times in day_rows: 1377 1378 # Write a heading describing the time. 1379 1380 append(fmt.table_row(on=1)) 1381 1382 # Show times only for distinct periods. 1383 1384 if not last_period or period.start != last_period.start: 1385 append(self.writeDayScaleHeading(times)) 1386 else: 1387 append(self.writeDayScaleHeading([])) 1388 1389 append(self.writeDaySpacer()) 1390 1391 # Visit each slot corresponding to a location (or no location). 1392 1393 for location, event in day_row: 1394 1395 # Output each location slot's contribution. 1396 1397 if event is None or event not in events_written: 1398 append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) 1399 if event is not None: 1400 events_written.add(event) 1401 1402 # Add a trailing spacer. 1403 1404 append(self.writeDaySpacer()) 1405 1406 append(fmt.table_row(on=0)) 1407 1408 last_period = period 1409 1410 # Write a final time heading if the last period ends in the current day. 1411 1412 if period is not None: 1413 if period.end == date: 1414 append(fmt.table_row(on=1)) 1415 append(self.writeDayScaleHeading(times)) 1416 1417 for slot in day_row: 1418 append(self.writeDaySpacer()) 1419 append(self.writeEmptyDaySlot()) 1420 1421 append(fmt.table_row(on=0)) 1422 1423 return "".join(output) 1424 1425 def writeDayScaleHeading(self, times): 1426 page = self.page 1427 fmt = page.request.formatter 1428 1429 output = [] 1430 append = output.append 1431 1432 append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"})) 1433 1434 first = 1 1435 for t in times: 1436 if isinstance(t, DateTime): 1437 if not first: 1438 append(fmt.linebreak(0)) 1439 append(fmt.text(t.time_string())) 1440 first = 0 1441 1442 append(fmt.table_cell(on=0)) 1443 1444 return "".join(output) 1445 1446 def getActiveEvent(self, period, events): 1447 for event in events: 1448 if period not in event: 1449 continue 1450 return event 1451 else: 1452 return None 1453 1454 def writeDaySlot(self, period, event, rowspan): 1455 page = self.page 1456 fmt = page.request.formatter 1457 1458 output = [] 1459 append = output.append 1460 1461 if event is not None: 1462 event_summary = event.getSummary(self.parent_name) 1463 style = self.getEventStyle(event_summary) 1464 1465 append(fmt.table_cell(on=1, attrs={ 1466 "class" : "event-timespan-content event-timespan-busy", 1467 "style" : style, 1468 "rowspan" : str(rowspan) 1469 })) 1470 append(self.writeEventSummaryBox(event)) 1471 append(fmt.table_cell(on=0)) 1472 else: 1473 append(self.writeEmptyDaySlot()) 1474 1475 return "".join(output) 1476 1477 def writeEmptyDaySlot(self): 1478 page = self.page 1479 fmt = page.request.formatter 1480 1481 output = [] 1482 append = output.append 1483 1484 append(fmt.table_cell(on=1, 1485 attrs={"class" : "event-timespan-content event-timespan-empty"})) 1486 append(fmt.table_cell(on=0)) 1487 1488 return "".join(output) 1489 1490 def writeDaySpacer(self, colspan=1, cls="timespan"): 1491 page = self.page 1492 fmt = page.request.formatter 1493 1494 output = [] 1495 append = output.append 1496 1497 append(fmt.table_cell(on=1, attrs={ 1498 "class" : "event-%s-spacer" % cls, 1499 "colspan" : str(colspan)})) 1500 append(fmt.table_cell(on=0)) 1501 return "".join(output) 1502 1503 # Map layout methods. 1504 1505 def writeMapTableHeading(self): 1506 page = self.page 1507 fmt = page.request.formatter 1508 1509 output = [] 1510 append = output.append 1511 1512 # Using a caption for accessibility reasons. 1513 1514 append(fmt.rawHTML('<caption class="event-map-heading">')) 1515 append(self.writeMapHeading()) 1516 append(fmt.rawHTML("</caption>")) 1517 1518 return "".join(output) 1519 1520 def showDictError(self, text, pagename): 1521 page = self.page 1522 request = page.request 1523 fmt = request.formatter 1524 1525 output = [] 1526 append = output.append 1527 1528 append(fmt.div(on=1, attrs={"class" : "event-aggregator-error"})) 1529 append(fmt.paragraph(on=1)) 1530 append(fmt.text(text)) 1531 append(fmt.paragraph(on=0)) 1532 append(fmt.paragraph(on=1)) 1533 append(linkToPage(request, Page(request, pagename), pagename)) 1534 append(fmt.paragraph(on=0)) 1535 1536 return "".join(output) 1537 1538 def writeMapMarker(self, marker_x, marker_y, map_x_scale, map_y_scale, location, events): 1539 1540 "Put a marker on the map." 1541 1542 page = self.page 1543 request = page.request 1544 fmt = request.formatter 1545 1546 output = [] 1547 append = output.append 1548 1549 append(fmt.listitem(on=1, css_class="event-map-label")) 1550 1551 # Have a positioned marker for the print mode. 1552 1553 append(fmt.div(on=1, css_class="event-map-label-only", 1554 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1555 marker_x, marker_y, map_x_scale, map_y_scale)) 1556 append(fmt.div(on=0)) 1557 1558 # Have a marker containing a pop-up when using the screen mode, 1559 # providing a normal block when using the print mode. 1560 1561 append(fmt.div(on=1, css_class="event-map-label", 1562 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1563 marker_x, marker_y, map_x_scale, map_y_scale)) 1564 append(fmt.div(on=1, css_class="event-map-details")) 1565 append(fmt.div(on=1, css_class="event-map-shadow")) 1566 append(fmt.div(on=1, css_class="event-map-location")) 1567 1568 # The location may have been given as formatted text, but this will not 1569 # be usable in a heading, so it must be first converted to plain text. 1570 1571 append(fmt.heading(on=1, depth=2)) 1572 append(fmt.text(to_plain_text(location, request))) 1573 append(fmt.heading(on=0, depth=2)) 1574 1575 append(self.writeMapEventSummaries(events)) 1576 1577 append(fmt.div(on=0)) 1578 append(fmt.div(on=0)) 1579 append(fmt.div(on=0)) 1580 append(fmt.div(on=0)) 1581 append(fmt.listitem(on=0)) 1582 1583 return "".join(output) 1584 1585 def writeMapEventSummaries(self, events): 1586 1587 "Write summaries of the given 'events' for the map." 1588 1589 page = self.page 1590 request = page.request 1591 fmt = request.formatter 1592 1593 # Sort the events by date. 1594 1595 events.sort(sort_start_first) 1596 1597 # Write out a self-contained list of events. 1598 1599 output = [] 1600 append = output.append 1601 1602 append(fmt.bullet_list(on=1, attr={"class" : "event-map-location-events"})) 1603 1604 for event in events: 1605 1606 # Get the event details. 1607 1608 event_summary = event.getSummary(self.parent_name) 1609 start, end = event.as_limits() 1610 event_period = self._getCalendarPeriod( 1611 start and self.getFullDateLabel(start), 1612 end and self.getFullDateLabel(end), 1613 "") 1614 1615 append(fmt.listitem(on=1)) 1616 1617 # Link to the page using the summary. 1618 1619 append(event.linkToEvent(request, event_summary)) 1620 1621 # Add the event period. 1622 1623 append(fmt.text(" ")) 1624 append(fmt.span(on=1, css_class="event-map-period")) 1625 append(fmt.text(event_period)) 1626 append(fmt.span(on=0)) 1627 1628 append(fmt.listitem(on=0)) 1629 1630 append(fmt.bullet_list(on=0)) 1631 1632 return "".join(output) 1633 1634 def render(self, all_shown_events): 1635 1636 """ 1637 Render the view, returning the rendered representation as a string. 1638 The view will show a list of 'all_shown_events'. 1639 """ 1640 1641 page = self.page 1642 request = page.request 1643 fmt = request.formatter 1644 _ = request.getText 1645 1646 # Make a calendar. 1647 1648 output = [] 1649 append = output.append 1650 1651 append(fmt.div(on=1, css_class="event-calendar", id=("EventAggregator-%s" % self.getIdentifier()))) 1652 1653 # Output download controls. 1654 1655 append(fmt.div(on=1, css_class="event-controls")) 1656 append(self.writeDownloadControls()) 1657 append(fmt.div(on=0)) 1658 1659 # Output a table. 1660 1661 if self.mode == "table": 1662 1663 # Start of table view output. 1664 1665 append(fmt.table(on=1, attrs={"tableclass" : "event-table", "summary" : _("A table of events")})) 1666 1667 append(fmt.table_row(on=1)) 1668 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1669 append(fmt.text(_("Event dates"))) 1670 append(fmt.table_cell(on=0)) 1671 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1672 append(fmt.text(_("Event location"))) 1673 append(fmt.table_cell(on=0)) 1674 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1675 append(fmt.text(_("Event details"))) 1676 append(fmt.table_cell(on=0)) 1677 append(fmt.table_row(on=0)) 1678 1679 # Show the events in order. 1680 1681 all_shown_events.sort(sort_start_first) 1682 1683 for event in all_shown_events: 1684 event_page = event.getPage() 1685 event_summary = event.getSummary(self.parent_name) 1686 event_details = event.getDetails() 1687 1688 # Prepare CSS classes with category-related styling. 1689 1690 css_classes = ["event-table-details"] 1691 1692 for topic in event_details.get("topics") or event_details.get("categories") or []: 1693 1694 # Filter the category text to avoid illegal characters. 1695 1696 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 1697 1698 attrs = {"class" : " ".join(css_classes)} 1699 1700 append(fmt.table_row(on=1)) 1701 1702 # Start and end dates. 1703 1704 append(fmt.table_cell(on=1, attrs=attrs)) 1705 append(fmt.span(on=1)) 1706 append(fmt.text(str(event_details["start"]))) 1707 append(fmt.span(on=0)) 1708 1709 if event_details["start"] != event_details["end"]: 1710 append(fmt.text(" - ")) 1711 append(fmt.span(on=1)) 1712 append(fmt.text(str(event_details["end"]))) 1713 append(fmt.span(on=0)) 1714 1715 append(fmt.table_cell(on=0)) 1716 1717 # Location. 1718 1719 append(fmt.table_cell(on=1, attrs=attrs)) 1720 1721 if event_details.has_key("location"): 1722 append(event_page.formatText(event_details["location"], fmt)) 1723 1724 append(fmt.table_cell(on=0)) 1725 1726 # Link to the page using the summary. 1727 1728 append(fmt.table_cell(on=1, attrs=attrs)) 1729 append(event.linkToEvent(request, event_summary)) 1730 append(fmt.table_cell(on=0)) 1731 1732 append(fmt.table_row(on=0)) 1733 1734 # End of table view output. 1735 1736 append(fmt.table(on=0)) 1737 1738 # Output a map view. 1739 1740 elif self.mode == "map": 1741 1742 # Special dictionary pages. 1743 1744 maps_page = getMapsPage(request) 1745 locations_page = getLocationsPage(request) 1746 1747 map_image = None 1748 1749 # Get the maps and locations. 1750 1751 maps = getWikiDict(maps_page, request) 1752 locations = getWikiDict(locations_page, request) 1753 1754 # Get the map image definition. 1755 1756 if maps is not None and self.map_name: 1757 try: 1758 map_details = maps[self.map_name].split() 1759 1760 map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \ 1761 map(getMapReference, map_details[:4]) 1762 map_width, map_height = map(int, map_details[4:6]) 1763 map_image = map_details[6] 1764 1765 map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees() 1766 map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees() 1767 1768 except (KeyError, ValueError): 1769 pass 1770 1771 # Report errors. 1772 1773 if maps is None: 1774 append(self.showDictError( 1775 _("You do not have read access to the maps page:"), 1776 maps_page)) 1777 1778 elif not self.map_name: 1779 append(self.showDictError( 1780 _("Please specify a valid map name corresponding to an entry on the following page:"), 1781 maps_page)) 1782 1783 elif map_image is None: 1784 append(self.showDictError( 1785 _("Please specify a valid entry for %s on the following page:") % self.map_name, 1786 maps_page)) 1787 1788 elif locations is None: 1789 append(self.showDictError( 1790 _("You do not have read access to the locations page:"), 1791 locations_page)) 1792 1793 # Attempt to show the map. 1794 1795 else: 1796 1797 # Get events by position. 1798 1799 events_by_location = {} 1800 event_locations = {} 1801 1802 for event in all_shown_events: 1803 event_details = event.getDetails() 1804 1805 location = event_details.get("location") 1806 geo = event_details.get("geo") 1807 1808 # Make a temporary location if an explicit position is given 1809 # but not a location name. 1810 1811 if not location and geo: 1812 location = "%s %s" % tuple(geo) 1813 1814 # Map the location to a position. 1815 1816 if location is not None and not event_locations.has_key(location): 1817 1818 # Get any explicit position of an event. 1819 1820 if geo: 1821 latitude, longitude = geo 1822 1823 # Or look up the position of a location using the locations 1824 # page. 1825 1826 else: 1827 latitude, longitude = Location(location, locations).getPosition() 1828 1829 # Use a normalised location if necessary. 1830 1831 if latitude is None and longitude is None: 1832 normalised_location = getNormalisedLocation(location) 1833 if normalised_location is not None: 1834 latitude, longitude = getLocationPosition(normalised_location, locations) 1835 if latitude is not None and longitude is not None: 1836 location = normalised_location 1837 1838 # Only remember positioned locations. 1839 1840 if latitude is not None and longitude is not None: 1841 event_locations[location] = latitude, longitude 1842 1843 # Record events according to location. 1844 1845 if not events_by_location.has_key(location): 1846 events_by_location[location] = [] 1847 1848 events_by_location[location].append(event) 1849 1850 # Get the map image URL. 1851 1852 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request) 1853 1854 # Start of map view output. 1855 1856 map_identifier = "map-%s" % self.getIdentifier() 1857 append(fmt.div(on=1, css_class="event-map", id=map_identifier)) 1858 1859 append(fmt.table(on=1, attrs={"summary" : _("A map showing events")})) 1860 1861 append(self.writeMapTableHeading()) 1862 1863 append(fmt.table_row(on=1)) 1864 append(fmt.table_cell(on=1)) 1865 1866 append(fmt.div(on=1, css_class="event-map-container")) 1867 append(fmt.image(map_image_url)) 1868 append(fmt.number_list(on=1)) 1869 1870 # Events with no location are unpositioned. 1871 1872 if events_by_location.has_key(None): 1873 unpositioned_events = events_by_location[None] 1874 del events_by_location[None] 1875 else: 1876 unpositioned_events = [] 1877 1878 # Events whose location is unpositioned are themselves considered 1879 # unpositioned. 1880 1881 for location in set(events_by_location.keys()).difference(event_locations.keys()): 1882 unpositioned_events += events_by_location[location] 1883 1884 # Sort the locations before traversing them. 1885 1886 event_locations = event_locations.items() 1887 event_locations.sort() 1888 1889 # Show the events in the map. 1890 1891 for location, (latitude, longitude) in event_locations: 1892 events = events_by_location[location] 1893 1894 # Skip unpositioned locations and locations outside the map. 1895 1896 if latitude is None or longitude is None or \ 1897 latitude < map_bottom_left_latitude or \ 1898 longitude < map_bottom_left_longitude or \ 1899 latitude > map_top_right_latitude or \ 1900 longitude > map_top_right_longitude: 1901 1902 unpositioned_events += events 1903 continue 1904 1905 # Get the position and dimensions of the map marker. 1906 # NOTE: Use one degree as the marker size. 1907 1908 marker_x, marker_y = getPositionForCentrePoint( 1909 getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude, 1910 map_x_scale, map_y_scale), 1911 map_x_scale, map_y_scale) 1912 1913 # Add the map marker. 1914 1915 append(self.writeMapMarker(marker_x, marker_y, map_x_scale, map_y_scale, location, events)) 1916 1917 append(fmt.number_list(on=0)) 1918 append(fmt.div(on=0)) 1919 append(fmt.table_cell(on=0)) 1920 append(fmt.table_row(on=0)) 1921 1922 # Write unpositioned events. 1923 1924 if unpositioned_events: 1925 unpositioned_identifier = "unpositioned-%s" % self.getIdentifier() 1926 1927 append(fmt.table_row(on=1, css_class="event-map-unpositioned", 1928 id=unpositioned_identifier)) 1929 append(fmt.table_cell(on=1)) 1930 1931 append(fmt.heading(on=1, depth=2)) 1932 append(fmt.text(_("Events not shown on the map"))) 1933 append(fmt.heading(on=0, depth=2)) 1934 1935 # Show and hide controls. 1936 1937 append(fmt.div(on=1, css_class="event-map-show-control")) 1938 append(fmt.anchorlink(on=1, name=unpositioned_identifier)) 1939 append(fmt.text(_("Show unpositioned events"))) 1940 append(fmt.anchorlink(on=0)) 1941 append(fmt.div(on=0)) 1942 1943 append(fmt.div(on=1, css_class="event-map-hide-control")) 1944 append(fmt.anchorlink(on=1, name=map_identifier)) 1945 append(fmt.text(_("Hide unpositioned events"))) 1946 append(fmt.anchorlink(on=0)) 1947 append(fmt.div(on=0)) 1948 1949 append(self.writeMapEventSummaries(unpositioned_events)) 1950 1951 # End of map view output. 1952 1953 append(fmt.table_cell(on=0)) 1954 append(fmt.table_row(on=0)) 1955 append(fmt.table(on=0)) 1956 append(fmt.div(on=0)) 1957 1958 # Output a list. 1959 1960 elif self.mode == "list": 1961 1962 # Start of list view output. 1963 1964 append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 1965 1966 # Output a list. 1967 # NOTE: Make the heading depth configurable. 1968 1969 for period in self.first.until(self.last): 1970 1971 append(fmt.listitem(on=1, attr={"class" : "event-listings-period"})) 1972 append(fmt.heading(on=1, depth=2, attr={"class" : "event-listings-heading"})) 1973 1974 # Either write a date heading or produce links for navigable 1975 # calendars. 1976 1977 append(self.writeDateHeading(period)) 1978 1979 append(fmt.heading(on=0, depth=2)) 1980 1981 append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"})) 1982 1983 # Show the events in order. 1984 1985 events_in_period = getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period)) 1986 events_in_period.sort(sort_start_first) 1987 1988 for event in events_in_period: 1989 event_page = event.getPage() 1990 event_details = event.getDetails() 1991 event_summary = event.getSummary(self.parent_name) 1992 1993 append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 1994 1995 # Link to the page using the summary. 1996 1997 append(fmt.paragraph(on=1)) 1998 append(event.linkToEvent(request, event_summary)) 1999 append(fmt.paragraph(on=0)) 2000 2001 # Start and end dates. 2002 2003 append(fmt.paragraph(on=1)) 2004 append(fmt.span(on=1)) 2005 append(fmt.text(str(event_details["start"]))) 2006 append(fmt.span(on=0)) 2007 append(fmt.text(" - ")) 2008 append(fmt.span(on=1)) 2009 append(fmt.text(str(event_details["end"]))) 2010 append(fmt.span(on=0)) 2011 append(fmt.paragraph(on=0)) 2012 2013 # Location. 2014 2015 if event_details.has_key("location"): 2016 append(fmt.paragraph(on=1)) 2017 append(event_page.formatText(event_details["location"], fmt)) 2018 append(fmt.paragraph(on=1)) 2019 2020 # Topics. 2021 2022 if event_details.has_key("topics") or event_details.has_key("categories"): 2023 append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 2024 2025 for topic in event_details.get("topics") or event_details.get("categories") or []: 2026 append(fmt.listitem(on=1)) 2027 append(event_page.formatText(topic, fmt)) 2028 append(fmt.listitem(on=0)) 2029 2030 append(fmt.bullet_list(on=0)) 2031 2032 append(fmt.listitem(on=0)) 2033 2034 append(fmt.bullet_list(on=0)) 2035 2036 # End of list view output. 2037 2038 append(fmt.bullet_list(on=0)) 2039 2040 # Output a month calendar. This shows month-by-month data. 2041 2042 elif self.mode == "calendar": 2043 2044 # Visit all months in the requested range, or across known events. 2045 2046 for month in self.first.months_until(self.last): 2047 2048 # Output a month. 2049 2050 append(fmt.table(on=1, attrs={"tableclass" : "event-month", "summary" : _("A table showing a calendar month")})) 2051 2052 # Either write a month heading or produce links for navigable 2053 # calendars. 2054 2055 append(self.writeMonthTableHeading(month)) 2056 2057 # Weekday headings. 2058 2059 append(self.writeWeekdayHeadings()) 2060 2061 # Process the days of the month. 2062 2063 start_weekday, number_of_days = month.month_properties() 2064 2065 # The start weekday is the weekday of day number 1. 2066 # Find the first day of the week, counting from below zero, if 2067 # necessary, in order to land on the first day of the month as 2068 # day number 1. 2069 2070 first_day = 1 - start_weekday 2071 2072 while first_day <= number_of_days: 2073 2074 # Find events in this week and determine how to mark them on the 2075 # calendar. 2076 2077 week_start = month.as_date(max(first_day, 1)) 2078 week_end = month.as_date(min(first_day + 6, number_of_days)) 2079 2080 full_coverage, week_slots = getCoverage( 2081 getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end))) 2082 2083 # Make a new table region. 2084 # NOTE: Moin opens a "tbody" element in the table method. 2085 2086 append(fmt.rawHTML("</tbody>")) 2087 append(fmt.rawHTML("<tbody>")) 2088 2089 # Output a week, starting with the day numbers. 2090 2091 append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 2092 2093 # Either generate empty days... 2094 2095 if not week_slots: 2096 append(self.writeEmptyWeek(first_day, number_of_days, month)) 2097 2098 # Or generate each set of scheduled events... 2099 2100 else: 2101 append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 2102 2103 # Process the next week... 2104 2105 first_day += 7 2106 2107 # End of month. 2108 # NOTE: Moin closes a "tbody" element in the table method. 2109 2110 append(fmt.table(on=0)) 2111 2112 # Output a day view. 2113 2114 elif self.mode == "day": 2115 2116 # Visit all days in the requested range, or across known events. 2117 2118 for date in self.first.days_until(self.last): 2119 2120 append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day", "summary" : _("A table showing a calendar day")})) 2121 2122 full_coverage, day_slots = getCoverage( 2123 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 2124 2125 # Work out how many columns the day title will need. 2126 # Include spacers after the scale and each event column. 2127 2128 colspan = sum(map(len, day_slots.values())) * 2 + 2 2129 2130 append(self.writeDayTableHeading(date, colspan)) 2131 2132 # Either generate empty days... 2133 2134 if not day_slots: 2135 append(self.writeEmptyDay(date)) 2136 2137 # Or generate each set of scheduled events... 2138 2139 else: 2140 append(self.writeDaySlots(date, full_coverage, day_slots)) 2141 2142 # End of day. 2143 2144 append(fmt.table(on=0)) 2145 2146 # Output view controls. 2147 2148 append(fmt.div(on=1, css_class="event-controls")) 2149 append(self.writeViewControls()) 2150 append(fmt.div(on=0)) 2151 2152 # Close the calendar region. 2153 2154 append(fmt.div(on=0)) 2155 2156 # Add any scripts. 2157 2158 if isinstance(fmt, request.html_formatter.__class__): 2159 append(self.update_script) 2160 2161 return ''.join(output) 2162 2163 update_script = """\ 2164 <script type="text/javascript"> 2165 function replaceCalendar(name, url) { 2166 var calendar = document.getElementById(name); 2167 2168 if (calendar == null) { 2169 return true; 2170 } 2171 2172 var xmlhttp = new XMLHttpRequest(); 2173 xmlhttp.open("GET", url, false); 2174 xmlhttp.send(null); 2175 2176 var newCalendar = xmlhttp.responseText; 2177 2178 if (newCalendar != null) { 2179 calendar.innerHTML = newCalendar; 2180 return false; 2181 } 2182 2183 return true; 2184 } 2185 </script> 2186 """ 2187 2188 # vim: tabstop=4 expandtab shiftwidth=4