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