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