Back to index

moin  1.9.0~rc2
__init__.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - Theme Package
00004 
00005     @copyright: 2003-2008 MoinMoin:ThomasWaldmann
00006     @license: GNU GPL, see COPYING for details.
00007 """
00008 
00009 import StringIO
00010 
00011 from MoinMoin import i18n, wikiutil, config, version, caching
00012 from MoinMoin.action import get_available_actions
00013 from MoinMoin.Page import Page
00014 from MoinMoin.util import pysupport
00015 
00016 modules = pysupport.getPackageModules(__file__)
00017 
00018 # Check whether we can emit a RSS feed.
00019 # RSS is broken on plain Python 2.4.x, and works only when installing PyXML.
00020 # News: A user reported that the RSS is valid when using Python 2.5.1 on Windows.
00021 import sys, xml
00022 rss_supported = sys.version_info[:3] >= (2, 5, 1) or '_xmlplus' in xml.__file__
00023 
00024 
00025 class ThemeBase:
00026     """ Base class for themes
00027 
00028     This class supply all the standard template that sub classes can
00029     use without rewriting the same code. If you want to change certain
00030     elements, override them.
00031     """
00032 
00033     name = 'base'
00034 
00035     # fake _ function to get gettext recognize those texts:
00036     _ = lambda x: x
00037 
00038     # TODO: remove icons that are not used any more.
00039     icons = {
00040         # key         alt                        icon filename      w   h
00041         # ------------------------------------------------------------------
00042         # navibar
00043         'help':        ("%(page_help_contents)s", "moin-help.png",   12, 11),
00044         'find':        ("%(page_find_page)s",     "moin-search.png", 12, 12),
00045         'diff':        (_("Diffs"),               "moin-diff.png",   15, 11),
00046         'info':        (_("Info"),                "moin-info.png",   12, 11),
00047         'edit':        (_("Edit"),                "moin-edit.png",   12, 12),
00048         'unsubscribe': (_("Unsubscribe"),         "moin-unsubscribe.png", 14, 10),
00049         'subscribe':   (_("Subscribe"),           "moin-subscribe.png", 14, 10),
00050         'raw':         (_("Raw"),                 "moin-raw.png",    12, 13),
00051         'xml':         (_("XML"),                 "moin-xml.png",    20, 13),
00052         'print':       (_("Print"),               "moin-print.png",  16, 14),
00053         'view':        (_("View"),                "moin-show.png",   12, 13),
00054         'home':        (_("Home"),                "moin-home.png",   13, 12),
00055         'up':          (_("Up"),                  "moin-parent.png", 15, 13),
00056         # FileAttach
00057         'attach':     ("%(attach_count)s",       "moin-attach.png",  7, 15),
00058         'attachimg':  ("",                       "attach.png",      32, 32),
00059         # RecentChanges
00060         'rss':        (_("[RSS]"),               "moin-rss.png",    24, 24),
00061         'deleted':    (_("[DELETED]"),           "moin-deleted.png", 60, 12),
00062         'updated':    (_("[UPDATED]"),           "moin-updated.png", 60, 12),
00063         'renamed':    (_("[RENAMED]"),           "moin-renamed.png", 60, 12),
00064         'conflict':   (_("[CONFLICT]"),          "moin-conflict.png", 60, 12),
00065         'new':        (_("[NEW]"),               "moin-new.png",    31, 12),
00066         'diffrc':     (_("[DIFF]"),              "moin-diff.png",   15, 11),
00067         # General
00068         'bottom':     (_("[BOTTOM]"),            "moin-bottom.png", 14, 10),
00069         'top':        (_("[TOP]"),               "moin-top.png",    14, 10),
00070         'www':        ("[WWW]",                  "moin-www.png",    11, 11),
00071         'mailto':     ("[MAILTO]",               "moin-email.png",  14, 10),
00072         'news':       ("[NEWS]",                 "moin-news.png",   10, 11),
00073         'telnet':     ("[TELNET]",               "moin-telnet.png", 10, 11),
00074         'ftp':        ("[FTP]",                  "moin-ftp.png",    11, 11),
00075         'file':       ("[FILE]",                 "moin-ftp.png",    11, 11),
00076         # search forms
00077         'searchbutton': ("[?]",                  "moin-search.png", 12, 12),
00078         'interwiki':  ("[%(wikitag)s]",          "moin-inter.png",  16, 16),
00079 
00080         # smileys (this is CONTENT, but good looking smileys depend on looking
00081         # adapted to the theme background color and theme style in general)
00082         #vvv    ==      vvv  this must be the same for GUI editor converter
00083         'X-(':        ("X-(",                    'angry.png',       15, 15),
00084         ':D':         (":D",                     'biggrin.png',     15, 15),
00085         '<:(':        ("<:(",                    'frown.png',       15, 15),
00086         ':o':         (":o",                     'redface.png',     15, 15),
00087         ':(':         (":(",                     'sad.png',         15, 15),
00088         ':)':         (":)",                     'smile.png',       15, 15),
00089         'B)':         ("B)",                     'smile2.png',      15, 15),
00090         ':))':        (":))",                    'smile3.png',      15, 15),
00091         ';)':         (";)",                     'smile4.png',      15, 15),
00092         '/!\\':       ("/!\\",                   'alert.png',       15, 15),
00093         '<!>':        ("<!>",                    'attention.png',   15, 15),
00094         '(!)':        ("(!)",                    'idea.png',        15, 15),
00095 
00096         # copied 2001-11-16 from http://pikie.darktech.org/cgi/pikie.py?EmotIcon
00097         ':-?':        (":-?",                    'tongue.png',      15, 15),
00098         ':\\':        (":\\",                    'ohwell.png',      15, 15),
00099         '>:>':        (">:>",                    'devil.png',       15, 15),
00100         '|)':         ("|)",                     'tired.png',       15, 15),
00101 
00102         # some folks use noses in their emoticons
00103         ':-(':        (":-(",                    'sad.png',         15, 15),
00104         ':-)':        (":-)",                    'smile.png',       15, 15),
00105         'B-)':        ("B-)",                    'smile2.png',      15, 15),
00106         ':-))':       (":-))",                   'smile3.png',      15, 15),
00107         ';-)':        (";-)",                    'smile4.png',      15, 15),
00108         '|-)':        ("|-)",                    'tired.png',       15, 15),
00109 
00110         # version 1.0
00111         '(./)':       ("(./)",                   'checkmark.png',   20, 15),
00112         '{OK}':       ("{OK}",                   'thumbs-up.png',   14, 12),
00113         '{X}':        ("{X}",                    'icon-error.png',  16, 16),
00114         '{i}':        ("{i}",                    'icon-info.png',   16, 16),
00115         '{1}':        ("{1}",                    'prio1.png',       15, 13),
00116         '{2}':        ("{2}",                    'prio2.png',       15, 13),
00117         '{3}':        ("{3}",                    'prio3.png',       15, 13),
00118 
00119         # version 1.3.4 (stars)
00120         # try {*}{*}{o}
00121         '{*}':        ("{*}",                    'star_on.png',     15, 15),
00122         '{o}':        ("{o}",                    'star_off.png',    15, 15),
00123     }
00124     del _
00125 
00126     # Style sheets - usually there is no need to override this in sub
00127     # classes. Simply supply the css files in the css directory.
00128 
00129     # Standard set of style sheets
00130     stylesheets = (
00131         # media         basename
00132         ('all',         'common'),
00133         ('screen',      'screen'),
00134         ('print',       'print'),
00135         ('projection',  'projection'),
00136         )
00137 
00138     # Used in print mode
00139     stylesheets_print = (
00140         # media         basename
00141         ('all',         'common'),
00142         ('all',         'print'),
00143         )
00144 
00145     # Used in slide show mode
00146     stylesheets_projection = (
00147         # media         basename
00148         ('all',         'common'),
00149         ('all',         'projection'),
00150        )
00151 
00152     stylesheetsCharset = 'utf-8'
00153 
00154     def __init__(self, request):
00155         """
00156         Initialize the theme object.
00157 
00158         @param request: the request object
00159         """
00160         self.request = request
00161         self.cfg = request.cfg
00162         self._cache = {} # Used to cache elements that may be used several times
00163         self._status = []
00164         self._send_title_called = False
00165 
00166     def img_url(self, img):
00167         """ Generate an image href
00168 
00169         @param img: the image filename
00170         @rtype: string
00171         @return: the image href
00172         """
00173         return "%s/%s/img/%s" % (self.cfg.url_prefix_static, self.name, img)
00174 
00175     def emit_custom_html(self, html):
00176         """
00177         generate custom HTML code in `html`
00178 
00179         @param html: a string or a callable object, in which case
00180                      it is called and its return value is used
00181         @rtype: string
00182         @return: string with html
00183         """
00184         if html:
00185             if callable(html):
00186                 html = html(self.request)
00187         return html
00188 
00189     def logo(self):
00190         """ Assemble logo with link to front page
00191 
00192         The logo contain an image and or text or any html markup the
00193         admin inserted in the config file. Everything it enclosed inside
00194         a div with id="logo".
00195 
00196         @rtype: unicode
00197         @return: logo html
00198         """
00199         html = u''
00200         if self.cfg.logo_string:
00201             page = wikiutil.getFrontPage(self.request)
00202             logo = page.link_to_raw(self.request, self.cfg.logo_string)
00203             html = u'''<div id="logo">%s</div>''' % logo
00204         return html
00205 
00206     def interwiki(self, d):
00207         """ Assemble the interwiki name display, linking to page_front_page
00208 
00209         @param d: parameter dictionary
00210         @rtype: string
00211         @return: interwiki html
00212         """
00213         if self.request.cfg.show_interwiki:
00214             page = wikiutil.getFrontPage(self.request)
00215             text = self.request.cfg.interwikiname or u'Self'
00216             link = page.link_to(self.request, text=text, rel='nofollow')
00217             html = u'<div id="interwiki"><span>%s</span></div>' % link
00218         else:
00219             html = u''
00220         return html
00221 
00222     def title(self, d):
00223         """ Assemble the title (now using breadcrumbs)
00224 
00225         @param d: parameter dictionary
00226         @rtype: string
00227         @return: title html
00228         """
00229         _ = self.request.getText
00230         content = []
00231         if d['title_text'] == d['page'].split_title(): # just showing a page, no action
00232             curpage = ''
00233             segments = d['page_name'].split('/') # was: title_text
00234             for s in segments[:-1]:
00235                 curpage += s
00236                 content.append("<li>%s</li>" % Page(self.request, curpage).link_to(self.request, s))
00237                 curpage += '/'
00238             link_text = segments[-1]
00239             link_title = _('Click to do a full-text search for this title')
00240             link_query = {
00241                 'action': 'fullsearch',
00242                 'value': 'linkto:"%s"' % d['page_name'],
00243                 'context': '180',
00244             }
00245             # we dont use d['title_link'] any more, but make it ourselves:
00246             link = d['page'].link_to(self.request, link_text, querystr=link_query, title=link_title, css_class='backlink', rel='nofollow')
00247             content.append(('<li>%s</li>') % link)
00248         else:
00249             content.append('<li>%s</li>' % wikiutil.escape(d['title_text']))
00250 
00251         html = '''
00252 <ul id="pagelocation">
00253 %s
00254 </ul>
00255 ''' % "".join(content)
00256         return html
00257 
00258     def title_with_separators(self, d):
00259         """ Assemble the title using slashes, not <ul>
00260 
00261         @param d: parameter dictionary
00262         @rtype: string
00263         @return: title html
00264         """
00265         _ = self.request.getText
00266         if d['title_text'] == d['page'].split_title():
00267             # just showing a page, no action
00268             segments = d['page_name'].split('/')
00269             link_text = segments[-1]
00270             link_title = _('Click to do a full-text search for this title')
00271             link_query = {'action': 'fullsearch', 'context': '180',
00272                           'value': 'linkto:"%s"' % d['page_name'], }
00273             link = d['page'].link_to(self.request, link_text,
00274                                      querystr=link_query, title=link_title,
00275                                      css_class='backlink', rel='nofollow')
00276             if len(segments) <= 1:
00277                 html = link
00278             else:
00279                 content = []
00280                 curpage = ''
00281                 for s in segments[:-1]:
00282                     curpage += s
00283                     content.append(Page(self.request,
00284                                         curpage).link_to(self.request, s))
00285                     curpage += '/'
00286                 path_html = u'<span class="sep">/</span>'.join(content)
00287                 html = u'<span class="pagepath">%s</span><span class="sep">/</span>%s' % (path_html, link)
00288         else:
00289             html = wikiutil.escape(d['title_text'])
00290         return u'<span id="pagelocation">%s</span>' % html
00291 
00292     def username(self, d):
00293         """ Assemble the username / userprefs link
00294 
00295         @param d: parameter dictionary
00296         @rtype: unicode
00297         @return: username html
00298         """
00299         request = self.request
00300         _ = request.getText
00301 
00302         userlinks = []
00303         # Add username/homepage link for registered users. We don't care
00304         # if it exists, the user can create it.
00305         if request.user.valid and request.user.name:
00306             interwiki = wikiutil.getInterwikiHomePage(request)
00307             name = request.user.name
00308             aliasname = request.user.aliasname
00309             if not aliasname:
00310                 aliasname = name
00311             title = "%s @ %s" % (aliasname, interwiki[0])
00312             # link to (interwiki) user homepage
00313             homelink = (request.formatter.interwikilink(1, title=title, id="userhome", generated=True, *interwiki) +
00314                         request.formatter.text(name) +
00315                         request.formatter.interwikilink(0, title=title, id="userhome", *interwiki))
00316             userlinks.append(homelink)
00317             # link to userprefs action
00318             if 'userprefs' not in self.request.cfg.actions_excluded:
00319                 userlinks.append(d['page'].link_to(request, text=_('Settings'),
00320                                                querystr={'action': 'userprefs'}, id='userprefs', rel='nofollow'))
00321 
00322         if request.user.valid:
00323             if request.user.auth_method in request.cfg.auth_can_logout:
00324                 userlinks.append(d['page'].link_to(request, text=_('Logout'),
00325                                                    querystr={'action': 'logout', 'logout': 'logout'}, id='logout', rel='nofollow'))
00326         else:
00327             query = {'action': 'login'}
00328             # special direct-login link if the auth methods want no input
00329             if request.cfg.auth_login_inputs == ['special_no_input']:
00330                 query['login'] = '1'
00331             if request.cfg.auth_have_login:
00332                 userlinks.append(d['page'].link_to(request, text=_("Login"),
00333                                                    querystr=query, id='login', rel='nofollow'))
00334 
00335         userlinks = [u'<li>%s</li>' % link for link in userlinks]
00336         html = u'<ul id="username">%s</ul>' % ''.join(userlinks)
00337         return html
00338 
00339     def splitNavilink(self, text, localize=1):
00340         """ Split navibar links into pagename, link to page
00341 
00342         Admin or user might want to use shorter navibar items by using
00343         the [[page|title]] or [[url|title]] syntax. In this case, we don't
00344         use localization, and the links goes to page or to the url, not
00345         the localized version of page.
00346 
00347         Supported syntax:
00348             * PageName
00349             * WikiName:PageName
00350             * wiki:WikiName:PageName
00351             * url
00352             * all targets as seen above with title: [[target|title]]
00353 
00354         @param text: the text used in config or user preferences
00355         @rtype: tuple
00356         @return: pagename or url, link to page or url
00357         """
00358         request = self.request
00359         fmt = request.formatter
00360         title = None
00361 
00362         # Handle [[pagename|title]] or [[url|title]] formats
00363         if text.startswith('[[') and text.endswith(']]'):
00364             text = text[2:-2]
00365             try:
00366                 pagename, title = text.split('|', 1)
00367                 pagename = pagename.strip()
00368                 title = title.strip()
00369                 localize = 0
00370             except (ValueError, TypeError):
00371                 # Just use the text as is.
00372                 pagename = text.strip()
00373         else:
00374             pagename = text
00375 
00376         if wikiutil.is_URL(pagename):
00377             if not title:
00378                 title = pagename
00379             link = fmt.url(1, pagename) + fmt.text(title) + fmt.url(0)
00380             return pagename, link
00381 
00382         # remove wiki: url prefix
00383         if pagename.startswith("wiki:"):
00384             pagename = pagename[5:]
00385 
00386         # try handling interwiki links
00387         try:
00388             interwiki, page = wikiutil.split_interwiki(pagename)
00389             thiswiki = request.cfg.interwikiname
00390             if interwiki == thiswiki or interwiki == 'Self':
00391                 pagename = page
00392             else:
00393                 if not title:
00394                     title = page
00395                 link = fmt.interwikilink(True, interwiki, page) + fmt.text(title) + fmt.interwikilink(False, interwiki, page)
00396                 return pagename, link
00397         except ValueError:
00398             pass
00399 
00400         # Handle regular pagename like "FrontPage"
00401         pagename = wikiutil.normalize_pagename(pagename, request.cfg)
00402 
00403         # Use localized pages for the current user
00404         if localize:
00405             page = wikiutil.getLocalizedPage(request, pagename)
00406         else:
00407             page = Page(request, pagename)
00408 
00409         pagename = page.page_name # can be different, due to i18n
00410 
00411         if not title:
00412             title = page.split_title()
00413             title = self.shortenPagename(title)
00414 
00415         link = page.link_to(request, title)
00416 
00417         return pagename, link
00418 
00419     def shortenPagename(self, name):
00420         """ Shorten page names
00421 
00422         Shorten very long page names that tend to break the user
00423         interface. The short name is usually fine, unless really stupid
00424         long names are used (WYGIWYD).
00425 
00426         If you don't like to do this in your theme, or want to use
00427         different algorithm, override this method.
00428 
00429         @param name: page name, unicode
00430         @rtype: unicode
00431         @return: shortened version.
00432         """
00433         maxLength = self.maxPagenameLength()
00434         # First use only the sub page name, that might be enough
00435         if len(name) > maxLength:
00436             name = name.split('/')[-1]
00437             # If it's not enough, replace the middle with '...'
00438             if len(name) > maxLength:
00439                 half, left = divmod(maxLength - 3, 2)
00440                 name = u'%s...%s' % (name[:half + left], name[-half:])
00441         return name
00442 
00443     def maxPagenameLength(self):
00444         """ Return maximum length for shortened page names """
00445         return 25
00446 
00447     def navibar(self, d):
00448         """ Assemble the navibar
00449 
00450         @param d: parameter dictionary
00451         @rtype: unicode
00452         @return: navibar html
00453         """
00454         request = self.request
00455         found = {} # pages we found. prevent duplicates
00456         items = [] # navibar items
00457         item = u'<li class="%s">%s</li>'
00458         current = d['page_name']
00459 
00460         # Process config navi_bar
00461         if request.cfg.navi_bar:
00462             for text in request.cfg.navi_bar:
00463                 pagename, link = self.splitNavilink(text)
00464                 if pagename == current:
00465                     cls = 'wikilink current'
00466                 else:
00467                     cls = 'wikilink'
00468                 items.append(item % (cls, link))
00469                 found[pagename] = 1
00470 
00471         # Add user links to wiki links, eliminating duplicates.
00472         userlinks = request.user.getQuickLinks()
00473         for text in userlinks:
00474             # Split text without localization, user knows what he wants
00475             pagename, link = self.splitNavilink(text, localize=0)
00476             if not pagename in found:
00477                 if pagename == current:
00478                     cls = 'userlink current'
00479                 else:
00480                     cls = 'userlink'
00481                 items.append(item % (cls, link))
00482                 found[pagename] = 1
00483 
00484         # Add current page at end of local pages
00485         if not current in found:
00486             title = d['page'].split_title()
00487             title = self.shortenPagename(title)
00488             link = d['page'].link_to(request, title)
00489             cls = 'current'
00490             items.append(item % (cls, link))
00491 
00492         # Add sister pages.
00493         for sistername, sisterurl in request.cfg.sistersites:
00494             if sistername == request.cfg.interwikiname: # it is THIS wiki
00495                 cls = 'sisterwiki current'
00496                 items.append(item % (cls, sistername))
00497             else:
00498                 # TODO optimize performance
00499                 cache = caching.CacheEntry(request, 'sisters', sistername, 'farm', use_pickle=True)
00500                 if cache.exists():
00501                     data = cache.content()
00502                     sisterpages = data['sisterpages']
00503                     if current in sisterpages:
00504                         cls = 'sisterwiki'
00505                         url = sisterpages[current]
00506                         link = request.formatter.url(1, url) + \
00507                                request.formatter.text(sistername) +\
00508                                request.formatter.url(0)
00509                         items.append(item % (cls, link))
00510 
00511         # Assemble html
00512         items = u''.join(items)
00513         html = u'''
00514 <ul id="navibar">
00515 %s
00516 </ul>
00517 ''' % items
00518         return html
00519 
00520     def get_icon(self, icon):
00521         """ Return icon data from self.icons
00522 
00523         If called from <<Icon(file)>> we have a filename, not a
00524         key. Using filenames is deprecated, but for now, we simulate old
00525         behavior.
00526 
00527         @param icon: icon name or file name (string)
00528         @rtype: tuple
00529         @return: alt (unicode), href (string), width, height (int)
00530         """
00531         if icon in self.icons:
00532             alt, icon, w, h = self.icons[icon]
00533         else:
00534             # Create filenames to icon data mapping on first call, then
00535             # cache in class for next calls.
00536             if not getattr(self.__class__, 'iconsByFile', None):
00537                 d = {}
00538                 for data in self.icons.values():
00539                     d[data[1]] = data
00540                 self.__class__.iconsByFile = d
00541 
00542             # Try to get icon data by file name
00543             if icon in self.iconsByFile:
00544                 alt, icon, w, h = self.iconsByFile[icon]
00545             else:
00546                 alt, icon, w, h = '', icon, '', ''
00547 
00548         return alt, self.img_url(icon), w, h
00549 
00550     def make_icon(self, icon, vars=None, **kw):
00551         """
00552         This is the central routine for making <img> tags for icons!
00553         All icons stuff except the top left logo and search field icons are
00554         handled here.
00555 
00556         @param icon: icon id (dict key)
00557         @param vars: ...
00558         @rtype: string
00559         @return: icon html (img tag)
00560         """
00561         if vars is None:
00562             vars = {}
00563         alt, img, w, h = self.get_icon(icon)
00564         try:
00565             alt = vars['icon-alt-text'] # if it is possible we take the alt-text from 'page_icons_table'
00566         except KeyError, err:
00567             try:
00568                 alt = alt % vars # if not we just leave the  alt-text from 'icons'
00569             except KeyError, err:
00570                 alt = 'KeyError: %s' % str(err)
00571         alt = self.request.getText(alt)
00572         tag = self.request.formatter.image(src=img, alt=alt, width=w, height=h, **kw)
00573         return tag
00574 
00575     def make_iconlink(self, which, d):
00576         """
00577         Make a link with an icon
00578 
00579         @param which: icon id (dictionary key)
00580         @param d: parameter dictionary
00581         @rtype: string
00582         @return: html link tag
00583         """
00584         qs = {}
00585         pagekey, querystr, title, icon = self.cfg.page_icons_table[which]
00586         qs.update(querystr) # do not modify the querystr dict in the cfg!
00587         d['icon-alt-text'] = d['title'] = title % d
00588         d['i18ntitle'] = self.request.getText(d['title'])
00589         img_src = self.make_icon(icon, d)
00590         rev = d['rev']
00591         if rev and which in ['raw', 'print', ]:
00592             qs['rev'] = str(rev)
00593         attrs = {'rel': 'nofollow', 'title': d['i18ntitle'], }
00594         page = d[pagekey]
00595         if isinstance(page, unicode):
00596             # e.g. d['page_parent_page'] is just the unicode pagename
00597             # while d['page'] will give a page object
00598             page = Page(self.request, page)
00599         return page.link_to_raw(self.request, text=img_src, querystr=qs, **attrs)
00600 
00601     def msg(self, d):
00602         """ Assemble the msg display
00603 
00604         Display a message with a widget or simple strings with a clear message link.
00605 
00606         @param d: parameter dictionary
00607         @rtype: unicode
00608         @return: msg display html
00609         """
00610         _ = self.request.getText
00611         msgs = d['msg']
00612 
00613         result = u""
00614         close = d['page'].link_to(self.request, text=_('Clear message'), css_class="clear-link")
00615         for msg, msg_class in msgs:
00616             try:
00617                 result += u'<p>%s</p>' % msg.render()
00618                 close = ''
00619             except AttributeError:
00620                 if msg and msg_class:
00621                     result += u'<p><div class="%s">%s</div></p>' % (msg_class, msg)
00622                 elif msg:
00623                     result += u'<p>%s</p>\n' % msg
00624         if result:
00625             html = result + close
00626             return u'<div id="message">\n%s\n</div>\n' % html
00627         else:
00628             return u''
00629 
00630         return u'<div id="message">\n%s\n</div>\n' % html
00631 
00632     def trail(self, d):
00633         """ Assemble page trail
00634 
00635         @param d: parameter dictionary
00636         @rtype: unicode
00637         @return: trail html
00638         """
00639         request = self.request
00640         user = request.user
00641         html = ''
00642         if not user.valid or user.show_page_trail:
00643             trail = user.getTrail()
00644             if trail:
00645                 items = []
00646                 for pagename in trail:
00647                     try:
00648                         interwiki, page = wikiutil.split_interwiki(pagename)
00649                         if interwiki != request.cfg.interwikiname and interwiki != 'Self':
00650                             link = (self.request.formatter.interwikilink(True, interwiki, page) +
00651                                     self.shortenPagename(page) +
00652                                     self.request.formatter.interwikilink(False, interwiki, page))
00653                             items.append('<li>%s</li>' % link)
00654                             continue
00655                         else:
00656                             pagename = page
00657 
00658                     except ValueError:
00659                         pass
00660                     page = Page(request, pagename)
00661                     title = page.split_title()
00662                     title = self.shortenPagename(title)
00663                     link = page.link_to(request, title)
00664                     items.append('<li>%s</li>' % link)
00665                 html = '''
00666 <ul id="pagetrail">
00667 %s
00668 </ul>''' % ''.join(items)
00669         return html
00670 
00671     def _stylesheet_link(self, theme, media, href, title=None):
00672         """
00673         Create a link tag for a stylesheet.
00674 
00675         @param theme: True: href gives the basename of a theme stylesheet,
00676                       False: href is a full url of a user/admin defined stylesheet.
00677         @param media: 'all', 'screen', 'print', 'projection', ...
00678         @param href: see param theme
00679         @param title: optional title (for alternate stylesheets), see
00680                       http://www.w3.org/Style/Examples/007/alternatives
00681         @rtype: string
00682         @return: stylesheet link html
00683         """
00684         if theme:
00685             href = '%s/%s/css/%s.css' % (self.cfg.url_prefix_static, self.name, href)
00686         attrs = 'type="text/css" charset="%s" media="%s" href="%s"' % (
00687                 self.stylesheetsCharset, media, href, )
00688         if title:
00689             return '<link rel="alternate stylesheet" %s title="%s">' % (attrs, title)
00690         else:
00691             return '<link rel="stylesheet" %s>' % attrs
00692 
00693     def html_stylesheets(self, d):
00694         """ Assemble html head stylesheet links
00695 
00696         @param d: parameter dictionary
00697         @rtype: string
00698         @return: stylesheets links
00699         """
00700         request = self.request
00701         # Check mode
00702         if d.get('print_mode'):
00703             media = d.get('media', 'print')
00704             stylesheets = getattr(self, 'stylesheets_' + media)
00705         else:
00706             stylesheets = self.stylesheets
00707 
00708         theme_css = [self._stylesheet_link(True, *stylesheet) for stylesheet in stylesheets]
00709         cfg_css = [self._stylesheet_link(False, *stylesheet) for stylesheet in request.cfg.stylesheets]
00710 
00711         msie_css = """
00712 <!-- css only for MS IE6/IE7 browsers -->
00713 <!--[if lt IE 8]>
00714    %s
00715 <![endif]-->
00716 """ % self._stylesheet_link(True, 'all', 'msie')
00717 
00718         # Add user css url (assuming that user css uses same charset)
00719         href = request.user.valid and request.user.css_url
00720         if href and href.lower() != "none":
00721             user_css = self._stylesheet_link(False, 'all', href)
00722         else:
00723             user_css = ''
00724 
00725         return '\n'.join(theme_css + cfg_css + [msie_css, user_css])
00726 
00727     def shouldShowPageinfo(self, page):
00728         """ Should we show page info?
00729 
00730         Should be implemented by actions. For now, we check here by action
00731         name and page.
00732 
00733         @param page: current page
00734         @rtype: bool
00735         @return: true if should show page info
00736         """
00737         if page.exists() and self.request.user.may.read(page.page_name):
00738             # These  actions show the  page content.
00739             # TODO: on new action, page info will not show.
00740             # A better solution will be if the action itself answer the question: showPageInfo().
00741             contentActions = [u'', u'show', u'refresh', u'preview', u'diff',
00742                               u'subscribe', u'RenamePage', u'CopyPage', u'DeletePage',
00743                               u'SpellCheck', u'print']
00744             return self.request.action in contentActions
00745         return False
00746 
00747     def pageinfo(self, page):
00748         """ Return html fragment with page meta data
00749 
00750         Since page information uses translated text, it uses the ui
00751         language and direction. It looks strange sometimes, but
00752         translated text using page direction looks worse.
00753 
00754         @param page: current page
00755         @rtype: unicode
00756         @return: page last edit information
00757         """
00758         _ = self.request.getText
00759         html = ''
00760         if self.shouldShowPageinfo(page):
00761             info = page.lastEditInfo()
00762             if info:
00763                 if info['editor']:
00764                     info = _("last edited %(time)s by %(editor)s") % info
00765                 else:
00766                     info = _("last modified %(time)s") % info
00767                 pagename = page.page_name
00768                 if self.request.cfg.show_interwiki:
00769                     pagename = "%s: %s" % (self.request.cfg.interwikiname, pagename)
00770                 info = "%s  (%s)" % (wikiutil.escape(pagename), info)
00771                 html = '<p id="pageinfo" class="info"%(lang)s>%(info)s</p>\n' % {
00772                     'lang': self.ui_lang_attr(),
00773                     'info': info
00774                     }
00775         return html
00776 
00777     def searchform(self, d):
00778         """
00779         assemble HTML code for the search forms
00780 
00781         @param d: parameter dictionary
00782         @rtype: unicode
00783         @return: search form html
00784         """
00785         _ = self.request.getText
00786         form = self.request.values
00787         updates = {
00788             'search_label': _('Search:'),
00789             'search_value': wikiutil.escape(form.get('value', ''), 1),
00790             'search_full_label': _('Text'),
00791             'search_title_label': _('Titles'),
00792             'url': self.request.href(d['page'].page_name)
00793             }
00794         d.update(updates)
00795 
00796         html = u'''
00797 <form id="searchform" method="get" action="%(url)s">
00798 <div>
00799 <input type="hidden" name="action" value="fullsearch">
00800 <input type="hidden" name="context" value="180">
00801 <label for="searchinput">%(search_label)s</label>
00802 <input id="searchinput" type="text" name="value" value="%(search_value)s" size="20"
00803     onfocus="searchFocus(this)" onblur="searchBlur(this)"
00804     onkeyup="searchChange(this)" onchange="searchChange(this)" alt="Search">
00805 <input id="titlesearch" name="titlesearch" type="submit"
00806     value="%(search_title_label)s" alt="Search Titles">
00807 <input id="fullsearch" name="fullsearch" type="submit"
00808     value="%(search_full_label)s" alt="Search Full Text">
00809 </div>
00810 </form>
00811 <script type="text/javascript">
00812 <!--// Initialize search form
00813 var f = document.getElementById('searchform');
00814 f.getElementsByTagName('label')[0].style.display = 'none';
00815 var e = document.getElementById('searchinput');
00816 searchChange(e);
00817 searchBlur(e);
00818 //-->
00819 </script>
00820 ''' % d
00821         return html
00822 
00823     def showversion(self, d, **keywords):
00824         """
00825         assemble HTML code for copyright and version display
00826 
00827         @param d: parameter dictionary
00828         @rtype: string
00829         @return: copyright and version display html
00830         """
00831         html = ''
00832         if self.cfg.show_version and not keywords.get('print_mode', 0):
00833             html = (u'<div id="version">MoinMoin Release %s [Revision %s], '
00834                      'Copyright by Juergen Hermann et al.</div>') % (version.release, version.revision, )
00835         return html
00836 
00837     def headscript(self, d):
00838         """ Return html head script with common functions
00839 
00840         @param d: parameter dictionary
00841         @rtype: unicode
00842         @return: script for html head
00843         """
00844         # Don't add script for print view
00845         if self.request.action == 'print':
00846             return u''
00847 
00848         _ = self.request.getText
00849         script = u"""
00850 <script type="text/javascript">
00851 <!--
00852 var search_hint = "%(search_hint)s";
00853 //-->
00854 </script>
00855 """ % {
00856     'search_hint': _('Search'),
00857     }
00858         return script
00859 
00860     def shouldUseRSS(self, page):
00861         """ Return True if RSS feature is available and we are on the
00862             RecentChanges page, or False.
00863 
00864             Currently rss is broken on plain Python, and works only when
00865             installing PyXML. Return true if PyXML is installed.
00866         """
00867         if not rss_supported:
00868             return False
00869         return page.page_name == u'RecentChanges' or \
00870            page.page_name == self.request.getText(u'RecentChanges')
00871 
00872     def rsshref(self, page):
00873         """ Create rss href, used for rss button and head link
00874 
00875         @rtype: unicode
00876         @return: rss href
00877         """
00878         request = self.request
00879         url = page.url(request, querystr={
00880                 'action': 'rss_rc', 'ddiffs': '1', 'unique': '1', }, escape=0)
00881         return url
00882 
00883     def rsslink(self, d):
00884         """ Create rss link in head, used by FireFox
00885 
00886         RSS link for FireFox. This shows an rss link in the bottom of
00887         the page and let you subscribe to the wiki rss feed.
00888 
00889         @rtype: unicode
00890         @return: html head
00891         """
00892         link = u''
00893         page = d['page']
00894         if self.shouldUseRSS(page):
00895             link = (u'<link rel="alternate" title="%s Recent Changes" '
00896                     u'href="%s" type="application/rss+xml">') % (
00897                         wikiutil.escape(self.cfg.sitename, True),
00898                         wikiutil.escape(self.rsshref(page), True) )
00899         return link
00900 
00901     def html_head(self, d):
00902         """ Assemble html head
00903 
00904         @param d: parameter dictionary
00905         @rtype: unicode
00906         @return: html head
00907         """
00908         html = [
00909             u'<title>%(title)s - %(sitename)s</title>' % {
00910                 'title': wikiutil.escape(d['title']),
00911                 'sitename': wikiutil.escape(d['sitename']),
00912             },
00913             self.externalScript('common'),
00914             self.headscript(d), # Should move to separate .js file
00915             self.guiEditorScript(d),
00916             self.html_stylesheets(d),
00917             self.rsslink(d),
00918             self.universal_edit_button(d),
00919             ]
00920         return '\n'.join(html)
00921 
00922     def externalScript(self, name):
00923         """ Format external script html """
00924         src = '%s/common/js/%s.js' % (self.request.cfg.url_prefix_static, name)
00925         return '<script type="text/javascript" src="%s"></script>' % src
00926 
00927     def universal_edit_button(self, d, **keywords):
00928         """ Generate HTML for an edit link in the header."""
00929         page = d['page']
00930         if 'edit' in self.request.cfg.actions_excluded:
00931             return ""
00932         if not (page.isWritable() and
00933                 self.request.user.may.write(page.page_name)):
00934             return ""
00935         _ = self.request.getText
00936         querystr = {'action': 'edit'}
00937         text = _(u'Edit')
00938         url = page.url(self.request, querystr=querystr, escape=0)
00939         return (u'<link rel="alternate" type="application/wiki" '
00940                 u'title="%s" href="%s">' % (text, url))
00941 
00942     def credits(self, d, **keywords):
00943         """ Create credits html from credits list """
00944         if isinstance(self.cfg.page_credits, (list, tuple)):
00945             items = ['<li>%s</li>' % i for i in self.cfg.page_credits]
00946             html = '<ul id="credits">\n%s\n</ul>\n' % ''.join(items)
00947         else:
00948             # Old config using string, output as is
00949             html = self.cfg.page_credits
00950         return html
00951 
00952     def actionsMenu(self, page):
00953         """ Create actions menu list and items data dict
00954 
00955         The menu will contain the same items always, but items that are
00956         not available will be disabled (some broken browsers will let
00957         you select disabled options though).
00958 
00959         The menu should give best user experience for javascript
00960         enabled browsers, and acceptable behavior for those who prefer
00961         not to use Javascript.
00962 
00963         TODO: Move actionsMenuInit() into body onload - requires that the theme will render body,
00964               it is currently done in wikiutil/page.
00965 
00966         @param page: current page, Page object
00967         @rtype: unicode
00968         @return: actions menu html fragment
00969         """
00970         request = self.request
00971         _ = request.getText
00972         rev = request.rev
00973 
00974         menu = [
00975             'raw',
00976             'print',
00977             'RenderAsDocbook',
00978             'refresh',
00979             '__separator__',
00980             'SpellCheck',
00981             'LikePages',
00982             'LocalSiteMap',
00983             '__separator__',
00984             'RenamePage',
00985             'CopyPage',
00986             'DeletePage',
00987             '__separator__',
00988             'MyPages',
00989             'SubscribeUser',
00990             '__separator__',
00991             'Despam',
00992             'revert',
00993             'PackagePages',
00994             'SyncPages',
00995             ]
00996 
00997         titles = {
00998             # action: menu title
00999             '__title__': _("More Actions:"),
01000             # Translation may need longer or shorter separator
01001             '__separator__': _('------------------------'),
01002             'raw': _('Raw Text'),
01003             'print': _('Print View'),
01004             'refresh': _('Delete Cache'),
01005             'SpellCheck': _('Check Spelling'), # rename action!
01006             'RenamePage': _('Rename Page'),
01007             'CopyPage': _('Copy Page'),
01008             'DeletePage': _('Delete Page'),
01009             'LikePages': _('Like Pages'),
01010             'LocalSiteMap': _('Local Site Map'),
01011             'MyPages': _('My Pages'),
01012             'SubscribeUser': _('Subscribe User'),
01013             'Despam': _('Remove Spam'),
01014             'revert': _('Revert to this revision'),
01015             'PackagePages': _('Package Pages'),
01016             'RenderAsDocbook': _('Render as Docbook'),
01017             'SyncPages': _('Sync Pages'),
01018             }
01019 
01020         options = []
01021         option = '<option value="%(action)s"%(disabled)s>%(title)s</option>'
01022         # class="disabled" is a workaround for browsers that ignore
01023         # "disabled", e.g IE, Safari
01024         # for XHTML: data['disabled'] = ' disabled="disabled"'
01025         disabled = ' disabled class="disabled"'
01026 
01027         # Format standard actions
01028         available = get_available_actions(request.cfg, page, request.user)
01029         for action in menu:
01030             data = {'action': action, 'disabled': '', 'title': titles[action]}
01031             # removes excluded actions from the more actions menu
01032             if action in request.cfg.actions_excluded:
01033                 continue
01034 
01035             # Enable delete cache only if page can use caching
01036             if action == 'refresh':
01037                 if not page.canUseCache():
01038                     data['action'] = 'show'
01039                     data['disabled'] = disabled
01040 
01041             # revert action enabled only if user can revert
01042             if action == 'revert' and not request.user.may.revert(page.page_name):
01043                 data['action'] = 'show'
01044                 data['disabled'] = disabled
01045 
01046             # SubscribeUser action enabled only if user has admin rights
01047             if action == 'SubscribeUser' and not request.user.may.admin(page.page_name):
01048                 data['action'] = 'show'
01049                 data['disabled'] = disabled
01050 
01051             # Despam action enabled only for superusers
01052             if action == 'Despam' and not request.user.isSuperUser():
01053                 data['action'] = 'show'
01054                 data['disabled'] = disabled
01055 
01056             # Special menu items. Without javascript, executing will
01057             # just return to the page.
01058             if action.startswith('__'):
01059                 data['action'] = 'show'
01060 
01061             # Actions which are not available for this wiki, user or page
01062             if (action == '__separator__' or
01063                 (action[0].isupper() and not action in available)):
01064                 data['disabled'] = disabled
01065 
01066             options.append(option % data)
01067 
01068         # Add custom actions not in the standard menu, except for
01069         # some actions like AttachFile (we have them on top level)
01070         more = [item for item in available if not item in titles and not item in ('AttachFile', )]
01071         more.sort()
01072         if more:
01073             # Add separator
01074             separator = option % {'action': 'show', 'disabled': disabled,
01075                                   'title': titles['__separator__']}
01076             options.append(separator)
01077             # Add more actions (all enabled)
01078             for action in more:
01079                 data = {'action': action, 'disabled': ''}
01080                 # Always add spaces: AttachFile -> Attach File
01081                 # XXX do not create page just for using split_title -
01082                 # creating pages for non-existent does 2 storage lookups
01083                 #title = Page(request, action).split_title(force=1)
01084                 title = action
01085                 # Use translated version if available
01086                 data['title'] = _(title)
01087                 options.append(option % data)
01088 
01089         data = {
01090             'label': titles['__title__'],
01091             'options': '\n'.join(options),
01092             'rev_field': rev and '<input type="hidden" name="rev" value="%d">' % rev or '',
01093             'do_button': _("Do"),
01094             'url': self.request.href(page.page_name)
01095             }
01096         html = '''
01097 <form class="actionsmenu" method="GET" action="%(url)s">
01098 <div>
01099     <label>%(label)s</label>
01100     <select name="action"
01101         onchange="if ((this.selectedIndex != 0) &&
01102                       (this.options[this.selectedIndex].disabled == false)) {
01103                 this.form.submit();
01104             }
01105             this.selectedIndex = 0;">
01106         %(options)s
01107     </select>
01108     <input type="submit" value="%(do_button)s">
01109     %(rev_field)s
01110 </div>
01111 <script type="text/javascript">
01112 <!--// Init menu
01113 actionsMenuInit('%(label)s');
01114 //-->
01115 </script>
01116 </form>
01117 ''' % data
01118 
01119         return html
01120 
01121     def editbar(self, d):
01122         """ Assemble the page edit bar.
01123 
01124         Create html on first call, then return cached html.
01125 
01126         @param d: parameter dictionary
01127         @rtype: unicode
01128         @return: iconbar html
01129         """
01130         page = d['page']
01131         if not self.shouldShowEditbar(page):
01132             return ''
01133 
01134         html = self._cache.get('editbar')
01135         if html is None:
01136             # Remove empty items and format as list. The item for showing inline comments
01137             # is hidden by default. It gets activated through javascript only if inline
01138             # comments exist on the page.
01139             items = []
01140             for item in self.editbarItems(page):
01141                 if item:
01142                     if 'nbcomment' in item:
01143                         # hiding the complete list item is cosmetically better than just
01144                         # hiding the contents (e.g. for sidebar themes).
01145                         items.append('<li class="toggleCommentsButton" style="display:none;">%s</li>' % item)
01146                     else:
01147                         items.append('<li>%s</li>' % item)
01148             html = u'<ul class="editbar">%s</ul>\n' % ''.join(items)
01149             self._cache['editbar'] = html
01150 
01151         return html
01152 
01153     def shouldShowEditbar(self, page):
01154         """ Should we show the editbar?
01155 
01156         Actions should implement this, because only the action knows if
01157         the edit bar makes sense. Until it goes into actions, we do the
01158         checking here.
01159 
01160         @param page: current page
01161         @rtype: bool
01162         @return: true if editbar should show
01163         """
01164         # Show editbar only for existing pages, including deleted pages,
01165         # that the user may read. If you may not read, you can't edit,
01166         # so you don't need editbar.
01167         if (page.exists(includeDeleted=1) and
01168             self.request.user.may.read(page.page_name)):
01169             form = self.request.form
01170             action = self.request.action
01171             # Do not show editbar on edit but on save/cancel
01172             return not (action == 'edit' and
01173                         not form.has_key('button_save') and
01174                         not form.has_key('button_cancel'))
01175         return False
01176 
01177     def editbarItems(self, page):
01178         """ Return list of items to show on the editbar
01179 
01180         This is separate method to make it easy to customize the
01181         edtibar in sub classes.
01182         """
01183         _ = self.request.getText
01184         editbar_actions = []
01185         for editbar_item in self.request.cfg.edit_bar:
01186             if (editbar_item == 'Discussion' and
01187                (self.request.getPragma('supplementation-page', self.request.cfg.supplementation_page)
01188                                                    in (True, 1, 'on', '1'))):
01189                     editbar_actions.append(self.supplementation_page_nameLink(page))
01190             elif editbar_item == 'Comments':
01191                 # we just use <a> to get same style as other links, but we add some dummy
01192                 # link target to get correct mouseover pointer appearance. return false
01193                 # keeps the browser away from jumping to the link target::
01194                 editbar_actions.append('<a href="#" class="nbcomment" onClick="toggleComments();return false;">%s</a>' % _('Comments'))
01195             elif editbar_item == 'Edit':
01196                 editbar_actions.append(self.editorLink(page))
01197             elif editbar_item == 'Info':
01198                 editbar_actions.append(self.infoLink(page))
01199             elif editbar_item == 'Subscribe':
01200                 editbar_actions.append(self.subscribeLink(page))
01201             elif editbar_item == 'Quicklink':
01202                 editbar_actions.append(self.quicklinkLink(page))
01203             elif editbar_item == 'Attachments':
01204                 editbar_actions.append(self.attachmentsLink(page))
01205             elif editbar_item == 'ActionsMenu':
01206                 editbar_actions.append(self.actionsMenu(page))
01207         return editbar_actions
01208 
01209     def supplementation_page_nameLink(self, page):
01210         """Return a link to the discussion page
01211 
01212            If the discussion page doesn't exist and the user
01213            has no right to create it, show a disabled link.
01214         """
01215         _ = self.request.getText
01216         suppl_name = self.request.cfg.supplementation_page_name
01217         suppl_name_full = "%s/%s" % (page.page_name, suppl_name)
01218 
01219         test = Page(self.request, suppl_name_full)
01220         if not test.exists() and not self.request.user.may.write(suppl_name_full):
01221             return ('<span class="disabled">%s</span>' % _(suppl_name))
01222         else:
01223             return page.link_to(self.request, text=_(suppl_name),
01224                                 querystr={'action': 'supplementation'}, css_class='nbsupplementation', rel='nofollow')
01225 
01226     def guiworks(self, page):
01227         """ Return whether the gui editor / converter can work for that page.
01228 
01229             The GUI editor currently only works for wiki format.
01230             For simplicity, we also tell it does not work if the admin forces the text editor.
01231         """
01232         is_wiki = page.pi['format'] == 'wiki'
01233         gui_disallowed = self.cfg.editor_force and self.cfg.editor_default == 'text'
01234         return is_wiki and not gui_disallowed
01235 
01236 
01237     def editorLink(self, page):
01238         """ Return a link to the editor
01239 
01240         If the user can't edit, return a disabled edit link.
01241 
01242         If the user want to show both editors, it will display "Edit
01243         (Text)", otherwise as "Edit".
01244         """
01245         if 'edit' in self.request.cfg.actions_excluded:
01246             return ""
01247 
01248         if not (page.isWritable() and
01249                 self.request.user.may.write(page.page_name)):
01250             return self.disabledEdit()
01251 
01252         _ = self.request.getText
01253         querystr = {'action': 'edit'}
01254 
01255         guiworks = self.guiworks(page)
01256         if self.showBothEditLinks() and guiworks:
01257             text = _('Edit (Text)')
01258             querystr['editor'] = 'text'
01259             attrs = {'name': 'texteditlink', 'rel': 'nofollow', }
01260         else:
01261             text = _('Edit')
01262             if guiworks:
01263                 # 'textonly' will be upgraded dynamically to 'guipossible' by JS
01264                 querystr['editor'] = 'textonly'
01265                 attrs = {'name': 'editlink', 'rel': 'nofollow', }
01266             else:
01267                 querystr['editor'] = 'text'
01268                 attrs = {'name': 'texteditlink', 'rel': 'nofollow', }
01269 
01270         return page.link_to(self.request, text=text, querystr=querystr, **attrs)
01271 
01272     def showBothEditLinks(self):
01273         """ Return True if both edit links should be displayed """
01274         editor = self.request.user.editor_ui
01275         if editor == '<default>':
01276             editor = self.request.cfg.editor_ui
01277         return editor == 'freechoice'
01278 
01279     def guiEditorScript(self, d):
01280         """ Return a script that set the gui editor link variables
01281 
01282         The link will be created only when javascript is enabled and
01283         the browser is compatible with the editor.
01284         """
01285         page = d['page']
01286         if not (page.isWritable() and
01287                 self.request.user.may.write(page.page_name) and
01288                 self.showBothEditLinks() and
01289                 self.guiworks(page)):
01290             return ''
01291 
01292         _ = self.request.getText
01293         return """\
01294 <script type="text/javascript">
01295 <!-- // GUI edit link and i18n
01296 var gui_editor_link_href = "%(url)s";
01297 var gui_editor_link_text = "%(text)s";
01298 //-->
01299 </script>
01300 """ % {'url': page.url(self.request, querystr={'action': 'edit', 'editor': 'gui', }),
01301        'text': _('Edit (GUI)'),
01302       }
01303 
01304     def disabledEdit(self):
01305         """ Return a disabled edit link """
01306         _ = self.request.getText
01307         return ('<span class="disabled">%s</span>'
01308                 % _('Immutable Page'))
01309 
01310     def infoLink(self, page):
01311         """ Return link to page information """
01312         if 'info' in self.request.cfg.actions_excluded:
01313             return ""
01314 
01315         _ = self.request.getText
01316         return page.link_to(self.request,
01317                             text=_('Info'),
01318                             querystr={'action': 'info'}, css_class='nbinfo', rel='nofollow')
01319 
01320     def subscribeLink(self, page):
01321         """ Return subscribe/unsubscribe link to valid users
01322 
01323         @rtype: unicode
01324         @return: subscribe or unsubscribe link
01325         """
01326         if not ((self.cfg.mail_enabled or self.cfg.jabber_enabled) and self.request.user.valid):
01327             return ''
01328 
01329         _ = self.request.getText
01330         if self.request.user.isSubscribedTo([page.page_name]):
01331             action, text = 'unsubscribe', _("Unsubscribe")
01332         else:
01333             action, text = 'subscribe', _("Subscribe")
01334         if action in self.request.cfg.actions_excluded:
01335             return ""
01336         return page.link_to(self.request, text=text, querystr={'action': action}, css_class='nbsubscribe', rel='nofollow')
01337 
01338     def quicklinkLink(self, page):
01339         """ Return add/remove quicklink link
01340 
01341         @rtype: unicode
01342         @return: link to add or remove a quicklink
01343         """
01344         if not self.request.user.valid:
01345             return ''
01346 
01347         _ = self.request.getText
01348         if self.request.user.isQuickLinkedTo([page.page_name]):
01349             action, text = 'quickunlink', _("Remove Link")
01350         else:
01351             action, text = 'quicklink', _("Add Link")
01352         if action in self.request.cfg.actions_excluded:
01353             return ""
01354         return page.link_to(self.request, text=text, querystr={'action': action}, css_class='nbquicklink', rel='nofollow')
01355 
01356     def attachmentsLink(self, page):
01357         """ Return link to page attachments """
01358         if 'AttachFile' in self.request.cfg.actions_excluded:
01359             return ""
01360 
01361         _ = self.request.getText
01362         return page.link_to(self.request,
01363                             text=_('Attachments'),
01364                             querystr={'action': 'AttachFile'}, css_class='nbattachments', rel='nofollow')
01365 
01366     def startPage(self):
01367         """ Start page div with page language and direction
01368 
01369         @rtype: unicode
01370         @return: page div with language and direction attribtues
01371         """
01372         return u'<div id="page"%s>\n' % self.content_lang_attr()
01373 
01374     def endPage(self):
01375         """ End page div
01376 
01377         Add an empty page bottom div to prevent floating elements to
01378         float out of the page bottom over the footer.
01379         """
01380         return '<div id="pagebottom"></div>\n</div>\n'
01381 
01382     # Public functions #####################################################
01383 
01384     def header(self, d, **kw):
01385         """ Assemble page header
01386 
01387         Default behavior is to start a page div. Sub class and add
01388         footer items.
01389 
01390         @param d: parameter dictionary
01391         @rtype: string
01392         @return: page header html
01393         """
01394         return self.startPage()
01395 
01396     editorheader = header
01397 
01398     def footer(self, d, **keywords):
01399         """ Assemble page footer
01400 
01401         Default behavior is to end page div. Sub class and add
01402         footer items.
01403 
01404         @param d: parameter dictionary
01405         @keyword ...:...
01406         @rtype: string
01407         @return: page footer html
01408         """
01409         return self.endPage()
01410 
01411     # RecentChanges ######################################################
01412 
01413     def recentchanges_entry(self, d):
01414         """
01415         Assemble a single recentchanges entry (table row)
01416 
01417         @param d: parameter dictionary
01418         @rtype: string
01419         @return: recentchanges entry html
01420         """
01421         _ = self.request.getText
01422         html = []
01423         html.append('<tr>\n')
01424 
01425         html.append('<td class="rcicon1">%(icon_html)s</td>\n' % d)
01426 
01427         html.append('<td class="rcpagelink">%(pagelink_html)s</td>\n' % d)
01428 
01429         html.append('<td class="rctime">')
01430         if d['time_html']:
01431             html.append("%(time_html)s" % d)
01432         html.append('</td>\n')
01433 
01434         html.append('<td class="rcicon2">%(info_html)s</td>\n' % d)
01435 
01436         html.append('<td class="rceditor">')
01437         if d['editors']:
01438             html.append('<br>'.join(d['editors']))
01439         html.append('</td>\n')
01440 
01441         html.append('<td class="rccomment">')
01442         if d['comments']:
01443             if d['changecount'] > 1:
01444                 notfirst = 0
01445                 for comment in d['comments']:
01446                     html.append('%s<tt>#%02d</tt>&nbsp;%s' % (
01447                         notfirst and '<br>' or '', comment[0], comment[1]))
01448                     notfirst = 1
01449             else:
01450                 comment = d['comments'][0]
01451                 html.append('%s' % comment[1])
01452         html.append('</td>\n')
01453 
01454         html.append('</tr>\n')
01455 
01456         return ''.join(html)
01457 
01458     def recentchanges_daybreak(self, d):
01459         """
01460         Assemble a rc daybreak indication (table row)
01461 
01462         @param d: parameter dictionary
01463         @rtype: string
01464         @return: recentchanges daybreak html
01465         """
01466         if d['bookmark_link_html']:
01467             set_bm = '&nbsp; %(bookmark_link_html)s' % d
01468         else:
01469             set_bm = ''
01470         return ('<tr class="rcdaybreak"><td colspan="%d">'
01471                 '<strong>%s</strong>'
01472                 '%s'
01473                 '</td></tr>\n') % (6, d['date'], set_bm)
01474 
01475     def recentchanges_header(self, d):
01476         """
01477         Assemble the recentchanges header (intro + open table)
01478 
01479         @param d: parameter dictionary
01480         @rtype: string
01481         @return: recentchanges header html
01482         """
01483         _ = self.request.getText
01484 
01485         # Should use user interface language and direction
01486         html = '<div class="recentchanges"%s>\n' % self.ui_lang_attr()
01487         html += '<div>\n'
01488         page = d['page']
01489         if self.shouldUseRSS(page):
01490             link = [
01491                 u'<div class="rcrss">',
01492                 self.request.formatter.url(1, self.rsshref(page)),
01493                 self.request.formatter.rawHTML(self.make_icon("rss")),
01494                 self.request.formatter.url(0),
01495                 u'</div>',
01496                 ]
01497             html += ''.join(link)
01498         html += '<p>'
01499         # Add day selector
01500         if d['rc_days']:
01501             days = []
01502             for day in d['rc_days']:
01503                 if day == d['rc_max_days']:
01504                     days.append('<strong>%d</strong>' % day)
01505                 else:
01506                     days.append(
01507                         wikiutil.link_tag(self.request,
01508                             '%s?max_days=%d' % (d['q_page_name'], day),
01509                             str(day),
01510                             self.request.formatter, rel='nofollow'))
01511             days = ' | '.join(days)
01512             html += (_("Show %s days.") % (days, ))
01513 
01514         if d['rc_update_bookmark']:
01515             html += " %(rc_update_bookmark)s %(rc_curr_bookmark)s" % d
01516 
01517         html += '</p>\n</div>\n'
01518 
01519         html += '<table>\n'
01520         return html
01521 
01522     def recentchanges_footer(self, d):
01523         """
01524         Assemble the recentchanges footer (close table)
01525 
01526         @param d: parameter dictionary
01527         @rtype: string
01528         @return: recentchanges footer html
01529         """
01530         _ = self.request.getText
01531         html = ''
01532         html += '</table>\n'
01533         if d['rc_msg']:
01534             html += "<br>%(rc_msg)s\n" % d
01535         html += '</div>\n'
01536         return html
01537 
01538     # Language stuff ####################################################
01539 
01540     def ui_lang_attr(self):
01541         """Generate language attributes for user interface elements
01542 
01543         User interface elements use the user language (if any), kept in
01544         request.lang.
01545 
01546         @rtype: string
01547         @return: lang and dir html attributes
01548         """
01549         lang = self.request.lang
01550         return ' lang="%s" dir="%s"' % (lang, i18n.getDirection(lang))
01551 
01552     def content_lang_attr(self):
01553         """Generate language attributes for wiki page content
01554 
01555         Page content uses the page language or the wiki default language.
01556 
01557         @rtype: string
01558         @return: lang and dir html attributes
01559         """
01560         lang = self.request.content_lang
01561         return ' lang="%s" dir="%s"' % (lang, i18n.getDirection(lang))
01562 
01563     def add_msg(self, msg, msg_class=None):
01564         """ Adds a message to a list which will be used to generate status
01565         information.
01566 
01567         @param msg: additional message
01568         @param msg_class: html class for the div of the additional message.
01569         """
01570         if not msg_class:
01571             msg_class = 'dialog'
01572         if self._send_title_called:
01573             raise Exception("You cannot call add_msg() after send_title()")
01574         self._status.append((msg, msg_class))
01575 
01576     # stuff from wikiutil.py
01577     def send_title(self, text, **keywords):
01578         """
01579         Output the page header (and title).
01580 
01581         @param text: the title text
01582         @keyword page: the page instance that called us - using this is more efficient than using pagename..
01583         @keyword pagename: 'PageName'
01584         @keyword print_mode: 1 (or 0)
01585         @keyword editor_mode: 1 (or 0)
01586         @keyword media: css media type, defaults to 'screen'
01587         @keyword allow_doubleclick: 1 (or 0)
01588         @keyword html_head: additional <head> code
01589         @keyword body_attr: additional <body> attributes
01590         @keyword body_onload: additional "onload" JavaScript code
01591         """
01592         request = self.request
01593         _ = request.getText
01594         rev = request.rev
01595 
01596         if keywords.has_key('page'):
01597             page = keywords['page']
01598             pagename = page.page_name
01599         else:
01600             pagename = keywords.get('pagename', '')
01601             page = Page(request, pagename)
01602         if keywords.get('msg', ''):
01603             raise DeprecationWarning("Using send_page(msg=) is deprecated! Use theme.add_msg() instead!")
01604         scriptname = request.script_root
01605 
01606         # get name of system pages
01607         page_front_page = wikiutil.getFrontPage(request).page_name
01608         page_help_contents = wikiutil.getLocalizedPage(request, 'HelpContents').page_name
01609         page_title_index = wikiutil.getLocalizedPage(request, 'TitleIndex').page_name
01610         page_site_navigation = wikiutil.getLocalizedPage(request, 'SiteNavigation').page_name
01611         page_word_index = wikiutil.getLocalizedPage(request, 'WordIndex').page_name
01612         page_help_formatting = wikiutil.getLocalizedPage(request, 'HelpOnFormatting').page_name
01613         page_find_page = wikiutil.getLocalizedPage(request, 'FindPage').page_name
01614         home_page = wikiutil.getInterwikiHomePage(request) # sorry theme API change!!! Either None or tuple (wikiname,pagename) now.
01615         page_parent_page = getattr(page.getParentPage(), 'page_name', None)
01616 
01617         # Prepare the HTML <head> element
01618         user_head = [request.cfg.html_head]
01619 
01620         # include charset information - needed for moin_dump or any other case
01621         # when reading the html without a web server
01622         user_head.append('''<meta http-equiv="Content-Type" content="%s;charset=%s">\n''' % (page.output_mimetype, page.output_charset))
01623 
01624         meta_keywords = request.getPragma('keywords')
01625         meta_desc = request.getPragma('description')
01626         if meta_keywords:
01627             user_head.append('<meta name="keywords" content="%s">\n' % wikiutil.escape(meta_keywords, 1))
01628         if meta_desc:
01629             user_head.append('<meta name="description" content="%s">\n' % wikiutil.escape(meta_desc, 1))
01630 
01631         # search engine precautions / optimization:
01632         # if it is an action or edit/search, send query headers (noindex,nofollow):
01633         if request.query_string:
01634             user_head.append(request.cfg.html_head_queries)
01635         elif request.method == 'POST':
01636             user_head.append(request.cfg.html_head_posts)
01637         # we don't want to have BadContent stuff indexed:
01638         elif pagename in ['BadContent', 'LocalBadContent', ]:
01639             user_head.append(request.cfg.html_head_posts)
01640         # if it is a special page, index it and follow the links - we do it
01641         # for the original, English pages as well as for (the possibly
01642         # modified) frontpage:
01643         elif pagename in [page_front_page, request.cfg.page_front_page,
01644                           page_title_index, 'TitleIndex',
01645                           page_find_page, 'FindPage',
01646                           page_site_navigation, 'SiteNavigation',
01647                           'RecentChanges', ]:
01648             user_head.append(request.cfg.html_head_index)
01649         # if it is a normal page, index it, but do not follow the links, because
01650         # there are a lot of illegal links (like actions) or duplicates:
01651         else:
01652             user_head.append(request.cfg.html_head_normal)
01653 
01654         if 'pi_refresh' in keywords and keywords['pi_refresh']:
01655             user_head.append('<meta http-equiv="refresh" content="%d;URL=%s">' % keywords['pi_refresh'])
01656 
01657         # output buffering increases latency but increases throughput as well
01658         output = []
01659         # later: <html xmlns=\"http://www.w3.org/1999/xhtml\">
01660         output.append("""\
01661 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
01662 <html>
01663 <head>
01664 %s
01665 %s
01666 %s
01667 """ % (
01668             ''.join(user_head),
01669             self.html_head({
01670                 'page': page,
01671                 'title': text,
01672                 'sitename': request.cfg.html_pagetitle or request.cfg.sitename,
01673                 'print_mode': keywords.get('print_mode', False),
01674                 'media': keywords.get('media', 'screen'),
01675             }),
01676             keywords.get('html_head', ''),
01677         ))
01678 
01679         # Links
01680         output.append('<link rel="Start" href="%s">\n' % request.href(page_front_page))
01681         if pagename:
01682             output.append('<link rel="Alternate" title="%s" href="%s">\n' % (
01683                     _('Wiki Markup'), request.href(pagename, action='raw')))
01684             output.append('<link rel="Alternate" media="print" title="%s" href="%s">\n' % (
01685                     _('Print View'), request.href(pagename, action='print')))
01686 
01687             # !!! currently disabled due to Mozilla link prefetching, see
01688             # http://www.mozilla.org/projects/netlib/Link_Prefetching_FAQ.html
01689             #~ all_pages = request.getPageList()
01690             #~ if all_pages:
01691             #~     try:
01692             #~         pos = all_pages.index(pagename)
01693             #~     except ValueError:
01694             #~         # this shopuld never happend in theory, but let's be sure
01695             #~         pass
01696             #~     else:
01697             #~         request.write('<link rel="First" href="%s/%s">\n' % (request.script_root, quoteWikinameURL(all_pages[0]))
01698             #~         if pos > 0:
01699             #~             request.write('<link rel="Previous" href="%s/%s">\n' % (request.script_root, quoteWikinameURL(all_pages[pos-1])))
01700             #~         if pos+1 < len(all_pages):
01701             #~             request.write('<link rel="Next" href="%s/%s">\n' % (request.script_root, quoteWikinameURL(all_pages[pos+1])))
01702             #~         request.write('<link rel="Last" href="%s/%s">\n' % (request.script_root, quoteWikinameURL(all_pages[-1])))
01703 
01704             if page_parent_page:
01705                 output.append('<link rel="Up" href="%s">\n' % request.href(page_parent_page))
01706 
01707         # write buffer because we call AttachFile
01708         request.write(''.join(output))
01709         output = []
01710 
01711         # XXX maybe this should be removed completely. moin emits all attachments as <link rel="Appendix" ...>
01712         # and it is at least questionable if this fits into the original intent of rel="Appendix".
01713         if pagename and request.user.may.read(pagename):
01714             from MoinMoin.action import AttachFile
01715             AttachFile.send_link_rel(request, pagename)
01716 
01717         output.extend([
01718             '<link rel="Search" href="%s">\n' % request.href(page_find_page),
01719             '<link rel="Index" href="%s">\n' % request.href(page_title_index),
01720             '<link rel="Glossary" href="%s">\n' % request.href(page_word_index),
01721             '<link rel="Help" href="%s">\n' % request.href(page_help_formatting),
01722                       ])
01723 
01724         output.append("</head>\n")
01725         request.write(''.join(output))
01726         output = []
01727 
01728         # start the <body>
01729         bodyattr = []
01730         if keywords.has_key('body_attr'):
01731             bodyattr.append(' ')
01732             bodyattr.append(keywords['body_attr'])
01733 
01734         # Add doubleclick edit action
01735         if (pagename and keywords.get('allow_doubleclick', 0) and
01736             not keywords.get('print_mode', 0) and
01737             request.user.edit_on_doubleclick):
01738             if request.user.may.write(pagename): # separating this gains speed
01739                 url = page.url(request, {'action': 'edit'})
01740                 bodyattr.append(''' ondblclick="location.href='%s'" ''' % wikiutil.escape(url, True))
01741 
01742         # Set body to the user interface language and direction
01743         bodyattr.append(' %s' % self.ui_lang_attr())
01744 
01745         body_onload = keywords.get('body_onload', '')
01746         if body_onload:
01747             bodyattr.append(''' onload="%s"''' % body_onload)
01748         output.append('\n<body%s>\n' % ''.join(bodyattr))
01749 
01750         # Output -----------------------------------------------------------
01751 
01752         # If in print mode, start page div and emit the title
01753         if keywords.get('print_mode', 0):
01754             d = {
01755                 'title_text': text,
01756                 'page': page,
01757                 'page_name': pagename or '',
01758                 'rev': rev,
01759             }
01760             request.themedict = d
01761             output.append(self.startPage())
01762             output.append(self.interwiki(d))
01763             output.append(self.title(d))
01764 
01765         # In standard mode, emit theme.header
01766         else:
01767             exists = pagename and page.exists(includeDeleted=True)
01768             # prepare dict for theme code:
01769             d = {
01770                 'theme': self.name,
01771                 'script_name': scriptname,
01772                 'title_text': text,
01773                 'logo_string': request.cfg.logo_string,
01774                 'site_name': request.cfg.sitename,
01775                 'page': page,
01776                 'rev': rev,
01777                 'pagesize': pagename and page.size() or 0,
01778                 # exists checked to avoid creation of empty edit-log for non-existing pages
01779                 'last_edit_info': exists and page.lastEditInfo() or '',
01780                 'page_name': pagename or '',
01781                 'page_find_page': page_find_page,
01782                 'page_front_page': page_front_page,
01783                 'home_page': home_page,
01784                 'page_help_contents': page_help_contents,
01785                 'page_help_formatting': page_help_formatting,
01786                 'page_parent_page': page_parent_page,
01787                 'page_title_index': page_title_index,
01788                 'page_word_index': page_word_index,
01789                 'user_name': request.user.name,
01790                 'user_valid': request.user.valid,
01791                 'msg': self._status,
01792                 'trail': keywords.get('trail', None),
01793                 # Discontinued keys, keep for a while for 3rd party theme developers
01794                 'titlesearch': 'use self.searchform(d)',
01795                 'textsearch': 'use self.searchform(d)',
01796                 'navibar': ['use self.navibar(d)'],
01797                 'available_actions': ['use self.request.availableActions(page)'],
01798             }
01799 
01800             # add quoted versions of pagenames
01801             newdict = {}
01802             for key in d:
01803                 if key.startswith('page_'):
01804                     if not d[key] is None:
01805                         newdict['q_'+key] = wikiutil.quoteWikinameURL(d[key])
01806                     else:
01807                         newdict['q_'+key] = None
01808             d.update(newdict)
01809             request.themedict = d
01810 
01811             # now call the theming code to do the rendering
01812             if keywords.get('editor_mode', 0):
01813                 output.append(self.editorheader(d))
01814             else:
01815                 output.append(self.header(d))
01816 
01817         # emit it
01818         request.write(''.join(output))
01819         output = []
01820         self._send_title_called = True
01821 
01822     def send_footer(self, pagename, **keywords):
01823         """
01824         Output the page footer.
01825 
01826         @param pagename: WikiName of the page
01827         @keyword print_mode: true, when page is displayed in Print mode
01828         """
01829         request = self.request
01830         d = request.themedict
01831 
01832         # Emit end of page in print mode, or complete footer in standard mode
01833         if keywords.get('print_mode', 0):
01834             request.write(self.pageinfo(d['page']))
01835             request.write(self.endPage())
01836         else:
01837             request.write(self.footer(d, **keywords))
01838 
01839     # stuff moved from request.py
01840     def send_closing_html(self):
01841         """ generate timing info html and closing html tag,
01842             everyone calling send_title must call this at the end to close
01843             the body and html tags.
01844         """
01845         request = self.request
01846 
01847         # as this is the last chance to emit some html, we stop the clocks:
01848         request.clock.stop('run')
01849         request.clock.stop('total')
01850 
01851         # Close html code
01852         if request.cfg.show_timings and request.action != 'print':
01853             request.write('<ul id="timings">\n')
01854             for t in request.clock.dump():
01855                 request.write('<li>%s</li>\n' % t)
01856             request.write('</ul>\n')
01857         #request.write('<!-- auth_method == %s -->' % repr(request.user.auth_method))
01858         request.write('</body>\n</html>\n\n')
01859 
01860     def sidebar(self, d, **keywords):
01861         """ Display page called SideBar as an additional element on every page
01862 
01863         @param d: parameter dictionary
01864         @rtype: string
01865         @return: sidebar html
01866         """
01867 
01868         # Check which page to display, return nothing if doesn't exist.
01869         sidebar = self.request.getPragma('sidebar', u'SideBar')
01870         page = Page(self.request, sidebar)
01871         if not page.exists():
01872             return u""
01873         # Capture the page's generated HTML in a buffer.
01874         buffer = StringIO.StringIO()
01875         self.request.redirect(buffer)
01876         try:
01877             page.send_page(content_only=1, content_id="sidebar")
01878         finally:
01879             self.request.redirect()
01880         return u'<div class="sidebar">%s</div>' % buffer.getvalue()
01881 
01882 
01883 class ThemeNotFound(Exception):
01884     """ Thrown if the supplied theme could not be found anywhere """
01885 
01886 def load_theme(request, theme_name=None):
01887     """ Load a theme for this request.
01888 
01889     @param request: moin request
01890     @param theme_name: the name of the theme
01891     @type theme_name: str
01892     @rtype: Theme
01893     @return: a theme initialized for the request
01894     """
01895     if theme_name is None or theme_name == '<default>':
01896         theme_name = request.cfg.theme_default
01897 
01898     try:
01899         Theme = wikiutil.importPlugin(request.cfg, 'theme', theme_name, 'Theme')
01900     except wikiutil.PluginMissingError:
01901         raise ThemeNotFound(theme_name)
01902 
01903     return Theme(request)
01904 
01905 def load_theme_fallback(request, theme_name=None):
01906     """ Try loading a theme, falling back to defaults on error.
01907 
01908     @param request: moin request
01909     @param theme_name: the name of the theme
01910     @type theme_name: str
01911     @rtype: int
01912     @return: A statuscode for how successful the loading was
01913              0 - theme was loaded
01914              1 - fallback to default theme
01915              2 - serious fallback to builtin theme
01916     """
01917     fallback = 0
01918     try:
01919         request.theme = load_theme(request, theme_name)
01920     except ThemeNotFound:
01921         fallback = 1
01922         try:
01923             request.theme = load_theme(request, request.cfg.theme_default)
01924         except ThemeNotFound:
01925             fallback = 2
01926             from MoinMoin.theme.modern import Theme
01927             request.theme = Theme(request)