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