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