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 # 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, template_name, parent_name, mode, resolution, 1417 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_page.linkToPage(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_page.linkToPage(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