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