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