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