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 __str__(self): 894 return "%04d-%02d-%02d" % self.as_tuple()[:3] 895 896 def as_datetime(self, hour, minute, second, zone): 897 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 898 899 def as_date(self): 900 return self 901 902 def as_month(self): 903 return Month(self.data[:2]) 904 905 def day(self): 906 return self.data[2] 907 908 def next_day(self): 909 910 "Return the date following this one." 911 912 year, month, day = self.as_tuple()[:3] 913 _wd, end_day = calendar.monthrange(year, month) 914 if day == end_day: 915 if month == 12: 916 return Date((year + 1, 1, 1)) 917 else: 918 return Date((year, month + 1, 1)) 919 else: 920 return Date((year, month, day + 1)) 921 922 def previous_day(self): 923 924 "Return the date preceding this one." 925 926 year, month, day = self.as_tuple()[:3] 927 if day == 1: 928 if month == 1: 929 return Date((year - 1, 12, 31)) 930 else: 931 _wd, end_day = calendar.monthrange(year, month - 1) 932 return Date((year, month - 1, end_day)) 933 else: 934 return Date((year, month, day - 1)) 935 936 def days_until(self, end): 937 938 "Return the collection of days from this date until 'end'." 939 940 return self.until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 941 942 class DateTime(Date): 943 944 "A simple date plus time representation." 945 946 def __init__(self, data): 947 Date.__init__(self, data) 948 949 def __str__(self): 950 if self.has_time(): 951 data = self.as_tuple() 952 time_str = " %02d:%02d" % data[3:5] 953 if data[5] is not None: 954 time_str += ":%02d" % data[5] 955 if data[6] is not None: 956 time_str += " %s" % data[6] 957 else: 958 time_str = "" 959 960 return Date.__str__(self) + time_str 961 962 def as_datetime(self): 963 return self 964 965 def as_date(self): 966 return Date(self.data[:3]) 967 968 def has_time(self): 969 return self.data[3] is not None and self.data[4] is not None 970 971 def seconds(self): 972 return self.data[5] 973 974 def time_zone(self): 975 return self.data[6] 976 977 def set_time_zone(self, value): 978 self.data[6] = value 979 980 def padded(self): 981 982 "Return a datetime with missing fields defined as being zero." 983 984 data = map(lambda x: x or 0, self.data[:6]) + self.data[6:] 985 return DateTime(data) 986 987 def to_utc(self): 988 989 """ 990 Return this object converted to UTC, or None if such a conversion is not 991 defined. 992 """ 993 994 offset = self.utc_offset() 995 if offset: 996 hours, minutes = offset 997 998 # Invert the offset to get the correction. 999 1000 hours, minutes = -hours, -minutes 1001 1002 # Get the components. 1003 1004 hour, minute, second, zone = self.as_tuple()[3:] 1005 date = self.as_date() 1006 1007 # Add the minutes and hours. 1008 1009 minute += minutes 1010 if minute < 0: 1011 hour -= 1 1012 minute += 60 1013 elif minute > 59: 1014 hour += 1 1015 minute -= 60 1016 1017 hour += hours 1018 if hour < 0: 1019 date = date.previous_day() 1020 hour += 24 1021 elif hour > 23: 1022 date = date.next_day() 1023 hour -= 24 1024 1025 return date.as_datetime(hour, minute, second, "UTC") 1026 else: 1027 return None 1028 1029 def utc_offset(self): 1030 1031 "Return the UTC offset in hours and minutes." 1032 1033 zone = self.time_zone() 1034 if not zone: 1035 return None 1036 1037 # Attempt to return a UTC offset where an explicit offset has been set. 1038 1039 match = timezone_offset_regexp.match(zone) 1040 if match: 1041 if match.group("sign") == "-": 1042 sign = -1 1043 else: 1044 sign = 1 1045 1046 hours = int(match.group("hours")) * sign 1047 minutes = int(match.group("minutes") or 0) * sign 1048 return hours, minutes 1049 1050 return None 1051 1052 def getCountry(s): 1053 1054 "Find a country code in the given string 's'." 1055 1056 match = country_code_regexp.search(s) 1057 1058 if match: 1059 return match.group("code") 1060 else: 1061 return None 1062 1063 def getDate(s): 1064 1065 "Parse the string 's', extracting and returning a datetime object." 1066 1067 m = datetime_regexp.search(s) 1068 if m: 1069 groups = list(m.groups()) 1070 1071 # Convert date and time data to integer or None. 1072 1073 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]) 1074 else: 1075 return None 1076 1077 def getDateStrings(s): 1078 1079 "Parse the string 's', extracting and returning all date strings." 1080 1081 start = 0 1082 m = date_regexp.search(s, start) 1083 l = [] 1084 while m: 1085 l.append("-".join(m.groups())) 1086 m = date_regexp.search(s, m.end()) 1087 return l 1088 1089 def getMonth(s): 1090 1091 "Parse the string 's', extracting and returning a month object." 1092 1093 m = month_regexp.search(s) 1094 if m: 1095 return Month(map(int, m.groups())) 1096 else: 1097 return None 1098 1099 def getCurrentMonth(): 1100 1101 "Return the current month as a (year, month) tuple." 1102 1103 today = datetime.date.today() 1104 return Month((today.year, today.month)) 1105 1106 def getCurrentYear(): 1107 1108 "Return the current year." 1109 1110 today = datetime.date.today() 1111 return today.year 1112 1113 # User interface functions. 1114 1115 def getParameter(request, name, default=None): 1116 1117 """ 1118 Using the given 'request', return the value of the parameter with the given 1119 'name', returning the optional 'default' (or None) if no value was supplied 1120 in the 'request'. 1121 """ 1122 1123 return request.form.get(name, [default])[0] 1124 1125 def getQualifiedParameter(request, calendar_name, argname, default=None): 1126 1127 """ 1128 Using the given 'request', 'calendar_name' and 'argname', retrieve the 1129 value of the qualified parameter, returning the optional 'default' (or None) 1130 if no value was supplied in the 'request'. 1131 """ 1132 1133 argname = getQualifiedParameterName(calendar_name, argname) 1134 return getParameter(request, argname, default) 1135 1136 def getQualifiedParameterName(calendar_name, argname): 1137 1138 """ 1139 Return the qualified parameter name using the given 'calendar_name' and 1140 'argname'. 1141 """ 1142 1143 if calendar_name is None: 1144 return argname 1145 else: 1146 return "%s-%s" % (calendar_name, argname) 1147 1148 def getParameterMonth(arg): 1149 1150 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1151 1152 n = None 1153 1154 if arg.startswith("current"): 1155 date = getCurrentMonth() 1156 if len(arg) > 8: 1157 n = int(arg[7:]) 1158 1159 elif arg.startswith("yearstart"): 1160 date = Month((getCurrentYear(), 1)) 1161 if len(arg) > 10: 1162 n = int(arg[9:]) 1163 1164 elif arg.startswith("yearend"): 1165 date = Month((getCurrentYear(), 12)) 1166 if len(arg) > 8: 1167 n = int(arg[7:]) 1168 1169 else: 1170 date = getMonth(arg) 1171 1172 if n is not None: 1173 date = date.month_update(n) 1174 1175 return date 1176 1177 def getFormMonth(request, calendar_name, argname): 1178 1179 """ 1180 Return the month from the 'request' for the calendar with the given 1181 'calendar_name' using the parameter having the given 'argname'. 1182 """ 1183 1184 arg = getQualifiedParameter(request, calendar_name, argname) 1185 if arg is not None: 1186 return getParameterMonth(arg) 1187 else: 1188 return None 1189 1190 def getFormMonthPair(request, yeararg, montharg): 1191 1192 """ 1193 Return the month from the 'request' for the calendar with the given 1194 'calendar_name' using the parameters having the given 'yeararg' and 1195 'montharg' names. 1196 """ 1197 1198 year = getParameter(request, yeararg) 1199 month = getParameter(request, montharg) 1200 if year and month: 1201 return Month((int(year), int(month))) 1202 else: 1203 return None 1204 1205 # Page-related functions. 1206 1207 def getPrettyPageName(page): 1208 1209 "Return a nicely formatted title/name for the given 'page'." 1210 1211 if isMoin15(): 1212 title = page.split_title(page.request, force=1) 1213 else: 1214 title = page.split_title(force=1) 1215 1216 return getPrettyTitle(title) 1217 1218 def linkToPage(request, page, text, query_string=None): 1219 1220 """ 1221 Using 'request', return a link to 'page' with the given link 'text' and 1222 optional 'query_string'. 1223 """ 1224 1225 text = wikiutil.escape(text) 1226 1227 if isMoin15(): 1228 url = wikiutil.quoteWikinameURL(page.page_name) 1229 if query_string is not None: 1230 url = "%s?%s" % (url, query_string) 1231 return wikiutil.link_tag(request, url, text, getattr(page, "formatter", None)) 1232 else: 1233 return page.link_to_raw(request, text, query_string) 1234 1235 def getFullPageName(parent, title): 1236 1237 """ 1238 Return a full page name from the given 'parent' page (can be empty or None) 1239 and 'title' (a simple page name). 1240 """ 1241 1242 if parent: 1243 return "%s/%s" % (parent.rstrip("/"), title) 1244 else: 1245 return title 1246 1247 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 1248 1249 """ 1250 Using the given 'template_page', complete the 'new_page' by copying the 1251 template and adding the given 'event_details' (a dictionary of event 1252 fields), setting also the 'category_pagenames' to define category 1253 membership. 1254 """ 1255 1256 event_page = EventPage(template_page) 1257 new_event_page = EventPage(new_page) 1258 new_event_page.copyPage(event_page) 1259 1260 if new_event_page.getFormat() == "wiki": 1261 new_event = Event(new_event_page, event_details) 1262 new_event_page.setEvents([new_event]) 1263 new_event_page.setCategoryMembership(category_pagenames) 1264 new_event_page.saveChanges() 1265 1266 # vim: tabstop=4 expandtab shiftwidth=4