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