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