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