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