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 request = self.page.request 449 return request.getQualifiedURL(self.page.url(request, relative=0)) 450 451 def getFormat(self): 452 453 "Get the format used on this page." 454 455 return self.page.pi["format"] 456 457 def getMetadata(self): 458 459 """ 460 Return a dictionary containing items describing the page's "created" 461 time, "last-modified" time, "sequence" (or revision number) and the 462 "last-comment" made about the last edit. 463 """ 464 465 request = self.page.request 466 467 # Get the initial revision of the page. 468 469 revisions = self.getRevisions() 470 event_page_initial = Page(request, self.getPageName(), rev=revisions[-1]) 471 472 # Get the created and last modified times. 473 474 initial_revision = getPageRevision(event_page_initial) 475 476 if self.metadata is None: 477 self.metadata = {} 478 self.metadata["created"] = initial_revision["timestamp"] 479 latest_revision = self.getPageRevision() 480 self.metadata["last-modified"] = latest_revision["timestamp"] 481 self.metadata["sequence"] = len(revisions) - 1 482 self.metadata["last-comment"] = latest_revision["comment"] 483 484 return self.metadata 485 486 def getRevisions(self): 487 488 "Return a list of page revisions." 489 490 return self.page.getRevList() 491 492 def getPageRevision(self): 493 494 "Return the revision details dictionary for this page." 495 496 return getPageRevision(self.page) 497 498 def getPageName(self): 499 500 "Return the page name." 501 502 return self.page.page_name 503 504 def getPrettyPageName(self): 505 506 "Return a nicely formatted title/name for this page." 507 508 return getPrettyPageName(self.page) 509 510 def getBody(self): 511 512 "Get the current page body." 513 514 if self.body is None: 515 self.body = self.page.get_raw_body() 516 return self.body 517 518 def getEvents(self): 519 520 "Return a list of events from this page." 521 522 if self.events is None: 523 details = {} 524 self.events = [Event(self, details)] 525 526 if self.getFormat() == "wiki": 527 for match in definition_list_regexp.finditer(self.getBody()): 528 529 # Skip commented-out items. 530 531 if match.group("optcomment"): 532 continue 533 534 # Permit case-insensitive list terms. 535 536 term = match.group("term").lower() 537 desc = match.group("desc") 538 539 # Special value type handling. 540 541 # Dates. 542 543 if term in ("start", "end"): 544 desc = getDateTime(desc) 545 546 # Lists (whose elements may be quoted). 547 548 elif term in ("topics", "categories"): 549 desc = map(getSimpleWikiText, to_list(desc, ",")) 550 551 # Position details. 552 553 elif term == "geo": 554 try: 555 desc = map(getMapReference, to_list(desc, None)) 556 if len(desc) != 2: 557 continue 558 except (KeyError, ValueError): 559 continue 560 561 # Labels which may well be quoted. 562 563 elif term in ("title", "summary", "description", "location"): 564 desc = getSimpleWikiText(desc.strip()) 565 566 if desc is not None: 567 568 # Handle apparent duplicates by creating a new set of 569 # details. 570 571 if details.has_key(term): 572 573 # Make a new event. 574 575 details = {} 576 self.events.append(Event(self, details)) 577 578 details[term] = desc 579 580 return self.events 581 582 def setEvents(self, events): 583 584 "Set the given 'events' on this page." 585 586 self.events = events 587 588 def getCategoryMembership(self): 589 590 "Get the category names from this page." 591 592 if self.categories is None: 593 body = self.getBody() 594 match = category_membership_regexp.search(body) 595 self.categories = match and [x for x in match.groups() if x] or [] 596 597 return self.categories 598 599 def setCategoryMembership(self, category_names): 600 601 """ 602 Set the category membership for the page using the specified 603 'category_names'. 604 """ 605 606 self.categories = category_names 607 608 def flushEventDetails(self): 609 610 "Flush the current event details to this page's body text." 611 612 new_body_parts = [] 613 end_of_last_match = 0 614 body = self.getBody() 615 616 events = iter(self.getEvents()) 617 618 event = events.next() 619 event_details = event.getDetails() 620 replaced_terms = set() 621 622 for match in definition_list_regexp.finditer(body): 623 624 # Permit case-insensitive list terms. 625 626 term = match.group("term").lower() 627 desc = match.group("desc") 628 629 # Check that the term has not already been substituted. If so, 630 # get the next event. 631 632 if term in replaced_terms: 633 try: 634 event = events.next() 635 636 # No more events. 637 638 except StopIteration: 639 break 640 641 event_details = event.getDetails() 642 replaced_terms = set() 643 644 # Add preceding text to the new body. 645 646 new_body_parts.append(body[end_of_last_match:match.start()]) 647 648 # Get the matching regions, adding the term to the new body. 649 650 new_body_parts.append(match.group("wholeterm")) 651 652 # Special value type handling. 653 654 if event_details.has_key(term): 655 656 # Dates. 657 658 if term in ("start", "end"): 659 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 660 661 # Lists (whose elements may be quoted). 662 663 elif term in ("topics", "categories"): 664 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 665 666 # Labels which must be quoted. 667 668 elif term in ("title", "summary"): 669 desc = getEncodedWikiText(event_details[term]) 670 671 # Position details. 672 673 elif term == "geo": 674 desc = " ".join(map(str, event_details[term])) 675 676 # Text which need not be quoted, but it will be Wiki text. 677 678 elif term in ("description", "link", "location"): 679 desc = event_details[term] 680 681 replaced_terms.add(term) 682 683 # Add the replaced value. 684 685 new_body_parts.append(desc) 686 687 # Remember where in the page has been processed. 688 689 end_of_last_match = match.end() 690 691 # Write the rest of the page. 692 693 new_body_parts.append(body[end_of_last_match:]) 694 695 self.body = "".join(new_body_parts) 696 697 def flushCategoryMembership(self): 698 699 "Flush the category membership to the page body." 700 701 body = self.getBody() 702 category_names = self.getCategoryMembership() 703 match = category_membership_regexp.search(body) 704 705 if match: 706 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 707 708 def saveChanges(self): 709 710 "Save changes to the event." 711 712 self.flushEventDetails() 713 self.flushCategoryMembership() 714 self.page.saveText(self.getBody(), 0) 715 716 def linkToPage(self, request, text, query_string=None): 717 718 """ 719 Using 'request', return a link to this page with the given link 'text' 720 and optional 'query_string'. 721 """ 722 723 return linkToPage(request, self.page, text, query_string) 724 725 # Formatting-related functions. 726 727 def getParserClass(self, request, format): 728 729 """ 730 Return a parser class using the 'request' for the given 'format', returning 731 a plain text parser if no parser can be found for the specified 'format'. 732 """ 733 734 try: 735 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 736 except wikiutil.PluginMissingError: 737 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 738 739 def formatText(self, text, request, fmt): 740 741 """ 742 Format the given 'text' using the specified 'request' and formatter 743 'fmt'. 744 """ 745 746 fmt.page = self.page 747 748 # Suppress line anchors. 749 750 parser_cls = self.getParserClass(request, self.getFormat()) 751 parser = parser_cls(text, request, line_anchors=False) 752 753 # Fix lists by indicating that a paragraph is already started. 754 755 return request.redirectedOutput(parser.format, fmt, inhibit_p=True) 756 757 # Event details. 758 759 class Event(ActsAsTimespan): 760 761 "A description of an event." 762 763 def __init__(self, page, details): 764 self.page = page 765 self.details = details 766 767 # Permit omission of the end of the event by duplicating the start. 768 769 if self.details.has_key("start") and not self.details.get("end"): 770 end = self.details["start"] 771 772 # Make any end time refer to the day instead. 773 774 if isinstance(end, DateTime): 775 end = end.as_date() 776 777 self.details["end"] = end 778 779 def __repr__(self): 780 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 781 782 def __hash__(self): 783 784 """ 785 Return a dictionary hash, avoiding mistaken equality of events in some 786 situations (notably membership tests) by including the URL as well as 787 the summary. 788 """ 789 790 return hash(self.getSummary() + self.getEventURL()) 791 792 def getPage(self): 793 794 "Return the page describing this event." 795 796 return self.page 797 798 def setPage(self, page): 799 800 "Set the 'page' describing this event." 801 802 self.page = page 803 804 def getEventURL(self): 805 806 "Return the URL of this event." 807 808 return self.page.getPageURL() 809 810 def linkToEvent(self, request, text, query_string=None): 811 812 """ 813 Using 'request', return a link to this event with the given link 'text' 814 and optional 'query_string'. 815 """ 816 817 return self.page.linkToPage(request, text, query_string) 818 819 def getMetadata(self): 820 821 """ 822 Return a dictionary containing items describing the event's "created" 823 time, "last-modified" time, "sequence" (or revision number) and the 824 "last-comment" made about the last edit. 825 """ 826 827 # Delegate this to the page. 828 829 return self.page.getMetadata() 830 831 def getSummary(self, event_parent=None): 832 833 """ 834 Return either the given title or summary of the event according to the 835 event details, or a summary made from using the pretty version of the 836 page name. 837 838 If the optional 'event_parent' is specified, any page beneath the given 839 'event_parent' page in the page hierarchy will omit this parent information 840 if its name is used as the summary. 841 """ 842 843 event_details = self.details 844 845 if event_details.has_key("title"): 846 return event_details["title"] 847 elif event_details.has_key("summary"): 848 return event_details["summary"] 849 else: 850 # If appropriate, remove the parent details and "/" character. 851 852 title = self.page.getPageName() 853 854 if event_parent and title.startswith(event_parent): 855 title = title[len(event_parent.rstrip("/")) + 1:] 856 857 return getPrettyTitle(title) 858 859 def getDetails(self): 860 861 "Return the details for this event." 862 863 return self.details 864 865 def setDetails(self, event_details): 866 867 "Set the 'event_details' for this event." 868 869 self.details = event_details 870 871 # Timespan-related methods. 872 873 def __contains__(self, other): 874 return self == other 875 876 def __eq__(self, other): 877 if isinstance(other, Event): 878 return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other) 879 else: 880 return self._cmp(other) == 0 881 882 def __ne__(self, other): 883 return not self.__eq__(other) 884 885 def __lt__(self, other): 886 return self._cmp(other) == -1 887 888 def __le__(self, other): 889 return self._cmp(other) in (-1, 0) 890 891 def __gt__(self, other): 892 return self._cmp(other) == 1 893 894 def __ge__(self, other): 895 return self._cmp(other) in (0, 1) 896 897 def _cmp(self, other): 898 899 "Compare this event to an 'other' event purely by their timespans." 900 901 if isinstance(other, Event): 902 return cmp(self.as_timespan(), other.as_timespan()) 903 else: 904 return cmp(self.as_timespan(), other) 905 906 def as_timespan(self): 907 details = self.details 908 if details.has_key("start") and details.has_key("end"): 909 return Timespan(details["start"], details["end"]) 910 else: 911 return None 912 913 def as_limits(self): 914 ts = self.as_timespan() 915 return ts and ts.as_limits() 916 917 class CalendarEvent(Event): 918 919 "An event from a remote calendar." 920 921 def getEventURL(self): 922 923 "Return the URL of this event." 924 925 return self.details.get("url") or self.page.getPageURL() 926 927 def linkToEvent(self, request, text, query_string=None): 928 929 """ 930 Using 'request', return a link to this event with the given link 'text' 931 and optional 'query_string'. 932 """ 933 934 return linkToResource(self.getEventURL(), request, text, query_string) 935 936 def getMetadata(self): 937 938 """ 939 Return a dictionary containing items describing the event's "created" 940 time, "last-modified" time, "sequence" (or revision number) and the 941 "last-comment" made about the last edit. 942 """ 943 944 return { 945 "created" : self.details.get("created") or self.details["dtstamp"], 946 "last-modified" : self.details.get("last-modified") or self.details["dtstamp"], 947 "sequence" : self.details.get("sequence") or 0, 948 "last-comment" : "" 949 } 950 951 # Obtaining event containers and events from such containers. 952 953 def getEventPages(pages): 954 955 "Return a list of events found on the given 'pages'." 956 957 # Get real pages instead of result pages. 958 959 return map(EventPage, pages) 960 961 def getAllEventSources(request): 962 963 "Return all event sources defined in the Wiki using the 'request'." 964 965 sources_page = getattr(request.cfg, "event_aggregator_sources_page", "EventSourcesDict") 966 967 # Remote sources are accessed via dictionary page definitions. 968 969 return getWikiDict(sources_page, request) 970 971 def getEventResources(sources, calendar_start, calendar_end, request): 972 973 """ 974 Return resource objects for the given 'sources' using the given 975 'calendar_start' and 'calendar_end' to parameterise requests to the sources, 976 and the 'request' to access configuration settings in the Wiki. 977 """ 978 979 sources_dict = getAllEventSources(request) 980 if not sources_dict: 981 return [] 982 983 # Use dates for the calendar limits. 984 985 if isinstance(calendar_start, Date): 986 pass 987 elif isinstance(calendar_start, Month): 988 calendar_start = calendar_start.as_date(1) 989 990 if isinstance(calendar_end, Date): 991 pass 992 elif isinstance(calendar_end, Month): 993 calendar_end = calendar_end.as_date(-1) 994 995 resources = [] 996 997 for source in sources: 998 try: 999 details = sources_dict[source].split() 1000 url = details[0] 1001 format = (details[1:] or ["ical"])[0] 1002 except (KeyError, ValueError): 1003 pass 1004 else: 1005 # Prevent local file access. 1006 1007 if url.startswith("file:"): 1008 continue 1009 1010 # Parameterise the URL. 1011 # Where other parameters are used, care must be taken to encode them 1012 # properly. 1013 1014 url = url.replace("{start}", urllib.quote_plus(calendar_start and str(calendar_start) or "")) 1015 url = url.replace("{end}", urllib.quote_plus(calendar_end and str(calendar_end) or "")) 1016 1017 # Get a parser. 1018 # NOTE: This could be done reactively by choosing a parser based on 1019 # NOTE: the content type provided by the URL. 1020 1021 if format == "ical" and vCalendar is not None: 1022 parser = vCalendar.parse 1023 resource_cls = EventCalendar 1024 required_content_type = "text/calendar" 1025 else: 1026 continue 1027 1028 # See if the URL is cached. 1029 1030 cache_key = cache.key(request, content=url) 1031 cache_entry = caching.CacheEntry(request, "EventAggregator", cache_key, scope='wiki') 1032 1033 # If no entry exists, or if the entry is older than a certain age 1034 # (5 minutes by default), create one with the response from the URL. 1035 1036 now = time.time() 1037 mtime = cache_entry.mtime() 1038 max_cache_age = int(getattr(request.cfg, "event_aggregator_max_cache_age", "300")) 1039 1040 # NOTE: The URL could be checked and the 'If-Modified-Since' header 1041 # NOTE: (see MoinMoin.action.pollsistersites) could be checked. 1042 1043 if not cache_entry.exists() or now - mtime >= max_cache_age: 1044 1045 # Access the remote data source. 1046 1047 cache_entry.open(mode="w") 1048 1049 try: 1050 f = urllib2.urlopen(url) 1051 try: 1052 cache_entry.write(url + "\n") 1053 cache_entry.write((f.headers.get("content-type") or "") + "\n") 1054 cache_entry.write(f.read()) 1055 finally: 1056 cache_entry.close() 1057 f.close() 1058 1059 # In case of an exception, just ignore the remote source. 1060 # NOTE: This could be reported somewhere. 1061 1062 except IOError: 1063 if cache_entry.exists(): 1064 cache_entry.remove() 1065 continue 1066 1067 # Open the cache entry and read it. 1068 1069 cache_entry.open() 1070 try: 1071 data = cache_entry.read() 1072 finally: 1073 cache_entry.close() 1074 1075 # Process the entry, parsing the content. 1076 1077 f = StringIO(data) 1078 try: 1079 url = f.readline() 1080 1081 # Get the content type and encoding, making sure that the data 1082 # can be parsed. 1083 1084 content_type, encoding = getContentTypeAndEncoding(f.readline()) 1085 if content_type != required_content_type: 1086 continue 1087 1088 # Send the data to the parser. 1089 1090 uf = codecs.getreader(encoding or "utf-8")(f) 1091 try: 1092 resources.append(resource_cls(url, parser(uf))) 1093 finally: 1094 uf.close() 1095 finally: 1096 f.close() 1097 1098 return resources 1099 1100 def getEventsFromResources(resources): 1101 1102 "Return a list of events supplied by the given event 'resources'." 1103 1104 events = [] 1105 1106 for resource in resources: 1107 1108 # Get all events described by the resource. 1109 1110 for event in resource.getEvents(): 1111 1112 # Remember the event. 1113 1114 events.append(event) 1115 1116 return events 1117 1118 # Event filtering and limits. 1119 1120 def getEventsInPeriod(events, calendar_period): 1121 1122 """ 1123 Return a collection containing those of the given 'events' which occur 1124 within the given 'calendar_period'. 1125 """ 1126 1127 all_shown_events = [] 1128 1129 for event in events: 1130 1131 # Test for the suitability of the event. 1132 1133 if event.as_timespan() is not None: 1134 1135 # Compare the dates to the requested calendar window, if any. 1136 1137 if event in calendar_period: 1138 all_shown_events.append(event) 1139 1140 return all_shown_events 1141 1142 def getEventLimits(events): 1143 1144 "Return the earliest and latest of the given 'events'." 1145 1146 earliest = None 1147 latest = None 1148 1149 for event in events: 1150 1151 # Test for the suitability of the event. 1152 1153 if event.as_timespan() is not None: 1154 ts = event.as_timespan() 1155 if earliest is None or ts.start < earliest: 1156 earliest = ts.start 1157 if latest is None or ts.end > latest: 1158 latest = ts.end 1159 1160 return earliest, latest 1161 1162 def setEventTimestamps(request, events): 1163 1164 """ 1165 Using 'request', set timestamp details in the details dictionary of each of 1166 the 'events'. 1167 1168 Return the latest timestamp found. 1169 """ 1170 1171 latest = None 1172 1173 for event in events: 1174 event_details = event.getDetails() 1175 1176 # Populate the details with event metadata. 1177 1178 event_details.update(event.getMetadata()) 1179 1180 if latest is None or latest < event_details["last-modified"]: 1181 latest = event_details["last-modified"] 1182 1183 return latest 1184 1185 def getOrderedEvents(events): 1186 1187 """ 1188 Return a list with the given 'events' ordered according to their start and 1189 end dates. 1190 """ 1191 1192 ordered_events = events[:] 1193 ordered_events.sort() 1194 return ordered_events 1195 1196 def getCalendarPeriod(calendar_start, calendar_end): 1197 1198 """ 1199 Return a calendar period for the given 'calendar_start' and 'calendar_end'. 1200 These parameters can be given as None. 1201 """ 1202 1203 # Re-order the window, if appropriate. 1204 1205 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 1206 calendar_start, calendar_end = calendar_end, calendar_start 1207 1208 return Timespan(calendar_start, calendar_end) 1209 1210 def getConcretePeriod(calendar_start, calendar_end, earliest, latest, resolution): 1211 1212 """ 1213 From the requested 'calendar_start' and 'calendar_end', which may be None, 1214 indicating that no restriction is imposed on the period for each of the 1215 boundaries, use the 'earliest' and 'latest' event months to define a 1216 specific period of interest. 1217 """ 1218 1219 # Define the period as starting with any specified start month or the 1220 # earliest event known, ending with any specified end month or the latest 1221 # event known. 1222 1223 first = calendar_start or earliest 1224 last = calendar_end or latest 1225 1226 # If there is no range of months to show, perhaps because there are no 1227 # events in the requested period, and there was no start or end month 1228 # specified, show only the month indicated by the start or end of the 1229 # requested period. If all events were to be shown but none were found show 1230 # the current month. 1231 1232 if resolution == "date": 1233 get_current = getCurrentDate 1234 else: 1235 get_current = getCurrentMonth 1236 1237 if first is None: 1238 first = last or get_current() 1239 if last is None: 1240 last = first or get_current() 1241 1242 if resolution == "month": 1243 first = first.as_month() 1244 last = last.as_month() 1245 1246 # Permit "expiring" periods (where the start date approaches the end date). 1247 1248 return min(first, last), last 1249 1250 def getCoverage(events, resolution="date"): 1251 1252 """ 1253 Determine the coverage of the given 'events', returning a collection of 1254 timespans, along with a dictionary mapping locations to collections of 1255 slots, where each slot contains a tuple of the form (timespans, events). 1256 """ 1257 1258 all_events = {} 1259 full_coverage = TimespanCollection(resolution) 1260 1261 # Get event details. 1262 1263 for event in events: 1264 event_details = event.getDetails() 1265 1266 # Find the coverage of this period for the event. 1267 1268 # For day views, each location has its own slot, but for month 1269 # views, all locations are pooled together since having separate 1270 # slots for each location can lead to poor usage of vertical space. 1271 1272 if resolution == "datetime": 1273 event_location = event_details.get("location") 1274 else: 1275 event_location = None 1276 1277 # Update the overall coverage. 1278 1279 full_coverage.insert_in_order(event) 1280 1281 # Add a new events list for a new location. 1282 # Locations can be unspecified, thus None refers to all unlocalised 1283 # events. 1284 1285 if not all_events.has_key(event_location): 1286 all_events[event_location] = [TimespanCollection(resolution, [event])] 1287 1288 # Try and fit the event into an events list. 1289 1290 else: 1291 slot = all_events[event_location] 1292 1293 for slot_events in slot: 1294 1295 # Where the event does not overlap with the events in the 1296 # current collection, add it alongside these events. 1297 1298 if not event in slot_events: 1299 slot_events.insert_in_order(event) 1300 break 1301 1302 # Make a new element in the list if the event cannot be 1303 # marked alongside existing events. 1304 1305 else: 1306 slot.append(TimespanCollection(resolution, [event])) 1307 1308 return full_coverage, all_events 1309 1310 def getCoverageScale(coverage): 1311 1312 """ 1313 Return a scale for the given coverage so that the times involved are 1314 exposed. The scale consists of a list of non-overlapping timespans forming 1315 a contiguous period of time. 1316 """ 1317 1318 times = set() 1319 for timespan in coverage: 1320 start, end = timespan.as_limits() 1321 1322 # Add either genuine times or dates converted to times. 1323 1324 if isinstance(start, DateTime): 1325 times.add(start) 1326 else: 1327 times.add(start.as_start_of_day()) 1328 1329 if isinstance(end, DateTime): 1330 times.add(end) 1331 else: 1332 times.add(end.as_date().next_day()) 1333 1334 times = list(times) 1335 times.sort(cmp_dates_as_day_start) 1336 1337 scale = [] 1338 first = 1 1339 start = None 1340 for time in times: 1341 if not first: 1342 scale.append(Timespan(start, time)) 1343 else: 1344 first = 0 1345 start = time 1346 1347 return scale 1348 1349 def getCountry(s): 1350 1351 "Find a country code in the given string 's'." 1352 1353 match = country_code_regexp.search(s) 1354 1355 if match: 1356 return match.group("code") 1357 else: 1358 return None 1359 1360 # Page-related functions. 1361 1362 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1363 1364 """ 1365 Using the given 'template_page', complete the 'new_page' by copying the 1366 template and adding the given 'event_details' (a dictionary of event 1367 fields), setting also the 'category_pagenames' to define category 1368 membership. 1369 """ 1370 1371 event_page = EventPage(template_page) 1372 new_event_page = EventPage(new_page) 1373 new_event_page.copyPage(event_page) 1374 1375 if new_event_page.getFormat() == "wiki": 1376 new_event = Event(new_event_page, event_details) 1377 new_event_page.setEvents([new_event]) 1378 new_event_page.setCategoryMembership(category_pagenames) 1379 new_event_page.flushEventDetails() 1380 1381 return new_event_page.getBody() 1382 1383 # vim: tabstop=4 expandtab shiftwidth=4