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