1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator library 4 5 @copyright: 2008, 2009, 2010 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 19 try: 20 set 21 except NameError: 22 from sets import Set as set 23 24 try: 25 import pytz 26 except ImportError: 27 pytz = None 28 29 __version__ = "0.6" 30 31 # Date labels. 32 33 month_labels = ["January", "February", "March", "April", "May", "June", 34 "July", "August", "September", "October", "November", "December"] 35 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 36 37 # Regular expressions where MoinMoin does not provide the required support. 38 39 category_regexp = None 40 41 # Page parsing. 42 43 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 44 category_membership_regexp = re.compile(ur"^\s*((Category\S+)(\s+Category\S+)*)\s*$", re.MULTILINE | re.UNICODE) 45 46 # Value parsing. 47 48 country_code_regexp = re.compile(ur'(?:^|\W)(?P<code>[A-Z]{2})(?:$|\W+$)', re.UNICODE) 49 50 month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})' 51 date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})' 52 time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?' 53 timezone_offset_str = ur'(?P<offset>(UTC)?(?:(?P<sign>[-+])(?P<hours>[0-9]{2})(?::?(?P<minutes>[0-9]{2}))?))' 54 timezone_olson_str = ur'(?P<olson>[a-zA-Z]+(?:/[-_a-zA-Z]+){1,2})' 55 timezone_utc_str = ur'UTC' 56 timezone_regexp_str = ur'(?P<zone>' + timezone_offset_str + '|' + timezone_olson_str + '|' + timezone_utc_str + ')' 57 datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?' 58 59 month_regexp = re.compile(month_regexp_str, re.UNICODE) 60 date_regexp = re.compile(date_regexp_str, re.UNICODE) 61 time_regexp = re.compile(time_regexp_str, re.UNICODE) 62 datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE) 63 timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE) 64 timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE) 65 66 verbatim_regexp = re.compile(ur'(?:' 67 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 68 ur'|' 69 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 70 ur'|' 71 ur'`(?P<monospace>.*?)`' 72 ur'|' 73 ur'{{{(?P<preformatted>.*?)}}}' 74 ur')', re.UNICODE) 75 76 # Utility functions. 77 78 def isMoin15(): 79 return version.release.startswith("1.5.") 80 81 def getCategoryPattern(request): 82 global category_regexp 83 84 try: 85 return request.cfg.cache.page_category_regexact 86 except AttributeError: 87 88 # Use regular expression from MoinMoin 1.7.1 otherwise. 89 90 if category_regexp is None: 91 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 92 return category_regexp 93 94 def int_or_none(x): 95 if x is None: 96 return x 97 else: 98 return int(x) 99 100 # Textual representations. 101 102 def getHTTPTimeString(tmtuple): 103 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( 104 weekday_labels[tmtuple.tm_wday], 105 tmtuple.tm_mday, 106 month_labels[tmtuple.tm_mon -1], # zero-based labels 107 tmtuple.tm_year, 108 tmtuple.tm_hour, 109 tmtuple.tm_min, 110 tmtuple.tm_sec 111 ) 112 113 def getSimpleWikiText(text): 114 115 """ 116 Return the plain text representation of the given 'text' which may employ 117 certain Wiki syntax features, such as those providing verbatim or monospaced 118 text. 119 """ 120 121 # NOTE: Re-implementing support for verbatim text and linking avoidance. 122 123 return "".join([s for s in verbatim_regexp.split(text) if s is not None]) 124 125 def getEncodedWikiText(text): 126 127 "Encode the given 'text' in a verbatim representation." 128 129 return "<<Verbatim(%s)>>" % text 130 131 def getPrettyTitle(title): 132 133 "Return a nicely formatted version of the given 'title'." 134 135 return title.replace("_", " ").replace("/", u" ? ") 136 137 def getMonthLabel(month): 138 139 "Return an unlocalised label for the given 'month'." 140 141 return month_labels[month - 1] # zero-based labels 142 143 def getDayLabel(weekday): 144 145 "Return an unlocalised label for the given 'weekday'." 146 147 return weekday_labels[weekday] 148 149 # Action support functions. 150 151 def getPageRevision(page): 152 153 "Return the revision details dictionary for the given 'page'." 154 155 # From Page.edit_info... 156 157 if hasattr(page, "editlog_entry"): 158 line = page.editlog_entry() 159 else: 160 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 161 162 # Similar to Page.mtime_usecs behaviour... 163 164 if line: 165 timestamp = line.ed_time_usecs 166 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 167 comment = line.comment 168 else: 169 mtime = 0 170 comment = "" 171 172 return {"timestamp" : time.gmtime(mtime), "comment" : comment} 173 174 # Category discovery and searching. 175 176 def getCategories(request): 177 178 """ 179 From the AdvancedSearch macro, return a list of category page names using 180 the given 'request'. 181 """ 182 183 # This will return all pages with "Category" in the title. 184 185 cat_filter = getCategoryPattern(request).search 186 return request.rootpage.getPageList(filter=cat_filter) 187 188 def getCategoryMapping(category_pagenames, request): 189 190 """ 191 For the given 'category_pagenames' return a list of tuples of the form 192 (category name, category page name) using the given 'request'. 193 """ 194 195 cat_pattern = getCategoryPattern(request) 196 mapping = [] 197 for pagename in category_pagenames: 198 name = cat_pattern.match(pagename).group("key") 199 if name != "Category": 200 mapping.append((name, pagename)) 201 mapping.sort() 202 return mapping 203 204 def getCategoryPages(pagename, request): 205 206 """ 207 Return the pages associated with the given category 'pagename' using the 208 'request'. 209 """ 210 211 query = search.QueryParser().parse_query('category:%s' % pagename) 212 if isMoin15(): 213 results = search.searchPages(request, query) 214 results.sortByPagename() 215 else: 216 results = search.searchPages(request, query, "page_name") 217 218 cat_pattern = getCategoryPattern(request) 219 pages = [] 220 for page in results.hits: 221 if not cat_pattern.match(page.page_name): 222 pages.append(page) 223 return pages 224 225 # The main activity functions. 226 227 class EventPage: 228 229 "An event page." 230 231 def __init__(self, page): 232 self.page = page 233 self.events = None 234 self.body = None 235 self.categories = None 236 237 def copyPage(self, page): 238 239 "Copy the body of the given 'page'." 240 241 self.body = page.getBody() 242 243 def getPageURL(self, request): 244 245 "Using 'request', return the URL of this page." 246 247 page = self.page 248 249 if isMoin15(): 250 return request.getQualifiedURL(page.url(request)) 251 else: 252 return request.getQualifiedURL(page.url(request, relative=0)) 253 254 def getFormat(self): 255 256 "Get the format used on this page." 257 258 if isMoin15(): 259 return "wiki" # page.pi_format 260 else: 261 return self.page.pi["format"] 262 263 def getRevisions(self): 264 265 "Return a list of page revisions." 266 267 return self.page.getRevList() 268 269 def getPageRevision(self): 270 271 "Return the revision details dictionary for this page." 272 273 return getPageRevision(self.page) 274 275 def getPageName(self): 276 277 "Return the page name." 278 279 return self.page.page_name 280 281 def getPrettyPageName(self): 282 283 "Return a nicely formatted title/name for this page." 284 285 return getPrettyPageName(self.page) 286 287 def getBody(self): 288 289 "Get the current page body." 290 291 if self.body is None: 292 self.body = self.page.get_raw_body() 293 return self.body 294 295 def getEvents(self): 296 297 "Return a list of events from this page." 298 299 if self.events is None: 300 details = {} 301 self.events = [Event(self, details)] 302 303 if self.getFormat() == "wiki": 304 for match in definition_list_regexp.finditer(self.getBody()): 305 306 # Skip commented-out items. 307 308 if match.group("optcomment"): 309 continue 310 311 # Permit case-insensitive list terms. 312 313 term = match.group("term").lower() 314 desc = match.group("desc") 315 316 # Special value type handling. 317 318 # Dates. 319 320 if term in ("start", "end"): 321 desc = getDate(desc) 322 323 # Lists (whose elements may be quoted). 324 325 elif term in ("topics", "categories"): 326 desc = [getSimpleWikiText(value.strip()) for value in desc.split(",") if value.strip()] 327 328 # Labels which may well be quoted. 329 330 elif term in ("title", "summary", "description", "location"): 331 desc = getSimpleWikiText(desc) 332 333 if desc is not None: 334 335 # Handle apparent duplicates by creating a new set of 336 # details. 337 338 if details.has_key(term): 339 340 # Make a new event. 341 342 details = {} 343 self.events.append(Event(self, details)) 344 345 details[term] = desc 346 347 return self.events 348 349 def setEvents(self, events): 350 351 "Set the given 'events' on this page." 352 353 self.events = events 354 355 def getCategoryMembership(self): 356 357 "Get the category names from this page." 358 359 if self.categories is None: 360 body = self.getBody() 361 match = category_membership_regexp.search(body) 362 self.categories = match.findall().split() 363 364 return self.categories 365 366 def setCategoryMembership(self, category_names): 367 368 """ 369 Set the category membership for the page using the specified 370 'category_names'. 371 """ 372 373 self.categories = category_names 374 375 def flushEventDetails(self): 376 377 "Flush the current event details to this page's body text." 378 379 new_body_parts = [] 380 end_of_last_match = 0 381 body = self.getBody() 382 383 events = iter(self.getEvents()) 384 385 event = events.next() 386 event_details = event.getDetails() 387 replaced_terms = set() 388 389 for match in definition_list_regexp.finditer(body): 390 391 # Permit case-insensitive list terms. 392 393 term = match.group("term").lower() 394 desc = match.group("desc") 395 396 # Check that the term has not already been substituted. If so, 397 # get the next event. 398 399 if term in replaced_terms: 400 try: 401 event = events.next() 402 403 # No more events. 404 405 except StopIteration: 406 break 407 408 event_details = event.getDetails() 409 replaced_terms = set() 410 411 # Add preceding text to the new body. 412 413 new_body_parts.append(body[end_of_last_match:match.start()]) 414 415 # Get the matching regions, adding the term to the new body. 416 417 new_body_parts.append(match.group("wholeterm")) 418 419 # Special value type handling. 420 421 if event_details.has_key(term): 422 423 # Dates. 424 425 if term in ("start", "end"): 426 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 427 428 # Lists (whose elements may be quoted). 429 430 elif term in ("topics", "categories"): 431 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 432 433 # Labels which must be quoted. 434 435 elif term in ("title", "summary"): 436 desc = getEncodedWikiText(event_details[term]) 437 438 # Text which need not be quoted, but it will be Wiki text. 439 440 elif term in ("description", "link", "location"): 441 desc = event_details[term] 442 443 replaced_terms.add(term) 444 445 # Add the replaced value. 446 447 new_body_parts.append(desc) 448 449 # Remember where in the page has been processed. 450 451 end_of_last_match = match.end() 452 453 # Write the rest of the page. 454 455 new_body_parts.append(body[end_of_last_match:]) 456 457 self.body = "".join(new_body_parts) 458 459 def flushCategoryMembership(self): 460 461 "Flush the category membership to the page body." 462 463 body = self.getBody() 464 category_names = self.getCategoryMembership() 465 match = category_membership_regexp.search(body) 466 467 if match: 468 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 469 470 def saveChanges(self): 471 472 "Save changes to the event." 473 474 self.flushEventDetails() 475 self.flushCategoryMembership() 476 self.page.saveText(self.getBody(), 0) 477 478 def linkToPage(self, request, text, query_string=None): 479 480 """ 481 Using 'request', return a link to this page with the given link 'text' 482 and optional 'query_string'. 483 """ 484 485 return linkToPage(request, self.page, text, query_string) 486 487 class Event: 488 489 "A description of an event." 490 491 def __init__(self, page, details): 492 self.page = page 493 self.details = details 494 495 def __cmp__(self, other): 496 497 """ 498 Compare this object with 'other' using the event start and end details. 499 """ 500 501 details1 = self.details 502 details2 = other.details 503 return cmp( 504 (details1["start"], details1["end"]), 505 (details2["start"], details2["end"]) 506 ) 507 508 def getPage(self): 509 510 "Return the page describing this event." 511 512 return self.page 513 514 def setPage(self, page): 515 516 "Set the 'page' describing this event." 517 518 self.page = page 519 520 def getSummary(self, event_parent=None): 521 522 """ 523 Return either the given title or summary of the event according to the 524 event details, or a summary made from using the pretty version of the 525 page name. 526 527 If the optional 'event_parent' is specified, any page beneath the given 528 'event_parent' page in the page hierarchy will omit this parent information 529 if its name is used as the summary. 530 """ 531 532 event_details = self.details 533 534 if event_details.has_key("title"): 535 return event_details["title"] 536 elif event_details.has_key("summary"): 537 return event_details["summary"] 538 else: 539 # If appropriate, remove the parent details and "/" character. 540 541 title = self.page.getPageName() 542 543 if event_parent and title.startswith(event_parent): 544 title = title[len(event_parent.rstrip("/")) + 1:] 545 546 return getPrettyTitle(title) 547 548 def getDetails(self): 549 550 "Return the details for this event." 551 552 return self.details 553 554 def setDetails(self, event_details): 555 556 "Set the 'event_details' for this event." 557 558 self.details = event_details 559 560 def getEvents(request, category_names, calendar_start=None, calendar_end=None): 561 562 """ 563 Using the 'request', generate a list of events found on pages belonging to 564 the specified 'category_names', using the optional 'calendar_start' and 565 'calendar_end' month tuples of the form (year, month) to indicate a window 566 of interest. 567 568 Return a list of events, a dictionary mapping months to event lists (within 569 the window of interest), a list of all events within the window of interest, 570 the earliest month of an event within the window of interest, and the latest 571 month of an event within the window of interest. 572 """ 573 574 # Re-order the window, if appropriate. 575 576 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 577 calendar_start, calendar_end = calendar_end, calendar_start 578 579 events = [] 580 shown_events = {} 581 all_shown_events = [] 582 processed_pages = set() 583 584 earliest = None 585 latest = None 586 587 for category_name in category_names: 588 589 # Get the pages and page names in the category. 590 591 pages_in_category = getCategoryPages(category_name, request) 592 593 # Visit each page in the category. 594 595 for page_in_category in pages_in_category: 596 pagename = page_in_category.page_name 597 598 # Only process each page once. 599 600 if pagename in processed_pages: 601 continue 602 else: 603 processed_pages.add(pagename) 604 605 # Get a real page, not a result page. 606 607 event_page = EventPage(Page(request, pagename)) 608 609 # Get all events described in the page. 610 611 for event in event_page.getEvents(): 612 event_details = event.getDetails() 613 614 # Remember the event. 615 616 events.append(event) 617 618 # Test for the suitability of the event. 619 620 if event_details.has_key("start") and event_details.has_key("end"): 621 622 start_month = event_details["start"].as_month() 623 end_month = event_details["end"].as_month() 624 625 # Compare the months of the dates to the requested calendar 626 # window, if any. 627 628 if (calendar_start is None or end_month >= calendar_start) and \ 629 (calendar_end is None or start_month <= calendar_end): 630 631 all_shown_events.append(event) 632 633 if earliest is None or start_month < earliest: 634 earliest = start_month 635 if latest is None or end_month > latest: 636 latest = end_month 637 638 # Store the event in the month-specific dictionary. 639 640 first = max(start_month, calendar_start or start_month) 641 last = min(end_month, calendar_end or end_month) 642 643 for event_month in first.months_until(last): 644 if not shown_events.has_key(event_month): 645 shown_events[event_month] = [] 646 shown_events[event_month].append(event) 647 648 return events, shown_events, all_shown_events, earliest, latest 649 650 def setEventTimestamps(request, events): 651 652 """ 653 Using 'request', set timestamp details in the details dictionary of each of 654 the 'events'. 655 656 Retutn the latest timestamp found. 657 """ 658 659 latest = None 660 661 for event in events: 662 event_details = event.getDetails() 663 event_page = event.getPage() 664 665 # Get the initial revision of the page. 666 667 revisions = event_page.getRevisions() 668 event_page_initial = Page(request, event_page.getPageName(), rev=revisions[-1]) 669 670 # Get the created and last modified times. 671 672 initial_revision = getPageRevision(event_page_initial) 673 event_details["created"] = initial_revision["timestamp"] 674 latest_revision = event_page.getPageRevision() 675 event_details["last-modified"] = latest_revision["timestamp"] 676 event_details["sequence"] = len(revisions) - 1 677 event_details["last-comment"] = latest_revision["comment"] 678 679 if latest is None or latest < event_details["last-modified"]: 680 latest = event_details["last-modified"] 681 682 return latest 683 684 def getOrderedEvents(events): 685 686 """ 687 Return a list with the given 'events' ordered according to their start and 688 end dates. 689 """ 690 691 ordered_events = events[:] 692 ordered_events.sort() 693 return ordered_events 694 695 def getConcretePeriod(calendar_start, calendar_end, earliest, latest): 696 697 """ 698 From the requested 'calendar_start' and 'calendar_end', which may be None, 699 indicating that no restriction is imposed on the period for each of the 700 boundaries, use the 'earliest' and 'latest' event months to define a 701 specific period of interest. 702 """ 703 704 # Define the period as starting with any specified start month or the 705 # earliest event known, ending with any specified end month or the latest 706 # event known. 707 708 first = calendar_start or earliest 709 last = calendar_end or latest 710 711 # If there is no range of months to show, perhaps because there are no 712 # events in the requested period, and there was no start or end month 713 # specified, show only the month indicated by the start or end of the 714 # requested period. If all events were to be shown but none were found show 715 # the current month. 716 717 if first is None: 718 first = last or getCurrentMonth() 719 if last is None: 720 last = first or getCurrentMonth() 721 722 # Permit "expiring" periods (where the start date approaches the end date). 723 724 return min(first, last), last 725 726 def getCoverage(start, end, events): 727 728 """ 729 Within the period defined by the 'start' and 'end' dates, determine the 730 coverage of the days in the period by the given 'events', returning a set of 731 covered days, along with a list of slots, where each slot contains a tuple 732 of the form (set of covered days, events). 733 """ 734 735 all_events = [] 736 full_coverage = set() 737 738 # Get event details. 739 740 for event in events: 741 event_details = event.getDetails() 742 743 # Test for the event in the period. 744 745 if event_details["start"] <= end and event_details["end"] >= start: 746 747 # Find the coverage of this period for the event. 748 749 event_start = max(event_details["start"], start) 750 event_end = min(event_details["end"], end) 751 event_coverage = set(event_start.days_until(event_end)) 752 753 # Update the overall coverage. 754 755 full_coverage.update(event_coverage) 756 757 # Try and fit the event into the events list. 758 759 for i, (coverage, covered_events) in enumerate(all_events): 760 761 # Where the event does not overlap with the current 762 # element, add it alongside existing events. 763 764 if not coverage.intersection(event_coverage): 765 covered_events.append(event) 766 all_events[i] = coverage.union(event_coverage), covered_events 767 break 768 769 # Make a new element in the list if the event cannot be 770 # marked alongside existing events. 771 772 else: 773 all_events.append((event_coverage, [event])) 774 775 return full_coverage, all_events 776 777 # Date-related functions. 778 779 class Period: 780 781 "A simple period of time." 782 783 def __init__(self, data): 784 self.data = data 785 786 def months(self): 787 return self.data[0] * 12 + self.data[1] 788 789 class Temporal: 790 791 "A simple temporal representation, common to dates and times." 792 793 def __init__(self, data): 794 self.data = list(data) 795 796 def __repr__(self): 797 return "%s(%r)" % (self.__class__.__name__, self.data) 798 799 def __hash__(self): 800 return hash(self.as_tuple()) 801 802 def as_tuple(self): 803 return tuple(self.data) 804 805 def __cmp__(self, other): 806 data = self.as_tuple() 807 other_data = other.as_tuple() 808 length = min(len(data), len(other_data)) 809 return cmp(self.data[:length], other.data[:length]) 810 811 def until(self, start, end, nextfn, prevfn): 812 813 """ 814 Return a collection of units of time by starting from the given 'start' 815 and stepping across intervening units until 'end' is reached, using the 816 given 'nextfn' and 'prevfn' to step from one unit to the next. 817 """ 818 819 current = start 820 units = [current] 821 if current < end: 822 while current < end: 823 current = nextfn(current) 824 units.append(current) 825 elif current > end: 826 while current > end: 827 current = prevfn(current) 828 units.append(current) 829 return units 830 831 class Month(Temporal): 832 833 "A simple year-month representation." 834 835 def __str__(self): 836 return "%04d-%02d" % self.as_tuple()[:2] 837 838 def as_datetime(self, day, hour, minute, second, zone): 839 return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) 840 841 def as_date(self, day): 842 return Date(self.as_tuple() + (day,)) 843 844 def as_month(self): 845 return self 846 847 def year(self): 848 return self.data[0] 849 850 def month(self): 851 return self.data[1] 852 853 def month_properties(self): 854 855 """ 856 Return the weekday of the 1st of the month, along with the number of 857 days, as a tuple. 858 """ 859 860 year, month = self.as_tuple()[:2] 861 return calendar.monthrange(year, month) 862 863 def month_update(self, n=1): 864 865 "Return the month updated by 'n' months." 866 867 year, month = self.as_tuple()[:2] 868 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1)) 869 870 def next_month(self): 871 872 "Return the month following this one." 873 874 return self.month_update(1) 875 876 def previous_month(self): 877 878 "Return the month preceding this one." 879 880 return self.month_update(-1) 881 882 def __sub__(self, start): 883 884 """ 885 Return the difference in years and months between this month and the 886 'start' month as a period. 887 """ 888 889 return Period([(x - y) for x, y in zip(self.data, start.data)]) 890 891 def months_until(self, end): 892 893 "Return the collection of months from this month until 'end'." 894 895 return self.until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month) 896 897 class Date(Month): 898 899 "A simple year-month-day representation." 900 901 def constrain(self): 902 year, month, day = self.as_tuple()[:3] 903 904 month = max(min(month, 12), 1) 905 wd, last_day = calendar.monthrange(year, month) 906 day = max(min(day, last_day), 1) 907 908 self.data[1:3] = month, day 909 910 def __str__(self): 911 return "%04d-%02d-%02d" % self.as_tuple()[:3] 912 913 def as_datetime(self, hour, minute, second, zone): 914 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 915 916 def as_date(self): 917 return self 918 919 def as_month(self): 920 return Month(self.data[:2]) 921 922 def day(self): 923 return self.data[2] 924 925 def next_day(self): 926 927 "Return the date following this one." 928 929 year, month, day = self.as_tuple()[:3] 930 _wd, end_day = calendar.monthrange(year, month) 931 if day == end_day: 932 if month == 12: 933 return Date((year + 1, 1, 1)) 934 else: 935 return Date((year, month + 1, 1)) 936 else: 937 return Date((year, month, day + 1)) 938 939 def previous_day(self): 940 941 "Return the date preceding this one." 942 943 year, month, day = self.as_tuple()[:3] 944 if day == 1: 945 if month == 1: 946 return Date((year - 1, 12, 31)) 947 else: 948 _wd, end_day = calendar.monthrange(year, month - 1) 949 return Date((year, month - 1, end_day)) 950 else: 951 return Date((year, month, day - 1)) 952 953 def days_until(self, end): 954 955 "Return the collection of days from this date until 'end'." 956 957 return self.until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 958 959 class DateTime(Date): 960 961 "A simple date plus time representation." 962 963 def constrain(self): 964 Date.constrain(self) 965 966 hour, minute, second = self.as_tuple()[3:6] 967 968 if self.has_time(): 969 hour = max(min(hour, 23), 0) 970 minute = max(min(minute, 59), 0) 971 972 if second is not None: 973 second = max(min(second, 60), 0) # support leap seconds 974 975 self.data[3:6] = hour, minute, second 976 977 def __str__(self): 978 if self.has_time(): 979 data = self.as_tuple() 980 time_str = " %02d:%02d" % data[3:5] 981 if data[5] is not None: 982 time_str += ":%02d" % data[5] 983 if data[6] is not None: 984 time_str += " %s" % data[6] 985 else: 986 time_str = "" 987 988 return Date.__str__(self) + time_str 989 990 def as_datetime(self): 991 return self 992 993 def as_date(self): 994 return Date(self.data[:3]) 995 996 def has_time(self): 997 return self.data[3] is not None and self.data[4] is not None 998 999 def seconds(self): 1000 return self.data[5] 1001 1002 def time_zone(self): 1003 return self.data[6] 1004 1005 def set_time_zone(self, value): 1006 self.data[6] = value 1007 1008 def padded(self): 1009 1010 "Return a datetime with missing fields defined as being zero." 1011 1012 data = map(lambda x: x or 0, self.data[:6]) + self.data[6:] 1013 return DateTime(data) 1014 1015 def to_utc(self): 1016 1017 """ 1018 Return this object converted to UTC, or None if such a conversion is not 1019 defined. 1020 """ 1021 1022 offset = self.utc_offset() 1023 if offset: 1024 hours, minutes = offset 1025 1026 # Invert the offset to get the correction. 1027 1028 hours, minutes = -hours, -minutes 1029 1030 # Get the components. 1031 1032 hour, minute, second, zone = self.as_tuple()[3:] 1033 date = self.as_date() 1034 1035 # Add the minutes and hours. 1036 1037 minute += minutes 1038 if minute < 0 or minute > 59: 1039 hour += minute / 60 1040 minute = minute % 60 1041 1042 # NOTE: This makes various assumptions and probably would not work 1043 # NOTE: for general arithmetic. 1044 1045 hour += hours 1046 if hour < 0: 1047 date = date.previous_day() 1048 hour += 24 1049 elif hour > 23: 1050 date = date.next_day() 1051 hour -= 24 1052 1053 return date.as_datetime(hour, minute, second, "UTC") 1054 1055 # Cannot convert. 1056 1057 else: 1058 return None 1059 1060 def utc_offset(self): 1061 1062 "Return the UTC offset in hours and minutes." 1063 1064 zone = self.time_zone() 1065 if not zone: 1066 return None 1067 1068 # Support explicit UTC zones. 1069 1070 if zone == "UTC": 1071 return 0, 0 1072 1073 # Attempt to return a UTC offset where an explicit offset has been set. 1074 1075 match = timezone_offset_regexp.match(zone) 1076 if match: 1077 if match.group("sign") == "-": 1078 sign = -1 1079 else: 1080 sign = 1 1081 1082 hours = int(match.group("hours")) * sign 1083 minutes = int(match.group("minutes") or 0) * sign 1084 return hours, minutes 1085 1086 # Attempt to handle Olson time zone identifiers. 1087 1088 dt = self.as_olson_datetime() 1089 if dt: 1090 seconds = dt.utcoffset().seconds 1091 hours = seconds / 3600 1092 minutes = (seconds % 3600) / 60 1093 return hours, minutes 1094 1095 # Otherwise return None. 1096 1097 return None 1098 1099 def olson_identifier(self): 1100 1101 "Return the Olson identifier from any zone information." 1102 1103 zone = self.time_zone() 1104 if not zone: 1105 return None 1106 1107 # Attempt to match an identifier. 1108 1109 match = timezone_olson_regexp.match(zone) 1110 if match: 1111 return match.group("olson") 1112 else: 1113 return None 1114 1115 def _as_olson_datetime(self, hours=None): 1116 1117 """ 1118 Return a Python datetime object for this datetime interpreted using any 1119 Olson time zone identifier and the given 'hours' offset, raising one of 1120 the pytz exceptions in case of ambiguity. 1121 """ 1122 1123 olson = self.olson_identifier() 1124 if olson and pytz: 1125 tz = pytz.timezone(olson) 1126 data = self.padded().as_tuple()[:6] 1127 dt = datetime.datetime(*data) 1128 1129 # With an hours offset, find a time probably in a previously 1130 # applicable time zone. 1131 1132 if hours is not None: 1133 td = datetime.timedelta(0, hours * 3600) 1134 dt += td 1135 1136 ldt = tz.localize(dt, None) 1137 1138 # With an hours offset, adjust the time to define it within the 1139 # previously applicable time zone but at the presumably intended 1140 # position. 1141 1142 if hours is not None: 1143 ldt -= td 1144 1145 return ldt 1146 else: 1147 return None 1148 1149 def as_olson_datetime(self): 1150 1151 """ 1152 Return a Python datetime object for this datetime interpreted using any 1153 Olson time zone identifier, choosing the time from the zone before the 1154 period of ambiguity. 1155 """ 1156 1157 try: 1158 return self._as_olson_datetime() 1159 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1160 1161 # Try again, using an earlier local time and then stepping forward 1162 # in the chosen zone. 1163 # NOTE: Four hours earlier seems reasonable. 1164 1165 return self._as_olson_datetime(-4) 1166 1167 def ambiguous(self): 1168 1169 "Return whether the time is local and ambiguous." 1170 1171 try: 1172 self._as_olson_datetime() 1173 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1174 return 1 1175 1176 return 0 1177 1178 def getCountry(s): 1179 1180 "Find a country code in the given string 's'." 1181 1182 match = country_code_regexp.search(s) 1183 1184 if match: 1185 return match.group("code") 1186 else: 1187 return None 1188 1189 def getDate(s): 1190 1191 "Parse the string 's', extracting and returning a datetime object." 1192 1193 m = datetime_regexp.search(s) 1194 if m: 1195 groups = list(m.groups()) 1196 1197 # Convert date and time data to integer or None. 1198 1199 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]) 1200 else: 1201 return None 1202 1203 def getDateStrings(s): 1204 1205 "Parse the string 's', extracting and returning all date strings." 1206 1207 start = 0 1208 m = date_regexp.search(s, start) 1209 l = [] 1210 while m: 1211 l.append("-".join(m.groups())) 1212 m = date_regexp.search(s, m.end()) 1213 return l 1214 1215 def getMonth(s): 1216 1217 "Parse the string 's', extracting and returning a month object." 1218 1219 m = month_regexp.search(s) 1220 if m: 1221 return Month(map(int, m.groups())) 1222 else: 1223 return None 1224 1225 def getCurrentMonth(): 1226 1227 "Return the current month as a (year, month) tuple." 1228 1229 today = datetime.date.today() 1230 return Month((today.year, today.month)) 1231 1232 def getCurrentYear(): 1233 1234 "Return the current year." 1235 1236 today = datetime.date.today() 1237 return today.year 1238 1239 # User interface functions. 1240 1241 def getParameter(request, name, default=None): 1242 1243 """ 1244 Using the given 'request', return the value of the parameter with the given 1245 'name', returning the optional 'default' (or None) if no value was supplied 1246 in the 'request'. 1247 """ 1248 1249 return request.form.get(name, [default])[0] 1250 1251 def getQualifiedParameter(request, calendar_name, argname, default=None): 1252 1253 """ 1254 Using the given 'request', 'calendar_name' and 'argname', retrieve the 1255 value of the qualified parameter, returning the optional 'default' (or None) 1256 if no value was supplied in the 'request'. 1257 """ 1258 1259 argname = getQualifiedParameterName(calendar_name, argname) 1260 return getParameter(request, argname, default) 1261 1262 def getQualifiedParameterName(calendar_name, argname): 1263 1264 """ 1265 Return the qualified parameter name using the given 'calendar_name' and 1266 'argname'. 1267 """ 1268 1269 if calendar_name is None: 1270 return argname 1271 else: 1272 return "%s-%s" % (calendar_name, argname) 1273 1274 def getParameterMonth(arg): 1275 1276 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1277 1278 n = None 1279 1280 if arg.startswith("current"): 1281 date = getCurrentMonth() 1282 if len(arg) > 8: 1283 n = int(arg[7:]) 1284 1285 elif arg.startswith("yearstart"): 1286 date = Month((getCurrentYear(), 1)) 1287 if len(arg) > 10: 1288 n = int(arg[9:]) 1289 1290 elif arg.startswith("yearend"): 1291 date = Month((getCurrentYear(), 12)) 1292 if len(arg) > 8: 1293 n = int(arg[7:]) 1294 1295 else: 1296 date = getMonth(arg) 1297 1298 if n is not None: 1299 date = date.month_update(n) 1300 1301 return date 1302 1303 def getFormMonth(request, calendar_name, argname): 1304 1305 """ 1306 Return the month from the 'request' for the calendar with the given 1307 'calendar_name' using the parameter having the given 'argname'. 1308 """ 1309 1310 arg = getQualifiedParameter(request, calendar_name, argname) 1311 if arg is not None: 1312 return getParameterMonth(arg) 1313 else: 1314 return None 1315 1316 def getFormMonthPair(request, yeararg, montharg): 1317 1318 """ 1319 Return the month from the 'request' for the calendar with the given 1320 'calendar_name' using the parameters having the given 'yeararg' and 1321 'montharg' names. 1322 """ 1323 1324 year = getParameter(request, yeararg) 1325 month = getParameter(request, montharg) 1326 if year and month: 1327 return Month((int(year), int(month))) 1328 else: 1329 return None 1330 1331 # Page-related functions. 1332 1333 def getPrettyPageName(page): 1334 1335 "Return a nicely formatted title/name for the given 'page'." 1336 1337 if isMoin15(): 1338 title = page.split_title(page.request, force=1) 1339 else: 1340 title = page.split_title(force=1) 1341 1342 return getPrettyTitle(title) 1343 1344 def linkToPage(request, page, text, query_string=None): 1345 1346 """ 1347 Using 'request', return a link to 'page' with the given link 'text' and 1348 optional 'query_string'. 1349 """ 1350 1351 text = wikiutil.escape(text) 1352 1353 if isMoin15(): 1354 url = wikiutil.quoteWikinameURL(page.page_name) 1355 if query_string is not None: 1356 url = "%s?%s" % (url, query_string) 1357 return wikiutil.link_tag(request, url, text, getattr(page, "formatter", None)) 1358 else: 1359 return page.link_to_raw(request, text, query_string) 1360 1361 def getFullPageName(parent, title): 1362 1363 """ 1364 Return a full page name from the given 'parent' page (can be empty or None) 1365 and 'title' (a simple page name). 1366 """ 1367 1368 if parent: 1369 return "%s/%s" % (parent.rstrip("/"), title) 1370 else: 1371 return title 1372 1373 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1374 1375 """ 1376 Using the given 'template_page', complete the 'new_page' by copying the 1377 template and adding the given 'event_details' (a dictionary of event 1378 fields), setting also the 'category_pagenames' to define category 1379 membership. 1380 """ 1381 1382 event_page = EventPage(template_page) 1383 new_event_page = EventPage(new_page) 1384 new_event_page.copyPage(event_page) 1385 1386 if new_event_page.getFormat() == "wiki": 1387 new_event = Event(new_event_page, event_details) 1388 new_event_page.setEvents([new_event]) 1389 new_event_page.setCategoryMembership(category_pagenames) 1390 new_event_page.saveChanges() 1391 1392 # vim: tabstop=4 expandtab shiftwidth=4