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