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 append( 1907 fmt.table_cell(on=1, attrs={ 1908 "class" : "event-day-heading event-day-%s" % (busy and "busy" or "empty"), 1909 "colspan" : "3" 1910 })) 1911 1912 # Output the number and pop-up menu. 1913 1914 append(fmt.div(on=1, css_class="event-day-box")) 1915 1916 append(fmt.span(on=1, css_class="event-day-number-popup")) 1917 append(fmt.span(on=1, css_class="event-day-number-link")) 1918 append(linkToPage(request, page, _("View day"), day_view_link, onclick=day_view_update_link)) 1919 append(fmt.span(on=0)) 1920 append(fmt.span(on=1, css_class="event-day-number-link")) 1921 append(linkToPage(request, page, _("New event"), new_event_link)) 1922 append(fmt.span(on=0)) 1923 append(fmt.span(on=0)) 1924 1925 append(fmt.span(on=1, css_class="event-day-number")) 1926 append(fmt.text(unicode(day))) 1927 append(fmt.span(on=0)) 1928 1929 append(fmt.div(on=0)) 1930 1931 # End of heading. 1932 1933 append(fmt.table_cell(on=0)) 1934 1935 return "".join(output) 1936 1937 # Common layout methods. 1938 1939 def getEventStyle(self, colour_seed): 1940 1941 "Generate colour style information using the given 'colour_seed'." 1942 1943 bg = getColour(colour_seed) 1944 fg = getBlackOrWhite(bg) 1945 return "background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg) 1946 1947 def writeEventSummaryBox(self, event): 1948 1949 "Return an event summary box linking to the given 'event'." 1950 1951 page = self.page 1952 request = page.request 1953 fmt = request.formatter 1954 1955 output = [] 1956 append = output.append 1957 1958 event_details = event.getDetails() 1959 event_summary = event.getSummary(self.parent_name) 1960 1961 is_ambiguous = event.as_timespan().ambiguous() 1962 style = self.getEventStyle(event_summary) 1963 1964 # The event box contains the summary, alongside 1965 # other elements. 1966 1967 append(fmt.div(on=1, css_class="event-summary-box")) 1968 append(fmt.div(on=1, css_class="event-summary", style=style)) 1969 1970 if is_ambiguous: 1971 append(fmt.icon("/!\\")) 1972 1973 append(event.linkToEvent(request, event_summary)) 1974 append(fmt.div(on=0)) 1975 1976 # Add a pop-up element for long summaries. 1977 1978 append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 1979 1980 if is_ambiguous: 1981 append(fmt.icon("/!\\")) 1982 1983 append(event.linkToEvent(request, event_summary)) 1984 append(fmt.div(on=0)) 1985 1986 append(fmt.div(on=0)) 1987 1988 return "".join(output) 1989 1990 # Calendar layout methods. 1991 1992 def writeMonthTableHeading(self, year_month): 1993 page = self.page 1994 fmt = page.request.formatter 1995 1996 output = [] 1997 append = output.append 1998 1999 append(fmt.table_row(on=1)) 2000 append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"})) 2001 2002 append(self.writeMonthHeading(year_month)) 2003 2004 append(fmt.table_cell(on=0)) 2005 append(fmt.table_row(on=0)) 2006 2007 return "".join(output) 2008 2009 def writeWeekdayHeadings(self): 2010 page = self.page 2011 request = page.request 2012 fmt = request.formatter 2013 _ = request.getText 2014 2015 output = [] 2016 append = output.append 2017 2018 append(fmt.table_row(on=1)) 2019 2020 for weekday in range(0, 7): 2021 append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"})) 2022 append(fmt.text(_(getDayLabel(weekday)))) 2023 append(fmt.table_cell(on=0)) 2024 2025 append(fmt.table_row(on=0)) 2026 return "".join(output) 2027 2028 def writeDayNumbers(self, first_day, number_of_days, month, coverage): 2029 page = self.page 2030 fmt = page.request.formatter 2031 2032 output = [] 2033 append = output.append 2034 2035 append(fmt.table_row(on=1)) 2036 2037 for weekday in range(0, 7): 2038 day = first_day + weekday 2039 date = month.as_date(day) 2040 2041 # Output out-of-month days. 2042 2043 if day < 1 or day > number_of_days: 2044 append(fmt.table_cell(on=1, 2045 attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"})) 2046 append(fmt.table_cell(on=0)) 2047 2048 # Output normal days. 2049 2050 else: 2051 # Output the day heading, making a link to a new event 2052 # action. 2053 2054 append(self.writeDayNumberHeading(date, date in coverage)) 2055 2056 # End of day numbers. 2057 2058 append(fmt.table_row(on=0)) 2059 return "".join(output) 2060 2061 def writeEmptyWeek(self, first_day, number_of_days): 2062 page = self.page 2063 fmt = page.request.formatter 2064 2065 output = [] 2066 append = output.append 2067 2068 append(fmt.table_row(on=1)) 2069 2070 for weekday in range(0, 7): 2071 day = first_day + weekday 2072 2073 # Output out-of-month days. 2074 2075 if day < 1 or day > number_of_days: 2076 append(fmt.table_cell(on=1, 2077 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 2078 append(fmt.table_cell(on=0)) 2079 2080 # Output empty days. 2081 2082 else: 2083 append(fmt.table_cell(on=1, 2084 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 2085 2086 append(fmt.table_row(on=0)) 2087 return "".join(output) 2088 2089 def writeWeekSlots(self, first_day, number_of_days, month, week_end, week_slots): 2090 output = [] 2091 append = output.append 2092 2093 locations = week_slots.keys() 2094 locations.sort(sort_none_first) 2095 2096 # Visit each slot corresponding to a location (or no location). 2097 2098 for location in locations: 2099 2100 # Visit each coverage span, presenting the events in the span. 2101 2102 for events in week_slots[location]: 2103 2104 # Output each set. 2105 2106 append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events)) 2107 2108 # Add a spacer. 2109 2110 append(self.writeWeekSpacer(first_day, number_of_days)) 2111 2112 return "".join(output) 2113 2114 def writeWeekSlot(self, first_day, number_of_days, month, week_end, events): 2115 page = self.page 2116 request = page.request 2117 fmt = request.formatter 2118 2119 output = [] 2120 append = output.append 2121 2122 append(fmt.table_row(on=1)) 2123 2124 # Then, output day details. 2125 2126 for weekday in range(0, 7): 2127 day = first_day + weekday 2128 date = month.as_date(day) 2129 2130 # Skip out-of-month days. 2131 2132 if day < 1 or day > number_of_days: 2133 append(fmt.table_cell(on=1, 2134 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 2135 append(fmt.table_cell(on=0)) 2136 continue 2137 2138 # Output the day. 2139 2140 if date not in events: 2141 append(fmt.table_cell(on=1, 2142 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 2143 2144 # Get event details for the current day. 2145 2146 for event in events: 2147 event_details = event.getDetails() 2148 2149 if date not in event: 2150 continue 2151 2152 # Get basic properties of the event. 2153 2154 starts_today = event_details["start"] == date 2155 ends_today = event_details["end"] == date 2156 event_summary = event.getSummary(self.parent_name) 2157 2158 style = self.getEventStyle(event_summary) 2159 2160 # Determine if the event name should be shown. 2161 2162 start_of_period = starts_today or weekday == 0 or day == 1 2163 2164 if self.name_usage == "daily" or start_of_period: 2165 hide_text = 0 2166 else: 2167 hide_text = 1 2168 2169 # Output start of day gap and determine whether 2170 # any event content should be explicitly output 2171 # for this day. 2172 2173 if starts_today: 2174 2175 # Single day events... 2176 2177 if ends_today: 2178 colspan = 3 2179 event_day_type = "event-day-single" 2180 2181 # Events starting today... 2182 2183 else: 2184 append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap"})) 2185 append(fmt.table_cell(on=0)) 2186 2187 # Calculate the span of this cell. 2188 # Events whose names appear on every day... 2189 2190 if self.name_usage == "daily": 2191 colspan = 2 2192 event_day_type = "event-day-starting" 2193 2194 # Events whose names appear once per week... 2195 2196 else: 2197 if event_details["end"] <= week_end: 2198 event_length = event_details["end"].day() - day + 1 2199 colspan = (event_length - 2) * 3 + 4 2200 else: 2201 event_length = week_end.day() - day + 1 2202 colspan = (event_length - 1) * 3 + 2 2203 2204 event_day_type = "event-day-multiple" 2205 2206 # Events continuing from a previous week... 2207 2208 elif start_of_period: 2209 2210 # End of continuing event... 2211 2212 if ends_today: 2213 colspan = 2 2214 event_day_type = "event-day-ending" 2215 2216 # Events continuing for at least one more day... 2217 2218 else: 2219 2220 # Calculate the span of this cell. 2221 # Events whose names appear on every day... 2222 2223 if self.name_usage == "daily": 2224 colspan = 3 2225 event_day_type = "event-day-full" 2226 2227 # Events whose names appear once per week... 2228 2229 else: 2230 if event_details["end"] <= week_end: 2231 event_length = event_details["end"].day() - day + 1 2232 colspan = (event_length - 1) * 3 + 2 2233 else: 2234 event_length = week_end.day() - day + 1 2235 colspan = event_length * 3 2236 2237 event_day_type = "event-day-multiple" 2238 2239 # Continuing events whose names appear on every day... 2240 2241 elif self.name_usage == "daily": 2242 if ends_today: 2243 colspan = 2 2244 event_day_type = "event-day-ending" 2245 else: 2246 colspan = 3 2247 event_day_type = "event-day-full" 2248 2249 # Continuing events whose names appear once per week... 2250 2251 else: 2252 colspan = None 2253 2254 # Output the main content only if it is not 2255 # continuing from a previous day. 2256 2257 if colspan is not None: 2258 2259 # Colour the cell for continuing events. 2260 2261 attrs={ 2262 "class" : "event-day-content event-day-busy %s" % event_day_type, 2263 "colspan" : str(colspan) 2264 } 2265 2266 if not (starts_today and ends_today): 2267 attrs["style"] = style 2268 2269 append(fmt.table_cell(on=1, attrs=attrs)) 2270 2271 # Output the event. 2272 2273 if starts_today and ends_today or not hide_text: 2274 append(self.writeEventSummaryBox(event)) 2275 2276 append(fmt.table_cell(on=0)) 2277 2278 # Output end of day gap. 2279 2280 if ends_today and not starts_today: 2281 append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap"})) 2282 append(fmt.table_cell(on=0)) 2283 2284 # End of set. 2285 2286 append(fmt.table_row(on=0)) 2287 return "".join(output) 2288 2289 def writeWeekSpacer(self, first_day, number_of_days): 2290 page = self.page 2291 fmt = page.request.formatter 2292 2293 output = [] 2294 append = output.append 2295 2296 append(fmt.table_row(on=1)) 2297 2298 for weekday in range(0, 7): 2299 day = first_day + weekday 2300 css_classes = "event-day-spacer" 2301 2302 # Skip out-of-month days. 2303 2304 if day < 1 or day > number_of_days: 2305 css_classes += " event-day-excluded" 2306 2307 append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"})) 2308 append(fmt.table_cell(on=0)) 2309 2310 append(fmt.table_row(on=0)) 2311 return "".join(output) 2312 2313 # Day layout methods. 2314 2315 def writeDayTableHeading(self, date, colspan=1): 2316 page = self.page 2317 fmt = page.request.formatter 2318 2319 output = [] 2320 append = output.append 2321 2322 append(fmt.table_row(on=1)) 2323 2324 append(fmt.table_cell(on=1, attrs={"class" : "event-full-day-heading", "colspan" : str(colspan)})) 2325 append(self.writeDayHeading(date)) 2326 append(fmt.table_cell(on=0)) 2327 2328 append(fmt.table_row(on=0)) 2329 return "".join(output) 2330 2331 def writeEmptyDay(self, date): 2332 page = self.page 2333 fmt = page.request.formatter 2334 2335 output = [] 2336 append = output.append 2337 2338 append(fmt.table_row(on=1)) 2339 2340 append(fmt.table_cell(on=1, 2341 attrs={"class" : "event-day-content event-day-empty"})) 2342 2343 append(fmt.table_row(on=0)) 2344 return "".join(output) 2345 2346 def writeDaySlots(self, date, full_coverage, day_slots): 2347 2348 """ 2349 Given a 'date', non-empty 'full_coverage' for the day concerned, and a 2350 non-empty mapping of 'day_slots' (from locations to event collections), 2351 output the day slots for the day. 2352 """ 2353 2354 page = self.page 2355 fmt = page.request.formatter 2356 2357 output = [] 2358 append = output.append 2359 2360 locations = day_slots.keys() 2361 locations.sort(sort_none_first) 2362 2363 # Traverse the time scale of the full coverage, visiting each slot to 2364 # determine whether it provides content for each period. 2365 2366 scale = getCoverageScale(full_coverage) 2367 2368 # Define a mapping of events to rowspans. 2369 2370 rowspans = {} 2371 2372 # Populate each period with event details, recording how many periods 2373 # each event populates. 2374 2375 day_rows = [] 2376 2377 for period in scale: 2378 2379 # Ignore timespans before this day. 2380 2381 if period != date: 2382 continue 2383 2384 # Visit each slot corresponding to a location (or no location). 2385 2386 day_row = [] 2387 2388 for location in locations: 2389 2390 # Visit each coverage span, presenting the events in the span. 2391 2392 for events in day_slots[location]: 2393 event = self.getActiveEvent(period, events) 2394 if event is not None: 2395 if not rowspans.has_key(event): 2396 rowspans[event] = 1 2397 else: 2398 rowspans[event] += 1 2399 day_row.append((location, event)) 2400 2401 day_rows.append((period, day_row)) 2402 2403 # Output the locations. 2404 2405 append(fmt.table_row(on=1)) 2406 2407 # Add a spacer. 2408 2409 append(self.writeDaySpacer(colspan=2, cls="location")) 2410 2411 for location in locations: 2412 2413 # Add spacers to the column spans. 2414 2415 columns = len(day_slots[location]) * 2 - 1 2416 append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)})) 2417 append(fmt.text(location or "")) 2418 append(fmt.table_cell(on=0)) 2419 2420 # Add a trailing spacer. 2421 2422 append(self.writeDaySpacer(cls="location")) 2423 2424 append(fmt.table_row(on=0)) 2425 2426 # Output the periods with event details. 2427 2428 period = None 2429 events_written = set() 2430 2431 for period, day_row in day_rows: 2432 2433 # Write an empty heading for the start of the day where the first 2434 # applicable timespan starts before this day. 2435 2436 if period.start < date: 2437 append(fmt.table_row(on=1)) 2438 append(self.writeDayScaleHeading("")) 2439 2440 # Otherwise, write a heading describing the time. 2441 2442 else: 2443 append(fmt.table_row(on=1)) 2444 append(self.writeDayScaleHeading(period.start.time_string())) 2445 2446 append(self.writeDaySpacer()) 2447 2448 # Visit each slot corresponding to a location (or no location). 2449 2450 for location, event in day_row: 2451 2452 # Output each location slot's contribution. 2453 2454 if event is None or event not in events_written: 2455 append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) 2456 if event is not None: 2457 events_written.add(event) 2458 2459 # Add a trailing spacer. 2460 2461 append(self.writeDaySpacer()) 2462 2463 append(fmt.table_row(on=0)) 2464 2465 # Write a final time heading if the last period ends in the current day. 2466 2467 if period is not None: 2468 if period.end == date: 2469 append(fmt.table_row(on=1)) 2470 append(self.writeDayScaleHeading(period.end.time_string())) 2471 2472 for slot in day_row: 2473 append(self.writeDaySpacer()) 2474 append(self.writeEmptyDaySlot()) 2475 2476 append(fmt.table_row(on=0)) 2477 2478 return "".join(output) 2479 2480 def writeDayScaleHeading(self, heading): 2481 page = self.page 2482 fmt = page.request.formatter 2483 2484 output = [] 2485 append = output.append 2486 2487 append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"})) 2488 append(fmt.text(heading)) 2489 append(fmt.table_cell(on=0)) 2490 2491 return "".join(output) 2492 2493 def getActiveEvent(self, period, events): 2494 for event in events: 2495 if period not in event: 2496 continue 2497 return event 2498 else: 2499 return None 2500 2501 def writeDaySlot(self, period, event, rowspan): 2502 page = self.page 2503 fmt = page.request.formatter 2504 2505 output = [] 2506 append = output.append 2507 2508 if event is not None: 2509 event_summary = event.getSummary(self.parent_name) 2510 style = self.getEventStyle(event_summary) 2511 2512 append(fmt.table_cell(on=1, attrs={ 2513 "class" : "event-timespan-content event-timespan-busy", 2514 "style" : style, 2515 "rowspan" : str(rowspan) 2516 })) 2517 append(self.writeEventSummaryBox(event)) 2518 append(fmt.table_cell(on=0)) 2519 else: 2520 append(self.writeEmptyDaySlot()) 2521 2522 return "".join(output) 2523 2524 def writeEmptyDaySlot(self): 2525 page = self.page 2526 fmt = page.request.formatter 2527 2528 output = [] 2529 append = output.append 2530 2531 append(fmt.table_cell(on=1, 2532 attrs={"class" : "event-timespan-content event-timespan-empty"})) 2533 append(fmt.table_cell(on=0)) 2534 2535 return "".join(output) 2536 2537 def writeDaySpacer(self, colspan=1, cls="timespan"): 2538 page = self.page 2539 fmt = page.request.formatter 2540 2541 output = [] 2542 append = output.append 2543 2544 append(fmt.table_cell(on=1, attrs={ 2545 "class" : "event-%s-spacer" % cls, 2546 "colspan" : str(colspan)})) 2547 append(fmt.table_cell(on=0)) 2548 return "".join(output) 2549 2550 # Map layout methods. 2551 2552 def writeMapTableHeading(self): 2553 page = self.page 2554 fmt = page.request.formatter 2555 2556 output = [] 2557 append = output.append 2558 2559 append(fmt.table_cell(on=1, attrs={"class" : "event-map-heading"})) 2560 append(self.writeMapHeading()) 2561 append(fmt.table_cell(on=0)) 2562 2563 return "".join(output) 2564 2565 def showDictError(self, text, pagename): 2566 page = self.page 2567 request = page.request 2568 fmt = request.formatter 2569 2570 output = [] 2571 append = output.append 2572 2573 append(fmt.div(on=1, attrs={"class" : "event-aggregator-error"})) 2574 append(fmt.paragraph(on=1)) 2575 append(fmt.text(text)) 2576 append(fmt.paragraph(on=0)) 2577 append(fmt.paragraph(on=1)) 2578 append(linkToPage(request, Page(request, pagename), pagename)) 2579 append(fmt.paragraph(on=0)) 2580 2581 return "".join(output) 2582 2583 def writeMapEventSummaries(self, events): 2584 page = self.page 2585 request = page.request 2586 fmt = request.formatter 2587 2588 # Sort the events by date. 2589 2590 events.sort(sort_start_first) 2591 2592 # Write out a self-contained list of events. 2593 2594 output = [] 2595 append = output.append 2596 2597 append(fmt.bullet_list(on=1, attr={"class" : "event-map-location-events"})) 2598 2599 for event in events: 2600 2601 # Get the event details. 2602 2603 event_summary = event.getSummary(self.parent_name) 2604 start, end = event.as_limits() 2605 event_period = self._getCalendarPeriod( 2606 start and self.getFullDateLabel(start), 2607 end and self.getFullDateLabel(end), 2608 "") 2609 2610 append(fmt.listitem(on=1)) 2611 2612 # Link to the page using the summary. 2613 2614 append(event.linkToEvent(request, event_summary)) 2615 2616 # Add the event period. 2617 2618 append(fmt.text(" ")) 2619 append(fmt.span(on=1, css_class="event-map-period")) 2620 append(fmt.text(event_period)) 2621 append(fmt.span(on=0)) 2622 2623 append(fmt.listitem(on=0)) 2624 2625 append(fmt.bullet_list(on=0)) 2626 2627 return "".join(output) 2628 2629 def render(self, all_shown_events): 2630 2631 """ 2632 Render the view, returning the rendered representation as a string. 2633 The view will show a list of 'all_shown_events'. 2634 """ 2635 2636 page = self.page 2637 request = page.request 2638 fmt = request.formatter 2639 _ = request.getText 2640 2641 # Make a calendar. 2642 2643 output = [] 2644 append = output.append 2645 2646 append(fmt.div(on=1, css_class="event-calendar", id=("EventAggregator-%s" % self.getIdentifier()))) 2647 2648 # Output download controls. 2649 2650 append(fmt.div(on=1, css_class="event-controls")) 2651 append(self.writeDownloadControls()) 2652 append(fmt.div(on=0)) 2653 2654 # Output a table. 2655 2656 if self.mode == "table": 2657 2658 # Start of table view output. 2659 2660 append(fmt.table(on=1, attrs={"tableclass" : "event-table"})) 2661 2662 append(fmt.table_row(on=1)) 2663 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 2664 append(fmt.text(_("Event dates"))) 2665 append(fmt.table_cell(on=0)) 2666 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 2667 append(fmt.text(_("Event location"))) 2668 append(fmt.table_cell(on=0)) 2669 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 2670 append(fmt.text(_("Event details"))) 2671 append(fmt.table_cell(on=0)) 2672 append(fmt.table_row(on=0)) 2673 2674 # Show the events in order. 2675 2676 all_shown_events.sort(sort_start_first) 2677 2678 for event in all_shown_events: 2679 event_page = event.getPage() 2680 event_summary = event.getSummary(self.parent_name) 2681 event_details = event.getDetails() 2682 2683 # Prepare CSS classes with category-related styling. 2684 2685 css_classes = ["event-table-details"] 2686 2687 for topic in event_details.get("topics") or event_details.get("categories") or []: 2688 2689 # Filter the category text to avoid illegal characters. 2690 2691 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 2692 2693 attrs = {"class" : " ".join(css_classes)} 2694 2695 append(fmt.table_row(on=1)) 2696 2697 # Start and end dates. 2698 2699 append(fmt.table_cell(on=1, attrs=attrs)) 2700 append(fmt.span(on=1)) 2701 append(fmt.text(str(event_details["start"]))) 2702 append(fmt.span(on=0)) 2703 2704 if event_details["start"] != event_details["end"]: 2705 append(fmt.text(" - ")) 2706 append(fmt.span(on=1)) 2707 append(fmt.text(str(event_details["end"]))) 2708 append(fmt.span(on=0)) 2709 2710 append(fmt.table_cell(on=0)) 2711 2712 # Location. 2713 2714 append(fmt.table_cell(on=1, attrs=attrs)) 2715 2716 if event_details.has_key("location"): 2717 append(event_page.formatText(event_details["location"], fmt)) 2718 2719 append(fmt.table_cell(on=0)) 2720 2721 # Link to the page using the summary. 2722 2723 append(fmt.table_cell(on=1, attrs=attrs)) 2724 append(event.linkToEvent(request, event_summary)) 2725 append(fmt.table_cell(on=0)) 2726 2727 append(fmt.table_row(on=0)) 2728 2729 # End of table view output. 2730 2731 append(fmt.table(on=0)) 2732 2733 # Output a map view. 2734 2735 elif self.mode == "map": 2736 2737 # Special dictionary pages. 2738 2739 maps_page = getMapsPage(request) 2740 locations_page = getLocationsPage(request) 2741 2742 map_image = None 2743 2744 # Get the maps and locations. 2745 2746 maps = getWikiDict(maps_page, request) 2747 locations = getWikiDict(locations_page, request) 2748 2749 # Get the map image definition. 2750 2751 if maps is not None and self.map_name: 2752 try: 2753 map_details = maps[self.map_name].split() 2754 2755 map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \ 2756 map(getMapReference, map_details[:4]) 2757 map_width, map_height = map(int, map_details[4:6]) 2758 map_image = map_details[6] 2759 2760 map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees() 2761 map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees() 2762 2763 except (KeyError, ValueError): 2764 pass 2765 2766 # Report errors. 2767 2768 if maps is None: 2769 append(self.showDictError( 2770 _("You do not have read access to the maps page:"), 2771 maps_page)) 2772 2773 elif not self.map_name: 2774 append(self.showDictError( 2775 _("Please specify a valid map name corresponding to an entry on the following page:"), 2776 maps_page)) 2777 2778 elif map_image is None: 2779 append(self.showDictError( 2780 _("Please specify a valid entry for %s on the following page:") % self.map_name, 2781 maps_page)) 2782 2783 elif locations is None: 2784 append(self.showDictError( 2785 _("You do not have read access to the locations page:"), 2786 locations_page)) 2787 2788 # Attempt to show the map. 2789 2790 else: 2791 2792 # Get events by position. 2793 2794 events_by_location = {} 2795 event_locations = {} 2796 2797 for event in all_shown_events: 2798 event_details = event.getDetails() 2799 2800 location = event_details.get("location") 2801 2802 if location is not None and not event_locations.has_key(location): 2803 2804 # Get any explicit position of an event. 2805 2806 if event_details.has_key("geo"): 2807 latitude, longitude = event_details["geo"] 2808 2809 # Or look up the position of a location using the locations 2810 # page. 2811 2812 else: 2813 latitude, longitude = Location(location, locations).getPosition() 2814 2815 # Use a normalised location if necessary. 2816 2817 if latitude is None and longitude is None: 2818 normalised_location = getNormalisedLocation(location) 2819 if normalised_location is not None: 2820 latitude, longitude = getLocationPosition(normalised_location, locations) 2821 if latitude is not None and longitude is not None: 2822 location = normalised_location 2823 2824 # Only remember positioned locations. 2825 2826 if latitude is not None and longitude is not None: 2827 event_locations[location] = latitude, longitude 2828 2829 # Record events according to location. 2830 2831 if not events_by_location.has_key(location): 2832 events_by_location[location] = [] 2833 2834 events_by_location[location].append(event) 2835 2836 # Get the map image URL. 2837 2838 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request) 2839 2840 # Start of map view output. 2841 2842 map_identifier = "map-%s" % self.getIdentifier() 2843 append(fmt.div(on=1, css_class="event-map", id=map_identifier)) 2844 2845 append(fmt.table(on=1)) 2846 2847 append(fmt.table_row(on=1)) 2848 append(self.writeMapTableHeading()) 2849 append(fmt.table_row(on=0)) 2850 2851 append(fmt.table_row(on=1)) 2852 append(fmt.table_cell(on=1)) 2853 2854 append(fmt.div(on=1, css_class="event-map-container")) 2855 append(fmt.image(map_image_url)) 2856 append(fmt.number_list(on=1)) 2857 2858 # Events with no location are unpositioned. 2859 2860 if events_by_location.has_key(None): 2861 unpositioned_events = events_by_location[None] 2862 del events_by_location[None] 2863 else: 2864 unpositioned_events = [] 2865 2866 # Events whose location is unpositioned are themselves considered 2867 # unpositioned. 2868 2869 for location in set(events_by_location.keys()).difference(event_locations.keys()): 2870 unpositioned_events += events_by_location[location] 2871 2872 # Sort the locations before traversing them. 2873 2874 event_locations = event_locations.items() 2875 event_locations.sort() 2876 2877 # Show the events in the map. 2878 2879 for location, (latitude, longitude) in event_locations: 2880 events = events_by_location[location] 2881 2882 # Skip unpositioned locations and locations outside the map. 2883 2884 if latitude is None or longitude is None or \ 2885 latitude < map_bottom_left_latitude or \ 2886 longitude < map_bottom_left_longitude or \ 2887 latitude > map_top_right_latitude or \ 2888 longitude > map_top_right_longitude: 2889 2890 unpositioned_events += events 2891 continue 2892 2893 # Get the position and dimensions of the map marker. 2894 # NOTE: Use one degree as the marker size. 2895 2896 marker_x, marker_y = getPositionForCentrePoint( 2897 getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude, 2898 map_x_scale, map_y_scale), 2899 map_x_scale, map_y_scale) 2900 2901 # Put a marker on the map. 2902 2903 append(fmt.listitem(on=1, css_class="event-map-label")) 2904 2905 # Have a positioned marker for the print mode. 2906 2907 append(fmt.div(on=1, css_class="event-map-label-only", 2908 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 2909 marker_x, marker_y, map_x_scale, map_y_scale)) 2910 append(fmt.div(on=0)) 2911 2912 # Have a marker containing a pop-up when using the screen mode, 2913 # providing a normal block when using the print mode. 2914 2915 append(fmt.div(on=1, css_class="event-map-label", 2916 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 2917 marker_x, marker_y, map_x_scale, map_y_scale)) 2918 append(fmt.div(on=1, css_class="event-map-details")) 2919 append(fmt.div(on=1, css_class="event-map-shadow")) 2920 append(fmt.div(on=1, css_class="event-map-location")) 2921 2922 append(fmt.heading(on=1, depth=2)) 2923 append(fmt.text(location)) 2924 append(fmt.heading(on=0, depth=2)) 2925 2926 append(self.writeMapEventSummaries(events)) 2927 2928 append(fmt.div(on=0)) 2929 append(fmt.div(on=0)) 2930 append(fmt.div(on=0)) 2931 append(fmt.div(on=0)) 2932 append(fmt.listitem(on=0)) 2933 2934 append(fmt.number_list(on=0)) 2935 append(fmt.div(on=0)) 2936 append(fmt.table_cell(on=0)) 2937 append(fmt.table_row(on=0)) 2938 2939 # Write unpositioned events. 2940 2941 if unpositioned_events: 2942 unpositioned_identifier = "unpositioned-%s" % self.getIdentifier() 2943 2944 append(fmt.table_row(on=1, css_class="event-map-unpositioned", 2945 id=unpositioned_identifier)) 2946 append(fmt.table_cell(on=1)) 2947 2948 append(fmt.heading(on=1, depth=2)) 2949 append(fmt.text(_("Events not shown on the map"))) 2950 append(fmt.heading(on=0, depth=2)) 2951 2952 # Show and hide controls. 2953 2954 append(fmt.div(on=1, css_class="event-map-show-control")) 2955 append(fmt.anchorlink(on=1, name=unpositioned_identifier)) 2956 append(fmt.text(_("Show unpositioned events"))) 2957 append(fmt.anchorlink(on=0)) 2958 append(fmt.div(on=0)) 2959 2960 append(fmt.div(on=1, css_class="event-map-hide-control")) 2961 append(fmt.anchorlink(on=1, name=map_identifier)) 2962 append(fmt.text(_("Hide unpositioned events"))) 2963 append(fmt.anchorlink(on=0)) 2964 append(fmt.div(on=0)) 2965 2966 append(self.writeMapEventSummaries(unpositioned_events)) 2967 2968 # End of map view output. 2969 2970 append(fmt.table_cell(on=0)) 2971 append(fmt.table_row(on=0)) 2972 append(fmt.table(on=0)) 2973 append(fmt.div(on=0)) 2974 2975 # Output a list. 2976 2977 elif self.mode == "list": 2978 2979 # Start of list view output. 2980 2981 append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 2982 2983 # Output a list. 2984 2985 for period in self.first.until(self.last): 2986 2987 append(fmt.listitem(on=1, attr={"class" : "event-listings-period"})) 2988 append(fmt.div(on=1, attr={"class" : "event-listings-heading"})) 2989 2990 # Either write a date heading or produce links for navigable 2991 # calendars. 2992 2993 append(self.writeDateHeading(period)) 2994 2995 append(fmt.div(on=0)) 2996 2997 append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"})) 2998 2999 # Show the events in order. 3000 3001 events_in_period = getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period)) 3002 events_in_period.sort(sort_start_first) 3003 3004 for event in events_in_period: 3005 event_page = event.getPage() 3006 event_details = event.getDetails() 3007 event_summary = event.getSummary(self.parent_name) 3008 3009 append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 3010 3011 # Link to the page using the summary. 3012 3013 append(fmt.paragraph(on=1)) 3014 append(event.linkToEvent(request, event_summary)) 3015 append(fmt.paragraph(on=0)) 3016 3017 # Start and end dates. 3018 3019 append(fmt.paragraph(on=1)) 3020 append(fmt.span(on=1)) 3021 append(fmt.text(str(event_details["start"]))) 3022 append(fmt.span(on=0)) 3023 append(fmt.text(" - ")) 3024 append(fmt.span(on=1)) 3025 append(fmt.text(str(event_details["end"]))) 3026 append(fmt.span(on=0)) 3027 append(fmt.paragraph(on=0)) 3028 3029 # Location. 3030 3031 if event_details.has_key("location"): 3032 append(fmt.paragraph(on=1)) 3033 append(event_page.formatText(event_details["location"], fmt)) 3034 append(fmt.paragraph(on=1)) 3035 3036 # Topics. 3037 3038 if event_details.has_key("topics") or event_details.has_key("categories"): 3039 append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 3040 3041 for topic in event_details.get("topics") or event_details.get("categories") or []: 3042 append(fmt.listitem(on=1)) 3043 append(event_page.formatText(topic, fmt)) 3044 append(fmt.listitem(on=0)) 3045 3046 append(fmt.bullet_list(on=0)) 3047 3048 append(fmt.listitem(on=0)) 3049 3050 append(fmt.bullet_list(on=0)) 3051 3052 # End of list view output. 3053 3054 append(fmt.bullet_list(on=0)) 3055 3056 # Output a month calendar. This shows month-by-month data. 3057 3058 elif self.mode == "calendar": 3059 3060 # Visit all months in the requested range, or across known events. 3061 3062 for month in self.first.months_until(self.last): 3063 3064 # Output a month. 3065 3066 append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 3067 3068 # Either write a month heading or produce links for navigable 3069 # calendars. 3070 3071 append(self.writeMonthTableHeading(month)) 3072 3073 # Weekday headings. 3074 3075 append(self.writeWeekdayHeadings()) 3076 3077 # Process the days of the month. 3078 3079 start_weekday, number_of_days = month.month_properties() 3080 3081 # The start weekday is the weekday of day number 1. 3082 # Find the first day of the week, counting from below zero, if 3083 # necessary, in order to land on the first day of the month as 3084 # day number 1. 3085 3086 first_day = 1 - start_weekday 3087 3088 while first_day <= number_of_days: 3089 3090 # Find events in this week and determine how to mark them on the 3091 # calendar. 3092 3093 week_start = month.as_date(max(first_day, 1)) 3094 week_end = month.as_date(min(first_day + 6, number_of_days)) 3095 3096 full_coverage, week_slots = getCoverage( 3097 getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end))) 3098 3099 # Output a week, starting with the day numbers. 3100 3101 append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 3102 3103 # Either generate empty days... 3104 3105 if not week_slots: 3106 append(self.writeEmptyWeek(first_day, number_of_days)) 3107 3108 # Or generate each set of scheduled events... 3109 3110 else: 3111 append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 3112 3113 # Process the next week... 3114 3115 first_day += 7 3116 3117 # End of month. 3118 3119 append(fmt.table(on=0)) 3120 3121 # Output a day view. 3122 3123 elif self.mode == "day": 3124 3125 # Visit all days in the requested range, or across known events. 3126 3127 for date in self.first.days_until(self.last): 3128 3129 append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"})) 3130 3131 full_coverage, day_slots = getCoverage( 3132 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 3133 3134 # Work out how many columns the day title will need. 3135 # Include spacers after the scale and each event column. 3136 3137 colspan = sum(map(len, day_slots.values())) * 2 + 2 3138 3139 append(self.writeDayTableHeading(date, colspan)) 3140 3141 # Either generate empty days... 3142 3143 if not day_slots: 3144 append(self.writeEmptyDay(date)) 3145 3146 # Or generate each set of scheduled events... 3147 3148 else: 3149 append(self.writeDaySlots(date, full_coverage, day_slots)) 3150 3151 # End of day. 3152 3153 append(fmt.table(on=0)) 3154 3155 # Output view controls. 3156 3157 append(fmt.div(on=1, css_class="event-controls")) 3158 append(self.writeViewControls()) 3159 append(fmt.div(on=0)) 3160 3161 # Close the calendar region. 3162 3163 append(fmt.div(on=0)) 3164 3165 # Add any scripts. 3166 3167 if isinstance(fmt, request.html_formatter.__class__): 3168 append(self.update_script) 3169 3170 return ''.join(output) 3171 3172 update_script = """\ 3173 <script type="text/javascript"> 3174 function replaceCalendar(name, url) { 3175 var calendar = document.getElementById(name); 3176 3177 if (calendar == null) { 3178 return true; 3179 } 3180 3181 var xmlhttp = new XMLHttpRequest(); 3182 xmlhttp.open("GET", url, false); 3183 xmlhttp.send(null); 3184 3185 var newCalendar = xmlhttp.responseText; 3186 3187 if (newCalendar != null) { 3188 calendar.innerHTML = newCalendar; 3189 return false; 3190 } 3191 3192 return true; 3193 } 3194 </script> 3195 """ 3196 3197 # Event-only formatting. 3198 3199 def formatEvent(event, request, fmt, write=None): 3200 3201 """ 3202 Format the given 'event' using the 'request' and formatter 'fmt'. If the 3203 'write' parameter is specified, use it to write output. 3204 """ 3205 3206 event_details = event.getDetails() 3207 write = write or request.write 3208 3209 if event_details.has_key("fragment"): 3210 write(fmt.anchordef(event_details["fragment"])) 3211 3212 write(fmt.definition_list(on=1)) 3213 3214 for term in event.all_terms: 3215 if event_details.has_key(term): 3216 value = event_details[term] 3217 if value: 3218 write(fmt.definition_term(on=1)) 3219 write(fmt.text(term)) 3220 write(fmt.definition_term(on=0)) 3221 write(fmt.definition_desc(on=1)) 3222 if term in event.list_terms: 3223 write(", ".join([formatText(str(v), request, fmt) for v in value])) 3224 else: 3225 write(formatText(str(value), request, fmt)) 3226 write(fmt.definition_desc(on=0)) 3227 3228 write(fmt.definition_list(on=0)) 3229 3230 def formatEventsForOutputType(events, request, mimetype, parent=None, descriptions=None, latest_timestamp=None, write=None): 3231 3232 """ 3233 Format the given 'events' using the 'request' for the given 'mimetype'. 3234 3235 The optional 'parent' indicates the "natural" parent page of the events. Any 3236 event pages residing beneath the parent page will have their names 3237 reproduced as relative to the parent page. 3238 3239 The optional 'descriptions' indicates the nature of any description given 3240 for events in the output resource. 3241 3242 The optional 'latest_timestamp' indicates the timestamp of the latest edit 3243 of the page or event collection. 3244 3245 If the 'write' parameter is specified, use it to write output. 3246 """ 3247 3248 write = write or request.write 3249 3250 # Start the collection. 3251 3252 if mimetype == "text/calendar": 3253 write("BEGIN:VCALENDAR\r\n") 3254 write("PRODID:-//MoinMoin//EventAggregatorSummary\r\n") 3255 write("VERSION:2.0\r\n") 3256 3257 elif mimetype == "application/rss+xml": 3258 3259 # Using the page name and the page URL in the title, link and 3260 # description. 3261 3262 path_info = getPathInfo(request) 3263 3264 write('<rss version="2.0">\r\n') 3265 write('<channel>\r\n') 3266 write('<title>%s</title>\r\n' % path_info[1:]) 3267 write('<link>%s%s</link>\r\n' % (request.getBaseURL(), path_info)) 3268 write('<description>Events published on %s%s</description>\r\n' % (request.getBaseURL(), path_info)) 3269 3270 if latest_timestamp is not None: 3271 write('<lastBuildDate>%s</lastBuildDate>\r\n' % latest_timestamp.as_HTTP_datetime_string()) 3272 3273 # Sort the events by start date, reversed. 3274 3275 ordered_events = getOrderedEvents(events) 3276 ordered_events.reverse() 3277 events = ordered_events 3278 3279 elif mimetype == "text/html": 3280 write('<html>') 3281 write('<body>') 3282 3283 # Output the collection one by one. 3284 3285 for event in events: 3286 formatEventForOutputType(event, request, mimetype, parent, descriptions) 3287 3288 # End the collection. 3289 3290 if mimetype == "text/calendar": 3291 write("END:VCALENDAR\r\n") 3292 3293 elif mimetype == "application/rss+xml": 3294 write('</channel>\r\n') 3295 write('</rss>\r\n') 3296 3297 elif mimetype == "text/html": 3298 write('</body>') 3299 write('</html>') 3300 3301 def formatEventForOutputType(event, request, mimetype, parent=None, descriptions=None, write=None): 3302 3303 """ 3304 Format the given 'event' using the 'request' for the given 'mimetype'. 3305 3306 The optional 'parent' indicates the "natural" parent page of the events. Any 3307 event pages residing beneath the parent page will have their names 3308 reproduced as relative to the parent page. 3309 3310 The optional 'descriptions' indicates the nature of any description given 3311 for events in the output resource. 3312 3313 If the 'write' parameter is specified, use it to write output. 3314 """ 3315 3316 write = write or request.write 3317 event_details = event.getDetails() 3318 event_metadata = event.getMetadata() 3319 3320 if mimetype == "text/calendar": 3321 3322 # NOTE: A custom formatter making attributes for links and plain 3323 # NOTE: text for values could be employed here. 3324 3325 # Get the summary details. 3326 3327 event_summary = event.getSummary(parent) 3328 link = event.getEventURL() 3329 3330 # Output the event details. 3331 3332 write("BEGIN:VEVENT\r\n") 3333 write("UID:%s\r\n" % link) 3334 write("URL:%s\r\n" % link) 3335 write("DTSTAMP:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_metadata["created"].as_tuple()[:6]) 3336 write("LAST-MODIFIED:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_metadata["last-modified"].as_tuple()[:6]) 3337 write("SEQUENCE:%d\r\n" % event_metadata["sequence"]) 3338 3339 start = event_details["start"] 3340 end = event_details["end"] 3341 3342 if isinstance(start, DateTime): 3343 write("DTSTART") 3344 write_calendar_datetime(request, start) 3345 else: 3346 write("DTSTART;VALUE=DATE:%04d%02d%02d\r\n" % start.as_date().as_tuple()) 3347 3348 if isinstance(end, DateTime): 3349 write("DTEND") 3350 write_calendar_datetime(request, end) 3351 else: 3352 write("DTEND;VALUE=DATE:%04d%02d%02d\r\n" % end.next_day().as_date().as_tuple()) 3353 3354 write("SUMMARY:%s\r\n" % getQuotedText(event_summary)) 3355 3356 # Optional details. 3357 3358 if event_details.get("topics") or event_details.get("categories"): 3359 write("CATEGORIES:%s\r\n" % ",".join( 3360 [getQuotedText(topic) 3361 for topic in event_details.get("topics") or event_details.get("categories")] 3362 )) 3363 if event_details.has_key("location"): 3364 write("LOCATION:%s\r\n" % getQuotedText(event_details["location"])) 3365 if event_details.has_key("geo"): 3366 write("GEO:%s\r\n" % getQuotedText(";".join([str(ref.to_degrees()) for ref in event_details["geo"]]))) 3367 3368 write("END:VEVENT\r\n") 3369 3370 elif mimetype == "application/rss+xml": 3371 3372 event_page = event.getPage() 3373 event_details = event.getDetails() 3374 3375 # Get a parser and formatter for the formatting of some attributes. 3376 3377 fmt = request.html_formatter 3378 3379 # Get the summary details. 3380 3381 event_summary = event.getSummary(parent) 3382 link = event.getEventURL() 3383 3384 write('<item>\r\n') 3385 write('<title>%s</title>\r\n' % escape(event_summary)) 3386 write('<link>%s</link>\r\n' % link) 3387 3388 # Write a description according to the preferred source of 3389 # descriptions. 3390 3391 if descriptions == "page": 3392 description = event_details.get("description", "") 3393 else: 3394 description = event_metadata["last-comment"] 3395 3396 write('<description>%s</description>\r\n' % 3397 fmt.text(event_page.formatText(description, fmt))) 3398 3399 for topic in event_details.get("topics") or event_details.get("categories") or []: 3400 write('<category>%s</category>\r\n' % 3401 fmt.text(event_page.formatText(topic, fmt))) 3402 3403 write('<pubDate>%s</pubDate>\r\n' % event_metadata["created"].as_HTTP_datetime_string()) 3404 write('<guid>%s#%s</guid>\r\n' % (link, event_metadata["sequence"])) 3405 write('</item>\r\n') 3406 3407 elif mimetype == "text/html": 3408 fmt = request.html_formatter 3409 fmt.setPage(request.page) 3410 formatEvent(event, request, fmt, write=write) 3411 3412 # iCalendar format helper functions. 3413 3414 def write_calendar_datetime(request, datetime): 3415 3416 """ 3417 Write to the given 'request' the 'datetime' using appropriate time zone 3418 information. 3419 """ 3420 3421 utc_datetime = datetime.to_utc() 3422 if utc_datetime: 3423 request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02dZ\r\n" % utc_datetime.padded().as_tuple()[:-1]) 3424 else: 3425 zone = datetime.time_zone() 3426 if zone: 3427 request.write(";TZID=/%s" % zone) 3428 request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02d\r\n" % datetime.padded().as_tuple()[:-1]) 3429 3430 def getQuotedText(text): 3431 3432 "Return the 'text' quoted for iCalendar purposes." 3433 3434 return text.replace(";", r"\;").replace(",", r"\,").replace("\n", "\\n") 3435 3436 # vim: tabstop=4 expandtab shiftwidth=4