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