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