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