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 import search, version 13 from MoinMoin import wikiutil 14 import calendar 15 import datetime 16 import time 17 import re 18 import bisect 19 import operator 20 21 try: 22 set 23 except NameError: 24 from sets import Set as set 25 26 try: 27 import pytz 28 except ImportError: 29 pytz = None 30 31 __version__ = "0.7" 32 33 # Date labels. 34 35 month_labels = ["January", "February", "March", "April", "May", "June", 36 "July", "August", "September", "October", "November", "December"] 37 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 38 39 # Regular expressions where MoinMoin does not provide the required support. 40 41 category_regexp = None 42 43 # Page parsing. 44 45 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 46 category_membership_regexp = re.compile(ur"^\s*((Category\S+)(\s+Category\S+)*)\s*$", re.MULTILINE | re.UNICODE) 47 48 # Value parsing. 49 50 country_code_regexp = re.compile(ur'(?:^|\W)(?P<code>[A-Z]{2})(?:$|\W+$)', re.UNICODE) 51 52 month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})' 53 date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})' 54 time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?' 55 timezone_offset_str = ur'(?P<offset>(UTC)?(?:(?P<sign>[-+])(?P<hours>[0-9]{2})(?::?(?P<minutes>[0-9]{2}))?))' 56 timezone_olson_str = ur'(?P<olson>[a-zA-Z]+(?:/[-_a-zA-Z]+){1,2})' 57 timezone_utc_str = ur'UTC' 58 timezone_regexp_str = ur'(?P<zone>' + timezone_offset_str + '|' + timezone_olson_str + '|' + timezone_utc_str + ')' 59 datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?' 60 61 month_regexp = re.compile(month_regexp_str, re.UNICODE) 62 date_regexp = re.compile(date_regexp_str, re.UNICODE) 63 time_regexp = re.compile(time_regexp_str, re.UNICODE) 64 datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE) 65 timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE) 66 timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE) 67 68 verbatim_regexp = re.compile(ur'(?:' 69 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 70 ur'|' 71 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 72 ur'|' 73 ur'`(?P<monospace>.*?)`' 74 ur'|' 75 ur'{{{(?P<preformatted>.*?)}}}' 76 ur')', re.UNICODE) 77 78 # Utility functions. 79 80 def getCategoryPattern(request): 81 global category_regexp 82 83 try: 84 return request.cfg.cache.page_category_regexact 85 except AttributeError: 86 87 # Use regular expression from MoinMoin 1.7.1 otherwise. 88 89 if category_regexp is None: 90 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 91 return category_regexp 92 93 def int_or_none(x): 94 if x is None: 95 return x 96 else: 97 return int(x) 98 99 def sort_none_first(x, y): 100 if x is None: 101 return -1 102 elif y is None: 103 return 1 104 else: 105 return cmp(x, y) 106 107 # Utility classes and associated functions. 108 109 class Form: 110 111 """ 112 A wrapper preserving MoinMoin 1.8.x (and earlier) behaviour in a 1.9.x 113 environment. 114 """ 115 116 def __init__(self, form): 117 self.form = form 118 119 def get(self, name, default=None): 120 values = self.form.getlist(name) 121 if not values: 122 return default 123 else: 124 return values 125 126 def __getitem__(self, name): 127 return self.form.getlist(name) 128 129 class ActionSupport: 130 131 """ 132 Work around disruptive MoinMoin changes in 1.9, and also provide useful 133 convenience methods. 134 """ 135 136 def get_form(self): 137 return get_form(self.request) 138 139 def _get_selected(self, value, input_value): 140 141 """ 142 Return the HTML attribute text indicating selection of an option (or 143 otherwise) if 'value' matches 'input_value'. 144 """ 145 146 return input_value is not None and value == input_value and 'selected="selected"' or '' 147 148 def _get_selected_for_list(self, value, input_values): 149 150 """ 151 Return the HTML attribute text indicating selection of an option (or 152 otherwise) if 'value' matches one of the 'input_values'. 153 """ 154 155 return value in input_values and 'selected="selected"' or '' 156 157 def _get_input(self, form, name, default=None): 158 159 """ 160 Return the input from 'form' having the given 'name', returning either 161 the input converted to an integer or the given 'default' (optional, None 162 if not specified). 163 """ 164 165 value = form.get(name, [None])[0] 166 if not value: # true if 0 obtained 167 return default 168 else: 169 return int(value) 170 171 def get_month_lists(self, default_as_current=0): 172 173 """ 174 Return two lists of HTML element definitions corresponding to the start 175 and end month selection controls, with months selected according to any 176 values that have been specified via request parameters. 177 """ 178 179 _ = self._ 180 form = self.get_form() 181 182 # Initialise month lists. 183 184 start_month_list = [] 185 end_month_list = [] 186 187 start_month = self._get_input(form, "start-month", default_as_current and getCurrentMonth().month() or None) 188 end_month = self._get_input(form, "end-month", start_month) 189 190 # Prepare month lists, selecting specified months. 191 192 if not default_as_current: 193 start_month_list.append('<option value=""></option>') 194 end_month_list.append('<option value=""></option>') 195 196 for month in range(1, 13): 197 month_label = _(getMonthLabel(month)) 198 selected = self._get_selected(month, start_month) 199 start_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 200 selected = self._get_selected(month, end_month) 201 end_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 202 203 return start_month_list, end_month_list 204 205 def get_year_defaults(self, default_as_current=0): 206 207 "Return defaults for the start and end years." 208 209 form = self.get_form() 210 211 start_year_default = form.get("start-year", [default_as_current and getCurrentYear() or ""])[0] 212 end_year_default = form.get("end-year", [default_as_current and start_year_default or ""])[0] 213 214 return start_year_default, end_year_default 215 216 def get_day_defaults(self, default_as_current=0): 217 218 "Return defaults for the start and end days." 219 220 form = self.get_form() 221 222 start_day_default = form.get("start-day", [default_as_current and getCurrentDate().day() or ""])[0] 223 end_day_default = form.get("end-day", [default_as_current and start_day_default or ""])[0] 224 225 return start_day_default, end_day_default 226 227 def get_form(request): 228 229 "Work around disruptive MoinMoin changes in 1.9." 230 231 if hasattr(request, "values"): 232 return Form(request.values) 233 else: 234 return request.form 235 236 class send_headers: 237 238 """ 239 A wrapper to preserve MoinMoin 1.8.x (and earlier) request behaviour in a 240 1.9.x environment. 241 """ 242 243 def __init__(self, request): 244 self.request = request 245 246 def __call__(self, headers): 247 for header in headers: 248 parts = header.split(":") 249 self.request.headers.add(parts[0], ":".join(parts[1:])) 250 251 # Textual representations. 252 253 def getHTTPTimeString(tmtuple): 254 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( 255 weekday_labels[tmtuple.tm_wday], 256 tmtuple.tm_mday, 257 month_labels[tmtuple.tm_mon -1], # zero-based labels 258 tmtuple.tm_year, 259 tmtuple.tm_hour, 260 tmtuple.tm_min, 261 tmtuple.tm_sec 262 ) 263 264 def getSimpleWikiText(text): 265 266 """ 267 Return the plain text representation of the given 'text' which may employ 268 certain Wiki syntax features, such as those providing verbatim or monospaced 269 text. 270 """ 271 272 # NOTE: Re-implementing support for verbatim text and linking avoidance. 273 274 return "".join([s for s in verbatim_regexp.split(text) if s is not None]) 275 276 def getEncodedWikiText(text): 277 278 "Encode the given 'text' in a verbatim representation." 279 280 return "<<Verbatim(%s)>>" % text 281 282 def getPrettyTitle(title): 283 284 "Return a nicely formatted version of the given 'title'." 285 286 return title.replace("_", " ").replace("/", u" ? ") 287 288 def getMonthLabel(month): 289 290 "Return an unlocalised label for the given 'month'." 291 292 return month_labels[month - 1] # zero-based labels 293 294 def getDayLabel(weekday): 295 296 "Return an unlocalised label for the given 'weekday'." 297 298 return weekday_labels[weekday] 299 300 # Action support functions. 301 302 def getPageRevision(page): 303 304 "Return the revision details dictionary for the given 'page'." 305 306 # From Page.edit_info... 307 308 if hasattr(page, "editlog_entry"): 309 line = page.editlog_entry() 310 else: 311 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 312 313 # Similar to Page.mtime_usecs behaviour... 314 315 if line: 316 timestamp = line.ed_time_usecs 317 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 318 comment = line.comment 319 else: 320 mtime = 0 321 comment = "" 322 323 return {"timestamp" : time.gmtime(mtime), "comment" : comment} 324 325 # Category discovery and searching. 326 327 def getCategories(request): 328 329 """ 330 From the AdvancedSearch macro, return a list of category page names using 331 the given 'request'. 332 """ 333 334 # This will return all pages with "Category" in the title. 335 336 cat_filter = getCategoryPattern(request).search 337 return request.rootpage.getPageList(filter=cat_filter) 338 339 def getCategoryMapping(category_pagenames, request): 340 341 """ 342 For the given 'category_pagenames' return a list of tuples of the form 343 (category name, category page name) using the given 'request'. 344 """ 345 346 cat_pattern = getCategoryPattern(request) 347 mapping = [] 348 for pagename in category_pagenames: 349 name = cat_pattern.match(pagename).group("key") 350 if name != "Category": 351 mapping.append((name, pagename)) 352 mapping.sort() 353 return mapping 354 355 def getCategoryPages(pagename, request): 356 357 """ 358 Return the pages associated with the given category 'pagename' using the 359 'request'. 360 """ 361 362 query = search.QueryParser().parse_query('category:%s' % pagename) 363 results = search.searchPages(request, query, "page_name") 364 365 cat_pattern = getCategoryPattern(request) 366 pages = [] 367 for page in results.hits: 368 if not cat_pattern.match(page.page_name): 369 pages.append(page) 370 return pages 371 372 # The main activity functions. 373 374 class EventPage: 375 376 "An event page." 377 378 def __init__(self, page): 379 self.page = page 380 self.events = None 381 self.body = None 382 self.categories = None 383 384 def copyPage(self, page): 385 386 "Copy the body of the given 'page'." 387 388 self.body = page.getBody() 389 390 def getPageURL(self, request): 391 392 "Using 'request', return the URL of this page." 393 394 return request.getQualifiedURL(self.page.url(request, relative=0)) 395 396 def getFormat(self): 397 398 "Get the format used on this page." 399 400 return self.page.pi["format"] 401 402 def getRevisions(self): 403 404 "Return a list of page revisions." 405 406 return self.page.getRevList() 407 408 def getPageRevision(self): 409 410 "Return the revision details dictionary for this page." 411 412 return getPageRevision(self.page) 413 414 def getPageName(self): 415 416 "Return the page name." 417 418 return self.page.page_name 419 420 def getPrettyPageName(self): 421 422 "Return a nicely formatted title/name for this page." 423 424 return getPrettyPageName(self.page) 425 426 def getBody(self): 427 428 "Get the current page body." 429 430 if self.body is None: 431 self.body = self.page.get_raw_body() 432 return self.body 433 434 def getEvents(self): 435 436 "Return a list of events from this page." 437 438 if self.events is None: 439 details = {} 440 self.events = [Event(self, details)] 441 442 if self.getFormat() == "wiki": 443 for match in definition_list_regexp.finditer(self.getBody()): 444 445 # Skip commented-out items. 446 447 if match.group("optcomment"): 448 continue 449 450 # Permit case-insensitive list terms. 451 452 term = match.group("term").lower() 453 desc = match.group("desc") 454 455 # Special value type handling. 456 457 # Dates. 458 459 if term in ("start", "end"): 460 desc = getDateTime(desc) 461 462 # Lists (whose elements may be quoted). 463 464 elif term in ("topics", "categories"): 465 desc = [getSimpleWikiText(value.strip()) for value in desc.split(",") if value.strip()] 466 467 # Labels which may well be quoted. 468 469 elif term in ("title", "summary", "description", "location"): 470 desc = getSimpleWikiText(desc) 471 472 if desc is not None: 473 474 # Handle apparent duplicates by creating a new set of 475 # details. 476 477 if details.has_key(term): 478 479 # Make a new event. 480 481 details = {} 482 self.events.append(Event(self, details)) 483 484 details[term] = desc 485 486 return self.events 487 488 def setEvents(self, events): 489 490 "Set the given 'events' on this page." 491 492 self.events = events 493 494 def getCategoryMembership(self): 495 496 "Get the category names from this page." 497 498 if self.categories is None: 499 body = self.getBody() 500 match = category_membership_regexp.search(body) 501 self.categories = match.findall().split() 502 503 return self.categories 504 505 def setCategoryMembership(self, category_names): 506 507 """ 508 Set the category membership for the page using the specified 509 'category_names'. 510 """ 511 512 self.categories = category_names 513 514 def flushEventDetails(self): 515 516 "Flush the current event details to this page's body text." 517 518 new_body_parts = [] 519 end_of_last_match = 0 520 body = self.getBody() 521 522 events = iter(self.getEvents()) 523 524 event = events.next() 525 event_details = event.getDetails() 526 replaced_terms = set() 527 528 for match in definition_list_regexp.finditer(body): 529 530 # Permit case-insensitive list terms. 531 532 term = match.group("term").lower() 533 desc = match.group("desc") 534 535 # Check that the term has not already been substituted. If so, 536 # get the next event. 537 538 if term in replaced_terms: 539 try: 540 event = events.next() 541 542 # No more events. 543 544 except StopIteration: 545 break 546 547 event_details = event.getDetails() 548 replaced_terms = set() 549 550 # Add preceding text to the new body. 551 552 new_body_parts.append(body[end_of_last_match:match.start()]) 553 554 # Get the matching regions, adding the term to the new body. 555 556 new_body_parts.append(match.group("wholeterm")) 557 558 # Special value type handling. 559 560 if event_details.has_key(term): 561 562 # Dates. 563 564 if term in ("start", "end"): 565 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 566 567 # Lists (whose elements may be quoted). 568 569 elif term in ("topics", "categories"): 570 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 571 572 # Labels which must be quoted. 573 574 elif term in ("title", "summary"): 575 desc = getEncodedWikiText(event_details[term]) 576 577 # Text which need not be quoted, but it will be Wiki text. 578 579 elif term in ("description", "link", "location"): 580 desc = event_details[term] 581 582 replaced_terms.add(term) 583 584 # Add the replaced value. 585 586 new_body_parts.append(desc) 587 588 # Remember where in the page has been processed. 589 590 end_of_last_match = match.end() 591 592 # Write the rest of the page. 593 594 new_body_parts.append(body[end_of_last_match:]) 595 596 self.body = "".join(new_body_parts) 597 598 def flushCategoryMembership(self): 599 600 "Flush the category membership to the page body." 601 602 body = self.getBody() 603 category_names = self.getCategoryMembership() 604 match = category_membership_regexp.search(body) 605 606 if match: 607 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 608 609 def saveChanges(self): 610 611 "Save changes to the event." 612 613 self.flushEventDetails() 614 self.flushCategoryMembership() 615 self.page.saveText(self.getBody(), 0) 616 617 def linkToPage(self, request, text, query_string=None): 618 619 """ 620 Using 'request', return a link to this page with the given link 'text' 621 and optional 'query_string'. 622 """ 623 624 return linkToPage(request, self.page, text, query_string) 625 626 class Event: 627 628 "A description of an event." 629 630 def __init__(self, page, details): 631 self.page = page 632 self.details = details 633 634 def __hash__(self): 635 return hash(self.getSummary()) 636 637 def getPage(self): 638 639 "Return the page describing this event." 640 641 return self.page 642 643 def setPage(self, page): 644 645 "Set the 'page' describing this event." 646 647 self.page = page 648 649 def getSummary(self, event_parent=None): 650 651 """ 652 Return either the given title or summary of the event according to the 653 event details, or a summary made from using the pretty version of the 654 page name. 655 656 If the optional 'event_parent' is specified, any page beneath the given 657 'event_parent' page in the page hierarchy will omit this parent information 658 if its name is used as the summary. 659 """ 660 661 event_details = self.details 662 663 if event_details.has_key("title"): 664 return event_details["title"] 665 elif event_details.has_key("summary"): 666 return event_details["summary"] 667 else: 668 # If appropriate, remove the parent details and "/" character. 669 670 title = self.page.getPageName() 671 672 if event_parent and title.startswith(event_parent): 673 title = title[len(event_parent.rstrip("/")) + 1:] 674 675 return getPrettyTitle(title) 676 677 def getDetails(self): 678 679 "Return the details for this event." 680 681 return self.details 682 683 def setDetails(self, event_details): 684 685 "Set the 'event_details' for this event." 686 687 self.details = event_details 688 689 # Timespan-related methods. 690 691 def __contains__(self, other): 692 return self == other 693 694 def __cmp__(self, other): 695 if isinstance(other, Event): 696 return cmp(self.as_timespan(), other.as_timespan()) 697 else: 698 return cmp(self.as_timespan(), other) 699 700 def as_timespan(self): 701 details = self.details 702 if details.has_key("start") and details.has_key("end"): 703 return Timespan(details["start"], details["end"]) 704 else: 705 return None 706 707 def as_limits(self): 708 return self.as_timespan().as_limits() 709 710 def getEvents(request, category_names, calendar_start=None, calendar_end=None, resolution="month"): 711 712 """ 713 Using the 'request', generate a list of events found on pages belonging to 714 the specified 'category_names', using the optional 'calendar_start' and 715 'calendar_end' values to indicate a window of interest. 716 717 The optional 'resolution' determines the unit of time used in providing the 718 results: 719 720 * a list of events 721 * a dictionary mapping time units to event lists (within the window of 722 interest), usable as a kind of index to groups of events 723 * a list of all events within the window of interest 724 * the earliest time value of an event within the window of interest 725 * the latest time value of an event within the window of interest. 726 """ 727 728 # Dates need to comply with the requested resolution. 729 # Here, None values need to be preserved when converting. 730 731 if resolution == "month": 732 convert = lambda x: x and x.as_month() 733 get_values = lambda x, y: x.months_until(y) 734 else: 735 convert = lambda x: x and x.as_date() 736 get_values = lambda x, y: x.days_until(y) 737 738 # Re-order the window, if appropriate. 739 740 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 741 calendar_start, calendar_end = map(convert, (calendar_end, calendar_start)) 742 743 # Otherwise, just convert the calendar limits. 744 745 else: 746 calendar_start, calendar_end = map(convert, (calendar_start, calendar_end)) 747 748 calendar_period = Timespan(calendar_start, calendar_end) 749 750 events = [] 751 shown_events = {} 752 all_shown_events = [] 753 processed_pages = set() 754 755 earliest = None 756 latest = None 757 758 for category_name in category_names: 759 760 # Get the pages and page names in the category. 761 762 pages_in_category = getCategoryPages(category_name, request) 763 764 # Visit each page in the category. 765 766 for page_in_category in pages_in_category: 767 pagename = page_in_category.page_name 768 769 # Only process each page once. 770 771 if pagename in processed_pages: 772 continue 773 else: 774 processed_pages.add(pagename) 775 776 # Get a real page, not a result page. 777 778 event_page = EventPage(Page(request, pagename)) 779 780 # Get all events described in the page. 781 782 for event in event_page.getEvents(): 783 event_details = event.getDetails() 784 785 # Remember the event. 786 787 events.append(event) 788 789 # Test for the suitability of the event. 790 791 if event.as_timespan() is not None: 792 start, end = map(convert, event.as_timespan().as_limits()) 793 794 # Compare the dates to the requested calendar window, if any. 795 796 if event in calendar_period: 797 798 all_shown_events.append(event) 799 800 if earliest is None or start < earliest: 801 earliest = start 802 if latest is None or end > latest: 803 latest = end 804 805 # Store the event in the time-specific dictionary. 806 807 first = max(start, calendar_start or start) 808 last = min(end, calendar_end or end) 809 810 for event_time_value in get_values(first, last): 811 if not shown_events.has_key(event_time_value): 812 shown_events[event_time_value] = [] 813 shown_events[event_time_value].append(event) 814 815 return events, shown_events, all_shown_events, earliest, latest 816 817 def setEventTimestamps(request, events): 818 819 """ 820 Using 'request', set timestamp details in the details dictionary of each of 821 the 'events'. 822 823 Return the latest timestamp found. 824 """ 825 826 latest = None 827 828 for event in events: 829 event_details = event.getDetails() 830 event_page = event.getPage() 831 832 # Get the initial revision of the page. 833 834 revisions = event_page.getRevisions() 835 event_page_initial = Page(request, event_page.getPageName(), rev=revisions[-1]) 836 837 # Get the created and last modified times. 838 839 initial_revision = getPageRevision(event_page_initial) 840 event_details["created"] = initial_revision["timestamp"] 841 latest_revision = event_page.getPageRevision() 842 event_details["last-modified"] = latest_revision["timestamp"] 843 event_details["sequence"] = len(revisions) - 1 844 event_details["last-comment"] = latest_revision["comment"] 845 846 if latest is None or latest < event_details["last-modified"]: 847 latest = event_details["last-modified"] 848 849 return latest 850 851 def getOrderedEvents(events): 852 853 """ 854 Return a list with the given 'events' ordered according to their start and 855 end dates. 856 """ 857 858 ordered_events = events[:] 859 ordered_events.sort() 860 return ordered_events 861 862 def getConcretePeriod(calendar_start, calendar_end, earliest, latest): 863 864 """ 865 From the requested 'calendar_start' and 'calendar_end', which may be None, 866 indicating that no restriction is imposed on the period for each of the 867 boundaries, use the 'earliest' and 'latest' event months to define a 868 specific period of interest. 869 """ 870 871 # Define the period as starting with any specified start month or the 872 # earliest event known, ending with any specified end month or the latest 873 # event known. 874 875 first = calendar_start or earliest 876 last = calendar_end or latest 877 878 # If there is no range of months to show, perhaps because there are no 879 # events in the requested period, and there was no start or end month 880 # specified, show only the month indicated by the start or end of the 881 # requested period. If all events were to be shown but none were found show 882 # the current month. 883 884 if isinstance(first, Date): 885 get_current = getCurrentDate 886 else: 887 get_current = getCurrentMonth 888 889 if first is None: 890 first = last or get_current() 891 if last is None: 892 last = first or get_current() 893 894 # Permit "expiring" periods (where the start date approaches the end date). 895 896 return min(first, last), last 897 898 def getCoverage(start, end, events, resolution="date"): 899 900 """ 901 Within the period defined by the 'start' and 'end' dates, determine the 902 coverage of the days in the period by the given 'events', returning a 903 collection of timespans, along with a dictionary mapping locations to 904 collections of slots, where each slot contains a tuple of the form 905 (timespans, events). 906 """ 907 908 all_events = {} 909 full_coverage = TimespanCollection(resolution) 910 coverage_period = full_coverage.convert(Timespan(start, end)) 911 912 # Get event details. 913 914 for event in events: 915 event_details = event.getDetails() 916 917 # Test for the event in the period. 918 919 if event in coverage_period: 920 921 # Find the coverage of this period for the event. 922 923 event_location = event_details.get("location") 924 925 # Update the overall coverage. 926 927 full_coverage.insert_in_order(event) 928 929 # Add a new events list for a new location. 930 # Locations can be unspecified, thus None refers to all unlocalised 931 # events. 932 933 if not all_events.has_key(event_location): 934 all_events[event_location] = [TimespanCollection(resolution, [event])] 935 936 # Try and fit the event into an events list. 937 938 else: 939 slot = all_events[event_location] 940 941 for slot_events in slot: 942 943 # Where the event does not overlap with the current 944 # element, add it alongside existing events. 945 # NOTE: Need to use the resolution when testing for overlaps. 946 947 if not event in slot_events: 948 slot_events.insert_in_order(event) 949 break 950 951 # Make a new element in the list if the event cannot be 952 # marked alongside existing events. 953 954 else: 955 slot.append(TimespanCollection(resolution, [event])) 956 957 return full_coverage, all_events 958 959 def getCoverageScale(coverage): 960 961 """ 962 Return a scale for the given coverage so that the times involved are 963 exposed. The scale consists of a list of non-overlapping timespans forming 964 a contiguous period of time. 965 """ 966 967 times = set() 968 for timespan in coverage: 969 start, end = timespan.as_limits() 970 971 # Add either genuine times or dates converted to times. 972 973 if isinstance(start, DateTime): 974 times.add(start) 975 976 if isinstance(end, DateTime): 977 if end.has_time(): 978 times.add(end) 979 else: 980 times.add(end.as_date().next_day()) 981 982 times = list(times) 983 times.sort() 984 985 scale = [] 986 first = 1 987 start = None 988 for time in times: 989 if not first: 990 scale.append(Timespan(start, time)) 991 else: 992 first = 0 993 start = time 994 995 return scale 996 997 # Date-related functions. 998 999 class Period: 1000 1001 "A simple period of time." 1002 1003 def __init__(self, data): 1004 self.data = data 1005 1006 def months(self): 1007 return self.data[0] * 12 + self.data[1] 1008 1009 class Temporal: 1010 1011 "A simple temporal representation, common to dates and times." 1012 1013 def __init__(self, data): 1014 self.data = list(data) 1015 1016 def __repr__(self): 1017 return "%s(%r)" % (self.__class__.__name__, self.data) 1018 1019 def __hash__(self): 1020 return hash(self.as_tuple()) 1021 1022 def as_tuple(self): 1023 return tuple(self.data) 1024 1025 def __cmp__(self, other): 1026 if not isinstance(other, Temporal): 1027 return NotImplemented 1028 else: 1029 data = self.as_tuple() 1030 other_data = other.as_tuple() 1031 length = min(len(data), len(other_data)) 1032 return cmp(data[:length], other_data[:length]) 1033 1034 def until(self, start, end, nextfn, prevfn): 1035 1036 """ 1037 Return a collection of units of time by starting from the given 'start' 1038 and stepping across intervening units until 'end' is reached, using the 1039 given 'nextfn' and 'prevfn' to step from one unit to the next. 1040 """ 1041 1042 current = start 1043 units = [current] 1044 if current < end: 1045 while current < end: 1046 current = nextfn(current) 1047 units.append(current) 1048 elif current > end: 1049 while current > end: 1050 current = prevfn(current) 1051 units.append(current) 1052 return units 1053 1054 class Month(Temporal): 1055 1056 "A simple year-month representation." 1057 1058 def __str__(self): 1059 return "%04d-%02d" % self.as_tuple()[:2] 1060 1061 def as_datetime(self, day, hour, minute, second, zone): 1062 return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) 1063 1064 def as_date(self, day): 1065 return Date(self.as_tuple() + (day,)) 1066 1067 def as_month(self): 1068 return self 1069 1070 def year(self): 1071 return self.data[0] 1072 1073 def month(self): 1074 return self.data[1] 1075 1076 def month_properties(self): 1077 1078 """ 1079 Return the weekday of the 1st of the month, along with the number of 1080 days, as a tuple. 1081 """ 1082 1083 year, month = self.as_tuple()[:2] 1084 return calendar.monthrange(year, month) 1085 1086 def month_update(self, n=1): 1087 1088 "Return the month updated by 'n' months." 1089 1090 year, month = self.as_tuple()[:2] 1091 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1)) 1092 1093 def next_month(self): 1094 1095 "Return the month following this one." 1096 1097 return self.month_update(1) 1098 1099 def previous_month(self): 1100 1101 "Return the month preceding this one." 1102 1103 return self.month_update(-1) 1104 1105 def __sub__(self, start): 1106 1107 """ 1108 Return the difference in years and months between this month and the 1109 'start' month as a period. 1110 """ 1111 1112 return Period([(x - y) for x, y in zip(self.data, start.data)]) 1113 1114 def months_until(self, end): 1115 1116 "Return the collection of months from this month until 'end'." 1117 1118 return self.until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month) 1119 1120 class Date(Month): 1121 1122 "A simple year-month-day representation." 1123 1124 def constrain(self): 1125 year, month, day = self.as_tuple()[:3] 1126 1127 month = max(min(month, 12), 1) 1128 wd, last_day = calendar.monthrange(year, month) 1129 day = max(min(day, last_day), 1) 1130 1131 self.data[1:3] = month, day 1132 1133 def __str__(self): 1134 return "%04d-%02d-%02d" % self.as_tuple()[:3] 1135 1136 def as_datetime(self, hour, minute, second, zone): 1137 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 1138 1139 def as_date(self): 1140 return self 1141 1142 def as_month(self): 1143 return Month(self.data[:2]) 1144 1145 def day(self): 1146 return self.data[2] 1147 1148 def day_update(self, n=1): 1149 1150 "Return the month updated by 'n' months." 1151 1152 delta = datetime.timedelta(n) 1153 dt = datetime.date(*self.as_tuple()[:3]) 1154 dt_new = dt + delta 1155 return Date((dt_new.year, dt_new.month, dt_new.day)) 1156 1157 def next_day(self): 1158 1159 "Return the date following this one." 1160 1161 year, month, day = self.as_tuple()[:3] 1162 _wd, end_day = calendar.monthrange(year, month) 1163 if day == end_day: 1164 if month == 12: 1165 return Date((year + 1, 1, 1)) 1166 else: 1167 return Date((year, month + 1, 1)) 1168 else: 1169 return Date((year, month, day + 1)) 1170 1171 def previous_day(self): 1172 1173 "Return the date preceding this one." 1174 1175 year, month, day = self.as_tuple()[:3] 1176 if day == 1: 1177 if month == 1: 1178 return Date((year - 1, 12, 31)) 1179 else: 1180 _wd, end_day = calendar.monthrange(year, month - 1) 1181 return Date((year, month - 1, end_day)) 1182 else: 1183 return Date((year, month, day - 1)) 1184 1185 def days_until(self, end): 1186 1187 "Return the collection of days from this date until 'end'." 1188 1189 return self.until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 1190 1191 class DateTime(Date): 1192 1193 "A simple date plus time representation." 1194 1195 def constrain(self): 1196 Date.constrain(self) 1197 1198 hour, minute, second = self.as_tuple()[3:6] 1199 1200 if self.has_time(): 1201 hour = max(min(hour, 23), 0) 1202 minute = max(min(minute, 59), 0) 1203 1204 if second is not None: 1205 second = max(min(second, 60), 0) # support leap seconds 1206 1207 self.data[3:6] = hour, minute, second 1208 1209 def __str__(self): 1210 return Date.__str__(self) + self.time_string() 1211 1212 def time_string(self): 1213 if self.has_time(): 1214 data = self.as_tuple() 1215 time_str = " %02d:%02d" % data[3:5] 1216 if data[5] is not None: 1217 time_str += ":%02d" % data[5] 1218 if data[6] is not None: 1219 time_str += " %s" % data[6] 1220 return time_str 1221 else: 1222 return "" 1223 1224 def as_datetime(self): 1225 return self 1226 1227 def as_date(self): 1228 return Date(self.data[:3]) 1229 1230 def as_datetime_or_date(self): 1231 1232 """ 1233 Return a date for this datetime if fields are missing. Otherwise, return 1234 this datetime itself. 1235 """ 1236 1237 if not self.has_time(): 1238 return self.as_date() 1239 else: 1240 return self 1241 1242 def __cmp__(self, other): 1243 this = self.as_datetime_or_date() 1244 1245 if isinstance(this, DateTime) and isinstance(other, DateTime): 1246 other = other.as_datetime_or_date() 1247 if isinstance(other, DateTime): 1248 this_utc = this.to_utc() 1249 other_utc = other.to_utc() 1250 if this_utc is not None and other_utc is not None: 1251 return cmp(this_utc.as_tuple(), other_utc.as_tuple()) 1252 1253 return Date.__cmp__(this, other) 1254 1255 def has_time(self): 1256 return self.data[3] is not None and self.data[4] is not None 1257 1258 def time(self): 1259 return self.data[3:] 1260 1261 def seconds(self): 1262 return self.data[5] 1263 1264 def time_zone(self): 1265 return self.data[6] 1266 1267 def set_time_zone(self, value): 1268 self.data[6] = value 1269 1270 def padded(self): 1271 1272 "Return a datetime with missing fields defined as being zero." 1273 1274 data = map(lambda x: x or 0, self.data[:6]) + self.data[6:] 1275 return DateTime(data) 1276 1277 def to_utc(self): 1278 1279 """ 1280 Return this object converted to UTC, or None if such a conversion is not 1281 defined. 1282 """ 1283 1284 if not self.has_time(): 1285 return None 1286 1287 offset = self.utc_offset() 1288 if offset: 1289 hours, minutes = offset 1290 1291 # Invert the offset to get the correction. 1292 1293 hours, minutes = -hours, -minutes 1294 1295 # Get the components. 1296 1297 hour, minute, second, zone = self.time() 1298 date = self.as_date() 1299 1300 # Add the minutes and hours. 1301 1302 minute += minutes 1303 if minute < 0 or minute > 59: 1304 hour += minute / 60 1305 minute = minute % 60 1306 1307 # NOTE: This makes various assumptions and probably would not work 1308 # NOTE: for general arithmetic. 1309 1310 hour += hours 1311 if hour < 0: 1312 date = date.previous_day() 1313 hour += 24 1314 elif hour > 23: 1315 date = date.next_day() 1316 hour -= 24 1317 1318 return date.as_datetime(hour, minute, second, "UTC") 1319 1320 # Cannot convert. 1321 1322 else: 1323 return None 1324 1325 def utc_offset(self): 1326 1327 "Return the UTC offset in hours and minutes." 1328 1329 zone = self.time_zone() 1330 if not zone: 1331 return None 1332 1333 # Support explicit UTC zones. 1334 1335 if zone == "UTC": 1336 return 0, 0 1337 1338 # Attempt to return a UTC offset where an explicit offset has been set. 1339 1340 match = timezone_offset_regexp.match(zone) 1341 if match: 1342 if match.group("sign") == "-": 1343 sign = -1 1344 else: 1345 sign = 1 1346 1347 hours = int(match.group("hours")) * sign 1348 minutes = int(match.group("minutes") or 0) * sign 1349 return hours, minutes 1350 1351 # Attempt to handle Olson time zone identifiers. 1352 1353 dt = self.as_olson_datetime() 1354 if dt: 1355 seconds = dt.utcoffset().seconds 1356 hours = seconds / 3600 1357 minutes = (seconds % 3600) / 60 1358 return hours, minutes 1359 1360 # Otherwise return None. 1361 1362 return None 1363 1364 def olson_identifier(self): 1365 1366 "Return the Olson identifier from any zone information." 1367 1368 zone = self.time_zone() 1369 if not zone: 1370 return None 1371 1372 # Attempt to match an identifier. 1373 1374 match = timezone_olson_regexp.match(zone) 1375 if match: 1376 return match.group("olson") 1377 else: 1378 return None 1379 1380 def _as_olson_datetime(self, hours=None): 1381 1382 """ 1383 Return a Python datetime object for this datetime interpreted using any 1384 Olson time zone identifier and the given 'hours' offset, raising one of 1385 the pytz exceptions in case of ambiguity. 1386 """ 1387 1388 olson = self.olson_identifier() 1389 if olson and pytz: 1390 tz = pytz.timezone(olson) 1391 data = self.padded().as_tuple()[:6] 1392 dt = datetime.datetime(*data) 1393 1394 # With an hours offset, find a time probably in a previously 1395 # applicable time zone. 1396 1397 if hours is not None: 1398 td = datetime.timedelta(0, hours * 3600) 1399 dt += td 1400 1401 ldt = tz.localize(dt, None) 1402 1403 # With an hours offset, adjust the time to define it within the 1404 # previously applicable time zone but at the presumably intended 1405 # position. 1406 1407 if hours is not None: 1408 ldt -= td 1409 1410 return ldt 1411 else: 1412 return None 1413 1414 def as_olson_datetime(self): 1415 1416 """ 1417 Return a Python datetime object for this datetime interpreted using any 1418 Olson time zone identifier, choosing the time from the zone before the 1419 period of ambiguity. 1420 """ 1421 1422 try: 1423 return self._as_olson_datetime() 1424 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1425 1426 # Try again, using an earlier local time and then stepping forward 1427 # in the chosen zone. 1428 # NOTE: Four hours earlier seems reasonable. 1429 1430 return self._as_olson_datetime(-4) 1431 1432 def ambiguous(self): 1433 1434 "Return whether the time is local and ambiguous." 1435 1436 try: 1437 self._as_olson_datetime() 1438 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1439 return 1 1440 1441 return 0 1442 1443 class Timespan: 1444 1445 """ 1446 A period of time which can be compared against others to check for overlaps. 1447 """ 1448 1449 def __init__(self, start, end): 1450 self.start = start 1451 self.end = end 1452 1453 def __repr__(self): 1454 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 1455 1456 def __hash__(self): 1457 return hash((self.start, self.end)) 1458 1459 def as_limits(self): 1460 return self.start, self.end 1461 1462 def is_before(self, a, b): 1463 if isinstance(a, DateTime) and a.has_time() and isinstance(b, DateTime) and b.has_time(): 1464 return a <= b 1465 else: 1466 return a < b 1467 1468 def is_after_or_during(self, a, b): 1469 if isinstance(a, DateTime) and a.has_time() and isinstance(b, DateTime) and b.has_time(): 1470 return a > b 1471 else: 1472 return a >= b 1473 1474 def __contains__(self, other): 1475 return self == other 1476 1477 def __cmp__(self, other): 1478 1479 """ 1480 Return whether this timespan occupies the same period of time as the 1481 'other'. 1482 """ 1483 1484 if isinstance(other, Timespan): 1485 if self.end is not None and other.start is not None and self.is_before(self.end, other.start): 1486 return -1 1487 elif self.start is not None and other.end is not None and self.is_before(other.end, self.start): 1488 return 1 1489 else: 1490 return 0 1491 1492 # Points in time are not considered to represent an upper bound on a 1493 # non-inclusive timespan. 1494 1495 else: 1496 if self.end is not None and self.is_before(self.end, other): 1497 return -1 1498 elif self.start is not None and self.start > other: 1499 return 1 1500 else: 1501 return 0 1502 1503 class TimespanCollection: 1504 1505 "A collection of timespans with a particular resolution." 1506 1507 def __init__(self, resolution, values=None): 1508 1509 # Timespans need to be given converted start and end dates/times. 1510 1511 if resolution == "date": 1512 self.convert_time = lambda x: x.as_date() 1513 else: 1514 self.convert_time = lambda x: x 1515 1516 self.values = values or [] 1517 1518 def convert(self, value): 1519 if isinstance(value, Event): 1520 value = value.as_timespan() 1521 1522 if isinstance(value, Timespan): 1523 start, end = map(self.convert_time, value.as_limits()) 1524 return Timespan(start, end) 1525 else: 1526 return self.convert_time(value) 1527 1528 def __iter__(self): 1529 return iter(self.values) 1530 1531 def __len__(self): 1532 return len(self.values) 1533 1534 def __getitem__(self, i): 1535 return self.values[i] 1536 1537 def __setitem__(self, i, value): 1538 self.values[i] = value 1539 1540 def __contains__(self, value): 1541 test_value = self.convert(value) 1542 return test_value in self.values 1543 1544 def append(self, value): 1545 self.values.append(value) 1546 1547 def insert(self, i, value): 1548 self.values.insert(i, value) 1549 1550 def pop(self): 1551 return self.values.pop() 1552 1553 def insert_in_order(self, value): 1554 bisect.insort_left(self, value) 1555 1556 def getCountry(s): 1557 1558 "Find a country code in the given string 's'." 1559 1560 match = country_code_regexp.search(s) 1561 1562 if match: 1563 return match.group("code") 1564 else: 1565 return None 1566 1567 def getDate(s): 1568 1569 "Parse the string 's', extracting and returning a date object." 1570 1571 dt = getDateTime(s) 1572 if dt is not None: 1573 return dt.as_date() 1574 else: 1575 return None 1576 1577 def getDateTime(s): 1578 1579 "Parse the string 's', extracting and returning a datetime object." 1580 1581 m = datetime_regexp.search(s) 1582 if m: 1583 groups = list(m.groups()) 1584 1585 # Convert date and time data to integer or None. 1586 1587 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]) 1588 else: 1589 return None 1590 1591 def getDateStrings(s): 1592 1593 "Parse the string 's', extracting and returning all date strings." 1594 1595 start = 0 1596 m = date_regexp.search(s, start) 1597 l = [] 1598 while m: 1599 l.append("-".join(m.groups())) 1600 m = date_regexp.search(s, m.end()) 1601 return l 1602 1603 def getMonth(s): 1604 1605 "Parse the string 's', extracting and returning a month object." 1606 1607 m = month_regexp.search(s) 1608 if m: 1609 return Month(map(int, m.groups())) 1610 else: 1611 return None 1612 1613 def getCurrentDate(): 1614 1615 "Return the current date as a (year, month, day) tuple." 1616 1617 today = datetime.date.today() 1618 return Date((today.year, today.month, today.day)) 1619 1620 def getCurrentMonth(): 1621 1622 "Return the current month as a (year, month) tuple." 1623 1624 today = datetime.date.today() 1625 return Month((today.year, today.month)) 1626 1627 def getCurrentYear(): 1628 1629 "Return the current year." 1630 1631 today = datetime.date.today() 1632 return today.year 1633 1634 # User interface functions. 1635 1636 def getParameter(request, name, default=None): 1637 1638 """ 1639 Using the given 'request', return the value of the parameter with the given 1640 'name', returning the optional 'default' (or None) if no value was supplied 1641 in the 'request'. 1642 """ 1643 1644 return get_form(request).get(name, [default])[0] 1645 1646 def getQualifiedParameter(request, calendar_name, argname, default=None): 1647 1648 """ 1649 Using the given 'request', 'calendar_name' and 'argname', retrieve the 1650 value of the qualified parameter, returning the optional 'default' (or None) 1651 if no value was supplied in the 'request'. 1652 """ 1653 1654 argname = getQualifiedParameterName(calendar_name, argname) 1655 return getParameter(request, argname, default) 1656 1657 def getQualifiedParameterName(calendar_name, argname): 1658 1659 """ 1660 Return the qualified parameter name using the given 'calendar_name' and 1661 'argname'. 1662 """ 1663 1664 if calendar_name is None: 1665 return argname 1666 else: 1667 return "%s-%s" % (calendar_name, argname) 1668 1669 def getParameterDate(arg): 1670 1671 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1672 1673 n = None 1674 1675 if arg is None: 1676 return None 1677 1678 elif arg.startswith("current"): 1679 date = getCurrentDate() 1680 if len(arg) > 8: 1681 n = int(arg[7:]) 1682 1683 elif arg.startswith("yearstart"): 1684 date = Date((getCurrentYear(), 1, 1)) 1685 if len(arg) > 10: 1686 n = int(arg[9:]) 1687 1688 elif arg.startswith("yearend"): 1689 date = Date((getCurrentYear(), 12, 31)) 1690 if len(arg) > 8: 1691 n = int(arg[7:]) 1692 1693 else: 1694 date = getDate(arg) 1695 1696 if n is not None: 1697 date = date.day_update(n) 1698 1699 return date 1700 1701 def getParameterMonth(arg): 1702 1703 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1704 1705 n = None 1706 1707 if arg is None: 1708 return None 1709 1710 elif arg.startswith("current"): 1711 date = getCurrentMonth() 1712 if len(arg) > 8: 1713 n = int(arg[7:]) 1714 1715 elif arg.startswith("yearstart"): 1716 date = Month((getCurrentYear(), 1)) 1717 if len(arg) > 10: 1718 n = int(arg[9:]) 1719 1720 elif arg.startswith("yearend"): 1721 date = Month((getCurrentYear(), 12)) 1722 if len(arg) > 8: 1723 n = int(arg[7:]) 1724 1725 else: 1726 date = getMonth(arg) 1727 1728 if n is not None: 1729 date = date.month_update(n) 1730 1731 return date 1732 1733 def getFormDate(request, calendar_name, argname): 1734 1735 """ 1736 Return the date from the 'request' for the calendar with the given 1737 'calendar_name' using the parameter having the given 'argname'. 1738 """ 1739 1740 arg = getQualifiedParameter(request, calendar_name, argname) 1741 return getParameterDate(arg) 1742 1743 def getFormMonth(request, calendar_name, argname): 1744 1745 """ 1746 Return the month from the 'request' for the calendar with the given 1747 'calendar_name' using the parameter having the given 'argname'. 1748 """ 1749 1750 arg = getQualifiedParameter(request, calendar_name, argname) 1751 return getParameterMonth(arg) 1752 1753 def getFormDateTriple(request, yeararg, montharg, dayarg): 1754 1755 """ 1756 Return the date from the 'request' for the calendar with the given 1757 'calendar_name' using the parameters having the given 'yeararg', 'montharg' 1758 and 'dayarg' names. 1759 """ 1760 1761 year = getParameter(request, yeararg) 1762 month = getParameter(request, montharg) 1763 day = getParameter(request, dayarg) 1764 if year and month and day: 1765 return Date((int(year), int(month), int(day))) 1766 else: 1767 return None 1768 1769 def getFormMonthPair(request, yeararg, montharg): 1770 1771 """ 1772 Return the month from the 'request' for the calendar with the given 1773 'calendar_name' using the parameters having the given 'yeararg' and 1774 'montharg' names. 1775 """ 1776 1777 year = getParameter(request, yeararg) 1778 month = getParameter(request, montharg) 1779 if year and month: 1780 return Month((int(year), int(month))) 1781 else: 1782 return None 1783 1784 def getFullDateLabel(request, date): 1785 1786 """ 1787 Return the full month plus year label using the given 'request' and 1788 'year_month'. 1789 """ 1790 1791 if not date: 1792 return "" 1793 1794 _ = request.getText 1795 year, month, day = date.as_tuple()[:3] 1796 start_weekday, number_of_days = date.month_properties() 1797 weekday = (start_weekday + day - 1) % 7 1798 day_label = _(getDayLabel(weekday)) 1799 month_label = _(getMonthLabel(month)) 1800 return "%s %s %s %s" % (day_label, day, month_label, year) 1801 1802 def getFullMonthLabel(request, year_month): 1803 1804 """ 1805 Return the full month plus year label using the given 'request' and 1806 'year_month'. 1807 """ 1808 1809 if not year_month: 1810 return "" 1811 1812 _ = request.getText 1813 year, month = year_month.as_tuple()[:2] 1814 month_label = _(getMonthLabel(month)) 1815 return "%s %s" % (month_label, year) 1816 1817 # Page-related functions. 1818 1819 def getPrettyPageName(page): 1820 1821 "Return a nicely formatted title/name for the given 'page'." 1822 1823 title = page.split_title(force=1) 1824 return getPrettyTitle(title) 1825 1826 def linkToPage(request, page, text, query_string=None): 1827 1828 """ 1829 Using 'request', return a link to 'page' with the given link 'text' and 1830 optional 'query_string'. 1831 """ 1832 1833 text = wikiutil.escape(text) 1834 return page.link_to_raw(request, text, query_string) 1835 1836 def getFullPageName(parent, title): 1837 1838 """ 1839 Return a full page name from the given 'parent' page (can be empty or None) 1840 and 'title' (a simple page name). 1841 """ 1842 1843 if parent: 1844 return "%s/%s" % (parent.rstrip("/"), title) 1845 else: 1846 return title 1847 1848 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1849 1850 """ 1851 Using the given 'template_page', complete the 'new_page' by copying the 1852 template and adding the given 'event_details' (a dictionary of event 1853 fields), setting also the 'category_pagenames' to define category 1854 membership. 1855 """ 1856 1857 event_page = EventPage(template_page) 1858 new_event_page = EventPage(new_page) 1859 new_event_page.copyPage(event_page) 1860 1861 if new_event_page.getFormat() == "wiki": 1862 new_event = Event(new_event_page, event_details) 1863 new_event_page.setEvents([new_event]) 1864 new_event_page.setCategoryMembership(category_pagenames) 1865 new_event_page.saveChanges() 1866 1867 # vim: tabstop=4 expandtab shiftwidth=4