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