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 step_to = months_until 1175 1176 class Date(Month): 1177 1178 "A simple year-month-day representation." 1179 1180 def constrain(self): 1181 year, month, day = self.as_tuple()[:3] 1182 1183 month = max(min(month, 12), 1) 1184 wd, last_day = calendar.monthrange(year, month) 1185 day = max(min(day, last_day), 1) 1186 1187 self.data[1:3] = month, day 1188 1189 def __str__(self): 1190 return "%04d-%02d-%02d" % self.as_tuple()[:3] 1191 1192 def as_datetime(self, hour, minute, second, zone): 1193 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 1194 1195 def as_date(self): 1196 return self 1197 1198 def as_datetime_or_date(self): 1199 return self 1200 1201 def as_month(self): 1202 return Month(self.data[:2]) 1203 1204 def day(self): 1205 return self.data[2] 1206 1207 def day_update(self, n=1): 1208 1209 "Return the month updated by 'n' days." 1210 1211 delta = datetime.timedelta(n) 1212 dt = datetime.date(*self.as_tuple()[:3]) 1213 dt_new = dt + delta 1214 return Date((dt_new.year, dt_new.month, dt_new.day)) 1215 1216 def next_day(self): 1217 1218 "Return the date following this one." 1219 1220 year, month, day = self.as_tuple()[:3] 1221 _wd, end_day = calendar.monthrange(year, month) 1222 if day == end_day: 1223 if month == 12: 1224 return Date((year + 1, 1, 1)) 1225 else: 1226 return Date((year, month + 1, 1)) 1227 else: 1228 return Date((year, month, day + 1)) 1229 1230 def previous_day(self): 1231 1232 "Return the date preceding this one." 1233 1234 year, month, day = self.as_tuple()[:3] 1235 if day == 1: 1236 if month == 1: 1237 return Date((year - 1, 12, 31)) 1238 else: 1239 _wd, end_day = calendar.monthrange(year, month - 1) 1240 return Date((year, month - 1, end_day)) 1241 else: 1242 return Date((year, month, day - 1)) 1243 1244 def days_until(self, end): 1245 1246 "Return the collection of days from this date until 'end'." 1247 1248 return self.until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 1249 1250 step_to = days_until 1251 1252 class DateTime(Date): 1253 1254 "A simple date plus time representation." 1255 1256 def constrain(self): 1257 Date.constrain(self) 1258 1259 hour, minute, second = self.as_tuple()[3:6] 1260 1261 if self.has_time(): 1262 hour = max(min(hour, 23), 0) 1263 minute = max(min(minute, 59), 0) 1264 1265 if second is not None: 1266 second = max(min(second, 60), 0) # support leap seconds 1267 1268 self.data[3:6] = hour, minute, second 1269 1270 def __str__(self): 1271 return Date.__str__(self) + self.time_string() 1272 1273 def time_string(self): 1274 if self.has_time(): 1275 data = self.as_tuple() 1276 time_str = " %02d:%02d" % data[3:5] 1277 if data[5] is not None: 1278 time_str += ":%02d" % data[5] 1279 if data[6] is not None: 1280 time_str += " %s" % data[6] 1281 return time_str 1282 else: 1283 return "" 1284 1285 def as_datetime(self): 1286 return self 1287 1288 def as_date(self): 1289 return Date(self.data[:3]) 1290 1291 def as_datetime_or_date(self): 1292 1293 """ 1294 Return a date for this datetime if fields are missing. Otherwise, return 1295 this datetime itself. 1296 """ 1297 1298 if not self.has_time(): 1299 return self.as_date() 1300 else: 1301 return self 1302 1303 def __cmp__(self, other): 1304 1305 """ 1306 The result of comparing this instance with 'other' is, if both instances 1307 are datetime instances, derived from a comparison of the datetimes 1308 converted to UTC. If one or both datetimes cannot be converted to UTC, 1309 the datetimes are compared using the basic temporal comparison which 1310 compares their raw time data. 1311 """ 1312 1313 this = self 1314 1315 if this.has_time(): 1316 if isinstance(other, DateTime): 1317 if other.has_time(): 1318 this_utc = this.to_utc() 1319 other_utc = other.to_utc() 1320 if this_utc is not None and other_utc is not None: 1321 return cmp(this_utc.as_tuple(), other_utc.as_tuple()) 1322 else: 1323 other = other.padded() 1324 else: 1325 this = this.padded() 1326 1327 return Date.__cmp__(this, other) 1328 1329 def has_time(self): 1330 1331 """ 1332 Return whether this object has any time information. Objects without 1333 time information can refer to the very start of a day. 1334 """ 1335 1336 return self.data[3] is not None and self.data[4] is not None 1337 1338 def time(self): 1339 return self.data[3:] 1340 1341 def seconds(self): 1342 return self.data[5] 1343 1344 def time_zone(self): 1345 return self.data[6] 1346 1347 def set_time_zone(self, value): 1348 self.data[6] = value 1349 1350 def padded(self, empty_value=0): 1351 1352 """ 1353 Return a datetime with missing fields defined as being the given 1354 'empty_value' or 0 if not specified. 1355 """ 1356 1357 data = [] 1358 for x in self.data[:6]: 1359 if x is None: 1360 data.append(empty_value) 1361 else: 1362 data.append(x) 1363 1364 data += self.data[6:] 1365 return DateTime(data) 1366 1367 def to_utc(self): 1368 1369 """ 1370 Return this object converted to UTC, or None if such a conversion is not 1371 defined. 1372 """ 1373 1374 if not self.has_time(): 1375 return None 1376 1377 offset = self.utc_offset() 1378 if offset: 1379 hours, minutes = offset 1380 1381 # Invert the offset to get the correction. 1382 1383 hours, minutes = -hours, -minutes 1384 1385 # Get the components. 1386 1387 hour, minute, second, zone = self.time() 1388 date = self.as_date() 1389 1390 # Add the minutes and hours. 1391 1392 minute += minutes 1393 if minute < 0 or minute > 59: 1394 hour += minute / 60 1395 minute = minute % 60 1396 1397 # NOTE: This makes various assumptions and probably would not work 1398 # NOTE: for general arithmetic. 1399 1400 hour += hours 1401 if hour < 0: 1402 date = date.previous_day() 1403 hour += 24 1404 elif hour > 23: 1405 date = date.next_day() 1406 hour -= 24 1407 1408 return date.as_datetime(hour, minute, second, "UTC") 1409 1410 # Cannot convert. 1411 1412 else: 1413 return None 1414 1415 def utc_offset(self): 1416 1417 "Return the UTC offset in hours and minutes." 1418 1419 zone = self.time_zone() 1420 if not zone: 1421 return None 1422 1423 # Support explicit UTC zones. 1424 1425 if zone == "UTC": 1426 return 0, 0 1427 1428 # Attempt to return a UTC offset where an explicit offset has been set. 1429 1430 match = timezone_offset_regexp.match(zone) 1431 if match: 1432 if match.group("sign") == "-": 1433 sign = -1 1434 else: 1435 sign = 1 1436 1437 hours = int(match.group("hours")) * sign 1438 minutes = int(match.group("minutes") or 0) * sign 1439 return hours, minutes 1440 1441 # Attempt to handle Olson time zone identifiers. 1442 1443 dt = self.as_olson_datetime() 1444 if dt: 1445 seconds = dt.utcoffset().seconds 1446 hours = seconds / 3600 1447 minutes = (seconds % 3600) / 60 1448 return hours, minutes 1449 1450 # Otherwise return None. 1451 1452 return None 1453 1454 def olson_identifier(self): 1455 1456 "Return the Olson identifier from any zone information." 1457 1458 zone = self.time_zone() 1459 if not zone: 1460 return None 1461 1462 # Attempt to match an identifier. 1463 1464 match = timezone_olson_regexp.match(zone) 1465 if match: 1466 return match.group("olson") 1467 else: 1468 return None 1469 1470 def _as_olson_datetime(self, hours=None): 1471 1472 """ 1473 Return a Python datetime object for this datetime interpreted using any 1474 Olson time zone identifier and the given 'hours' offset, raising one of 1475 the pytz exceptions in case of ambiguity. 1476 """ 1477 1478 olson = self.olson_identifier() 1479 if olson and pytz: 1480 tz = pytz.timezone(olson) 1481 data = self.padded().as_tuple()[:6] 1482 dt = datetime.datetime(*data) 1483 1484 # With an hours offset, find a time probably in a previously 1485 # applicable time zone. 1486 1487 if hours is not None: 1488 td = datetime.timedelta(0, hours * 3600) 1489 dt += td 1490 1491 ldt = tz.localize(dt, None) 1492 1493 # With an hours offset, adjust the time to define it within the 1494 # previously applicable time zone but at the presumably intended 1495 # position. 1496 1497 if hours is not None: 1498 ldt -= td 1499 1500 return ldt 1501 else: 1502 return None 1503 1504 def as_olson_datetime(self): 1505 1506 """ 1507 Return a Python datetime object for this datetime interpreted using any 1508 Olson time zone identifier, choosing the time from the zone before the 1509 period of ambiguity. 1510 """ 1511 1512 try: 1513 return self._as_olson_datetime() 1514 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1515 1516 # Try again, using an earlier local time and then stepping forward 1517 # in the chosen zone. 1518 # NOTE: Four hours earlier seems reasonable. 1519 1520 return self._as_olson_datetime(-4) 1521 1522 def ambiguous(self): 1523 1524 "Return whether the time is local and ambiguous." 1525 1526 try: 1527 self._as_olson_datetime() 1528 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1529 return 1 1530 1531 return 0 1532 1533 class Timespan(ActsAsTimespan, Convertible): 1534 1535 """ 1536 A period of time which can be compared against others to check for overlaps. 1537 """ 1538 1539 def __init__(self, start, end): 1540 self.start = start 1541 self.end = end 1542 1543 def __repr__(self): 1544 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 1545 1546 def __hash__(self): 1547 return hash((self.start, self.end)) 1548 1549 def as_timespan(self): 1550 return self 1551 1552 def as_limits(self): 1553 return self.start, self.end 1554 1555 def convert(self, resolution): 1556 return Timespan(*map(self._get_converter(resolution), self.as_limits())) 1557 1558 def is_before(self, a, b): 1559 1560 """ 1561 Return whether 'a' is before 'b'. Since the end datetime of one period 1562 may be the same as the start datetime of another period, and yet the 1563 first period is intended to be concluded by the end datetime and not 1564 overlap with the other period, a different test is employed for datetime 1565 comparisons. 1566 """ 1567 1568 # Datetimes without times can be equal to dates and be considered as 1569 # occurring before those dates. 1570 1571 if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()): 1572 return a <= b 1573 else: 1574 return a < b 1575 1576 def __contains__(self, other): 1577 1578 """ 1579 This instance is considered to contain 'other' if one is not before or 1580 after the other. If this instance overlaps or coincides with 'other', 1581 then 'other' is regarded as belonging to this instance's time period. 1582 """ 1583 1584 return self == other 1585 1586 def __cmp__(self, other): 1587 1588 """ 1589 Return whether this timespan occupies the same period of time as the 1590 'other'. Timespans are considered less than others if their end points 1591 precede the other's start point, and are considered greater than others 1592 if their start points follow the other's end point. 1593 """ 1594 1595 if isinstance(other, ActsAsTimespan): 1596 other = other.as_timespan() 1597 1598 if self.end is not None and other.start is not None and self.is_before(self.end, other.start): 1599 return -1 1600 elif self.start is not None and other.end is not None and self.is_before(other.end, self.start): 1601 return 1 1602 else: 1603 return 0 1604 1605 else: 1606 if self.end is not None and self.is_before(self.end, other): 1607 return -1 1608 elif self.start is not None and self.is_before(other, self.start): 1609 return 1 1610 else: 1611 return 0 1612 1613 class TimespanCollection: 1614 1615 """ 1616 A collection of timespans providing a list-like interface supporting 1617 membership tests at a particular resolution. 1618 """ 1619 1620 def __init__(self, resolution, values=None): 1621 self.resolution = resolution 1622 self.values = values or [] 1623 1624 def as_timespan(self): 1625 return Timespan(*self.as_limits()) 1626 1627 def as_limits(self): 1628 1629 "Return the earliest and latest points in time for this collection." 1630 1631 if not self.values: 1632 return None, None 1633 else: 1634 first, last = self.values[0], self.values[-1] 1635 if isinstance(first, ActsAsTimespan): 1636 first = first.as_timespan().start 1637 if isinstance(last, ActsAsTimespan): 1638 last = last.as_timespan().end 1639 return first, last 1640 1641 def convert(self, value): 1642 if isinstance(value, ActsAsTimespan): 1643 return value.as_timespan().convert(self.resolution) 1644 else: 1645 return value.convert(self.resolution) 1646 1647 def __iter__(self): 1648 return iter(self.values) 1649 1650 def __len__(self): 1651 return len(self.values) 1652 1653 def __getitem__(self, i): 1654 return self.values[i] 1655 1656 def __setitem__(self, i, value): 1657 self.values[i] = value 1658 1659 def __contains__(self, value): 1660 test_value = self.convert(value) 1661 return test_value in self.values 1662 1663 def append(self, value): 1664 self.values.append(value) 1665 1666 def insert(self, i, value): 1667 self.values.insert(i, value) 1668 1669 def pop(self): 1670 return self.values.pop() 1671 1672 def insert_in_order(self, value): 1673 bisect.insort_left(self, value) 1674 1675 def items_in_range(self, start, end): 1676 slice_start = bisect.bisect_left(self, start) 1677 slice_end = bisect.bisect_right(self, end, slice_start) 1678 return self.values[slice_start:slice_end] 1679 1680 def getCountry(s): 1681 1682 "Find a country code in the given string 's'." 1683 1684 match = country_code_regexp.search(s) 1685 1686 if match: 1687 return match.group("code") 1688 else: 1689 return None 1690 1691 def getDate(s): 1692 1693 "Parse the string 's', extracting and returning a date object." 1694 1695 dt = getDateTime(s) 1696 if dt is not None: 1697 return dt.as_date() 1698 else: 1699 return None 1700 1701 def getDateTime(s): 1702 1703 """ 1704 Parse the string 's', extracting and returning a datetime object where time 1705 information has been given or a date object where time information is 1706 absent. 1707 """ 1708 1709 m = datetime_regexp.search(s) 1710 if m: 1711 groups = list(m.groups()) 1712 1713 # Convert date and time data to integer or None. 1714 1715 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date() 1716 else: 1717 return None 1718 1719 def getDateStrings(s): 1720 1721 "Parse the string 's', extracting and returning all date strings." 1722 1723 start = 0 1724 m = date_regexp.search(s, start) 1725 l = [] 1726 while m: 1727 l.append("-".join(m.groups())) 1728 m = date_regexp.search(s, m.end()) 1729 return l 1730 1731 def getMonth(s): 1732 1733 "Parse the string 's', extracting and returning a month object." 1734 1735 m = month_regexp.search(s) 1736 if m: 1737 return Month(map(int, m.groups())) 1738 else: 1739 return None 1740 1741 def getCurrentDate(): 1742 1743 "Return the current date as a (year, month, day) tuple." 1744 1745 today = datetime.date.today() 1746 return Date((today.year, today.month, today.day)) 1747 1748 def getCurrentMonth(): 1749 1750 "Return the current month as a (year, month) tuple." 1751 1752 today = datetime.date.today() 1753 return Month((today.year, today.month)) 1754 1755 def getCurrentYear(): 1756 1757 "Return the current year." 1758 1759 today = datetime.date.today() 1760 return today.year 1761 1762 # User interface functions. 1763 1764 def getParameter(request, name, default=None): 1765 1766 """ 1767 Using the given 'request', return the value of the parameter with the given 1768 'name', returning the optional 'default' (or None) if no value was supplied 1769 in the 'request'. 1770 """ 1771 1772 return get_form(request).get(name, [default])[0] 1773 1774 def getQualifiedParameter(request, calendar_name, argname, default=None): 1775 1776 """ 1777 Using the given 'request', 'calendar_name' and 'argname', retrieve the 1778 value of the qualified parameter, returning the optional 'default' (or None) 1779 if no value was supplied in the 'request'. 1780 """ 1781 1782 argname = getQualifiedParameterName(calendar_name, argname) 1783 return getParameter(request, argname, default) 1784 1785 def getQualifiedParameterName(calendar_name, argname): 1786 1787 """ 1788 Return the qualified parameter name using the given 'calendar_name' and 1789 'argname'. 1790 """ 1791 1792 if calendar_name is None: 1793 return argname 1794 else: 1795 return "%s-%s" % (calendar_name, argname) 1796 1797 def getParameterDate(arg): 1798 1799 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1800 1801 n = None 1802 1803 if arg is None: 1804 return None 1805 1806 elif arg.startswith("current"): 1807 date = getCurrentDate() 1808 if len(arg) > 8: 1809 n = int(arg[7:]) 1810 1811 elif arg.startswith("yearstart"): 1812 date = Date((getCurrentYear(), 1, 1)) 1813 if len(arg) > 10: 1814 n = int(arg[9:]) 1815 1816 elif arg.startswith("yearend"): 1817 date = Date((getCurrentYear(), 12, 31)) 1818 if len(arg) > 8: 1819 n = int(arg[7:]) 1820 1821 else: 1822 date = getDate(arg) 1823 1824 if n is not None: 1825 date = date.day_update(n) 1826 1827 return date 1828 1829 def getParameterMonth(arg): 1830 1831 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1832 1833 n = None 1834 1835 if arg is None: 1836 return None 1837 1838 elif arg.startswith("current"): 1839 date = getCurrentMonth() 1840 if len(arg) > 8: 1841 n = int(arg[7:]) 1842 1843 elif arg.startswith("yearstart"): 1844 date = Month((getCurrentYear(), 1)) 1845 if len(arg) > 10: 1846 n = int(arg[9:]) 1847 1848 elif arg.startswith("yearend"): 1849 date = Month((getCurrentYear(), 12)) 1850 if len(arg) > 8: 1851 n = int(arg[7:]) 1852 1853 else: 1854 date = getMonth(arg) 1855 1856 if n is not None: 1857 date = date.month_update(n) 1858 1859 return date 1860 1861 def getFormDate(request, calendar_name, argname): 1862 1863 """ 1864 Return the date from the 'request' for the calendar with the given 1865 'calendar_name' using the parameter having the given 'argname'. 1866 """ 1867 1868 arg = getQualifiedParameter(request, calendar_name, argname) 1869 return getParameterDate(arg) 1870 1871 def getFormMonth(request, calendar_name, argname): 1872 1873 """ 1874 Return the month from the 'request' for the calendar with the given 1875 'calendar_name' using the parameter having the given 'argname'. 1876 """ 1877 1878 arg = getQualifiedParameter(request, calendar_name, argname) 1879 return getParameterMonth(arg) 1880 1881 def getFormDateTriple(request, yeararg, montharg, dayarg): 1882 1883 """ 1884 Return the date from the 'request' for the calendar with the given 1885 'calendar_name' using the parameters having the given 'yeararg', 'montharg' 1886 and 'dayarg' names. 1887 """ 1888 1889 year = getParameter(request, yeararg) 1890 month = getParameter(request, montharg) 1891 day = getParameter(request, dayarg) 1892 if year and month and day: 1893 return Date((int(year), int(month), int(day))) 1894 else: 1895 return None 1896 1897 def getFormMonthPair(request, yeararg, montharg): 1898 1899 """ 1900 Return the month from the 'request' for the calendar with the given 1901 'calendar_name' using the parameters having the given 'yeararg' and 1902 'montharg' names. 1903 """ 1904 1905 year = getParameter(request, yeararg) 1906 month = getParameter(request, montharg) 1907 if year and month: 1908 return Month((int(year), int(month))) 1909 else: 1910 return None 1911 1912 def getFullDateLabel(request, date): 1913 1914 """ 1915 Return the full month plus year label using the given 'request' and 1916 'year_month'. 1917 """ 1918 1919 if not date: 1920 return "" 1921 1922 _ = request.getText 1923 year, month, day = date.as_tuple()[:3] 1924 start_weekday, number_of_days = date.month_properties() 1925 weekday = (start_weekday + day - 1) % 7 1926 day_label = _(getDayLabel(weekday)) 1927 month_label = _(getMonthLabel(month)) 1928 return "%s %s %s %s" % (day_label, day, month_label, year) 1929 1930 def getFullMonthLabel(request, year_month): 1931 1932 """ 1933 Return the full month plus year label using the given 'request' and 1934 'year_month'. 1935 """ 1936 1937 if not year_month: 1938 return "" 1939 1940 _ = request.getText 1941 year, month = year_month.as_tuple()[:2] 1942 month_label = _(getMonthLabel(month)) 1943 return "%s %s" % (month_label, year) 1944 1945 # Page-related functions. 1946 1947 def getPrettyPageName(page): 1948 1949 "Return a nicely formatted title/name for the given 'page'." 1950 1951 title = page.split_title(force=1) 1952 return getPrettyTitle(title) 1953 1954 def linkToPage(request, page, text, query_string=None): 1955 1956 """ 1957 Using 'request', return a link to 'page' with the given link 'text' and 1958 optional 'query_string'. 1959 """ 1960 1961 text = wikiutil.escape(text) 1962 return page.link_to_raw(request, text, query_string) 1963 1964 def getFullPageName(parent, title): 1965 1966 """ 1967 Return a full page name from the given 'parent' page (can be empty or None) 1968 and 'title' (a simple page name). 1969 """ 1970 1971 if parent: 1972 return "%s/%s" % (parent.rstrip("/"), title) 1973 else: 1974 return title 1975 1976 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1977 1978 """ 1979 Using the given 'template_page', complete the 'new_page' by copying the 1980 template and adding the given 'event_details' (a dictionary of event 1981 fields), setting also the 'category_pagenames' to define category 1982 membership. 1983 """ 1984 1985 event_page = EventPage(template_page) 1986 new_event_page = EventPage(new_page) 1987 new_event_page.copyPage(event_page) 1988 1989 if new_event_page.getFormat() == "wiki": 1990 new_event = Event(new_event_page, event_details) 1991 new_event_page.setEvents([new_event]) 1992 new_event_page.setCategoryMembership(category_pagenames) 1993 new_event_page.saveChanges() 1994 1995 # vim: tabstop=4 expandtab shiftwidth=4