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