1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator library 4 5 @copyright: 2008, 2009, 2010, 2011, 2012 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 LocationSupport import * 12 from MoinDateSupport import * 13 from MoinSupport import * 14 15 from MoinMoin.Page import Page 16 from MoinMoin.action import AttachFile, cache 17 from MoinMoin import caching 18 from MoinMoin import search 19 from MoinMoin import wikiutil 20 21 import codecs 22 import time 23 import re 24 import urllib, urllib2 25 26 try: 27 from cStringIO import StringIO 28 except ImportError: 29 from StringIO import StringIO 30 31 try: 32 set 33 except NameError: 34 from sets import Set as set 35 36 try: 37 import vCalendar 38 except ImportError: 39 vCalendar = None 40 41 escape = wikiutil.escape 42 43 __version__ = "0.9" 44 45 # Regular expressions where MoinMoin does not provide the required support. 46 47 category_regexp = None 48 49 # Page parsing. 50 51 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 52 category_membership_regexp = re.compile(ur"^\s*(?:(Category\S+)(?:\s+(Category\S+))*)\s*$", re.MULTILINE | re.UNICODE) 53 54 # Value parsing. 55 56 country_code_regexp = re.compile(ur'(?:^|\W)(?P<code>[A-Z]{2})(?:$|\W+$)', re.UNICODE) 57 58 # Simple content parsing. 59 60 verbatim_regexp = re.compile(ur'(?:' 61 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 62 ur'|' 63 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 64 ur'|' 65 ur'!(?P<verbatim3>.*?)(\s|$)?' 66 ur'|' 67 ur'`(?P<monospace>.*?)`' 68 ur'|' 69 ur'{{{(?P<preformatted>.*?)}}}' 70 ur')', re.UNICODE) 71 72 # Utility functions. 73 74 def getCategoryPattern(request): 75 global category_regexp 76 77 try: 78 return request.cfg.cache.page_category_regexact 79 except AttributeError: 80 81 # Use regular expression from MoinMoin 1.7.1 otherwise. 82 83 if category_regexp is None: 84 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 85 return category_regexp 86 87 def getWikiDict(pagename, request): 88 if Page(request, pagename).exists() and request.user.may.read(pagename): 89 if hasattr(request.dicts, "dict"): 90 return request.dicts.dict(pagename) 91 else: 92 return request.dicts[pagename] 93 else: 94 return None 95 96 def to_list(s, sep): 97 return [x.strip() for x in s.split(sep) if x.strip()] 98 99 def sort_none_first(x, y): 100 if x is None: 101 return -1 102 elif y is None: 103 return 1 104 else: 105 return cmp(x, y) 106 107 def sort_start_first(x, y): 108 x_ts = x.as_limits() 109 if x_ts is not None: 110 x_start, x_end = x_ts 111 y_ts = y.as_limits() 112 if y_ts is not None: 113 y_start, y_end = y_ts 114 start_order = cmp(x_start, y_start) 115 if start_order == 0: 116 return cmp(x_end, y_end) 117 else: 118 return start_order 119 return 0 120 121 # Utility classes and associated functions. 122 123 class ActionSupport(ActionSupport): 124 125 "Extend the generic action support." 126 127 def get_month_lists(self, default_as_current=0): 128 129 """ 130 Return two lists of HTML element definitions corresponding to the start 131 and end month selection controls, with months selected according to any 132 values that have been specified via request parameters. 133 """ 134 135 _ = self._ 136 form = self.get_form() 137 138 # Initialise month lists. 139 140 start_month_list = [] 141 end_month_list = [] 142 143 start_month = self._get_input(form, "start-month", default_as_current and getCurrentMonth().month() or None) 144 end_month = self._get_input(form, "end-month", start_month) 145 146 # Prepare month lists, selecting specified months. 147 148 if not default_as_current: 149 start_month_list.append('<option value=""></option>') 150 end_month_list.append('<option value=""></option>') 151 152 for month in range(1, 13): 153 month_label = escape(_(getMonthLabel(month))) 154 selected = self._get_selected(month, start_month) 155 start_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 156 selected = self._get_selected(month, end_month) 157 end_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 158 159 return start_month_list, end_month_list 160 161 def get_year_defaults(self, default_as_current=0): 162 163 "Return defaults for the start and end years." 164 165 form = self.get_form() 166 167 start_year_default = form.get("start-year", [default_as_current and getCurrentYear() or ""])[0] 168 end_year_default = form.get("end-year", [default_as_current and start_year_default or ""])[0] 169 170 return start_year_default, end_year_default 171 172 def get_day_defaults(self, default_as_current=0): 173 174 "Return defaults for the start and end days." 175 176 form = self.get_form() 177 178 start_day_default = form.get("start-day", [default_as_current and getCurrentDate().day() or ""])[0] 179 end_day_default = form.get("end-day", [default_as_current and start_day_default or ""])[0] 180 181 return start_day_default, end_day_default 182 183 # Textual representations. 184 185 def getSimpleWikiText(text): 186 187 """ 188 Return the plain text representation of the given 'text' which may employ 189 certain Wiki syntax features, such as those providing verbatim or monospaced 190 text. 191 """ 192 193 # NOTE: Re-implementing support for verbatim text and linking avoidance. 194 195 return "".join([s for s in verbatim_regexp.split(text) if s is not None]) 196 197 def getEncodedWikiText(text): 198 199 "Encode the given 'text' in a verbatim representation." 200 201 return "<<Verbatim(%s)>>" % text 202 203 def getPrettyTitle(title): 204 205 "Return a nicely formatted version of the given 'title'." 206 207 return title.replace("_", " ").replace("/", u" ? ") 208 209 # Category discovery and searching. 210 211 def getCategories(request): 212 213 """ 214 From the AdvancedSearch macro, return a list of category page names using 215 the given 'request'. 216 """ 217 218 # This will return all pages with "Category" in the title. 219 220 cat_filter = getCategoryPattern(request).search 221 return request.rootpage.getPageList(filter=cat_filter) 222 223 def getCategoryMapping(category_pagenames, request): 224 225 """ 226 For the given 'category_pagenames' return a list of tuples of the form 227 (category name, category page name) using the given 'request'. 228 """ 229 230 cat_pattern = getCategoryPattern(request) 231 mapping = [] 232 for pagename in category_pagenames: 233 name = cat_pattern.match(pagename).group("key") 234 if name != "Category": 235 mapping.append((name, pagename)) 236 mapping.sort() 237 return mapping 238 239 def getCategoryPages(pagename, request): 240 241 """ 242 Return the pages associated with the given category 'pagename' using the 243 'request'. 244 """ 245 246 query = search.QueryParser().parse_query('category:%s' % pagename) 247 results = search.searchPages(request, query, "page_name") 248 249 cat_pattern = getCategoryPattern(request) 250 pages = [] 251 for page in results.hits: 252 if not cat_pattern.match(page.page_name): 253 pages.append(page) 254 return pages 255 256 def getAllCategoryPages(category_names, request): 257 258 """ 259 Return all pages belonging to the categories having the given 260 'category_names', using the given 'request'. 261 """ 262 263 pages = [] 264 pagenames = set() 265 266 for category_name in category_names: 267 268 # Get the pages and page names in the category. 269 270 pages_in_category = getCategoryPages(category_name, request) 271 272 # Visit each page in the category. 273 274 for page_in_category in pages_in_category: 275 pagename = page_in_category.page_name 276 277 # Only process each page once. 278 279 if pagename in pagenames: 280 continue 281 else: 282 pagenames.add(pagename) 283 284 pages.append(page_in_category) 285 286 return pages 287 288 def getPagesFromResults(result_pages, request): 289 290 "Return genuine pages for the given 'result_pages' using the 'request'." 291 292 return [Page(request, page.page_name) for page in result_pages] 293 294 # Event resources providing collections of events. 295 296 class EventResource: 297 298 "A resource providing event information." 299 300 def __init__(self, url): 301 self.url = url 302 303 def getPageURL(self): 304 305 "Return the URL of this page." 306 307 return self.url 308 309 def getFormat(self): 310 311 "Get the format used by this resource." 312 313 return "plain" 314 315 def getMetadata(self): 316 317 """ 318 Return a dictionary containing items describing the page's "created" 319 time, "last-modified" time, "sequence" (or revision number) and the 320 "last-comment" made about the last edit. 321 """ 322 323 return {} 324 325 def getEvents(self): 326 327 "Return a list of events from this resource." 328 329 return [] 330 331 def linkToPage(self, request, text, query_string=None): 332 333 """ 334 Using 'request', return a link to this page with the given link 'text' 335 and optional 'query_string'. 336 """ 337 338 return linkToResource(self.url, request, text, query_string) 339 340 # Formatting-related functions. 341 342 def formatText(self, text, request, fmt): 343 344 """ 345 Format the given 'text' using the specified 'request' and formatter 346 'fmt'. 347 """ 348 349 # Assume plain text which is then formatted appropriately. 350 351 return fmt.text(text) 352 353 class EventCalendar(EventResource): 354 355 "An iCalendar resource." 356 357 def __init__(self, url, calendar): 358 EventResource.__init__(self, url) 359 self.calendar = calendar 360 self.events = None 361 362 def getEvents(self): 363 364 "Return a list of events from this resource." 365 366 if self.events is None: 367 self.events = [] 368 369 _calendar, _empty, calendar = self.calendar 370 371 for objtype, attrs, obj in calendar: 372 373 # Read events. 374 375 if objtype == "VEVENT": 376 details = {} 377 378 for property, attrs, value in obj: 379 380 # Convert dates. 381 382 if property in ("DTSTART", "DTEND", "CREATED", "DTSTAMP", "LAST-MODIFIED"): 383 if property in ("DTSTART", "DTEND"): 384 property = property[2:] 385 if attrs.get("VALUE") == "DATE": 386 value = getDateFromCalendar(value) 387 else: 388 value = getDateTimeFromCalendar(value) 389 390 # Convert numeric data. 391 392 elif property == "SEQUENCE": 393 value = int(value) 394 395 # Convert lists. 396 397 elif property == "CATEGORIES": 398 value = to_list(value, ",") 399 400 # Convert positions (using decimal values). 401 402 elif property == "GEO": 403 try: 404 value = map(getMapReferenceFromDecimal, to_list(value, ";")) 405 if len(value) != 2: 406 continue 407 except (KeyError, ValueError): 408 continue 409 410 # Accept other textual data as it is. 411 412 elif property in ("LOCATION", "SUMMARY", "URL"): 413 value = value or None 414 415 # Ignore other properties. 416 417 else: 418 continue 419 420 property = property.lower() 421 details[property] = value 422 423 self.events.append(CalendarEvent(self, details)) 424 425 return self.events 426 427 class EventPage: 428 429 "An event page acting as an event resource." 430 431 def __init__(self, page): 432 self.page = page 433 self.events = None 434 self.body = None 435 self.categories = None 436 self.metadata = None 437 438 def copyPage(self, page): 439 440 "Copy the body of the given 'page'." 441 442 self.body = page.getBody() 443 444 def getPageURL(self): 445 446 "Return the URL of this page." 447 448 return getPageURL(self.page) 449 450 def getFormat(self): 451 452 "Get the format used on this page." 453 454 return getFormat(self.page) 455 456 def getMetadata(self): 457 458 """ 459 Return a dictionary containing items describing the page's "created" 460 time, "last-modified" time, "sequence" (or revision number) and the 461 "last-comment" made about the last edit. 462 """ 463 464 if self.metadata is None: 465 self.metadata = getMetadata(self.page) 466 return self.metadata 467 468 def getRevisions(self): 469 470 "Return a list of page revisions." 471 472 return self.page.getRevList() 473 474 def getPageRevision(self): 475 476 "Return the revision details dictionary for this page." 477 478 return getPageRevision(self.page) 479 480 def getPageName(self): 481 482 "Return the page name." 483 484 return self.page.page_name 485 486 def getPrettyPageName(self): 487 488 "Return a nicely formatted title/name for this page." 489 490 return getPrettyPageName(self.page) 491 492 def getBody(self): 493 494 "Get the current page body." 495 496 if self.body is None: 497 self.body = self.page.get_raw_body() 498 return self.body 499 500 def getEvents(self): 501 502 "Return a list of events from this page." 503 504 if self.events is None: 505 details = {} 506 self.events = [Event(self, details)] 507 508 if self.getFormat() == "wiki": 509 for match in definition_list_regexp.finditer(self.getBody()): 510 511 # Skip commented-out items. 512 513 if match.group("optcomment"): 514 continue 515 516 # Permit case-insensitive list terms. 517 518 term = match.group("term").lower() 519 desc = match.group("desc") 520 521 # Special value type handling. 522 523 # Dates. 524 525 if term in ("start", "end"): 526 desc = getDateTime(desc) 527 528 # Lists (whose elements may be quoted). 529 530 elif term in ("topics", "categories"): 531 desc = map(getSimpleWikiText, to_list(desc, ",")) 532 533 # Position details. 534 535 elif term == "geo": 536 try: 537 desc = map(getMapReference, to_list(desc, None)) 538 if len(desc) != 2: 539 continue 540 except (KeyError, ValueError): 541 continue 542 543 # Labels which may well be quoted. 544 545 elif term in ("title", "summary", "description", "location"): 546 desc = getSimpleWikiText(desc.strip()) 547 548 if desc is not None: 549 550 # Handle apparent duplicates by creating a new set of 551 # details. 552 553 if details.has_key(term): 554 555 # Make a new event. 556 557 details = {} 558 self.events.append(Event(self, details)) 559 560 details[term] = desc 561 562 return self.events 563 564 def setEvents(self, events): 565 566 "Set the given 'events' on this page." 567 568 self.events = events 569 570 def getCategoryMembership(self): 571 572 "Get the category names from this page." 573 574 if self.categories is None: 575 body = self.getBody() 576 match = category_membership_regexp.search(body) 577 self.categories = match and [x for x in match.groups() if x] or [] 578 579 return self.categories 580 581 def setCategoryMembership(self, category_names): 582 583 """ 584 Set the category membership for the page using the specified 585 'category_names'. 586 """ 587 588 self.categories = category_names 589 590 def flushEventDetails(self): 591 592 "Flush the current event details to this page's body text." 593 594 new_body_parts = [] 595 end_of_last_match = 0 596 body = self.getBody() 597 598 events = iter(self.getEvents()) 599 600 event = events.next() 601 event_details = event.getDetails() 602 replaced_terms = set() 603 604 for match in definition_list_regexp.finditer(body): 605 606 # Permit case-insensitive list terms. 607 608 term = match.group("term").lower() 609 desc = match.group("desc") 610 611 # Check that the term has not already been substituted. If so, 612 # get the next event. 613 614 if term in replaced_terms: 615 try: 616 event = events.next() 617 618 # No more events. 619 620 except StopIteration: 621 break 622 623 event_details = event.getDetails() 624 replaced_terms = set() 625 626 # Add preceding text to the new body. 627 628 new_body_parts.append(body[end_of_last_match:match.start()]) 629 630 # Get the matching regions, adding the term to the new body. 631 632 new_body_parts.append(match.group("wholeterm")) 633 634 # Special value type handling. 635 636 if event_details.has_key(term): 637 638 # Dates. 639 640 if term in ("start", "end"): 641 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 642 643 # Lists (whose elements may be quoted). 644 645 elif term in ("topics", "categories"): 646 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 647 648 # Labels which must be quoted. 649 650 elif term in ("title", "summary"): 651 desc = getEncodedWikiText(event_details[term]) 652 653 # Position details. 654 655 elif term == "geo": 656 desc = " ".join(map(str, event_details[term])) 657 658 # Text which need not be quoted, but it will be Wiki text. 659 660 elif term in ("description", "link", "location"): 661 desc = event_details[term] 662 663 replaced_terms.add(term) 664 665 # Add the replaced value. 666 667 new_body_parts.append(desc) 668 669 # Remember where in the page has been processed. 670 671 end_of_last_match = match.end() 672 673 # Write the rest of the page. 674 675 new_body_parts.append(body[end_of_last_match:]) 676 677 self.body = "".join(new_body_parts) 678 679 def flushCategoryMembership(self): 680 681 "Flush the category membership to the page body." 682 683 body = self.getBody() 684 category_names = self.getCategoryMembership() 685 match = category_membership_regexp.search(body) 686 687 if match: 688 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 689 690 def saveChanges(self): 691 692 "Save changes to the event." 693 694 self.flushEventDetails() 695 self.flushCategoryMembership() 696 self.page.saveText(self.getBody(), 0) 697 698 def linkToPage(self, request, text, query_string=None): 699 700 """ 701 Using 'request', return a link to this page with the given link 'text' 702 and optional 'query_string'. 703 """ 704 705 return linkToPage(request, self.page, text, query_string) 706 707 # Formatting-related functions. 708 709 def getParserClass(self, request, format): 710 711 """ 712 Return a parser class using the 'request' for the given 'format', returning 713 a plain text parser if no parser can be found for the specified 'format'. 714 """ 715 716 try: 717 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 718 except wikiutil.PluginMissingError: 719 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 720 721 def formatText(self, text, request, fmt): 722 723 """ 724 Format the given 'text' using the specified 'request' and formatter 725 'fmt'. 726 """ 727 728 fmt.page = self.page 729 730 # Suppress line anchors. 731 732 parser_cls = self.getParserClass(request, self.getFormat()) 733 parser = parser_cls(text, request, line_anchors=False) 734 735 # Fix lists by indicating that a paragraph is already started. 736 737 return request.redirectedOutput(parser.format, fmt, inhibit_p=True) 738 739 # Event details. 740 741 class Event(ActsAsTimespan): 742 743 "A description of an event." 744 745 def __init__(self, page, details): 746 self.page = page 747 self.details = details 748 749 # Permit omission of the end of the event by duplicating the start. 750 751 if self.details.has_key("start") and not self.details.get("end"): 752 end = self.details["start"] 753 754 # Make any end time refer to the day instead. 755 756 if isinstance(end, DateTime): 757 end = end.as_date() 758 759 self.details["end"] = end 760 761 def __repr__(self): 762 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 763 764 def __hash__(self): 765 766 """ 767 Return a dictionary hash, avoiding mistaken equality of events in some 768 situations (notably membership tests) by including the URL as well as 769 the summary. 770 """ 771 772 return hash(self.getSummary() + self.getEventURL()) 773 774 def getPage(self): 775 776 "Return the page describing this event." 777 778 return self.page 779 780 def setPage(self, page): 781 782 "Set the 'page' describing this event." 783 784 self.page = page 785 786 def getEventURL(self): 787 788 "Return the URL of this event." 789 790 return self.page.getPageURL() 791 792 def linkToEvent(self, request, text, query_string=None): 793 794 """ 795 Using 'request', return a link to this event with the given link 'text' 796 and optional 'query_string'. 797 """ 798 799 return self.page.linkToPage(request, text, query_string) 800 801 def getMetadata(self): 802 803 """ 804 Return a dictionary containing items describing the event's "created" 805 time, "last-modified" time, "sequence" (or revision number) and the 806 "last-comment" made about the last edit. 807 """ 808 809 # Delegate this to the page. 810 811 return self.page.getMetadata() 812 813 def getSummary(self, event_parent=None): 814 815 """ 816 Return either the given title or summary of the event according to the 817 event details, or a summary made from using the pretty version of the 818 page name. 819 820 If the optional 'event_parent' is specified, any page beneath the given 821 'event_parent' page in the page hierarchy will omit this parent information 822 if its name is used as the summary. 823 """ 824 825 event_details = self.details 826 827 if event_details.has_key("title"): 828 return event_details["title"] 829 elif event_details.has_key("summary"): 830 return event_details["summary"] 831 else: 832 # If appropriate, remove the parent details and "/" character. 833 834 title = self.page.getPageName() 835 836 if event_parent and title.startswith(event_parent): 837 title = title[len(event_parent.rstrip("/")) + 1:] 838 839 return getPrettyTitle(title) 840 841 def getDetails(self): 842 843 "Return the details for this event." 844 845 return self.details 846 847 def setDetails(self, event_details): 848 849 "Set the 'event_details' for this event." 850 851 self.details = event_details 852 853 # Timespan-related methods. 854 855 def __contains__(self, other): 856 return self == other 857 858 def __eq__(self, other): 859 if isinstance(other, Event): 860 return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other) 861 else: 862 return self._cmp(other) == 0 863 864 def __ne__(self, other): 865 return not self.__eq__(other) 866 867 def __lt__(self, other): 868 return self._cmp(other) == -1 869 870 def __le__(self, other): 871 return self._cmp(other) in (-1, 0) 872 873 def __gt__(self, other): 874 return self._cmp(other) == 1 875 876 def __ge__(self, other): 877 return self._cmp(other) in (0, 1) 878 879 def _cmp(self, other): 880 881 "Compare this event to an 'other' event purely by their timespans." 882 883 if isinstance(other, Event): 884 return cmp(self.as_timespan(), other.as_timespan()) 885 else: 886 return cmp(self.as_timespan(), other) 887 888 def as_timespan(self): 889 details = self.details 890 if details.has_key("start") and details.has_key("end"): 891 return Timespan(details["start"], details["end"]) 892 else: 893 return None 894 895 def as_limits(self): 896 ts = self.as_timespan() 897 return ts and ts.as_limits() 898 899 class CalendarEvent(Event): 900 901 "An event from a remote calendar." 902 903 def getEventURL(self): 904 905 "Return the URL of this event." 906 907 return self.details.get("url") or self.page.getPageURL() 908 909 def linkToEvent(self, request, text, query_string=None): 910 911 """ 912 Using 'request', return a link to this event with the given link 'text' 913 and optional 'query_string'. 914 """ 915 916 return linkToResource(self.getEventURL(), request, text, query_string) 917 918 def getMetadata(self): 919 920 """ 921 Return a dictionary containing items describing the event's "created" 922 time, "last-modified" time, "sequence" (or revision number) and the 923 "last-comment" made about the last edit. 924 """ 925 926 return { 927 "created" : self.details.get("created") or self.details["dtstamp"], 928 "last-modified" : self.details.get("last-modified") or self.details["dtstamp"], 929 "sequence" : self.details.get("sequence") or 0, 930 "last-comment" : "" 931 } 932 933 # Obtaining event containers and events from such containers. 934 935 def getEventPages(pages): 936 937 "Return a list of events found on the given 'pages'." 938 939 # Get real pages instead of result pages. 940 941 return map(EventPage, pages) 942 943 def getAllEventSources(request): 944 945 "Return all event sources defined in the Wiki using the 'request'." 946 947 sources_page = getattr(request.cfg, "event_aggregator_sources_page", "EventSourcesDict") 948 949 # Remote sources are accessed via dictionary page definitions. 950 951 return getWikiDict(sources_page, request) 952 953 def getEventResources(sources, calendar_start, calendar_end, request): 954 955 """ 956 Return resource objects for the given 'sources' using the given 957 'calendar_start' and 'calendar_end' to parameterise requests to the sources, 958 and the 'request' to access configuration settings in the Wiki. 959 """ 960 961 sources_dict = getAllEventSources(request) 962 if not sources_dict: 963 return [] 964 965 # Use dates for the calendar limits. 966 967 if isinstance(calendar_start, Date): 968 pass 969 elif isinstance(calendar_start, Month): 970 calendar_start = calendar_start.as_date(1) 971 972 if isinstance(calendar_end, Date): 973 pass 974 elif isinstance(calendar_end, Month): 975 calendar_end = calendar_end.as_date(-1) 976 977 resources = [] 978 979 for source in sources: 980 try: 981 details = sources_dict[source].split() 982 url = details[0] 983 format = (details[1:] or ["ical"])[0] 984 except (KeyError, ValueError): 985 pass 986 else: 987 # Prevent local file access. 988 989 if url.startswith("file:"): 990 continue 991 992 # Parameterise the URL. 993 # Where other parameters are used, care must be taken to encode them 994 # properly. 995 996 url = url.replace("{start}", urllib.quote_plus(calendar_start and str(calendar_start) or "")) 997 url = url.replace("{end}", urllib.quote_plus(calendar_end and str(calendar_end) or "")) 998 999 # Get a parser. 1000 # NOTE: This could be done reactively by choosing a parser based on 1001 # NOTE: the content type provided by the URL. 1002 1003 if format == "ical" and vCalendar is not None: 1004 parser = vCalendar.parse 1005 resource_cls = EventCalendar 1006 required_content_type = "text/calendar" 1007 else: 1008 continue 1009 1010 # See if the URL is cached. 1011 1012 cache_key = cache.key(request, content=url) 1013 cache_entry = caching.CacheEntry(request, "EventAggregator", cache_key, scope='wiki') 1014 1015 # If no entry exists, or if the entry is older than a certain age 1016 # (5 minutes by default), create one with the response from the URL. 1017 1018 now = time.time() 1019 mtime = cache_entry.mtime() 1020 max_cache_age = int(getattr(request.cfg, "event_aggregator_max_cache_age", "300")) 1021 1022 # NOTE: The URL could be checked and the 'If-Modified-Since' header 1023 # NOTE: (see MoinMoin.action.pollsistersites) could be checked. 1024 1025 if not cache_entry.exists() or now - mtime >= max_cache_age: 1026 1027 # Access the remote data source. 1028 1029 cache_entry.open(mode="w") 1030 1031 try: 1032 f = urllib2.urlopen(url) 1033 try: 1034 cache_entry.write(url + "\n") 1035 cache_entry.write((f.headers.get("content-type") or "") + "\n") 1036 cache_entry.write(f.read()) 1037 finally: 1038 cache_entry.close() 1039 f.close() 1040 1041 # In case of an exception, just ignore the remote source. 1042 # NOTE: This could be reported somewhere. 1043 1044 except IOError: 1045 if cache_entry.exists(): 1046 cache_entry.remove() 1047 continue 1048 1049 # Open the cache entry and read it. 1050 1051 cache_entry.open() 1052 try: 1053 data = cache_entry.read() 1054 finally: 1055 cache_entry.close() 1056 1057 # Process the entry, parsing the content. 1058 1059 f = StringIO(data) 1060 try: 1061 url = f.readline() 1062 1063 # Get the content type and encoding, making sure that the data 1064 # can be parsed. 1065 1066 content_type, encoding = getContentTypeAndEncoding(f.readline()) 1067 if content_type != required_content_type: 1068 continue 1069 1070 # Send the data to the parser. 1071 1072 uf = codecs.getreader(encoding or "utf-8")(f) 1073 try: 1074 resources.append(resource_cls(url, parser(uf))) 1075 finally: 1076 uf.close() 1077 finally: 1078 f.close() 1079 1080 return resources 1081 1082 def getEventsFromResources(resources): 1083 1084 "Return a list of events supplied by the given event 'resources'." 1085 1086 events = [] 1087 1088 for resource in resources: 1089 1090 # Get all events described by the resource. 1091 1092 for event in resource.getEvents(): 1093 1094 # Remember the event. 1095 1096 events.append(event) 1097 1098 return events 1099 1100 # Event filtering and limits. 1101 1102 def getEventsInPeriod(events, calendar_period): 1103 1104 """ 1105 Return a collection containing those of the given 'events' which occur 1106 within the given 'calendar_period'. 1107 """ 1108 1109 all_shown_events = [] 1110 1111 for event in events: 1112 1113 # Test for the suitability of the event. 1114 1115 if event.as_timespan() is not None: 1116 1117 # Compare the dates to the requested calendar window, if any. 1118 1119 if event in calendar_period: 1120 all_shown_events.append(event) 1121 1122 return all_shown_events 1123 1124 def getEventLimits(events): 1125 1126 "Return the earliest and latest of the given 'events'." 1127 1128 earliest = None 1129 latest = None 1130 1131 for event in events: 1132 1133 # Test for the suitability of the event. 1134 1135 if event.as_timespan() is not None: 1136 ts = event.as_timespan() 1137 if earliest is None or ts.start < earliest: 1138 earliest = ts.start 1139 if latest is None or ts.end > latest: 1140 latest = ts.end 1141 1142 return earliest, latest 1143 1144 def setEventTimestamps(request, events): 1145 1146 """ 1147 Using 'request', set timestamp details in the details dictionary of each of 1148 the 'events'. 1149 1150 Return the latest timestamp found. 1151 """ 1152 1153 latest = None 1154 1155 for event in events: 1156 event_details = event.getDetails() 1157 1158 # Populate the details with event metadata. 1159 1160 event_details.update(event.getMetadata()) 1161 1162 if latest is None or latest < event_details["last-modified"]: 1163 latest = event_details["last-modified"] 1164 1165 return latest 1166 1167 def getOrderedEvents(events): 1168 1169 """ 1170 Return a list with the given 'events' ordered according to their start and 1171 end dates. 1172 """ 1173 1174 ordered_events = events[:] 1175 ordered_events.sort() 1176 return ordered_events 1177 1178 def getCalendarPeriod(calendar_start, calendar_end): 1179 1180 """ 1181 Return a calendar period for the given 'calendar_start' and 'calendar_end'. 1182 These parameters can be given as None. 1183 """ 1184 1185 # Re-order the window, if appropriate. 1186 1187 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 1188 calendar_start, calendar_end = calendar_end, calendar_start 1189 1190 return Timespan(calendar_start, calendar_end) 1191 1192 def getConcretePeriod(calendar_start, calendar_end, earliest, latest, resolution): 1193 1194 """ 1195 From the requested 'calendar_start' and 'calendar_end', which may be None, 1196 indicating that no restriction is imposed on the period for each of the 1197 boundaries, use the 'earliest' and 'latest' event months to define a 1198 specific period of interest. 1199 """ 1200 1201 # Define the period as starting with any specified start month or the 1202 # earliest event known, ending with any specified end month or the latest 1203 # event known. 1204 1205 first = calendar_start or earliest 1206 last = calendar_end or latest 1207 1208 # If there is no range of months to show, perhaps because there are no 1209 # events in the requested period, and there was no start or end month 1210 # specified, show only the month indicated by the start or end of the 1211 # requested period. If all events were to be shown but none were found show 1212 # the current month. 1213 1214 if resolution == "date": 1215 get_current = getCurrentDate 1216 else: 1217 get_current = getCurrentMonth 1218 1219 if first is None: 1220 first = last or get_current() 1221 if last is None: 1222 last = first or get_current() 1223 1224 if resolution == "month": 1225 first = first.as_month() 1226 last = last.as_month() 1227 1228 # Permit "expiring" periods (where the start date approaches the end date). 1229 1230 return min(first, last), last 1231 1232 def getCoverage(events, resolution="date"): 1233 1234 """ 1235 Determine the coverage of the given 'events', returning a collection of 1236 timespans, along with a dictionary mapping locations to collections of 1237 slots, where each slot contains a tuple of the form (timespans, events). 1238 """ 1239 1240 all_events = {} 1241 full_coverage = TimespanCollection(resolution) 1242 1243 # Get event details. 1244 1245 for event in events: 1246 event_details = event.getDetails() 1247 1248 # Find the coverage of this period for the event. 1249 1250 # For day views, each location has its own slot, but for month 1251 # views, all locations are pooled together since having separate 1252 # slots for each location can lead to poor usage of vertical space. 1253 1254 if resolution == "datetime": 1255 event_location = event_details.get("location") 1256 else: 1257 event_location = None 1258 1259 # Update the overall coverage. 1260 1261 full_coverage.insert_in_order(event) 1262 1263 # Add a new events list for a new location. 1264 # Locations can be unspecified, thus None refers to all unlocalised 1265 # events. 1266 1267 if not all_events.has_key(event_location): 1268 all_events[event_location] = [TimespanCollection(resolution, [event])] 1269 1270 # Try and fit the event into an events list. 1271 1272 else: 1273 slot = all_events[event_location] 1274 1275 for slot_events in slot: 1276 1277 # Where the event does not overlap with the events in the 1278 # current collection, add it alongside these events. 1279 1280 if not event in slot_events: 1281 slot_events.insert_in_order(event) 1282 break 1283 1284 # Make a new element in the list if the event cannot be 1285 # marked alongside existing events. 1286 1287 else: 1288 slot.append(TimespanCollection(resolution, [event])) 1289 1290 return full_coverage, all_events 1291 1292 def getCoverageScale(coverage): 1293 1294 """ 1295 Return a scale for the given coverage so that the times involved are 1296 exposed. The scale consists of a list of non-overlapping timespans forming 1297 a contiguous period of time. 1298 """ 1299 1300 times = set() 1301 for timespan in coverage: 1302 start, end = timespan.as_limits() 1303 1304 # Add either genuine times or dates converted to times. 1305 1306 if isinstance(start, DateTime): 1307 times.add(start) 1308 else: 1309 times.add(start.as_start_of_day()) 1310 1311 if isinstance(end, DateTime): 1312 times.add(end) 1313 else: 1314 times.add(end.as_date().next_day()) 1315 1316 times = list(times) 1317 times.sort(cmp_dates_as_day_start) 1318 1319 scale = [] 1320 first = 1 1321 start = None 1322 for time in times: 1323 if not first: 1324 scale.append(Timespan(start, time)) 1325 else: 1326 first = 0 1327 start = time 1328 1329 return scale 1330 1331 def getCountry(s): 1332 1333 "Find a country code in the given string 's'." 1334 1335 match = country_code_regexp.search(s) 1336 1337 if match: 1338 return match.group("code") 1339 else: 1340 return None 1341 1342 # Page-related functions. 1343 1344 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1345 1346 """ 1347 Using the given 'template_page', complete the 'new_page' by copying the 1348 template and adding the given 'event_details' (a dictionary of event 1349 fields), setting also the 'category_pagenames' to define category 1350 membership. 1351 """ 1352 1353 event_page = EventPage(template_page) 1354 new_event_page = EventPage(new_page) 1355 new_event_page.copyPage(event_page) 1356 1357 if new_event_page.getFormat() == "wiki": 1358 new_event = Event(new_event_page, event_details) 1359 new_event_page.setEvents([new_event]) 1360 new_event_page.setCategoryMembership(category_pagenames) 1361 new_event_page.flushEventDetails() 1362 1363 return new_event_page.getBody() 1364 1365 # Colour-related functions. 1366 1367 def getColour(s): 1368 colour = [0, 0, 0] 1369 digit = 0 1370 for c in s: 1371 colour[digit] += ord(c) 1372 colour[digit] = colour[digit] % 256 1373 digit += 1 1374 digit = digit % 3 1375 return tuple(colour) 1376 1377 def getBlackOrWhite(colour): 1378 if sum(colour) / 3.0 > 127: 1379 return (0, 0, 0) 1380 else: 1381 return (255, 255, 255) 1382 1383 # User interface abstractions. 1384 1385 class View: 1386 1387 "A view of the event calendar." 1388 1389 def __init__(self, page, calendar_name, raw_calendar_start, raw_calendar_end, 1390 original_calendar_start, original_calendar_end, calendar_start, calendar_end, 1391 first, last, category_names, remote_sources, template_name, parent_name, mode, 1392 resolution, name_usage, map_name): 1393 1394 """ 1395 Initialise the view with the current 'page', a 'calendar_name' (which 1396 may be None), the 'raw_calendar_start' and 'raw_calendar_end' (which 1397 are the actual start and end values provided by the request), the 1398 calculated 'original_calendar_start' and 'original_calendar_end' (which 1399 are the result of calculating the calendar's limits from the raw start 1400 and end values), and the requested, calculated 'calendar_start' and 1401 'calendar_end' (which may involve different start and end values due to 1402 navigation in the user interface), along with the 'first' and 'last' 1403 months of event coverage. 1404 1405 The additional 'category_names', 'remote_sources', 'template_name', 1406 'parent_name' and 'mode' parameters are used to configure the links 1407 employed by the view. 1408 1409 The 'resolution' affects the view for certain modes and is also used to 1410 parameterise links. 1411 1412 The 'name_usage' parameter controls how names are shown on calendar mode 1413 events, such as how often labels are repeated. 1414 1415 The 'map_name' parameter provides the name of a map to be used in the 1416 map mode. 1417 """ 1418 1419 self.page = page 1420 self.calendar_name = calendar_name 1421 self.raw_calendar_start = raw_calendar_start 1422 self.raw_calendar_end = raw_calendar_end 1423 self.original_calendar_start = original_calendar_start 1424 self.original_calendar_end = original_calendar_end 1425 self.calendar_start = calendar_start 1426 self.calendar_end = calendar_end 1427 self.template_name = template_name 1428 self.parent_name = parent_name 1429 self.mode = mode 1430 self.resolution = resolution 1431 self.name_usage = name_usage 1432 self.map_name = map_name 1433 1434 self.category_name_parameters = "&".join([("category=%s" % name) for name in category_names]) 1435 self.remote_source_parameters = "&".join([("source=%s" % source) for source in remote_sources]) 1436 1437 # Calculate the duration in terms of the highest common unit of time. 1438 1439 self.first = first 1440 self.last = last 1441 self.duration = last - first 1442 1443 if self.calendar_name is not None: 1444 1445 # Store the view parameters. 1446 1447 self.previous_start = first.previous() 1448 self.next_start = first.next() 1449 self.previous_end = last.previous() 1450 self.next_end = last.next() 1451 1452 self.previous_set_start = first.update(-self.duration) 1453 self.next_set_start = first.update(self.duration) 1454 self.previous_set_end = last.update(-self.duration) 1455 self.next_set_end = last.update(self.duration) 1456 1457 def getIdentifier(self): 1458 1459 "Return a unique identifier to be used to refer to this view." 1460 1461 # NOTE: Nasty hack to get a unique identifier if no name is given. 1462 1463 return self.calendar_name or str(id(self)) 1464 1465 def getQualifiedParameterName(self, argname): 1466 1467 "Return the 'argname' qualified using the calendar name." 1468 1469 return getQualifiedParameterName(self.calendar_name, argname) 1470 1471 def getDateQueryString(self, argname, date, prefix=1): 1472 1473 """ 1474 Return a query string fragment for the given 'argname', referring to the 1475 month given by the specified 'year_month' object, appropriate for this 1476 calendar. 1477 1478 If 'prefix' is specified and set to a false value, the parameters in the 1479 query string will not be calendar-specific, but could be used with the 1480 summary action. 1481 """ 1482 1483 suffixes = ["year", "month", "day"] 1484 1485 if date is not None: 1486 args = [] 1487 for suffix, value in zip(suffixes, date.as_tuple()): 1488 suffixed_argname = "%s-%s" % (argname, suffix) 1489 if prefix: 1490 suffixed_argname = self.getQualifiedParameterName(suffixed_argname) 1491 args.append("%s=%s" % (suffixed_argname, value)) 1492 return "&".join(args) 1493 else: 1494 return "" 1495 1496 def getRawDateQueryString(self, argname, date, prefix=1): 1497 1498 """ 1499 Return a query string fragment for the given 'argname', referring to the 1500 date given by the specified 'date' value, appropriate for this 1501 calendar. 1502 1503 If 'prefix' is specified and set to a false value, the parameters in the 1504 query string will not be calendar-specific, but could be used with the 1505 summary action. 1506 """ 1507 1508 if date is not None: 1509 if prefix: 1510 argname = self.getQualifiedParameterName(argname) 1511 return "%s=%s" % (argname, wikiutil.url_quote_plus(date)) 1512 else: 1513 return "" 1514 1515 def getNavigationLink(self, start, end, mode=None, resolution=None): 1516 1517 """ 1518 Return a query string fragment for navigation to a view showing months 1519 from 'start' to 'end' inclusive, with the optional 'mode' indicating the 1520 view style and the optional 'resolution' indicating the resolution of a 1521 view, if configurable. 1522 """ 1523 1524 return "%s&%s&%s=%s&%s=%s" % ( 1525 self.getRawDateQueryString("start", start), 1526 self.getRawDateQueryString("end", end), 1527 self.getQualifiedParameterName("mode"), mode or self.mode, 1528 self.getQualifiedParameterName("resolution"), resolution or self.resolution 1529 ) 1530 1531 def getUpdateLink(self, start, end, mode=None, resolution=None): 1532 1533 """ 1534 Return a query string fragment for navigation to a view showing months 1535 from 'start' to 'end' inclusive, with the optional 'mode' indicating the 1536 view style and the optional 'resolution' indicating the resolution of a 1537 view, if configurable. This link differs from the conventional 1538 navigation link in that it is sufficient to activate the update action 1539 and produce an updated region of the page without needing to locate and 1540 process the page or any macro invocation. 1541 """ 1542 1543 parameters = [ 1544 self.getRawDateQueryString("start", start, 0), 1545 self.getRawDateQueryString("end", end, 0), 1546 self.category_name_parameters, 1547 self.remote_source_parameters, 1548 ] 1549 1550 pairs = [ 1551 ("calendar", self.calendar_name or ""), 1552 ("calendarstart", self.raw_calendar_start or ""), 1553 ("calendarend", self.raw_calendar_end or ""), 1554 ("mode", mode or self.mode), 1555 ("resolution", resolution or self.resolution), 1556 ("parent", self.parent_name or ""), 1557 ("template", self.template_name or ""), 1558 ("names", self.name_usage), 1559 ("map", self.map_name or ""), 1560 ] 1561 1562 url = self.page.url(self.page.request, 1563 "action=EventAggregatorUpdate&%s" % ( 1564 "&".join([("%s=%s" % pair) for pair in pairs] + parameters) 1565 ), relative=True) 1566 1567 return "return replaceCalendar('EventAggregator-%s', '%s')" % (self.getIdentifier(), url) 1568 1569 def getNewEventLink(self, start): 1570 1571 """ 1572 Return a query string activating the new event form, incorporating the 1573 calendar parameters, specialising the form for the given 'start' date or 1574 month. 1575 """ 1576 1577 if start is not None: 1578 details = start.as_tuple() 1579 pairs = zip(["start-year=%d", "start-month=%d", "start-day=%d"], details) 1580 args = [(param % value) for (param, value) in pairs] 1581 args = "&".join(args) 1582 else: 1583 args = "" 1584 1585 # Prepare navigation details for the calendar shown with the new event 1586 # form. 1587 1588 navigation_link = self.getNavigationLink( 1589 self.calendar_start, self.calendar_end 1590 ) 1591 1592 return "action=EventAggregatorNewEvent&%s&%s&template=%s&parent=%s&%s" % ( 1593 args, self.category_name_parameters, self.template_name, self.parent_name or "", 1594 navigation_link) 1595 1596 def getFullDateLabel(self, date): 1597 page = self.page 1598 request = page.request 1599 return getFullDateLabel(request, date) 1600 1601 def getFullMonthLabel(self, year_month): 1602 page = self.page 1603 request = page.request 1604 return getFullMonthLabel(request, year_month) 1605 1606 def getFullLabel(self, arg): 1607 return self.resolution == "date" and self.getFullDateLabel(arg) or self.getFullMonthLabel(arg) 1608 1609 def _getCalendarPeriod(self, start_label, end_label, default_label): 1610 output = [] 1611 if start_label: 1612 output.append(start_label) 1613 if end_label and start_label != end_label: 1614 if output: 1615 output.append(" - ") 1616 output.append(end_label) 1617 return "".join(output) or default_label 1618 1619 def getCalendarPeriod(self): 1620 _ = self.page.request.getText 1621 return self._getCalendarPeriod( 1622 self.calendar_start and self.getFullLabel(self.calendar_start), 1623 self.calendar_end and self.getFullLabel(self.calendar_end), 1624 _("All events") 1625 ) 1626 1627 def getOriginalCalendarPeriod(self): 1628 _ = self.page.request.getText 1629 return self._getCalendarPeriod( 1630 self.original_calendar_start and self.getFullLabel(self.original_calendar_start), 1631 self.original_calendar_end and self.getFullLabel(self.original_calendar_end), 1632 _("All events") 1633 ) 1634 1635 def getRawCalendarPeriod(self): 1636 _ = self.page.request.getText 1637 return self._getCalendarPeriod( 1638 self.raw_calendar_start, 1639 self.raw_calendar_end, 1640 _("No period specified") 1641 ) 1642 1643 def writeDownloadControls(self): 1644 1645 """ 1646 Return a representation of the download controls, featuring links for 1647 view, calendar and customised downloads and subscriptions. 1648 """ 1649 1650 page = self.page 1651 request = page.request 1652 fmt = request.formatter 1653 _ = request.getText 1654 1655 output = [] 1656 1657 # The full URL is needed for webcal links. 1658 1659 full_url = "%s%s" % (request.getBaseURL(), getPathInfo(request)) 1660 1661 # Generate the links. 1662 1663 download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s&%s&%s" % ( 1664 self.parent_name or "", 1665 self.resolution, 1666 self.category_name_parameters, 1667 self.remote_source_parameters 1668 ) 1669 download_all_link = download_dialogue_link + "&doit=1" 1670 download_link = download_all_link + ("&%s&%s" % ( 1671 self.getDateQueryString("start", self.calendar_start, prefix=0), 1672 self.getDateQueryString("end", self.calendar_end, prefix=0) 1673 )) 1674 1675 # Subscription links just explicitly select the RSS format. 1676 1677 subscribe_dialogue_link = download_dialogue_link + "&format=RSS" 1678 subscribe_all_link = download_all_link + "&format=RSS" 1679 subscribe_link = download_link + "&format=RSS" 1680 1681 # Adjust the "download all" and "subscribe all" links if the calendar 1682 # has an inherent period associated with it. 1683 1684 period_limits = [] 1685 1686 if self.raw_calendar_start: 1687 period_limits.append("&%s" % 1688 self.getRawDateQueryString("start", self.raw_calendar_start, prefix=0) 1689 ) 1690 if self.raw_calendar_end: 1691 period_limits.append("&%s" % 1692 self.getRawDateQueryString("end", self.raw_calendar_end, prefix=0) 1693 ) 1694 1695 period_limits = "".join(period_limits) 1696 1697 download_dialogue_link += period_limits 1698 download_all_link += period_limits 1699 subscribe_dialogue_link += period_limits 1700 subscribe_all_link += period_limits 1701 1702 # Pop-up descriptions of the downloadable calendars. 1703 1704 calendar_period = self.getCalendarPeriod() 1705 original_calendar_period = self.getOriginalCalendarPeriod() 1706 raw_calendar_period = self.getRawCalendarPeriod() 1707 1708 # Write the controls. 1709 1710 # Download controls. 1711 1712 output.append(fmt.div(on=1, css_class="event-download-controls")) 1713 1714 output.append(fmt.span(on=1, css_class="event-download")) 1715 output.append(fmt.text(_("Download..."))) 1716 output.append(fmt.div(on=1, css_class="event-download-popup")) 1717 1718 output.append(fmt.div(on=1, css_class="event-download-item")) 1719 output.append(fmt.span(on=1, css_class="event-download-types")) 1720 output.append(fmt.span(on=1, css_class="event-download-webcal")) 1721 output.append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_link)) 1722 output.append(fmt.span(on=0)) 1723 output.append(fmt.span(on=1, css_class="event-download-http")) 1724 output.append(linkToPage(request, page, _("http"), download_link)) 1725 output.append(fmt.span(on=0)) 1726 output.append(fmt.span(on=0)) # end types 1727 output.append(fmt.span(on=1, css_class="event-download-label")) 1728 output.append(fmt.text(_("Download this view"))) 1729 output.append(fmt.span(on=0)) # end label 1730 output.append(fmt.span(on=1, css_class="event-download-period")) 1731 output.append(fmt.text(calendar_period)) 1732 output.append(fmt.span(on=0)) 1733 output.append(fmt.div(on=0)) 1734 1735 output.append(fmt.div(on=1, css_class="event-download-item")) 1736 output.append(fmt.span(on=1, css_class="event-download-types")) 1737 output.append(fmt.span(on=1, css_class="event-download-webcal")) 1738 output.append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_all_link)) 1739 output.append(fmt.span(on=0)) 1740 output.append(fmt.span(on=1, css_class="event-download-http")) 1741 output.append(linkToPage(request, page, _("http"), download_all_link)) 1742 output.append(fmt.span(on=0)) 1743 output.append(fmt.span(on=0)) # end types 1744 output.append(fmt.span(on=1, css_class="event-download-label")) 1745 output.append(fmt.text(_("Download this calendar"))) 1746 output.append(fmt.span(on=0)) # end label 1747 output.append(fmt.span(on=1, css_class="event-download-period")) 1748 output.append(fmt.text(original_calendar_period)) 1749 output.append(fmt.span(on=0)) 1750 output.append(fmt.span(on=1, css_class="event-download-period-raw")) 1751 output.append(fmt.text(raw_calendar_period)) 1752 output.append(fmt.span(on=0)) 1753 output.append(fmt.div(on=0)) 1754 1755 output.append(fmt.div(on=1, css_class="event-download-item")) 1756 output.append(fmt.span(on=1, css_class="event-download-link")) 1757 output.append(linkToPage(request, page, _("Edit download options..."), download_dialogue_link)) 1758 output.append(fmt.span(on=0)) # end label 1759 output.append(fmt.div(on=0)) 1760 1761 output.append(fmt.div(on=0)) # end of pop-up 1762 output.append(fmt.span(on=0)) # end of download 1763 1764 # Subscription controls. 1765 1766 output.append(fmt.span(on=1, css_class="event-download")) 1767 output.append(fmt.text(_("Subscribe..."))) 1768 output.append(fmt.div(on=1, css_class="event-download-popup")) 1769 1770 output.append(fmt.div(on=1, css_class="event-download-item")) 1771 output.append(fmt.span(on=1, css_class="event-download-label")) 1772 output.append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link)) 1773 output.append(fmt.span(on=0)) # end label 1774 output.append(fmt.span(on=1, css_class="event-download-period")) 1775 output.append(fmt.text(calendar_period)) 1776 output.append(fmt.span(on=0)) 1777 output.append(fmt.div(on=0)) 1778 1779 output.append(fmt.div(on=1, css_class="event-download-item")) 1780 output.append(fmt.span(on=1, css_class="event-download-label")) 1781 output.append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link)) 1782 output.append(fmt.span(on=0)) # end label 1783 output.append(fmt.span(on=1, css_class="event-download-period")) 1784 output.append(fmt.text(original_calendar_period)) 1785 output.append(fmt.span(on=0)) 1786 output.append(fmt.span(on=1, css_class="event-download-period-raw")) 1787 output.append(fmt.text(raw_calendar_period)) 1788 output.append(fmt.span(on=0)) 1789 output.append(fmt.div(on=0)) 1790 1791 output.append(fmt.div(on=1, css_class="event-download-item")) 1792 output.append(fmt.span(on=1, css_class="event-download-link")) 1793 output.append(linkToPage(request, page, _("Edit subscription options..."), subscribe_dialogue_link)) 1794 output.append(fmt.span(on=0)) # end label 1795 output.append(fmt.div(on=0)) 1796 1797 output.append(fmt.div(on=0)) # end of pop-up 1798 output.append(fmt.span(on=0)) # end of download 1799 1800 output.append(fmt.div(on=0)) # end of controls 1801 1802 return "".join(output) 1803 1804 def writeViewControls(self): 1805 1806 """ 1807 Return a representation of the view mode controls, permitting viewing of 1808 aggregated events in calendar, list or table form. 1809 """ 1810 1811 page = self.page 1812 request = page.request 1813 fmt = request.formatter 1814 _ = request.getText 1815 1816 output = [] 1817 1818 start = self.calendar_start 1819 end = self.calendar_end 1820 1821 help_page = Page(request, "HelpOnEventAggregator") 1822 calendar_link = self.getNavigationLink(start and start.as_month(), end and end.as_month(), "calendar", "month") 1823 list_link = self.getNavigationLink(start, end, "list") 1824 table_link = self.getNavigationLink(start, end, "table") 1825 map_link = self.getNavigationLink(start, end, "map") 1826 new_event_link = self.getNewEventLink(start) 1827 1828 # Write the controls. 1829 1830 output.append(fmt.div(on=1, css_class="event-view-controls")) 1831 1832 output.append(fmt.span(on=1, css_class="event-view")) 1833 output.append(linkToPage(request, help_page, _("Help"))) 1834 output.append(fmt.span(on=0)) 1835 1836 output.append(fmt.span(on=1, css_class="event-view")) 1837 output.append(linkToPage(request, page, _("New event"), new_event_link)) 1838 output.append(fmt.span(on=0)) 1839 1840 if self.mode != "calendar": 1841 output.append(fmt.span(on=1, css_class="event-view")) 1842 output.append(linkToPage(request, page, _("View as calendar"), calendar_link)) 1843 output.append(fmt.span(on=0)) 1844 1845 if self.mode != "list": 1846 output.append(fmt.span(on=1, css_class="event-view")) 1847 output.append(linkToPage(request, page, _("View as list"), list_link)) 1848 output.append(fmt.span(on=0)) 1849 1850 if self.mode != "table": 1851 output.append(fmt.span(on=1, css_class="event-view")) 1852 output.append(linkToPage(request, page, _("View as table"), table_link)) 1853 output.append(fmt.span(on=0)) 1854 1855 if self.mode != "map" and self.map_name is not None: 1856 output.append(fmt.span(on=1, css_class="event-view")) 1857 output.append(linkToPage(request, page, _("View as map"), map_link)) 1858 output.append(fmt.span(on=0)) 1859 1860 output.append(fmt.div(on=0)) 1861 1862 return "".join(output) 1863 1864 def writeMapHeading(self): 1865 1866 """ 1867 Return the calendar heading for the current calendar, providing links 1868 permitting navigation to other periods. 1869 """ 1870 1871 label = self.getCalendarPeriod() 1872 1873 if self.raw_calendar_start is None or self.raw_calendar_end is None: 1874 fmt = self.page.request.formatter 1875 output = [] 1876 output.append(fmt.span(on=1)) 1877 output.append(fmt.text(label)) 1878 output.append(fmt.span(on=0)) 1879 return "".join(output) 1880 else: 1881 return self._writeCalendarHeading(label, self.calendar_start, self.calendar_end) 1882 1883 def writeDateHeading(self, date): 1884 if isinstance(date, Date): 1885 return self.writeDayHeading(date) 1886 else: 1887 return self.writeMonthHeading(date) 1888 1889 def writeMonthHeading(self, year_month): 1890 1891 """ 1892 Return the calendar heading for the given 'year_month' (a Month object) 1893 providing links permitting navigation to other months. 1894 """ 1895 1896 full_month_label = self.getFullMonthLabel(year_month) 1897 end_month = year_month.update(self.duration - 1) 1898 return self._writeCalendarHeading(full_month_label, year_month, end_month) 1899 1900 def writeDayHeading(self, date): 1901 1902 """ 1903 Return the calendar heading for the given 'date' (a Date object) 1904 providing links permitting navigation to other dates. 1905 """ 1906 1907 full_date_label = self.getFullDateLabel(date) 1908 end_date = date.update(self.duration - 1) 1909 return self._writeCalendarHeading(full_date_label, date, end_date) 1910 1911 def _writeCalendarHeading(self, label, start, end): 1912 1913 """ 1914 Write a calendar heading providing links permitting navigation to other 1915 periods, using the given 'label' along with the 'start' and 'end' dates 1916 to provide a link to a particular period. 1917 """ 1918 1919 page = self.page 1920 request = page.request 1921 fmt = request.formatter 1922 _ = request.getText 1923 1924 output = [] 1925 1926 # Prepare navigation links. 1927 1928 if self.calendar_name is not None: 1929 calendar_name = self.calendar_name 1930 1931 # Links to the previous set of months and to a calendar shifted 1932 # back one month. 1933 1934 previous_set_link = self.getNavigationLink( 1935 self.previous_set_start, self.previous_set_end 1936 ) 1937 previous_link = self.getNavigationLink( 1938 self.previous_start, self.previous_end 1939 ) 1940 previous_set_update_link = self.getUpdateLink( 1941 self.previous_set_start, self.previous_set_end 1942 ) 1943 previous_update_link = self.getUpdateLink( 1944 self.previous_start, self.previous_end 1945 ) 1946 1947 # Links to the next set of months and to a calendar shifted 1948 # forward one month. 1949 1950 next_set_link = self.getNavigationLink( 1951 self.next_set_start, self.next_set_end 1952 ) 1953 next_link = self.getNavigationLink( 1954 self.next_start, self.next_end 1955 ) 1956 next_set_update_link = self.getUpdateLink( 1957 self.next_set_start, self.next_set_end 1958 ) 1959 next_update_link = self.getUpdateLink( 1960 self.next_start, self.next_end 1961 ) 1962 1963 # A link leading to this date being at the top of the calendar. 1964 1965 date_link = self.getNavigationLink(start, end) 1966 date_update_link = self.getUpdateLink(start, end) 1967 1968 output.append(fmt.span(on=1, css_class="previous")) 1969 output.append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link)) 1970 output.append(fmt.text(" ")) 1971 output.append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link)) 1972 output.append(fmt.span(on=0)) 1973 1974 output.append(fmt.span(on=1, css_class="next")) 1975 output.append(linkToPage(request, page, ">", next_link, onclick=next_update_link)) 1976 output.append(fmt.text(" ")) 1977 output.append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link)) 1978 output.append(fmt.span(on=0)) 1979 1980 output.append(linkToPage(request, page, label, date_link, onclick=date_update_link)) 1981 1982 else: 1983 output.append(fmt.span(on=1)) 1984 output.append(fmt.text(label)) 1985 output.append(fmt.span(on=0)) 1986 1987 return "".join(output) 1988 1989 def writeDayNumberHeading(self, date, busy): 1990 1991 """ 1992 Return a link for the given 'date' which will activate the new event 1993 action for the given day. If 'busy' is given as a true value, the 1994 heading will be marked as busy. 1995 """ 1996 1997 page = self.page 1998 request = page.request 1999 fmt = request.formatter 2000 _ = request.getText 2001 2002 output = [] 2003 2004 year, month, day = date.as_tuple() 2005 new_event_link = self.getNewEventLink(date) 2006 2007 # Prepare a link to the day view for this day. 2008 2009 day_view_link = self.getNavigationLink(date, date, "day", "date") 2010 2011 # Output the heading class. 2012 2013 output.append( 2014 fmt.table_cell(on=1, attrs={ 2015 "class" : "event-day-heading event-day-%s" % (busy and "busy" or "empty"), 2016 "colspan" : "3" 2017 })) 2018 2019 # Output the number and pop-up menu. 2020 2021 output.append(fmt.div(on=1, css_class="event-day-box")) 2022 2023 output.append(fmt.span(on=1, css_class="event-day-number-popup")) 2024 output.append(fmt.span(on=1, css_class="event-day-number-link")) 2025 output.append(linkToPage(request, page, _("View day"), day_view_link)) 2026 output.append(fmt.span(on=0)) 2027 output.append(fmt.span(on=1, css_class="event-day-number-link")) 2028 output.append(linkToPage(request, page, _("New event"), new_event_link)) 2029 output.append(fmt.span(on=0)) 2030 output.append(fmt.span(on=0)) 2031 2032 output.append(fmt.span(on=1, css_class="event-day-number")) 2033 output.append(fmt.text(unicode(day))) 2034 output.append(fmt.span(on=0)) 2035 2036 output.append(fmt.div(on=0)) 2037 2038 # End of heading. 2039 2040 output.append(fmt.table_cell(on=0)) 2041 2042 return "".join(output) 2043 2044 # Common layout methods. 2045 2046 def getEventStyle(self, colour_seed): 2047 2048 "Generate colour style information using the given 'colour_seed'." 2049 2050 bg = getColour(colour_seed) 2051 fg = getBlackOrWhite(bg) 2052 return "background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg) 2053 2054 def writeEventSummaryBox(self, event): 2055 2056 "Return an event summary box linking to the given 'event'." 2057 2058 page = self.page 2059 request = page.request 2060 fmt = request.formatter 2061 2062 output = [] 2063 2064 event_details = event.getDetails() 2065 event_summary = event.getSummary(self.parent_name) 2066 2067 is_ambiguous = event.as_timespan().ambiguous() 2068 style = self.getEventStyle(event_summary) 2069 2070 # The event box contains the summary, alongside 2071 # other elements. 2072 2073 output.append(fmt.div(on=1, css_class="event-summary-box")) 2074 output.append(fmt.div(on=1, css_class="event-summary", style=style)) 2075 2076 if is_ambiguous: 2077 output.append(fmt.icon("/!\\")) 2078 2079 output.append(event.linkToEvent(request, event_summary)) 2080 output.append(fmt.div(on=0)) 2081 2082 # Add a pop-up element for long summaries. 2083 2084 output.append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 2085 2086 if is_ambiguous: 2087 output.append(fmt.icon("/!\\")) 2088 2089 output.append(event.linkToEvent(request, event_summary)) 2090 output.append(fmt.div(on=0)) 2091 2092 output.append(fmt.div(on=0)) 2093 2094 return "".join(output) 2095 2096 # Calendar layout methods. 2097 2098 def writeMonthTableHeading(self, year_month): 2099 page = self.page 2100 fmt = page.request.formatter 2101 2102 output = [] 2103 output.append(fmt.table_row(on=1)) 2104 output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"})) 2105 2106 output.append(self.writeMonthHeading(year_month)) 2107 2108 output.append(fmt.table_cell(on=0)) 2109 output.append(fmt.table_row(on=0)) 2110 2111 return "".join(output) 2112 2113 def writeWeekdayHeadings(self): 2114 page = self.page 2115 request = page.request 2116 fmt = request.formatter 2117 _ = request.getText 2118 2119 output = [] 2120 output.append(fmt.table_row(on=1)) 2121 2122 for weekday in range(0, 7): 2123 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"})) 2124 output.append(fmt.text(_(getDayLabel(weekday)))) 2125 output.append(fmt.table_cell(on=0)) 2126 2127 output.append(fmt.table_row(on=0)) 2128 return "".join(output) 2129 2130 def writeDayNumbers(self, first_day, number_of_days, month, coverage): 2131 page = self.page 2132 fmt = page.request.formatter 2133 2134 output = [] 2135 output.append(fmt.table_row(on=1)) 2136 2137 for weekday in range(0, 7): 2138 day = first_day + weekday 2139 date = month.as_date(day) 2140 2141 # Output out-of-month days. 2142 2143 if day < 1 or day > number_of_days: 2144 output.append(fmt.table_cell(on=1, 2145 attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"})) 2146 output.append(fmt.table_cell(on=0)) 2147 2148 # Output normal days. 2149 2150 else: 2151 # Output the day heading, making a link to a new event 2152 # action. 2153 2154 output.append(self.writeDayNumberHeading(date, date in coverage)) 2155 2156 # End of day numbers. 2157 2158 output.append(fmt.table_row(on=0)) 2159 return "".join(output) 2160 2161 def writeEmptyWeek(self, first_day, number_of_days): 2162 page = self.page 2163 fmt = page.request.formatter 2164 2165 output = [] 2166 output.append(fmt.table_row(on=1)) 2167 2168 for weekday in range(0, 7): 2169 day = first_day + weekday 2170 2171 # Output out-of-month days. 2172 2173 if day < 1 or day > number_of_days: 2174 output.append(fmt.table_cell(on=1, 2175 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 2176 output.append(fmt.table_cell(on=0)) 2177 2178 # Output empty days. 2179 2180 else: 2181 output.append(fmt.table_cell(on=1, 2182 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 2183 2184 output.append(fmt.table_row(on=0)) 2185 return "".join(output) 2186 2187 def writeWeekSlots(self, first_day, number_of_days, month, week_end, week_slots): 2188 output = [] 2189 2190 locations = week_slots.keys() 2191 locations.sort(sort_none_first) 2192 2193 # Visit each slot corresponding to a location (or no location). 2194 2195 for location in locations: 2196 2197 # Visit each coverage span, presenting the events in the span. 2198 2199 for events in week_slots[location]: 2200 2201 # Output each set. 2202 2203 output.append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events)) 2204 2205 # Add a spacer. 2206 2207 output.append(self.writeWeekSpacer(first_day, number_of_days)) 2208 2209 return "".join(output) 2210 2211 def writeWeekSlot(self, first_day, number_of_days, month, week_end, events): 2212 page = self.page 2213 request = page.request 2214 fmt = request.formatter 2215 2216 output = [] 2217 output.append(fmt.table_row(on=1)) 2218 2219 # Then, output day details. 2220 2221 for weekday in range(0, 7): 2222 day = first_day + weekday 2223 date = month.as_date(day) 2224 2225 # Skip out-of-month days. 2226 2227 if day < 1 or day > number_of_days: 2228 output.append(fmt.table_cell(on=1, 2229 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 2230 output.append(fmt.table_cell(on=0)) 2231 continue 2232 2233 # Output the day. 2234 2235 if date not in events: 2236 output.append(fmt.table_cell(on=1, 2237 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 2238 2239 # Get event details for the current day. 2240 2241 for event in events: 2242 event_details = event.getDetails() 2243 2244 if date not in event: 2245 continue 2246 2247 # Get basic properties of the event. 2248 2249 starts_today = event_details["start"] == date 2250 ends_today = event_details["end"] == date 2251 event_summary = event.getSummary(self.parent_name) 2252 2253 style = self.getEventStyle(event_summary) 2254 2255 # Determine if the event name should be shown. 2256 2257 start_of_period = starts_today or weekday == 0 or day == 1 2258 2259 if self.name_usage == "daily" or start_of_period: 2260 hide_text = 0 2261 else: 2262 hide_text = 1 2263 2264 # Output start of day gap and determine whether 2265 # any event content should be explicitly output 2266 # for this day. 2267 2268 if starts_today: 2269 2270 # Single day events... 2271 2272 if ends_today: 2273 colspan = 3 2274 event_day_type = "event-day-single" 2275 2276 # Events starting today... 2277 2278 else: 2279 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap"})) 2280 output.append(fmt.table_cell(on=0)) 2281 2282 # Calculate the span of this cell. 2283 # Events whose names appear on every day... 2284 2285 if self.name_usage == "daily": 2286 colspan = 2 2287 event_day_type = "event-day-starting" 2288 2289 # Events whose names appear once per week... 2290 2291 else: 2292 if event_details["end"] <= week_end: 2293 event_length = event_details["end"].day() - day + 1 2294 colspan = (event_length - 2) * 3 + 4 2295 else: 2296 event_length = week_end.day() - day + 1 2297 colspan = (event_length - 1) * 3 + 2 2298 2299 event_day_type = "event-day-multiple" 2300 2301 # Events continuing from a previous week... 2302 2303 elif start_of_period: 2304 2305 # End of continuing event... 2306 2307 if ends_today: 2308 colspan = 2 2309 event_day_type = "event-day-ending" 2310 2311 # Events continuing for at least one more day... 2312 2313 else: 2314 2315 # Calculate the span of this cell. 2316 # Events whose names appear on every day... 2317 2318 if self.name_usage == "daily": 2319 colspan = 3 2320 event_day_type = "event-day-full" 2321 2322 # Events whose names appear once per week... 2323 2324 else: 2325 if event_details["end"] <= week_end: 2326 event_length = event_details["end"].day() - day + 1 2327 colspan = (event_length - 1) * 3 + 2 2328 else: 2329 event_length = week_end.day() - day + 1 2330 colspan = event_length * 3 2331 2332 event_day_type = "event-day-multiple" 2333 2334 # Continuing events whose names appear on every day... 2335 2336 elif self.name_usage == "daily": 2337 if ends_today: 2338 colspan = 2 2339 event_day_type = "event-day-ending" 2340 else: 2341 colspan = 3 2342 event_day_type = "event-day-full" 2343 2344 # Continuing events whose names appear once per week... 2345 2346 else: 2347 colspan = None 2348 2349 # Output the main content only if it is not 2350 # continuing from a previous day. 2351 2352 if colspan is not None: 2353 2354 # Colour the cell for continuing events. 2355 2356 attrs={ 2357 "class" : "event-day-content event-day-busy %s" % event_day_type, 2358 "colspan" : str(colspan) 2359 } 2360 2361 if not (starts_today and ends_today): 2362 attrs["style"] = style 2363 2364 output.append(fmt.table_cell(on=1, attrs=attrs)) 2365 2366 # Output the event. 2367 2368 if starts_today and ends_today or not hide_text: 2369 output.append(self.writeEventSummaryBox(event)) 2370 2371 output.append(fmt.table_cell(on=0)) 2372 2373 # Output end of day gap. 2374 2375 if ends_today and not starts_today: 2376 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap"})) 2377 output.append(fmt.table_cell(on=0)) 2378 2379 # End of set. 2380 2381 output.append(fmt.table_row(on=0)) 2382 return "".join(output) 2383 2384 def writeWeekSpacer(self, first_day, number_of_days): 2385 page = self.page 2386 fmt = page.request.formatter 2387 2388 output = [] 2389 output.append(fmt.table_row(on=1)) 2390 2391 for weekday in range(0, 7): 2392 day = first_day + weekday 2393 css_classes = "event-day-spacer" 2394 2395 # Skip out-of-month days. 2396 2397 if day < 1 or day > number_of_days: 2398 css_classes += " event-day-excluded" 2399 2400 output.append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"})) 2401 output.append(fmt.table_cell(on=0)) 2402 2403 output.append(fmt.table_row(on=0)) 2404 return "".join(output) 2405 2406 # Day layout methods. 2407 2408 def writeDayTableHeading(self, date, colspan=1): 2409 page = self.page 2410 fmt = page.request.formatter 2411 2412 output = [] 2413 output.append(fmt.table_row(on=1)) 2414 2415 output.append(fmt.table_cell(on=1, attrs={"class" : "event-full-day-heading", "colspan" : str(colspan)})) 2416 output.append(self.writeDayHeading(date)) 2417 output.append(fmt.table_cell(on=0)) 2418 2419 output.append(fmt.table_row(on=0)) 2420 return "".join(output) 2421 2422 def writeEmptyDay(self, date): 2423 page = self.page 2424 fmt = page.request.formatter 2425 2426 output = [] 2427 output.append(fmt.table_row(on=1)) 2428 2429 output.append(fmt.table_cell(on=1, 2430 attrs={"class" : "event-day-content event-day-empty"})) 2431 2432 output.append(fmt.table_row(on=0)) 2433 return "".join(output) 2434 2435 def writeDaySlots(self, date, full_coverage, day_slots): 2436 2437 """ 2438 Given a 'date', non-empty 'full_coverage' for the day concerned, and a 2439 non-empty mapping of 'day_slots' (from locations to event collections), 2440 output the day slots for the day. 2441 """ 2442 2443 page = self.page 2444 fmt = page.request.formatter 2445 2446 output = [] 2447 2448 locations = day_slots.keys() 2449 locations.sort(sort_none_first) 2450 2451 # Traverse the time scale of the full coverage, visiting each slot to 2452 # determine whether it provides content for each period. 2453 2454 scale = getCoverageScale(full_coverage) 2455 2456 # Define a mapping of events to rowspans. 2457 2458 rowspans = {} 2459 2460 # Populate each period with event details, recording how many periods 2461 # each event populates. 2462 2463 day_rows = [] 2464 2465 for period in scale: 2466 2467 # Ignore timespans before this day. 2468 2469 if period != date: 2470 continue 2471 2472 # Visit each slot corresponding to a location (or no location). 2473 2474 day_row = [] 2475 2476 for location in locations: 2477 2478 # Visit each coverage span, presenting the events in the span. 2479 2480 for events in day_slots[location]: 2481 event = self.getActiveEvent(period, events) 2482 if event is not None: 2483 if not rowspans.has_key(event): 2484 rowspans[event] = 1 2485 else: 2486 rowspans[event] += 1 2487 day_row.append((location, event)) 2488 2489 day_rows.append((period, day_row)) 2490 2491 # Output the locations. 2492 2493 output.append(fmt.table_row(on=1)) 2494 2495 # Add a spacer. 2496 2497 output.append(self.writeDaySpacer(colspan=2, cls="location")) 2498 2499 for location in locations: 2500 2501 # Add spacers to the column spans. 2502 2503 columns = len(day_slots[location]) * 2 - 1 2504 output.append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)})) 2505 output.append(fmt.text(location or "")) 2506 output.append(fmt.table_cell(on=0)) 2507 2508 # Add a trailing spacer. 2509 2510 output.append(self.writeDaySpacer(cls="location")) 2511 2512 output.append(fmt.table_row(on=0)) 2513 2514 # Output the periods with event details. 2515 2516 period = None 2517 events_written = set() 2518 2519 for period, day_row in day_rows: 2520 2521 # Write an empty heading for the start of the day where the first 2522 # applicable timespan starts before this day. 2523 2524 if period.start < date: 2525 output.append(fmt.table_row(on=1)) 2526 output.append(self.writeDayScaleHeading("")) 2527 2528 # Otherwise, write a heading describing the time. 2529 2530 else: 2531 output.append(fmt.table_row(on=1)) 2532 output.append(self.writeDayScaleHeading(period.start.time_string())) 2533 2534 output.append(self.writeDaySpacer()) 2535 2536 # Visit each slot corresponding to a location (or no location). 2537 2538 for location, event in day_row: 2539 2540 # Output each location slot's contribution. 2541 2542 if event is None or event not in events_written: 2543 output.append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) 2544 if event is not None: 2545 events_written.add(event) 2546 2547 # Add a trailing spacer. 2548 2549 output.append(self.writeDaySpacer()) 2550 2551 output.append(fmt.table_row(on=0)) 2552 2553 # Write a final time heading if the last period ends in the current day. 2554 2555 if period is not None: 2556 if period.end == date: 2557 output.append(fmt.table_row(on=1)) 2558 output.append(self.writeDayScaleHeading(period.end.time_string())) 2559 2560 for slot in day_row: 2561 output.append(self.writeDaySpacer()) 2562 output.append(self.writeEmptyDaySlot()) 2563 2564 output.append(fmt.table_row(on=0)) 2565 2566 return "".join(output) 2567 2568 def writeDayScaleHeading(self, heading): 2569 page = self.page 2570 fmt = page.request.formatter 2571 2572 output = [] 2573 output.append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"})) 2574 output.append(fmt.text(heading)) 2575 output.append(fmt.table_cell(on=0)) 2576 2577 return "".join(output) 2578 2579 def getActiveEvent(self, period, events): 2580 for event in events: 2581 if period not in event: 2582 continue 2583 return event 2584 else: 2585 return None 2586 2587 def writeDaySlot(self, period, event, rowspan): 2588 page = self.page 2589 fmt = page.request.formatter 2590 2591 output = [] 2592 2593 if event is not None: 2594 event_summary = event.getSummary(self.parent_name) 2595 style = self.getEventStyle(event_summary) 2596 2597 output.append(fmt.table_cell(on=1, attrs={ 2598 "class" : "event-timespan-content event-timespan-busy", 2599 "style" : style, 2600 "rowspan" : str(rowspan) 2601 })) 2602 output.append(self.writeEventSummaryBox(event)) 2603 output.append(fmt.table_cell(on=0)) 2604 else: 2605 output.append(self.writeEmptyDaySlot()) 2606 2607 return "".join(output) 2608 2609 def writeEmptyDaySlot(self): 2610 page = self.page 2611 fmt = page.request.formatter 2612 2613 output = [] 2614 2615 output.append(fmt.table_cell(on=1, 2616 attrs={"class" : "event-timespan-content event-timespan-empty"})) 2617 output.append(fmt.table_cell(on=0)) 2618 2619 return "".join(output) 2620 2621 def writeDaySpacer(self, colspan=1, cls="timespan"): 2622 page = self.page 2623 fmt = page.request.formatter 2624 2625 output = [] 2626 output.append(fmt.table_cell(on=1, attrs={ 2627 "class" : "event-%s-spacer" % cls, 2628 "colspan" : str(colspan)})) 2629 output.append(fmt.table_cell(on=0)) 2630 return "".join(output) 2631 2632 # Map layout methods. 2633 2634 def writeMapTableHeading(self): 2635 page = self.page 2636 fmt = page.request.formatter 2637 2638 output = [] 2639 output.append(fmt.table_cell(on=1, attrs={"class" : "event-map-heading"})) 2640 output.append(self.writeMapHeading()) 2641 output.append(fmt.table_cell(on=0)) 2642 2643 return "".join(output) 2644 2645 def showDictError(self, text, pagename): 2646 page = self.page 2647 request = page.request 2648 fmt = request.formatter 2649 2650 output = [] 2651 2652 output.append(fmt.div(on=1, attrs={"class" : "event-aggregator-error"})) 2653 output.append(fmt.paragraph(on=1)) 2654 output.append(fmt.text(text)) 2655 output.append(fmt.paragraph(on=0)) 2656 output.append(fmt.paragraph(on=1)) 2657 output.append(linkToPage(request, Page(request, pagename), pagename)) 2658 output.append(fmt.paragraph(on=0)) 2659 2660 return "".join(output) 2661 2662 def writeMapEventSummaries(self, events): 2663 page = self.page 2664 request = page.request 2665 fmt = request.formatter 2666 2667 # Sort the events by date. 2668 2669 events.sort(sort_start_first) 2670 2671 # Write out a self-contained list of events. 2672 2673 output = [] 2674 output.append(fmt.bullet_list(on=1, attr={"class" : "event-map-location-events"})) 2675 2676 for event in events: 2677 2678 # Get the event details. 2679 2680 event_summary = event.getSummary(self.parent_name) 2681 start, end = event.as_limits() 2682 event_period = self._getCalendarPeriod( 2683 start and self.getFullDateLabel(start), 2684 end and self.getFullDateLabel(end), 2685 "") 2686 2687 output.append(fmt.listitem(on=1)) 2688 2689 # Link to the page using the summary. 2690 2691 output.append(event.linkToEvent(request, event_summary)) 2692 2693 # Add the event period. 2694 2695 output.append(fmt.text(" ")) 2696 output.append(fmt.span(on=1, css_class="event-map-period")) 2697 output.append(fmt.text(event_period)) 2698 output.append(fmt.span(on=0)) 2699 2700 output.append(fmt.listitem(on=0)) 2701 2702 output.append(fmt.bullet_list(on=0)) 2703 2704 return "".join(output) 2705 2706 def render(self, all_shown_events): 2707 2708 """ 2709 Render the view, returning the rendered representation as a string. 2710 The view will show a list of 'all_shown_events'. 2711 """ 2712 2713 page = self.page 2714 request = page.request 2715 fmt = request.formatter 2716 _ = request.getText 2717 2718 # Make a calendar. 2719 2720 output = [] 2721 2722 output.append(fmt.div(on=1, css_class="event-calendar", id=("EventAggregator-%s" % self.getIdentifier()))) 2723 2724 # Output download controls. 2725 2726 output.append(fmt.div(on=1, css_class="event-controls")) 2727 output.append(self.writeDownloadControls()) 2728 output.append(fmt.div(on=0)) 2729 2730 # Output a table. 2731 2732 if self.mode == "table": 2733 2734 # Start of table view output. 2735 2736 output.append(fmt.table(on=1, attrs={"tableclass" : "event-table"})) 2737 2738 output.append(fmt.table_row(on=1)) 2739 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 2740 output.append(fmt.text(_("Event dates"))) 2741 output.append(fmt.table_cell(on=0)) 2742 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 2743 output.append(fmt.text(_("Event location"))) 2744 output.append(fmt.table_cell(on=0)) 2745 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 2746 output.append(fmt.text(_("Event details"))) 2747 output.append(fmt.table_cell(on=0)) 2748 output.append(fmt.table_row(on=0)) 2749 2750 # Show the events in order. 2751 2752 all_shown_events.sort(sort_start_first) 2753 2754 for event in all_shown_events: 2755 event_page = event.getPage() 2756 event_summary = event.getSummary(self.parent_name) 2757 event_details = event.getDetails() 2758 2759 # Prepare CSS classes with category-related styling. 2760 2761 css_classes = ["event-table-details"] 2762 2763 for topic in event_details.get("topics") or event_details.get("categories") or []: 2764 2765 # Filter the category text to avoid illegal characters. 2766 2767 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 2768 2769 attrs = {"class" : " ".join(css_classes)} 2770 2771 output.append(fmt.table_row(on=1)) 2772 2773 # Start and end dates. 2774 2775 output.append(fmt.table_cell(on=1, attrs=attrs)) 2776 output.append(fmt.span(on=1)) 2777 output.append(fmt.text(str(event_details["start"]))) 2778 output.append(fmt.span(on=0)) 2779 2780 if event_details["start"] != event_details["end"]: 2781 output.append(fmt.text(" - ")) 2782 output.append(fmt.span(on=1)) 2783 output.append(fmt.text(str(event_details["end"]))) 2784 output.append(fmt.span(on=0)) 2785 2786 output.append(fmt.table_cell(on=0)) 2787 2788 # Location. 2789 2790 output.append(fmt.table_cell(on=1, attrs=attrs)) 2791 2792 if event_details.has_key("location"): 2793 output.append(event_page.formatText(event_details["location"], request, fmt)) 2794 2795 output.append(fmt.table_cell(on=0)) 2796 2797 # Link to the page using the summary. 2798 2799 output.append(fmt.table_cell(on=1, attrs=attrs)) 2800 output.append(event.linkToEvent(request, event_summary)) 2801 output.append(fmt.table_cell(on=0)) 2802 2803 output.append(fmt.table_row(on=0)) 2804 2805 # End of table view output. 2806 2807 output.append(fmt.table(on=0)) 2808 2809 # Output a map view. 2810 2811 elif self.mode == "map": 2812 2813 # Special dictionary pages. 2814 2815 maps_page = getattr(request.cfg, "event_aggregator_maps_page", "EventMapsDict") 2816 locations_page = getattr(request.cfg, "event_aggregator_locations_page", "EventLocationsDict") 2817 2818 map_image = None 2819 2820 # Get the maps and locations. 2821 2822 maps = getWikiDict(maps_page, request) 2823 locations = getWikiDict(locations_page, request) 2824 2825 # Get the map image definition. 2826 2827 if maps is not None and self.map_name is not None: 2828 try: 2829 map_details = maps[self.map_name].split() 2830 2831 map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \ 2832 map(getMapReference, map_details[:4]) 2833 map_width, map_height = map(int, map_details[4:6]) 2834 map_image = map_details[6] 2835 2836 map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees() 2837 map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees() 2838 2839 except (KeyError, ValueError): 2840 pass 2841 2842 # Report errors. 2843 2844 if maps is None: 2845 output.append(self.showDictError( 2846 _("You do not have read access to the maps page:"), 2847 maps_page)) 2848 2849 elif self.map_name is None: 2850 output.append(self.showDictError( 2851 _("Please specify a valid map name corresponding to an entry on the following page:"), 2852 maps_page)) 2853 2854 elif map_image is None: 2855 output.append(self.showDictError( 2856 _("Please specify a valid entry for %s on the following page:") % self.map_name, 2857 maps_page)) 2858 2859 elif locations is None: 2860 output.append(self.showDictError( 2861 _("You do not have read access to the locations page:"), 2862 locations_page)) 2863 2864 # Attempt to show the map. 2865 2866 else: 2867 2868 # Get events by position. 2869 2870 events_by_location = {} 2871 event_locations = {} 2872 2873 for event in all_shown_events: 2874 event_details = event.getDetails() 2875 2876 location = event_details.get("location") 2877 2878 if location is not None and not event_locations.has_key(location): 2879 2880 # Get any explicit position of an event. 2881 2882 if event_details.has_key("geo"): 2883 latitude, longitude = event_details["geo"] 2884 2885 # Or look up the position of a location using the locations 2886 # page. 2887 2888 else: 2889 latitude, longitude = getLocationPosition(location, locations) 2890 2891 # Use a normalised location if necessary. 2892 2893 if latitude is None and longitude is None: 2894 normalised_location = getNormalisedLocation(location) 2895 if normalised_location is not None: 2896 latitude, longitude = getLocationPosition(normalised_location, locations) 2897 if latitude is not None and longitude is not None: 2898 location = normalised_location 2899 2900 # Only remember positioned locations. 2901 2902 if latitude is not None and longitude is not None: 2903 event_locations[location] = latitude, longitude 2904 2905 # Record events according to location. 2906 2907 if not events_by_location.has_key(location): 2908 events_by_location[location] = [] 2909 2910 events_by_location[location].append(event) 2911 2912 # Get the map image URL. 2913 2914 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request) 2915 2916 # Start of map view output. 2917 2918 map_identifier = "map-%s" % self.getIdentifier() 2919 output.append(fmt.div(on=1, css_class="event-map", id=map_identifier)) 2920 2921 output.append(fmt.table(on=1)) 2922 2923 output.append(fmt.table_row(on=1)) 2924 output.append(self.writeMapTableHeading()) 2925 output.append(fmt.table_row(on=0)) 2926 2927 output.append(fmt.table_row(on=1)) 2928 output.append(fmt.table_cell(on=1)) 2929 2930 output.append(fmt.div(on=1, css_class="event-map-container")) 2931 output.append(fmt.image(map_image_url)) 2932 output.append(fmt.number_list(on=1)) 2933 2934 # Events with no location are unpositioned. 2935 2936 if events_by_location.has_key(None): 2937 unpositioned_events = events_by_location[None] 2938 del events_by_location[None] 2939 else: 2940 unpositioned_events = [] 2941 2942 # Events whose location is unpositioned are themselves considered 2943 # unpositioned. 2944 2945 for location in set(events_by_location.keys()).difference(event_locations.keys()): 2946 unpositioned_events += events_by_location[location] 2947 2948 # Sort the locations before traversing them. 2949 2950 event_locations = event_locations.items() 2951 event_locations.sort() 2952 2953 # Show the events in the map. 2954 2955 for location, (latitude, longitude) in event_locations: 2956 events = events_by_location[location] 2957 2958 # Skip unpositioned locations and locations outside the map. 2959 2960 if latitude is None or longitude is None or \ 2961 latitude < map_bottom_left_latitude or \ 2962 longitude < map_bottom_left_longitude or \ 2963 latitude > map_top_right_latitude or \ 2964 longitude > map_top_right_longitude: 2965 2966 unpositioned_events += events 2967 continue 2968 2969 # Get the position and dimensions of the map marker. 2970 # NOTE: Use one degree as the marker size. 2971 2972 marker_x, marker_y = getPositionForCentrePoint( 2973 getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude, 2974 map_x_scale, map_y_scale), 2975 map_x_scale, map_y_scale) 2976 2977 # Put a marker on the map. 2978 2979 output.append(fmt.listitem(on=1, css_class="event-map-label")) 2980 2981 # Have a positioned marker for the print mode. 2982 2983 output.append(fmt.div(on=1, css_class="event-map-label-only", 2984 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 2985 marker_x, marker_y, map_x_scale, map_y_scale)) 2986 output.append(fmt.div(on=0)) 2987 2988 # Have a marker containing a pop-up when using the screen mode, 2989 # providing a normal block when using the print mode. 2990 2991 output.append(fmt.div(on=1, css_class="event-map-label", 2992 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 2993 marker_x, marker_y, map_x_scale, map_y_scale)) 2994 output.append(fmt.div(on=1, css_class="event-map-details")) 2995 output.append(fmt.div(on=1, css_class="event-map-shadow")) 2996 output.append(fmt.div(on=1, css_class="event-map-location")) 2997 2998 output.append(fmt.heading(on=1, depth=2)) 2999 output.append(fmt.text(location)) 3000 output.append(fmt.heading(on=0, depth=2)) 3001 3002 output.append(self.writeMapEventSummaries(events)) 3003 3004 output.append(fmt.div(on=0)) 3005 output.append(fmt.div(on=0)) 3006 output.append(fmt.div(on=0)) 3007 output.append(fmt.div(on=0)) 3008 output.append(fmt.listitem(on=0)) 3009 3010 output.append(fmt.number_list(on=0)) 3011 output.append(fmt.div(on=0)) 3012 output.append(fmt.table_cell(on=0)) 3013 output.append(fmt.table_row(on=0)) 3014 3015 # Write unpositioned events. 3016 3017 if unpositioned_events: 3018 unpositioned_identifier = "unpositioned-%s" % self.getIdentifier() 3019 3020 output.append(fmt.table_row(on=1, css_class="event-map-unpositioned", 3021 id=unpositioned_identifier)) 3022 output.append(fmt.table_cell(on=1)) 3023 3024 output.append(fmt.heading(on=1, depth=2)) 3025 output.append(fmt.text(_("Events not shown on the map"))) 3026 output.append(fmt.heading(on=0, depth=2)) 3027 3028 # Show and hide controls. 3029 3030 output.append(fmt.div(on=1, css_class="event-map-show-control")) 3031 output.append(fmt.anchorlink(on=1, name=unpositioned_identifier)) 3032 output.append(fmt.text(_("Show unpositioned events"))) 3033 output.append(fmt.anchorlink(on=0)) 3034 output.append(fmt.div(on=0)) 3035 3036 output.append(fmt.div(on=1, css_class="event-map-hide-control")) 3037 output.append(fmt.anchorlink(on=1, name=map_identifier)) 3038 output.append(fmt.text(_("Hide unpositioned events"))) 3039 output.append(fmt.anchorlink(on=0)) 3040 output.append(fmt.div(on=0)) 3041 3042 output.append(self.writeMapEventSummaries(unpositioned_events)) 3043 3044 # End of map view output. 3045 3046 output.append(fmt.table_cell(on=0)) 3047 output.append(fmt.table_row(on=0)) 3048 output.append(fmt.table(on=0)) 3049 output.append(fmt.div(on=0)) 3050 3051 # Output a list. 3052 3053 elif self.mode == "list": 3054 3055 # Start of list view output. 3056 3057 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 3058 3059 # Output a list. 3060 3061 for period in self.first.until(self.last): 3062 3063 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-period"})) 3064 output.append(fmt.div(on=1, attr={"class" : "event-listings-heading"})) 3065 3066 # Either write a date heading or produce links for navigable 3067 # calendars. 3068 3069 output.append(self.writeDateHeading(period)) 3070 3071 output.append(fmt.div(on=0)) 3072 3073 output.append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"})) 3074 3075 # Show the events in order. 3076 3077 events_in_period = getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period)) 3078 events_in_period.sort(sort_start_first) 3079 3080 for event in events_in_period: 3081 event_page = event.getPage() 3082 event_details = event.getDetails() 3083 event_summary = event.getSummary(self.parent_name) 3084 3085 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 3086 3087 # Link to the page using the summary. 3088 3089 output.append(fmt.paragraph(on=1)) 3090 output.append(event.linkToEvent(request, event_summary)) 3091 output.append(fmt.paragraph(on=0)) 3092 3093 # Start and end dates. 3094 3095 output.append(fmt.paragraph(on=1)) 3096 output.append(fmt.span(on=1)) 3097 output.append(fmt.text(str(event_details["start"]))) 3098 output.append(fmt.span(on=0)) 3099 output.append(fmt.text(" - ")) 3100 output.append(fmt.span(on=1)) 3101 output.append(fmt.text(str(event_details["end"]))) 3102 output.append(fmt.span(on=0)) 3103 output.append(fmt.paragraph(on=0)) 3104 3105 # Location. 3106 3107 if event_details.has_key("location"): 3108 output.append(fmt.paragraph(on=1)) 3109 output.append(event_page.formatText(event_details["location"], request, fmt)) 3110 output.append(fmt.paragraph(on=1)) 3111 3112 # Topics. 3113 3114 if event_details.has_key("topics") or event_details.has_key("categories"): 3115 output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 3116 3117 for topic in event_details.get("topics") or event_details.get("categories") or []: 3118 output.append(fmt.listitem(on=1)) 3119 output.append(event_page.formatText(topic, request, fmt)) 3120 output.append(fmt.listitem(on=0)) 3121 3122 output.append(fmt.bullet_list(on=0)) 3123 3124 output.append(fmt.listitem(on=0)) 3125 3126 output.append(fmt.bullet_list(on=0)) 3127 3128 # End of list view output. 3129 3130 output.append(fmt.bullet_list(on=0)) 3131 3132 # Output a month calendar. This shows month-by-month data. 3133 3134 elif self.mode == "calendar": 3135 3136 # Visit all months in the requested range, or across known events. 3137 3138 for month in self.first.months_until(self.last): 3139 3140 # Output a month. 3141 3142 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 3143 3144 # Either write a month heading or produce links for navigable 3145 # calendars. 3146 3147 output.append(self.writeMonthTableHeading(month)) 3148 3149 # Weekday headings. 3150 3151 output.append(self.writeWeekdayHeadings()) 3152 3153 # Process the days of the month. 3154 3155 start_weekday, number_of_days = month.month_properties() 3156 3157 # The start weekday is the weekday of day number 1. 3158 # Find the first day of the week, counting from below zero, if 3159 # necessary, in order to land on the first day of the month as 3160 # day number 1. 3161 3162 first_day = 1 - start_weekday 3163 3164 while first_day <= number_of_days: 3165 3166 # Find events in this week and determine how to mark them on the 3167 # calendar. 3168 3169 week_start = month.as_date(max(first_day, 1)) 3170 week_end = month.as_date(min(first_day + 6, number_of_days)) 3171 3172 full_coverage, week_slots = getCoverage( 3173 getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end))) 3174 3175 # Output a week, starting with the day numbers. 3176 3177 output.append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 3178 3179 # Either generate empty days... 3180 3181 if not week_slots: 3182 output.append(self.writeEmptyWeek(first_day, number_of_days)) 3183 3184 # Or generate each set of scheduled events... 3185 3186 else: 3187 output.append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 3188 3189 # Process the next week... 3190 3191 first_day += 7 3192 3193 # End of month. 3194 3195 output.append(fmt.table(on=0)) 3196 3197 # Output a day view. 3198 3199 elif self.mode == "day": 3200 3201 # Visit all days in the requested range, or across known events. 3202 3203 for date in self.first.days_until(self.last): 3204 3205 output.append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"})) 3206 3207 full_coverage, day_slots = getCoverage( 3208 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 3209 3210 # Work out how many columns the day title will need. 3211 # Include spacers after the scale and each event column. 3212 3213 colspan = sum(map(len, day_slots.values())) * 2 + 2 3214 3215 output.append(self.writeDayTableHeading(date, colspan)) 3216 3217 # Either generate empty days... 3218 3219 if not day_slots: 3220 output.append(self.writeEmptyDay(date)) 3221 3222 # Or generate each set of scheduled events... 3223 3224 else: 3225 output.append(self.writeDaySlots(date, full_coverage, day_slots)) 3226 3227 # End of day. 3228 3229 output.append(fmt.table(on=0)) 3230 3231 # Output view controls. 3232 3233 output.append(fmt.div(on=1, css_class="event-controls")) 3234 output.append(self.writeViewControls()) 3235 output.append(fmt.div(on=0)) 3236 3237 # Close the calendar region. 3238 3239 output.append(fmt.div(on=0)) 3240 3241 # Add any scripts. 3242 3243 if isinstance(fmt, request.html_formatter.__class__): 3244 output.append(self.update_script) 3245 3246 return ''.join(output) 3247 3248 update_script = """\ 3249 <script type="text/javascript"> 3250 function replaceCalendar(name, url) { 3251 var calendar = document.getElementById(name); 3252 3253 if (calendar == null) { 3254 return true; 3255 } 3256 3257 var xmlhttp = new XMLHttpRequest(); 3258 xmlhttp.open("GET", url, false); 3259 xmlhttp.send(null); 3260 3261 var newCalendar = xmlhttp.responseText; 3262 3263 if (newCalendar != null) { 3264 calendar.parentNode.innerHTML = newCalendar; 3265 return false; 3266 } 3267 3268 return true; 3269 } 3270 </script> 3271 """ 3272 3273 # vim: tabstop=4 expandtab shiftwidth=4