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