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