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