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