comparison MoinMoin/theme/__init__.py @ 0:77665d8e2254

tag of nonpublic@localhost--archive/moin--enterprise--1.5--base-0 (automatically generated log message) imported from: moin--main--1.5--base-0
author Thomas Waldmann <tw-public@gmx.de>
date Thu, 22 Sep 2005 15:09:50 +0000
parents
children 13f4178d6c13
comparison
equal deleted inserted replaced
-1:000000000000 0:77665d8e2254
1 # -*- coding: iso-8859-1 -*-
2 """
3 MoinMoin - Theme Package
4
5 @copyright: 2003-2005 by Thomas Waldmann (MoinMoin:ThomasWaldmann)
6 @license: GNU GPL, see COPYING for details.
7 """
8
9 from MoinMoin import i18n, wikiutil, config, version
10 from MoinMoin.Page import Page
11 from MoinMoin.util import pysupport
12
13 modules = pysupport.getPackageModules(__file__)
14
15
16 class ThemeBase:
17 """ Base class for themes
18
19 This class supply all the standard template that sub classes can
20 use without rewriting the same code. If you want to change certain
21 elements, override them.
22 """
23
24 name = 'base'
25
26 # fake _ function to get gettext recognize those texts:
27 _ = lambda x: x
28
29 # TODO: remove icons that are not used any more.
30 icons = {
31 # key alt icon filename w h
32 # ------------------------------------------------------------------
33 # navibar
34 'help': ("%(page_help_contents)s", "moin-help.png", 12, 11),
35 'find': ("%(page_find_page)s", "moin-search.png", 12, 12),
36 'diff': (_("Diffs"), "moin-diff.png", 15, 11),
37 'info': (_("Info"), "moin-info.png", 12, 11),
38 'edit': (_("Edit"), "moin-edit.png", 12, 12),
39 'unsubscribe':(_("Unsubscribe"), "moin-unsubscribe.png", 14, 10),
40 'subscribe': (_("Subscribe"), "moin-subscribe.png",14, 10),
41 'raw': (_("Raw"), "moin-raw.png", 12, 13),
42 'xml': (_("XML"), "moin-xml.png", 20, 13),
43 'print': (_("Print"), "moin-print.png", 16, 14),
44 'view': (_("View"), "moin-show.png", 12, 13),
45 'home': (_("Home"), "moin-home.png", 13, 12),
46 'up': (_("Up"), "moin-parent.png", 15, 13),
47 # FileAttach
48 'attach': ("%(attach_count)s", "moin-attach.png", 7, 15),
49 # RecentChanges
50 'rss': (_("[RSS]"), "moin-rss.png", 36, 14),
51 'deleted': (_("[DELETED]"), "moin-deleted.png",60, 12),
52 'updated': (_("[UPDATED]"), "moin-updated.png",60, 12),
53 'new': (_("[NEW]"), "moin-new.png", 31, 12),
54 'diffrc': (_("[DIFF]"), "moin-diff.png", 15, 11),
55 # General
56 'bottom': (_("[BOTTOM]"), "moin-bottom.png", 14, 10),
57 'top': (_("[TOP]"), "moin-top.png", 14, 10),
58 'www': ("[WWW]", "moin-www.png", 11, 11),
59 'mailto': ("[MAILTO]", "moin-email.png", 14, 10),
60 'news': ("[NEWS]", "moin-news.png", 10, 11),
61 'telnet': ("[TELNET]", "moin-telnet.png", 10, 11),
62 'ftp': ("[FTP]", "moin-ftp.png", 11, 11),
63 'file': ("[FILE]", "moin-ftp.png", 11, 11),
64 # search forms
65 'searchbutton': ("[?]", "moin-search.png", 12, 12),
66 'interwiki': ("[%(wikitag)s]", "moin-inter.png", 16, 16),
67 }
68 del _
69
70 # Style sheets - usually there is no need to override this in sub
71 # classes. Simply supply the css files in the css directory.
72
73 # Standard set of style sheets
74 stylesheets = (
75 # media basename
76 ('all', 'common'),
77 ('screen', 'screen'),
78 ('print', 'print'),
79 ('projection', 'projection'),
80 )
81
82 # Used in print mode
83 stylesheets_print = (
84 # media basename
85 ('all', 'common'),
86 ('all', 'print'),
87 )
88
89 # Used in slide show mode
90 stylesheets_projection = (
91 # media basename
92 ('all', 'common'),
93 ('all', 'projection'),
94 )
95
96 stylesheetsCharset = 'utf-8'
97
98 def __init__(self, request):
99 """
100 Initialize the theme object.
101
102 @param request: the request object
103 """
104 self.request = request
105 self.cfg = request.cfg
106 self._cache = {} # Used to cache elements that may be used several times
107
108 def img_url(self, img):
109 """ Generate an image href
110
111 @param img: the image filename
112 @rtype: string
113 @return: the image href
114 """
115 return "%s/%s/img/%s" % (self.cfg.url_prefix, self.name, img)
116
117 def emit_custom_html(self, html):
118 """
119 generate custom HTML code in `html`
120
121 @param html: a string or a callable object, in which case
122 it is called and its return value is used
123 @rtype: string
124 @return: string with html
125 """
126 if html:
127 if callable(html): html = html(self.request)
128 return html
129
130 def logo(self):
131 """ Assemble logo with link to front page
132
133 The logo contain an image and or text or any html markup the
134 admin inserted in the config file. Everything it enclosed inside
135 a div with id="logo".
136
137 @param d: parameter dictionary
138 @rtype: unicode
139 @return: logo html
140 """
141 if self.cfg.logo_string:
142 pagename = wikiutil.getFrontPage(self.request).page_name
143 pagename = wikiutil.quoteWikinameURL(pagename)
144 logo = wikiutil.link_tag(self.request, pagename, self.cfg.logo_string)
145 html = u'''<div id="logo">%s</div>''' % logo
146 return html
147 return u''
148
149 def title(self, d):
150 """ Assemble the title
151
152 @param d: parameter dictionary
153 @rtype: string
154 @return: title html
155 """
156 _ = self.request.getText
157 if d['title_link']:
158 content = ('<a title="%(title)s" href="%(href)s">%(text)s</a>') % {
159 'title': _('Click to do a full-text search for this title'),
160 'href': d['title_link'],
161 'text': wikiutil.escape(d['title_text']),
162 }
163 else:
164 content = wikiutil.escape(d['title_text'])
165 html = '''
166 <h1 id="title">%s</h1>
167 ''' % content
168
169 return html
170
171 def username(self, d):
172 """ Assemble the username / userprefs link
173
174 @param d: parameter dictionary
175 @rtype: unicode
176 @return: username html
177 """
178 request = self.request
179 _ = request.getText
180 preferencesPage = wikiutil.getSysPage(request, 'UserPreferences')
181
182 userlinks = []
183 # Add username/homepage link for registered users. We don't care
184 # if it exists, the user can create it.
185 if request.user.valid:
186 interwiki = wikiutil.getInterwikiHomePage(request)
187 name = request.user.name
188 aliasname = request.user.aliasname
189 if not aliasname:
190 aliasname = name
191 title = "%s @ %s" % (aliasname, interwiki[0])
192 homelink = (request.formatter.interwikilink(1, title=title, *interwiki) +
193 request.formatter.text(name) +
194 request.formatter.interwikilink(0))
195 userlinks.append(homelink)
196 # Set pref page to localized Preferences page
197 title = preferencesPage.split_title(request)
198 userlinks.append(preferencesPage.link_to(request, text=title))
199 else:
200 # Add prefpage links with title: Login
201 userlinks.append(preferencesPage.link_to(request, text=_("Login")))
202
203 userlinks = [u'<li>%s</li>\n' % link for link in userlinks]
204 html = u'<ul id="username">\n%s</ul>' % ''.join(userlinks)
205 return html
206
207 # Schemas supported in toolbar links, using [url label] foramrt
208 linkSchemas = [r'http://', r'https://', r'ftp://', 'mailto:'] + \
209 [x + ':' for x in config.url_schemas]
210
211 def splitNavilink(self, text, localize=1):
212 """ Split navibar links into pagename, link to page
213
214 Admin or user might want to use shorter navibar items by using
215 the [page title] or [url title] syntax. In this case, we don't
216 use localization, and the links goes to page or to the url, not
217 the localized version of page.
218
219 @param text: the text used in config or user preferences
220 @rtype: tuple
221 @return: pagename or url, link to page or url
222 """
223 request = self.request
224
225 # Handle [pagename title] or [url title] formats
226 if text.startswith('[') and text.endswith(']'):
227 try:
228 pagename, title = text[1:-1].strip().split(' ', 1)
229 title = title.strip()
230 localize = 0
231 except (ValueError, TypeError):
232 # Just use the text as is.
233 pagename = title = text
234
235
236 # Handle regular pagename like "FrontPage"
237 else:
238 # Use localized pages for the current user
239 if localize:
240 page = wikiutil.getSysPage(request, text)
241 else:
242 page = Page(request, text)
243 pagename = page.page_name
244 title = page.split_title(request)
245 title = self.shortenPagename(title)
246 link = page.link_to(request, title)
247
248
249 from MoinMoin import config
250 for scheme in self.linkSchemas:
251 if pagename.startswith(scheme):
252 title = wikiutil.escape(title)
253 link = '<a href="%s">%s</a>' % (pagename, title)
254 return pagename, link
255
256 # remove wiki: url prefix
257 if pagename.startswith("wiki:"):
258 pagename = pagename[5:]
259
260 # try handling interwiki links
261 try:
262 interwiki, page = pagename.split(':', 1)
263 return (pagename,
264 self.request.formatter.interwikilink(True, interwiki, page) +
265 page +
266 self.request.formatter.interwikilink(False)
267 )
268
269 except ValueError:
270 pass
271
272 # Normalize page names, replace '_' with ' '. Usually
273 # all names use spaces internally, but for
274 # [name_with_spaces label] we must save the underscores
275 # until this point.
276 pagename = request.normalizePagename(pagename)
277 link = Page(request, pagename).link_to(request, title)
278
279 return pagename, link
280
281 def shortenPagename(self, name):
282 """ Shorten page names
283
284 Shorten very long page names that tend to break the user
285 interface. The short name is usually fine, unless really stupid
286 long names are used (WYGIWYD).
287
288 If you don't like to do this in your theme, or want to use
289 different algorithm, override this method.
290
291 @param text: page name, unicode
292 @rtype: unicode
293 @return: shortened version.
294 """
295 maxLength = self.maxPagenameLength()
296 # First use only the sub page name, that might be enough
297 if len(name) > maxLength:
298 name = name.split('/')[-1]
299 # If its not enough, replace the middle with '...'
300 if len(name) > maxLength:
301 half, left = divmod(maxLength - 3, 2)
302 name = u'%s...%s' % (name[:half + left], name[-half:])
303 return name
304
305 def maxPagenameLength(self):
306 """ Return maximum length for shortened page names """
307 return 25
308
309 def navibar(self, d):
310 """ Assemble the navibar
311
312 @param d: parameter dictionary
313 @rtype: unicode
314 @return: navibar html
315 """
316 request = self.request
317 found = {} # pages we found. prevent duplicates
318 items = [] # navibar items
319 item = u'<li class="%s">%s</li>'
320 current = d['page_name']
321
322 # Process config navi_bar
323 if request.cfg.navi_bar:
324 for text in request.cfg.navi_bar:
325 pagename, link = self.splitNavilink(text)
326 cls = 'wikilink'
327 if pagename == current:
328 cls = 'wikilink current'
329 items.append(item % (cls, link))
330 found[pagename] = 1
331
332 # Add user links to wiki links, eliminating duplicates.
333 userlinks = request.user.getQuickLinks()
334 for text in userlinks:
335 # Split text without localization, user know what she wants
336 pagename, link = self.splitNavilink(text, localize=0)
337 if not pagename in found:
338 cls = 'userlink'
339 if pagename == current:
340 cls = 'userlink current'
341 items.append(item % (cls, link))
342 found[pagename] = 1
343
344 # Add current page at end
345 if not current in found:
346 title = d['page'].split_title(request)
347 title = self.shortenPagename(title)
348 link = d['page'].link_to(request, title)
349 cls = 'current'
350 items.append(item % (cls, link))
351
352 # Assemble html
353 items = u'\n'.join(items)
354 html = u'''
355 <ul id="navibar">
356 %s
357 </ul>
358 ''' % items
359 return html
360
361 def get_icon(self, icon):
362 """ Return icon data from self.icons
363
364 If called from [[Icon(file)]] we have a filename, not a
365 key. Using filenames is deprecated, but for now, we simulate old
366 behavior.
367
368 @param icon: icon name or file name (string)
369 @rtype: tuple
370 @return: alt (unicode), href (string), width, height (int)
371 """
372 if icon in self.icons:
373 alt, filename, w, h = self.icons[icon]
374 else:
375 # Create filenames to icon data mapping on first call, then
376 # cache in class for next calls.
377 if not getattr(self.__class__, 'iconsByFile', None):
378 d = {}
379 for data in self.icons.values():
380 d[data[1]] = data
381 self.__class__.iconsByFile = d
382
383 # Try to get icon data by file name
384 filename = icon.replace('.gif','.png')
385 if filename in self.iconsByFile:
386 alt, filename, w, h = self.iconsByFile[filename]
387 else:
388 alt, filename, w, h = '', icon, '', ''
389
390 return alt, self.img_url(filename), w, h
391
392 def make_icon(self, icon, vars=None):
393 """
394 This is the central routine for making <img> tags for icons!
395 All icons stuff except the top left logo, smileys and search
396 field icons are handled here.
397
398 @param icon: icon id (dict key)
399 @param vars: ...
400 @rtype: string
401 @return: icon html (img tag)
402 """
403 if vars is None:
404 vars = {}
405 alt, img, w, h = self.get_icon(icon)
406 try:
407 alt = alt % vars
408 except KeyError, err:
409 alt = 'KeyError: %s' % str(err)
410 if self.request:
411 alt = self.request.getText(alt, formatted=False)
412 try:
413 tag = self.request.formatter.image(src=img, alt=alt, width=w, height=h)
414 except AttributeError: # XXX FIXME if we have no formatter or no request
415 tag = '<img src="%s" alt="%s" width="%s" height="%s">' % (
416 img, alt, w, h)
417 import warnings
418 warnings.warn("calling themes without correct request", DeprecationWarning)
419 return tag
420
421 def make_iconlink(self, which, d):
422 """
423 Make a link with an icon
424
425 @param which: icon id (dictionary key)
426 @param d: parameter dictionary
427 @rtype: string
428 @return: html link tag
429 """
430 page_params, title, icon = self.cfg.page_icons_table[which]
431 d['title'] = title % d
432 d['i18ntitle'] = self.request.getText(d['title'], formatted=False)
433 img_src = self.make_icon(icon, d)
434 return wikiutil.link_tag(self.request, page_params % d, img_src, attrs='title="%(i18ntitle)s"' % d)
435
436 def msg(self, d):
437 """ Assemble the msg display
438
439 Display a message with a widget or simple strings with a clear message link.
440
441 @param d: parameter dictionary
442 @rtype: unicode
443 @return: msg display html
444 """
445 _ = self.request.getText
446 msg = d['msg']
447 if not msg:
448 return u''
449
450 if isinstance(msg, (str, unicode)):
451 # Render simple strings with a close link
452 close = d['page'].link_to(self.request,
453 text=_('Clear message'),
454 querystr={'action': 'show'})
455 html = u'<p>%s</p>\n<div class="buttons">%s</div>\n' % (msg, close)
456 else:
457 # msg is a widget
458 html = msg.render()
459
460 return u'<div id="message">\n%s\n</div>\n' % html
461
462 def trail(self, d):
463 """ Assemble page trail
464
465 @param d: parameter dictionary
466 @rtype: unicode
467 @return: trail html
468 """
469 request = self.request
470 user = request.user
471 if user.valid and user.show_page_trail:
472 trail = user.getTrail()
473 if trail:
474 items = []
475 # Show all items except the last one which is this page.
476 for pagename in trail[:-1]:
477 try:
478 interwiki, page = pagename.split(":", 1)
479 if request.cfg.interwikiname != interwiki:
480 link = (self.request.formatter.interwikilink(
481 True, interwiki, page) +
482 self.shortenPagename(page) +
483 self.request.formatter.interwikilink(False))
484 items.append('<li>%s</li>' % link)
485 continue
486 else:
487 pagename = page
488
489 except ValueError:
490 pass
491 page = Page(request, pagename)
492 title = page.split_title(request)
493 title = self.shortenPagename(title)
494 link = page.link_to(request, title)
495 items.append('<li>%s</li>' % link)
496 html = '''
497 <ul id="pagetrail">
498 %s
499 </ul>''' % '\n'.join(items)
500 return html
501 return ''
502
503 def html_stylesheets(self, d):
504 """ Assemble html head stylesheet links
505
506 @param d: parameter dictionary
507 @rtype: string
508 @return: stylesheets links
509 """
510 link = '<link rel="stylesheet" type="text/css" charset="%s" media="%s" href="%s">'
511
512 # Check mode
513 if d.get('print_mode'):
514 media = d.get('media', 'print')
515 stylesheets = getattr(self, 'stylesheets_' + media)
516 else:
517 stylesheets = self.stylesheets
518 usercss = self.request.user.valid and self.request.user.css_url
519
520 # Create stylesheets links
521 html = []
522 prefix = self.cfg.url_prefix
523 csshref = '%s/%s/css' % (prefix, self.name)
524 for media, basename in stylesheets:
525 href = '%s/%s.css' % (csshref, basename)
526 html.append(link % (self.stylesheetsCharset, media, href))
527
528 # Don't add user css url if it matches one of ours
529 if usercss and usercss == href:
530 usercss = None
531
532 # tribute to the most sucking browser...
533 if self.cfg.hacks.get('ie7', False):
534 html.append("""
535 <!-- compliance patch for microsoft browsers -->
536 <!--[if lt IE 7]>
537 <script src="%s/common/ie7/ie7-standard-p.js" type="text/javascript"></script>
538 <![endif]-->
539 """ % prefix)
540
541 # Add user css url (assuming that user css uses same charset)
542 if usercss and usercss.lower() != "none":
543 html.append(link % (self.stylesheetsCharset, 'all', usercss))
544
545 return '\n'.join(html)
546
547 def shouldShowPageinfo(self, page):
548 """ Should we show page info?
549
550 Should be implemented by actions. For now, we check here by action
551 name and page.
552
553 @param page: current page
554 @rtype: bool
555 @return: true if should show page info
556 """
557 if page.exists() and self.request.user.may.read(page.page_name):
558 # These actions show the page content.
559 # TODO: on new action, page info will not show. A better
560 # solution will be if the action itself answer the question:
561 # showPageInfo().
562 contentActions = [u'', u'show', u'refresh', u'preview', u'diff',
563 u'subscribe', u'RenamePage', u'DeletePage',
564 u'SpellCheck', u'print']
565 action = self.request.form.get('action', [''])[0]
566 return action in contentActions
567 return False
568
569 def pageinfo(self, page):
570 """ Return html fragment with page meta data
571
572 Since page information use translated text, it use the ui
573 language and direction. It looks strange sometimes, but
574 translated text using page direction look worse.
575
576 @param page: current page
577 @rtype: unicode
578 @return: page last edit information
579 """
580 _ = self.request.getText
581
582 if self.shouldShowPageinfo(page):
583 info = page.lastEditInfo()
584 if info:
585 if info['editor']:
586 info = _("last edited %(time)s by %(editor)s") % info
587 else:
588 info = _("last modified %(time)s") % info
589 return '<p id="pageinfo" class="info"%(lang)s>%(info)s</p>\n' % {
590 'lang': self.ui_lang_attr(),
591 'info': info
592 }
593 return ''
594
595 def searchform(self, d):
596 """
597 assemble HTML code for the search forms
598
599 @param d: parameter dictionary
600 @rtype: unicode
601 @return: search form html
602 """
603 _ = self.request.getText
604 form = self.request.form
605 updates = {
606 'search_label' : _('Search:'),
607 'search_value': wikiutil.escape(form.get('value', [''])[0], 1),
608 'search_full_label' : _('Text', formatted=False),
609 'search_title_label' : _('Titles', formatted=False),
610 }
611 d.update(updates)
612
613 html = u'''
614 <form id="searchform" method="get" action="">
615 <div>
616 <input type="hidden" name="action" value="fullsearch">
617 <input type="hidden" name="context" value="180">
618 <label for="searchinput">%(search_label)s</label>
619 <input id="searchinput" type="text" name="value" value="%(search_value)s" size="20"
620 onfocus="searchFocus(this)" onblur="searchBlur(this)"
621 onkeyup="searchChange(this)" onchange="searchChange(this)" alt="Search">
622 <input id="titlesearch" name="titlesearch" type="submit"
623 value="%(search_title_label)s" alt="Search Titles">
624 <input id="fullsearch" name="fullsearch" type="submit"
625 value="%(search_full_label)s" alt="Search Full Text">
626 </div>
627 </form>
628 <script type="text/javascript">
629 <!--// Initialize search form
630 var f = document.getElementById('searchform');
631 f.getElementsByTagName('label')[0].style.display = 'none';
632 var e = document.getElementById('searchinput');
633 searchChange(e);
634 searchBlur(e);
635 //-->
636 </script>
637 ''' % d
638 return html
639
640 def showversion(self, d, **keywords):
641 """
642 assemble HTML code for copyright and version display
643
644 @param d: parameter dictionary
645 @rtype: string
646 @return: copyright and version display html
647 """
648 html = ''
649 if self.cfg.show_version and not keywords.get('print_mode', 0):
650 html = (u'<div id="version">MoinMoin %s, Copyright 2000-2004 by '
651 'Juergen Hermann</div>') % (version.revision,)
652 return html
653
654 def headscript(self, d):
655 """ Return html head script with common functions
656
657 TODO: put these on common.js instead, so they can be downloaded
658 only once.
659
660 TODO: actionMenuInit should be called once, from body onload,
661 but currently body is not written by theme.
662
663 @param d: parameter dictionary
664 @rtype: unicode
665 @return: script for html head
666 """
667 # Don't add script for print view
668 if self.request.form.get('action', [''])[0] == 'print':
669 return u''
670
671 _ = self.request.getText
672 script = u"""
673 <script type=\"text/javascript\">
674 <!--// common functions
675
676 // We keep here the state of the search box
677 searchIsDisabled = false;
678
679 function searchChange(e) {
680 // Update search buttons status according to search box content.
681 // Ignore empty or whitespace search term.
682 var value = e.value.replace(/\s+/, '');
683 if (value == '' || searchIsDisabled) {
684 searchSetDisabled(true);
685 } else {
686 searchSetDisabled(false);
687 }
688 }
689
690 function searchSetDisabled(flag) {
691 // Enable or disable search
692 document.getElementById('fullsearch').disabled = flag;
693 document.getElementById('titlesearch').disabled = flag;
694 }
695
696 function searchFocus(e) {
697 // Update search input content on focus
698 if (e.value == '%(search_hint)s') {
699 e.value = '';
700 e.className = '';
701 searchIsDisabled = false;
702 }
703 }
704
705 function searchBlur(e) {
706 // Update search input content on blur
707 if (e.value == '') {
708 e.value = '%(search_hint)s';
709 e.className = 'disabled';
710 searchIsDisabled = true;
711 }
712 }
713
714 function actionsMenuInit(title) {
715 // Initialize action menu
716 for (i = 0; i < document.forms.length; i++) {
717 var form = document.forms[i];
718 if (form.className == 'actionsmenu') {
719 // Check if this form needs update
720 var div = form.getElementsByTagName('div')[0];
721 var label = div.getElementsByTagName('label')[0];
722 if (label) {
723 // This is the first time: remove label and do buton.
724 div.removeChild(label);
725 var dobutton = div.getElementsByTagName('input')[0];
726 div.removeChild(dobutton);
727 // and add menu title
728 var select = div.getElementsByTagName('select')[0];
729 var item = document.createElement('option');
730 item.appendChild(document.createTextNode(title));
731 item.value = 'show';
732 select.insertBefore(item, select.options[0]);
733 select.selectedIndex = 0;
734 }
735 }
736 }
737 }
738 //-->
739 </script>
740 """ % {
741 'search_hint' : _('Search:', formatted=False).replace(':',''), # XXX TODO make own i18n string in 1.4
742 }
743 return script
744
745 def shouldUseRSS(self):
746 """ Return True if rss feature is available, or False
747
748 Currently rss is broken on plain Python, and works only when
749 installing PyXML. Return true if PyXML is installed.
750 """
751 # Stolen from wikitest.py
752 try:
753 import xml
754 return xml.__file__.find('_xmlplus') != -1
755 except ImportError:
756 # This error reported on Python 2.2
757 return False
758
759 def rsshref(self):
760 """ Create rss href, used for rss button and head link
761
762 @rtype: unicode
763 @return: html head
764 """
765 href = u'%s/RecentChanges?action=rss_rc&amp;ddiffs=1&amp;unique=1' % \
766 self.request.getScriptname()
767 return href
768
769 def rsslink(self):
770 """ Create rss link in head, used by FireFox
771
772 RSS link for FireFox. This shows an rss link in the bottom of
773 the page and let you subscribe to the wiki rss feed.
774
775 @rtype: unicode
776 @return: html head
777 """
778 if self.shouldUseRSS():
779 link = [
780 u'<link rel="alternate"',
781 u' title="%s Recent Changes"' % self.cfg.sitename,
782 u' href="%s"' % self.rsshref(),
783 u' type="application/rss+xml">',
784 ]
785 return ''.join(link)
786 return ''
787
788 def html_head(self, d):
789 """ Assemble html head
790
791 @param d: parameter dictionary
792 @rtype: unicode
793 @return: html head
794 """
795 html = [
796 u'<title>%(title)s - %(sitename)s</title>' % d,
797 self.headscript(d), # Should move to separate .js file
798 self.html_stylesheets(d),
799 self.rsslink(),
800 ]
801 return '\n'.join(html)
802
803 def credits(self, d, **keywords):
804 """ Create credits html from credits list """
805 if isinstance(self.cfg.page_credits, (list, tuple)):
806 items = ['<li>%s</li>' % i for i in self.cfg.page_credits]
807 html = '<ul id="credits">\n%s\n</ul>\n' % '\n'.join(items)
808 else:
809 # Old config using string, output as is
810 html = self.cfg.page_credits
811 return html
812
813 def shouldShowEditbar(self, page):
814 """ Should we show the editbar?
815
816 Actions should implement this, because only the action knows if
817 the edit bar makes sense. Until it goes into actions, we do the
818 checking here.
819
820 @param page: current page
821 @rtype: bool
822 @return: true if editbar should show
823 """
824 # Show editbar only for existing pages, including deleted pages,
825 # that the user may read. If you may not read, you can't edit,
826 # so you don't need editbar.
827 if (page.exists(includeDeleted=1) and
828 self.request.user.may.read(page.page_name)):
829 form = self.request.form
830 action = form.get('action', [''])[0]
831 # Do not show editbar on edit but on save/cancel
832 return not (action == 'edit' and
833 not form.has_key('button_save') and
834 not form.has_key('button_cancel'))
835 return False
836
837 def quicklinkLink(self, page):
838 """ Return add/remove quicklink link to valid users
839
840 @rtype: unicode
841 @return: quicklink / quickunlink link
842 """
843 _ = self.request.getText
844 user = self.request.user
845 if user.valid:
846 # user valid, get current page status
847 if user.isQuickLinkedTo([page.page_name]):
848 title = _("Remove from Quicklinks")
849 else:
850 title = _("Add to Quicklinks")
851 quotedname = wikiutil.quoteWikinameURL(page.page_name)
852 link = wikiutil.link_tag(self.request, quotedname +
853 '?action=quicklink', title)
854 return link
855 return ''
856
857 def subscribeLink(self, page):
858 """ Return subscribe/unsubscribe link to valid users
859
860 @rtype: unicode
861 @return: subscribe or unsubscribe link
862 """
863 _ = self.request.getText
864 user = self.request.user
865 if self.cfg.mail_smarthost and user.valid:
866 # Email enabled and user valid, get current page status
867 if user.isSubscribedTo([page.page_name]):
868 title = _("Unsubscribe")
869 else:
870 title = _("Subscribe")
871 quotedname = wikiutil.quoteWikinameURL(page.page_name)
872 link = wikiutil.link_tag(self.request, quotedname +
873 '?action=subscribe', title)
874 return link
875 return ''
876
877 def actionsMenu(self, page):
878 """ Create actions menu list and items data dict
879
880 The menu will contain the same items always, but items that are not
881 available will be disabled (some broken browsers will let you select
882 disabled options though).
883
884 The menu should give best user experience for javascript enabled
885 browsers, and acceptable behavior for those who prefer not to
886 use Javascript.
887
888 TODO: Move actionsMenuInit() into body onload. This require that
889 the theme will render body, its currently done on wikiutil/page.
890
891 @param page: current page, Page object
892 @rtype: unicode
893 @return: actions menu html fragment
894 """
895 request = self.request
896 _ = request.getText
897
898 menu = [
899 'raw',
900 'print',
901 'refresh',
902 '__separator__',
903 'AttachFile',
904 'SpellCheck',
905 'LikePages',
906 'LocalSiteMap',
907 '__separator__',
908 'RenamePage',
909 'DeletePage',
910 ]
911
912 titles = {
913 # action: menu title
914 '__title__': _("More Actions:", formatted=False),
915 '__separator__': '--------', # spacer
916 'raw': _('Show Raw Text', formatted=False),
917 'print': _('Show Print View', formatted=False),
918 'refresh': _('Delete Cache', formatted=False),
919 'AttachFile': _('Attach File', formatted=False),
920 'SpellCheck': _('Check Spelling', formatted=False), # rename action!
921 'RenamePage': _('Rename Page', formatted=False),
922 'DeletePage': _('Delete Page', formatted=False),
923 'LikePages': _('Show Like Pages', formatted=False),
924 'LocalSiteMap': _('Show Local Site Map', formatted=False),
925 }
926
927 options = []
928 option = '<option value="%(action)s"%(disabled)s>%(title)s</option>'
929 # class="disabled" is a workaround for browsers that ignore
930 # "disabled", e.g IE, Safari
931 # for XHTML: data['disabled'] = ' disabled="disabled"'
932 disabled = ' disabled class="disabled"'
933
934 # Format standard actions
935 available = request.getAvailableActions(page)
936 for action in menu:
937 data = {'action': action, 'disabled': '', 'title': titles[action]}
938
939 # Enable delete cache only if page can use caching
940 if action == 'refresh':
941 if not page.canUseCache():
942 data['action'] = 'show'
943 data['disabled'] = disabled
944
945 # Special menu items. Without javascript, executing will
946 # just return to the page.
947 elif action.startswith('__'):
948 data['action'] = 'show'
949
950 # Actions which are not available for this wiki, user or page
951 if (action == '__separator__' or
952 (action[0].isupper() and not action in available)):
953 data['disabled'] = disabled
954
955 options.append(option % data)
956
957 # Add custom actions not in the standard menu
958 more = [item for item in available if not item in titles]
959 more.sort()
960 if more:
961 # Add separator
962 separator = option % {'action': 'show', 'disabled': disabled,
963 'title': titles['__separator__']}
964 options.append(separator)
965 # Add more actions (all enabled)
966 for action in more:
967 data = {'action': action, 'disabled': ''}
968 # Always add spaces: AttachFile -> Attach File
969 # XXX TODO do not create page just for using split_title
970 title = Page(request, action).split_title(request, force=1)
971 # Use translated version if available
972 data['title'] = _(title, formatted=False)
973 options.append(option % data)
974
975 data = {
976 'label': titles['__title__'],
977 'options': '\n'.join(options),
978 'do_button': _("Do")
979 }
980
981 html = '''
982 <form class="actionsmenu" method="get" action="">
983 <div>
984 <label>%(label)s</label>
985 <select name="action"
986 onchange="if ((this.selectedIndex != 0) &&
987 (this.options[this.selectedIndex].disabled == false)) {
988 this.form.submit();
989 }
990 this.selectedIndex = 0;">
991 %(options)s
992 </select>
993 <input type="submit" value="%(do_button)s">
994 </div>
995 <script type="text/javascript">
996 <!--// Init menu
997 actionsMenuInit('%(label)s');
998 //-->
999 </script>
1000 </form>
1001 ''' % data
1002
1003 return html
1004
1005 def editbar(self, d):
1006 """ Assemble the page edit bar.
1007
1008 Display on existing page. Replace iconbar, showtext, edit text,
1009 refresh cache and available actions.
1010
1011 @param d: parameter dictionary
1012 @rtype: unicode
1013 @return: iconbar html
1014 """
1015 page = d['page']
1016 if not self.shouldShowEditbar(page):
1017 return ''
1018
1019 # Use cached editbar if possible.
1020 cacheKey = 'editbar'
1021 cached = self._cache.get(cacheKey)
1022 if cached:
1023 return cached
1024
1025 # Make new edit bar
1026 request = self.request
1027 _ = self.request.getText
1028 link = wikiutil.link_tag
1029 quotedname = wikiutil.quoteWikinameURL(page.page_name)
1030 links = []
1031 add = links.append
1032
1033 # Parent page
1034 parent = page.getParentPage()
1035 if parent:
1036 add(parent.link_to(request, _("Show Parent", formatted=False)))
1037
1038 # Page actions
1039 if page.isWritable() and request.user.may.write(page.page_name):
1040 editor = request.user.editor_ui
1041 if editor == '<default>':
1042 editor = request.cfg.editor_ui
1043 if editor == 'freechoice':
1044 add(link(request, '%s?action=edit&editor=%s' % (quotedname, 'text'), _('Edit (Text)')))
1045 add(link(request, '%s?action=edit&editor=%s' % (quotedname, 'gui'), _('Edit (GUI)')))
1046 else: # editor == 'theonepreferred'
1047 add(link(request, '%s?action=edit' % (quotedname, ), _('Edit')))
1048 # we dont need to specify editor as edit action will choose the one from userprefs by default
1049 else:
1050 add(_('Immutable Page', formatted=False))
1051
1052 add(link(request, quotedname + '?action=diff',
1053 _('Show Changes', formatted=False)))
1054 add(link(request, quotedname + '?action=info',
1055 _('Get Info', formatted=False)))
1056 add(self.subscribeLink(page))
1057 add(self.quicklinkLink(page))
1058 add(self.actionsMenu(page))
1059
1060 # Format
1061 items = '\n'.join(['<li>%s</li>' % item for item in links if item != ''])
1062 html = u'<ul class="editbar">\n%s\n</ul>\n' % items
1063
1064 # cache for next call
1065 self._cache[cacheKey] = html
1066 return html
1067
1068 def startPage(self):
1069 """ Start page div with page language and direction
1070
1071 @rtype: unicode
1072 @return: page div with language and direction attribtues
1073 """
1074 return u'<div id="page"%s>\n' % self.content_lang_attr()
1075
1076 def endPage(self):
1077 """ End page div
1078
1079 Add an empty page bottom div to prevent floating elements to
1080 float out of the page bottom over the footer.
1081 """
1082 return '<div id="pagebottom"></div>\n</div>\n'
1083
1084 # Public functions #####################################################
1085
1086 def header(self, d, **kw):
1087 """ Assemble page header
1088
1089 Default behavior is to start a page div. Sub class and add
1090 footer items.
1091
1092 @param d: parameter dictionary
1093 @rtype: string
1094 @return: page header html
1095 """
1096 return self.startPage()
1097
1098 editorheader = header
1099
1100 def footer(self, d, **keywords):
1101 """ Assemble page footer
1102
1103 Default behavior is to end page div. Sub class and add
1104 footer items.
1105
1106 @param d: parameter dictionary
1107 @keyword ...:...
1108 @rtype: string
1109 @return: page footer html
1110 """
1111 return self.endPage()
1112
1113 # RecentChanges ######################################################
1114
1115 def recentchanges_entry(self, d):
1116 """
1117 Assemble a single recentchanges entry (table row)
1118
1119 @param d: parameter dictionary
1120 @rtype: string
1121 @return: recentchanges entry html
1122 """
1123 _ = self.request.getText
1124 html = []
1125 html.append('<tr>\n')
1126
1127 html.append('<td class="rcicon1">%(icon_html)s</td>\n' % d)
1128
1129 html.append('<td class="rcpagelink">%(pagelink_html)s</td>\n' % d)
1130
1131 html.append('<td class="rctime">')
1132 if d['time_html']:
1133 html.append("%(time_html)s" % d)
1134 html.append('</td>\n')
1135
1136 html.append('<td class="rcicon2">%(info_html)s</td>\n' % d)
1137
1138 html.append('<td class="rceditor">')
1139 if d['editors']:
1140 html.append('<br>'.join(d['editors']))
1141 html.append('</td>\n')
1142
1143 html.append('<td class="rccomment">')
1144 if d['comments']:
1145 if d['changecount'] > 1:
1146 notfirst = 0
1147 for comment in d['comments']:
1148 html.append('%s<tt>#%02d</tt>&nbsp;%s' % (
1149 notfirst and '<br>' or '' , comment[0], comment[1]))
1150 notfirst = 1
1151 else:
1152 comment = d['comments'][0]
1153 html.append('%s' % comment[1])
1154 html.append('</td>\n')
1155
1156 html.append('</tr>\n')
1157
1158 return ''.join(html)
1159
1160 def recentchanges_daybreak(self, d):
1161 """
1162 Assemble a rc daybreak indication (table row)
1163
1164 @param d: parameter dictionary
1165 @rtype: string
1166 @return: recentchanges daybreak html
1167 """
1168 if d['bookmark_link_html']:
1169 set_bm = '&nbsp; %(bookmark_link_html)s' % d
1170 else:
1171 set_bm = ''
1172 return ('<tr class="rcdaybreak"><td colspan="%d">'
1173 '<strong>%s</strong>'
1174 '%s'
1175 '</td></tr>\n') % (6, d['date'], set_bm)
1176
1177 def recentchanges_header(self, d):
1178 """
1179 Assemble the recentchanges header (intro + open table)
1180
1181 @param d: parameter dictionary
1182 @rtype: string
1183 @return: recentchanges header html
1184 """
1185 _ = self.request.getText
1186
1187 # Should use user interface language and direction
1188 html = '<div class="recentchanges"%s>\n' % self.ui_lang_attr()
1189 html += '<div>\n'
1190 if self.shouldUseRSS():
1191 link = [
1192 u'<div class="rcrss">',
1193 u'<a href="%s">' % self.rsshref(),
1194 self.make_icon("rss"),
1195 u'</a>',
1196 u'</div>',
1197 ]
1198 html += ''.join(link)
1199 html += '<p>'
1200 if d['rc_update_bookmark']:
1201 html += "%(rc_update_bookmark)s %(rc_curr_bookmark)s<br>" % d
1202
1203 # Add day selector
1204 if d['rc_days']:
1205 days = []
1206 for day in d['rc_days']:
1207 if day == d['rc_max_days']:
1208 days.append('<strong>%d</strong>' % day)
1209 else:
1210 days.append(
1211 wikiutil.link_tag(self.request,
1212 '%s?max_days=%d' % (d['q_page_name'], day),
1213 str(day)))
1214 days = ' | '.join(days)
1215 html += (_("Show %s days.") % (days,))
1216 html += '</p>\n</div>\n'
1217
1218 html += '<table>\n'
1219 return html
1220
1221 def recentchanges_footer(self, d):
1222 """
1223 Assemble the recentchanges footer (close table)
1224
1225 @param d: parameter dictionary
1226 @rtype: string
1227 @return: recentchanges footer html
1228 """
1229 _ = self.request.getText
1230 html = ''
1231 html += '</table>\n'
1232 if d['rc_msg']:
1233 html += "<br>%(rc_msg)s\n" % d
1234 html += '</div>\n'
1235 return html
1236
1237 # Language stuff ####################################################
1238
1239 def ui_lang_attr(self):
1240 """Generate language attributes for user interface elements
1241
1242 User interface elements use the user language (if any), kept in
1243 request.lang.
1244
1245 @rtype: string
1246 @return: lang and dir html attributes
1247 """
1248 lang = self.request.lang
1249 return ' lang="%s" dir="%s"' % (lang, i18n.getDirection(lang))
1250
1251 def content_lang_attr(self):
1252 """Generate language attributes for wiki page content
1253
1254 Page content uses the page language or the wiki default language.
1255
1256 @rtype: string
1257 @return: lang and dir html attributes
1258 """
1259 lang = self.request.content_lang
1260 return ' lang="%s" dir="%s"' % (lang, i18n.getDirection(lang))
1261