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 class Month(Temporal): 812 813 "A simple year-month representation." 814 815 def __str__(self): 816 return "%04d-%02d" % self.as_tuple()[:2] 817 818 def as_datetime(self, day, hour, minute, second, zone): 819 return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) 820 821 def as_date(self, day): 822 return Date(self.as_tuple() + (day,)) 823 824 def as_month(self): 825 return self 826 827 def year(self): 828 return self.data[0] 829 830 def month(self): 831 return self.data[1] 832 833 def month_properties(self): 834 835 """ 836 Return the weekday of the 1st of the month, along with the number of 837 days, as a tuple. 838 """ 839 840 year, month = self.as_tuple()[:2] 841 return calendar.monthrange(year, month) 842 843 def month_update(self, n=1): 844 845 "Return the month updated by 'n' months." 846 847 year, month = self.as_tuple()[:2] 848 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1)) 849 850 def next_month(self): 851 852 "Return the month following this one." 853 854 return self.month_update(1) 855 856 def previous_month(self): 857 858 "Return the month preceding this one." 859 860 return self.month_update(-1) 861 862 def __sub__(self, start): 863 864 """ 865 Return the difference in years and months between this month and the 866 'start' month as a period. 867 """ 868 869 return Period([(x - y) for x, y in zip(self.data, start.data)]) 870 871 def until(self, start, end, nextfn, prevfn): 872 873 """ 874 Return a collection of units of time by starting from the given 'start' 875 and stepping across intervening units until 'end' is reached, using the 876 given 'nextfn' and 'prevfn' to step from one unit to the next. 877 """ 878 879 current = start 880 units = [current] 881 if current < end: 882 while current < end: 883 current = nextfn(current) 884 units.append(current) 885 elif current > end: 886 while current > end: 887 current = prevfn(current) 888 units.append(current) 889 return units 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: 1039 hour -= 1 1040 minute += 60 1041 elif minute > 59: 1042 hour += 1 1043 minute -= 60 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 return None 1087 1088 def olson_identifier(self): 1089 1090 "Return the Olson identifier from any zone information." 1091 1092 zone = self.time_zone() 1093 if not zone: 1094 return None 1095 1096 # Attempt to match an identifier. 1097 1098 match = timezone_olson_regexp.match(zone) 1099 if match: 1100 return match.group("olson") 1101 else: 1102 return None 1103 1104 def ambiguous(self): 1105 1106 "Return whether the time is local and ambiguous." 1107 1108 olson = self.olson_identifier() 1109 if olson and pytz: 1110 try: 1111 tz = pytz.timezone(olson) 1112 data = self.padded().as_tuple()[:6] 1113 dt = tz.localize(datetime.datetime(*data), None) 1114 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1115 return 1 1116 1117 return 0 1118 1119 def getCountry(s): 1120 1121 "Find a country code in the given string 's'." 1122 1123 match = country_code_regexp.search(s) 1124 1125 if match: 1126 return match.group("code") 1127 else: 1128 return None 1129 1130 def getDate(s): 1131 1132 "Parse the string 's', extracting and returning a datetime object." 1133 1134 m = datetime_regexp.search(s) 1135 if m: 1136 groups = list(m.groups()) 1137 1138 # Convert date and time data to integer or None. 1139 1140 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]) 1141 else: 1142 return None 1143 1144 def getDateStrings(s): 1145 1146 "Parse the string 's', extracting and returning all date strings." 1147 1148 start = 0 1149 m = date_regexp.search(s, start) 1150 l = [] 1151 while m: 1152 l.append("-".join(m.groups())) 1153 m = date_regexp.search(s, m.end()) 1154 return l 1155 1156 def getMonth(s): 1157 1158 "Parse the string 's', extracting and returning a month object." 1159 1160 m = month_regexp.search(s) 1161 if m: 1162 return Month(map(int, m.groups())) 1163 else: 1164 return None 1165 1166 def getCurrentMonth(): 1167 1168 "Return the current month as a (year, month) tuple." 1169 1170 today = datetime.date.today() 1171 return Month((today.year, today.month)) 1172 1173 def getCurrentYear(): 1174 1175 "Return the current year." 1176 1177 today = datetime.date.today() 1178 return today.year 1179 1180 # User interface functions. 1181 1182 def getParameter(request, name, default=None): 1183 1184 """ 1185 Using the given 'request', return the value of the parameter with the given 1186 'name', returning the optional 'default' (or None) if no value was supplied 1187 in the 'request'. 1188 """ 1189 1190 return request.form.get(name, [default])[0] 1191 1192 def getQualifiedParameter(request, calendar_name, argname, default=None): 1193 1194 """ 1195 Using the given 'request', 'calendar_name' and 'argname', retrieve the 1196 value of the qualified parameter, returning the optional 'default' (or None) 1197 if no value was supplied in the 'request'. 1198 """ 1199 1200 argname = getQualifiedParameterName(calendar_name, argname) 1201 return getParameter(request, argname, default) 1202 1203 def getQualifiedParameterName(calendar_name, argname): 1204 1205 """ 1206 Return the qualified parameter name using the given 'calendar_name' and 1207 'argname'. 1208 """ 1209 1210 if calendar_name is None: 1211 return argname 1212 else: 1213 return "%s-%s" % (calendar_name, argname) 1214 1215 def getParameterMonth(arg): 1216 1217 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1218 1219 n = None 1220 1221 if arg.startswith("current"): 1222 date = getCurrentMonth() 1223 if len(arg) > 8: 1224 n = int(arg[7:]) 1225 1226 elif arg.startswith("yearstart"): 1227 date = Month((getCurrentYear(), 1)) 1228 if len(arg) > 10: 1229 n = int(arg[9:]) 1230 1231 elif arg.startswith("yearend"): 1232 date = Month((getCurrentYear(), 12)) 1233 if len(arg) > 8: 1234 n = int(arg[7:]) 1235 1236 else: 1237 date = getMonth(arg) 1238 1239 if n is not None: 1240 date = date.month_update(n) 1241 1242 return date 1243 1244 def getFormMonth(request, calendar_name, argname): 1245 1246 """ 1247 Return the month from the 'request' for the calendar with the given 1248 'calendar_name' using the parameter having the given 'argname'. 1249 """ 1250 1251 arg = getQualifiedParameter(request, calendar_name, argname) 1252 if arg is not None: 1253 return getParameterMonth(arg) 1254 else: 1255 return None 1256 1257 def getFormMonthPair(request, yeararg, montharg): 1258 1259 """ 1260 Return the month from the 'request' for the calendar with the given 1261 'calendar_name' using the parameters having the given 'yeararg' and 1262 'montharg' names. 1263 """ 1264 1265 year = getParameter(request, yeararg) 1266 month = getParameter(request, montharg) 1267 if year and month: 1268 return Month((int(year), int(month))) 1269 else: 1270 return None 1271 1272 # Page-related functions. 1273 1274 def getPrettyPageName(page): 1275 1276 "Return a nicely formatted title/name for the given 'page'." 1277 1278 if isMoin15(): 1279 title = page.split_title(page.request, force=1) 1280 else: 1281 title = page.split_title(force=1) 1282 1283 return getPrettyTitle(title) 1284 1285 def linkToPage(request, page, text, query_string=None): 1286 1287 """ 1288 Using 'request', return a link to 'page' with the given link 'text' and 1289 optional 'query_string'. 1290 """ 1291 1292 text = wikiutil.escape(text) 1293 1294 if isMoin15(): 1295 url = wikiutil.quoteWikinameURL(page.page_name) 1296 if query_string is not None: 1297 url = "%s?%s" % (url, query_string) 1298 return wikiutil.link_tag(request, url, text, getattr(page, "formatter", None)) 1299 else: 1300 return page.link_to_raw(request, text, query_string) 1301 1302 def getFullPageName(parent, title): 1303 1304 """ 1305 Return a full page name from the given 'parent' page (can be empty or None) 1306 and 'title' (a simple page name). 1307 """ 1308 1309 if parent: 1310 return "%s/%s" % (parent.rstrip("/"), title) 1311 else: 1312 return title 1313 1314 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1315 1316 """ 1317 Using the given 'template_page', complete the 'new_page' by copying the 1318 template and adding the given 'event_details' (a dictionary of event 1319 fields), setting also the 'category_pagenames' to define category 1320 membership. 1321 """ 1322 1323 event_page = EventPage(template_page) 1324 new_event_page = EventPage(new_page) 1325 new_event_page.copyPage(event_page) 1326 1327 if new_event_page.getFormat() == "wiki": 1328 new_event = Event(new_event_page, event_details) 1329 new_event_page.setEvents([new_event]) 1330 new_event_page.setCategoryMembership(category_pagenames) 1331 new_event_page.saveChanges() 1332 1333 # vim: tabstop=4 expandtab shiftwidth=4