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