1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator library 4 5 @copyright: 2008, 2009, 2010, 2011 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 MoinMoin.Page import Page 12 from MoinMoin.action import cache 13 from MoinMoin import caching 14 from MoinMoin import search, version 15 from MoinMoin import wikiutil 16 import calendar 17 import codecs 18 import datetime 19 import time 20 import re 21 import bisect 22 import operator 23 import urllib 24 25 try: 26 from cStringIO import StringIO 27 except ImportError: 28 from StringIO import StringIO 29 30 try: 31 set 32 except NameError: 33 from sets import Set as set 34 35 try: 36 import pytz 37 except ImportError: 38 pytz = None 39 40 try: 41 import vCalendar 42 except ImportError: 43 vCalendar = None 44 45 escape = wikiutil.escape 46 47 __version__ = "0.7.1" 48 49 # Date labels. 50 51 month_labels = ["January", "February", "March", "April", "May", "June", 52 "July", "August", "September", "October", "November", "December"] 53 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 54 55 # Regular expressions where MoinMoin does not provide the required support. 56 57 category_regexp = None 58 59 # Page parsing. 60 61 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 62 category_membership_regexp = re.compile(ur"^\s*(?:(Category\S+)(?:\s+(Category\S+))*)\s*$", re.MULTILINE | re.UNICODE) 63 64 # Value parsing. 65 66 country_code_regexp = re.compile(ur'(?:^|\W)(?P<code>[A-Z]{2})(?:$|\W+$)', re.UNICODE) 67 location_normalised_regexp = re.compile( 68 ur"(?:\d+\w*\s+)?" # preceding postcode (optional) 69 ur"(?P<location>" # start of group of interest 70 ur"\w[\w\s-]+?" # area or town 71 ur"(?:,(?:\s*[\w-]+)+)?" # country (optional) 72 ur")$", re.UNICODE) 73 74 # Month, date, time and datetime parsing. 75 76 month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})' 77 date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})' 78 time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?' 79 timezone_offset_str = ur'(?P<offset>(UTC)?(?:(?P<sign>[-+])(?P<hours>[0-9]{2})(?::?(?P<minutes>[0-9]{2}))?))' 80 timezone_olson_str = ur'(?P<olson>[a-zA-Z]+(?:/[-_a-zA-Z]+){1,2})' 81 timezone_utc_str = ur'UTC' 82 timezone_regexp_str = ur'(?P<zone>' + timezone_offset_str + '|' + timezone_olson_str + '|' + timezone_utc_str + ')' 83 datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?' 84 85 month_regexp = re.compile(month_regexp_str, re.UNICODE) 86 date_regexp = re.compile(date_regexp_str, re.UNICODE) 87 time_regexp = re.compile(time_regexp_str, re.UNICODE) 88 timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE) 89 timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE) 90 datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE) 91 92 # iCalendar date and datetime parsing. 93 94 date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})' 95 datetime_icalendar_regexp_str = date_icalendar_regexp_str + \ 96 ur'(?:' \ 97 ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \ 98 ur'(?P<utc>Z)?' \ 99 ur')?' 100 101 date_icalendar_regexp = re.compile(date_icalendar_regexp_str, re.UNICODE) 102 datetime_icalendar_regexp = re.compile(datetime_icalendar_regexp_str, re.UNICODE) 103 104 # Content type parsing. 105 106 encoding_regexp_str = ur'charset=(?P<encoding>[-A-Za-z0-9]+)' 107 encoding_regexp = re.compile(encoding_regexp_str) 108 109 # Simple content parsing. 110 111 verbatim_regexp = re.compile(ur'(?:' 112 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 113 ur'|' 114 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 115 ur'|' 116 ur'!(?P<verbatim3>.*?)(\s|$)?' 117 ur'|' 118 ur'`(?P<monospace>.*?)`' 119 ur'|' 120 ur'{{{(?P<preformatted>.*?)}}}' 121 ur')', re.UNICODE) 122 123 # Utility functions. 124 125 def getCategoryPattern(request): 126 global category_regexp 127 128 try: 129 return request.cfg.cache.page_category_regexact 130 except AttributeError: 131 132 # Use regular expression from MoinMoin 1.7.1 otherwise. 133 134 if category_regexp is None: 135 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 136 return category_regexp 137 138 def getWikiDict(pagename, request): 139 if Page(request, pagename).exists() and request.user.may.read(pagename): 140 if hasattr(request.dicts, "dict"): 141 return request.dicts.dict(pagename) 142 else: 143 return request.dicts[pagename] 144 else: 145 return None 146 147 def getContentEncoding(content_type): 148 m = encoding_regexp.search(content_type) 149 if m: 150 return m.group("encoding") 151 else: 152 return None 153 154 def int_or_none(x): 155 if x is None: 156 return x 157 else: 158 return int(x) 159 160 def to_list(s, sep): 161 return [x.strip() for x in s.split(sep) if x.strip()] 162 163 def sort_none_first(x, y): 164 if x is None: 165 return -1 166 elif y is None: 167 return 1 168 else: 169 return cmp(x, y) 170 171 def sort_start_first(x, y): 172 x_ts = x.as_limits() 173 if x_ts is not None: 174 x_start, x_end = x_ts 175 y_ts = y.as_limits() 176 if y_ts is not None: 177 y_start, y_end = y_ts 178 start_order = cmp(x_start, y_start) 179 if start_order == 0: 180 return cmp(x_end, y_end) 181 else: 182 return start_order 183 return 0 184 185 def sign(x): 186 if x < 0: 187 return -1 188 else: 189 return 1 190 191 # Utility classes and associated functions. 192 193 class Form: 194 195 """ 196 A wrapper preserving MoinMoin 1.8.x (and earlier) behaviour in a 1.9.x 197 environment. 198 """ 199 200 def __init__(self, form): 201 self.form = form 202 203 def get(self, name, default=None): 204 values = self.form.getlist(name) 205 if not values: 206 return default 207 else: 208 return values 209 210 def __getitem__(self, name): 211 return self.form.getlist(name) 212 213 class ActionSupport: 214 215 """ 216 Work around disruptive MoinMoin changes in 1.9, and also provide useful 217 convenience methods. 218 """ 219 220 def get_form(self): 221 return get_form(self.request) 222 223 def _get_selected(self, value, input_value): 224 225 """ 226 Return the HTML attribute text indicating selection of an option (or 227 otherwise) if 'value' matches 'input_value'. 228 """ 229 230 return input_value is not None and value == input_value and 'selected="selected"' or '' 231 232 def _get_selected_for_list(self, value, input_values): 233 234 """ 235 Return the HTML attribute text indicating selection of an option (or 236 otherwise) if 'value' matches one of the 'input_values'. 237 """ 238 239 return value in input_values and 'selected="selected"' or '' 240 241 def _get_input(self, form, name, default=None): 242 243 """ 244 Return the input from 'form' having the given 'name', returning either 245 the input converted to an integer or the given 'default' (optional, None 246 if not specified). 247 """ 248 249 value = form.get(name, [None])[0] 250 if not value: # true if 0 obtained 251 return default 252 else: 253 return int(value) 254 255 def get_month_lists(self, default_as_current=0): 256 257 """ 258 Return two lists of HTML element definitions corresponding to the start 259 and end month selection controls, with months selected according to any 260 values that have been specified via request parameters. 261 """ 262 263 _ = self._ 264 form = self.get_form() 265 266 # Initialise month lists. 267 268 start_month_list = [] 269 end_month_list = [] 270 271 start_month = self._get_input(form, "start-month", default_as_current and getCurrentMonth().month() or None) 272 end_month = self._get_input(form, "end-month", start_month) 273 274 # Prepare month lists, selecting specified months. 275 276 if not default_as_current: 277 start_month_list.append('<option value=""></option>') 278 end_month_list.append('<option value=""></option>') 279 280 for month in range(1, 13): 281 month_label = escape(_(getMonthLabel(month))) 282 selected = self._get_selected(month, start_month) 283 start_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 284 selected = self._get_selected(month, end_month) 285 end_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 286 287 return start_month_list, end_month_list 288 289 def get_year_defaults(self, default_as_current=0): 290 291 "Return defaults for the start and end years." 292 293 form = self.get_form() 294 295 start_year_default = form.get("start-year", [default_as_current and getCurrentYear() or ""])[0] 296 end_year_default = form.get("end-year", [default_as_current and start_year_default or ""])[0] 297 298 return start_year_default, end_year_default 299 300 def get_day_defaults(self, default_as_current=0): 301 302 "Return defaults for the start and end days." 303 304 form = self.get_form() 305 306 start_day_default = form.get("start-day", [default_as_current and getCurrentDate().day() or ""])[0] 307 end_day_default = form.get("end-day", [default_as_current and start_day_default or ""])[0] 308 309 return start_day_default, end_day_default 310 311 def get_form(request): 312 313 "Work around disruptive MoinMoin changes in 1.9." 314 315 if hasattr(request, "values"): 316 return Form(request.values) 317 else: 318 return request.form 319 320 class send_headers_cls: 321 322 """ 323 A wrapper to preserve MoinMoin 1.8.x (and earlier) request behaviour in a 324 1.9.x environment. 325 """ 326 327 def __init__(self, request): 328 self.request = request 329 330 def __call__(self, headers): 331 for header in headers: 332 parts = header.split(":") 333 self.request.headers.add(parts[0], ":".join(parts[1:])) 334 335 def escattr(s): 336 return escape(s, 1) 337 338 # Textual representations. 339 340 def getSimpleWikiText(text): 341 342 """ 343 Return the plain text representation of the given 'text' which may employ 344 certain Wiki syntax features, such as those providing verbatim or monospaced 345 text. 346 """ 347 348 # NOTE: Re-implementing support for verbatim text and linking avoidance. 349 350 return "".join([s for s in verbatim_regexp.split(text) if s is not None]) 351 352 def getEncodedWikiText(text): 353 354 "Encode the given 'text' in a verbatim representation." 355 356 return "<<Verbatim(%s)>>" % text 357 358 def getPrettyTitle(title): 359 360 "Return a nicely formatted version of the given 'title'." 361 362 return title.replace("_", " ").replace("/", u" ? ") 363 364 def getMonthLabel(month): 365 366 "Return an unlocalised label for the given 'month'." 367 368 return month_labels[month - 1] # zero-based labels 369 370 def getDayLabel(weekday): 371 372 "Return an unlocalised label for the given 'weekday'." 373 374 return weekday_labels[weekday] 375 376 def getNormalisedLocation(location): 377 378 """ 379 Attempt to return a normalised 'location' of the form "<town>, <country>" or 380 "<town>". 381 """ 382 383 match = location_normalised_regexp.search(location) 384 if match: 385 return match.group("location") 386 else: 387 return None 388 389 def getLocationPosition(location, locations): 390 391 """ 392 Attempt to return the position of the given 'location' using the 'locations' 393 dictionary provided. If no position can be found, return a latitude of None 394 and a longitude of None. 395 """ 396 397 latitude, longitude = None, None 398 399 if location is not None: 400 try: 401 latitude, longitude = map(getMapReference, locations[location].split()) 402 except (KeyError, ValueError): 403 pass 404 405 return latitude, longitude 406 407 # Action support functions. 408 409 def getPageRevision(page): 410 411 "Return the revision details dictionary for the given 'page'." 412 413 # From Page.edit_info... 414 415 if hasattr(page, "editlog_entry"): 416 line = page.editlog_entry() 417 else: 418 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 419 420 # Similar to Page.mtime_usecs behaviour... 421 422 if line: 423 timestamp = line.ed_time_usecs 424 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 425 comment = line.comment 426 else: 427 mtime = 0 428 comment = "" 429 430 # Leave the time zone empty. 431 432 return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} 433 434 # Category discovery and searching. 435 436 def getCategories(request): 437 438 """ 439 From the AdvancedSearch macro, return a list of category page names using 440 the given 'request'. 441 """ 442 443 # This will return all pages with "Category" in the title. 444 445 cat_filter = getCategoryPattern(request).search 446 return request.rootpage.getPageList(filter=cat_filter) 447 448 def getCategoryMapping(category_pagenames, request): 449 450 """ 451 For the given 'category_pagenames' return a list of tuples of the form 452 (category name, category page name) using the given 'request'. 453 """ 454 455 cat_pattern = getCategoryPattern(request) 456 mapping = [] 457 for pagename in category_pagenames: 458 name = cat_pattern.match(pagename).group("key") 459 if name != "Category": 460 mapping.append((name, pagename)) 461 mapping.sort() 462 return mapping 463 464 def getCategoryPages(pagename, request): 465 466 """ 467 Return the pages associated with the given category 'pagename' using the 468 'request'. 469 """ 470 471 query = search.QueryParser().parse_query('category:%s' % pagename) 472 results = search.searchPages(request, query, "page_name") 473 474 cat_pattern = getCategoryPattern(request) 475 pages = [] 476 for page in results.hits: 477 if not cat_pattern.match(page.page_name): 478 pages.append(page) 479 return pages 480 481 def getAllCategoryPages(category_names, request): 482 483 """ 484 Return all pages belonging to the categories having the given 485 'category_names', using the given 'request'. 486 """ 487 488 pages = [] 489 pagenames = set() 490 491 for category_name in category_names: 492 493 # Get the pages and page names in the category. 494 495 pages_in_category = getCategoryPages(category_name, request) 496 497 # Visit each page in the category. 498 499 for page_in_category in pages_in_category: 500 pagename = page_in_category.page_name 501 502 # Only process each page once. 503 504 if pagename in pagenames: 505 continue 506 else: 507 pagenames.add(pagename) 508 509 pages.append(page_in_category) 510 511 return pages 512 513 def getPagesFromResults(result_pages, request): 514 515 "Return genuine pages for the given 'result_pages' using the 'request'." 516 517 return [Page(request, page.page_name) for page in result_pages] 518 519 # Interfaces. 520 521 class ActsAsTimespan: 522 pass 523 524 # Event resources providing collections of events. 525 526 class EventResource: 527 528 "A resource providing event information." 529 530 def __init__(self, url): 531 self.url = url 532 533 def getPageURL(self): 534 535 "Return the URL of this page." 536 537 return self.url 538 539 def getFormat(self): 540 541 "Get the format used by this resource." 542 543 return "plain" 544 545 def getMetadata(self): 546 547 """ 548 Return a dictionary containing items describing the page's "created" 549 time, "last-modified" time, "sequence" (or revision number) and the 550 "last-comment" made about the last edit. 551 """ 552 553 return {} 554 555 def getEvents(self): 556 557 "Return a list of events from this resource." 558 559 return [] 560 561 def linkToPage(self, request, text, query_string=None): 562 563 """ 564 Using 'request', return a link to this page with the given link 'text' 565 and optional 'query_string'. 566 """ 567 568 return linkToResource(self.url, request, text, query_string) 569 570 # Formatting-related functions. 571 572 def formatText(self, text, request, fmt): 573 574 """ 575 Format the given 'text' using the specified 'request' and formatter 576 'fmt'. 577 """ 578 579 # Assume plain text which is then formatted appropriately. 580 581 return fmt.text(text) 582 583 class EventCalendar(EventResource): 584 585 "An iCalendar resource." 586 587 def __init__(self, url, calendar): 588 EventResource.__init__(self, url) 589 self.calendar = calendar 590 self.events = None 591 592 def getEvents(self): 593 594 "Return a list of events from this resource." 595 596 if self.events is None: 597 self.events = [] 598 599 _calendar, _empty, calendar = self.calendar 600 601 for objtype, attrs, obj in calendar: 602 603 # Read events. 604 605 if objtype == "VEVENT": 606 details = {} 607 608 for property, attrs, value in obj: 609 610 # Convert dates. 611 612 if property in ("DTSTART", "DTEND", "CREATED", "DTSTAMP", "LAST-MODIFIED"): 613 if property in ("DTSTART", "DTEND"): 614 property = property[2:] 615 if attrs.get("VALUE") == "DATE": 616 value = getDateFromCalendar(value) 617 else: 618 value = getDateTimeFromCalendar(value) 619 620 # Convert numeric data. 621 622 elif property == "SEQUENCE": 623 value = int(value) 624 625 # Convert lists. 626 627 elif property == "CATEGORIES": 628 value = to_list(value, ",") 629 630 # Convert positions (using decimal values). 631 632 elif property == "GEO": 633 try: 634 value = map(getMapReferenceFromDecimal, to_list(value, ";")) 635 except (KeyError, ValueError): 636 continue 637 638 # Accept other textual data as it is. 639 640 elif property in ("LOCATION", "SUMMARY", "URL"): 641 pass 642 643 # Ignore other properties. 644 645 else: 646 continue 647 648 property = property.lower() 649 details[property] = value 650 651 self.events.append(CalendarEvent(self, details)) 652 653 return self.events 654 655 class EventPage: 656 657 "An event page acting as an event resource." 658 659 def __init__(self, page): 660 self.page = page 661 self.events = None 662 self.body = None 663 self.categories = None 664 self.metadata = None 665 666 def copyPage(self, page): 667 668 "Copy the body of the given 'page'." 669 670 self.body = page.getBody() 671 672 def getPageURL(self): 673 674 "Return the URL of this page." 675 676 request = self.page.request 677 return request.getQualifiedURL(self.page.url(request, relative=0)) 678 679 def getFormat(self): 680 681 "Get the format used on this page." 682 683 return self.page.pi["format"] 684 685 def getMetadata(self): 686 687 """ 688 Return a dictionary containing items describing the page's "created" 689 time, "last-modified" time, "sequence" (or revision number) and the 690 "last-comment" made about the last edit. 691 """ 692 693 request = self.page.request 694 695 # Get the initial revision of the page. 696 697 revisions = self.getRevisions() 698 event_page_initial = Page(request, self.getPageName(), rev=revisions[-1]) 699 700 # Get the created and last modified times. 701 702 initial_revision = getPageRevision(event_page_initial) 703 704 if self.metadata is None: 705 self.metadata = {} 706 self.metadata["created"] = initial_revision["timestamp"] 707 latest_revision = self.getPageRevision() 708 self.metadata["last-modified"] = latest_revision["timestamp"] 709 self.metadata["sequence"] = len(revisions) - 1 710 self.metadata["last-comment"] = latest_revision["comment"] 711 712 return self.metadata 713 714 def getRevisions(self): 715 716 "Return a list of page revisions." 717 718 return self.page.getRevList() 719 720 def getPageRevision(self): 721 722 "Return the revision details dictionary for this page." 723 724 return getPageRevision(self.page) 725 726 def getPageName(self): 727 728 "Return the page name." 729 730 return self.page.page_name 731 732 def getPrettyPageName(self): 733 734 "Return a nicely formatted title/name for this page." 735 736 return getPrettyPageName(self.page) 737 738 def getBody(self): 739 740 "Get the current page body." 741 742 if self.body is None: 743 self.body = self.page.get_raw_body() 744 return self.body 745 746 def getEvents(self): 747 748 "Return a list of events from this page." 749 750 if self.events is None: 751 details = {} 752 self.events = [Event(self, details)] 753 754 if self.getFormat() == "wiki": 755 for match in definition_list_regexp.finditer(self.getBody()): 756 757 # Skip commented-out items. 758 759 if match.group("optcomment"): 760 continue 761 762 # Permit case-insensitive list terms. 763 764 term = match.group("term").lower() 765 desc = match.group("desc") 766 767 # Special value type handling. 768 769 # Dates. 770 771 if term in ("start", "end"): 772 desc = getDateTime(desc) 773 774 # Lists (whose elements may be quoted). 775 776 elif term in ("topics", "categories"): 777 desc = map(getSimpleWikiText, to_list(desc, ",")) 778 779 # Position details. 780 781 elif term == "geo": 782 try: 783 desc = map(getMapReference, to_list(desc, None)) 784 except (KeyError, ValueError): 785 continue 786 787 # Labels which may well be quoted. 788 789 elif term in ("title", "summary", "description", "location"): 790 desc = getSimpleWikiText(desc.strip()) 791 792 if desc is not None: 793 794 # Handle apparent duplicates by creating a new set of 795 # details. 796 797 if details.has_key(term): 798 799 # Make a new event. 800 801 details = {} 802 self.events.append(Event(self, details)) 803 804 details[term] = desc 805 806 return self.events 807 808 def setEvents(self, events): 809 810 "Set the given 'events' on this page." 811 812 self.events = events 813 814 def getCategoryMembership(self): 815 816 "Get the category names from this page." 817 818 if self.categories is None: 819 body = self.getBody() 820 match = category_membership_regexp.search(body) 821 self.categories = match and [x for x in match.groups() if x] or [] 822 823 return self.categories 824 825 def setCategoryMembership(self, category_names): 826 827 """ 828 Set the category membership for the page using the specified 829 'category_names'. 830 """ 831 832 self.categories = category_names 833 834 def flushEventDetails(self): 835 836 "Flush the current event details to this page's body text." 837 838 new_body_parts = [] 839 end_of_last_match = 0 840 body = self.getBody() 841 842 events = iter(self.getEvents()) 843 844 event = events.next() 845 event_details = event.getDetails() 846 replaced_terms = set() 847 848 for match in definition_list_regexp.finditer(body): 849 850 # Permit case-insensitive list terms. 851 852 term = match.group("term").lower() 853 desc = match.group("desc") 854 855 # Check that the term has not already been substituted. If so, 856 # get the next event. 857 858 if term in replaced_terms: 859 try: 860 event = events.next() 861 862 # No more events. 863 864 except StopIteration: 865 break 866 867 event_details = event.getDetails() 868 replaced_terms = set() 869 870 # Add preceding text to the new body. 871 872 new_body_parts.append(body[end_of_last_match:match.start()]) 873 874 # Get the matching regions, adding the term to the new body. 875 876 new_body_parts.append(match.group("wholeterm")) 877 878 # Special value type handling. 879 880 if event_details.has_key(term): 881 882 # Dates. 883 884 if term in ("start", "end"): 885 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 886 887 # Lists (whose elements may be quoted). 888 889 elif term in ("topics", "categories"): 890 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 891 892 # Labels which must be quoted. 893 894 elif term in ("title", "summary"): 895 desc = getEncodedWikiText(event_details[term]) 896 897 # Position details. 898 899 elif term == "geo": 900 desc = " ".join(map(str, event_details[term])) 901 902 # Text which need not be quoted, but it will be Wiki text. 903 904 elif term in ("description", "link", "location"): 905 desc = event_details[term] 906 907 replaced_terms.add(term) 908 909 # Add the replaced value. 910 911 new_body_parts.append(desc) 912 913 # Remember where in the page has been processed. 914 915 end_of_last_match = match.end() 916 917 # Write the rest of the page. 918 919 new_body_parts.append(body[end_of_last_match:]) 920 921 self.body = "".join(new_body_parts) 922 923 def flushCategoryMembership(self): 924 925 "Flush the category membership to the page body." 926 927 body = self.getBody() 928 category_names = self.getCategoryMembership() 929 match = category_membership_regexp.search(body) 930 931 if match: 932 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 933 934 def saveChanges(self): 935 936 "Save changes to the event." 937 938 self.flushEventDetails() 939 self.flushCategoryMembership() 940 self.page.saveText(self.getBody(), 0) 941 942 def linkToPage(self, request, text, query_string=None): 943 944 """ 945 Using 'request', return a link to this page with the given link 'text' 946 and optional 'query_string'. 947 """ 948 949 return linkToPage(request, self.page, text, query_string) 950 951 # Formatting-related functions. 952 953 def getParserClass(self, request, format): 954 955 """ 956 Return a parser class using the 'request' for the given 'format', returning 957 a plain text parser if no parser can be found for the specified 'format'. 958 """ 959 960 try: 961 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 962 except wikiutil.PluginMissingError: 963 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 964 965 def formatText(self, text, request, fmt): 966 967 """ 968 Format the given 'text' using the specified 'request' and formatter 969 'fmt'. 970 """ 971 972 fmt.page = self.page 973 974 # Suppress line anchors. 975 976 parser_cls = self.getParserClass(request, self.getFormat()) 977 parser = parser_cls(text, request, line_anchors=False) 978 979 # Fix lists by indicating that a paragraph is already started. 980 981 return request.redirectedOutput(parser.format, fmt, inhibit_p=True) 982 983 # Event details. 984 985 class Event(ActsAsTimespan): 986 987 "A description of an event." 988 989 def __init__(self, page, details): 990 self.page = page 991 self.details = details 992 993 # Permit omission of the end of the event by duplicating the start. 994 995 if self.details.has_key("start") and not self.details.has_key("end"): 996 end = self.details["start"] 997 998 # Make any end time refer to the day instead. 999 1000 if isinstance(end, DateTime): 1001 end = end.as_date() 1002 1003 self.details["end"] = end 1004 1005 def __repr__(self): 1006 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 1007 1008 def __hash__(self): 1009 1010 """ 1011 Return a dictionary hash, avoiding mistaken equality of events in some 1012 situations (notably membership tests) by including the URL as well as 1013 the summary. 1014 """ 1015 1016 return hash(self.getSummary() + self.getEventURL()) 1017 1018 def getPage(self): 1019 1020 "Return the page describing this event." 1021 1022 return self.page 1023 1024 def setPage(self, page): 1025 1026 "Set the 'page' describing this event." 1027 1028 self.page = page 1029 1030 def getEventURL(self): 1031 1032 "Return the URL of this event." 1033 1034 return self.page.getPageURL() 1035 1036 def linkToEvent(self, request, text, query_string=None): 1037 1038 """ 1039 Using 'request', return a link to this event with the given link 'text' 1040 and optional 'query_string'. 1041 """ 1042 1043 return self.page.linkToPage(request, text, query_string) 1044 1045 def getMetadata(self): 1046 1047 """ 1048 Return a dictionary containing items describing the event's "created" 1049 time, "last-modified" time, "sequence" (or revision number) and the 1050 "last-comment" made about the last edit. 1051 """ 1052 1053 # Delegate this to the page. 1054 1055 return self.page.getMetadata() 1056 1057 def getSummary(self, event_parent=None): 1058 1059 """ 1060 Return either the given title or summary of the event according to the 1061 event details, or a summary made from using the pretty version of the 1062 page name. 1063 1064 If the optional 'event_parent' is specified, any page beneath the given 1065 'event_parent' page in the page hierarchy will omit this parent information 1066 if its name is used as the summary. 1067 """ 1068 1069 event_details = self.details 1070 1071 if event_details.has_key("title"): 1072 return event_details["title"] 1073 elif event_details.has_key("summary"): 1074 return event_details["summary"] 1075 else: 1076 # If appropriate, remove the parent details and "/" character. 1077 1078 title = self.page.getPageName() 1079 1080 if event_parent and title.startswith(event_parent): 1081 title = title[len(event_parent.rstrip("/")) + 1:] 1082 1083 return getPrettyTitle(title) 1084 1085 def getDetails(self): 1086 1087 "Return the details for this event." 1088 1089 return self.details 1090 1091 def setDetails(self, event_details): 1092 1093 "Set the 'event_details' for this event." 1094 1095 self.details = event_details 1096 1097 # Timespan-related methods. 1098 1099 def __contains__(self, other): 1100 return self == other 1101 1102 def __eq__(self, other): 1103 if isinstance(other, Event): 1104 return self.getSummary() == other.getSummary() and self.getEventURL() == other.getEventURL() and self._cmp(other) 1105 else: 1106 return self._cmp(other) == 0 1107 1108 def __ne__(self, other): 1109 return not self.__eq__(other) 1110 1111 def __lt__(self, other): 1112 return self._cmp(other) == -1 1113 1114 def __le__(self, other): 1115 return self._cmp(other) in (-1, 0) 1116 1117 def __gt__(self, other): 1118 return self._cmp(other) == 1 1119 1120 def __ge__(self, other): 1121 return self._cmp(other) in (0, 1) 1122 1123 def _cmp(self, other): 1124 1125 "Compare this event to an 'other' event purely by their timespans." 1126 1127 if isinstance(other, Event): 1128 return cmp(self.as_timespan(), other.as_timespan()) 1129 else: 1130 return cmp(self.as_timespan(), other) 1131 1132 def as_timespan(self): 1133 details = self.details 1134 if details.has_key("start") and details.has_key("end"): 1135 return Timespan(details["start"], details["end"]) 1136 else: 1137 return None 1138 1139 def as_limits(self): 1140 ts = self.as_timespan() 1141 return ts and ts.as_limits() 1142 1143 class CalendarEvent(Event): 1144 1145 "An event from a remote calendar." 1146 1147 def getEventURL(self): 1148 1149 "Return the URL of this event." 1150 1151 return self.details.get("url") or self.page.getPageURL() 1152 1153 def linkToEvent(self, request, text, query_string=None): 1154 1155 """ 1156 Using 'request', return a link to this event with the given link 'text' 1157 and optional 'query_string'. 1158 """ 1159 1160 return linkToResource(self.getEventURL(), request, text, query_string) 1161 1162 def getMetadata(self): 1163 1164 """ 1165 Return a dictionary containing items describing the event's "created" 1166 time, "last-modified" time, "sequence" (or revision number) and the 1167 "last-comment" made about the last edit. 1168 """ 1169 1170 return { 1171 "created" : self.details.get("created") or self.details["dtstamp"], 1172 "last-modified" : self.details.get("last-modified") or self.details["dtstamp"], 1173 "sequence" : self.details.get("sequence") or 0, 1174 "last-comment" : "" 1175 } 1176 1177 # Obtaining event containers and events from such containers. 1178 1179 def getEventPages(pages): 1180 1181 "Return a list of events found on the given 'pages'." 1182 1183 # Get real pages instead of result pages. 1184 1185 return map(EventPage, pages) 1186 1187 def getAllEventSources(request): 1188 1189 "Return all event sources defined in the Wiki using the 'request'." 1190 1191 sources_page = getattr(request.cfg, "event_aggregator_sources_page", "EventSourcesDict") 1192 1193 # Remote sources are accessed via dictionary page definitions. 1194 1195 return getWikiDict(sources_page, request) 1196 1197 def getEventResources(sources, calendar_start, calendar_end, request): 1198 1199 """ 1200 Return resource objects for the given 'sources' using the given 1201 'calendar_start' and 'calendar_end' to parameterise requests to the sources, 1202 and the 'request' to access configuration settings in the Wiki. 1203 """ 1204 1205 sources_dict = getAllEventSources(request) 1206 if not sources_dict: 1207 return [] 1208 1209 # Use dates for the calendar limits. 1210 1211 if isinstance(calendar_start, Date): 1212 pass 1213 elif isinstance(calendar_start, Month): 1214 calendar_start = calendar_start.as_date(1) 1215 1216 if isinstance(calendar_end, Date): 1217 pass 1218 elif isinstance(calendar_end, Month): 1219 calendar_end = calendar_end.as_date(-1) 1220 1221 resources = [] 1222 1223 for source in sources: 1224 try: 1225 url, format = sources_dict[source].split() 1226 except (KeyError, ValueError): 1227 pass 1228 else: 1229 # Prevent local file access. 1230 1231 if url.startswith("file:"): 1232 continue 1233 1234 # Parameterise the URL. 1235 # Where other parameters are used, care must be taken to encode them 1236 # properly. 1237 1238 url = url.replace("{start}", urllib.quote_plus(calendar_start and str(calendar_start) or "")) 1239 url = url.replace("{end}", urllib.quote_plus(calendar_end and str(calendar_end) or "")) 1240 1241 # Get a parser. 1242 1243 if format == "ical" and vCalendar is not None: 1244 parser = vCalendar.parse 1245 resource_cls = EventCalendar 1246 else: 1247 continue 1248 1249 # See if the URL is cached. 1250 1251 cache_key = cache.key(request, content=url) 1252 cache_entry = caching.CacheEntry(request, "EventAggregator", cache_key, scope='wiki') 1253 1254 # If no entry exists, or if the entry is older than a certain age 1255 # (5 minutes by default), create one with the response from the URL. 1256 1257 now = time.time() 1258 mtime = cache_entry.mtime() 1259 max_cache_age = int(getattr(request.cfg, "event_aggregator_max_cache_age", "300")) 1260 1261 # NOTE: The URL could be checked and the 'If-Modified-Since' header 1262 # NOTE: (see MoinMoin.action.pollsistersites) could be checked. 1263 1264 if not cache_entry.exists() or now - mtime >= max_cache_age: 1265 1266 # Access the remote data source. 1267 1268 cache_entry.open(mode="w") 1269 f = urllib.urlopen(url) 1270 try: 1271 cache_entry.write(url + "\n") 1272 cache_entry.write((f.headers.get("content-type") or "") + "\n") 1273 cache_entry.write(f.read()) 1274 finally: 1275 cache_entry.close() 1276 f.close() 1277 1278 # Open the cache entry and read it. 1279 1280 cache_entry.open() 1281 try: 1282 data = cache_entry.read() 1283 finally: 1284 cache_entry.close() 1285 1286 # Process the entry, parsing the content. 1287 1288 f = StringIO(data) 1289 try: 1290 url = f.readline() 1291 encoding = getContentEncoding(f.readline()) 1292 uf = codecs.getreader(encoding or "utf-8")(f) 1293 try: 1294 resources.append(resource_cls(url, parser(uf))) 1295 finally: 1296 uf.close() 1297 finally: 1298 f.close() 1299 1300 return resources 1301 1302 def getEventsFromResources(resources): 1303 1304 "Return a list of events supplied by the given event 'resources'." 1305 1306 events = [] 1307 1308 for resource in resources: 1309 1310 # Get all events described by the resource. 1311 1312 for event in resource.getEvents(): 1313 1314 # Remember the event. 1315 1316 events.append(event) 1317 1318 return events 1319 1320 # Event filtering and limits. 1321 1322 def getEventsInPeriod(events, calendar_period): 1323 1324 """ 1325 Return a collection containing those of the given 'events' which occur 1326 within the given 'calendar_period'. 1327 """ 1328 1329 all_shown_events = [] 1330 1331 for event in events: 1332 1333 # Test for the suitability of the event. 1334 1335 if event.as_timespan() is not None: 1336 1337 # Compare the dates to the requested calendar window, if any. 1338 1339 if event in calendar_period: 1340 all_shown_events.append(event) 1341 1342 return all_shown_events 1343 1344 def getEventLimits(events): 1345 1346 "Return the earliest and latest of the given 'events'." 1347 1348 earliest = None 1349 latest = None 1350 1351 for event in events: 1352 1353 # Test for the suitability of the event. 1354 1355 if event.as_timespan() is not None: 1356 ts = event.as_timespan() 1357 if earliest is None or ts.start < earliest: 1358 earliest = ts.start 1359 if latest is None or ts.end > latest: 1360 latest = ts.end 1361 1362 return earliest, latest 1363 1364 def setEventTimestamps(request, events): 1365 1366 """ 1367 Using 'request', set timestamp details in the details dictionary of each of 1368 the 'events'. 1369 1370 Return the latest timestamp found. 1371 """ 1372 1373 latest = None 1374 1375 for event in events: 1376 event_details = event.getDetails() 1377 1378 # Populate the details with event metadata. 1379 1380 event_details.update(event.getMetadata()) 1381 1382 if latest is None or latest < event_details["last-modified"]: 1383 latest = event_details["last-modified"] 1384 1385 return latest 1386 1387 def getOrderedEvents(events): 1388 1389 """ 1390 Return a list with the given 'events' ordered according to their start and 1391 end dates. 1392 """ 1393 1394 ordered_events = events[:] 1395 ordered_events.sort() 1396 return ordered_events 1397 1398 def getCalendarPeriod(calendar_start, calendar_end): 1399 1400 """ 1401 Return a calendar period for the given 'calendar_start' and 'calendar_end'. 1402 These parameters can be given as None. 1403 """ 1404 1405 # Re-order the window, if appropriate. 1406 1407 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 1408 calendar_start, calendar_end = calendar_end, calendar_start 1409 1410 return Timespan(calendar_start, calendar_end) 1411 1412 def getConcretePeriod(calendar_start, calendar_end, earliest, latest, resolution): 1413 1414 """ 1415 From the requested 'calendar_start' and 'calendar_end', which may be None, 1416 indicating that no restriction is imposed on the period for each of the 1417 boundaries, use the 'earliest' and 'latest' event months to define a 1418 specific period of interest. 1419 """ 1420 1421 # Define the period as starting with any specified start month or the 1422 # earliest event known, ending with any specified end month or the latest 1423 # event known. 1424 1425 first = calendar_start or earliest 1426 last = calendar_end or latest 1427 1428 # If there is no range of months to show, perhaps because there are no 1429 # events in the requested period, and there was no start or end month 1430 # specified, show only the month indicated by the start or end of the 1431 # requested period. If all events were to be shown but none were found show 1432 # the current month. 1433 1434 if resolution == "date": 1435 get_current = getCurrentDate 1436 else: 1437 get_current = getCurrentMonth 1438 1439 if first is None: 1440 first = last or get_current() 1441 if last is None: 1442 last = first or get_current() 1443 1444 if resolution == "month": 1445 first = first.as_month() 1446 last = last.as_month() 1447 1448 # Permit "expiring" periods (where the start date approaches the end date). 1449 1450 return min(first, last), last 1451 1452 def getCoverage(events, resolution="date"): 1453 1454 """ 1455 Determine the coverage of the given 'events', returning a collection of 1456 timespans, along with a dictionary mapping locations to collections of 1457 slots, where each slot contains a tuple of the form (timespans, events). 1458 """ 1459 1460 all_events = {} 1461 full_coverage = TimespanCollection(resolution) 1462 1463 # Get event details. 1464 1465 for event in events: 1466 event_details = event.getDetails() 1467 1468 # Find the coverage of this period for the event. 1469 1470 # For day views, each location has its own slot, but for month 1471 # views, all locations are pooled together since having separate 1472 # slots for each location can lead to poor usage of vertical space. 1473 1474 if resolution == "datetime": 1475 event_location = event_details.get("location") 1476 else: 1477 event_location = None 1478 1479 # Update the overall coverage. 1480 1481 full_coverage.insert_in_order(event) 1482 1483 # Add a new events list for a new location. 1484 # Locations can be unspecified, thus None refers to all unlocalised 1485 # events. 1486 1487 if not all_events.has_key(event_location): 1488 all_events[event_location] = [TimespanCollection(resolution, [event])] 1489 1490 # Try and fit the event into an events list. 1491 1492 else: 1493 slot = all_events[event_location] 1494 1495 for slot_events in slot: 1496 1497 # Where the event does not overlap with the events in the 1498 # current collection, add it alongside these events. 1499 1500 if not event in slot_events: 1501 slot_events.insert_in_order(event) 1502 break 1503 1504 # Make a new element in the list if the event cannot be 1505 # marked alongside existing events. 1506 1507 else: 1508 slot.append(TimespanCollection(resolution, [event])) 1509 1510 return full_coverage, all_events 1511 1512 def getCoverageScale(coverage): 1513 1514 """ 1515 Return a scale for the given coverage so that the times involved are 1516 exposed. The scale consists of a list of non-overlapping timespans forming 1517 a contiguous period of time. 1518 """ 1519 1520 times = set() 1521 for timespan in coverage: 1522 start, end = timespan.as_limits() 1523 1524 # Add either genuine times or dates converted to times. 1525 1526 if isinstance(start, DateTime): 1527 times.add(start) 1528 else: 1529 times.add(start.as_start_of_day()) 1530 1531 if isinstance(end, DateTime): 1532 times.add(end) 1533 else: 1534 times.add(end.as_date().next_day()) 1535 1536 times = list(times) 1537 times.sort(cmp_dates_as_day_start) 1538 1539 scale = [] 1540 first = 1 1541 start = None 1542 for time in times: 1543 if not first: 1544 scale.append(Timespan(start, time)) 1545 else: 1546 first = 0 1547 start = time 1548 1549 return scale 1550 1551 # Date-related functions. 1552 1553 def cmp_dates_as_day_start(a, b): 1554 1555 """ 1556 Compare dates/datetimes 'a' and 'b' treating dates without time information 1557 as the earliest time in a particular day. 1558 """ 1559 1560 are_equal = a == b 1561 1562 if are_equal: 1563 a2 = a.as_datetime_or_date() 1564 b2 = b.as_datetime_or_date() 1565 1566 if isinstance(a2, Date) and isinstance(b2, DateTime): 1567 return -1 1568 elif isinstance(a2, DateTime) and isinstance(b2, Date): 1569 return 1 1570 1571 return cmp(a, b) 1572 1573 class Convertible: 1574 1575 "Support for converting temporal objects." 1576 1577 def _get_converter(self, resolution): 1578 if resolution == "month": 1579 return lambda x: x and x.as_month() 1580 elif resolution == "date": 1581 return lambda x: x and x.as_date() 1582 elif resolution == "datetime": 1583 return lambda x: x and x.as_datetime_or_date() 1584 else: 1585 return lambda x: x 1586 1587 class Temporal(Convertible): 1588 1589 "A simple temporal representation, common to dates and times." 1590 1591 def __init__(self, data): 1592 self.data = list(data) 1593 1594 def __repr__(self): 1595 return "%s(%r)" % (self.__class__.__name__, self.data) 1596 1597 def __hash__(self): 1598 return hash(self.as_tuple()) 1599 1600 def as_tuple(self): 1601 return tuple(self.data) 1602 1603 def convert(self, resolution): 1604 return self._get_converter(resolution)(self) 1605 1606 def __cmp__(self, other): 1607 1608 """ 1609 The result of comparing this instance with 'other' is derived from a 1610 comparison of the instances' date(time) data at the highest common 1611 resolution, meaning that if a date is compared to a datetime, the 1612 datetime will be considered as a date. Thus, a date and a datetime 1613 referring to the same date will be considered equal. 1614 """ 1615 1616 if not isinstance(other, Temporal): 1617 return NotImplemented 1618 else: 1619 data = self.as_tuple() 1620 other_data = other.as_tuple() 1621 length = min(len(data), len(other_data)) 1622 return cmp(data[:length], other_data[:length]) 1623 1624 def __sub__(self, other): 1625 1626 """ 1627 Return the difference between this object and the 'other' object at the 1628 highest common accuracy of both objects. 1629 """ 1630 1631 if not isinstance(other, Temporal): 1632 return NotImplemented 1633 else: 1634 data = self.as_tuple() 1635 other_data = other.as_tuple() 1636 if len(data) < len(other_data): 1637 return len(self.until(other)) 1638 else: 1639 return len(other.until(self)) 1640 1641 def _until(self, start, end, nextfn, prevfn): 1642 1643 """ 1644 Return a collection of units of time by starting from the given 'start' 1645 and stepping across intervening units until 'end' is reached, using the 1646 given 'nextfn' and 'prevfn' to step from one unit to the next. 1647 """ 1648 1649 current = start 1650 units = [current] 1651 if current < end: 1652 while current < end: 1653 current = nextfn(current) 1654 units.append(current) 1655 elif current > end: 1656 while current > end: 1657 current = prevfn(current) 1658 units.append(current) 1659 return units 1660 1661 def ambiguous(self): 1662 1663 "Only times can be ambiguous." 1664 1665 return 0 1666 1667 class Month(Temporal): 1668 1669 "A simple year-month representation." 1670 1671 def __str__(self): 1672 return "%04d-%02d" % self.as_tuple()[:2] 1673 1674 def as_datetime(self, day, hour, minute, second, zone): 1675 return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) 1676 1677 def as_date(self, day): 1678 if day < 0: 1679 weekday, ndays = self.month_properties() 1680 day = ndays + 1 + day 1681 return Date(self.as_tuple() + (day,)) 1682 1683 def as_month(self): 1684 return self 1685 1686 def year(self): 1687 return self.data[0] 1688 1689 def month(self): 1690 return self.data[1] 1691 1692 def month_properties(self): 1693 1694 """ 1695 Return the weekday of the 1st of the month, along with the number of 1696 days, as a tuple. 1697 """ 1698 1699 year, month = self.as_tuple()[:2] 1700 return calendar.monthrange(year, month) 1701 1702 def month_update(self, n=1): 1703 1704 "Return the month updated by 'n' months." 1705 1706 year, month = self.as_tuple()[:2] 1707 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1)) 1708 1709 update = month_update 1710 1711 def next_month(self): 1712 1713 "Return the month following this one." 1714 1715 return self.month_update(1) 1716 1717 next = next_month 1718 1719 def previous_month(self): 1720 1721 "Return the month preceding this one." 1722 1723 return self.month_update(-1) 1724 1725 previous = previous_month 1726 1727 def months_until(self, end): 1728 1729 "Return the collection of months from this month until 'end'." 1730 1731 return self._until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month) 1732 1733 until = months_until 1734 1735 class Date(Month): 1736 1737 "A simple year-month-day representation." 1738 1739 def constrain(self): 1740 year, month, day = self.as_tuple()[:3] 1741 1742 month = max(min(month, 12), 1) 1743 wd, last_day = calendar.monthrange(year, month) 1744 day = max(min(day, last_day), 1) 1745 1746 self.data[1:3] = month, day 1747 1748 def __str__(self): 1749 return "%04d-%02d-%02d" % self.as_tuple()[:3] 1750 1751 def as_datetime(self, hour, minute, second, zone): 1752 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 1753 1754 def as_start_of_day(self): 1755 return self.as_datetime(None, None, None, None) 1756 1757 def as_date(self): 1758 return self 1759 1760 def as_datetime_or_date(self): 1761 return self 1762 1763 def as_month(self): 1764 return Month(self.data[:2]) 1765 1766 def day(self): 1767 return self.data[2] 1768 1769 def day_update(self, n=1): 1770 1771 "Return the month updated by 'n' days." 1772 1773 delta = datetime.timedelta(n) 1774 dt = datetime.date(*self.as_tuple()[:3]) 1775 dt_new = dt + delta 1776 return Date((dt_new.year, dt_new.month, dt_new.day)) 1777 1778 update = day_update 1779 1780 def next_day(self): 1781 1782 "Return the date following this one." 1783 1784 year, month, day = self.as_tuple()[:3] 1785 _wd, end_day = calendar.monthrange(year, month) 1786 if day == end_day: 1787 if month == 12: 1788 return Date((year + 1, 1, 1)) 1789 else: 1790 return Date((year, month + 1, 1)) 1791 else: 1792 return Date((year, month, day + 1)) 1793 1794 next = next_day 1795 1796 def previous_day(self): 1797 1798 "Return the date preceding this one." 1799 1800 year, month, day = self.as_tuple()[:3] 1801 if day == 1: 1802 if month == 1: 1803 return Date((year - 1, 12, 31)) 1804 else: 1805 _wd, end_day = calendar.monthrange(year, month - 1) 1806 return Date((year, month - 1, end_day)) 1807 else: 1808 return Date((year, month, day - 1)) 1809 1810 previous = previous_day 1811 1812 def days_until(self, end): 1813 1814 "Return the collection of days from this date until 'end'." 1815 1816 return self._until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 1817 1818 until = days_until 1819 1820 class DateTime(Date): 1821 1822 "A simple date plus time representation." 1823 1824 def constrain(self): 1825 Date.constrain(self) 1826 1827 hour, minute, second = self.as_tuple()[3:6] 1828 1829 if self.has_time(): 1830 hour = max(min(hour, 23), 0) 1831 minute = max(min(minute, 59), 0) 1832 1833 if second is not None: 1834 second = max(min(second, 60), 0) # support leap seconds 1835 1836 self.data[3:6] = hour, minute, second 1837 1838 def __str__(self): 1839 return Date.__str__(self) + self.time_string() 1840 1841 def time_string(self): 1842 if self.has_time(): 1843 data = self.as_tuple() 1844 time_str = " %02d:%02d" % data[3:5] 1845 if data[5] is not None: 1846 time_str += ":%02d" % data[5] 1847 if data[6] is not None: 1848 time_str += " %s" % data[6] 1849 return time_str 1850 else: 1851 return "" 1852 1853 def as_HTTP_datetime_string(self): 1854 weekday = calendar.weekday(*self.data[:3]) 1855 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (( 1856 getDayLabel(weekday), 1857 self.data[2], 1858 getMonthLabel(self.data[1]), 1859 self.data[0] 1860 ) + tuple(self.data[3:6])) 1861 1862 def as_datetime(self): 1863 return self 1864 1865 def as_date(self): 1866 return Date(self.data[:3]) 1867 1868 def as_datetime_or_date(self): 1869 1870 """ 1871 Return a date for this datetime if fields are missing. Otherwise, return 1872 this datetime itself. 1873 """ 1874 1875 if not self.has_time(): 1876 return self.as_date() 1877 else: 1878 return self 1879 1880 def __cmp__(self, other): 1881 1882 """ 1883 The result of comparing this instance with 'other' is, if both instances 1884 are datetime instances, derived from a comparison of the datetimes 1885 converted to UTC. If one or both datetimes cannot be converted to UTC, 1886 the datetimes are compared using the basic temporal comparison which 1887 compares their raw time data. 1888 """ 1889 1890 this = self 1891 1892 if this.has_time(): 1893 if isinstance(other, DateTime): 1894 if other.has_time(): 1895 this_utc = this.to_utc() 1896 other_utc = other.to_utc() 1897 if this_utc is not None and other_utc is not None: 1898 return cmp(this_utc.as_tuple(), other_utc.as_tuple()) 1899 else: 1900 other = other.padded() 1901 else: 1902 this = this.padded() 1903 1904 return Date.__cmp__(this, other) 1905 1906 def has_time(self): 1907 1908 """ 1909 Return whether this object has any time information. Objects without 1910 time information can refer to the very start of a day. 1911 """ 1912 1913 return self.data[3] is not None and self.data[4] is not None 1914 1915 def time(self): 1916 return self.data[3:] 1917 1918 def seconds(self): 1919 return self.data[5] 1920 1921 def time_zone(self): 1922 return self.data[6] 1923 1924 def set_time_zone(self, value): 1925 self.data[6] = value 1926 1927 def padded(self, empty_value=0): 1928 1929 """ 1930 Return a datetime with missing fields defined as being the given 1931 'empty_value' or 0 if not specified. 1932 """ 1933 1934 data = [] 1935 for x in self.data[:6]: 1936 if x is None: 1937 data.append(empty_value) 1938 else: 1939 data.append(x) 1940 1941 data += self.data[6:] 1942 return DateTime(data) 1943 1944 def to_utc(self): 1945 1946 """ 1947 Return this object converted to UTC, or None if such a conversion is not 1948 defined. 1949 """ 1950 1951 if not self.has_time(): 1952 return None 1953 1954 offset = self.utc_offset() 1955 if offset: 1956 hours, minutes = offset 1957 1958 # Invert the offset to get the correction. 1959 1960 hours, minutes = -hours, -minutes 1961 1962 # Get the components. 1963 1964 hour, minute, second, zone = self.time() 1965 date = self.as_date() 1966 1967 # Add the minutes and hours. 1968 1969 minute += minutes 1970 if minute < 0 or minute > 59: 1971 hour += minute / 60 1972 minute = minute % 60 1973 1974 # NOTE: This makes various assumptions and probably would not work 1975 # NOTE: for general arithmetic. 1976 1977 hour += hours 1978 if hour < 0: 1979 date = date.previous_day() 1980 hour += 24 1981 elif hour > 23: 1982 date = date.next_day() 1983 hour -= 24 1984 1985 return date.as_datetime(hour, minute, second, "UTC") 1986 1987 # Cannot convert. 1988 1989 else: 1990 return None 1991 1992 def utc_offset(self): 1993 1994 "Return the UTC offset in hours and minutes." 1995 1996 zone = self.time_zone() 1997 if not zone: 1998 return None 1999 2000 # Support explicit UTC zones. 2001 2002 if zone == "UTC": 2003 return 0, 0 2004 2005 # Attempt to return a UTC offset where an explicit offset has been set. 2006 2007 match = timezone_offset_regexp.match(zone) 2008 if match: 2009 if match.group("sign") == "-": 2010 sign = -1 2011 else: 2012 sign = 1 2013 2014 hours = int(match.group("hours")) * sign 2015 minutes = int(match.group("minutes") or 0) * sign 2016 return hours, minutes 2017 2018 # Attempt to handle Olson time zone identifiers. 2019 2020 dt = self.as_olson_datetime() 2021 if dt: 2022 seconds = dt.utcoffset().seconds 2023 hours = seconds / 3600 2024 minutes = (seconds % 3600) / 60 2025 return hours, minutes 2026 2027 # Otherwise return None. 2028 2029 return None 2030 2031 def olson_identifier(self): 2032 2033 "Return the Olson identifier from any zone information." 2034 2035 zone = self.time_zone() 2036 if not zone: 2037 return None 2038 2039 # Attempt to match an identifier. 2040 2041 match = timezone_olson_regexp.match(zone) 2042 if match: 2043 return match.group("olson") 2044 else: 2045 return None 2046 2047 def _as_olson_datetime(self, hours=None): 2048 2049 """ 2050 Return a Python datetime object for this datetime interpreted using any 2051 Olson time zone identifier and the given 'hours' offset, raising one of 2052 the pytz exceptions in case of ambiguity. 2053 """ 2054 2055 olson = self.olson_identifier() 2056 if olson and pytz: 2057 tz = pytz.timezone(olson) 2058 data = self.padded().as_tuple()[:6] 2059 dt = datetime.datetime(*data) 2060 2061 # With an hours offset, find a time probably in a previously 2062 # applicable time zone. 2063 2064 if hours is not None: 2065 td = datetime.timedelta(0, hours * 3600) 2066 dt += td 2067 2068 ldt = tz.localize(dt, None) 2069 2070 # With an hours offset, adjust the time to define it within the 2071 # previously applicable time zone but at the presumably intended 2072 # position. 2073 2074 if hours is not None: 2075 ldt -= td 2076 2077 return ldt 2078 else: 2079 return None 2080 2081 def as_olson_datetime(self): 2082 2083 """ 2084 Return a Python datetime object for this datetime interpreted using any 2085 Olson time zone identifier, choosing the time from the zone before the 2086 period of ambiguity. 2087 """ 2088 2089 try: 2090 return self._as_olson_datetime() 2091 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 2092 2093 # Try again, using an earlier local time and then stepping forward 2094 # in the chosen zone. 2095 # NOTE: Four hours earlier seems reasonable. 2096 2097 return self._as_olson_datetime(-4) 2098 2099 def ambiguous(self): 2100 2101 "Return whether the time is local and ambiguous." 2102 2103 try: 2104 self._as_olson_datetime() 2105 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 2106 return 1 2107 2108 return 0 2109 2110 class Timespan(ActsAsTimespan, Convertible): 2111 2112 """ 2113 A period of time which can be compared against others to check for overlaps. 2114 """ 2115 2116 def __init__(self, start, end): 2117 self.start = start 2118 self.end = end 2119 2120 # NOTE: Should perhaps catch ambiguous time problems elsewhere. 2121 2122 if self.ambiguous() and self.start is not None and self.end is not None and start > end: 2123 self.start, self.end = end, start 2124 2125 def __repr__(self): 2126 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 2127 2128 def __hash__(self): 2129 return hash((self.start, self.end)) 2130 2131 def as_timespan(self): 2132 return self 2133 2134 def as_limits(self): 2135 return self.start, self.end 2136 2137 def ambiguous(self): 2138 return self.start is not None and self.start.ambiguous() or self.end is not None and self.end.ambiguous() 2139 2140 def convert(self, resolution): 2141 return Timespan(*map(self._get_converter(resolution), self.as_limits())) 2142 2143 def is_before(self, a, b): 2144 2145 """ 2146 Return whether 'a' is before 'b'. Since the end datetime of one period 2147 may be the same as the start datetime of another period, and yet the 2148 first period is intended to be concluded by the end datetime and not 2149 overlap with the other period, a different test is employed for datetime 2150 comparisons. 2151 """ 2152 2153 # Datetimes without times can be equal to dates and be considered as 2154 # occurring before those dates. Generally, datetimes should not be 2155 # produced without time information as getDateTime converts such 2156 # datetimes to dates. 2157 2158 if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()): 2159 return a <= b 2160 else: 2161 return a < b 2162 2163 def __contains__(self, other): 2164 2165 """ 2166 This instance is considered to contain 'other' if one is not before or 2167 after the other. If this instance overlaps or coincides with 'other', 2168 then 'other' is regarded as belonging to this instance's time period. 2169 """ 2170 2171 return self == other 2172 2173 def __cmp__(self, other): 2174 2175 """ 2176 Return whether this timespan occupies the same period of time as the 2177 'other'. Timespans are considered less than others if their end points 2178 precede the other's start point, and are considered greater than others 2179 if their start points follow the other's end point. 2180 """ 2181 2182 if isinstance(other, ActsAsTimespan): 2183 other = other.as_timespan() 2184 2185 if self.end is not None and other.start is not None and self.is_before(self.end, other.start): 2186 return -1 2187 elif self.start is not None and other.end is not None and self.is_before(other.end, self.start): 2188 return 1 2189 else: 2190 return 0 2191 2192 else: 2193 if self.end is not None and self.is_before(self.end, other): 2194 return -1 2195 elif self.start is not None and self.is_before(other, self.start): 2196 return 1 2197 else: 2198 return 0 2199 2200 class TimespanCollection: 2201 2202 """ 2203 A class providing a list-like interface supporting membership tests at a 2204 particular resolution in order to maintain a collection of non-overlapping 2205 timespans. 2206 """ 2207 2208 def __init__(self, resolution, values=None): 2209 self.resolution = resolution 2210 self.values = values or [] 2211 2212 def as_timespan(self): 2213 return Timespan(*self.as_limits()) 2214 2215 def as_limits(self): 2216 2217 "Return the earliest and latest points in time for this collection." 2218 2219 if not self.values: 2220 return None, None 2221 else: 2222 first, last = self.values[0], self.values[-1] 2223 if isinstance(first, ActsAsTimespan): 2224 first = first.as_timespan().start 2225 if isinstance(last, ActsAsTimespan): 2226 last = last.as_timespan().end 2227 return first, last 2228 2229 def convert(self, value): 2230 if isinstance(value, ActsAsTimespan): 2231 ts = value.as_timespan() 2232 return ts and ts.convert(self.resolution) 2233 else: 2234 return value.convert(self.resolution) 2235 2236 def __iter__(self): 2237 return iter(self.values) 2238 2239 def __len__(self): 2240 return len(self.values) 2241 2242 def __getitem__(self, i): 2243 return self.values[i] 2244 2245 def __setitem__(self, i, value): 2246 self.values[i] = value 2247 2248 def __contains__(self, value): 2249 test_value = self.convert(value) 2250 return test_value in self.values 2251 2252 def append(self, value): 2253 self.values.append(value) 2254 2255 def insert(self, i, value): 2256 self.values.insert(i, value) 2257 2258 def pop(self): 2259 return self.values.pop() 2260 2261 def insert_in_order(self, value): 2262 bisect.insort_left(self, value) 2263 2264 def getCountry(s): 2265 2266 "Find a country code in the given string 's'." 2267 2268 match = country_code_regexp.search(s) 2269 2270 if match: 2271 return match.group("code") 2272 else: 2273 return None 2274 2275 def getDate(s): 2276 2277 "Parse the string 's', extracting and returning a date object." 2278 2279 dt = getDateTime(s) 2280 if dt is not None: 2281 return dt.as_date() 2282 else: 2283 return None 2284 2285 def getDateTime(s): 2286 2287 """ 2288 Parse the string 's', extracting and returning a datetime object where time 2289 information has been given or a date object where time information is 2290 absent. 2291 """ 2292 2293 m = datetime_regexp.search(s) 2294 if m: 2295 groups = list(m.groups()) 2296 2297 # Convert date and time data to integer or None. 2298 2299 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date() 2300 else: 2301 return None 2302 2303 def getDateFromCalendar(s): 2304 2305 """ 2306 Parse the iCalendar format string 's', extracting and returning a date 2307 object. 2308 """ 2309 2310 dt = getDateTimeFromCalendar(s) 2311 if dt is not None: 2312 return dt.as_date() 2313 else: 2314 return None 2315 2316 def getDateTimeFromCalendar(s): 2317 2318 """ 2319 Parse the iCalendar format datetime string 's', extracting and returning a 2320 datetime object where time information has been given or a date object where 2321 time information is absent. 2322 """ 2323 2324 m = datetime_icalendar_regexp.search(s) 2325 if m: 2326 groups = list(m.groups()) 2327 2328 # Convert date and time data to integer or None. 2329 2330 return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or None]).as_datetime_or_date() 2331 else: 2332 return None 2333 2334 def getDateStrings(s): 2335 2336 "Parse the string 's', extracting and returning all date strings." 2337 2338 start = 0 2339 m = date_regexp.search(s, start) 2340 l = [] 2341 while m: 2342 l.append("-".join(m.groups())) 2343 m = date_regexp.search(s, m.end()) 2344 return l 2345 2346 def getMonth(s): 2347 2348 "Parse the string 's', extracting and returning a month object." 2349 2350 m = month_regexp.search(s) 2351 if m: 2352 return Month(map(int, m.groups())) 2353 else: 2354 return None 2355 2356 def getCurrentDate(): 2357 2358 "Return the current date as a (year, month, day) tuple." 2359 2360 today = datetime.date.today() 2361 return Date((today.year, today.month, today.day)) 2362 2363 def getCurrentMonth(): 2364 2365 "Return the current month as a (year, month) tuple." 2366 2367 today = datetime.date.today() 2368 return Month((today.year, today.month)) 2369 2370 def getCurrentYear(): 2371 2372 "Return the current year." 2373 2374 today = datetime.date.today() 2375 return today.year 2376 2377 # Location-related functions. 2378 2379 class Reference: 2380 2381 "A map reference." 2382 2383 def __init__(self, degrees, minutes=0, seconds=0): 2384 self.degrees = degrees 2385 self.minutes = minutes 2386 self.seconds = seconds 2387 2388 def __repr__(self): 2389 return "Reference(%d, %d, %f)" % (self.degrees, self.minutes, self.seconds) 2390 2391 def __str__(self): 2392 return "%d:%d:%f" % (self.degrees, self.minutes, self.seconds) 2393 2394 def __add__(self, other): 2395 if not isinstance(other, Reference): 2396 return NotImplemented 2397 else: 2398 s = sign(self.degrees) 2399 o = sign(other.degrees) 2400 carry, seconds = adc(s * self.seconds, o * other.seconds) 2401 carry, minutes = adc(s * self.minutes, o * other.minutes + carry) 2402 return Reference(self.degrees + other.degrees + carry, minutes, seconds) 2403 2404 def __sub__(self, other): 2405 if not isinstance(other, Reference): 2406 return NotImplemented 2407 else: 2408 return self.__add__(Reference(-other.degrees, other.minutes, other.seconds)) 2409 2410 def _compare(self, op, other): 2411 if not isinstance(other, Reference): 2412 return NotImplemented 2413 else: 2414 return op(self.to_degrees(), other.to_degrees()) 2415 2416 def __eq__(self, other): 2417 return self._compare(operator.eq, other) 2418 2419 def __ne__(self, other): 2420 return self._compare(operator.ne, other) 2421 2422 def __lt__(self, other): 2423 return self._compare(operator.lt, other) 2424 2425 def __le__(self, other): 2426 return self._compare(operator.le, other) 2427 2428 def __gt__(self, other): 2429 return self._compare(operator.gt, other) 2430 2431 def __ge__(self, other): 2432 return self._compare(operator.ge, other) 2433 2434 def to_degrees(self): 2435 return sign(self.degrees) * (abs(self.degrees) + self.minutes / 60.0 + self.seconds / 3600.0) 2436 2437 def to_pixels(self, scale): 2438 return self.to_degrees() * scale 2439 2440 def adc(x, y): 2441 result = x + y 2442 return divmod(result, 60) 2443 2444 def getPositionForReference(latitude, longitude, map_y, map_x, map_x_scale, map_y_scale): 2445 return (longitude - map_x).to_pixels(map_x_scale), (latitude - map_y).to_pixels(map_y_scale) 2446 2447 def getPositionForCentrePoint(position, map_x_scale, map_y_scale): 2448 x, y = position 2449 return x - map_x_scale / 2.0, y - map_y_scale / 2.0 2450 2451 def getMapReference(value): 2452 2453 "Return a map reference by parsing the given 'value'." 2454 2455 if value.find(":") != -1: 2456 return getMapReferenceFromDMS(value) 2457 else: 2458 return getMapReferenceFromDecimal(value) 2459 2460 def getMapReferenceFromDMS(value): 2461 2462 """ 2463 Return a map reference by parsing the given 'value' expressed as degrees, 2464 minutes, seconds. 2465 """ 2466 2467 values = value.split(":") 2468 values = map(int, values[:2]) + map(float, values[2:3]) 2469 return Reference(*values) 2470 2471 def getMapReferenceFromDecimal(value): 2472 2473 "Return a map reference by parsing the given 'value' in decimal degrees." 2474 2475 value = float(value) 2476 degrees, remainder = divmod(abs(value * 3600), 3600) 2477 minutes, seconds = divmod(remainder, 60) 2478 return Reference(sign(value) * degrees, minutes, seconds) 2479 2480 # vim: tabstop=4 expandtab shiftwidth=4 2481 2482 # User interface functions. 2483 2484 def getParameter(request, name, default=None): 2485 2486 """ 2487 Using the given 'request', return the value of the parameter with the given 2488 'name', returning the optional 'default' (or None) if no value was supplied 2489 in the 'request'. 2490 """ 2491 2492 return get_form(request).get(name, [default])[0] 2493 2494 def getQualifiedParameter(request, calendar_name, argname, default=None): 2495 2496 """ 2497 Using the given 'request', 'calendar_name' and 'argname', retrieve the 2498 value of the qualified parameter, returning the optional 'default' (or None) 2499 if no value was supplied in the 'request'. 2500 """ 2501 2502 argname = getQualifiedParameterName(calendar_name, argname) 2503 return getParameter(request, argname, default) 2504 2505 def getQualifiedParameterName(calendar_name, argname): 2506 2507 """ 2508 Return the qualified parameter name using the given 'calendar_name' and 2509 'argname'. 2510 """ 2511 2512 if calendar_name is None: 2513 return argname 2514 else: 2515 return "%s-%s" % (calendar_name, argname) 2516 2517 def getParameterDate(arg): 2518 2519 "Interpret 'arg', recognising keywords and simple arithmetic operations." 2520 2521 n = None 2522 2523 if arg is None: 2524 return None 2525 2526 elif arg.startswith("current"): 2527 date = getCurrentDate() 2528 if len(arg) > 8: 2529 n = int(arg[7:]) 2530 2531 elif arg.startswith("yearstart"): 2532 date = Date((getCurrentYear(), 1, 1)) 2533 if len(arg) > 10: 2534 n = int(arg[9:]) 2535 2536 elif arg.startswith("yearend"): 2537 date = Date((getCurrentYear(), 12, 31)) 2538 if len(arg) > 8: 2539 n = int(arg[7:]) 2540 2541 else: 2542 date = getDate(arg) 2543 2544 if n is not None: 2545 date = date.day_update(n) 2546 2547 return date 2548 2549 def getParameterMonth(arg): 2550 2551 "Interpret 'arg', recognising keywords and simple arithmetic operations." 2552 2553 n = None 2554 2555 if arg is None: 2556 return None 2557 2558 elif arg.startswith("current"): 2559 date = getCurrentMonth() 2560 if len(arg) > 8: 2561 n = int(arg[7:]) 2562 2563 elif arg.startswith("yearstart"): 2564 date = Month((getCurrentYear(), 1)) 2565 if len(arg) > 10: 2566 n = int(arg[9:]) 2567 2568 elif arg.startswith("yearend"): 2569 date = Month((getCurrentYear(), 12)) 2570 if len(arg) > 8: 2571 n = int(arg[7:]) 2572 2573 else: 2574 date = getMonth(arg) 2575 2576 if n is not None: 2577 date = date.month_update(n) 2578 2579 return date 2580 2581 def getFormDate(request, calendar_name, argname): 2582 2583 """ 2584 Return the date from the 'request' for the calendar with the given 2585 'calendar_name' using the parameter having the given 'argname'. 2586 """ 2587 2588 arg = getQualifiedParameter(request, calendar_name, argname) 2589 return getParameterDate(arg) 2590 2591 def getFormMonth(request, calendar_name, argname): 2592 2593 """ 2594 Return the month from the 'request' for the calendar with the given 2595 'calendar_name' using the parameter having the given 'argname'. 2596 """ 2597 2598 arg = getQualifiedParameter(request, calendar_name, argname) 2599 return getParameterMonth(arg) 2600 2601 def getFormDateTriple(request, yeararg, montharg, dayarg): 2602 2603 """ 2604 Return the date from the 'request' for the calendar with the given 2605 'calendar_name' using the parameters having the given 'yeararg', 'montharg' 2606 and 'dayarg' names. 2607 """ 2608 2609 year = getParameter(request, yeararg) 2610 month = getParameter(request, montharg) 2611 day = getParameter(request, dayarg) 2612 if year and month and day: 2613 return Date((int(year), int(month), int(day))) 2614 else: 2615 return None 2616 2617 def getFormMonthPair(request, yeararg, montharg): 2618 2619 """ 2620 Return the month from the 'request' for the calendar with the given 2621 'calendar_name' using the parameters having the given 'yeararg' and 2622 'montharg' names. 2623 """ 2624 2625 year = getParameter(request, yeararg) 2626 month = getParameter(request, montharg) 2627 if year and month: 2628 return Month((int(year), int(month))) 2629 else: 2630 return None 2631 2632 def getFullDateLabel(request, date): 2633 2634 """ 2635 Return the full month plus year label using the given 'request' and 2636 'year_month'. 2637 """ 2638 2639 if not date: 2640 return "" 2641 2642 _ = request.getText 2643 year, month, day = date.as_tuple()[:3] 2644 start_weekday, number_of_days = date.month_properties() 2645 weekday = (start_weekday + day - 1) % 7 2646 day_label = _(getDayLabel(weekday)) 2647 month_label = _(getMonthLabel(month)) 2648 return "%s %s %s %s" % (day_label, day, month_label, year) 2649 2650 def getFullMonthLabel(request, year_month): 2651 2652 """ 2653 Return the full month plus year label using the given 'request' and 2654 'year_month'. 2655 """ 2656 2657 if not year_month: 2658 return "" 2659 2660 _ = request.getText 2661 year, month = year_month.as_tuple()[:2] 2662 month_label = _(getMonthLabel(month)) 2663 return "%s %s" % (month_label, year) 2664 2665 # Page-related functions. 2666 2667 def getPrettyPageName(page): 2668 2669 "Return a nicely formatted title/name for the given 'page'." 2670 2671 title = page.split_title(force=1) 2672 return getPrettyTitle(title) 2673 2674 def linkToPage(request, page, text, query_string=None): 2675 2676 """ 2677 Using 'request', return a link to 'page' with the given link 'text' and 2678 optional 'query_string'. 2679 """ 2680 2681 text = wikiutil.escape(text) 2682 return page.link_to_raw(request, text, query_string) 2683 2684 def linkToResource(url, request, text, query_string=None): 2685 2686 """ 2687 Using 'request', return a link to 'url' with the given link 'text' and 2688 optional 'query_string'. 2689 """ 2690 2691 if query_string: 2692 query_string = wikiutil.makeQueryString(query_string) 2693 url = "%s?%s" % (url, query_string) 2694 2695 formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter 2696 2697 output = [] 2698 output.append(formatter.url(1, url)) 2699 output.append(formatter.text(text)) 2700 output.append(formatter.url(0)) 2701 return "".join(output) 2702 2703 def getFullPageName(parent, title): 2704 2705 """ 2706 Return a full page name from the given 'parent' page (can be empty or None) 2707 and 'title' (a simple page name). 2708 """ 2709 2710 if parent: 2711 return "%s/%s" % (parent.rstrip("/"), title) 2712 else: 2713 return title 2714 2715 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 2716 2717 """ 2718 Using the given 'template_page', complete the 'new_page' by copying the 2719 template and adding the given 'event_details' (a dictionary of event 2720 fields), setting also the 'category_pagenames' to define category 2721 membership. 2722 """ 2723 2724 event_page = EventPage(template_page) 2725 new_event_page = EventPage(new_page) 2726 new_event_page.copyPage(event_page) 2727 2728 if new_event_page.getFormat() == "wiki": 2729 new_event = Event(new_event_page, event_details) 2730 new_event_page.setEvents([new_event]) 2731 new_event_page.setCategoryMembership(category_pagenames) 2732 new_event_page.saveChanges() 2733 2734 # vim: tabstop=4 expandtab shiftwidth=4