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