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