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