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