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