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