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