1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator library 4 5 @copyright: 2008, 2009, 2010, 2011, 2012 by Paul Boddie <paul@boddie.org.uk> 6 @copyright: 2000-2004 Juergen Hermann <jh@web.de>, 7 2005-2008 MoinMoin:ThomasWaldmann. 8 @license: GNU GPL (v2 or later), see COPYING.txt for details. 9 """ 10 11 from LocationSupport import * 12 from MoinDateSupport import * 13 from MoinSupport import * 14 15 from MoinMoin.Page import Page 16 from MoinMoin.action import cache 17 from MoinMoin import caching 18 from MoinMoin import search 19 from MoinMoin import wikiutil 20 21 import codecs 22 import time 23 import re 24 import urllib, urllib2 25 26 try: 27 from cStringIO import StringIO 28 except ImportError: 29 from StringIO import StringIO 30 31 try: 32 set 33 except NameError: 34 from sets import Set as set 35 36 try: 37 import vCalendar 38 except ImportError: 39 vCalendar = None 40 41 escape = wikiutil.escape 42 43 __version__ = "0.9" 44 45 # Regular expressions where MoinMoin does not provide the required support. 46 47 category_regexp = None 48 49 # Page parsing. 50 51 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 52 category_membership_regexp = re.compile(ur"^\s*(?:(Category\S+)(?:\s+(Category\S+))*)\s*$", re.MULTILINE | re.UNICODE) 53 54 # Value parsing. 55 56 country_code_regexp = re.compile(ur'(?:^|\W)(?P<code>[A-Z]{2})(?:$|\W+$)', re.UNICODE) 57 58 # Simple content parsing. 59 60 verbatim_regexp = re.compile(ur'(?:' 61 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 62 ur'|' 63 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 64 ur'|' 65 ur'!(?P<verbatim3>.*?)(\s|$)?' 66 ur'|' 67 ur'`(?P<monospace>.*?)`' 68 ur'|' 69 ur'{{{(?P<preformatted>.*?)}}}' 70 ur')', re.UNICODE) 71 72 # Utility functions. 73 74 def getCategoryPattern(request): 75 global category_regexp 76 77 try: 78 return request.cfg.cache.page_category_regexact 79 except AttributeError: 80 81 # Use regular expression from MoinMoin 1.7.1 otherwise. 82 83 if category_regexp is None: 84 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 85 return category_regexp 86 87 def getWikiDict(pagename, request): 88 if Page(request, pagename).exists() and request.user.may.read(pagename): 89 if hasattr(request.dicts, "dict"): 90 return request.dicts.dict(pagename) 91 else: 92 return request.dicts[pagename] 93 else: 94 return None 95 96 def to_list(s, sep): 97 return [x.strip() for x in s.split(sep) if x.strip()] 98 99 def sort_none_first(x, y): 100 if x is None: 101 return -1 102 elif y is None: 103 return 1 104 else: 105 return cmp(x, y) 106 107 def sort_start_first(x, y): 108 x_ts = x.as_limits() 109 if x_ts is not None: 110 x_start, x_end = x_ts 111 y_ts = y.as_limits() 112 if y_ts is not None: 113 y_start, y_end = y_ts 114 start_order = cmp(x_start, y_start) 115 if start_order == 0: 116 return cmp(x_end, y_end) 117 else: 118 return start_order 119 return 0 120 121 # Utility classes and associated functions. 122 123 class ActionSupport(ActionSupport): 124 125 "Extend the generic action support." 126 127 def get_month_lists(self, default_as_current=0): 128 129 """ 130 Return two lists of HTML element definitions corresponding to the start 131 and end month selection controls, with months selected according to any 132 values that have been specified via request parameters. 133 """ 134 135 _ = self._ 136 form = self.get_form() 137 138 # Initialise month lists. 139 140 start_month_list = [] 141 end_month_list = [] 142 143 start_month = self._get_input(form, "start-month", default_as_current and getCurrentMonth().month() or None) 144 end_month = self._get_input(form, "end-month", start_month) 145 146 # Prepare month lists, selecting specified months. 147 148 if not default_as_current: 149 start_month_list.append('<option value=""></option>') 150 end_month_list.append('<option value=""></option>') 151 152 for month in range(1, 13): 153 month_label = escape(_(getMonthLabel(month))) 154 selected = self._get_selected(month, start_month) 155 start_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 156 selected = self._get_selected(month, end_month) 157 end_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 158 159 return start_month_list, end_month_list 160 161 def get_year_defaults(self, default_as_current=0): 162 163 "Return defaults for the start and end years." 164 165 form = self.get_form() 166 167 start_year_default = form.get("start-year", [default_as_current and getCurrentYear() or ""])[0] 168 end_year_default = form.get("end-year", [default_as_current and start_year_default or ""])[0] 169 170 return start_year_default, end_year_default 171 172 def get_day_defaults(self, default_as_current=0): 173 174 "Return defaults for the start and end days." 175 176 form = self.get_form() 177 178 start_day_default = form.get("start-day", [default_as_current and getCurrentDate().day() or ""])[0] 179 end_day_default = form.get("end-day", [default_as_current and start_day_default or ""])[0] 180 181 return start_day_default, end_day_default 182 183 # Textual representations. 184 185 def getSimpleWikiText(text): 186 187 """ 188 Return the plain text representation of the given 'text' which may employ 189 certain Wiki syntax features, such as those providing verbatim or monospaced 190 text. 191 """ 192 193 # NOTE: Re-implementing support for verbatim text and linking avoidance. 194 195 return "".join([s for s in verbatim_regexp.split(text) if s is not None]) 196 197 def getEncodedWikiText(text): 198 199 "Encode the given 'text' in a verbatim representation." 200 201 return "<<Verbatim(%s)>>" % text 202 203 def getPrettyTitle(title): 204 205 "Return a nicely formatted version of the given 'title'." 206 207 return title.replace("_", " ").replace("/", u" ? ") 208 209 # Category discovery and searching. 210 211 def getCategories(request): 212 213 """ 214 From the AdvancedSearch macro, return a list of category page names using 215 the given 'request'. 216 """ 217 218 # This will return all pages with "Category" in the title. 219 220 cat_filter = getCategoryPattern(request).search 221 return request.rootpage.getPageList(filter=cat_filter) 222 223 def getCategoryMapping(category_pagenames, request): 224 225 """ 226 For the given 'category_pagenames' return a list of tuples of the form 227 (category name, category page name) using the given 'request'. 228 """ 229 230 cat_pattern = getCategoryPattern(request) 231 mapping = [] 232 for pagename in category_pagenames: 233 name = cat_pattern.match(pagename).group("key") 234 if name != "Category": 235 mapping.append((name, pagename)) 236 mapping.sort() 237 return mapping 238 239 def getCategoryPages(pagename, request): 240 241 """ 242 Return the pages associated with the given category 'pagename' using the 243 'request'. 244 """ 245 246 query = search.QueryParser().parse_query('category:%s' % pagename) 247 results = search.searchPages(request, query, "page_name") 248 249 cat_pattern = getCategoryPattern(request) 250 pages = [] 251 for page in results.hits: 252 if not cat_pattern.match(page.page_name): 253 pages.append(page) 254 return pages 255 256 def getAllCategoryPages(category_names, request): 257 258 """ 259 Return all pages belonging to the categories having the given 260 'category_names', using the given 'request'. 261 """ 262 263 pages = [] 264 pagenames = set() 265 266 for category_name in category_names: 267 268 # Get the pages and page names in the category. 269 270 pages_in_category = getCategoryPages(category_name, request) 271 272 # Visit each page in the category. 273 274 for page_in_category in pages_in_category: 275 pagename = page_in_category.page_name 276 277 # Only process each page once. 278 279 if pagename in pagenames: 280 continue 281 else: 282 pagenames.add(pagename) 283 284 pages.append(page_in_category) 285 286 return pages 287 288 def getPagesFromResults(result_pages, request): 289 290 "Return genuine pages for the given 'result_pages' using the 'request'." 291 292 return [Page(request, page.page_name) for page in result_pages] 293 294 # Event resources providing collections of events. 295 296 class EventResource: 297 298 "A resource providing event information." 299 300 def __init__(self, url): 301 self.url = url 302 303 def getPageURL(self): 304 305 "Return the URL of this page." 306 307 return self.url 308 309 def getFormat(self): 310 311 "Get the format used by this resource." 312 313 return "plain" 314 315 def getMetadata(self): 316 317 """ 318 Return a dictionary containing items describing the page's "created" 319 time, "last-modified" time, "sequence" (or revision number) and the 320 "last-comment" made about the last edit. 321 """ 322 323 return {} 324 325 def getEvents(self): 326 327 "Return a list of events from this resource." 328 329 return [] 330 331 def linkToPage(self, request, text, query_string=None): 332 333 """ 334 Using 'request', return a link to this page with the given link 'text' 335 and optional 'query_string'. 336 """ 337 338 return linkToResource(self.url, request, text, query_string) 339 340 # Formatting-related functions. 341 342 def formatText(self, text, request, fmt): 343 344 """ 345 Format the given 'text' using the specified 'request' and formatter 346 'fmt'. 347 """ 348 349 # Assume plain text which is then formatted appropriately. 350 351 return fmt.text(text) 352 353 class EventCalendar(EventResource): 354 355 "An iCalendar resource." 356 357 def __init__(self, url, calendar): 358 EventResource.__init__(self, url) 359 self.calendar = calendar 360 self.events = None 361 362 def getEvents(self): 363 364 "Return a list of events from this resource." 365 366 if self.events is None: 367 self.events = [] 368 369 _calendar, _empty, calendar = self.calendar 370 371 for objtype, attrs, obj in calendar: 372 373 # Read events. 374 375 if objtype == "VEVENT": 376 details = {} 377 378 for property, attrs, value in obj: 379 380 # Convert dates. 381 382 if property in ("DTSTART", "DTEND", "CREATED", "DTSTAMP", "LAST-MODIFIED"): 383 if property in ("DTSTART", "DTEND"): 384 property = property[2:] 385 if attrs.get("VALUE") == "DATE": 386 value = getDateFromCalendar(value) 387 else: 388 value = getDateTimeFromCalendar(value) 389 390 # Convert numeric data. 391 392 elif property == "SEQUENCE": 393 value = int(value) 394 395 # Convert lists. 396 397 elif property == "CATEGORIES": 398 value = to_list(value, ",") 399 400 # Convert positions (using decimal values). 401 402 elif property == "GEO": 403 try: 404 value = map(getMapReferenceFromDecimal, to_list(value, ";")) 405 if len(value) != 2: 406 continue 407 except (KeyError, ValueError): 408 continue 409 410 # Accept other textual data as it is. 411 412 elif property in ("LOCATION", "SUMMARY", "URL"): 413 value = value or None 414 415 # Ignore other properties. 416 417 else: 418 continue 419 420 property = property.lower() 421 details[property] = value 422 423 self.events.append(CalendarEvent(self, details)) 424 425 return self.events 426 427 class EventPage: 428 429 "An event page acting as an event resource." 430 431 def __init__(self, page): 432 self.page = page 433 self.events = None 434 self.body = None 435 self.categories = None 436 self.metadata = None 437 438 def copyPage(self, page): 439 440 "Copy the body of the given 'page'." 441 442 self.body = page.getBody() 443 444 def getPageURL(self): 445 446 "Return the URL of this page." 447 448 return getPageURL(self.page) 449 450 def getFormat(self): 451 452 "Get the format used on this page." 453 454 return getFormat(self.page) 455 456 def getMetadata(self): 457 458 """ 459 Return a dictionary containing items describing the page's "created" 460 time, "last-modified" time, "sequence" (or revision number) and the 461 "last-comment" made about the last edit. 462 """ 463 464 if self.metadata is None: 465 self.metadata = getMetadata(self.page) 466 return self.metadata 467 468 def getRevisions(self): 469 470 "Return a list of page revisions." 471 472 return self.page.getRevList() 473 474 def getPageRevision(self): 475 476 "Return the revision details dictionary for this page." 477 478 return getPageRevision(self.page) 479 480 def getPageName(self): 481 482 "Return the page name." 483 484 return self.page.page_name 485 486 def getPrettyPageName(self): 487 488 "Return a nicely formatted title/name for this page." 489 490 return getPrettyPageName(self.page) 491 492 def getBody(self): 493 494 "Get the current page body." 495 496 if self.body is None: 497 self.body = self.page.get_raw_body() 498 return self.body 499 500 def getEvents(self): 501 502 "Return a list of events from this page." 503 504 if self.events is None: 505 details = {} 506 self.events = [Event(self, details)] 507 508 if self.getFormat() == "wiki": 509 for match in definition_list_regexp.finditer(self.getBody()): 510 511 # Skip commented-out items. 512 513 if match.group("optcomment"): 514 continue 515 516 # Permit case-insensitive list terms. 517 518 term = match.group("term").lower() 519 desc = match.group("desc") 520 521 # Special value type handling. 522 523 # Dates. 524 525 if term in ("start", "end"): 526 desc = getDateTime(desc) 527 528 # Lists (whose elements may be quoted). 529 530 elif term in ("topics", "categories"): 531 desc = map(getSimpleWikiText, to_list(desc, ",")) 532 533 # Position details. 534 535 elif term == "geo": 536 try: 537 desc = map(getMapReference, to_list(desc, None)) 538 if len(desc) != 2: 539 continue 540 except (KeyError, ValueError): 541 continue 542 543 # Labels which may well be quoted. 544 545 elif term in ("title", "summary", "description", "location"): 546 desc = getSimpleWikiText(desc.strip()) 547 548 if desc is not None: 549 550 # Handle apparent duplicates by creating a new set of 551 # details. 552 553 if details.has_key(term): 554 555 # Make a new event. 556 557 details = {} 558 self.events.append(Event(self, details)) 559 560 details[term] = desc 561 562 return self.events 563 564 def setEvents(self, events): 565 566 "Set the given 'events' on this page." 567 568 self.events = events 569 570 def getCategoryMembership(self): 571 572 "Get the category names from this page." 573 574 if self.categories is None: 575 body = self.getBody() 576 match = category_membership_regexp.search(body) 577 self.categories = match and [x for x in match.groups() if x] or [] 578 579 return self.categories 580 581 def setCategoryMembership(self, category_names): 582 583 """ 584 Set the category membership for the page using the specified 585 'category_names'. 586 """ 587 588 self.categories = category_names 589 590 def flushEventDetails(self): 591 592 "Flush the current event details to this page's body text." 593 594 new_body_parts = [] 595 end_of_last_match = 0 596 body = self.getBody() 597 598 events = iter(self.getEvents()) 599 600 event = events.next() 601 event_details = event.getDetails() 602 replaced_terms = set() 603 604 for match in definition_list_regexp.finditer(body): 605 606 # Permit case-insensitive list terms. 607 608 term = match.group("term").lower() 609 desc = match.group("desc") 610 611 # Check that the term has not already been substituted. If so, 612 # get the next event. 613 614 if term in replaced_terms: 615 try: 616 event = events.next() 617 618 # No more events. 619 620 except StopIteration: 621 break 622 623 event_details = event.getDetails() 624 replaced_terms = set() 625 626 # Add preceding text to the new body. 627 628 new_body_parts.append(body[end_of_last_match:match.start()]) 629 630 # Get the matching regions, adding the term to the new body. 631 632 new_body_parts.append(match.group("wholeterm")) 633 634 # Special value type handling. 635 636 if event_details.has_key(term): 637 638 # Dates. 639 640 if term in ("start", "end"): 641 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 642 643 # Lists (whose elements may be quoted). 644 645 elif term in ("topics", "categories"): 646 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 647 648 # Labels which must be quoted. 649 650 elif term in ("title", "summary"): 651 desc = getEncodedWikiText(event_details[term]) 652 653 # Position details. 654 655 elif term == "geo": 656 desc = " ".join(map(str, event_details[term])) 657 658 # Text which need not be quoted, but it will be Wiki text. 659 660 elif term in ("description", "link", "location"): 661 desc = event_details[term] 662 663 replaced_terms.add(term) 664 665 # Add the replaced value. 666 667 new_body_parts.append(desc) 668 669 # Remember where in the page has been processed. 670 671 end_of_last_match = match.end() 672 673 # Write the rest of the page. 674 675 new_body_parts.append(body[end_of_last_match:]) 676 677 self.body = "".join(new_body_parts) 678 679 def flushCategoryMembership(self): 680 681 "Flush the category membership to the page body." 682 683 body = self.getBody() 684 category_names = self.getCategoryMembership() 685 match = category_membership_regexp.search(body) 686 687 if match: 688 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 689 690 def saveChanges(self): 691 692 "Save changes to the event." 693 694 self.flushEventDetails() 695 self.flushCategoryMembership() 696 self.page.saveText(self.getBody(), 0) 697 698 def linkToPage(self, request, text, query_string=None): 699 700 """ 701 Using 'request', return a link to this page with the given link 'text' 702 and optional 'query_string'. 703 """ 704 705 return linkToPage(request, self.page, text, query_string) 706 707 # Formatting-related functions. 708 709 def getParserClass(self, request, format): 710 711 """ 712 Return a parser class using the 'request' for the given 'format', returning 713 a plain text parser if no parser can be found for the specified 'format'. 714 """ 715 716 try: 717 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 718 except wikiutil.PluginMissingError: 719 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 720 721 def formatText(self, text, request, fmt): 722 723 """ 724 Format the given 'text' using the specified 'request' and formatter 725 'fmt'. 726 """ 727 728 fmt.page = self.page 729 730 # Suppress line anchors. 731 732 parser_cls = self.getParserClass(request, self.getFormat()) 733 parser = parser_cls(text, request, line_anchors=False) 734 735 # Fix lists by indicating that a paragraph is already started. 736 737 return request.redirectedOutput(parser.format, fmt, inhibit_p=True) 738 739 # Event details. 740 741 class Event(ActsAsTimespan): 742 743 "A description of an event." 744 745 def __init__(self, page, details): 746 self.page = page 747 self.details = details 748 749 # Permit omission of the end of the event by duplicating the start. 750 751 if self.details.has_key("start") and not self.details.get("end"): 752 end = self.details["start"] 753 754 # Make any end time refer to the day instead. 755 756 if isinstance(end, DateTime): 757 end = end.as_date() 758 759 self.details["end"] = end 760 761 def __repr__(self): 762 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 763 764 def __hash__(self): 765 766 """ 767 Return a dictionary hash, avoiding mistaken equality of events in some 768 situations (notably membership tests) by including the URL as well as 769 the summary. 770 """ 771 772 return hash(self.getSummary() + self.getEventURL()) 773 774 def getPage(self): 775 776 "Return the page describing this event." 777 778 return self.page 779 780 def setPage(self, page): 781 782 "Set the 'page' describing this event." 783 784 self.page = page 785 786 def getEventURL(self): 787 788 "Return the URL of this event." 789 790 return self.page.getPageURL() 791 792 def linkToEvent(self, request, text, query_string=None): 793 794 """ 795 Using 'request', return a link to this event with the given link 'text' 796 and optional 'query_string'. 797 """ 798 799 return self.page.linkToPage(request, text, query_string) 800 801 def getMetadata(self): 802 803 """ 804 Return a dictionary containing items describing the event's "created" 805 time, "last-modified" time, "sequence" (or revision number) and the 806 "last-comment" made about the last edit. 807 """ 808 809 # Delegate this to the page. 810 811 return self.page.getMetadata() 812 813 def getSummary(self, event_parent=None): 814 815 """ 816 Return either the given title or summary of the event according to the 817 event details, or a summary made from using the pretty version of the 818 page name. 819 820 If the optional 'event_parent' is specified, any page beneath the given 821 'event_parent' page in the page hierarchy will omit this parent information 822 if its name is used as the summary. 823 """ 824 825 event_details = self.details 826 827 if event_details.has_key("title"): 828 return event_details["title"] 829 elif event_details.has_key("summary"): 830 return event_details["summary"] 831 else: 832 # If appropriate, remove the parent details and "/" character. 833 834 title = self.page.getPageName() 835 836 if event_parent and title.startswith(event_parent): 837 title = title[len(event_parent.rstrip("/")) + 1:] 838 839 return getPrettyTitle(title) 840 841 def getDetails(self): 842 843 "Return the details for this event." 844 845 return self.details 846 847 def setDetails(self, event_details): 848 849 "Set the 'event_details' for this event." 850 851 self.details = event_details 852 853 # Timespan-related methods. 854 855 def __contains__(self, other): 856 return self == other 857 858 def __eq__(self, other): 859 if isinstance(other, Event): 860 return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other) 861 else: 862 return self._cmp(other) == 0 863 864 def __ne__(self, other): 865 return not self.__eq__(other) 866 867 def __lt__(self, other): 868 return self._cmp(other) == -1 869 870 def __le__(self, other): 871 return self._cmp(other) in (-1, 0) 872 873 def __gt__(self, other): 874 return self._cmp(other) == 1 875 876 def __ge__(self, other): 877 return self._cmp(other) in (0, 1) 878 879 def _cmp(self, other): 880 881 "Compare this event to an 'other' event purely by their timespans." 882 883 if isinstance(other, Event): 884 return cmp(self.as_timespan(), other.as_timespan()) 885 else: 886 return cmp(self.as_timespan(), other) 887 888 def as_timespan(self): 889 details = self.details 890 if details.has_key("start") and details.has_key("end"): 891 return Timespan(details["start"], details["end"]) 892 else: 893 return None 894 895 def as_limits(self): 896 ts = self.as_timespan() 897 return ts and ts.as_limits() 898 899 class CalendarEvent(Event): 900 901 "An event from a remote calendar." 902 903 def getEventURL(self): 904 905 "Return the URL of this event." 906 907 return self.details.get("url") or self.page.getPageURL() 908 909 def linkToEvent(self, request, text, query_string=None): 910 911 """ 912 Using 'request', return a link to this event with the given link 'text' 913 and optional 'query_string'. 914 """ 915 916 return linkToResource(self.getEventURL(), request, text, query_string) 917 918 def getMetadata(self): 919 920 """ 921 Return a dictionary containing items describing the event's "created" 922 time, "last-modified" time, "sequence" (or revision number) and the 923 "last-comment" made about the last edit. 924 """ 925 926 return { 927 "created" : self.details.get("created") or self.details["dtstamp"], 928 "last-modified" : self.details.get("last-modified") or self.details["dtstamp"], 929 "sequence" : self.details.get("sequence") or 0, 930 "last-comment" : "" 931 } 932 933 # Obtaining event containers and events from such containers. 934 935 def getEventPages(pages): 936 937 "Return a list of events found on the given 'pages'." 938 939 # Get real pages instead of result pages. 940 941 return map(EventPage, pages) 942 943 def getAllEventSources(request): 944 945 "Return all event sources defined in the Wiki using the 'request'." 946 947 sources_page = getattr(request.cfg, "event_aggregator_sources_page", "EventSourcesDict") 948 949 # Remote sources are accessed via dictionary page definitions. 950 951 return getWikiDict(sources_page, request) 952 953 def getEventResources(sources, calendar_start, calendar_end, request): 954 955 """ 956 Return resource objects for the given 'sources' using the given 957 'calendar_start' and 'calendar_end' to parameterise requests to the sources, 958 and the 'request' to access configuration settings in the Wiki. 959 """ 960 961 sources_dict = getAllEventSources(request) 962 if not sources_dict: 963 return [] 964 965 # Use dates for the calendar limits. 966 967 if isinstance(calendar_start, Date): 968 pass 969 elif isinstance(calendar_start, Month): 970 calendar_start = calendar_start.as_date(1) 971 972 if isinstance(calendar_end, Date): 973 pass 974 elif isinstance(calendar_end, Month): 975 calendar_end = calendar_end.as_date(-1) 976 977 resources = [] 978 979 for source in sources: 980 try: 981 details = sources_dict[source].split() 982 url = details[0] 983 format = (details[1:] or ["ical"])[0] 984 except (KeyError, ValueError): 985 pass 986 else: 987 # Prevent local file access. 988 989 if url.startswith("file:"): 990 continue 991 992 # Parameterise the URL. 993 # Where other parameters are used, care must be taken to encode them 994 # properly. 995 996 url = url.replace("{start}", urllib.quote_plus(calendar_start and str(calendar_start) or "")) 997 url = url.replace("{end}", urllib.quote_plus(calendar_end and str(calendar_end) or "")) 998 999 # Get a parser. 1000 # NOTE: This could be done reactively by choosing a parser based on 1001 # NOTE: the content type provided by the URL. 1002 1003 if format == "ical" and vCalendar is not None: 1004 parser = vCalendar.parse 1005 resource_cls = EventCalendar 1006 required_content_type = "text/calendar" 1007 else: 1008 continue 1009 1010 # See if the URL is cached. 1011 1012 cache_key = cache.key(request, content=url) 1013 cache_entry = caching.CacheEntry(request, "EventAggregator", cache_key, scope='wiki') 1014 1015 # If no entry exists, or if the entry is older than a certain age 1016 # (5 minutes by default), create one with the response from the URL. 1017 1018 now = time.time() 1019 mtime = cache_entry.mtime() 1020 max_cache_age = int(getattr(request.cfg, "event_aggregator_max_cache_age", "300")) 1021 1022 # NOTE: The URL could be checked and the 'If-Modified-Since' header 1023 # NOTE: (see MoinMoin.action.pollsistersites) could be checked. 1024 1025 if not cache_entry.exists() or now - mtime >= max_cache_age: 1026 1027 # Access the remote data source. 1028 1029 cache_entry.open(mode="w") 1030 1031 try: 1032 f = urllib2.urlopen(url) 1033 try: 1034 cache_entry.write(url + "\n") 1035 cache_entry.write((f.headers.get("content-type") or "") + "\n") 1036 cache_entry.write(f.read()) 1037 finally: 1038 cache_entry.close() 1039 f.close() 1040 1041 # In case of an exception, just ignore the remote source. 1042 # NOTE: This could be reported somewhere. 1043 1044 except IOError: 1045 if cache_entry.exists(): 1046 cache_entry.remove() 1047 continue 1048 1049 # Open the cache entry and read it. 1050 1051 cache_entry.open() 1052 try: 1053 data = cache_entry.read() 1054 finally: 1055 cache_entry.close() 1056 1057 # Process the entry, parsing the content. 1058 1059 f = StringIO(data) 1060 try: 1061 url = f.readline() 1062 1063 # Get the content type and encoding, making sure that the data 1064 # can be parsed. 1065 1066 content_type, encoding = getContentTypeAndEncoding(f.readline()) 1067 if content_type != required_content_type: 1068 continue 1069 1070 # Send the data to the parser. 1071 1072 uf = codecs.getreader(encoding or "utf-8")(f) 1073 try: 1074 resources.append(resource_cls(url, parser(uf))) 1075 finally: 1076 uf.close() 1077 finally: 1078 f.close() 1079 1080 return resources 1081 1082 def getEventsFromResources(resources): 1083 1084 "Return a list of events supplied by the given event 'resources'." 1085 1086 events = [] 1087 1088 for resource in resources: 1089 1090 # Get all events described by the resource. 1091 1092 for event in resource.getEvents(): 1093 1094 # Remember the event. 1095 1096 events.append(event) 1097 1098 return events 1099 1100 # Event filtering and limits. 1101 1102 def getEventsInPeriod(events, calendar_period): 1103 1104 """ 1105 Return a collection containing those of the given 'events' which occur 1106 within the given 'calendar_period'. 1107 """ 1108 1109 all_shown_events = [] 1110 1111 for event in events: 1112 1113 # Test for the suitability of the event. 1114 1115 if event.as_timespan() is not None: 1116 1117 # Compare the dates to the requested calendar window, if any. 1118 1119 if event in calendar_period: 1120 all_shown_events.append(event) 1121 1122 return all_shown_events 1123 1124 def getEventLimits(events): 1125 1126 "Return the earliest and latest of the given 'events'." 1127 1128 earliest = None 1129 latest = None 1130 1131 for event in events: 1132 1133 # Test for the suitability of the event. 1134 1135 if event.as_timespan() is not None: 1136 ts = event.as_timespan() 1137 if earliest is None or ts.start < earliest: 1138 earliest = ts.start 1139 if latest is None or ts.end > latest: 1140 latest = ts.end 1141 1142 return earliest, latest 1143 1144 def setEventTimestamps(request, events): 1145 1146 """ 1147 Using 'request', set timestamp details in the details dictionary of each of 1148 the 'events'. 1149 1150 Return the latest timestamp found. 1151 """ 1152 1153 latest = None 1154 1155 for event in events: 1156 event_details = event.getDetails() 1157 1158 # Populate the details with event metadata. 1159 1160 event_details.update(event.getMetadata()) 1161 1162 if latest is None or latest < event_details["last-modified"]: 1163 latest = event_details["last-modified"] 1164 1165 return latest 1166 1167 def getOrderedEvents(events): 1168 1169 """ 1170 Return a list with the given 'events' ordered according to their start and 1171 end dates. 1172 """ 1173 1174 ordered_events = events[:] 1175 ordered_events.sort() 1176 return ordered_events 1177 1178 def getCalendarPeriod(calendar_start, calendar_end): 1179 1180 """ 1181 Return a calendar period for the given 'calendar_start' and 'calendar_end'. 1182 These parameters can be given as None. 1183 """ 1184 1185 # Re-order the window, if appropriate. 1186 1187 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 1188 calendar_start, calendar_end = calendar_end, calendar_start 1189 1190 return Timespan(calendar_start, calendar_end) 1191 1192 def getConcretePeriod(calendar_start, calendar_end, earliest, latest, resolution): 1193 1194 """ 1195 From the requested 'calendar_start' and 'calendar_end', which may be None, 1196 indicating that no restriction is imposed on the period for each of the 1197 boundaries, use the 'earliest' and 'latest' event months to define a 1198 specific period of interest. 1199 """ 1200 1201 # Define the period as starting with any specified start month or the 1202 # earliest event known, ending with any specified end month or the latest 1203 # event known. 1204 1205 first = calendar_start or earliest 1206 last = calendar_end or latest 1207 1208 # If there is no range of months to show, perhaps because there are no 1209 # events in the requested period, and there was no start or end month 1210 # specified, show only the month indicated by the start or end of the 1211 # requested period. If all events were to be shown but none were found show 1212 # the current month. 1213 1214 if resolution == "date": 1215 get_current = getCurrentDate 1216 else: 1217 get_current = getCurrentMonth 1218 1219 if first is None: 1220 first = last or get_current() 1221 if last is None: 1222 last = first or get_current() 1223 1224 if resolution == "month": 1225 first = first.as_month() 1226 last = last.as_month() 1227 1228 # Permit "expiring" periods (where the start date approaches the end date). 1229 1230 return min(first, last), last 1231 1232 def getCoverage(events, resolution="date"): 1233 1234 """ 1235 Determine the coverage of the given 'events', returning a collection of 1236 timespans, along with a dictionary mapping locations to collections of 1237 slots, where each slot contains a tuple of the form (timespans, events). 1238 """ 1239 1240 all_events = {} 1241 full_coverage = TimespanCollection(resolution) 1242 1243 # Get event details. 1244 1245 for event in events: 1246 event_details = event.getDetails() 1247 1248 # Find the coverage of this period for the event. 1249 1250 # For day views, each location has its own slot, but for month 1251 # views, all locations are pooled together since having separate 1252 # slots for each location can lead to poor usage of vertical space. 1253 1254 if resolution == "datetime": 1255 event_location = event_details.get("location") 1256 else: 1257 event_location = None 1258 1259 # Update the overall coverage. 1260 1261 full_coverage.insert_in_order(event) 1262 1263 # Add a new events list for a new location. 1264 # Locations can be unspecified, thus None refers to all unlocalised 1265 # events. 1266 1267 if not all_events.has_key(event_location): 1268 all_events[event_location] = [TimespanCollection(resolution, [event])] 1269 1270 # Try and fit the event into an events list. 1271 1272 else: 1273 slot = all_events[event_location] 1274 1275 for slot_events in slot: 1276 1277 # Where the event does not overlap with the events in the 1278 # current collection, add it alongside these events. 1279 1280 if not event in slot_events: 1281 slot_events.insert_in_order(event) 1282 break 1283 1284 # Make a new element in the list if the event cannot be 1285 # marked alongside existing events. 1286 1287 else: 1288 slot.append(TimespanCollection(resolution, [event])) 1289 1290 return full_coverage, all_events 1291 1292 def getCoverageScale(coverage): 1293 1294 """ 1295 Return a scale for the given coverage so that the times involved are 1296 exposed. The scale consists of a list of non-overlapping timespans forming 1297 a contiguous period of time. 1298 """ 1299 1300 times = set() 1301 for timespan in coverage: 1302 start, end = timespan.as_limits() 1303 1304 # Add either genuine times or dates converted to times. 1305 1306 if isinstance(start, DateTime): 1307 times.add(start) 1308 else: 1309 times.add(start.as_start_of_day()) 1310 1311 if isinstance(end, DateTime): 1312 times.add(end) 1313 else: 1314 times.add(end.as_date().next_day()) 1315 1316 times = list(times) 1317 times.sort(cmp_dates_as_day_start) 1318 1319 scale = [] 1320 first = 1 1321 start = None 1322 for time in times: 1323 if not first: 1324 scale.append(Timespan(start, time)) 1325 else: 1326 first = 0 1327 start = time 1328 1329 return scale 1330 1331 def getCountry(s): 1332 1333 "Find a country code in the given string 's'." 1334 1335 match = country_code_regexp.search(s) 1336 1337 if match: 1338 return match.group("code") 1339 else: 1340 return None 1341 1342 # Page-related functions. 1343 1344 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1345 1346 """ 1347 Using the given 'template_page', complete the 'new_page' by copying the 1348 template and adding the given 'event_details' (a dictionary of event 1349 fields), setting also the 'category_pagenames' to define category 1350 membership. 1351 """ 1352 1353 event_page = EventPage(template_page) 1354 new_event_page = EventPage(new_page) 1355 new_event_page.copyPage(event_page) 1356 1357 if new_event_page.getFormat() == "wiki": 1358 new_event = Event(new_event_page, event_details) 1359 new_event_page.setEvents([new_event]) 1360 new_event_page.setCategoryMembership(category_pagenames) 1361 new_event_page.flushEventDetails() 1362 1363 return new_event_page.getBody() 1364 1365 # vim: tabstop=4 expandtab shiftwidth=4