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