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