1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator library 4 5 @copyright: 2008, 2009, 2010, 2011 by Paul Boddie <paul@boddie.org.uk> 6 @copyright: 2000-2004 Juergen Hermann <jh@web.de>, 7 2005-2008 MoinMoin:ThomasWaldmann. 8 @license: GNU GPL (v2 or later), see COPYING.txt for details. 9 """ 10 11 from MoinMoin.Page import Page 12 from MoinMoin.action import cache 13 from MoinMoin import caching 14 from MoinMoin import search, version 15 from MoinMoin import wikiutil 16 import calendar 17 import codecs 18 import datetime 19 import time 20 import re 21 import bisect 22 import operator 23 import urllib, 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.1" 48 49 # Date labels. 50 51 month_labels = ["January", "February", "March", "April", "May", "June", 52 "July", "August", "September", "October", "November", "December"] 53 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 54 55 # Regular expressions where MoinMoin does not provide the required support. 56 57 category_regexp = None 58 59 # Page parsing. 60 61 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 62 category_membership_regexp = re.compile(ur"^\s*(?:(Category\S+)(?:\s+(Category\S+))*)\s*$", re.MULTILINE | re.UNICODE) 63 64 # Value parsing. 65 66 country_code_regexp = re.compile(ur'(?:^|\W)(?P<code>[A-Z]{2})(?:$|\W+$)', re.UNICODE) 67 location_normalised_regexp = re.compile( 68 ur"(?:\d+\w*\s+)?" # preceding postcode (optional) 69 ur"(?P<location>" # start of group of interest 70 ur"\w[\w\s-]+?" # area or town 71 ur"(?:,(?:\s*[\w-]+)+)?" # country (optional) 72 ur")$", re.UNICODE) 73 74 # Month, date, time and datetime parsing. 75 76 month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})' 77 date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})' 78 time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?' 79 timezone_offset_str = ur'(?P<offset>(UTC)?(?:(?P<sign>[-+])(?P<hours>[0-9]{2})(?::?(?P<minutes>[0-9]{2}))?))' 80 timezone_olson_str = ur'(?P<olson>[a-zA-Z]+(?:/[-_a-zA-Z]+){1,2})' 81 timezone_utc_str = ur'UTC' 82 timezone_regexp_str = ur'(?P<zone>' + timezone_offset_str + '|' + timezone_olson_str + '|' + timezone_utc_str + ')' 83 datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?' 84 85 month_regexp = re.compile(month_regexp_str, re.UNICODE) 86 date_regexp = re.compile(date_regexp_str, re.UNICODE) 87 time_regexp = re.compile(time_regexp_str, re.UNICODE) 88 timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE) 89 timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE) 90 datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE) 91 92 # iCalendar date and datetime parsing. 93 94 date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})' 95 datetime_icalendar_regexp_str = date_icalendar_regexp_str + \ 96 ur'(?:' \ 97 ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \ 98 ur'(?P<utc>Z)?' \ 99 ur')?' 100 101 date_icalendar_regexp = re.compile(date_icalendar_regexp_str, re.UNICODE) 102 datetime_icalendar_regexp = re.compile(datetime_icalendar_regexp_str, re.UNICODE) 103 104 # Content type parsing. 105 106 encoding_regexp_str = ur'(?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 try: 1298 f = urllib2.urlopen(url) 1299 cache_entry.open(mode="w") 1300 try: 1301 cache_entry.write(url + "\n") 1302 cache_entry.write((f.headers.get("content-type") or "") + "\n") 1303 cache_entry.write(f.read()) 1304 finally: 1305 cache_entry.close() 1306 f.close() 1307 1308 # In case of an exception, just ignore the remote source. 1309 # NOTE: This could be reported somewhere. 1310 1311 except IOError: 1312 continue 1313 1314 # Open the cache entry and read it. 1315 1316 cache_entry.open() 1317 try: 1318 data = cache_entry.read() 1319 finally: 1320 cache_entry.close() 1321 1322 # Process the entry, parsing the content. 1323 1324 f = StringIO(data) 1325 try: 1326 url = f.readline() 1327 1328 # Get the content type and encoding, making sure that the data 1329 # can be parsed. 1330 1331 content_type, encoding = getContentTypeAndEncoding(f.readline()) 1332 if content_type != required_content_type: 1333 continue 1334 1335 # Send the data to the parser. 1336 1337 uf = codecs.getreader(encoding or "utf-8")(f) 1338 try: 1339 resources.append(resource_cls(url, parser(uf))) 1340 finally: 1341 uf.close() 1342 finally: 1343 f.close() 1344 1345 return resources 1346 1347 def getEventsFromResources(resources): 1348 1349 "Return a list of events supplied by the given event 'resources'." 1350 1351 events = [] 1352 1353 for resource in resources: 1354 1355 # Get all events described by the resource. 1356 1357 for event in resource.getEvents(): 1358 1359 # Remember the event. 1360 1361 events.append(event) 1362 1363 return events 1364 1365 # Event filtering and limits. 1366 1367 def getEventsInPeriod(events, calendar_period): 1368 1369 """ 1370 Return a collection containing those of the given 'events' which occur 1371 within the given 'calendar_period'. 1372 """ 1373 1374 all_shown_events = [] 1375 1376 for event in events: 1377 1378 # Test for the suitability of the event. 1379 1380 if event.as_timespan() is not None: 1381 1382 # Compare the dates to the requested calendar window, if any. 1383 1384 if event in calendar_period: 1385 all_shown_events.append(event) 1386 1387 return all_shown_events 1388 1389 def getEventLimits(events): 1390 1391 "Return the earliest and latest of the given 'events'." 1392 1393 earliest = None 1394 latest = None 1395 1396 for event in events: 1397 1398 # Test for the suitability of the event. 1399 1400 if event.as_timespan() is not None: 1401 ts = event.as_timespan() 1402 if earliest is None or ts.start < earliest: 1403 earliest = ts.start 1404 if latest is None or ts.end > latest: 1405 latest = ts.end 1406 1407 return earliest, latest 1408 1409 def setEventTimestamps(request, events): 1410 1411 """ 1412 Using 'request', set timestamp details in the details dictionary of each of 1413 the 'events'. 1414 1415 Return the latest timestamp found. 1416 """ 1417 1418 latest = None 1419 1420 for event in events: 1421 event_details = event.getDetails() 1422 1423 # Populate the details with event metadata. 1424 1425 event_details.update(event.getMetadata()) 1426 1427 if latest is None or latest < event_details["last-modified"]: 1428 latest = event_details["last-modified"] 1429 1430 return latest 1431 1432 def getOrderedEvents(events): 1433 1434 """ 1435 Return a list with the given 'events' ordered according to their start and 1436 end dates. 1437 """ 1438 1439 ordered_events = events[:] 1440 ordered_events.sort() 1441 return ordered_events 1442 1443 def getCalendarPeriod(calendar_start, calendar_end): 1444 1445 """ 1446 Return a calendar period for the given 'calendar_start' and 'calendar_end'. 1447 These parameters can be given as None. 1448 """ 1449 1450 # Re-order the window, if appropriate. 1451 1452 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 1453 calendar_start, calendar_end = calendar_end, calendar_start 1454 1455 return Timespan(calendar_start, calendar_end) 1456 1457 def getConcretePeriod(calendar_start, calendar_end, earliest, latest, resolution): 1458 1459 """ 1460 From the requested 'calendar_start' and 'calendar_end', which may be None, 1461 indicating that no restriction is imposed on the period for each of the 1462 boundaries, use the 'earliest' and 'latest' event months to define a 1463 specific period of interest. 1464 """ 1465 1466 # Define the period as starting with any specified start month or the 1467 # earliest event known, ending with any specified end month or the latest 1468 # event known. 1469 1470 first = calendar_start or earliest 1471 last = calendar_end or latest 1472 1473 # If there is no range of months to show, perhaps because there are no 1474 # events in the requested period, and there was no start or end month 1475 # specified, show only the month indicated by the start or end of the 1476 # requested period. If all events were to be shown but none were found show 1477 # the current month. 1478 1479 if resolution == "date": 1480 get_current = getCurrentDate 1481 else: 1482 get_current = getCurrentMonth 1483 1484 if first is None: 1485 first = last or get_current() 1486 if last is None: 1487 last = first or get_current() 1488 1489 if resolution == "month": 1490 first = first.as_month() 1491 last = last.as_month() 1492 1493 # Permit "expiring" periods (where the start date approaches the end date). 1494 1495 return min(first, last), last 1496 1497 def getCoverage(events, resolution="date"): 1498 1499 """ 1500 Determine the coverage of the given 'events', returning a collection of 1501 timespans, along with a dictionary mapping locations to collections of 1502 slots, where each slot contains a tuple of the form (timespans, events). 1503 """ 1504 1505 all_events = {} 1506 full_coverage = TimespanCollection(resolution) 1507 1508 # Get event details. 1509 1510 for event in events: 1511 event_details = event.getDetails() 1512 1513 # Find the coverage of this period for the event. 1514 1515 # For day views, each location has its own slot, but for month 1516 # views, all locations are pooled together since having separate 1517 # slots for each location can lead to poor usage of vertical space. 1518 1519 if resolution == "datetime": 1520 event_location = event_details.get("location") 1521 else: 1522 event_location = None 1523 1524 # Update the overall coverage. 1525 1526 full_coverage.insert_in_order(event) 1527 1528 # Add a new events list for a new location. 1529 # Locations can be unspecified, thus None refers to all unlocalised 1530 # events. 1531 1532 if not all_events.has_key(event_location): 1533 all_events[event_location] = [TimespanCollection(resolution, [event])] 1534 1535 # Try and fit the event into an events list. 1536 1537 else: 1538 slot = all_events[event_location] 1539 1540 for slot_events in slot: 1541 1542 # Where the event does not overlap with the events in the 1543 # current collection, add it alongside these events. 1544 1545 if not event in slot_events: 1546 slot_events.insert_in_order(event) 1547 break 1548 1549 # Make a new element in the list if the event cannot be 1550 # marked alongside existing events. 1551 1552 else: 1553 slot.append(TimespanCollection(resolution, [event])) 1554 1555 return full_coverage, all_events 1556 1557 def getCoverageScale(coverage): 1558 1559 """ 1560 Return a scale for the given coverage so that the times involved are 1561 exposed. The scale consists of a list of non-overlapping timespans forming 1562 a contiguous period of time. 1563 """ 1564 1565 times = set() 1566 for timespan in coverage: 1567 start, end = timespan.as_limits() 1568 1569 # Add either genuine times or dates converted to times. 1570 1571 if isinstance(start, DateTime): 1572 times.add(start) 1573 else: 1574 times.add(start.as_start_of_day()) 1575 1576 if isinstance(end, DateTime): 1577 times.add(end) 1578 else: 1579 times.add(end.as_date().next_day()) 1580 1581 times = list(times) 1582 times.sort(cmp_dates_as_day_start) 1583 1584 scale = [] 1585 first = 1 1586 start = None 1587 for time in times: 1588 if not first: 1589 scale.append(Timespan(start, time)) 1590 else: 1591 first = 0 1592 start = time 1593 1594 return scale 1595 1596 # Date-related functions. 1597 1598 def cmp_dates_as_day_start(a, b): 1599 1600 """ 1601 Compare dates/datetimes 'a' and 'b' treating dates without time information 1602 as the earliest time in a particular day. 1603 """ 1604 1605 are_equal = a == b 1606 1607 if are_equal: 1608 a2 = a.as_datetime_or_date() 1609 b2 = b.as_datetime_or_date() 1610 1611 if isinstance(a2, Date) and isinstance(b2, DateTime): 1612 return -1 1613 elif isinstance(a2, DateTime) and isinstance(b2, Date): 1614 return 1 1615 1616 return cmp(a, b) 1617 1618 class Convertible: 1619 1620 "Support for converting temporal objects." 1621 1622 def _get_converter(self, resolution): 1623 if resolution == "month": 1624 return lambda x: x and x.as_month() 1625 elif resolution == "date": 1626 return lambda x: x and x.as_date() 1627 elif resolution == "datetime": 1628 return lambda x: x and x.as_datetime_or_date() 1629 else: 1630 return lambda x: x 1631 1632 class Temporal(Convertible): 1633 1634 "A simple temporal representation, common to dates and times." 1635 1636 def __init__(self, data): 1637 self.data = list(data) 1638 1639 def __repr__(self): 1640 return "%s(%r)" % (self.__class__.__name__, self.data) 1641 1642 def __hash__(self): 1643 return hash(self.as_tuple()) 1644 1645 def as_tuple(self): 1646 return tuple(self.data) 1647 1648 def convert(self, resolution): 1649 return self._get_converter(resolution)(self) 1650 1651 def __cmp__(self, other): 1652 1653 """ 1654 The result of comparing this instance with 'other' is derived from a 1655 comparison of the instances' date(time) data at the highest common 1656 resolution, meaning that if a date is compared to a datetime, the 1657 datetime will be considered as a date. Thus, a date and a datetime 1658 referring to the same date will be considered equal. 1659 """ 1660 1661 if not isinstance(other, Temporal): 1662 return NotImplemented 1663 else: 1664 data = self.as_tuple() 1665 other_data = other.as_tuple() 1666 length = min(len(data), len(other_data)) 1667 return cmp(data[:length], other_data[:length]) 1668 1669 def __sub__(self, other): 1670 1671 """ 1672 Return the difference between this object and the 'other' object at the 1673 highest common accuracy of both objects. 1674 """ 1675 1676 if not isinstance(other, Temporal): 1677 return NotImplemented 1678 else: 1679 data = self.as_tuple() 1680 other_data = other.as_tuple() 1681 if len(data) < len(other_data): 1682 return len(self.until(other)) 1683 else: 1684 return len(other.until(self)) 1685 1686 def _until(self, start, end, nextfn, prevfn): 1687 1688 """ 1689 Return a collection of units of time by starting from the given 'start' 1690 and stepping across intervening units until 'end' is reached, using the 1691 given 'nextfn' and 'prevfn' to step from one unit to the next. 1692 """ 1693 1694 current = start 1695 units = [current] 1696 if current < end: 1697 while current < end: 1698 current = nextfn(current) 1699 units.append(current) 1700 elif current > end: 1701 while current > end: 1702 current = prevfn(current) 1703 units.append(current) 1704 return units 1705 1706 def ambiguous(self): 1707 1708 "Only times can be ambiguous." 1709 1710 return 0 1711 1712 class Month(Temporal): 1713 1714 "A simple year-month representation." 1715 1716 def __str__(self): 1717 return "%04d-%02d" % self.as_tuple()[:2] 1718 1719 def as_datetime(self, day, hour, minute, second, zone): 1720 return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) 1721 1722 def as_date(self, day): 1723 if day < 0: 1724 weekday, ndays = self.month_properties() 1725 day = ndays + 1 + day 1726 return Date(self.as_tuple() + (day,)) 1727 1728 def as_month(self): 1729 return self 1730 1731 def year(self): 1732 return self.data[0] 1733 1734 def month(self): 1735 return self.data[1] 1736 1737 def month_properties(self): 1738 1739 """ 1740 Return the weekday of the 1st of the month, along with the number of 1741 days, as a tuple. 1742 """ 1743 1744 year, month = self.as_tuple()[:2] 1745 return calendar.monthrange(year, month) 1746 1747 def month_update(self, n=1): 1748 1749 "Return the month updated by 'n' months." 1750 1751 year, month = self.as_tuple()[:2] 1752 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1)) 1753 1754 update = month_update 1755 1756 def next_month(self): 1757 1758 "Return the month following this one." 1759 1760 return self.month_update(1) 1761 1762 next = next_month 1763 1764 def previous_month(self): 1765 1766 "Return the month preceding this one." 1767 1768 return self.month_update(-1) 1769 1770 previous = previous_month 1771 1772 def months_until(self, end): 1773 1774 "Return the collection of months from this month until 'end'." 1775 1776 return self._until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month) 1777 1778 until = months_until 1779 1780 class Date(Month): 1781 1782 "A simple year-month-day representation." 1783 1784 def constrain(self): 1785 year, month, day = self.as_tuple()[:3] 1786 1787 month = max(min(month, 12), 1) 1788 wd, last_day = calendar.monthrange(year, month) 1789 day = max(min(day, last_day), 1) 1790 1791 self.data[1:3] = month, day 1792 1793 def __str__(self): 1794 return "%04d-%02d-%02d" % self.as_tuple()[:3] 1795 1796 def as_datetime(self, hour, minute, second, zone): 1797 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 1798 1799 def as_start_of_day(self): 1800 return self.as_datetime(None, None, None, None) 1801 1802 def as_date(self): 1803 return self 1804 1805 def as_datetime_or_date(self): 1806 return self 1807 1808 def as_month(self): 1809 return Month(self.data[:2]) 1810 1811 def day(self): 1812 return self.data[2] 1813 1814 def day_update(self, n=1): 1815 1816 "Return the month updated by 'n' days." 1817 1818 delta = datetime.timedelta(n) 1819 dt = datetime.date(*self.as_tuple()[:3]) 1820 dt_new = dt + delta 1821 return Date((dt_new.year, dt_new.month, dt_new.day)) 1822 1823 update = day_update 1824 1825 def next_day(self): 1826 1827 "Return the date following this one." 1828 1829 year, month, day = self.as_tuple()[:3] 1830 _wd, end_day = calendar.monthrange(year, month) 1831 if day == end_day: 1832 if month == 12: 1833 return Date((year + 1, 1, 1)) 1834 else: 1835 return Date((year, month + 1, 1)) 1836 else: 1837 return Date((year, month, day + 1)) 1838 1839 next = next_day 1840 1841 def previous_day(self): 1842 1843 "Return the date preceding this one." 1844 1845 year, month, day = self.as_tuple()[:3] 1846 if day == 1: 1847 if month == 1: 1848 return Date((year - 1, 12, 31)) 1849 else: 1850 _wd, end_day = calendar.monthrange(year, month - 1) 1851 return Date((year, month - 1, end_day)) 1852 else: 1853 return Date((year, month, day - 1)) 1854 1855 previous = previous_day 1856 1857 def days_until(self, end): 1858 1859 "Return the collection of days from this date until 'end'." 1860 1861 return self._until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 1862 1863 until = days_until 1864 1865 class DateTime(Date): 1866 1867 "A simple date plus time representation." 1868 1869 def constrain(self): 1870 Date.constrain(self) 1871 1872 hour, minute, second = self.as_tuple()[3:6] 1873 1874 if self.has_time(): 1875 hour = max(min(hour, 23), 0) 1876 minute = max(min(minute, 59), 0) 1877 1878 if second is not None: 1879 second = max(min(second, 60), 0) # support leap seconds 1880 1881 self.data[3:6] = hour, minute, second 1882 1883 def __str__(self): 1884 return Date.__str__(self) + self.time_string() 1885 1886 def time_string(self): 1887 if self.has_time(): 1888 data = self.as_tuple() 1889 time_str = " %02d:%02d" % data[3:5] 1890 if data[5] is not None: 1891 time_str += ":%02d" % data[5] 1892 if data[6] is not None: 1893 time_str += " %s" % data[6] 1894 return time_str 1895 else: 1896 return "" 1897 1898 def as_HTTP_datetime_string(self): 1899 weekday = calendar.weekday(*self.data[:3]) 1900 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (( 1901 getDayLabel(weekday), 1902 self.data[2], 1903 getMonthLabel(self.data[1]), 1904 self.data[0] 1905 ) + tuple(self.data[3:6])) 1906 1907 def as_datetime(self): 1908 return self 1909 1910 def as_date(self): 1911 return Date(self.data[:3]) 1912 1913 def as_datetime_or_date(self): 1914 1915 """ 1916 Return a date for this datetime if fields are missing. Otherwise, return 1917 this datetime itself. 1918 """ 1919 1920 if not self.has_time(): 1921 return self.as_date() 1922 else: 1923 return self 1924 1925 def __cmp__(self, other): 1926 1927 """ 1928 The result of comparing this instance with 'other' is, if both instances 1929 are datetime instances, derived from a comparison of the datetimes 1930 converted to UTC. If one or both datetimes cannot be converted to UTC, 1931 the datetimes are compared using the basic temporal comparison which 1932 compares their raw time data. 1933 """ 1934 1935 this = self 1936 1937 if this.has_time(): 1938 if isinstance(other, DateTime): 1939 if other.has_time(): 1940 this_utc = this.to_utc() 1941 other_utc = other.to_utc() 1942 if this_utc is not None and other_utc is not None: 1943 return cmp(this_utc.as_tuple(), other_utc.as_tuple()) 1944 else: 1945 other = other.padded() 1946 else: 1947 this = this.padded() 1948 1949 return Date.__cmp__(this, other) 1950 1951 def has_time(self): 1952 1953 """ 1954 Return whether this object has any time information. Objects without 1955 time information can refer to the very start of a day. 1956 """ 1957 1958 return self.data[3] is not None and self.data[4] is not None 1959 1960 def time(self): 1961 return self.data[3:] 1962 1963 def seconds(self): 1964 return self.data[5] 1965 1966 def time_zone(self): 1967 return self.data[6] 1968 1969 def set_time_zone(self, value): 1970 self.data[6] = value 1971 1972 def padded(self, empty_value=0): 1973 1974 """ 1975 Return a datetime with missing fields defined as being the given 1976 'empty_value' or 0 if not specified. 1977 """ 1978 1979 data = [] 1980 for x in self.data[:6]: 1981 if x is None: 1982 data.append(empty_value) 1983 else: 1984 data.append(x) 1985 1986 data += self.data[6:] 1987 return DateTime(data) 1988 1989 def to_utc(self): 1990 1991 """ 1992 Return this object converted to UTC, or None if such a conversion is not 1993 defined. 1994 """ 1995 1996 if not self.has_time(): 1997 return None 1998 1999 offset = self.utc_offset() 2000 if offset: 2001 hours, minutes = offset 2002 2003 # Invert the offset to get the correction. 2004 2005 hours, minutes = -hours, -minutes 2006 2007 # Get the components. 2008 2009 hour, minute, second, zone = self.time() 2010 date = self.as_date() 2011 2012 # Add the minutes and hours. 2013 2014 minute += minutes 2015 if minute < 0 or minute > 59: 2016 hour += minute / 60 2017 minute = minute % 60 2018 2019 # NOTE: This makes various assumptions and probably would not work 2020 # NOTE: for general arithmetic. 2021 2022 hour += hours 2023 if hour < 0: 2024 date = date.previous_day() 2025 hour += 24 2026 elif hour > 23: 2027 date = date.next_day() 2028 hour -= 24 2029 2030 return date.as_datetime(hour, minute, second, "UTC") 2031 2032 # Cannot convert. 2033 2034 else: 2035 return None 2036 2037 def utc_offset(self): 2038 2039 "Return the UTC offset in hours and minutes." 2040 2041 zone = self.time_zone() 2042 if not zone: 2043 return None 2044 2045 # Support explicit UTC zones. 2046 2047 if zone == "UTC": 2048 return 0, 0 2049 2050 # Attempt to return a UTC offset where an explicit offset has been set. 2051 2052 match = timezone_offset_regexp.match(zone) 2053 if match: 2054 if match.group("sign") == "-": 2055 sign = -1 2056 else: 2057 sign = 1 2058 2059 hours = int(match.group("hours")) * sign 2060 minutes = int(match.group("minutes") or 0) * sign 2061 return hours, minutes 2062 2063 # Attempt to handle Olson time zone identifiers. 2064 2065 dt = self.as_olson_datetime() 2066 if dt: 2067 seconds = dt.utcoffset().seconds 2068 hours = seconds / 3600 2069 minutes = (seconds % 3600) / 60 2070 return hours, minutes 2071 2072 # Otherwise return None. 2073 2074 return None 2075 2076 def olson_identifier(self): 2077 2078 "Return the Olson identifier from any zone information." 2079 2080 zone = self.time_zone() 2081 if not zone: 2082 return None 2083 2084 # Attempt to match an identifier. 2085 2086 match = timezone_olson_regexp.match(zone) 2087 if match: 2088 return match.group("olson") 2089 else: 2090 return None 2091 2092 def _as_olson_datetime(self, hours=None): 2093 2094 """ 2095 Return a Python datetime object for this datetime interpreted using any 2096 Olson time zone identifier and the given 'hours' offset, raising one of 2097 the pytz exceptions in case of ambiguity. 2098 """ 2099 2100 olson = self.olson_identifier() 2101 if olson and pytz: 2102 tz = pytz.timezone(olson) 2103 data = self.padded().as_tuple()[:6] 2104 dt = datetime.datetime(*data) 2105 2106 # With an hours offset, find a time probably in a previously 2107 # applicable time zone. 2108 2109 if hours is not None: 2110 td = datetime.timedelta(0, hours * 3600) 2111 dt += td 2112 2113 ldt = tz.localize(dt, None) 2114 2115 # With an hours offset, adjust the time to define it within the 2116 # previously applicable time zone but at the presumably intended 2117 # position. 2118 2119 if hours is not None: 2120 ldt -= td 2121 2122 return ldt 2123 else: 2124 return None 2125 2126 def as_olson_datetime(self): 2127 2128 """ 2129 Return a Python datetime object for this datetime interpreted using any 2130 Olson time zone identifier, choosing the time from the zone before the 2131 period of ambiguity. 2132 """ 2133 2134 try: 2135 return self._as_olson_datetime() 2136 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 2137 2138 # Try again, using an earlier local time and then stepping forward 2139 # in the chosen zone. 2140 # NOTE: Four hours earlier seems reasonable. 2141 2142 return self._as_olson_datetime(-4) 2143 2144 def ambiguous(self): 2145 2146 "Return whether the time is local and ambiguous." 2147 2148 try: 2149 self._as_olson_datetime() 2150 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 2151 return 1 2152 2153 return 0 2154 2155 class Timespan(ActsAsTimespan, Convertible): 2156 2157 """ 2158 A period of time which can be compared against others to check for overlaps. 2159 """ 2160 2161 def __init__(self, start, end): 2162 self.start = start 2163 self.end = end 2164 2165 # NOTE: Should perhaps catch ambiguous time problems elsewhere. 2166 2167 if self.ambiguous() and self.start is not None and self.end is not None and start > end: 2168 self.start, self.end = end, start 2169 2170 def __repr__(self): 2171 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 2172 2173 def __hash__(self): 2174 return hash((self.start, self.end)) 2175 2176 def as_timespan(self): 2177 return self 2178 2179 def as_limits(self): 2180 return self.start, self.end 2181 2182 def ambiguous(self): 2183 return self.start is not None and self.start.ambiguous() or self.end is not None and self.end.ambiguous() 2184 2185 def convert(self, resolution): 2186 return Timespan(*map(self._get_converter(resolution), self.as_limits())) 2187 2188 def is_before(self, a, b): 2189 2190 """ 2191 Return whether 'a' is before 'b'. Since the end datetime of one period 2192 may be the same as the start datetime of another period, and yet the 2193 first period is intended to be concluded by the end datetime and not 2194 overlap with the other period, a different test is employed for datetime 2195 comparisons. 2196 """ 2197 2198 # Datetimes without times can be equal to dates and be considered as 2199 # occurring before those dates. Generally, datetimes should not be 2200 # produced without time information as getDateTime converts such 2201 # datetimes to dates. 2202 2203 if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()): 2204 return a <= b 2205 else: 2206 return a < b 2207 2208 def __contains__(self, other): 2209 2210 """ 2211 This instance is considered to contain 'other' if one is not before or 2212 after the other. If this instance overlaps or coincides with 'other', 2213 then 'other' is regarded as belonging to this instance's time period. 2214 """ 2215 2216 return self == other 2217 2218 def __cmp__(self, other): 2219 2220 """ 2221 Return whether this timespan occupies the same period of time as the 2222 'other'. Timespans are considered less than others if their end points 2223 precede the other's start point, and are considered greater than others 2224 if their start points follow the other's end point. 2225 """ 2226 2227 if isinstance(other, ActsAsTimespan): 2228 other = other.as_timespan() 2229 2230 if self.end is not None and other.start is not None and self.is_before(self.end, other.start): 2231 return -1 2232 elif self.start is not None and other.end is not None and self.is_before(other.end, self.start): 2233 return 1 2234 else: 2235 return 0 2236 2237 else: 2238 if self.end is not None and self.is_before(self.end, other): 2239 return -1 2240 elif self.start is not None and self.is_before(other, self.start): 2241 return 1 2242 else: 2243 return 0 2244 2245 class TimespanCollection: 2246 2247 """ 2248 A class providing a list-like interface supporting membership tests at a 2249 particular resolution in order to maintain a collection of non-overlapping 2250 timespans. 2251 """ 2252 2253 def __init__(self, resolution, values=None): 2254 self.resolution = resolution 2255 self.values = values or [] 2256 2257 def as_timespan(self): 2258 return Timespan(*self.as_limits()) 2259 2260 def as_limits(self): 2261 2262 "Return the earliest and latest points in time for this collection." 2263 2264 if not self.values: 2265 return None, None 2266 else: 2267 first, last = self.values[0], self.values[-1] 2268 if isinstance(first, ActsAsTimespan): 2269 first = first.as_timespan().start 2270 if isinstance(last, ActsAsTimespan): 2271 last = last.as_timespan().end 2272 return first, last 2273 2274 def convert(self, value): 2275 if isinstance(value, ActsAsTimespan): 2276 ts = value.as_timespan() 2277 return ts and ts.convert(self.resolution) 2278 else: 2279 return value.convert(self.resolution) 2280 2281 def __iter__(self): 2282 return iter(self.values) 2283 2284 def __len__(self): 2285 return len(self.values) 2286 2287 def __getitem__(self, i): 2288 return self.values[i] 2289 2290 def __setitem__(self, i, value): 2291 self.values[i] = value 2292 2293 def __contains__(self, value): 2294 test_value = self.convert(value) 2295 return test_value in self.values 2296 2297 def append(self, value): 2298 self.values.append(value) 2299 2300 def insert(self, i, value): 2301 self.values.insert(i, value) 2302 2303 def pop(self): 2304 return self.values.pop() 2305 2306 def insert_in_order(self, value): 2307 bisect.insort_left(self, value) 2308 2309 def getCountry(s): 2310 2311 "Find a country code in the given string 's'." 2312 2313 match = country_code_regexp.search(s) 2314 2315 if match: 2316 return match.group("code") 2317 else: 2318 return None 2319 2320 def getDate(s): 2321 2322 "Parse the string 's', extracting and returning a date object." 2323 2324 dt = getDateTime(s) 2325 if dt is not None: 2326 return dt.as_date() 2327 else: 2328 return None 2329 2330 def getDateTime(s): 2331 2332 """ 2333 Parse the string 's', extracting and returning a datetime object where time 2334 information has been given or a date object where time information is 2335 absent. 2336 """ 2337 2338 m = datetime_regexp.search(s) 2339 if m: 2340 groups = list(m.groups()) 2341 2342 # Convert date and time data to integer or None. 2343 2344 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date() 2345 else: 2346 return None 2347 2348 def getDateFromCalendar(s): 2349 2350 """ 2351 Parse the iCalendar format string 's', extracting and returning a date 2352 object. 2353 """ 2354 2355 dt = getDateTimeFromCalendar(s) 2356 if dt is not None: 2357 return dt.as_date() 2358 else: 2359 return None 2360 2361 def getDateTimeFromCalendar(s): 2362 2363 """ 2364 Parse the iCalendar format datetime string 's', extracting and returning a 2365 datetime object where time information has been given or a date object where 2366 time information is absent. 2367 """ 2368 2369 m = datetime_icalendar_regexp.search(s) 2370 if m: 2371 groups = list(m.groups()) 2372 2373 # Convert date and time data to integer or None. 2374 2375 return DateTime(map(int_or_none, groups[:6]) + [m.group("utc") and "UTC" or None]).as_datetime_or_date() 2376 else: 2377 return None 2378 2379 def getDateStrings(s): 2380 2381 "Parse the string 's', extracting and returning all date strings." 2382 2383 start = 0 2384 m = date_regexp.search(s, start) 2385 l = [] 2386 while m: 2387 l.append("-".join(m.groups())) 2388 m = date_regexp.search(s, m.end()) 2389 return l 2390 2391 def getMonth(s): 2392 2393 "Parse the string 's', extracting and returning a month object." 2394 2395 m = month_regexp.search(s) 2396 if m: 2397 return Month(map(int, m.groups())) 2398 else: 2399 return None 2400 2401 def getCurrentDate(): 2402 2403 "Return the current date as a (year, month, day) tuple." 2404 2405 today = datetime.date.today() 2406 return Date((today.year, today.month, today.day)) 2407 2408 def getCurrentMonth(): 2409 2410 "Return the current month as a (year, month) tuple." 2411 2412 today = datetime.date.today() 2413 return Month((today.year, today.month)) 2414 2415 def getCurrentYear(): 2416 2417 "Return the current year." 2418 2419 today = datetime.date.today() 2420 return today.year 2421 2422 # Location-related functions. 2423 2424 class Reference: 2425 2426 "A map reference." 2427 2428 def __init__(self, degrees, minutes=0, seconds=0): 2429 self.degrees = degrees 2430 self.minutes = minutes 2431 self.seconds = seconds 2432 2433 def __repr__(self): 2434 return "Reference(%d, %d, %f)" % (self.degrees, self.minutes, self.seconds) 2435 2436 def __str__(self): 2437 return "%d:%d:%f" % (self.degrees, self.minutes, self.seconds) 2438 2439 def __add__(self, other): 2440 if not isinstance(other, Reference): 2441 return NotImplemented 2442 else: 2443 s = sign(self.degrees) 2444 o = sign(other.degrees) 2445 carry, seconds = adc(s * self.seconds, o * other.seconds) 2446 carry, minutes = adc(s * self.minutes, o * other.minutes + carry) 2447 return Reference(self.degrees + other.degrees + carry, minutes, seconds) 2448 2449 def __sub__(self, other): 2450 if not isinstance(other, Reference): 2451 return NotImplemented 2452 else: 2453 return self.__add__(Reference(-other.degrees, other.minutes, other.seconds)) 2454 2455 def _compare(self, op, other): 2456 if not isinstance(other, Reference): 2457 return NotImplemented 2458 else: 2459 return op(self.to_degrees(), other.to_degrees()) 2460 2461 def __eq__(self, other): 2462 return self._compare(operator.eq, other) 2463 2464 def __ne__(self, other): 2465 return self._compare(operator.ne, other) 2466 2467 def __lt__(self, other): 2468 return self._compare(operator.lt, other) 2469 2470 def __le__(self, other): 2471 return self._compare(operator.le, other) 2472 2473 def __gt__(self, other): 2474 return self._compare(operator.gt, other) 2475 2476 def __ge__(self, other): 2477 return self._compare(operator.ge, other) 2478 2479 def to_degrees(self): 2480 return sign(self.degrees) * (abs(self.degrees) + self.minutes / 60.0 + self.seconds / 3600.0) 2481 2482 def to_pixels(self, scale): 2483 return self.to_degrees() * scale 2484 2485 def adc(x, y): 2486 result = x + y 2487 return divmod(result, 60) 2488 2489 def getPositionForReference(latitude, longitude, map_y, map_x, map_x_scale, map_y_scale): 2490 return (longitude - map_x).to_pixels(map_x_scale), (latitude - map_y).to_pixels(map_y_scale) 2491 2492 def getPositionForCentrePoint(position, map_x_scale, map_y_scale): 2493 x, y = position 2494 return x - map_x_scale / 2.0, y - map_y_scale / 2.0 2495 2496 def getMapReference(value): 2497 2498 "Return a map reference by parsing the given 'value'." 2499 2500 if value.find(":") != -1: 2501 return getMapReferenceFromDMS(value) 2502 else: 2503 return getMapReferenceFromDecimal(value) 2504 2505 def getMapReferenceFromDMS(value): 2506 2507 """ 2508 Return a map reference by parsing the given 'value' expressed as degrees, 2509 minutes, seconds. 2510 """ 2511 2512 values = value.split(":") 2513 values = map(int, values[:2]) + map(float, values[2:3]) 2514 return Reference(*values) 2515 2516 def getMapReferenceFromDecimal(value): 2517 2518 "Return a map reference by parsing the given 'value' in decimal degrees." 2519 2520 value = float(value) 2521 degrees, remainder = divmod(abs(value * 3600), 3600) 2522 minutes, seconds = divmod(remainder, 60) 2523 return Reference(sign(value) * degrees, minutes, seconds) 2524 2525 # User interface functions. 2526 2527 def getParameter(request, name, default=None): 2528 2529 """ 2530 Using the given 'request', return the value of the parameter with the given 2531 'name', returning the optional 'default' (or None) if no value was supplied 2532 in the 'request'. 2533 """ 2534 2535 return get_form(request).get(name, [default])[0] 2536 2537 def getQualifiedParameter(request, calendar_name, argname, default=None): 2538 2539 """ 2540 Using the given 'request', 'calendar_name' and 'argname', retrieve the 2541 value of the qualified parameter, returning the optional 'default' (or None) 2542 if no value was supplied in the 'request'. 2543 """ 2544 2545 argname = getQualifiedParameterName(calendar_name, argname) 2546 return getParameter(request, argname, default) 2547 2548 def getQualifiedParameterName(calendar_name, argname): 2549 2550 """ 2551 Return the qualified parameter name using the given 'calendar_name' and 2552 'argname'. 2553 """ 2554 2555 if calendar_name is None: 2556 return argname 2557 else: 2558 return "%s-%s" % (calendar_name, argname) 2559 2560 def getParameterDate(arg): 2561 2562 "Interpret 'arg', recognising keywords and simple arithmetic operations." 2563 2564 n = None 2565 2566 if arg is None: 2567 return None 2568 2569 elif arg.startswith("current"): 2570 date = getCurrentDate() 2571 if len(arg) > 8: 2572 n = int(arg[7:]) 2573 2574 elif arg.startswith("yearstart"): 2575 date = Date((getCurrentYear(), 1, 1)) 2576 if len(arg) > 10: 2577 n = int(arg[9:]) 2578 2579 elif arg.startswith("yearend"): 2580 date = Date((getCurrentYear(), 12, 31)) 2581 if len(arg) > 8: 2582 n = int(arg[7:]) 2583 2584 else: 2585 date = getDate(arg) 2586 2587 if n is not None: 2588 date = date.day_update(n) 2589 2590 return date 2591 2592 def getParameterMonth(arg): 2593 2594 "Interpret 'arg', recognising keywords and simple arithmetic operations." 2595 2596 n = None 2597 2598 if arg is None: 2599 return None 2600 2601 elif arg.startswith("current"): 2602 date = getCurrentMonth() 2603 if len(arg) > 8: 2604 n = int(arg[7:]) 2605 2606 elif arg.startswith("yearstart"): 2607 date = Month((getCurrentYear(), 1)) 2608 if len(arg) > 10: 2609 n = int(arg[9:]) 2610 2611 elif arg.startswith("yearend"): 2612 date = Month((getCurrentYear(), 12)) 2613 if len(arg) > 8: 2614 n = int(arg[7:]) 2615 2616 else: 2617 date = getMonth(arg) 2618 2619 if n is not None: 2620 date = date.month_update(n) 2621 2622 return date 2623 2624 def getFormDate(request, calendar_name, argname): 2625 2626 """ 2627 Return the date from the 'request' for the calendar with the given 2628 'calendar_name' using the parameter having the given 'argname'. 2629 """ 2630 2631 arg = getQualifiedParameter(request, calendar_name, argname) 2632 return getParameterDate(arg) 2633 2634 def getFormMonth(request, calendar_name, argname): 2635 2636 """ 2637 Return the month from the 'request' for the calendar with the given 2638 'calendar_name' using the parameter having the given 'argname'. 2639 """ 2640 2641 arg = getQualifiedParameter(request, calendar_name, argname) 2642 return getParameterMonth(arg) 2643 2644 def getFormDateTriple(request, yeararg, montharg, dayarg): 2645 2646 """ 2647 Return the date from the 'request' for the calendar with the given 2648 'calendar_name' using the parameters having the given 'yeararg', 'montharg' 2649 and 'dayarg' names. 2650 """ 2651 2652 year = getParameter(request, yeararg) 2653 month = getParameter(request, montharg) 2654 day = getParameter(request, dayarg) 2655 if year and month and day: 2656 return Date((int(year), int(month), int(day))) 2657 else: 2658 return None 2659 2660 def getFormMonthPair(request, yeararg, montharg): 2661 2662 """ 2663 Return the month from the 'request' for the calendar with the given 2664 'calendar_name' using the parameters having the given 'yeararg' and 2665 'montharg' names. 2666 """ 2667 2668 year = getParameter(request, yeararg) 2669 month = getParameter(request, montharg) 2670 if year and month: 2671 return Month((int(year), int(month))) 2672 else: 2673 return None 2674 2675 def getFullDateLabel(request, date): 2676 2677 """ 2678 Return the full month plus year label using the given 'request' and 2679 'year_month'. 2680 """ 2681 2682 if not date: 2683 return "" 2684 2685 _ = request.getText 2686 year, month, day = date.as_tuple()[:3] 2687 start_weekday, number_of_days = date.month_properties() 2688 weekday = (start_weekday + day - 1) % 7 2689 day_label = _(getDayLabel(weekday)) 2690 month_label = _(getMonthLabel(month)) 2691 return "%s %s %s %s" % (day_label, day, month_label, year) 2692 2693 def getFullMonthLabel(request, year_month): 2694 2695 """ 2696 Return the full month plus year label using the given 'request' and 2697 'year_month'. 2698 """ 2699 2700 if not year_month: 2701 return "" 2702 2703 _ = request.getText 2704 year, month = year_month.as_tuple()[:2] 2705 month_label = _(getMonthLabel(month)) 2706 return "%s %s" % (month_label, year) 2707 2708 # Page-related functions. 2709 2710 def getPrettyPageName(page): 2711 2712 "Return a nicely formatted title/name for the given 'page'." 2713 2714 title = page.split_title(force=1) 2715 return getPrettyTitle(title) 2716 2717 def linkToPage(request, page, text, query_string=None): 2718 2719 """ 2720 Using 'request', return a link to 'page' with the given link 'text' and 2721 optional 'query_string'. 2722 """ 2723 2724 text = wikiutil.escape(text) 2725 return page.link_to_raw(request, text, query_string) 2726 2727 def linkToResource(url, request, text, query_string=None): 2728 2729 """ 2730 Using 'request', return a link to 'url' with the given link 'text' and 2731 optional 'query_string'. 2732 """ 2733 2734 if query_string: 2735 query_string = wikiutil.makeQueryString(query_string) 2736 url = "%s?%s" % (url, query_string) 2737 2738 formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter 2739 2740 output = [] 2741 output.append(formatter.url(1, url)) 2742 output.append(formatter.text(text)) 2743 output.append(formatter.url(0)) 2744 return "".join(output) 2745 2746 def getFullPageName(parent, title): 2747 2748 """ 2749 Return a full page name from the given 'parent' page (can be empty or None) 2750 and 'title' (a simple page name). 2751 """ 2752 2753 if parent: 2754 return "%s/%s" % (parent.rstrip("/"), title) 2755 else: 2756 return title 2757 2758 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 2759 2760 """ 2761 Using the given 'template_page', complete the 'new_page' by copying the 2762 template and adding the given 'event_details' (a dictionary of event 2763 fields), setting also the 'category_pagenames' to define category 2764 membership. 2765 """ 2766 2767 event_page = EventPage(template_page) 2768 new_event_page = EventPage(new_page) 2769 new_event_page.copyPage(event_page) 2770 2771 if new_event_page.getFormat() == "wiki": 2772 new_event = Event(new_event_page, event_details) 2773 new_event_page.setEvents([new_event]) 2774 new_event_page.setCategoryMembership(category_pagenames) 2775 new_event_page.flushEventDetails() 2776 2777 return new_event_page.getBody() 2778 2779 # vim: tabstop=4 expandtab shiftwidth=4