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