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