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 def getAllCategoryPages(category_names, request): 378 379 """ 380 Return all pages belonging to the categories having the given 381 'category_names', using the given 'request'. 382 """ 383 384 pages = [] 385 pagenames = set() 386 387 for category_name in category_names: 388 389 # Get the pages and page names in the category. 390 391 pages_in_category = getCategoryPages(category_name, request) 392 393 # Visit each page in the category. 394 395 for page_in_category in pages_in_category: 396 pagename = page_in_category.page_name 397 398 # Only process each page once. 399 400 if pagename in pagenames: 401 continue 402 else: 403 pagenames.add(pagename) 404 405 pages.append(page_in_category) 406 407 return pages 408 409 def getPagesFromResults(result_pages, request): 410 411 "Return genuine pages for the given 'result_pages' using the 'request'." 412 413 return [Page(request, page.page_name) for page in result_pages] 414 415 # Interfaces. 416 417 class ActsAsTimespan: 418 pass 419 420 # The main activity functions. 421 422 class EventPage: 423 424 "An event page." 425 426 def __init__(self, page): 427 self.page = page 428 self.events = None 429 self.body = None 430 self.categories = None 431 432 def copyPage(self, page): 433 434 "Copy the body of the given 'page'." 435 436 self.body = page.getBody() 437 438 def getPageURL(self, request): 439 440 "Using 'request', return the URL of this page." 441 442 return request.getQualifiedURL(self.page.url(request, relative=0)) 443 444 def getFormat(self): 445 446 "Get the format used on this page." 447 448 return self.page.pi["format"] 449 450 def getRevisions(self): 451 452 "Return a list of page revisions." 453 454 return self.page.getRevList() 455 456 def getPageRevision(self): 457 458 "Return the revision details dictionary for this page." 459 460 return getPageRevision(self.page) 461 462 def getPageName(self): 463 464 "Return the page name." 465 466 return self.page.page_name 467 468 def getPrettyPageName(self): 469 470 "Return a nicely formatted title/name for this page." 471 472 return getPrettyPageName(self.page) 473 474 def getBody(self): 475 476 "Get the current page body." 477 478 if self.body is None: 479 self.body = self.page.get_raw_body() 480 return self.body 481 482 def getEvents(self): 483 484 "Return a list of events from this page." 485 486 if self.events is None: 487 details = {} 488 self.events = [Event(self, details)] 489 490 if self.getFormat() == "wiki": 491 for match in definition_list_regexp.finditer(self.getBody()): 492 493 # Skip commented-out items. 494 495 if match.group("optcomment"): 496 continue 497 498 # Permit case-insensitive list terms. 499 500 term = match.group("term").lower() 501 desc = match.group("desc") 502 503 # Special value type handling. 504 505 # Dates. 506 507 if term in ("start", "end"): 508 desc = getDateTime(desc) 509 510 # Lists (whose elements may be quoted). 511 512 elif term in ("topics", "categories"): 513 desc = [getSimpleWikiText(value.strip()) for value in desc.split(",") if value.strip()] 514 515 # Labels which may well be quoted. 516 517 elif term in ("title", "summary", "description", "location"): 518 desc = getSimpleWikiText(desc) 519 520 if desc is not None: 521 522 # Handle apparent duplicates by creating a new set of 523 # details. 524 525 if details.has_key(term): 526 527 # Make a new event. 528 529 details = {} 530 self.events.append(Event(self, details)) 531 532 details[term] = desc 533 534 return self.events 535 536 def setEvents(self, events): 537 538 "Set the given 'events' on this page." 539 540 self.events = events 541 542 def getCategoryMembership(self): 543 544 "Get the category names from this page." 545 546 if self.categories is None: 547 body = self.getBody() 548 match = category_membership_regexp.search(body) 549 self.categories = match and [x for x in match.groups() if x] or [] 550 551 return self.categories 552 553 def setCategoryMembership(self, category_names): 554 555 """ 556 Set the category membership for the page using the specified 557 'category_names'. 558 """ 559 560 self.categories = category_names 561 562 def flushEventDetails(self): 563 564 "Flush the current event details to this page's body text." 565 566 new_body_parts = [] 567 end_of_last_match = 0 568 body = self.getBody() 569 570 events = iter(self.getEvents()) 571 572 event = events.next() 573 event_details = event.getDetails() 574 replaced_terms = set() 575 576 for match in definition_list_regexp.finditer(body): 577 578 # Permit case-insensitive list terms. 579 580 term = match.group("term").lower() 581 desc = match.group("desc") 582 583 # Check that the term has not already been substituted. If so, 584 # get the next event. 585 586 if term in replaced_terms: 587 try: 588 event = events.next() 589 590 # No more events. 591 592 except StopIteration: 593 break 594 595 event_details = event.getDetails() 596 replaced_terms = set() 597 598 # Add preceding text to the new body. 599 600 new_body_parts.append(body[end_of_last_match:match.start()]) 601 602 # Get the matching regions, adding the term to the new body. 603 604 new_body_parts.append(match.group("wholeterm")) 605 606 # Special value type handling. 607 608 if event_details.has_key(term): 609 610 # Dates. 611 612 if term in ("start", "end"): 613 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 614 615 # Lists (whose elements may be quoted). 616 617 elif term in ("topics", "categories"): 618 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 619 620 # Labels which must be quoted. 621 622 elif term in ("title", "summary"): 623 desc = getEncodedWikiText(event_details[term]) 624 625 # Text which need not be quoted, but it will be Wiki text. 626 627 elif term in ("description", "link", "location"): 628 desc = event_details[term] 629 630 replaced_terms.add(term) 631 632 # Add the replaced value. 633 634 new_body_parts.append(desc) 635 636 # Remember where in the page has been processed. 637 638 end_of_last_match = match.end() 639 640 # Write the rest of the page. 641 642 new_body_parts.append(body[end_of_last_match:]) 643 644 self.body = "".join(new_body_parts) 645 646 def flushCategoryMembership(self): 647 648 "Flush the category membership to the page body." 649 650 body = self.getBody() 651 category_names = self.getCategoryMembership() 652 match = category_membership_regexp.search(body) 653 654 if match: 655 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 656 657 def saveChanges(self): 658 659 "Save changes to the event." 660 661 self.flushEventDetails() 662 self.flushCategoryMembership() 663 self.page.saveText(self.getBody(), 0) 664 665 def linkToPage(self, request, text, query_string=None): 666 667 """ 668 Using 'request', return a link to this page with the given link 'text' 669 and optional 'query_string'. 670 """ 671 672 return linkToPage(request, self.page, text, query_string) 673 674 class Event(ActsAsTimespan): 675 676 "A description of an event." 677 678 def __init__(self, page, details): 679 self.page = page 680 self.details = details 681 682 def __repr__(self): 683 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 684 685 def __hash__(self): 686 return hash(self.getSummary()) 687 688 def getPage(self): 689 690 "Return the page describing this event." 691 692 return self.page 693 694 def setPage(self, page): 695 696 "Set the 'page' describing this event." 697 698 self.page = page 699 700 def getSummary(self, event_parent=None): 701 702 """ 703 Return either the given title or summary of the event according to the 704 event details, or a summary made from using the pretty version of the 705 page name. 706 707 If the optional 'event_parent' is specified, any page beneath the given 708 'event_parent' page in the page hierarchy will omit this parent information 709 if its name is used as the summary. 710 """ 711 712 event_details = self.details 713 714 if event_details.has_key("title"): 715 return event_details["title"] 716 elif event_details.has_key("summary"): 717 return event_details["summary"] 718 else: 719 # If appropriate, remove the parent details and "/" character. 720 721 title = self.page.getPageName() 722 723 if event_parent and title.startswith(event_parent): 724 title = title[len(event_parent.rstrip("/")) + 1:] 725 726 return getPrettyTitle(title) 727 728 def getDetails(self): 729 730 "Return the details for this event." 731 732 return self.details 733 734 def setDetails(self, event_details): 735 736 "Set the 'event_details' for this event." 737 738 self.details = event_details 739 740 # Timespan-related methods. 741 742 def __contains__(self, other): 743 return self == other 744 745 def __cmp__(self, other): 746 if isinstance(other, Event): 747 return cmp(self.as_timespan(), other.as_timespan()) 748 else: 749 return cmp(self.as_timespan(), other) 750 751 def as_timespan(self): 752 details = self.details 753 if details.has_key("start") and details.has_key("end"): 754 return Timespan(details["start"], details["end"]) 755 else: 756 return None 757 758 def as_limits(self): 759 ts = self.as_timespan() 760 return ts and ts.as_limits() 761 762 def getEventsFromPages(pages): 763 764 "Return a list of events found on the given 'pages'." 765 766 events = [] 767 768 for page in pages: 769 770 # Get a real page, not a result page. 771 772 event_page = EventPage(page) 773 774 # Get all events described in the page. 775 776 for event in event_page.getEvents(): 777 778 # Remember the event. 779 780 events.append(event) 781 782 return events 783 784 def getEventsInPeriod(events, calendar_period, resolution): 785 786 """ 787 Return a collection containing those of the given 'events' which occur 788 within the given 'calendar_period' at the given 'resolution'. 789 """ 790 791 all_shown_events = TimespanCollection(resolution) 792 793 for event in events: 794 795 # Test for the suitability of the event. 796 797 if event.as_timespan() is not None: 798 799 # Compare the dates to the requested calendar window, if any. 800 801 if event in calendar_period: 802 all_shown_events.insert_in_order(event) 803 804 return all_shown_events 805 806 def setEventTimestamps(request, events): 807 808 """ 809 Using 'request', set timestamp details in the details dictionary of each of 810 the 'events'. 811 812 Return the latest timestamp found. 813 """ 814 815 latest = None 816 817 for event in events: 818 event_details = event.getDetails() 819 event_page = event.getPage() 820 821 # Get the initial revision of the page. 822 823 revisions = event_page.getRevisions() 824 event_page_initial = Page(request, event_page.getPageName(), rev=revisions[-1]) 825 826 # Get the created and last modified times. 827 828 initial_revision = getPageRevision(event_page_initial) 829 event_details["created"] = initial_revision["timestamp"] 830 latest_revision = event_page.getPageRevision() 831 event_details["last-modified"] = latest_revision["timestamp"] 832 event_details["sequence"] = len(revisions) - 1 833 event_details["last-comment"] = latest_revision["comment"] 834 835 if latest is None or latest < event_details["last-modified"]: 836 latest = event_details["last-modified"] 837 838 return latest 839 840 def getOrderedEvents(events): 841 842 """ 843 Return a list with the given 'events' ordered according to their start and 844 end dates. 845 """ 846 847 ordered_events = events[:] 848 ordered_events.sort() 849 return ordered_events 850 851 def getCalendarPeriod(calendar_start, calendar_end, resolution): 852 853 """ 854 Return a calendar period for the given 'calendar_start' and 'calendar_end', 855 employing the given 'resolution'. The 'calendar_start' and 'calendar_end' 856 parameters can be given as None. 857 """ 858 859 # Re-order the window, if appropriate. 860 861 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 862 calendar_start, calendar_end = calendar_end, calendar_start 863 864 # Return a timespan at the given resolution. 865 866 return Timespan(calendar_start, calendar_end).convert(resolution) 867 868 def getConcretePeriod(calendar_start, calendar_end, earliest, latest): 869 870 """ 871 From the requested 'calendar_start' and 'calendar_end', which may be None, 872 indicating that no restriction is imposed on the period for each of the 873 boundaries, use the 'earliest' and 'latest' event months to define a 874 specific period of interest. 875 """ 876 877 # Define the period as starting with any specified start month or the 878 # earliest event known, ending with any specified end month or the latest 879 # event known. 880 881 first = calendar_start or earliest 882 last = calendar_end or latest 883 884 # If there is no range of months to show, perhaps because there are no 885 # events in the requested period, and there was no start or end month 886 # specified, show only the month indicated by the start or end of the 887 # requested period. If all events were to be shown but none were found show 888 # the current month. 889 890 if isinstance(first, Date): 891 get_current = getCurrentDate 892 else: 893 get_current = getCurrentMonth 894 895 if first is None: 896 first = last or get_current() 897 if last is None: 898 last = first or get_current() 899 900 # Permit "expiring" periods (where the start date approaches the end date). 901 902 return min(first, last), last 903 904 def getCoverage(events, resolution="date"): 905 906 """ 907 Determine the coverage of the given 'events', returning a collection of 908 timespans, along with a dictionary mapping locations to collections of 909 slots, where each slot contains a tuple of the form (timespans, events). 910 """ 911 912 all_events = {} 913 full_coverage = TimespanCollection(resolution) 914 915 # Get event details. 916 917 for event in events: 918 event_details = event.getDetails() 919 920 # Find the coverage of this period for the event. 921 922 # For day views, each location has its own slot, but for month 923 # views, all locations are pooled together since having separate 924 # slots for each location can lead to poor usage of vertical space. 925 926 if resolution == "datetime": 927 event_location = event_details.get("location") 928 else: 929 event_location = None 930 931 # Update the overall coverage. 932 933 full_coverage.insert_in_order(event) 934 935 # Add a new events list for a new location. 936 # Locations can be unspecified, thus None refers to all unlocalised 937 # events. 938 939 if not all_events.has_key(event_location): 940 all_events[event_location] = [TimespanCollection(resolution, [event])] 941 942 # Try and fit the event into an events list. 943 944 else: 945 slot = all_events[event_location] 946 947 for slot_events in slot: 948 949 # Where the event does not overlap with the events in the 950 # current collection, add it alongside these events. 951 952 if not event in slot_events: 953 slot_events.insert_in_order(event) 954 break 955 956 # Make a new element in the list if the event cannot be 957 # marked alongside existing events. 958 959 else: 960 slot.append(TimespanCollection(resolution, [event])) 961 962 return full_coverage, all_events 963 964 def getCoverageScale(coverage): 965 966 """ 967 Return a scale for the given coverage so that the times involved are 968 exposed. The scale consists of a list of non-overlapping timespans forming 969 a contiguous period of time. 970 """ 971 972 times = set() 973 for timespan in coverage: 974 start, end = timespan.as_limits() 975 976 # Add either genuine times or dates converted to times. 977 978 if isinstance(start, DateTime): 979 times.add(start) 980 else: 981 times.add(start.as_datetime(None, None, None, None)) 982 983 if isinstance(end, DateTime): 984 times.add(end) 985 else: 986 times.add(end.as_date().next_day()) 987 988 times = list(times) 989 times.sort(cmp_dates_as_day_start) 990 991 scale = [] 992 first = 1 993 start = None 994 for time in times: 995 if not first: 996 scale.append(Timespan(start, time)) 997 else: 998 first = 0 999 start = time 1000 1001 return scale 1002 1003 # Date-related functions. 1004 1005 def cmp_dates_as_day_start(a, b): 1006 1007 """ 1008 Compare dates/datetimes 'a' and 'b' treating dates without time information 1009 as the earliest time in a particular day. 1010 """ 1011 1012 are_equal = a == b 1013 1014 if are_equal: 1015 a2 = a.as_datetime_or_date() 1016 b2 = b.as_datetime_or_date() 1017 1018 if isinstance(a2, Date) and isinstance(b2, DateTime): 1019 return -1 1020 elif isinstance(a2, DateTime) and isinstance(b2, Date): 1021 return 1 1022 1023 return cmp(a, b) 1024 1025 class Period: 1026 1027 "A simple period of time." 1028 1029 def __init__(self, data): 1030 self.data = data 1031 1032 def months(self): 1033 return self.data[0] * 12 + self.data[1] 1034 1035 class Convertible: 1036 1037 "Support for converting temporal objects." 1038 1039 def _get_converter(self, resolution): 1040 if resolution == "month": 1041 return lambda x: x and x.as_month() 1042 elif resolution == "date": 1043 return lambda x: x and x.as_date() 1044 elif resolution == "datetime": 1045 return lambda x: x and x.as_datetime_or_date() 1046 else: 1047 return lambda x: x 1048 1049 class Temporal(Convertible): 1050 1051 "A simple temporal representation, common to dates and times." 1052 1053 def __init__(self, data): 1054 self.data = list(data) 1055 1056 def __repr__(self): 1057 return "%s(%r)" % (self.__class__.__name__, self.data) 1058 1059 def __hash__(self): 1060 return hash(self.as_tuple()) 1061 1062 def as_tuple(self): 1063 return tuple(self.data) 1064 1065 def convert(self, resolution): 1066 return self._get_converter(resolution)(self) 1067 1068 def __cmp__(self, other): 1069 1070 """ 1071 The result of comparing this instance with 'other' is derived from a 1072 comparison of the instances' date(time) data at the highest common 1073 resolution, meaning that if a date is compared to a datetime, the 1074 datetime will be considered as a date. Thus, a date and a datetime 1075 referring to the same date will be considered equal. 1076 """ 1077 1078 if not isinstance(other, Temporal): 1079 return NotImplemented 1080 else: 1081 data = self.as_tuple() 1082 other_data = other.as_tuple() 1083 length = min(len(data), len(other_data)) 1084 return cmp(data[:length], other_data[:length]) 1085 1086 def until(self, start, end, nextfn, prevfn): 1087 1088 """ 1089 Return a collection of units of time by starting from the given 'start' 1090 and stepping across intervening units until 'end' is reached, using the 1091 given 'nextfn' and 'prevfn' to step from one unit to the next. 1092 """ 1093 1094 current = start 1095 units = [current] 1096 if current < end: 1097 while current < end: 1098 current = nextfn(current) 1099 units.append(current) 1100 elif current > end: 1101 while current > end: 1102 current = prevfn(current) 1103 units.append(current) 1104 return units 1105 1106 def ambiguous(self): 1107 1108 "Only times can be ambiguous." 1109 1110 return 0 1111 1112 class Month(Temporal): 1113 1114 "A simple year-month representation." 1115 1116 def __str__(self): 1117 return "%04d-%02d" % self.as_tuple()[:2] 1118 1119 def as_datetime(self, day, hour, minute, second, zone): 1120 return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) 1121 1122 def as_date(self, day): 1123 return Date(self.as_tuple() + (day,)) 1124 1125 def as_month(self): 1126 return self 1127 1128 def year(self): 1129 return self.data[0] 1130 1131 def month(self): 1132 return self.data[1] 1133 1134 def month_properties(self): 1135 1136 """ 1137 Return the weekday of the 1st of the month, along with the number of 1138 days, as a tuple. 1139 """ 1140 1141 year, month = self.as_tuple()[:2] 1142 return calendar.monthrange(year, month) 1143 1144 def month_update(self, n=1): 1145 1146 "Return the month updated by 'n' months." 1147 1148 year, month = self.as_tuple()[:2] 1149 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1)) 1150 1151 def next_month(self): 1152 1153 "Return the month following this one." 1154 1155 return self.month_update(1) 1156 1157 def previous_month(self): 1158 1159 "Return the month preceding this one." 1160 1161 return self.month_update(-1) 1162 1163 def __sub__(self, start): 1164 1165 """ 1166 Return the difference in years and months between this month and the 1167 'start' month as a period. 1168 """ 1169 1170 return Period([(x - y) for x, y in zip(self.data, start.data)]) 1171 1172 def months_until(self, end): 1173 1174 "Return the collection of months from this month until 'end'." 1175 1176 return self.until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month) 1177 1178 class Date(Month): 1179 1180 "A simple year-month-day representation." 1181 1182 def constrain(self): 1183 year, month, day = self.as_tuple()[:3] 1184 1185 month = max(min(month, 12), 1) 1186 wd, last_day = calendar.monthrange(year, month) 1187 day = max(min(day, last_day), 1) 1188 1189 self.data[1:3] = month, day 1190 1191 def __str__(self): 1192 return "%04d-%02d-%02d" % self.as_tuple()[:3] 1193 1194 def as_datetime(self, hour, minute, second, zone): 1195 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 1196 1197 def as_date(self): 1198 return self 1199 1200 def as_datetime_or_date(self): 1201 return self 1202 1203 def as_month(self): 1204 return Month(self.data[:2]) 1205 1206 def day(self): 1207 return self.data[2] 1208 1209 def day_update(self, n=1): 1210 1211 "Return the month updated by 'n' days." 1212 1213 delta = datetime.timedelta(n) 1214 dt = datetime.date(*self.as_tuple()[:3]) 1215 dt_new = dt + delta 1216 return Date((dt_new.year, dt_new.month, dt_new.day)) 1217 1218 def next_day(self): 1219 1220 "Return the date following this one." 1221 1222 year, month, day = self.as_tuple()[:3] 1223 _wd, end_day = calendar.monthrange(year, month) 1224 if day == end_day: 1225 if month == 12: 1226 return Date((year + 1, 1, 1)) 1227 else: 1228 return Date((year, month + 1, 1)) 1229 else: 1230 return Date((year, month, day + 1)) 1231 1232 def previous_day(self): 1233 1234 "Return the date preceding this one." 1235 1236 year, month, day = self.as_tuple()[:3] 1237 if day == 1: 1238 if month == 1: 1239 return Date((year - 1, 12, 31)) 1240 else: 1241 _wd, end_day = calendar.monthrange(year, month - 1) 1242 return Date((year, month - 1, end_day)) 1243 else: 1244 return Date((year, month, day - 1)) 1245 1246 def days_until(self, end): 1247 1248 "Return the collection of days from this date until 'end'." 1249 1250 return self.until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 1251 1252 class DateTime(Date): 1253 1254 "A simple date plus time representation." 1255 1256 def constrain(self): 1257 Date.constrain(self) 1258 1259 hour, minute, second = self.as_tuple()[3:6] 1260 1261 if self.has_time(): 1262 hour = max(min(hour, 23), 0) 1263 minute = max(min(minute, 59), 0) 1264 1265 if second is not None: 1266 second = max(min(second, 60), 0) # support leap seconds 1267 1268 self.data[3:6] = hour, minute, second 1269 1270 def __str__(self): 1271 return Date.__str__(self) + self.time_string() 1272 1273 def time_string(self): 1274 if self.has_time(): 1275 data = self.as_tuple() 1276 time_str = " %02d:%02d" % data[3:5] 1277 if data[5] is not None: 1278 time_str += ":%02d" % data[5] 1279 if data[6] is not None: 1280 time_str += " %s" % data[6] 1281 return time_str 1282 else: 1283 return "" 1284 1285 def as_datetime(self): 1286 return self 1287 1288 def as_date(self): 1289 return Date(self.data[:3]) 1290 1291 def as_datetime_or_date(self): 1292 1293 """ 1294 Return a date for this datetime if fields are missing. Otherwise, return 1295 this datetime itself. 1296 """ 1297 1298 if not self.has_time(): 1299 return self.as_date() 1300 else: 1301 return self 1302 1303 def __cmp__(self, other): 1304 1305 """ 1306 The result of comparing this instance with 'other' is, if both instances 1307 are datetime instances, derived from a comparison of the datetimes 1308 converted to UTC. If one or both datetimes cannot be converted to UTC, 1309 the datetimes are compared using the basic temporal comparison which 1310 compares their raw time data. 1311 """ 1312 1313 this = self 1314 1315 if this.has_time(): 1316 if isinstance(other, DateTime): 1317 if other.has_time(): 1318 this_utc = this.to_utc() 1319 other_utc = other.to_utc() 1320 if this_utc is not None and other_utc is not None: 1321 return cmp(this_utc.as_tuple(), other_utc.as_tuple()) 1322 else: 1323 other = other.padded() 1324 else: 1325 this = this.padded() 1326 1327 return Date.__cmp__(this, other) 1328 1329 def has_time(self): 1330 1331 """ 1332 Return whether this object has any time information. Objects without 1333 time information can refer to the very start of a day. 1334 """ 1335 1336 return self.data[3] is not None and self.data[4] is not None 1337 1338 def time(self): 1339 return self.data[3:] 1340 1341 def seconds(self): 1342 return self.data[5] 1343 1344 def time_zone(self): 1345 return self.data[6] 1346 1347 def set_time_zone(self, value): 1348 self.data[6] = value 1349 1350 def padded(self, empty_value=0): 1351 1352 """ 1353 Return a datetime with missing fields defined as being the given 1354 'empty_value' or 0 if not specified. 1355 """ 1356 1357 data = [] 1358 for x in self.data[:6]: 1359 if x is None: 1360 data.append(empty_value) 1361 else: 1362 data.append(x) 1363 1364 data += self.data[6:] 1365 return DateTime(data) 1366 1367 def to_utc(self): 1368 1369 """ 1370 Return this object converted to UTC, or None if such a conversion is not 1371 defined. 1372 """ 1373 1374 if not self.has_time(): 1375 return None 1376 1377 offset = self.utc_offset() 1378 if offset: 1379 hours, minutes = offset 1380 1381 # Invert the offset to get the correction. 1382 1383 hours, minutes = -hours, -minutes 1384 1385 # Get the components. 1386 1387 hour, minute, second, zone = self.time() 1388 date = self.as_date() 1389 1390 # Add the minutes and hours. 1391 1392 minute += minutes 1393 if minute < 0 or minute > 59: 1394 hour += minute / 60 1395 minute = minute % 60 1396 1397 # NOTE: This makes various assumptions and probably would not work 1398 # NOTE: for general arithmetic. 1399 1400 hour += hours 1401 if hour < 0: 1402 date = date.previous_day() 1403 hour += 24 1404 elif hour > 23: 1405 date = date.next_day() 1406 hour -= 24 1407 1408 return date.as_datetime(hour, minute, second, "UTC") 1409 1410 # Cannot convert. 1411 1412 else: 1413 return None 1414 1415 def utc_offset(self): 1416 1417 "Return the UTC offset in hours and minutes." 1418 1419 zone = self.time_zone() 1420 if not zone: 1421 return None 1422 1423 # Support explicit UTC zones. 1424 1425 if zone == "UTC": 1426 return 0, 0 1427 1428 # Attempt to return a UTC offset where an explicit offset has been set. 1429 1430 match = timezone_offset_regexp.match(zone) 1431 if match: 1432 if match.group("sign") == "-": 1433 sign = -1 1434 else: 1435 sign = 1 1436 1437 hours = int(match.group("hours")) * sign 1438 minutes = int(match.group("minutes") or 0) * sign 1439 return hours, minutes 1440 1441 # Attempt to handle Olson time zone identifiers. 1442 1443 dt = self.as_olson_datetime() 1444 if dt: 1445 seconds = dt.utcoffset().seconds 1446 hours = seconds / 3600 1447 minutes = (seconds % 3600) / 60 1448 return hours, minutes 1449 1450 # Otherwise return None. 1451 1452 return None 1453 1454 def olson_identifier(self): 1455 1456 "Return the Olson identifier from any zone information." 1457 1458 zone = self.time_zone() 1459 if not zone: 1460 return None 1461 1462 # Attempt to match an identifier. 1463 1464 match = timezone_olson_regexp.match(zone) 1465 if match: 1466 return match.group("olson") 1467 else: 1468 return None 1469 1470 def _as_olson_datetime(self, hours=None): 1471 1472 """ 1473 Return a Python datetime object for this datetime interpreted using any 1474 Olson time zone identifier and the given 'hours' offset, raising one of 1475 the pytz exceptions in case of ambiguity. 1476 """ 1477 1478 olson = self.olson_identifier() 1479 if olson and pytz: 1480 tz = pytz.timezone(olson) 1481 data = self.padded().as_tuple()[:6] 1482 dt = datetime.datetime(*data) 1483 1484 # With an hours offset, find a time probably in a previously 1485 # applicable time zone. 1486 1487 if hours is not None: 1488 td = datetime.timedelta(0, hours * 3600) 1489 dt += td 1490 1491 ldt = tz.localize(dt, None) 1492 1493 # With an hours offset, adjust the time to define it within the 1494 # previously applicable time zone but at the presumably intended 1495 # position. 1496 1497 if hours is not None: 1498 ldt -= td 1499 1500 return ldt 1501 else: 1502 return None 1503 1504 def as_olson_datetime(self): 1505 1506 """ 1507 Return a Python datetime object for this datetime interpreted using any 1508 Olson time zone identifier, choosing the time from the zone before the 1509 period of ambiguity. 1510 """ 1511 1512 try: 1513 return self._as_olson_datetime() 1514 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1515 1516 # Try again, using an earlier local time and then stepping forward 1517 # in the chosen zone. 1518 # NOTE: Four hours earlier seems reasonable. 1519 1520 return self._as_olson_datetime(-4) 1521 1522 def ambiguous(self): 1523 1524 "Return whether the time is local and ambiguous." 1525 1526 try: 1527 self._as_olson_datetime() 1528 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1529 return 1 1530 1531 return 0 1532 1533 class Timespan(ActsAsTimespan, Convertible): 1534 1535 """ 1536 A period of time which can be compared against others to check for overlaps. 1537 """ 1538 1539 def __init__(self, start, end): 1540 self.start = start 1541 self.end = end 1542 1543 def __repr__(self): 1544 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 1545 1546 def __hash__(self): 1547 return hash((self.start, self.end)) 1548 1549 def as_timespan(self): 1550 return self 1551 1552 def as_limits(self): 1553 return self.start, self.end 1554 1555 def convert(self, resolution): 1556 return Timespan(*map(self._get_converter(resolution), self.as_limits())) 1557 1558 def is_before(self, a, b): 1559 1560 """ 1561 Return whether 'a' is before 'b'. Since the end datetime of one period 1562 may be the same as the start datetime of another period, and yet the 1563 first period is intended to be concluded by the end datetime and not 1564 overlap with the other period, a different test is employed for datetime 1565 comparisons. 1566 """ 1567 1568 # Datetimes without times can be equal to dates and be considered as 1569 # occurring before those dates. 1570 1571 if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()): 1572 return a <= b 1573 else: 1574 return a < b 1575 1576 def __contains__(self, other): 1577 1578 """ 1579 This instance is considered to contain 'other' if one is not before or 1580 after the other. If this instance overlaps or coincides with 'other', 1581 then 'other' is regarded as belonging to this instance's time period. 1582 """ 1583 1584 return self == other 1585 1586 def __cmp__(self, other): 1587 1588 """ 1589 Return whether this timespan occupies the same period of time as the 1590 'other'. Timespans are considered less than others if their end points 1591 precede the other's start point, and are considered greater than others 1592 if their start points follow the other's end point. 1593 """ 1594 1595 if isinstance(other, ActsAsTimespan): 1596 other = other.as_timespan() 1597 1598 if self.end is not None and other.start is not None and self.is_before(self.end, other.start): 1599 return -1 1600 elif self.start is not None and other.end is not None and self.is_before(other.end, self.start): 1601 return 1 1602 else: 1603 return 0 1604 1605 else: 1606 if self.end is not None and self.is_before(self.end, other): 1607 return -1 1608 elif self.start is not None and self.is_before(other, self.start): 1609 return 1 1610 else: 1611 return 0 1612 1613 class TimespanCollection: 1614 1615 """ 1616 A collection of timespans providing a list-like interface supporting 1617 membership tests at a particular resolution. 1618 """ 1619 1620 def __init__(self, resolution, values=None): 1621 self.resolution = resolution 1622 self.values = values or [] 1623 1624 def as_timespan(self): 1625 return Timespan(*self.as_limits()) 1626 1627 def as_limits(self): 1628 1629 "Return the earliest and latest points in time for this collection." 1630 1631 if not self.values: 1632 return None, None 1633 else: 1634 first, last = self.values[0], self.values[-1] 1635 if isinstance(first, ActsAsTimespan): 1636 first = first.as_timespan().start 1637 if isinstance(last, ActsAsTimespan): 1638 last = last.as_timespan().end 1639 return first, last 1640 1641 def convert(self, value): 1642 if isinstance(value, ActsAsTimespan): 1643 return value.as_timespan().convert(self.resolution) 1644 else: 1645 return value.convert(self.resolution) 1646 1647 def __iter__(self): 1648 return iter(self.values) 1649 1650 def __len__(self): 1651 return len(self.values) 1652 1653 def __getitem__(self, i): 1654 return self.values[i] 1655 1656 def __setitem__(self, i, value): 1657 self.values[i] = value 1658 1659 def __contains__(self, value): 1660 test_value = self.convert(value) 1661 return test_value in self.values 1662 1663 def append(self, value): 1664 self.values.append(value) 1665 1666 def insert(self, i, value): 1667 self.values.insert(i, value) 1668 1669 def pop(self): 1670 return self.values.pop() 1671 1672 def insert_in_order(self, value): 1673 i = bisect.bisect_left(self, self.convert(value)) 1674 self.insert(i, value) 1675 1676 def items_in_range(self, start, end): 1677 slice_start = bisect.bisect_left(self, start) 1678 slice_end = bisect.bisect_right(self, end, slice_start) 1679 return self.values[slice_start:slice_end] 1680 1681 def getCountry(s): 1682 1683 "Find a country code in the given string 's'." 1684 1685 match = country_code_regexp.search(s) 1686 1687 if match: 1688 return match.group("code") 1689 else: 1690 return None 1691 1692 def getDate(s): 1693 1694 "Parse the string 's', extracting and returning a date object." 1695 1696 dt = getDateTime(s) 1697 if dt is not None: 1698 return dt.as_date() 1699 else: 1700 return None 1701 1702 def getDateTime(s): 1703 1704 """ 1705 Parse the string 's', extracting and returning a datetime object where time 1706 information has been given or a date object where time information is 1707 absent. 1708 """ 1709 1710 m = datetime_regexp.search(s) 1711 if m: 1712 groups = list(m.groups()) 1713 1714 # Convert date and time data to integer or None. 1715 1716 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date() 1717 else: 1718 return None 1719 1720 def getDateStrings(s): 1721 1722 "Parse the string 's', extracting and returning all date strings." 1723 1724 start = 0 1725 m = date_regexp.search(s, start) 1726 l = [] 1727 while m: 1728 l.append("-".join(m.groups())) 1729 m = date_regexp.search(s, m.end()) 1730 return l 1731 1732 def getMonth(s): 1733 1734 "Parse the string 's', extracting and returning a month object." 1735 1736 m = month_regexp.search(s) 1737 if m: 1738 return Month(map(int, m.groups())) 1739 else: 1740 return None 1741 1742 def getCurrentDate(): 1743 1744 "Return the current date as a (year, month, day) tuple." 1745 1746 today = datetime.date.today() 1747 return Date((today.year, today.month, today.day)) 1748 1749 def getCurrentMonth(): 1750 1751 "Return the current month as a (year, month) tuple." 1752 1753 today = datetime.date.today() 1754 return Month((today.year, today.month)) 1755 1756 def getCurrentYear(): 1757 1758 "Return the current year." 1759 1760 today = datetime.date.today() 1761 return today.year 1762 1763 # User interface functions. 1764 1765 def getParameter(request, name, default=None): 1766 1767 """ 1768 Using the given 'request', return the value of the parameter with the given 1769 'name', returning the optional 'default' (or None) if no value was supplied 1770 in the 'request'. 1771 """ 1772 1773 return get_form(request).get(name, [default])[0] 1774 1775 def getQualifiedParameter(request, calendar_name, argname, default=None): 1776 1777 """ 1778 Using the given 'request', 'calendar_name' and 'argname', retrieve the 1779 value of the qualified parameter, returning the optional 'default' (or None) 1780 if no value was supplied in the 'request'. 1781 """ 1782 1783 argname = getQualifiedParameterName(calendar_name, argname) 1784 return getParameter(request, argname, default) 1785 1786 def getQualifiedParameterName(calendar_name, argname): 1787 1788 """ 1789 Return the qualified parameter name using the given 'calendar_name' and 1790 'argname'. 1791 """ 1792 1793 if calendar_name is None: 1794 return argname 1795 else: 1796 return "%s-%s" % (calendar_name, argname) 1797 1798 def getParameterDate(arg): 1799 1800 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1801 1802 n = None 1803 1804 if arg is None: 1805 return None 1806 1807 elif arg.startswith("current"): 1808 date = getCurrentDate() 1809 if len(arg) > 8: 1810 n = int(arg[7:]) 1811 1812 elif arg.startswith("yearstart"): 1813 date = Date((getCurrentYear(), 1, 1)) 1814 if len(arg) > 10: 1815 n = int(arg[9:]) 1816 1817 elif arg.startswith("yearend"): 1818 date = Date((getCurrentYear(), 12, 31)) 1819 if len(arg) > 8: 1820 n = int(arg[7:]) 1821 1822 else: 1823 date = getDate(arg) 1824 1825 if n is not None: 1826 date = date.day_update(n) 1827 1828 return date 1829 1830 def getParameterMonth(arg): 1831 1832 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1833 1834 n = None 1835 1836 if arg is None: 1837 return None 1838 1839 elif arg.startswith("current"): 1840 date = getCurrentMonth() 1841 if len(arg) > 8: 1842 n = int(arg[7:]) 1843 1844 elif arg.startswith("yearstart"): 1845 date = Month((getCurrentYear(), 1)) 1846 if len(arg) > 10: 1847 n = int(arg[9:]) 1848 1849 elif arg.startswith("yearend"): 1850 date = Month((getCurrentYear(), 12)) 1851 if len(arg) > 8: 1852 n = int(arg[7:]) 1853 1854 else: 1855 date = getMonth(arg) 1856 1857 if n is not None: 1858 date = date.month_update(n) 1859 1860 return date 1861 1862 def getFormDate(request, calendar_name, argname): 1863 1864 """ 1865 Return the date from the 'request' for the calendar with the given 1866 'calendar_name' using the parameter having the given 'argname'. 1867 """ 1868 1869 arg = getQualifiedParameter(request, calendar_name, argname) 1870 return getParameterDate(arg) 1871 1872 def getFormMonth(request, calendar_name, argname): 1873 1874 """ 1875 Return the month from the 'request' for the calendar with the given 1876 'calendar_name' using the parameter having the given 'argname'. 1877 """ 1878 1879 arg = getQualifiedParameter(request, calendar_name, argname) 1880 return getParameterMonth(arg) 1881 1882 def getFormDateTriple(request, yeararg, montharg, dayarg): 1883 1884 """ 1885 Return the date from the 'request' for the calendar with the given 1886 'calendar_name' using the parameters having the given 'yeararg', 'montharg' 1887 and 'dayarg' names. 1888 """ 1889 1890 year = getParameter(request, yeararg) 1891 month = getParameter(request, montharg) 1892 day = getParameter(request, dayarg) 1893 if year and month and day: 1894 return Date((int(year), int(month), int(day))) 1895 else: 1896 return None 1897 1898 def getFormMonthPair(request, yeararg, montharg): 1899 1900 """ 1901 Return the month from the 'request' for the calendar with the given 1902 'calendar_name' using the parameters having the given 'yeararg' and 1903 'montharg' names. 1904 """ 1905 1906 year = getParameter(request, yeararg) 1907 month = getParameter(request, montharg) 1908 if year and month: 1909 return Month((int(year), int(month))) 1910 else: 1911 return None 1912 1913 def getFullDateLabel(request, date): 1914 1915 """ 1916 Return the full month plus year label using the given 'request' and 1917 'year_month'. 1918 """ 1919 1920 if not date: 1921 return "" 1922 1923 _ = request.getText 1924 year, month, day = date.as_tuple()[:3] 1925 start_weekday, number_of_days = date.month_properties() 1926 weekday = (start_weekday + day - 1) % 7 1927 day_label = _(getDayLabel(weekday)) 1928 month_label = _(getMonthLabel(month)) 1929 return "%s %s %s %s" % (day_label, day, month_label, year) 1930 1931 def getFullMonthLabel(request, year_month): 1932 1933 """ 1934 Return the full month plus year label using the given 'request' and 1935 'year_month'. 1936 """ 1937 1938 if not year_month: 1939 return "" 1940 1941 _ = request.getText 1942 year, month = year_month.as_tuple()[:2] 1943 month_label = _(getMonthLabel(month)) 1944 return "%s %s" % (month_label, year) 1945 1946 # Page-related functions. 1947 1948 def getPrettyPageName(page): 1949 1950 "Return a nicely formatted title/name for the given 'page'." 1951 1952 title = page.split_title(force=1) 1953 return getPrettyTitle(title) 1954 1955 def linkToPage(request, page, text, query_string=None): 1956 1957 """ 1958 Using 'request', return a link to 'page' with the given link 'text' and 1959 optional 'query_string'. 1960 """ 1961 1962 text = wikiutil.escape(text) 1963 return page.link_to_raw(request, text, query_string) 1964 1965 def getFullPageName(parent, title): 1966 1967 """ 1968 Return a full page name from the given 'parent' page (can be empty or None) 1969 and 'title' (a simple page name). 1970 """ 1971 1972 if parent: 1973 return "%s/%s" % (parent.rstrip("/"), title) 1974 else: 1975 return title 1976 1977 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1978 1979 """ 1980 Using the given 'template_page', complete the 'new_page' by copying the 1981 template and adding the given 'event_details' (a dictionary of event 1982 fields), setting also the 'category_pagenames' to define category 1983 membership. 1984 """ 1985 1986 event_page = EventPage(template_page) 1987 new_event_page = EventPage(new_page) 1988 new_event_page.copyPage(event_page) 1989 1990 if new_event_page.getFormat() == "wiki": 1991 new_event = Event(new_event_page, event_details) 1992 new_event_page.setEvents([new_event]) 1993 new_event_page.setCategoryMembership(category_pagenames) 1994 new_event_page.saveChanges() 1995 1996 # vim: tabstop=4 expandtab shiftwidth=4