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