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