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 if request.user.may.read(maps_page): 1519 maps = request.dicts.dict(maps_page) 1520 else: 1521 maps = None 1522 1523 if request.user.may.read(locations_page): 1524 locations = request.dicts.dict(locations_page) 1525 else: 1526 locations = None 1527 1528 # Get the map image definition. 1529 1530 if maps is not None and map_name is not None: 1531 try: 1532 map_details = maps[map_name].split() 1533 1534 map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \ 1535 map(getMapReference, map_details[:4]) 1536 map_width, map_height = map(int, map_details[4:6]) 1537 map_image = map_details[6] 1538 1539 map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees() 1540 map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees() 1541 1542 except (KeyError, ValueError): 1543 pass 1544 1545 # Report errors. 1546 1547 if maps is None: 1548 output.append(view.showDictError( 1549 _("You do not have read access to the maps page:"), 1550 maps_page)) 1551 1552 elif map_name is None: 1553 output.append(view.showDictError( 1554 _("Please specify a valid map name corresponding to an entry on the following page:"), 1555 maps_page)) 1556 1557 elif map_image is None: 1558 output.append(view.showDictError( 1559 _("Please specify a valid entry for %s on the following page:") % map_name, 1560 maps_page)) 1561 1562 elif locations is None: 1563 output.append(view.showDictError( 1564 _("You do not have read access to the locations page:"), 1565 locations_page)) 1566 1567 # Attempt to show the map. 1568 1569 else: 1570 1571 # Get events by position. 1572 1573 events_by_location = {} 1574 event_locations = {} 1575 1576 for event in all_shown_events: 1577 event_details = event.getDetails() 1578 1579 location = event_details.get("location") 1580 1581 if location is not None and not event_locations.has_key(location): 1582 1583 # Look up the position of a location using the locations page. 1584 1585 latitude, longitude = getLocationPosition(location, locations) 1586 1587 # Use a normalised location if necessary. 1588 1589 if latitude is None and longitude is None: 1590 normalised_location = getNormalisedLocation(location) 1591 if normalised_location is not None: 1592 latitude, longitude = getLocationPosition(normalised_location, locations) 1593 if latitude is not None and longitude is not None: 1594 location = normalised_location 1595 1596 event_locations[location] = latitude, longitude 1597 1598 if not events_by_location.has_key(location): 1599 events_by_location[location] = [] 1600 1601 events_by_location[location].append(event) 1602 1603 # Get the map image URL. 1604 1605 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request) 1606 1607 # Start of map view output. 1608 1609 map_identifier = "map-%s" % view.getIdentifier() 1610 output.append(fmt.div(on=1, css_class="event-map", id=map_identifier)) 1611 1612 output.append(fmt.table(on=1)) 1613 1614 output.append(fmt.table_row(on=1)) 1615 output.append(view.writeMapTableHeading()) 1616 output.append(fmt.table_row(on=0)) 1617 1618 output.append(fmt.table_row(on=1)) 1619 output.append(fmt.table_cell(on=1)) 1620 1621 output.append(fmt.div(on=1, css_class="event-map-container")) 1622 output.append(fmt.image(map_image_url)) 1623 output.append(fmt.number_list(on=1)) 1624 1625 # Events with no location are unpositioned. 1626 1627 if events_by_location.has_key(None): 1628 unpositioned_events = events_by_location[None] 1629 del events_by_location[None] 1630 else: 1631 unpositioned_events = [] 1632 1633 event_locations = event_locations.items() 1634 event_locations.sort() 1635 1636 # Show the events in the map. 1637 1638 for location, (latitude, longitude) in event_locations: 1639 events = events_by_location[location] 1640 1641 # Skip unpositioned locations and locations outside the map. 1642 1643 if latitude is None or longitude is None or \ 1644 latitude < map_bottom_left_latitude or \ 1645 longitude < map_bottom_left_longitude or \ 1646 latitude > map_top_right_latitude or \ 1647 longitude > map_top_right_longitude: 1648 1649 unpositioned_events += events 1650 continue 1651 1652 # Get the position and dimensions of the map marker. 1653 # NOTE: Use one degree as the marker size. 1654 1655 marker_x, marker_y = getPositionForCentrePoint( 1656 getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude, 1657 map_x_scale, map_y_scale), 1658 map_x_scale, map_y_scale) 1659 1660 # Put a marker on the map. 1661 1662 output.append(fmt.listitem(on=1, css_class="event-map-label")) 1663 1664 # Have a positioned marker for the print mode. 1665 1666 output.append(fmt.div(on=1, css_class="event-map-label-only", 1667 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1668 marker_x, marker_y, map_x_scale, map_y_scale)) 1669 output.append(fmt.div(on=0)) 1670 1671 # Have a marker containing a pop-up when using the screen mode, 1672 # providing a normal block when using the print mode. 1673 1674 output.append(fmt.div(on=1, css_class="event-map-label", 1675 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 1676 marker_x, marker_y, map_x_scale, map_y_scale)) 1677 output.append(fmt.div(on=1, css_class="event-map-details")) 1678 output.append(fmt.div(on=1, css_class="event-map-shadow")) 1679 output.append(fmt.div(on=1, css_class="event-map-location")) 1680 1681 output.append(fmt.heading(on=1, depth=2)) 1682 output.append(fmt.text(location)) 1683 output.append(fmt.heading(on=0, depth=2)) 1684 1685 output.append(view.writeMapEventSummaries(events)) 1686 1687 output.append(fmt.div(on=0)) 1688 output.append(fmt.div(on=0)) 1689 output.append(fmt.div(on=0)) 1690 output.append(fmt.div(on=0)) 1691 output.append(fmt.listitem(on=0)) 1692 1693 output.append(fmt.number_list(on=0)) 1694 output.append(fmt.div(on=0)) 1695 output.append(fmt.table_cell(on=0)) 1696 output.append(fmt.table_row(on=0)) 1697 1698 # Write unpositioned events. 1699 1700 if unpositioned_events: 1701 unpositioned_identifier = "unpositioned-%s" % view.getIdentifier() 1702 1703 output.append(fmt.table_row(on=1, css_class="event-map-unpositioned", 1704 id=unpositioned_identifier)) 1705 output.append(fmt.table_cell(on=1)) 1706 1707 output.append(fmt.heading(on=1, depth=2)) 1708 output.append(fmt.text(_("Events not shown on the map"))) 1709 output.append(fmt.heading(on=0, depth=2)) 1710 1711 # Show and hide controls. 1712 1713 output.append(fmt.div(on=1, css_class="event-map-show-control")) 1714 output.append(fmt.anchorlink(on=1, name=unpositioned_identifier)) 1715 output.append(fmt.text(_("Show unpositioned events"))) 1716 output.append(fmt.anchorlink(on=0)) 1717 output.append(fmt.div(on=0)) 1718 1719 output.append(fmt.div(on=1, css_class="event-map-hide-control")) 1720 output.append(fmt.anchorlink(on=1, name=map_identifier)) 1721 output.append(fmt.text(_("Hide unpositioned events"))) 1722 output.append(fmt.anchorlink(on=0)) 1723 output.append(fmt.div(on=0)) 1724 1725 output.append(view.writeMapEventSummaries(unpositioned_events)) 1726 1727 # End of map view output. 1728 1729 output.append(fmt.table_cell(on=0)) 1730 output.append(fmt.table_row(on=0)) 1731 output.append(fmt.table(on=0)) 1732 output.append(fmt.div(on=0)) 1733 1734 # Output a list. 1735 1736 elif mode == "list": 1737 1738 # Start of list view output. 1739 1740 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 1741 1742 # Output a list. 1743 1744 for period in first.until(last): 1745 1746 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-period"})) 1747 output.append(fmt.div(on=1, attr={"class" : "event-listings-heading"})) 1748 1749 # Either write a date heading or produce links for navigable 1750 # calendars. 1751 1752 output.append(view.writeDateHeading(period)) 1753 1754 output.append(fmt.div(on=0)) 1755 1756 output.append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"})) 1757 1758 # Show the events in order. 1759 1760 for event in getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period)): 1761 event_page = event.getPage() 1762 event_details = event.getDetails() 1763 event_summary = event.getSummary(parent_name) 1764 1765 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 1766 1767 # Link to the page using the summary. 1768 1769 output.append(fmt.paragraph(on=1)) 1770 output.append(event_page.linkToPage(request, event_summary)) 1771 output.append(fmt.paragraph(on=0)) 1772 1773 # Start and end dates. 1774 1775 output.append(fmt.paragraph(on=1)) 1776 output.append(fmt.span(on=1)) 1777 output.append(fmt.text(str(event_details["start"]))) 1778 output.append(fmt.span(on=0)) 1779 output.append(fmt.text(" - ")) 1780 output.append(fmt.span(on=1)) 1781 output.append(fmt.text(str(event_details["end"]))) 1782 output.append(fmt.span(on=0)) 1783 output.append(fmt.paragraph(on=0)) 1784 1785 # Location. 1786 1787 if event_details.has_key("location"): 1788 output.append(fmt.paragraph(on=1)) 1789 output.append(formatText(event_details["location"], request, fmt, parser_cls)) 1790 output.append(fmt.paragraph(on=1)) 1791 1792 # Topics. 1793 1794 if event_details.has_key("topics") or event_details.has_key("categories"): 1795 output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 1796 1797 for topic in event_details.get("topics") or event_details.get("categories") or []: 1798 output.append(fmt.listitem(on=1)) 1799 output.append(formatText(topic, request, fmt, parser_cls)) 1800 output.append(fmt.listitem(on=0)) 1801 1802 output.append(fmt.bullet_list(on=0)) 1803 1804 output.append(fmt.listitem(on=0)) 1805 1806 output.append(fmt.bullet_list(on=0)) 1807 1808 # End of list view output. 1809 1810 output.append(fmt.bullet_list(on=0)) 1811 1812 # Output a month calendar. This shows month-by-month data. 1813 1814 elif mode == "calendar": 1815 1816 # Visit all months in the requested range, or across known events. 1817 1818 for month in first.months_until(last): 1819 1820 # Output a month. 1821 1822 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 1823 1824 # Either write a month heading or produce links for navigable 1825 # calendars. 1826 1827 output.append(view.writeMonthTableHeading(month)) 1828 1829 # Weekday headings. 1830 1831 output.append(view.writeWeekdayHeadings()) 1832 1833 # Process the days of the month. 1834 1835 start_weekday, number_of_days = month.month_properties() 1836 1837 # The start weekday is the weekday of day number 1. 1838 # Find the first day of the week, counting from below zero, if 1839 # necessary, in order to land on the first day of the month as 1840 # day number 1. 1841 1842 first_day = 1 - start_weekday 1843 1844 while first_day <= number_of_days: 1845 1846 # Find events in this week and determine how to mark them on the 1847 # calendar. 1848 1849 week_start = month.as_date(max(first_day, 1)) 1850 week_end = month.as_date(min(first_day + 6, number_of_days)) 1851 1852 full_coverage, week_slots = getCoverage( 1853 getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end))) 1854 1855 # Output a week, starting with the day numbers. 1856 1857 output.append(view.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 1858 1859 # Either generate empty days... 1860 1861 if not week_slots: 1862 output.append(view.writeEmptyWeek(first_day, number_of_days)) 1863 1864 # Or generate each set of scheduled events... 1865 1866 else: 1867 output.append(view.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 1868 1869 # Process the next week... 1870 1871 first_day += 7 1872 1873 # End of month. 1874 1875 output.append(fmt.table(on=0)) 1876 1877 # Output a day view. 1878 1879 elif mode == "day": 1880 1881 # Visit all days in the requested range, or across known events. 1882 1883 for date in first.days_until(last): 1884 1885 output.append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"})) 1886 1887 full_coverage, day_slots = getCoverage( 1888 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 1889 1890 # Work out how many columns the day title will need. 1891 # Include spacers after the scale and each event column. 1892 1893 colspan = sum(map(len, day_slots.values())) * 2 + 2 1894 1895 output.append(view.writeDayTableHeading(date, colspan)) 1896 1897 # Either generate empty days... 1898 1899 if not day_slots: 1900 output.append(view.writeEmptyDay(date)) 1901 1902 # Or generate each set of scheduled events... 1903 1904 else: 1905 output.append(view.writeDaySlots(date, full_coverage, day_slots)) 1906 1907 # End of day. 1908 1909 output.append(fmt.table(on=0)) 1910 1911 # Output view controls. 1912 1913 output.append(fmt.div(on=1, css_class="event-controls")) 1914 output.append(view.writeViewControls()) 1915 output.append(fmt.div(on=0)) 1916 1917 # Close the calendar region. 1918 1919 output.append(fmt.div(on=0)) 1920 1921 return ''.join(output) 1922 1923 # vim: tabstop=4 expandtab shiftwidth=4