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