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