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(fmt.text(_("Download..."))) 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)) 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)) 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(fmt.text(_("Subscribe..."))) 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)) 776 append(fmt.text(" ")) 777 append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link)) 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)) 782 append(fmt.text(" ")) 783 append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link)) 784 append(fmt.span(on=0)) 785 786 append(linkToPage(request, page, label, date_link, onclick=date_update_link)) 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 append(fmt.span(on=1, css_class="event-day-number")) 843 append(fmt.text(unicode(day))) 844 append(fmt.span(on=0)) 845 846 append(fmt.div(on=0)) 847 848 # End of heading. 849 850 append(fmt.table_cell(on=0)) 851 852 return "".join(output) 853 854 # Common layout methods. 855 856 def getEventStyle(self, colour_seed): 857 858 "Generate colour style information using the given 'colour_seed'." 859 860 bg = getColour(colour_seed) 861 fg = getBlackOrWhite(bg) 862 return "background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg) 863 864 def writeEventSummaryBox(self, event): 865 866 "Return an event summary box linking to the given 'event'." 867 868 page = self.page 869 request = page.request 870 fmt = request.formatter 871 872 output = [] 873 append = output.append 874 875 event_details = event.getDetails() 876 event_summary = event.getSummary(self.parent_name) 877 878 is_ambiguous = event.as_timespan().ambiguous() 879 style = self.getEventStyle(event_summary) 880 881 # The event box contains the summary, alongside 882 # other elements. 883 884 append(fmt.div(on=1, css_class="event-summary-box")) 885 append(fmt.div(on=1, css_class="event-summary", style=style)) 886 887 if is_ambiguous: 888 append(fmt.icon("/!\\")) 889 890 append(event.linkToEvent(request, event_summary)) 891 append(fmt.div(on=0)) 892 893 # Add a pop-up element for long summaries. 894 895 append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 896 897 if is_ambiguous: 898 append(fmt.icon("/!\\")) 899 900 append(event.linkToEvent(request, event_summary)) 901 append(fmt.div(on=0)) 902 903 append(fmt.div(on=0)) 904 905 return "".join(output) 906 907 # Calendar layout methods. 908 909 def writeMonthTableHeading(self, year_month): 910 page = self.page 911 fmt = page.request.formatter 912 913 output = [] 914 append = output.append 915 916 append(fmt.table_row(on=1)) 917 append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"})) 918 919 append(self.writeMonthHeading(year_month)) 920 921 append(fmt.table_cell(on=0)) 922 append(fmt.table_row(on=0)) 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 append(fmt.table_row(on=1)) 1251 1252 append(fmt.table_cell(on=1, attrs={"class" : "event-full-day-heading", "colspan" : str(colspan)})) 1253 append(self.writeDayHeading(date)) 1254 append(fmt.table_cell(on=0)) 1255 1256 append(fmt.table_row(on=0)) 1257 return "".join(output) 1258 1259 def writeEmptyDay(self, date): 1260 page = self.page 1261 fmt = page.request.formatter 1262 1263 output = [] 1264 append = output.append 1265 1266 append(fmt.table_row(on=1)) 1267 1268 append(fmt.table_cell(on=1, 1269 attrs={"class" : "event-day-content event-day-empty"})) 1270 1271 append(fmt.table_row(on=0)) 1272 return "".join(output) 1273 1274 def writeDaySlots(self, date, full_coverage, day_slots): 1275 1276 """ 1277 Given a 'date', non-empty 'full_coverage' for the day concerned, and a 1278 non-empty mapping of 'day_slots' (from locations to event collections), 1279 output the day slots for the day. 1280 """ 1281 1282 page = self.page 1283 fmt = page.request.formatter 1284 1285 output = [] 1286 append = output.append 1287 1288 locations = day_slots.keys() 1289 locations.sort(sort_none_first) 1290 1291 # Traverse the time scale of the full coverage, visiting each slot to 1292 # determine whether it provides content for each period. 1293 1294 scale = getCoverageScale(full_coverage) 1295 1296 # Define a mapping of events to rowspans. 1297 1298 rowspans = {} 1299 1300 # Populate each period with event details, recording how many periods 1301 # each event populates. 1302 1303 day_rows = [] 1304 1305 for period, limit, times in scale: 1306 1307 # Ignore timespans before this day. 1308 1309 if period != date: 1310 continue 1311 1312 # Visit each slot corresponding to a location (or no location). 1313 1314 day_row = [] 1315 1316 for location in locations: 1317 1318 # Visit each coverage span, presenting the events in the span. 1319 1320 for events in day_slots[location]: 1321 event = self.getActiveEvent(period, events) 1322 if event is not None: 1323 if not rowspans.has_key(event): 1324 rowspans[event] = 1 1325 else: 1326 rowspans[event] += 1 1327 day_row.append((location, event)) 1328 1329 day_rows.append((period, day_row, times)) 1330 1331 # Output the locations. 1332 1333 append(fmt.table_row(on=1)) 1334 1335 # Add a spacer. 1336 1337 append(self.writeDaySpacer(colspan=2, cls="location")) 1338 1339 for location in locations: 1340 1341 # Add spacers to the column spans. 1342 1343 columns = len(day_slots[location]) * 2 - 1 1344 append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)})) 1345 append(fmt.text(location or "")) 1346 append(fmt.table_cell(on=0)) 1347 1348 # Add a trailing spacer. 1349 1350 append(self.writeDaySpacer(cls="location")) 1351 1352 append(fmt.table_row(on=0)) 1353 1354 # Output the periods with event details. 1355 1356 last_period = period = None 1357 events_written = set() 1358 1359 for period, day_row, times in day_rows: 1360 1361 # Write a heading describing the time. 1362 1363 append(fmt.table_row(on=1)) 1364 1365 # Show times only for distinct periods. 1366 1367 if not last_period or period.start != last_period.start: 1368 append(self.writeDayScaleHeading(times)) 1369 else: 1370 append(self.writeDayScaleHeading([])) 1371 1372 append(self.writeDaySpacer()) 1373 1374 # Visit each slot corresponding to a location (or no location). 1375 1376 for location, event in day_row: 1377 1378 # Output each location slot's contribution. 1379 1380 if event is None or event not in events_written: 1381 append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) 1382 if event is not None: 1383 events_written.add(event) 1384 1385 # Add a trailing spacer. 1386 1387 append(self.writeDaySpacer()) 1388 1389 append(fmt.table_row(on=0)) 1390 1391 last_period = period 1392 1393 # Write a final time heading if the last period ends in the current day. 1394 1395 if period is not None: 1396 if period.end == date: 1397 append(fmt.table_row(on=1)) 1398 append(self.writeDayScaleHeading(times)) 1399 1400 for slot in day_row: 1401 append(self.writeDaySpacer()) 1402 append(self.writeEmptyDaySlot()) 1403 1404 append(fmt.table_row(on=0)) 1405 1406 return "".join(output) 1407 1408 def writeDayScaleHeading(self, times): 1409 page = self.page 1410 fmt = page.request.formatter 1411 1412 output = [] 1413 append = output.append 1414 1415 append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"})) 1416 1417 first = 1 1418 for t in times: 1419 if isinstance(t, DateTime): 1420 if not first: 1421 append(fmt.linebreak(0)) 1422 append(fmt.text(t.time_string())) 1423 first = 0 1424 1425 append(fmt.table_cell(on=0)) 1426 1427 return "".join(output) 1428 1429 def getActiveEvent(self, period, events): 1430 for event in events: 1431 if period not in event: 1432 continue 1433 return event 1434 else: 1435 return None 1436 1437 def writeDaySlot(self, period, event, rowspan): 1438 page = self.page 1439 fmt = page.request.formatter 1440 1441 output = [] 1442 append = output.append 1443 1444 if event is not None: 1445 event_summary = event.getSummary(self.parent_name) 1446 style = self.getEventStyle(event_summary) 1447 1448 append(fmt.table_cell(on=1, attrs={ 1449 "class" : "event-timespan-content event-timespan-busy", 1450 "style" : style, 1451 "rowspan" : str(rowspan) 1452 })) 1453 append(self.writeEventSummaryBox(event)) 1454 append(fmt.table_cell(on=0)) 1455 else: 1456 append(self.writeEmptyDaySlot()) 1457 1458 return "".join(output) 1459 1460 def writeEmptyDaySlot(self): 1461 page = self.page 1462 fmt = page.request.formatter 1463 1464 output = [] 1465 append = output.append 1466 1467 append(fmt.table_cell(on=1, 1468 attrs={"class" : "event-timespan-content event-timespan-empty"})) 1469 append(fmt.table_cell(on=0)) 1470 1471 return "".join(output) 1472 1473 def writeDaySpacer(self, colspan=1, cls="timespan"): 1474 page = self.page 1475 fmt = page.request.formatter 1476 1477 output = [] 1478 append = output.append 1479 1480 append(fmt.table_cell(on=1, attrs={ 1481 "class" : "event-%s-spacer" % cls, 1482 "colspan" : str(colspan)})) 1483 append(fmt.table_cell(on=0)) 1484 return "".join(output) 1485 1486 # Map layout methods. 1487 1488 def writeMapTableHeading(self): 1489 page = self.page 1490 fmt = page.request.formatter 1491 1492 output = [] 1493 append = output.append 1494 1495 append(fmt.table_cell(on=1, attrs={"class" : "event-map-heading"})) 1496 append(self.writeMapHeading()) 1497 append(fmt.table_cell(on=0)) 1498 1499 return "".join(output) 1500 1501 def showDictError(self, text, pagename): 1502 page = self.page 1503 request = page.request 1504 fmt = request.formatter 1505 1506 output = [] 1507 append = output.append 1508 1509 append(fmt.div(on=1, attrs={"class" : "event-aggregator-error"})) 1510 append(fmt.paragraph(on=1)) 1511 append(fmt.text(text)) 1512 append(fmt.paragraph(on=0)) 1513 append(fmt.paragraph(on=1)) 1514 append(linkToPage(request, Page(request, pagename), pagename)) 1515 append(fmt.paragraph(on=0)) 1516 1517 return "".join(output) 1518 1519 def writeMapMarker(self, marker_x, marker_y, map_x_scale, map_y_scale, location, events): 1520 1521 "Put a marker on the map." 1522 1523 page = self.page 1524 request = page.request 1525 fmt = request.formatter 1526 1527 output = [] 1528 append = output.append 1529 1530 append(fmt.listitem(on=1, css_class="event-map-label")) 1531 1532 # Have a positioned marker for the print mode. 1533 1534 append(fmt.div(on=1, css_class="event-map-label-only", 1535 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1536 marker_x, marker_y, map_x_scale, map_y_scale)) 1537 append(fmt.div(on=0)) 1538 1539 # Have a marker containing a pop-up when using the screen mode, 1540 # providing a normal block when using the print mode. 1541 1542 append(fmt.div(on=1, css_class="event-map-label", 1543 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1544 marker_x, marker_y, map_x_scale, map_y_scale)) 1545 append(fmt.div(on=1, css_class="event-map-details")) 1546 append(fmt.div(on=1, css_class="event-map-shadow")) 1547 append(fmt.div(on=1, css_class="event-map-location")) 1548 1549 # The location may have been given as formatted text, but this will not 1550 # be usable in a heading, so it must be first converted to plain text. 1551 1552 append(fmt.heading(on=1, depth=2)) 1553 append(fmt.text(to_plain_text(location, request))) 1554 append(fmt.heading(on=0, depth=2)) 1555 1556 append(self.writeMapEventSummaries(events)) 1557 1558 append(fmt.div(on=0)) 1559 append(fmt.div(on=0)) 1560 append(fmt.div(on=0)) 1561 append(fmt.div(on=0)) 1562 append(fmt.listitem(on=0)) 1563 1564 return "".join(output) 1565 1566 def writeMapEventSummaries(self, events): 1567 1568 "Write summaries of the given 'events' for the map." 1569 1570 page = self.page 1571 request = page.request 1572 fmt = request.formatter 1573 1574 # Sort the events by date. 1575 1576 events.sort(sort_start_first) 1577 1578 # Write out a self-contained list of events. 1579 1580 output = [] 1581 append = output.append 1582 1583 append(fmt.bullet_list(on=1, attr={"class" : "event-map-location-events"})) 1584 1585 for event in events: 1586 1587 # Get the event details. 1588 1589 event_summary = event.getSummary(self.parent_name) 1590 start, end = event.as_limits() 1591 event_period = self._getCalendarPeriod( 1592 start and self.getFullDateLabel(start), 1593 end and self.getFullDateLabel(end), 1594 "") 1595 1596 append(fmt.listitem(on=1)) 1597 1598 # Link to the page using the summary. 1599 1600 append(event.linkToEvent(request, event_summary)) 1601 1602 # Add the event period. 1603 1604 append(fmt.text(" ")) 1605 append(fmt.span(on=1, css_class="event-map-period")) 1606 append(fmt.text(event_period)) 1607 append(fmt.span(on=0)) 1608 1609 append(fmt.listitem(on=0)) 1610 1611 append(fmt.bullet_list(on=0)) 1612 1613 return "".join(output) 1614 1615 def render(self, all_shown_events): 1616 1617 """ 1618 Render the view, returning the rendered representation as a string. 1619 The view will show a list of 'all_shown_events'. 1620 """ 1621 1622 page = self.page 1623 request = page.request 1624 fmt = request.formatter 1625 _ = request.getText 1626 1627 # Make a calendar. 1628 1629 output = [] 1630 append = output.append 1631 1632 append(fmt.div(on=1, css_class="event-calendar", id=("EventAggregator-%s" % self.getIdentifier()))) 1633 1634 # Output download controls. 1635 1636 append(fmt.div(on=1, css_class="event-controls")) 1637 append(self.writeDownloadControls()) 1638 append(fmt.div(on=0)) 1639 1640 # Output a table. 1641 1642 if self.mode == "table": 1643 1644 # Start of table view output. 1645 1646 append(fmt.table(on=1, attrs={"tableclass" : "event-table"})) 1647 1648 append(fmt.table_row(on=1)) 1649 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1650 append(fmt.text(_("Event dates"))) 1651 append(fmt.table_cell(on=0)) 1652 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1653 append(fmt.text(_("Event location"))) 1654 append(fmt.table_cell(on=0)) 1655 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 1656 append(fmt.text(_("Event details"))) 1657 append(fmt.table_cell(on=0)) 1658 append(fmt.table_row(on=0)) 1659 1660 # Show the events in order. 1661 1662 all_shown_events.sort(sort_start_first) 1663 1664 for event in all_shown_events: 1665 event_page = event.getPage() 1666 event_summary = event.getSummary(self.parent_name) 1667 event_details = event.getDetails() 1668 1669 # Prepare CSS classes with category-related styling. 1670 1671 css_classes = ["event-table-details"] 1672 1673 for topic in event_details.get("topics") or event_details.get("categories") or []: 1674 1675 # Filter the category text to avoid illegal characters. 1676 1677 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 1678 1679 attrs = {"class" : " ".join(css_classes)} 1680 1681 append(fmt.table_row(on=1)) 1682 1683 # Start and end dates. 1684 1685 append(fmt.table_cell(on=1, attrs=attrs)) 1686 append(fmt.span(on=1)) 1687 append(fmt.text(str(event_details["start"]))) 1688 append(fmt.span(on=0)) 1689 1690 if event_details["start"] != event_details["end"]: 1691 append(fmt.text(" - ")) 1692 append(fmt.span(on=1)) 1693 append(fmt.text(str(event_details["end"]))) 1694 append(fmt.span(on=0)) 1695 1696 append(fmt.table_cell(on=0)) 1697 1698 # Location. 1699 1700 append(fmt.table_cell(on=1, attrs=attrs)) 1701 1702 if event_details.has_key("location"): 1703 append(event_page.formatText(event_details["location"], fmt)) 1704 1705 append(fmt.table_cell(on=0)) 1706 1707 # Link to the page using the summary. 1708 1709 append(fmt.table_cell(on=1, attrs=attrs)) 1710 append(event.linkToEvent(request, event_summary)) 1711 append(fmt.table_cell(on=0)) 1712 1713 append(fmt.table_row(on=0)) 1714 1715 # End of table view output. 1716 1717 append(fmt.table(on=0)) 1718 1719 # Output a map view. 1720 1721 elif self.mode == "map": 1722 1723 # Special dictionary pages. 1724 1725 maps_page = getMapsPage(request) 1726 locations_page = getLocationsPage(request) 1727 1728 map_image = None 1729 1730 # Get the maps and locations. 1731 1732 maps = getWikiDict(maps_page, request) 1733 locations = getWikiDict(locations_page, request) 1734 1735 # Get the map image definition. 1736 1737 if maps is not None and self.map_name: 1738 try: 1739 map_details = maps[self.map_name].split() 1740 1741 map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \ 1742 map(getMapReference, map_details[:4]) 1743 map_width, map_height = map(int, map_details[4:6]) 1744 map_image = map_details[6] 1745 1746 map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees() 1747 map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees() 1748 1749 except (KeyError, ValueError): 1750 pass 1751 1752 # Report errors. 1753 1754 if maps is None: 1755 append(self.showDictError( 1756 _("You do not have read access to the maps page:"), 1757 maps_page)) 1758 1759 elif not self.map_name: 1760 append(self.showDictError( 1761 _("Please specify a valid map name corresponding to an entry on the following page:"), 1762 maps_page)) 1763 1764 elif map_image is None: 1765 append(self.showDictError( 1766 _("Please specify a valid entry for %s on the following page:") % self.map_name, 1767 maps_page)) 1768 1769 elif locations is None: 1770 append(self.showDictError( 1771 _("You do not have read access to the locations page:"), 1772 locations_page)) 1773 1774 # Attempt to show the map. 1775 1776 else: 1777 1778 # Get events by position. 1779 1780 events_by_location = {} 1781 event_locations = {} 1782 1783 for event in all_shown_events: 1784 event_details = event.getDetails() 1785 1786 location = event_details.get("location") 1787 geo = event_details.get("geo") 1788 1789 # Make a temporary location if an explicit position is given 1790 # but not a location name. 1791 1792 if not location and geo: 1793 location = "%s %s" % tuple(geo) 1794 1795 # Map the location to a position. 1796 1797 if location is not None and not event_locations.has_key(location): 1798 1799 # Get any explicit position of an event. 1800 1801 if geo: 1802 latitude, longitude = geo 1803 1804 # Or look up the position of a location using the locations 1805 # page. 1806 1807 else: 1808 latitude, longitude = Location(location, locations).getPosition() 1809 1810 # Use a normalised location if necessary. 1811 1812 if latitude is None and longitude is None: 1813 normalised_location = getNormalisedLocation(location) 1814 if normalised_location is not None: 1815 latitude, longitude = getLocationPosition(normalised_location, locations) 1816 if latitude is not None and longitude is not None: 1817 location = normalised_location 1818 1819 # Only remember positioned locations. 1820 1821 if latitude is not None and longitude is not None: 1822 event_locations[location] = latitude, longitude 1823 1824 # Record events according to location. 1825 1826 if not events_by_location.has_key(location): 1827 events_by_location[location] = [] 1828 1829 events_by_location[location].append(event) 1830 1831 # Get the map image URL. 1832 1833 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request) 1834 1835 # Start of map view output. 1836 1837 map_identifier = "map-%s" % self.getIdentifier() 1838 append(fmt.div(on=1, css_class="event-map", id=map_identifier)) 1839 1840 append(fmt.table(on=1)) 1841 1842 append(fmt.table_row(on=1)) 1843 append(self.writeMapTableHeading()) 1844 append(fmt.table_row(on=0)) 1845 1846 append(fmt.table_row(on=1)) 1847 append(fmt.table_cell(on=1)) 1848 1849 append(fmt.div(on=1, css_class="event-map-container")) 1850 append(fmt.image(map_image_url)) 1851 append(fmt.number_list(on=1)) 1852 1853 # Events with no location are unpositioned. 1854 1855 if events_by_location.has_key(None): 1856 unpositioned_events = events_by_location[None] 1857 del events_by_location[None] 1858 else: 1859 unpositioned_events = [] 1860 1861 # Events whose location is unpositioned are themselves considered 1862 # unpositioned. 1863 1864 for location in set(events_by_location.keys()).difference(event_locations.keys()): 1865 unpositioned_events += events_by_location[location] 1866 1867 # Sort the locations before traversing them. 1868 1869 event_locations = event_locations.items() 1870 event_locations.sort() 1871 1872 # Show the events in the map. 1873 1874 for location, (latitude, longitude) in event_locations: 1875 events = events_by_location[location] 1876 1877 # Skip unpositioned locations and locations outside the map. 1878 1879 if latitude is None or longitude is None or \ 1880 latitude < map_bottom_left_latitude or \ 1881 longitude < map_bottom_left_longitude or \ 1882 latitude > map_top_right_latitude or \ 1883 longitude > map_top_right_longitude: 1884 1885 unpositioned_events += events 1886 continue 1887 1888 # Get the position and dimensions of the map marker. 1889 # NOTE: Use one degree as the marker size. 1890 1891 marker_x, marker_y = getPositionForCentrePoint( 1892 getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude, 1893 map_x_scale, map_y_scale), 1894 map_x_scale, map_y_scale) 1895 1896 # Add the map marker. 1897 1898 append(self.writeMapMarker(marker_x, marker_y, map_x_scale, map_y_scale, location, events)) 1899 1900 append(fmt.number_list(on=0)) 1901 append(fmt.div(on=0)) 1902 append(fmt.table_cell(on=0)) 1903 append(fmt.table_row(on=0)) 1904 1905 # Write unpositioned events. 1906 1907 if unpositioned_events: 1908 unpositioned_identifier = "unpositioned-%s" % self.getIdentifier() 1909 1910 append(fmt.table_row(on=1, css_class="event-map-unpositioned", 1911 id=unpositioned_identifier)) 1912 append(fmt.table_cell(on=1)) 1913 1914 append(fmt.heading(on=1, depth=2)) 1915 append(fmt.text(_("Events not shown on the map"))) 1916 append(fmt.heading(on=0, depth=2)) 1917 1918 # Show and hide controls. 1919 1920 append(fmt.div(on=1, css_class="event-map-show-control")) 1921 append(fmt.anchorlink(on=1, name=unpositioned_identifier)) 1922 append(fmt.text(_("Show unpositioned events"))) 1923 append(fmt.anchorlink(on=0)) 1924 append(fmt.div(on=0)) 1925 1926 append(fmt.div(on=1, css_class="event-map-hide-control")) 1927 append(fmt.anchorlink(on=1, name=map_identifier)) 1928 append(fmt.text(_("Hide unpositioned events"))) 1929 append(fmt.anchorlink(on=0)) 1930 append(fmt.div(on=0)) 1931 1932 append(self.writeMapEventSummaries(unpositioned_events)) 1933 1934 # End of map view output. 1935 1936 append(fmt.table_cell(on=0)) 1937 append(fmt.table_row(on=0)) 1938 append(fmt.table(on=0)) 1939 append(fmt.div(on=0)) 1940 1941 # Output a list. 1942 1943 elif self.mode == "list": 1944 1945 # Start of list view output. 1946 1947 append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 1948 1949 # Output a list. 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.div(on=1, 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.div(on=0)) 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"})) 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 # Output a week, starting with the day numbers. 2066 2067 append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 2068 2069 # Either generate empty days... 2070 2071 if not week_slots: 2072 append(self.writeEmptyWeek(first_day, number_of_days, month)) 2073 2074 # Or generate each set of scheduled events... 2075 2076 else: 2077 append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 2078 2079 # Process the next week... 2080 2081 first_day += 7 2082 2083 # End of month. 2084 2085 append(fmt.table(on=0)) 2086 2087 # Output a day view. 2088 2089 elif self.mode == "day": 2090 2091 # Visit all days in the requested range, or across known events. 2092 2093 for date in self.first.days_until(self.last): 2094 2095 append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"})) 2096 2097 full_coverage, day_slots = getCoverage( 2098 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 2099 2100 # Work out how many columns the day title will need. 2101 # Include spacers after the scale and each event column. 2102 2103 colspan = sum(map(len, day_slots.values())) * 2 + 2 2104 2105 append(self.writeDayTableHeading(date, colspan)) 2106 2107 # Either generate empty days... 2108 2109 if not day_slots: 2110 append(self.writeEmptyDay(date)) 2111 2112 # Or generate each set of scheduled events... 2113 2114 else: 2115 append(self.writeDaySlots(date, full_coverage, day_slots)) 2116 2117 # End of day. 2118 2119 append(fmt.table(on=0)) 2120 2121 # Output view controls. 2122 2123 append(fmt.div(on=1, css_class="event-controls")) 2124 append(self.writeViewControls()) 2125 append(fmt.div(on=0)) 2126 2127 # Close the calendar region. 2128 2129 append(fmt.div(on=0)) 2130 2131 # Add any scripts. 2132 2133 if isinstance(fmt, request.html_formatter.__class__): 2134 append(self.update_script) 2135 2136 return ''.join(output) 2137 2138 update_script = """\ 2139 <script type="text/javascript"> 2140 function replaceCalendar(name, url) { 2141 var calendar = document.getElementById(name); 2142 2143 if (calendar == null) { 2144 return true; 2145 } 2146 2147 var xmlhttp = new XMLHttpRequest(); 2148 xmlhttp.open("GET", url, false); 2149 xmlhttp.send(null); 2150 2151 var newCalendar = xmlhttp.responseText; 2152 2153 if (newCalendar != null) { 2154 calendar.innerHTML = newCalendar; 2155 return false; 2156 } 2157 2158 return true; 2159 } 2160 </script> 2161 """ 2162 2163 # vim: tabstop=4 expandtab shiftwidth=4