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