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 else: 983 times.add(start.as_datetime(None, None, None, None)) 984 985 if isinstance(end, DateTime): 986 times.add(end) 987 else: 988 times.add(end.as_date().next_day()) 989 990 times = list(times) 991 times.sort(cmp_dates_as_day_start) 992 993 scale = [] 994 first = 1 995 start = None 996 for time in times: 997 if not first: 998 scale.append(Timespan(start, time)) 999 else: 1000 first = 0 1001 start = time 1002 1003 return scale 1004 1005 # Date-related functions. 1006 1007 def cmp_dates_as_day_start(a, b): 1008 1009 """ 1010 Compare dates/datetimes 'a' and 'b' treating dates without time information 1011 as the earliest time in a particular day. 1012 """ 1013 1014 are_equal = a == b 1015 1016 if are_equal: 1017 a2 = a.as_datetime_or_date() 1018 b2 = b.as_datetime_or_date() 1019 1020 if isinstance(a2, Date) and isinstance(b2, DateTime): 1021 return -1 1022 elif isinstance(a2, DateTime) and isinstance(b2, Date): 1023 return 1 1024 1025 return cmp(a, b) 1026 1027 class Period: 1028 1029 "A simple period of time." 1030 1031 def __init__(self, data): 1032 self.data = data 1033 1034 def months(self): 1035 return self.data[0] * 12 + self.data[1] 1036 1037 class Temporal: 1038 1039 "A simple temporal representation, common to dates and times." 1040 1041 def __init__(self, data): 1042 self.data = list(data) 1043 1044 def __repr__(self): 1045 return "%s(%r)" % (self.__class__.__name__, self.data) 1046 1047 def __hash__(self): 1048 return hash(self.as_tuple()) 1049 1050 def as_tuple(self): 1051 return tuple(self.data) 1052 1053 def __cmp__(self, other): 1054 1055 """ 1056 The result of comparing this instance with 'other' is derived from a 1057 comparison of the instances' date(time) data at the highest common 1058 resolution, meaning that if a date is compared to a datetime, the 1059 datetime will be considered as a date. Thus, a date and a datetime 1060 referring to the same date will be considered equal. 1061 """ 1062 1063 if not isinstance(other, Temporal): 1064 return NotImplemented 1065 else: 1066 data = self.as_tuple() 1067 other_data = other.as_tuple() 1068 length = min(len(data), len(other_data)) 1069 return cmp(data[:length], other_data[:length]) 1070 1071 def until(self, start, end, nextfn, prevfn): 1072 1073 """ 1074 Return a collection of units of time by starting from the given 'start' 1075 and stepping across intervening units until 'end' is reached, using the 1076 given 'nextfn' and 'prevfn' to step from one unit to the next. 1077 """ 1078 1079 current = start 1080 units = [current] 1081 if current < end: 1082 while current < end: 1083 current = nextfn(current) 1084 units.append(current) 1085 elif current > end: 1086 while current > end: 1087 current = prevfn(current) 1088 units.append(current) 1089 return units 1090 1091 def ambiguous(self): 1092 1093 "Only times can be ambiguous." 1094 1095 return 0 1096 1097 class Month(Temporal): 1098 1099 "A simple year-month representation." 1100 1101 def __str__(self): 1102 return "%04d-%02d" % self.as_tuple()[:2] 1103 1104 def as_datetime(self, day, hour, minute, second, zone): 1105 return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) 1106 1107 def as_date(self, day): 1108 return Date(self.as_tuple() + (day,)) 1109 1110 def as_month(self): 1111 return self 1112 1113 def year(self): 1114 return self.data[0] 1115 1116 def month(self): 1117 return self.data[1] 1118 1119 def month_properties(self): 1120 1121 """ 1122 Return the weekday of the 1st of the month, along with the number of 1123 days, as a tuple. 1124 """ 1125 1126 year, month = self.as_tuple()[:2] 1127 return calendar.monthrange(year, month) 1128 1129 def month_update(self, n=1): 1130 1131 "Return the month updated by 'n' months." 1132 1133 year, month = self.as_tuple()[:2] 1134 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1)) 1135 1136 def next_month(self): 1137 1138 "Return the month following this one." 1139 1140 return self.month_update(1) 1141 1142 def previous_month(self): 1143 1144 "Return the month preceding this one." 1145 1146 return self.month_update(-1) 1147 1148 def __sub__(self, start): 1149 1150 """ 1151 Return the difference in years and months between this month and the 1152 'start' month as a period. 1153 """ 1154 1155 return Period([(x - y) for x, y in zip(self.data, start.data)]) 1156 1157 def months_until(self, end): 1158 1159 "Return the collection of months from this month until 'end'." 1160 1161 return self.until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month) 1162 1163 class Date(Month): 1164 1165 "A simple year-month-day representation." 1166 1167 def constrain(self): 1168 year, month, day = self.as_tuple()[:3] 1169 1170 month = max(min(month, 12), 1) 1171 wd, last_day = calendar.monthrange(year, month) 1172 day = max(min(day, last_day), 1) 1173 1174 self.data[1:3] = month, day 1175 1176 def __str__(self): 1177 return "%04d-%02d-%02d" % self.as_tuple()[:3] 1178 1179 def as_datetime(self, hour, minute, second, zone): 1180 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 1181 1182 def as_date(self): 1183 return self 1184 1185 def as_datetime_or_date(self): 1186 return self 1187 1188 def as_month(self): 1189 return Month(self.data[:2]) 1190 1191 def day(self): 1192 return self.data[2] 1193 1194 def day_update(self, n=1): 1195 1196 "Return the month updated by 'n' months." 1197 1198 delta = datetime.timedelta(n) 1199 dt = datetime.date(*self.as_tuple()[:3]) 1200 dt_new = dt + delta 1201 return Date((dt_new.year, dt_new.month, dt_new.day)) 1202 1203 def next_day(self): 1204 1205 "Return the date following this one." 1206 1207 year, month, day = self.as_tuple()[:3] 1208 _wd, end_day = calendar.monthrange(year, month) 1209 if day == end_day: 1210 if month == 12: 1211 return Date((year + 1, 1, 1)) 1212 else: 1213 return Date((year, month + 1, 1)) 1214 else: 1215 return Date((year, month, day + 1)) 1216 1217 def previous_day(self): 1218 1219 "Return the date preceding this one." 1220 1221 year, month, day = self.as_tuple()[:3] 1222 if day == 1: 1223 if month == 1: 1224 return Date((year - 1, 12, 31)) 1225 else: 1226 _wd, end_day = calendar.monthrange(year, month - 1) 1227 return Date((year, month - 1, end_day)) 1228 else: 1229 return Date((year, month, day - 1)) 1230 1231 def days_until(self, end): 1232 1233 "Return the collection of days from this date until 'end'." 1234 1235 return self.until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 1236 1237 class DateTime(Date): 1238 1239 "A simple date plus time representation." 1240 1241 def constrain(self): 1242 Date.constrain(self) 1243 1244 hour, minute, second = self.as_tuple()[3:6] 1245 1246 if self.has_time(): 1247 hour = max(min(hour, 23), 0) 1248 minute = max(min(minute, 59), 0) 1249 1250 if second is not None: 1251 second = max(min(second, 60), 0) # support leap seconds 1252 1253 self.data[3:6] = hour, minute, second 1254 1255 def __str__(self): 1256 return Date.__str__(self) + self.time_string() 1257 1258 def time_string(self): 1259 if self.has_time(): 1260 data = self.as_tuple() 1261 time_str = " %02d:%02d" % data[3:5] 1262 if data[5] is not None: 1263 time_str += ":%02d" % data[5] 1264 if data[6] is not None: 1265 time_str += " %s" % data[6] 1266 return time_str 1267 else: 1268 return "" 1269 1270 def as_datetime(self): 1271 return self 1272 1273 def as_date(self): 1274 return Date(self.data[:3]) 1275 1276 def as_datetime_or_date(self): 1277 1278 """ 1279 Return a date for this datetime if fields are missing. Otherwise, return 1280 this datetime itself. 1281 """ 1282 1283 if not self.has_time(): 1284 return self.as_date() 1285 else: 1286 return self 1287 1288 def __cmp__(self, other): 1289 1290 """ 1291 The result of comparing this instance with 'other' is, if both instances 1292 are datetime instances, derived from a comparison of the datetimes 1293 converted to UTC. If one or both datetimes cannot be converted to UTC, 1294 the datetimes are compared using the basic temporal comparison which 1295 compares their raw time data. 1296 """ 1297 1298 this = self 1299 1300 if this.has_time(): 1301 if isinstance(other, DateTime): 1302 if other.has_time(): 1303 this_utc = this.to_utc() 1304 other_utc = other.to_utc() 1305 if this_utc is not None and other_utc is not None: 1306 return cmp(this_utc.as_tuple(), other_utc.as_tuple()) 1307 else: 1308 other = other.padded() 1309 else: 1310 this = this.padded() 1311 1312 return Date.__cmp__(this, other) 1313 1314 def has_time(self): 1315 1316 """ 1317 Return whether this object has any time information. Objects without 1318 time information can refer to the very start of a day. 1319 """ 1320 1321 return self.data[3] is not None and self.data[4] is not None 1322 1323 def time(self): 1324 return self.data[3:] 1325 1326 def seconds(self): 1327 return self.data[5] 1328 1329 def time_zone(self): 1330 return self.data[6] 1331 1332 def set_time_zone(self, value): 1333 self.data[6] = value 1334 1335 def padded(self, empty_value=0): 1336 1337 """ 1338 Return a datetime with missing fields defined as being the given 1339 'empty_value' or 0 if not specified. 1340 """ 1341 1342 data = [] 1343 for x in self.data[:6]: 1344 if x is None: 1345 data.append(empty_value) 1346 else: 1347 data.append(x) 1348 1349 data += self.data[6:] 1350 return DateTime(data) 1351 1352 def to_utc(self): 1353 1354 """ 1355 Return this object converted to UTC, or None if such a conversion is not 1356 defined. 1357 """ 1358 1359 if not self.has_time(): 1360 return None 1361 1362 offset = self.utc_offset() 1363 if offset: 1364 hours, minutes = offset 1365 1366 # Invert the offset to get the correction. 1367 1368 hours, minutes = -hours, -minutes 1369 1370 # Get the components. 1371 1372 hour, minute, second, zone = self.time() 1373 date = self.as_date() 1374 1375 # Add the minutes and hours. 1376 1377 minute += minutes 1378 if minute < 0 or minute > 59: 1379 hour += minute / 60 1380 minute = minute % 60 1381 1382 # NOTE: This makes various assumptions and probably would not work 1383 # NOTE: for general arithmetic. 1384 1385 hour += hours 1386 if hour < 0: 1387 date = date.previous_day() 1388 hour += 24 1389 elif hour > 23: 1390 date = date.next_day() 1391 hour -= 24 1392 1393 return date.as_datetime(hour, minute, second, "UTC") 1394 1395 # Cannot convert. 1396 1397 else: 1398 return None 1399 1400 def utc_offset(self): 1401 1402 "Return the UTC offset in hours and minutes." 1403 1404 zone = self.time_zone() 1405 if not zone: 1406 return None 1407 1408 # Support explicit UTC zones. 1409 1410 if zone == "UTC": 1411 return 0, 0 1412 1413 # Attempt to return a UTC offset where an explicit offset has been set. 1414 1415 match = timezone_offset_regexp.match(zone) 1416 if match: 1417 if match.group("sign") == "-": 1418 sign = -1 1419 else: 1420 sign = 1 1421 1422 hours = int(match.group("hours")) * sign 1423 minutes = int(match.group("minutes") or 0) * sign 1424 return hours, minutes 1425 1426 # Attempt to handle Olson time zone identifiers. 1427 1428 dt = self.as_olson_datetime() 1429 if dt: 1430 seconds = dt.utcoffset().seconds 1431 hours = seconds / 3600 1432 minutes = (seconds % 3600) / 60 1433 return hours, minutes 1434 1435 # Otherwise return None. 1436 1437 return None 1438 1439 def olson_identifier(self): 1440 1441 "Return the Olson identifier from any zone information." 1442 1443 zone = self.time_zone() 1444 if not zone: 1445 return None 1446 1447 # Attempt to match an identifier. 1448 1449 match = timezone_olson_regexp.match(zone) 1450 if match: 1451 return match.group("olson") 1452 else: 1453 return None 1454 1455 def _as_olson_datetime(self, hours=None): 1456 1457 """ 1458 Return a Python datetime object for this datetime interpreted using any 1459 Olson time zone identifier and the given 'hours' offset, raising one of 1460 the pytz exceptions in case of ambiguity. 1461 """ 1462 1463 olson = self.olson_identifier() 1464 if olson and pytz: 1465 tz = pytz.timezone(olson) 1466 data = self.padded().as_tuple()[:6] 1467 dt = datetime.datetime(*data) 1468 1469 # With an hours offset, find a time probably in a previously 1470 # applicable time zone. 1471 1472 if hours is not None: 1473 td = datetime.timedelta(0, hours * 3600) 1474 dt += td 1475 1476 ldt = tz.localize(dt, None) 1477 1478 # With an hours offset, adjust the time to define it within the 1479 # previously applicable time zone but at the presumably intended 1480 # position. 1481 1482 if hours is not None: 1483 ldt -= td 1484 1485 return ldt 1486 else: 1487 return None 1488 1489 def as_olson_datetime(self): 1490 1491 """ 1492 Return a Python datetime object for this datetime interpreted using any 1493 Olson time zone identifier, choosing the time from the zone before the 1494 period of ambiguity. 1495 """ 1496 1497 try: 1498 return self._as_olson_datetime() 1499 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1500 1501 # Try again, using an earlier local time and then stepping forward 1502 # in the chosen zone. 1503 # NOTE: Four hours earlier seems reasonable. 1504 1505 return self._as_olson_datetime(-4) 1506 1507 def ambiguous(self): 1508 1509 "Return whether the time is local and ambiguous." 1510 1511 try: 1512 self._as_olson_datetime() 1513 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1514 return 1 1515 1516 return 0 1517 1518 class Timespan(ActsAsTimespan): 1519 1520 """ 1521 A period of time which can be compared against others to check for overlaps. 1522 """ 1523 1524 def __init__(self, start, end): 1525 self.start = start 1526 self.end = end 1527 1528 def __repr__(self): 1529 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 1530 1531 def __hash__(self): 1532 return hash((self.start, self.end)) 1533 1534 def as_timespan(self): 1535 return self 1536 1537 def as_limits(self): 1538 return self.start, self.end 1539 1540 def is_before(self, a, b): 1541 1542 """ 1543 Return whether 'a' is before 'b'. Since the end datetime of one period 1544 may be the same as the start datetime of another period, and yet the 1545 first period is intended to be concluded by the end datetime and not 1546 overlap with the other period, a different test is employed for datetime 1547 comparisons. 1548 """ 1549 1550 # Datetimes without times can be equal to dates and be considered as 1551 # occurring before those dates. 1552 1553 if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()): 1554 return a <= b 1555 else: 1556 return a < b 1557 1558 def __contains__(self, other): 1559 1560 """ 1561 This instance is considered to contain 'other' if one is not before or 1562 after the other. If this instance overlaps or coincides with 'other', 1563 then 'other' is regarded as belonging to this instance's time period. 1564 """ 1565 1566 return self == other 1567 1568 def __cmp__(self, other): 1569 1570 """ 1571 Return whether this timespan occupies the same period of time as the 1572 'other'. Timespans are considered less than others if their end points 1573 precede the other's start point, and are considered greater than others 1574 if their start points follow the other's end point. 1575 """ 1576 1577 if isinstance(other, ActsAsTimespan): 1578 other = other.as_timespan() 1579 1580 if self.end is not None and other.start is not None and self.is_before(self.end, other.start): 1581 return -1 1582 elif self.start is not None and other.end is not None and self.is_before(other.end, self.start): 1583 return 1 1584 else: 1585 return 0 1586 1587 else: 1588 if self.end is not None and self.is_before(self.end, other): 1589 return -1 1590 elif self.start is not None and self.is_before(other, self.start): 1591 return 1 1592 else: 1593 return 0 1594 1595 class TimespanCollection: 1596 1597 """ 1598 A collection of timespans providing a list-like interface supporting 1599 membership tests at a particular resolution. 1600 """ 1601 1602 def __init__(self, resolution, values=None): 1603 1604 # Timespans need to be given converted start and end dates/times. 1605 1606 if resolution == "date": 1607 self.convert_time = lambda x: x.as_date() 1608 elif resolution == "datetime": 1609 self.convert_time = lambda x: x.as_datetime_or_date() 1610 else: 1611 self.convert_time = lambda x: x 1612 1613 self.values = values or [] 1614 1615 def convert(self, value): 1616 if isinstance(value, ActsAsTimespan): 1617 value = value.as_timespan() 1618 start, end = map(self.convert_time, value.as_limits()) 1619 return Timespan(start, end) 1620 else: 1621 return self.convert_time(value) 1622 1623 def __iter__(self): 1624 return iter(self.values) 1625 1626 def __len__(self): 1627 return len(self.values) 1628 1629 def __getitem__(self, i): 1630 return self.values[i] 1631 1632 def __setitem__(self, i, value): 1633 self.values[i] = value 1634 1635 def __contains__(self, value): 1636 test_value = self.convert(value) 1637 return test_value in self.values 1638 1639 def append(self, value): 1640 self.values.append(value) 1641 1642 def insert(self, i, value): 1643 self.values.insert(i, value) 1644 1645 def pop(self): 1646 return self.values.pop() 1647 1648 def insert_in_order(self, value): 1649 bisect.insort_left(self, value) 1650 1651 def getCountry(s): 1652 1653 "Find a country code in the given string 's'." 1654 1655 match = country_code_regexp.search(s) 1656 1657 if match: 1658 return match.group("code") 1659 else: 1660 return None 1661 1662 def getDate(s): 1663 1664 "Parse the string 's', extracting and returning a date object." 1665 1666 dt = getDateTime(s) 1667 if dt is not None: 1668 return dt.as_date() 1669 else: 1670 return None 1671 1672 def getDateTime(s): 1673 1674 """ 1675 Parse the string 's', extracting and returning a datetime object where time 1676 information has been given or a date object where time information is 1677 absent. 1678 """ 1679 1680 m = datetime_regexp.search(s) 1681 if m: 1682 groups = list(m.groups()) 1683 1684 # Convert date and time data to integer or None. 1685 1686 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date() 1687 else: 1688 return None 1689 1690 def getDateStrings(s): 1691 1692 "Parse the string 's', extracting and returning all date strings." 1693 1694 start = 0 1695 m = date_regexp.search(s, start) 1696 l = [] 1697 while m: 1698 l.append("-".join(m.groups())) 1699 m = date_regexp.search(s, m.end()) 1700 return l 1701 1702 def getMonth(s): 1703 1704 "Parse the string 's', extracting and returning a month object." 1705 1706 m = month_regexp.search(s) 1707 if m: 1708 return Month(map(int, m.groups())) 1709 else: 1710 return None 1711 1712 def getCurrentDate(): 1713 1714 "Return the current date as a (year, month, day) tuple." 1715 1716 today = datetime.date.today() 1717 return Date((today.year, today.month, today.day)) 1718 1719 def getCurrentMonth(): 1720 1721 "Return the current month as a (year, month) tuple." 1722 1723 today = datetime.date.today() 1724 return Month((today.year, today.month)) 1725 1726 def getCurrentYear(): 1727 1728 "Return the current year." 1729 1730 today = datetime.date.today() 1731 return today.year 1732 1733 # User interface functions. 1734 1735 def getParameter(request, name, default=None): 1736 1737 """ 1738 Using the given 'request', return the value of the parameter with the given 1739 'name', returning the optional 'default' (or None) if no value was supplied 1740 in the 'request'. 1741 """ 1742 1743 return get_form(request).get(name, [default])[0] 1744 1745 def getQualifiedParameter(request, calendar_name, argname, default=None): 1746 1747 """ 1748 Using the given 'request', 'calendar_name' and 'argname', retrieve the 1749 value of the qualified parameter, returning the optional 'default' (or None) 1750 if no value was supplied in the 'request'. 1751 """ 1752 1753 argname = getQualifiedParameterName(calendar_name, argname) 1754 return getParameter(request, argname, default) 1755 1756 def getQualifiedParameterName(calendar_name, argname): 1757 1758 """ 1759 Return the qualified parameter name using the given 'calendar_name' and 1760 'argname'. 1761 """ 1762 1763 if calendar_name is None: 1764 return argname 1765 else: 1766 return "%s-%s" % (calendar_name, argname) 1767 1768 def getParameterDate(arg): 1769 1770 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1771 1772 n = None 1773 1774 if arg is None: 1775 return None 1776 1777 elif arg.startswith("current"): 1778 date = getCurrentDate() 1779 if len(arg) > 8: 1780 n = int(arg[7:]) 1781 1782 elif arg.startswith("yearstart"): 1783 date = Date((getCurrentYear(), 1, 1)) 1784 if len(arg) > 10: 1785 n = int(arg[9:]) 1786 1787 elif arg.startswith("yearend"): 1788 date = Date((getCurrentYear(), 12, 31)) 1789 if len(arg) > 8: 1790 n = int(arg[7:]) 1791 1792 else: 1793 date = getDate(arg) 1794 1795 if n is not None: 1796 date = date.day_update(n) 1797 1798 return date 1799 1800 def getParameterMonth(arg): 1801 1802 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1803 1804 n = None 1805 1806 if arg is None: 1807 return None 1808 1809 elif arg.startswith("current"): 1810 date = getCurrentMonth() 1811 if len(arg) > 8: 1812 n = int(arg[7:]) 1813 1814 elif arg.startswith("yearstart"): 1815 date = Month((getCurrentYear(), 1)) 1816 if len(arg) > 10: 1817 n = int(arg[9:]) 1818 1819 elif arg.startswith("yearend"): 1820 date = Month((getCurrentYear(), 12)) 1821 if len(arg) > 8: 1822 n = int(arg[7:]) 1823 1824 else: 1825 date = getMonth(arg) 1826 1827 if n is not None: 1828 date = date.month_update(n) 1829 1830 return date 1831 1832 def getFormDate(request, calendar_name, argname): 1833 1834 """ 1835 Return the date from the 'request' for the calendar with the given 1836 'calendar_name' using the parameter having the given 'argname'. 1837 """ 1838 1839 arg = getQualifiedParameter(request, calendar_name, argname) 1840 return getParameterDate(arg) 1841 1842 def getFormMonth(request, calendar_name, argname): 1843 1844 """ 1845 Return the month from the 'request' for the calendar with the given 1846 'calendar_name' using the parameter having the given 'argname'. 1847 """ 1848 1849 arg = getQualifiedParameter(request, calendar_name, argname) 1850 return getParameterMonth(arg) 1851 1852 def getFormDateTriple(request, yeararg, montharg, dayarg): 1853 1854 """ 1855 Return the date from the 'request' for the calendar with the given 1856 'calendar_name' using the parameters having the given 'yeararg', 'montharg' 1857 and 'dayarg' names. 1858 """ 1859 1860 year = getParameter(request, yeararg) 1861 month = getParameter(request, montharg) 1862 day = getParameter(request, dayarg) 1863 if year and month and day: 1864 return Date((int(year), int(month), int(day))) 1865 else: 1866 return None 1867 1868 def getFormMonthPair(request, yeararg, montharg): 1869 1870 """ 1871 Return the month from the 'request' for the calendar with the given 1872 'calendar_name' using the parameters having the given 'yeararg' and 1873 'montharg' names. 1874 """ 1875 1876 year = getParameter(request, yeararg) 1877 month = getParameter(request, montharg) 1878 if year and month: 1879 return Month((int(year), int(month))) 1880 else: 1881 return None 1882 1883 def getFullDateLabel(request, date): 1884 1885 """ 1886 Return the full month plus year label using the given 'request' and 1887 'year_month'. 1888 """ 1889 1890 if not date: 1891 return "" 1892 1893 _ = request.getText 1894 year, month, day = date.as_tuple()[:3] 1895 start_weekday, number_of_days = date.month_properties() 1896 weekday = (start_weekday + day - 1) % 7 1897 day_label = _(getDayLabel(weekday)) 1898 month_label = _(getMonthLabel(month)) 1899 return "%s %s %s %s" % (day_label, day, month_label, year) 1900 1901 def getFullMonthLabel(request, year_month): 1902 1903 """ 1904 Return the full month plus year label using the given 'request' and 1905 'year_month'. 1906 """ 1907 1908 if not year_month: 1909 return "" 1910 1911 _ = request.getText 1912 year, month = year_month.as_tuple()[:2] 1913 month_label = _(getMonthLabel(month)) 1914 return "%s %s" % (month_label, year) 1915 1916 # Page-related functions. 1917 1918 def getPrettyPageName(page): 1919 1920 "Return a nicely formatted title/name for the given 'page'." 1921 1922 title = page.split_title(force=1) 1923 return getPrettyTitle(title) 1924 1925 def linkToPage(request, page, text, query_string=None): 1926 1927 """ 1928 Using 'request', return a link to 'page' with the given link 'text' and 1929 optional 'query_string'. 1930 """ 1931 1932 text = wikiutil.escape(text) 1933 return page.link_to_raw(request, text, query_string) 1934 1935 def getFullPageName(parent, title): 1936 1937 """ 1938 Return a full page name from the given 'parent' page (can be empty or None) 1939 and 'title' (a simple page name). 1940 """ 1941 1942 if parent: 1943 return "%s/%s" % (parent.rstrip("/"), title) 1944 else: 1945 return title 1946 1947 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1948 1949 """ 1950 Using the given 'template_page', complete the 'new_page' by copying the 1951 template and adding the given 'event_details' (a dictionary of event 1952 fields), setting also the 'category_pagenames' to define category 1953 membership. 1954 """ 1955 1956 event_page = EventPage(template_page) 1957 new_event_page = EventPage(new_page) 1958 new_event_page.copyPage(event_page) 1959 1960 if new_event_page.getFormat() == "wiki": 1961 new_event = Event(new_event_page, event_details) 1962 new_event_page.setEvents([new_event]) 1963 new_event_page.setCategoryMembership(category_pagenames) 1964 new_event_page.saveChanges() 1965 1966 # vim: tabstop=4 expandtab shiftwidth=4