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