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