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