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, search_pattern, template_name, 1284 parent_name, mode, 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', 'search_pattern', 1298 'template_name', 'parent_name' and 'mode' parameters are used to 1299 configure the links 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 # Search-related parameters for links. 1327 1328 self.category_name_parameters = "&".join([("category=%s" % name) for name in category_names]) 1329 self.remote_source_parameters = "&".join([("source=%s" % source) for source in remote_sources]) 1330 self.search_pattern = search_pattern 1331 1332 # Calculate the duration in terms of the highest common unit of time. 1333 1334 self.first = first 1335 self.last = last 1336 self.duration = abs(last - first) + 1 1337 1338 if self.calendar_name: 1339 1340 # Store the view parameters. 1341 1342 self.previous_start = first.previous() 1343 self.next_start = first.next() 1344 self.previous_end = last.previous() 1345 self.next_end = last.next() 1346 1347 self.previous_set_start = first.update(-self.duration) 1348 self.next_set_start = first.update(self.duration) 1349 self.previous_set_end = last.update(-self.duration) 1350 self.next_set_end = last.update(self.duration) 1351 1352 def getIdentifier(self): 1353 1354 "Return a unique identifier to be used to refer to this view." 1355 1356 # NOTE: Nasty hack to get a unique identifier if no name is given. 1357 1358 return self.calendar_name or str(id(self)) 1359 1360 def getQualifiedParameterName(self, argname): 1361 1362 "Return the 'argname' qualified using the calendar name." 1363 1364 return getQualifiedParameterName(self.calendar_name, argname) 1365 1366 def getDateQueryString(self, argname, date, prefix=1): 1367 1368 """ 1369 Return a query string fragment for the given 'argname', referring to the 1370 month given by the specified 'year_month' object, appropriate for this 1371 calendar. 1372 1373 If 'prefix' is specified and set to a false value, the parameters in the 1374 query string will not be calendar-specific, but could be used with the 1375 summary action. 1376 """ 1377 1378 suffixes = ["year", "month", "day"] 1379 1380 if date is not None: 1381 args = [] 1382 for suffix, value in zip(suffixes, date.as_tuple()): 1383 suffixed_argname = "%s-%s" % (argname, suffix) 1384 if prefix: 1385 suffixed_argname = self.getQualifiedParameterName(suffixed_argname) 1386 args.append("%s=%s" % (suffixed_argname, value)) 1387 return "&".join(args) 1388 else: 1389 return "" 1390 1391 def getRawDateQueryString(self, argname, date, prefix=1): 1392 1393 """ 1394 Return a query string fragment for the given 'argname', referring to the 1395 date given by the specified 'date' value, appropriate for this 1396 calendar. 1397 1398 If 'prefix' is specified and set to a false value, the parameters in the 1399 query string will not be calendar-specific, but could be used with the 1400 summary action. 1401 """ 1402 1403 if date is not None: 1404 if prefix: 1405 argname = self.getQualifiedParameterName(argname) 1406 return "%s=%s" % (argname, wikiutil.url_quote_plus(date)) 1407 else: 1408 return "" 1409 1410 def getNavigationLink(self, start, end, mode=None, resolution=None): 1411 1412 """ 1413 Return a query string fragment for navigation to a view showing months 1414 from 'start' to 'end' inclusive, with the optional 'mode' indicating the 1415 view style and the optional 'resolution' indicating the resolution of a 1416 view, if configurable. 1417 """ 1418 1419 return "%s&%s&%s=%s&%s=%s" % ( 1420 self.getRawDateQueryString("start", start), 1421 self.getRawDateQueryString("end", end), 1422 self.getQualifiedParameterName("mode"), mode or self.mode, 1423 self.getQualifiedParameterName("resolution"), resolution or self.resolution 1424 ) 1425 1426 def getUpdateLink(self, start, end, mode=None, resolution=None): 1427 1428 """ 1429 Return a query string fragment for navigation to a view showing months 1430 from 'start' to 'end' inclusive, with the optional 'mode' indicating the 1431 view style and the optional 'resolution' indicating the resolution of a 1432 view, if configurable. This link differs from the conventional 1433 navigation link in that it is sufficient to activate the update action 1434 and produce an updated region of the page without needing to locate and 1435 process the page or any macro invocation. 1436 """ 1437 1438 parameters = [ 1439 self.getRawDateQueryString("start", start, 0), 1440 self.getRawDateQueryString("end", end, 0), 1441 self.category_name_parameters, 1442 self.remote_source_parameters, 1443 ] 1444 1445 pairs = [ 1446 ("calendar", self.calendar_name or ""), 1447 ("calendarstart", self.raw_calendar_start or ""), 1448 ("calendarend", self.raw_calendar_end or ""), 1449 ("mode", mode or self.mode), 1450 ("resolution", resolution or self.resolution), 1451 ("parent", self.parent_name or ""), 1452 ("template", self.template_name or ""), 1453 ("names", self.name_usage), 1454 ("map", self.map_name or ""), 1455 ("search", self.search_pattern or ""), 1456 ] 1457 1458 url = self.page.url(self.page.request, 1459 "action=EventAggregatorUpdate&%s" % ( 1460 "&".join([("%s=%s" % pair) for pair in pairs] + parameters) 1461 ), relative=True) 1462 1463 return "return replaceCalendar('EventAggregator-%s', '%s')" % (self.getIdentifier(), url) 1464 1465 def getNewEventLink(self, start): 1466 1467 """ 1468 Return a query string activating the new event form, incorporating the 1469 calendar parameters, specialising the form for the given 'start' date or 1470 month. 1471 """ 1472 1473 if start is not None: 1474 details = start.as_tuple() 1475 pairs = zip(["start-year=%d", "start-month=%d", "start-day=%d"], details) 1476 args = [(param % value) for (param, value) in pairs] 1477 args = "&".join(args) 1478 else: 1479 args = "" 1480 1481 # Prepare navigation details for the calendar shown with the new event 1482 # form. 1483 1484 navigation_link = self.getNavigationLink( 1485 self.calendar_start, self.calendar_end 1486 ) 1487 1488 return "action=EventAggregatorNewEvent%s%s&template=%s&parent=%s&%s" % ( 1489 args and "&%s" % args, 1490 self.category_name_parameters and "&%s" % self.category_name_parameters, 1491 self.template_name, self.parent_name or "", 1492 navigation_link) 1493 1494 def getFullDateLabel(self, date): 1495 page = self.page 1496 request = page.request 1497 return getFullDateLabel(request, date) 1498 1499 def getFullMonthLabel(self, year_month): 1500 page = self.page 1501 request = page.request 1502 return getFullMonthLabel(request, year_month) 1503 1504 def getFullLabel(self, arg): 1505 return self.resolution == "date" and self.getFullDateLabel(arg) or self.getFullMonthLabel(arg) 1506 1507 def _getCalendarPeriod(self, start_label, end_label, default_label): 1508 output = [] 1509 append = output.append 1510 1511 if start_label: 1512 append(start_label) 1513 if end_label and start_label != end_label: 1514 if output: 1515 append(" - ") 1516 append(end_label) 1517 return "".join(output) or default_label 1518 1519 def getCalendarPeriod(self): 1520 _ = self.page.request.getText 1521 return self._getCalendarPeriod( 1522 self.calendar_start and self.getFullLabel(self.calendar_start), 1523 self.calendar_end and self.getFullLabel(self.calendar_end), 1524 _("All events") 1525 ) 1526 1527 def getOriginalCalendarPeriod(self): 1528 _ = self.page.request.getText 1529 return self._getCalendarPeriod( 1530 self.original_calendar_start and self.getFullLabel(self.original_calendar_start), 1531 self.original_calendar_end and self.getFullLabel(self.original_calendar_end), 1532 _("All events") 1533 ) 1534 1535 def getRawCalendarPeriod(self): 1536 _ = self.page.request.getText 1537 return self._getCalendarPeriod( 1538 self.raw_calendar_start, 1539 self.raw_calendar_end, 1540 _("No period specified") 1541 ) 1542 1543 def writeDownloadControls(self): 1544 1545 """ 1546 Return a representation of the download controls, featuring links for 1547 view, calendar and customised downloads and subscriptions. 1548 """ 1549 1550 page = self.page 1551 request = page.request 1552 fmt = request.formatter 1553 _ = request.getText 1554 1555 output = [] 1556 append = output.append 1557 1558 # The full URL is needed for webcal links. 1559 1560 full_url = "%s%s" % (request.getBaseURL(), getPathInfo(request)) 1561 1562 # Generate the links. 1563 1564 download_dialogue_link = "action=EventAggregatorSummary&parent=%s&resolution=%s&search=%s%s%s" % ( 1565 self.parent_name or "", 1566 self.resolution, 1567 self.search_pattern, 1568 self.category_name_parameters and "&%s" % self.category_name_parameters, 1569 self.remote_source_parameters and "&%s" % self.remote_source_parameters 1570 ) 1571 download_all_link = download_dialogue_link + "&doit=1" 1572 download_link = download_all_link + ("&%s&%s" % ( 1573 self.getDateQueryString("start", self.calendar_start, prefix=0), 1574 self.getDateQueryString("end", self.calendar_end, prefix=0) 1575 )) 1576 1577 # Subscription links just explicitly select the RSS format. 1578 1579 subscribe_dialogue_link = download_dialogue_link + "&format=RSS" 1580 subscribe_all_link = download_all_link + "&format=RSS" 1581 subscribe_link = download_link + "&format=RSS" 1582 1583 # Adjust the "download all" and "subscribe all" links if the calendar 1584 # has an inherent period associated with it. 1585 1586 period_limits = [] 1587 1588 if self.raw_calendar_start: 1589 period_limits.append("&%s" % 1590 self.getRawDateQueryString("start", self.raw_calendar_start, prefix=0) 1591 ) 1592 if self.raw_calendar_end: 1593 period_limits.append("&%s" % 1594 self.getRawDateQueryString("end", self.raw_calendar_end, prefix=0) 1595 ) 1596 1597 period_limits = "".join(period_limits) 1598 1599 download_dialogue_link += period_limits 1600 download_all_link += period_limits 1601 subscribe_dialogue_link += period_limits 1602 subscribe_all_link += period_limits 1603 1604 # Pop-up descriptions of the downloadable calendars. 1605 1606 calendar_period = self.getCalendarPeriod() 1607 original_calendar_period = self.getOriginalCalendarPeriod() 1608 raw_calendar_period = self.getRawCalendarPeriod() 1609 1610 # Write the controls. 1611 1612 # Download controls. 1613 1614 append(fmt.div(on=1, css_class="event-download-controls")) 1615 1616 append(fmt.span(on=1, css_class="event-download")) 1617 append(fmt.text(_("Download..."))) 1618 append(fmt.div(on=1, css_class="event-download-popup")) 1619 1620 append(fmt.div(on=1, css_class="event-download-item")) 1621 append(fmt.span(on=1, css_class="event-download-types")) 1622 append(fmt.span(on=1, css_class="event-download-webcal")) 1623 append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_link)) 1624 append(fmt.span(on=0)) 1625 append(fmt.span(on=1, css_class="event-download-http")) 1626 append(linkToPage(request, page, _("http"), download_link)) 1627 append(fmt.span(on=0)) 1628 append(fmt.span(on=0)) # end types 1629 append(fmt.span(on=1, css_class="event-download-label")) 1630 append(fmt.text(_("Download this view"))) 1631 append(fmt.span(on=0)) # end label 1632 append(fmt.span(on=1, css_class="event-download-period")) 1633 append(fmt.text(calendar_period)) 1634 append(fmt.span(on=0)) 1635 append(fmt.div(on=0)) 1636 1637 append(fmt.div(on=1, css_class="event-download-item")) 1638 append(fmt.span(on=1, css_class="event-download-types")) 1639 append(fmt.span(on=1, css_class="event-download-webcal")) 1640 append(linkToResource(full_url.replace("http", "webcal", 1), request, _("webcal"), download_all_link)) 1641 append(fmt.span(on=0)) 1642 append(fmt.span(on=1, css_class="event-download-http")) 1643 append(linkToPage(request, page, _("http"), download_all_link)) 1644 append(fmt.span(on=0)) 1645 append(fmt.span(on=0)) # end types 1646 append(fmt.span(on=1, css_class="event-download-label")) 1647 append(fmt.text(_("Download this calendar"))) 1648 append(fmt.span(on=0)) # end label 1649 append(fmt.span(on=1, css_class="event-download-period")) 1650 append(fmt.text(original_calendar_period)) 1651 append(fmt.span(on=0)) 1652 append(fmt.span(on=1, css_class="event-download-period-raw")) 1653 append(fmt.text(raw_calendar_period)) 1654 append(fmt.span(on=0)) 1655 append(fmt.div(on=0)) 1656 1657 append(fmt.div(on=1, css_class="event-download-item")) 1658 append(fmt.span(on=1, css_class="event-download-link")) 1659 append(linkToPage(request, page, _("Edit download options..."), download_dialogue_link)) 1660 append(fmt.span(on=0)) # end label 1661 append(fmt.div(on=0)) 1662 1663 append(fmt.div(on=0)) # end of pop-up 1664 append(fmt.span(on=0)) # end of download 1665 1666 # Subscription controls. 1667 1668 append(fmt.span(on=1, css_class="event-download")) 1669 append(fmt.text(_("Subscribe..."))) 1670 append(fmt.div(on=1, css_class="event-download-popup")) 1671 1672 append(fmt.div(on=1, css_class="event-download-item")) 1673 append(fmt.span(on=1, css_class="event-download-label")) 1674 append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link)) 1675 append(fmt.span(on=0)) # end label 1676 append(fmt.span(on=1, css_class="event-download-period")) 1677 append(fmt.text(calendar_period)) 1678 append(fmt.span(on=0)) 1679 append(fmt.div(on=0)) 1680 1681 append(fmt.div(on=1, css_class="event-download-item")) 1682 append(fmt.span(on=1, css_class="event-download-label")) 1683 append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link)) 1684 append(fmt.span(on=0)) # end label 1685 append(fmt.span(on=1, css_class="event-download-period")) 1686 append(fmt.text(original_calendar_period)) 1687 append(fmt.span(on=0)) 1688 append(fmt.span(on=1, css_class="event-download-period-raw")) 1689 append(fmt.text(raw_calendar_period)) 1690 append(fmt.span(on=0)) 1691 append(fmt.div(on=0)) 1692 1693 append(fmt.div(on=1, css_class="event-download-item")) 1694 append(fmt.span(on=1, css_class="event-download-link")) 1695 append(linkToPage(request, page, _("Edit subscription options..."), subscribe_dialogue_link)) 1696 append(fmt.span(on=0)) # end label 1697 append(fmt.div(on=0)) 1698 1699 append(fmt.div(on=0)) # end of pop-up 1700 append(fmt.span(on=0)) # end of download 1701 1702 append(fmt.div(on=0)) # end of controls 1703 1704 return "".join(output) 1705 1706 def writeViewControls(self): 1707 1708 """ 1709 Return a representation of the view mode controls, permitting viewing of 1710 aggregated events in calendar, list or table form. 1711 """ 1712 1713 page = self.page 1714 request = page.request 1715 fmt = request.formatter 1716 _ = request.getText 1717 1718 output = [] 1719 append = output.append 1720 1721 start = self.calendar_start 1722 end = self.calendar_end 1723 1724 help_page = Page(request, "HelpOnEventAggregator") 1725 calendar_link = self.getNavigationLink(start and start.as_month(), end and end.as_month(), "calendar", "month") 1726 calendar_update_link = self.getUpdateLink(start and start.as_month(), end and end.as_month(), "calendar", "month") 1727 list_link = self.getNavigationLink(start, end, "list") 1728 list_update_link = self.getUpdateLink(start, end, "list") 1729 table_link = self.getNavigationLink(start, end, "table") 1730 table_update_link = self.getUpdateLink(start, end, "table") 1731 map_link = self.getNavigationLink(start, end, "map") 1732 map_update_link = self.getUpdateLink(start, end, "map") 1733 new_event_link = self.getNewEventLink(start) 1734 1735 # Write the controls. 1736 1737 append(fmt.div(on=1, css_class="event-view-controls")) 1738 1739 append(fmt.span(on=1, css_class="event-view")) 1740 append(linkToPage(request, help_page, _("Help"))) 1741 append(fmt.span(on=0)) 1742 1743 append(fmt.span(on=1, css_class="event-view")) 1744 append(linkToPage(request, page, _("New event"), new_event_link)) 1745 append(fmt.span(on=0)) 1746 1747 if self.mode != "calendar": 1748 append(fmt.span(on=1, css_class="event-view")) 1749 append(linkToPage(request, page, _("View as calendar"), calendar_link, onclick=calendar_update_link)) 1750 append(fmt.span(on=0)) 1751 1752 if self.mode != "list": 1753 append(fmt.span(on=1, css_class="event-view")) 1754 append(linkToPage(request, page, _("View as list"), list_link, onclick=list_update_link)) 1755 append(fmt.span(on=0)) 1756 1757 if self.mode != "table": 1758 append(fmt.span(on=1, css_class="event-view")) 1759 append(linkToPage(request, page, _("View as table"), table_link, onclick=table_update_link)) 1760 append(fmt.span(on=0)) 1761 1762 if self.mode != "map" and self.map_name: 1763 append(fmt.span(on=1, css_class="event-view")) 1764 append(linkToPage(request, page, _("View as map"), map_link, onclick=map_update_link)) 1765 append(fmt.span(on=0)) 1766 1767 append(fmt.div(on=0)) 1768 1769 return "".join(output) 1770 1771 def writeMapHeading(self): 1772 1773 """ 1774 Return the calendar heading for the current calendar, providing links 1775 permitting navigation to other periods. 1776 """ 1777 1778 label = self.getCalendarPeriod() 1779 1780 if self.raw_calendar_start is None or self.raw_calendar_end is None: 1781 fmt = self.page.request.formatter 1782 output = [] 1783 append = output.append 1784 append(fmt.span(on=1)) 1785 append(fmt.text(label)) 1786 append(fmt.span(on=0)) 1787 return "".join(output) 1788 else: 1789 return self._writeCalendarHeading(label, self.calendar_start, self.calendar_end) 1790 1791 def writeDateHeading(self, date): 1792 if isinstance(date, Date): 1793 return self.writeDayHeading(date) 1794 else: 1795 return self.writeMonthHeading(date) 1796 1797 def writeMonthHeading(self, year_month): 1798 1799 """ 1800 Return the calendar heading for the given 'year_month' (a Month object) 1801 providing links permitting navigation to other months. 1802 """ 1803 1804 full_month_label = self.getFullMonthLabel(year_month) 1805 end_month = year_month.update(self.duration - 1) 1806 return self._writeCalendarHeading(full_month_label, year_month, end_month) 1807 1808 def writeDayHeading(self, date): 1809 1810 """ 1811 Return the calendar heading for the given 'date' (a Date object) 1812 providing links permitting navigation to other dates. 1813 """ 1814 1815 full_date_label = self.getFullDateLabel(date) 1816 end_date = date.update(self.duration - 1) 1817 return self._writeCalendarHeading(full_date_label, date, end_date) 1818 1819 def _writeCalendarHeading(self, label, start, end): 1820 1821 """ 1822 Write a calendar heading providing links permitting navigation to other 1823 periods, using the given 'label' along with the 'start' and 'end' dates 1824 to provide a link to a particular period. 1825 """ 1826 1827 page = self.page 1828 request = page.request 1829 fmt = request.formatter 1830 _ = request.getText 1831 1832 output = [] 1833 append = output.append 1834 1835 # Prepare navigation links. 1836 1837 if self.calendar_name: 1838 calendar_name = self.calendar_name 1839 1840 # Links to the previous set of months and to a calendar shifted 1841 # back one month. 1842 1843 previous_set_link = self.getNavigationLink( 1844 self.previous_set_start, self.previous_set_end 1845 ) 1846 previous_link = self.getNavigationLink( 1847 self.previous_start, self.previous_end 1848 ) 1849 previous_set_update_link = self.getUpdateLink( 1850 self.previous_set_start, self.previous_set_end 1851 ) 1852 previous_update_link = self.getUpdateLink( 1853 self.previous_start, self.previous_end 1854 ) 1855 1856 # Links to the next set of months and to a calendar shifted 1857 # forward one month. 1858 1859 next_set_link = self.getNavigationLink( 1860 self.next_set_start, self.next_set_end 1861 ) 1862 next_link = self.getNavigationLink( 1863 self.next_start, self.next_end 1864 ) 1865 next_set_update_link = self.getUpdateLink( 1866 self.next_set_start, self.next_set_end 1867 ) 1868 next_update_link = self.getUpdateLink( 1869 self.next_start, self.next_end 1870 ) 1871 1872 # A link leading to this date being at the top of the calendar. 1873 1874 date_link = self.getNavigationLink(start, end) 1875 date_update_link = self.getUpdateLink(start, end) 1876 1877 append(fmt.span(on=1, css_class="previous")) 1878 append(linkToPage(request, page, "<<", previous_set_link, onclick=previous_set_update_link)) 1879 append(fmt.text(" ")) 1880 append(linkToPage(request, page, "<", previous_link, onclick=previous_update_link)) 1881 append(fmt.span(on=0)) 1882 1883 append(fmt.span(on=1, css_class="next")) 1884 append(linkToPage(request, page, ">", next_link, onclick=next_update_link)) 1885 append(fmt.text(" ")) 1886 append(linkToPage(request, page, ">>", next_set_link, onclick=next_set_update_link)) 1887 append(fmt.span(on=0)) 1888 1889 append(linkToPage(request, page, label, date_link, onclick=date_update_link)) 1890 1891 else: 1892 append(fmt.span(on=1)) 1893 append(fmt.text(label)) 1894 append(fmt.span(on=0)) 1895 1896 return "".join(output) 1897 1898 def writeDayNumberHeading(self, date, busy): 1899 1900 """ 1901 Return a link for the given 'date' which will activate the new event 1902 action for the given day. If 'busy' is given as a true value, the 1903 heading will be marked as busy. 1904 """ 1905 1906 page = self.page 1907 request = page.request 1908 fmt = request.formatter 1909 _ = request.getText 1910 1911 output = [] 1912 append = output.append 1913 1914 year, month, day = date.as_tuple() 1915 new_event_link = self.getNewEventLink(date) 1916 1917 # Prepare a link to the day view for this day. 1918 1919 day_view_link = self.getNavigationLink(date, date, "day", "date") 1920 day_view_update_link = self.getUpdateLink(date, date, "day", "date") 1921 1922 # Output the heading class. 1923 1924 today_attr = date == getCurrentDate() and "event-day-current" or "" 1925 1926 append( 1927 fmt.table_cell(on=1, attrs={ 1928 "class" : "event-day-heading event-day-%s %s" % (busy and "busy" or "empty", today_attr), 1929 "colspan" : "3" 1930 })) 1931 1932 # Output the number and pop-up menu. 1933 1934 append(fmt.div(on=1, css_class="event-day-box")) 1935 1936 append(fmt.span(on=1, css_class="event-day-number-popup")) 1937 append(fmt.span(on=1, css_class="event-day-number-link")) 1938 append(linkToPage(request, page, _("View day"), day_view_link, onclick=day_view_update_link)) 1939 append(fmt.span(on=0)) 1940 append(fmt.span(on=1, css_class="event-day-number-link")) 1941 append(linkToPage(request, page, _("New event"), new_event_link)) 1942 append(fmt.span(on=0)) 1943 append(fmt.span(on=0)) 1944 1945 append(fmt.span(on=1, css_class="event-day-number")) 1946 append(fmt.text(unicode(day))) 1947 append(fmt.span(on=0)) 1948 1949 append(fmt.div(on=0)) 1950 1951 # End of heading. 1952 1953 append(fmt.table_cell(on=0)) 1954 1955 return "".join(output) 1956 1957 # Common layout methods. 1958 1959 def getEventStyle(self, colour_seed): 1960 1961 "Generate colour style information using the given 'colour_seed'." 1962 1963 bg = getColour(colour_seed) 1964 fg = getBlackOrWhite(bg) 1965 return "background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg) 1966 1967 def writeEventSummaryBox(self, event): 1968 1969 "Return an event summary box linking to the given 'event'." 1970 1971 page = self.page 1972 request = page.request 1973 fmt = request.formatter 1974 1975 output = [] 1976 append = output.append 1977 1978 event_details = event.getDetails() 1979 event_summary = event.getSummary(self.parent_name) 1980 1981 is_ambiguous = event.as_timespan().ambiguous() 1982 style = self.getEventStyle(event_summary) 1983 1984 # The event box contains the summary, alongside 1985 # other elements. 1986 1987 append(fmt.div(on=1, css_class="event-summary-box")) 1988 append(fmt.div(on=1, css_class="event-summary", style=style)) 1989 1990 if is_ambiguous: 1991 append(fmt.icon("/!\\")) 1992 1993 append(event.linkToEvent(request, event_summary)) 1994 append(fmt.div(on=0)) 1995 1996 # Add a pop-up element for long summaries. 1997 1998 append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 1999 2000 if is_ambiguous: 2001 append(fmt.icon("/!\\")) 2002 2003 append(event.linkToEvent(request, event_summary)) 2004 append(fmt.div(on=0)) 2005 2006 append(fmt.div(on=0)) 2007 2008 return "".join(output) 2009 2010 # Calendar layout methods. 2011 2012 def writeMonthTableHeading(self, year_month): 2013 page = self.page 2014 fmt = page.request.formatter 2015 2016 output = [] 2017 append = output.append 2018 2019 append(fmt.table_row(on=1)) 2020 append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"})) 2021 2022 append(self.writeMonthHeading(year_month)) 2023 2024 append(fmt.table_cell(on=0)) 2025 append(fmt.table_row(on=0)) 2026 2027 return "".join(output) 2028 2029 def writeWeekdayHeadings(self): 2030 page = self.page 2031 request = page.request 2032 fmt = request.formatter 2033 _ = request.getText 2034 2035 output = [] 2036 append = output.append 2037 2038 append(fmt.table_row(on=1)) 2039 2040 for weekday in range(0, 7): 2041 append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"})) 2042 append(fmt.text(_(getDayLabel(weekday)))) 2043 append(fmt.table_cell(on=0)) 2044 2045 append(fmt.table_row(on=0)) 2046 return "".join(output) 2047 2048 def writeDayNumbers(self, first_day, number_of_days, month, coverage): 2049 page = self.page 2050 fmt = page.request.formatter 2051 2052 output = [] 2053 append = output.append 2054 2055 append(fmt.table_row(on=1)) 2056 2057 for weekday in range(0, 7): 2058 day = first_day + weekday 2059 date = month.as_date(day) 2060 2061 # Output out-of-month days. 2062 2063 if day < 1 or day > number_of_days: 2064 append(fmt.table_cell(on=1, 2065 attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"})) 2066 append(fmt.table_cell(on=0)) 2067 2068 # Output normal days. 2069 2070 else: 2071 # Output the day heading, making a link to a new event 2072 # action. 2073 2074 append(self.writeDayNumberHeading(date, date in coverage)) 2075 2076 # End of day numbers. 2077 2078 append(fmt.table_row(on=0)) 2079 return "".join(output) 2080 2081 def writeEmptyWeek(self, first_day, number_of_days, month): 2082 page = self.page 2083 fmt = page.request.formatter 2084 2085 output = [] 2086 append = output.append 2087 2088 append(fmt.table_row(on=1)) 2089 2090 for weekday in range(0, 7): 2091 day = first_day + weekday 2092 date = month.as_date(day) 2093 2094 today_attr = date == getCurrentDate() and "event-day-current" or "" 2095 2096 # Output out-of-month days. 2097 2098 if day < 1 or day > number_of_days: 2099 append(fmt.table_cell(on=1, 2100 attrs={"class" : "event-day-content event-day-excluded %s" % today_attr, "colspan" : "3"})) 2101 append(fmt.table_cell(on=0)) 2102 2103 # Output empty days. 2104 2105 else: 2106 append(fmt.table_cell(on=1, 2107 attrs={"class" : "event-day-content event-day-empty %s" % today_attr, "colspan" : "3"})) 2108 2109 append(fmt.table_row(on=0)) 2110 return "".join(output) 2111 2112 def writeWeekSlots(self, first_day, number_of_days, month, week_end, week_slots): 2113 output = [] 2114 append = output.append 2115 2116 locations = week_slots.keys() 2117 locations.sort(sort_none_first) 2118 2119 # Visit each slot corresponding to a location (or no location). 2120 2121 for location in locations: 2122 2123 # Visit each coverage span, presenting the events in the span. 2124 2125 for events in week_slots[location]: 2126 2127 # Output each set. 2128 2129 append(self.writeWeekSlot(first_day, number_of_days, month, week_end, events)) 2130 2131 # Add a spacer. 2132 2133 append(self.writeWeekSpacer(first_day, number_of_days, month)) 2134 2135 return "".join(output) 2136 2137 def writeWeekSlot(self, first_day, number_of_days, month, week_end, events): 2138 page = self.page 2139 request = page.request 2140 fmt = request.formatter 2141 2142 output = [] 2143 append = output.append 2144 2145 append(fmt.table_row(on=1)) 2146 2147 # Then, output day details. 2148 2149 for weekday in range(0, 7): 2150 day = first_day + weekday 2151 date = month.as_date(day) 2152 2153 # Skip out-of-month days. 2154 2155 if day < 1 or day > number_of_days: 2156 append(fmt.table_cell(on=1, 2157 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 2158 append(fmt.table_cell(on=0)) 2159 continue 2160 2161 # Output the day. 2162 # Where a day does not contain an event, a single cell is used. 2163 # Otherwise, multiple cells are used to provide space before, during 2164 # and after events. 2165 2166 today_attr = date == getCurrentDate() and "event-day-current" or "" 2167 2168 if date not in events: 2169 append(fmt.table_cell(on=1, 2170 attrs={"class" : "event-day-content event-day-empty %s" % today_attr, "colspan" : "3"})) 2171 2172 # Get event details for the current day. 2173 2174 for event in events: 2175 event_details = event.getDetails() 2176 2177 if date not in event: 2178 continue 2179 2180 # Get basic properties of the event. 2181 2182 starts_today = event_details["start"] == date 2183 ends_today = event_details["end"] == date 2184 event_summary = event.getSummary(self.parent_name) 2185 2186 style = self.getEventStyle(event_summary) 2187 2188 # Determine if the event name should be shown. 2189 2190 start_of_period = starts_today or weekday == 0 or day == 1 2191 2192 if self.name_usage == "daily" or start_of_period: 2193 hide_text = 0 2194 else: 2195 hide_text = 1 2196 2197 # Output start of day gap and determine whether 2198 # any event content should be explicitly output 2199 # for this day. 2200 2201 if starts_today: 2202 2203 # Single day events... 2204 2205 if ends_today: 2206 colspan = 3 2207 event_day_type = "event-day-single" 2208 2209 # Events starting today... 2210 2211 else: 2212 append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap %s" % today_attr})) 2213 append(fmt.table_cell(on=0)) 2214 2215 # Calculate the span of this cell. 2216 # Events whose names appear on every day... 2217 2218 if self.name_usage == "daily": 2219 colspan = 2 2220 event_day_type = "event-day-starting" 2221 2222 # Events whose names appear once per week... 2223 2224 else: 2225 if event_details["end"] <= week_end: 2226 event_length = event_details["end"].day() - day + 1 2227 colspan = (event_length - 2) * 3 + 4 2228 else: 2229 event_length = week_end.day() - day + 1 2230 colspan = (event_length - 1) * 3 + 2 2231 2232 event_day_type = "event-day-multiple" 2233 2234 # Events continuing from a previous week... 2235 2236 elif start_of_period: 2237 2238 # End of continuing event... 2239 2240 if ends_today: 2241 colspan = 2 2242 event_day_type = "event-day-ending" 2243 2244 # Events continuing for at least one more day... 2245 2246 else: 2247 2248 # Calculate the span of this cell. 2249 # Events whose names appear on every day... 2250 2251 if self.name_usage == "daily": 2252 colspan = 3 2253 event_day_type = "event-day-full" 2254 2255 # Events whose names appear once per week... 2256 2257 else: 2258 if event_details["end"] <= week_end: 2259 event_length = event_details["end"].day() - day + 1 2260 colspan = (event_length - 1) * 3 + 2 2261 else: 2262 event_length = week_end.day() - day + 1 2263 colspan = event_length * 3 2264 2265 event_day_type = "event-day-multiple" 2266 2267 # Continuing events whose names appear on every day... 2268 2269 elif self.name_usage == "daily": 2270 if ends_today: 2271 colspan = 2 2272 event_day_type = "event-day-ending" 2273 else: 2274 colspan = 3 2275 event_day_type = "event-day-full" 2276 2277 # Continuing events whose names appear once per week... 2278 2279 else: 2280 colspan = None 2281 2282 # Output the main content only if it is not 2283 # continuing from a previous day. 2284 2285 if colspan is not None: 2286 2287 # Colour the cell for continuing events. 2288 2289 attrs={ 2290 "class" : "event-day-content event-day-busy %s %s" % (event_day_type, today_attr), 2291 "colspan" : str(colspan) 2292 } 2293 2294 if not (starts_today and ends_today): 2295 attrs["style"] = style 2296 2297 append(fmt.table_cell(on=1, attrs=attrs)) 2298 2299 # Output the event. 2300 2301 if starts_today and ends_today or not hide_text: 2302 append(self.writeEventSummaryBox(event)) 2303 2304 append(fmt.table_cell(on=0)) 2305 2306 # Output end of day gap. 2307 2308 if ends_today and not starts_today: 2309 append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap %s" % today_attr})) 2310 append(fmt.table_cell(on=0)) 2311 2312 # End of set. 2313 2314 append(fmt.table_row(on=0)) 2315 return "".join(output) 2316 2317 def writeWeekSpacer(self, first_day, number_of_days, month): 2318 page = self.page 2319 fmt = page.request.formatter 2320 2321 output = [] 2322 append = output.append 2323 2324 append(fmt.table_row(on=1)) 2325 2326 for weekday in range(0, 7): 2327 day = first_day + weekday 2328 date = month.as_date(day) 2329 today_attr = date == getCurrentDate() and "event-day-current" or "" 2330 2331 css_classes = "event-day-spacer %s" % today_attr 2332 2333 # Skip out-of-month days. 2334 2335 if day < 1 or day > number_of_days: 2336 css_classes += " event-day-excluded" 2337 2338 append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"})) 2339 append(fmt.table_cell(on=0)) 2340 2341 append(fmt.table_row(on=0)) 2342 return "".join(output) 2343 2344 # Day layout methods. 2345 2346 def writeDayTableHeading(self, date, colspan=1): 2347 page = self.page 2348 fmt = page.request.formatter 2349 2350 output = [] 2351 append = output.append 2352 2353 append(fmt.table_row(on=1)) 2354 2355 append(fmt.table_cell(on=1, attrs={"class" : "event-full-day-heading", "colspan" : str(colspan)})) 2356 append(self.writeDayHeading(date)) 2357 append(fmt.table_cell(on=0)) 2358 2359 append(fmt.table_row(on=0)) 2360 return "".join(output) 2361 2362 def writeEmptyDay(self, date): 2363 page = self.page 2364 fmt = page.request.formatter 2365 2366 output = [] 2367 append = output.append 2368 2369 append(fmt.table_row(on=1)) 2370 2371 append(fmt.table_cell(on=1, 2372 attrs={"class" : "event-day-content event-day-empty"})) 2373 2374 append(fmt.table_row(on=0)) 2375 return "".join(output) 2376 2377 def writeDaySlots(self, date, full_coverage, day_slots): 2378 2379 """ 2380 Given a 'date', non-empty 'full_coverage' for the day concerned, and a 2381 non-empty mapping of 'day_slots' (from locations to event collections), 2382 output the day slots for the day. 2383 """ 2384 2385 page = self.page 2386 fmt = page.request.formatter 2387 2388 output = [] 2389 append = output.append 2390 2391 locations = day_slots.keys() 2392 locations.sort(sort_none_first) 2393 2394 # Traverse the time scale of the full coverage, visiting each slot to 2395 # determine whether it provides content for each period. 2396 2397 scale = getCoverageScale(full_coverage) 2398 2399 # Define a mapping of events to rowspans. 2400 2401 rowspans = {} 2402 2403 # Populate each period with event details, recording how many periods 2404 # each event populates. 2405 2406 day_rows = [] 2407 2408 for period in scale: 2409 2410 # Ignore timespans before this day. 2411 2412 if period != date: 2413 continue 2414 2415 # Visit each slot corresponding to a location (or no location). 2416 2417 day_row = [] 2418 2419 for location in locations: 2420 2421 # Visit each coverage span, presenting the events in the span. 2422 2423 for events in day_slots[location]: 2424 event = self.getActiveEvent(period, events) 2425 if event is not None: 2426 if not rowspans.has_key(event): 2427 rowspans[event] = 1 2428 else: 2429 rowspans[event] += 1 2430 day_row.append((location, event)) 2431 2432 day_rows.append((period, day_row)) 2433 2434 # Output the locations. 2435 2436 append(fmt.table_row(on=1)) 2437 2438 # Add a spacer. 2439 2440 append(self.writeDaySpacer(colspan=2, cls="location")) 2441 2442 for location in locations: 2443 2444 # Add spacers to the column spans. 2445 2446 columns = len(day_slots[location]) * 2 - 1 2447 append(fmt.table_cell(on=1, attrs={"class" : "event-location-heading", "colspan" : str(columns)})) 2448 append(fmt.text(location or "")) 2449 append(fmt.table_cell(on=0)) 2450 2451 # Add a trailing spacer. 2452 2453 append(self.writeDaySpacer(cls="location")) 2454 2455 append(fmt.table_row(on=0)) 2456 2457 # Output the periods with event details. 2458 2459 period = None 2460 events_written = set() 2461 2462 for period, day_row in day_rows: 2463 2464 # Write an empty heading for the start of the day where the first 2465 # applicable timespan starts before this day. 2466 2467 if period.start < date: 2468 append(fmt.table_row(on=1)) 2469 append(self.writeDayScaleHeading("")) 2470 2471 # Otherwise, write a heading describing the time. 2472 2473 else: 2474 append(fmt.table_row(on=1)) 2475 append(self.writeDayScaleHeading(period.start.time_string())) 2476 2477 append(self.writeDaySpacer()) 2478 2479 # Visit each slot corresponding to a location (or no location). 2480 2481 for location, event in day_row: 2482 2483 # Output each location slot's contribution. 2484 2485 if event is None or event not in events_written: 2486 append(self.writeDaySlot(period, event, event is None and 1 or rowspans[event])) 2487 if event is not None: 2488 events_written.add(event) 2489 2490 # Add a trailing spacer. 2491 2492 append(self.writeDaySpacer()) 2493 2494 append(fmt.table_row(on=0)) 2495 2496 # Write a final time heading if the last period ends in the current day. 2497 2498 if period is not None: 2499 if period.end == date: 2500 append(fmt.table_row(on=1)) 2501 append(self.writeDayScaleHeading(period.end.time_string())) 2502 2503 for slot in day_row: 2504 append(self.writeDaySpacer()) 2505 append(self.writeEmptyDaySlot()) 2506 2507 append(fmt.table_row(on=0)) 2508 2509 return "".join(output) 2510 2511 def writeDayScaleHeading(self, heading): 2512 page = self.page 2513 fmt = page.request.formatter 2514 2515 output = [] 2516 append = output.append 2517 2518 append(fmt.table_cell(on=1, attrs={"class" : "event-scale-heading"})) 2519 append(fmt.text(heading)) 2520 append(fmt.table_cell(on=0)) 2521 2522 return "".join(output) 2523 2524 def getActiveEvent(self, period, events): 2525 for event in events: 2526 if period not in event: 2527 continue 2528 return event 2529 else: 2530 return None 2531 2532 def writeDaySlot(self, period, event, rowspan): 2533 page = self.page 2534 fmt = page.request.formatter 2535 2536 output = [] 2537 append = output.append 2538 2539 if event is not None: 2540 event_summary = event.getSummary(self.parent_name) 2541 style = self.getEventStyle(event_summary) 2542 2543 append(fmt.table_cell(on=1, attrs={ 2544 "class" : "event-timespan-content event-timespan-busy", 2545 "style" : style, 2546 "rowspan" : str(rowspan) 2547 })) 2548 append(self.writeEventSummaryBox(event)) 2549 append(fmt.table_cell(on=0)) 2550 else: 2551 append(self.writeEmptyDaySlot()) 2552 2553 return "".join(output) 2554 2555 def writeEmptyDaySlot(self): 2556 page = self.page 2557 fmt = page.request.formatter 2558 2559 output = [] 2560 append = output.append 2561 2562 append(fmt.table_cell(on=1, 2563 attrs={"class" : "event-timespan-content event-timespan-empty"})) 2564 append(fmt.table_cell(on=0)) 2565 2566 return "".join(output) 2567 2568 def writeDaySpacer(self, colspan=1, cls="timespan"): 2569 page = self.page 2570 fmt = page.request.formatter 2571 2572 output = [] 2573 append = output.append 2574 2575 append(fmt.table_cell(on=1, attrs={ 2576 "class" : "event-%s-spacer" % cls, 2577 "colspan" : str(colspan)})) 2578 append(fmt.table_cell(on=0)) 2579 return "".join(output) 2580 2581 # Map layout methods. 2582 2583 def writeMapTableHeading(self): 2584 page = self.page 2585 fmt = page.request.formatter 2586 2587 output = [] 2588 append = output.append 2589 2590 append(fmt.table_cell(on=1, attrs={"class" : "event-map-heading"})) 2591 append(self.writeMapHeading()) 2592 append(fmt.table_cell(on=0)) 2593 2594 return "".join(output) 2595 2596 def showDictError(self, text, pagename): 2597 page = self.page 2598 request = page.request 2599 fmt = request.formatter 2600 2601 output = [] 2602 append = output.append 2603 2604 append(fmt.div(on=1, attrs={"class" : "event-aggregator-error"})) 2605 append(fmt.paragraph(on=1)) 2606 append(fmt.text(text)) 2607 append(fmt.paragraph(on=0)) 2608 append(fmt.paragraph(on=1)) 2609 append(linkToPage(request, Page(request, pagename), pagename)) 2610 append(fmt.paragraph(on=0)) 2611 2612 return "".join(output) 2613 2614 def writeMapEventSummaries(self, events): 2615 page = self.page 2616 request = page.request 2617 fmt = request.formatter 2618 2619 # Sort the events by date. 2620 2621 events.sort(sort_start_first) 2622 2623 # Write out a self-contained list of events. 2624 2625 output = [] 2626 append = output.append 2627 2628 append(fmt.bullet_list(on=1, attr={"class" : "event-map-location-events"})) 2629 2630 for event in events: 2631 2632 # Get the event details. 2633 2634 event_summary = event.getSummary(self.parent_name) 2635 start, end = event.as_limits() 2636 event_period = self._getCalendarPeriod( 2637 start and self.getFullDateLabel(start), 2638 end and self.getFullDateLabel(end), 2639 "") 2640 2641 append(fmt.listitem(on=1)) 2642 2643 # Link to the page using the summary. 2644 2645 append(event.linkToEvent(request, event_summary)) 2646 2647 # Add the event period. 2648 2649 append(fmt.text(" ")) 2650 append(fmt.span(on=1, css_class="event-map-period")) 2651 append(fmt.text(event_period)) 2652 append(fmt.span(on=0)) 2653 2654 append(fmt.listitem(on=0)) 2655 2656 append(fmt.bullet_list(on=0)) 2657 2658 return "".join(output) 2659 2660 def render(self, all_shown_events): 2661 2662 """ 2663 Render the view, returning the rendered representation as a string. 2664 The view will show a list of 'all_shown_events'. 2665 """ 2666 2667 page = self.page 2668 request = page.request 2669 fmt = request.formatter 2670 _ = request.getText 2671 2672 # Make a calendar. 2673 2674 output = [] 2675 append = output.append 2676 2677 append(fmt.div(on=1, css_class="event-calendar", id=("EventAggregator-%s" % self.getIdentifier()))) 2678 2679 # Output download controls. 2680 2681 append(fmt.div(on=1, css_class="event-controls")) 2682 append(self.writeDownloadControls()) 2683 append(fmt.div(on=0)) 2684 2685 # Output a table. 2686 2687 if self.mode == "table": 2688 2689 # Start of table view output. 2690 2691 append(fmt.table(on=1, attrs={"tableclass" : "event-table"})) 2692 2693 append(fmt.table_row(on=1)) 2694 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 2695 append(fmt.text(_("Event dates"))) 2696 append(fmt.table_cell(on=0)) 2697 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 2698 append(fmt.text(_("Event location"))) 2699 append(fmt.table_cell(on=0)) 2700 append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 2701 append(fmt.text(_("Event details"))) 2702 append(fmt.table_cell(on=0)) 2703 append(fmt.table_row(on=0)) 2704 2705 # Show the events in order. 2706 2707 all_shown_events.sort(sort_start_first) 2708 2709 for event in all_shown_events: 2710 event_page = event.getPage() 2711 event_summary = event.getSummary(self.parent_name) 2712 event_details = event.getDetails() 2713 2714 # Prepare CSS classes with category-related styling. 2715 2716 css_classes = ["event-table-details"] 2717 2718 for topic in event_details.get("topics") or event_details.get("categories") or []: 2719 2720 # Filter the category text to avoid illegal characters. 2721 2722 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 2723 2724 attrs = {"class" : " ".join(css_classes)} 2725 2726 append(fmt.table_row(on=1)) 2727 2728 # Start and end dates. 2729 2730 append(fmt.table_cell(on=1, attrs=attrs)) 2731 append(fmt.span(on=1)) 2732 append(fmt.text(str(event_details["start"]))) 2733 append(fmt.span(on=0)) 2734 2735 if event_details["start"] != event_details["end"]: 2736 append(fmt.text(" - ")) 2737 append(fmt.span(on=1)) 2738 append(fmt.text(str(event_details["end"]))) 2739 append(fmt.span(on=0)) 2740 2741 append(fmt.table_cell(on=0)) 2742 2743 # Location. 2744 2745 append(fmt.table_cell(on=1, attrs=attrs)) 2746 2747 if event_details.has_key("location"): 2748 append(event_page.formatText(event_details["location"], fmt)) 2749 2750 append(fmt.table_cell(on=0)) 2751 2752 # Link to the page using the summary. 2753 2754 append(fmt.table_cell(on=1, attrs=attrs)) 2755 append(event.linkToEvent(request, event_summary)) 2756 append(fmt.table_cell(on=0)) 2757 2758 append(fmt.table_row(on=0)) 2759 2760 # End of table view output. 2761 2762 append(fmt.table(on=0)) 2763 2764 # Output a map view. 2765 2766 elif self.mode == "map": 2767 2768 # Special dictionary pages. 2769 2770 maps_page = getMapsPage(request) 2771 locations_page = getLocationsPage(request) 2772 2773 map_image = None 2774 2775 # Get the maps and locations. 2776 2777 maps = getWikiDict(maps_page, request) 2778 locations = getWikiDict(locations_page, request) 2779 2780 # Get the map image definition. 2781 2782 if maps is not None and self.map_name: 2783 try: 2784 map_details = maps[self.map_name].split() 2785 2786 map_bottom_left_latitude, map_bottom_left_longitude, map_top_right_latitude, map_top_right_longitude = \ 2787 map(getMapReference, map_details[:4]) 2788 map_width, map_height = map(int, map_details[4:6]) 2789 map_image = map_details[6] 2790 2791 map_x_scale = map_width / (map_top_right_longitude - map_bottom_left_longitude).to_degrees() 2792 map_y_scale = map_height / (map_top_right_latitude - map_bottom_left_latitude).to_degrees() 2793 2794 except (KeyError, ValueError): 2795 pass 2796 2797 # Report errors. 2798 2799 if maps is None: 2800 append(self.showDictError( 2801 _("You do not have read access to the maps page:"), 2802 maps_page)) 2803 2804 elif not self.map_name: 2805 append(self.showDictError( 2806 _("Please specify a valid map name corresponding to an entry on the following page:"), 2807 maps_page)) 2808 2809 elif map_image is None: 2810 append(self.showDictError( 2811 _("Please specify a valid entry for %s on the following page:") % self.map_name, 2812 maps_page)) 2813 2814 elif locations is None: 2815 append(self.showDictError( 2816 _("You do not have read access to the locations page:"), 2817 locations_page)) 2818 2819 # Attempt to show the map. 2820 2821 else: 2822 2823 # Get events by position. 2824 2825 events_by_location = {} 2826 event_locations = {} 2827 2828 for event in all_shown_events: 2829 event_details = event.getDetails() 2830 2831 location = event_details.get("location") 2832 2833 if location is not None and not event_locations.has_key(location): 2834 2835 # Get any explicit position of an event. 2836 2837 if event_details.has_key("geo"): 2838 latitude, longitude = event_details["geo"] 2839 2840 # Or look up the position of a location using the locations 2841 # page. 2842 2843 else: 2844 latitude, longitude = Location(location, locations).getPosition() 2845 2846 # Use a normalised location if necessary. 2847 2848 if latitude is None and longitude is None: 2849 normalised_location = getNormalisedLocation(location) 2850 if normalised_location is not None: 2851 latitude, longitude = getLocationPosition(normalised_location, locations) 2852 if latitude is not None and longitude is not None: 2853 location = normalised_location 2854 2855 # Only remember positioned locations. 2856 2857 if latitude is not None and longitude is not None: 2858 event_locations[location] = latitude, longitude 2859 2860 # Record events according to location. 2861 2862 if not events_by_location.has_key(location): 2863 events_by_location[location] = [] 2864 2865 events_by_location[location].append(event) 2866 2867 # Get the map image URL. 2868 2869 map_image_url = AttachFile.getAttachUrl(maps_page, map_image, request) 2870 2871 # Start of map view output. 2872 2873 map_identifier = "map-%s" % self.getIdentifier() 2874 append(fmt.div(on=1, css_class="event-map", id=map_identifier)) 2875 2876 append(fmt.table(on=1)) 2877 2878 append(fmt.table_row(on=1)) 2879 append(self.writeMapTableHeading()) 2880 append(fmt.table_row(on=0)) 2881 2882 append(fmt.table_row(on=1)) 2883 append(fmt.table_cell(on=1)) 2884 2885 append(fmt.div(on=1, css_class="event-map-container")) 2886 append(fmt.image(map_image_url)) 2887 append(fmt.number_list(on=1)) 2888 2889 # Events with no location are unpositioned. 2890 2891 if events_by_location.has_key(None): 2892 unpositioned_events = events_by_location[None] 2893 del events_by_location[None] 2894 else: 2895 unpositioned_events = [] 2896 2897 # Events whose location is unpositioned are themselves considered 2898 # unpositioned. 2899 2900 for location in set(events_by_location.keys()).difference(event_locations.keys()): 2901 unpositioned_events += events_by_location[location] 2902 2903 # Sort the locations before traversing them. 2904 2905 event_locations = event_locations.items() 2906 event_locations.sort() 2907 2908 # Show the events in the map. 2909 2910 for location, (latitude, longitude) in event_locations: 2911 events = events_by_location[location] 2912 2913 # Skip unpositioned locations and locations outside the map. 2914 2915 if latitude is None or longitude is None or \ 2916 latitude < map_bottom_left_latitude or \ 2917 longitude < map_bottom_left_longitude or \ 2918 latitude > map_top_right_latitude or \ 2919 longitude > map_top_right_longitude: 2920 2921 unpositioned_events += events 2922 continue 2923 2924 # Get the position and dimensions of the map marker. 2925 # NOTE: Use one degree as the marker size. 2926 2927 marker_x, marker_y = getPositionForCentrePoint( 2928 getPositionForReference(map_top_right_latitude, longitude, latitude, map_bottom_left_longitude, 2929 map_x_scale, map_y_scale), 2930 map_x_scale, map_y_scale) 2931 2932 # Put a marker on the map. 2933 2934 append(fmt.listitem(on=1, css_class="event-map-label")) 2935 2936 # Have a positioned marker for the print mode. 2937 2938 append(fmt.div(on=1, css_class="event-map-label-only", 2939 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 2940 marker_x, marker_y, map_x_scale, map_y_scale)) 2941 append(fmt.div(on=0)) 2942 2943 # Have a marker containing a pop-up when using the screen mode, 2944 # providing a normal block when using the print mode. 2945 2946 append(fmt.div(on=1, css_class="event-map-label", 2947 style="left:%dpx; top:%dpx; min-width:%dpx; min-height:%dpx") % ( 2948 marker_x, marker_y, map_x_scale, map_y_scale)) 2949 append(fmt.div(on=1, css_class="event-map-details")) 2950 append(fmt.div(on=1, css_class="event-map-shadow")) 2951 append(fmt.div(on=1, css_class="event-map-location")) 2952 2953 append(fmt.heading(on=1, depth=2)) 2954 append(fmt.text(location)) 2955 append(fmt.heading(on=0, depth=2)) 2956 2957 append(self.writeMapEventSummaries(events)) 2958 2959 append(fmt.div(on=0)) 2960 append(fmt.div(on=0)) 2961 append(fmt.div(on=0)) 2962 append(fmt.div(on=0)) 2963 append(fmt.listitem(on=0)) 2964 2965 append(fmt.number_list(on=0)) 2966 append(fmt.div(on=0)) 2967 append(fmt.table_cell(on=0)) 2968 append(fmt.table_row(on=0)) 2969 2970 # Write unpositioned events. 2971 2972 if unpositioned_events: 2973 unpositioned_identifier = "unpositioned-%s" % self.getIdentifier() 2974 2975 append(fmt.table_row(on=1, css_class="event-map-unpositioned", 2976 id=unpositioned_identifier)) 2977 append(fmt.table_cell(on=1)) 2978 2979 append(fmt.heading(on=1, depth=2)) 2980 append(fmt.text(_("Events not shown on the map"))) 2981 append(fmt.heading(on=0, depth=2)) 2982 2983 # Show and hide controls. 2984 2985 append(fmt.div(on=1, css_class="event-map-show-control")) 2986 append(fmt.anchorlink(on=1, name=unpositioned_identifier)) 2987 append(fmt.text(_("Show unpositioned events"))) 2988 append(fmt.anchorlink(on=0)) 2989 append(fmt.div(on=0)) 2990 2991 append(fmt.div(on=1, css_class="event-map-hide-control")) 2992 append(fmt.anchorlink(on=1, name=map_identifier)) 2993 append(fmt.text(_("Hide unpositioned events"))) 2994 append(fmt.anchorlink(on=0)) 2995 append(fmt.div(on=0)) 2996 2997 append(self.writeMapEventSummaries(unpositioned_events)) 2998 2999 # End of map view output. 3000 3001 append(fmt.table_cell(on=0)) 3002 append(fmt.table_row(on=0)) 3003 append(fmt.table(on=0)) 3004 append(fmt.div(on=0)) 3005 3006 # Output a list. 3007 3008 elif self.mode == "list": 3009 3010 # Start of list view output. 3011 3012 append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 3013 3014 # Output a list. 3015 3016 for period in self.first.until(self.last): 3017 3018 append(fmt.listitem(on=1, attr={"class" : "event-listings-period"})) 3019 append(fmt.div(on=1, attr={"class" : "event-listings-heading"})) 3020 3021 # Either write a date heading or produce links for navigable 3022 # calendars. 3023 3024 append(self.writeDateHeading(period)) 3025 3026 append(fmt.div(on=0)) 3027 3028 append(fmt.bullet_list(on=1, attr={"class" : "event-period-listings"})) 3029 3030 # Show the events in order. 3031 3032 events_in_period = getEventsInPeriod(all_shown_events, getCalendarPeriod(period, period)) 3033 events_in_period.sort(sort_start_first) 3034 3035 for event in events_in_period: 3036 event_page = event.getPage() 3037 event_details = event.getDetails() 3038 event_summary = event.getSummary(self.parent_name) 3039 3040 append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 3041 3042 # Link to the page using the summary. 3043 3044 append(fmt.paragraph(on=1)) 3045 append(event.linkToEvent(request, event_summary)) 3046 append(fmt.paragraph(on=0)) 3047 3048 # Start and end dates. 3049 3050 append(fmt.paragraph(on=1)) 3051 append(fmt.span(on=1)) 3052 append(fmt.text(str(event_details["start"]))) 3053 append(fmt.span(on=0)) 3054 append(fmt.text(" - ")) 3055 append(fmt.span(on=1)) 3056 append(fmt.text(str(event_details["end"]))) 3057 append(fmt.span(on=0)) 3058 append(fmt.paragraph(on=0)) 3059 3060 # Location. 3061 3062 if event_details.has_key("location"): 3063 append(fmt.paragraph(on=1)) 3064 append(event_page.formatText(event_details["location"], fmt)) 3065 append(fmt.paragraph(on=1)) 3066 3067 # Topics. 3068 3069 if event_details.has_key("topics") or event_details.has_key("categories"): 3070 append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 3071 3072 for topic in event_details.get("topics") or event_details.get("categories") or []: 3073 append(fmt.listitem(on=1)) 3074 append(event_page.formatText(topic, fmt)) 3075 append(fmt.listitem(on=0)) 3076 3077 append(fmt.bullet_list(on=0)) 3078 3079 append(fmt.listitem(on=0)) 3080 3081 append(fmt.bullet_list(on=0)) 3082 3083 # End of list view output. 3084 3085 append(fmt.bullet_list(on=0)) 3086 3087 # Output a month calendar. This shows month-by-month data. 3088 3089 elif self.mode == "calendar": 3090 3091 # Visit all months in the requested range, or across known events. 3092 3093 for month in self.first.months_until(self.last): 3094 3095 # Output a month. 3096 3097 append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 3098 3099 # Either write a month heading or produce links for navigable 3100 # calendars. 3101 3102 append(self.writeMonthTableHeading(month)) 3103 3104 # Weekday headings. 3105 3106 append(self.writeWeekdayHeadings()) 3107 3108 # Process the days of the month. 3109 3110 start_weekday, number_of_days = month.month_properties() 3111 3112 # The start weekday is the weekday of day number 1. 3113 # Find the first day of the week, counting from below zero, if 3114 # necessary, in order to land on the first day of the month as 3115 # day number 1. 3116 3117 first_day = 1 - start_weekday 3118 3119 while first_day <= number_of_days: 3120 3121 # Find events in this week and determine how to mark them on the 3122 # calendar. 3123 3124 week_start = month.as_date(max(first_day, 1)) 3125 week_end = month.as_date(min(first_day + 6, number_of_days)) 3126 3127 full_coverage, week_slots = getCoverage( 3128 getEventsInPeriod(all_shown_events, getCalendarPeriod(week_start, week_end))) 3129 3130 # Output a week, starting with the day numbers. 3131 3132 append(self.writeDayNumbers(first_day, number_of_days, month, full_coverage)) 3133 3134 # Either generate empty days... 3135 3136 if not week_slots: 3137 append(self.writeEmptyWeek(first_day, number_of_days, month)) 3138 3139 # Or generate each set of scheduled events... 3140 3141 else: 3142 append(self.writeWeekSlots(first_day, number_of_days, month, week_end, week_slots)) 3143 3144 # Process the next week... 3145 3146 first_day += 7 3147 3148 # End of month. 3149 3150 append(fmt.table(on=0)) 3151 3152 # Output a day view. 3153 3154 elif self.mode == "day": 3155 3156 # Visit all days in the requested range, or across known events. 3157 3158 for date in self.first.days_until(self.last): 3159 3160 append(fmt.table(on=1, attrs={"tableclass" : "event-calendar-day"})) 3161 3162 full_coverage, day_slots = getCoverage( 3163 getEventsInPeriod(all_shown_events, getCalendarPeriod(date, date)), "datetime") 3164 3165 # Work out how many columns the day title will need. 3166 # Include spacers after the scale and each event column. 3167 3168 colspan = sum(map(len, day_slots.values())) * 2 + 2 3169 3170 append(self.writeDayTableHeading(date, colspan)) 3171 3172 # Either generate empty days... 3173 3174 if not day_slots: 3175 append(self.writeEmptyDay(date)) 3176 3177 # Or generate each set of scheduled events... 3178 3179 else: 3180 append(self.writeDaySlots(date, full_coverage, day_slots)) 3181 3182 # End of day. 3183 3184 append(fmt.table(on=0)) 3185 3186 # Output view controls. 3187 3188 append(fmt.div(on=1, css_class="event-controls")) 3189 append(self.writeViewControls()) 3190 append(fmt.div(on=0)) 3191 3192 # Close the calendar region. 3193 3194 append(fmt.div(on=0)) 3195 3196 # Add any scripts. 3197 3198 if isinstance(fmt, request.html_formatter.__class__): 3199 append(self.update_script) 3200 3201 return ''.join(output) 3202 3203 update_script = """\ 3204 <script type="text/javascript"> 3205 function replaceCalendar(name, url) { 3206 var calendar = document.getElementById(name); 3207 3208 if (calendar == null) { 3209 return true; 3210 } 3211 3212 var xmlhttp = new XMLHttpRequest(); 3213 xmlhttp.open("GET", url, false); 3214 xmlhttp.send(null); 3215 3216 var newCalendar = xmlhttp.responseText; 3217 3218 if (newCalendar != null) { 3219 calendar.innerHTML = newCalendar; 3220 return false; 3221 } 3222 3223 return true; 3224 } 3225 </script> 3226 """ 3227 3228 # Event selection from request parameters. 3229 3230 def getEventsUsingParameters(category_names, search_pattern, remote_sources, 3231 calendar_start, calendar_end, resolution, request): 3232 3233 "Get the events according to the resolution of the calendar." 3234 3235 if search_pattern: 3236 results = getPagesForSearch(search_pattern, request) 3237 else: 3238 results = [] 3239 3240 results += getAllCategoryPages(category_names, request) 3241 pages = getPagesFromResults(results, request) 3242 events = getEventsFromResources(getEventPages(pages)) 3243 events += getEventsFromResources(getEventResources(remote_sources, calendar_start, calendar_end, request)) 3244 all_shown_events = getEventsInPeriod(events, getCalendarPeriod(calendar_start, calendar_end)) 3245 earliest, latest = getEventLimits(all_shown_events) 3246 3247 # Get a concrete period of time. 3248 3249 first, last = getConcretePeriod(calendar_start, calendar_end, earliest, latest, resolution) 3250 3251 return all_shown_events, first, last 3252 3253 # Event-only formatting. 3254 3255 def formatEvent(event, request, fmt, write=None): 3256 3257 """ 3258 Format the given 'event' using the 'request' and formatter 'fmt'. If the 3259 'write' parameter is specified, use it to write output. 3260 """ 3261 3262 details = event.getDetails() 3263 raw_details = event.getRawDetails() 3264 write = write or request.write 3265 3266 if details.has_key("fragment"): 3267 write(fmt.anchordef(details["fragment"])) 3268 3269 write(fmt.definition_list(on=1)) 3270 3271 for term in event.all_terms: 3272 3273 raw_value = raw_details.get(term) 3274 value = details.get(term) 3275 3276 if raw_value or value: 3277 write(fmt.definition_term(on=1)) 3278 write(fmt.text(term)) 3279 write(fmt.definition_term(on=0)) 3280 write(fmt.definition_desc(on=1)) 3281 3282 # Try and use the raw details, if available. 3283 3284 if raw_value: 3285 write(formatText(raw_value, request, fmt)) 3286 3287 # Otherwise, format the processed details. 3288 3289 else: 3290 if term in event.list_terms: 3291 write(", ".join([formatText(str(v), request, fmt) for v in value])) 3292 else: 3293 write(fmt.text(str(value))) 3294 3295 write(fmt.definition_desc(on=0)) 3296 3297 write(fmt.definition_list(on=0)) 3298 3299 def formatEventsForOutputType(events, request, mimetype, parent=None, descriptions=None, latest_timestamp=None, write=None): 3300 3301 """ 3302 Format the given 'events' using the 'request' for the given 'mimetype'. 3303 3304 The optional 'parent' indicates the "natural" parent page of the events. Any 3305 event pages residing beneath the parent page will have their names 3306 reproduced as relative to the parent page. 3307 3308 The optional 'descriptions' indicates the nature of any description given 3309 for events in the output resource. 3310 3311 The optional 'latest_timestamp' indicates the timestamp of the latest edit 3312 of the page or event collection. 3313 3314 If the 'write' parameter is specified, use it to write output. 3315 """ 3316 3317 write = write or request.write 3318 3319 # Start the collection. 3320 3321 if mimetype == "text/calendar": 3322 write("BEGIN:VCALENDAR\r\n") 3323 write("PRODID:-//MoinMoin//EventAggregatorSummary\r\n") 3324 write("VERSION:2.0\r\n") 3325 3326 elif mimetype == "application/rss+xml": 3327 3328 # Using the page name and the page URL in the title, link and 3329 # description. 3330 3331 path_info = getPathInfo(request) 3332 3333 write('<rss version="2.0">\r\n') 3334 write('<channel>\r\n') 3335 write('<title>%s</title>\r\n' % path_info[1:]) 3336 write('<link>%s%s</link>\r\n' % (request.getBaseURL(), path_info)) 3337 write('<description>Events published on %s%s</description>\r\n' % (request.getBaseURL(), path_info)) 3338 3339 if latest_timestamp is not None: 3340 write('<lastBuildDate>%s</lastBuildDate>\r\n' % latest_timestamp.as_HTTP_datetime_string()) 3341 3342 # Sort the events by start date, reversed. 3343 3344 ordered_events = getOrderedEvents(events) 3345 ordered_events.reverse() 3346 events = ordered_events 3347 3348 elif mimetype == "text/html": 3349 write('<html>') 3350 write('<body>') 3351 3352 # Output the collection one by one. 3353 3354 for event in events: 3355 formatEventForOutputType(event, request, mimetype, parent, descriptions) 3356 3357 # End the collection. 3358 3359 if mimetype == "text/calendar": 3360 write("END:VCALENDAR\r\n") 3361 3362 elif mimetype == "application/rss+xml": 3363 write('</channel>\r\n') 3364 write('</rss>\r\n') 3365 3366 elif mimetype == "text/html": 3367 write('</body>') 3368 write('</html>') 3369 3370 def formatEventForOutputType(event, request, mimetype, parent=None, descriptions=None, write=None): 3371 3372 """ 3373 Format the given 'event' using the 'request' for the given 'mimetype'. 3374 3375 The optional 'parent' indicates the "natural" parent page of the events. Any 3376 event pages residing beneath the parent page will have their names 3377 reproduced as relative to the parent page. 3378 3379 The optional 'descriptions' indicates the nature of any description given 3380 for events in the output resource. 3381 3382 If the 'write' parameter is specified, use it to write output. 3383 """ 3384 3385 write = write or request.write 3386 event_details = event.getDetails() 3387 event_metadata = event.getMetadata() 3388 3389 if mimetype == "text/calendar": 3390 3391 # NOTE: A custom formatter making attributes for links and plain 3392 # NOTE: text for values could be employed here. 3393 3394 # Get the summary details. 3395 3396 event_summary = event.getSummary(parent) 3397 link = event.getEventURL() 3398 3399 # Output the event details. 3400 3401 write("BEGIN:VEVENT\r\n") 3402 write("UID:%s\r\n" % link) 3403 write("URL:%s\r\n" % link) 3404 write("DTSTAMP:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_metadata["created"].as_tuple()[:6]) 3405 write("LAST-MODIFIED:%04d%02d%02dT%02d%02d%02dZ\r\n" % event_metadata["last-modified"].as_tuple()[:6]) 3406 write("SEQUENCE:%d\r\n" % event_metadata["sequence"]) 3407 3408 start = event_details["start"] 3409 end = event_details["end"] 3410 3411 if isinstance(start, DateTime): 3412 write("DTSTART") 3413 write_calendar_datetime(request, start) 3414 else: 3415 write("DTSTART;VALUE=DATE:%04d%02d%02d\r\n" % start.as_date().as_tuple()) 3416 3417 if isinstance(end, DateTime): 3418 write("DTEND") 3419 write_calendar_datetime(request, end) 3420 else: 3421 write("DTEND;VALUE=DATE:%04d%02d%02d\r\n" % end.next_day().as_date().as_tuple()) 3422 3423 write("SUMMARY:%s\r\n" % getQuotedText(event_summary)) 3424 3425 # Optional details. 3426 3427 if event_details.get("topics") or event_details.get("categories"): 3428 write("CATEGORIES:%s\r\n" % ",".join( 3429 [getQuotedText(topic) 3430 for topic in event_details.get("topics") or event_details.get("categories")] 3431 )) 3432 if event_details.has_key("location"): 3433 write("LOCATION:%s\r\n" % getQuotedText(event_details["location"])) 3434 if event_details.has_key("geo"): 3435 write("GEO:%s\r\n" % getQuotedText(";".join([str(ref.to_degrees()) for ref in event_details["geo"]]))) 3436 3437 write("END:VEVENT\r\n") 3438 3439 elif mimetype == "application/rss+xml": 3440 3441 event_page = event.getPage() 3442 event_details = event.getDetails() 3443 3444 # Get a parser and formatter for the formatting of some attributes. 3445 3446 fmt = request.html_formatter 3447 3448 # Get the summary details. 3449 3450 event_summary = event.getSummary(parent) 3451 link = event.getEventURL() 3452 3453 write('<item>\r\n') 3454 write('<title>%s</title>\r\n' % escape(event_summary)) 3455 write('<link>%s</link>\r\n' % link) 3456 3457 # Write a description according to the preferred source of 3458 # descriptions. 3459 3460 if descriptions == "page": 3461 description = event_details.get("description", "") 3462 else: 3463 description = event_metadata["last-comment"] 3464 3465 write('<description>%s</description>\r\n' % 3466 fmt.text(event_page.formatText(description, fmt))) 3467 3468 for topic in event_details.get("topics") or event_details.get("categories") or []: 3469 write('<category>%s</category>\r\n' % 3470 fmt.text(event_page.formatText(topic, fmt))) 3471 3472 write('<pubDate>%s</pubDate>\r\n' % event_metadata["created"].as_HTTP_datetime_string()) 3473 write('<guid>%s#%s</guid>\r\n' % (link, event_metadata["sequence"])) 3474 write('</item>\r\n') 3475 3476 elif mimetype == "text/html": 3477 fmt = request.html_formatter 3478 fmt.setPage(request.page) 3479 formatEvent(event, request, fmt, write=write) 3480 3481 # iCalendar format helper functions. 3482 3483 def write_calendar_datetime(request, datetime): 3484 3485 """ 3486 Write to the given 'request' the 'datetime' using appropriate time zone 3487 information. 3488 """ 3489 3490 utc_datetime = datetime.to_utc() 3491 if utc_datetime: 3492 request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02dZ\r\n" % utc_datetime.padded().as_tuple()[:-1]) 3493 else: 3494 zone = datetime.time_zone() 3495 if zone: 3496 request.write(";TZID=/%s" % zone) 3497 request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02d\r\n" % datetime.padded().as_tuple()[:-1]) 3498 3499 def getQuotedText(text): 3500 3501 "Return the 'text' quoted for iCalendar purposes." 3502 3503 return text.replace(";", r"\;").replace(",", r"\,").replace("\n", "\\n") 3504 3505 # vim: tabstop=4 expandtab shiftwidth=4