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