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