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