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