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