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