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