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