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