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