ATLAS Offline Software
Loading...
Searching...
No Matches
WebPage.py
Go to the documentation of this file.
1# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
2
3"""
4This module provides a simple framework for generating CSS-based dynamic web
5pages with a typical layout consisting of a header, navigation bar, content area
6and a footer. It is compatible with different web servers using e.g. CGI, mod_python,
7or CherryPy.
8"""
9__author__ = 'Juerg Beringer'
10__version__ = 'WebPage.py atlas/athena'
11
12
13import time
14from cgi import escape
15
16
17#
18# HTML templates
19#
20startPage = """\
21%(contentType)s<?xml version="1.0" encoding="UTF-8"?>
22<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
23<html xmlns="http://www.w3.org/1999/xhtml">
24<head>
25<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
26<title>%(pageTitle)s</title>
27<link href="%(cssName)s" rel="stylesheet" type="text/css" />
28%(pageHeaderSnippets)s\
29</head>
30<body>
31"""
32
33endPage = """\
34</body>
35</html>\
36"""
37
38
39#
40# Utilities for generating HTML snippets
41#
42def sep(s):
43 """Add separator to string s unless s is empty."""
44 return ' '+s if s else s
45
46def htmlDiv(id, contents='', attr='', keepEmptyDiv=True):
47 """Make a named div element containing contents. If
48 contents is empty, an empty string is returned,
49 unless keepEmtpyDiv is set True."""
50 if contents or keepEmptyDiv:
51 if id:
52 return '<div id="%s"%s>\n%s</div>\n' % (id,sep(attr),contents)
53 else:
54 return '<div%s>\n%s</div>\n' % (sep(attr),contents)
55 else:
56 return ''
57
58def htmlText(text, attr='', escapeText=False):
59 """Make a text consisting of an unnamed div. Special HTML characters
60 in the text are properly replaced (using escape from cgi) if
61 escapeText is set True."""
62 return '<div%s>%s</div>\n' % (sep(attr),escape(text) if escapeText else text)
63
64def htmlPre(text, attr='', escapeText=False):
65 """Make a preformatted text section. Special HTML characters
66 in the text are properly replaced (using escape from cgi) if
67 escapeText is set True."""
68 return '<pre%s>\n%s\n</pre>\n' % (sep(attr),escape(text) if escapeText else text)
69
70def htmlPara(text='', attr='', escapeText=False):
71 """Make a paragraph."""
72 if text or attr:
73 return '<p%s>%s</p>\n' % (sep(attr), escape(text) if escapeText else text)
74 else:
75 return '<p>\n'
76
77def htmlLink(text, link, attr='', escapeText=False):
78 return '<a href="%s"%s>%s</a>' % (link,sep(attr),escape(text) if escapeText else text)
79
80def htmlList(contents, attr='', listType='ul'):
81 """Enclose list contents (a string with one or more list items) with
82 the proper list tag. The type of the list is given by listType."""
83 return '<%s%s>\n%s</%s>\n' % (listType,sep(attr),contents,listType)
84
85def htmlLI(text, attr='', escapeText=False):
86 """Make a list item. Special HTML characters
87 in the text are properly replaced (using escape from cgi) if
88 escapeText is set True."""
89 return '<li%s>%s</li>\n' % (sep(attr), escape(text) if escapeText else text)
90
92 """Make a table. Table row data is accumulated internally in a list.
93 The table can be made sortable (using jQuery's plugin Tablesorter)
94 by setting useTableSorter to True. Special HTML characters in cell
95 data are properly replaced (using escape from cgi) if escapeText
96 is set True.
97 """
98 def __init__(self, tableAttr='', defaultCellAttr = [], useTableSorter=False, escapeText=False):
99 self.tableAttr = tableAttr
100 self.defaultCellAttr = defaultCellAttr
101 self.useTableSorter = useTableSorter
102 self.escapeText = escapeText
103 self.rows = [] # list of HTML strings with complete rows of the table
104
105 def __str__(self):
106 return self.getHtml()
107
108 def appendRow(self, cellData, rowAttr=''):
109 """Append a row to the table. cellData is a list of the contents of
110 the cells in the row. The data for each cell is either a string
111 with the contents of the cell, or a tuple where the first element
112 is the contents of the cell and the second element contains any
113 HTML tags. Special HTML characters are properly replaced (using
114 escape from cgi) if escapeText was set True when creating the
115 table. If the table uses Tablesorter, <th> is used instead of
116 <td> in the first row."""
117 r = '<tr%s>\n' % sep(rowAttr)
118 if self.useTableSorter and len(self.rows)==0:
119 # First row using Tablesorter - must use <th> instead of <td>
120 cellFormat = '<th%s>%s</th>\n'
121 else:
122 cellFormat = '<td%s>%s</td>\n'
123 iCell = 0
124 for c in cellData:
125 cellAttr = self.defaultCellAttr[iCell] if iCell<len(self.defaultCellAttr) else ''
126 if isinstance(c,tuple):
127 r += cellFormat % (sep(c[1]),escape(str(c[0])) if self.escapeText else str(c[0]))
128 else:
129 r += cellFormat % (sep(cellAttr),escape(str(c)) if self.escapeText else str(c))
130 iCell += 1
131 r += '</tr>'
132 self.rows.append(r)
133
134 def getHtml(self):
135 """Return the HTML code for the table."""
136 if len(self.rows)<1:
137 return ''
138 if self.useTableSorter:
139 if self.tableAttr:
140 h = '<table %s>\n' % self.tableAttr
141 else:
142 h = '<table class="tablesorter">\n'
143 h += '<thead>\n'
144 h += self.rows[0]
145 h += '\n</thead><tbody>\n'
146 h += '\n'.join(self.rows[1:])
147 h += '\n</tbody></table>\n'
148 else:
149 h = '<table%s>\n' % sep(self.tableAttr)
150 h += '\n'.join(self.rows)
151 h += '\n</table>\n'
152 return h
153
154def htmlFoldingSection(header, content, isClosed=True,
155 headerClass='section-header',
156 contentClass='section-content'):
157 """Generate the html for a folding section using the toggleSection JavaScript utility
158 from WebPageUtils.js and CSS classes section-closed, section-open, and hidden."""
159 if isClosed:
160 s = '<div class="section-closed" onclick="toggleSection(this);">'
161 else:
162 s = '<div class="section-open" onclick="toggleSection(this);">'
163 s += '<span class="%s">%s</span></div>\n' % (headerClass,header)
164 h = ' hidden' if isClosed else ''
165 s += '<div class="%s%s">\n%s</div>\n' % (contentClass,h,content)
166 return s
167
168def htmlForm(contents, action='', method='post', attr=''):
169 snippet = '<form action="%s" method="%s"%s>\n' % (action,method,sep(attr))
170 snippet += '<fieldset>\n'
171 snippet += contents
172 snippet += '</fieldset>\n</form>\n'
173 return snippet
174
175def htmlLabel(labelText, parName, attr=''):
176 """Make a label for parName. If labelText is None,
177 an empty string is returned."""
178 if labelText!=None:
179 return '<label for="%s"%s>%s</label>' % (parName,sep(attr),labelText)
180 else:
181 return ''
182
183def htmlSelect(labelText, parName, args, choiceList, hint=None, descriptionSeparator='::',
184 labelAttr='', attr=''):
185 """Make a select statement (including label with text)."""
186 snippet = htmlLabel(labelText,parName,labelAttr)
187 default = args[parName] if parName in args else ''
188 if not isinstance(default,list):
189 default = [default]
190 snippet += '<select name="%s"%s>\n' % (parName,sep(attr))
191 if hint:
192 snippet += '<option value="">%s</option>\n' % hint
193 for c in choiceList:
194 p = c.split(descriptionSeparator)
195 if len(p)==2:
196 (desc,val) = p
197 else:
198 (desc,val) = (c,c)
199 if val in default:
200 snippet += '<option selected="yes" value="%s">%s</option>\n' % (val,desc)
201 else:
202 snippet += '<option value="%s">%s</option>\n' % (val,desc)
203 snippet += '</select>\n'
204 return snippet
205
206def htmlCheckbox(labelText, parName, args, labelAttr='', attr=''):
207 """Make a checkbox (including label with text)."""
208 snippet = htmlLabel(labelText,parName,labelAttr)
209 checked = 'checked="checked"' if parName in args else ''
210 snippet += '<input type="checkbox" name="%s"%s%s/>\n' % (parName,sep(checked),sep(attr))
211 return snippet
212
213def htmlTextInput(labelText, parName, args, size=None, maxLength = None, labelAttr='', attr=''):
214 """Make a text input area (including label with text). Special HTML
215 characters in any default text are properly replaced."""
216 snippet = htmlLabel(labelText,parName,labelAttr)
217 snippet += '<input type="text" name="%s"' % parName
218 if parName in args:
219 snippet += ' value="%s"' % escape(args[parName],True)
220 if size!=None:
221 snippet += ' size="%s"' % size
222 if maxLength!=None:
223 snippet += ' maxlength="%s"' % maxLength
224 snippet += '%s/>\n' % sep(attr)
225 return snippet
226
227def htmlSubmit(text, parName, attr='', onlyOnce=False):
228 """Make a submit button. If onlyOnce is true, the button can only
229 be clicked once in order to prevent multiple clicking of the
230 submit button."""
231 if onlyOnce:
232 # FIXME: this doesn't work yet - it disables submission
233 s = '<input type="button" name="%s" value="%s"' % (parName,text)
234 s += ' onclick="this.form.submit()"'
235 s += '%s />\n' % sep(attr)
236 return s
237 else:
238 return '<input type="submit" name="%s" value="%s"%s />\n' % (parName,text,sep(attr))
239
240
241#
242# Utility classes
243#
245 def __init__(self,*args):
246 Exception.__init__(self,args)
247
248
250 """Class to store global configuration data for a tree of web pages. Data members
251 are used to store information used by the framework, while the dict is
252 intended to be used for application-specific configuration data."""
253
254 def __init__(self, **args):
255 self.baseUrl = 'webapp' # Common base path
256 self.pageList = [] # List of all web pages
257 self.pageAttributes = {} # Optional attributes for web pages, such as specific link styles
258
259 # Initialize application-specific configuration data, if any
260 for k in args:
261 self[k] = args[k]
262
263
264#
265# Base class for all web pages
266#
268 """Base class for creating CSS-based dynamic web pages."""
269
270 def __init__(self, **args):
271 """Constructor. You may override any of the default values in self.pageConfig by
272 passing the corresponding value in a named variable."""
273 self.pageConfig = { 'contentType': '', # For plain CGI need: 'Content-type: text/html\n\n'
274 'pageName': '',
275 'pageTitle': '',
276 'pageHeaderSnippets': '',
277 'cssName': 'default.css',
278 'css_currentLink': 'acurrentlink',
279 'header': '',
280 'navigation': '',
281 'content': '',
282 'footer': '',
283 'keepEmptyHeader': False, # Set to true to anyway generate div if empty
284 'keepEmptyNavigation': False,
285 'keepEmptyContent': False,
286 'keepEmptyFooter': False
287 }
288 for k in args:
289 self.pageConfig[k] = args[k]
291 self.isConfigured = False
292 pass
293
294 def pageName(self):
295 """Short cut to retrieve the name (the last element in the URL)
296 of the current page. This works only if the page has been
297 added into the page tree using addPage."""
298 return self.pageConfig['pageName']
299
300 def addPage(self, name, page, **attrs):
301 """Add a new web page to the page tree. All pages added via addPage
302 share the same GlobalConfiguration object."""
303 page.globalConfig = self.globalConfig
304 page.pageConfig['pageName'] = name
305 self.globalConfig.pageList.append(name)
306 self.globalConfig.pageAttributes[name] = dict(attrs)
307 setattr(self,name,page) # Link page into page tree (for CherryPy)
308
309 def addLink(self, name, alias, **attrs):
310 """Add a new link to the page tree. Links share page objects with
311 other pages through aliasing. This allows using different links
312 with different queries to the same page as if they were separate
313 pages. For links, highlighting of the current page is disabled
314 (if enabled, all links leading to the same page would be
315 highlighted)."""
316 self.globalConfig.pageList.append(name)
317 self.globalConfig.pageAttributes[name] = dict(attrs)
318 self.globalConfig.pageAttributes[name]['alias'] = alias
319
320 def addToPageHeader(self, snippet):
321 """Add a snippet of code to the page header. Use this e.g. to include JavaScript libraries."""
322 self.pageConfig['pageHeaderSnippets'] += snippet
323
324 def index(self, **args):
325 """Return the complete page."""
326 if not self.isConfigured:
327 self.configure()
328 s = self.override()
329 if not s:
330 self.pageConfig['timeStamp'] = time.strftime('%a %b %d %X %Z %Y')
331 contents = self.content(**args) # Make sure contents is run first (so it
332 # can change any pageConfig entries if desired
333 s = startPage % self.pageConfig
334 s = s + htmlDiv('header', self.header(**args),
335 keepEmptyDiv=self.pageConfig['keepEmptyHeader'])
336 s = s + htmlDiv('navigation', self.navigation(**args),
337 keepEmptyDiv=self.pageConfig['keepEmptyNavigation'])
338 s = s + htmlDiv('content', contents,
339 keepEmptyDiv=self.pageConfig['keepEmptyContent'])
340 s = s + htmlDiv('footer', self.footer(**args),
341 keepEmptyDiv=self.pageConfig['keepEmptyFooter'])
342 s = s + endPage
343 return s
344 # For CherryPy
345 index.exposed = True
346
347 def configure(self):
348 """Final configuration of web application after all data is initialized."""
349 self.configureLinks()
350 self.isConfigured = True
351 return
352
353 def configureLinks(self):
354 """Based on configuration data, for each web page create two link entries in pageConfig:
355 The first entry has the form url_PAGENAME and contains the complete URL to link to
356 a page. The second entry is named href_PAGENAME and contains style information and a
357 href= prefix with proper quotation marks in addition to the URL. These link entries
358 should be used to generate links in HTML pages with snippets like <a %(href_PAGENAME)s>
359 or <a href="%(url_PAGENAME)>. The link entries can only be generated once all
360 configuration data is available, ie configureLinks must be called from configure
361 and not from __init__."""
362 for p in self.globalConfig.pageList:
363 page = self.globalConfig.pageAttributes[p].get('alias',p)
364 query = self.globalConfig.pageAttributes[p].get('query','')
365 style = self.globalConfig.pageAttributes[p].get('style',None)
366 self.pageConfig['url_'+p] = '%s/%s/%s' % (self.globalConfig.baseUrl,page,query)
367 if p==self.pageName():
368 self.pageConfig['href_'+p] = 'href="%s/%s/%s" class="%s"' % (self.globalConfig.baseUrl,page,query,self.pageConfig['css_currentLink'])
369 else:
370 if style:
371 self.pageConfig['href_'+p] = 'href="%s/%s/%s" class="%s"' % (self.globalConfig.baseUrl,page,query,style)
372 else:
373 self.pageConfig['href_'+p] = 'href="%s/%s/%s"' % (self.globalConfig.baseUrl,page,query)
374
375 def override(self):
376 """Override provides a hook where code to generate or redirect to an alternative
377 page can be placed by derived classes. If not override of the normal page is
378 desired, override should return None. Otherwise it should either raise an
379 appropriate exception or return a string containing the complete alternate page
380 to display."""
381 return None
382
383 def header(self, **args):
384 """Generate the page header. Default value comes from self.pageConfig['header']."""
385 return self.pageConfig['header'] % self.pageConfig
386
387 def navigation(self, **args):
388 """Generate the navigation bar. Default value comes from self.pageConfig['navigation']."""
389 return self.pageConfig['navigation'] % self.pageConfig
390
391 def content(self, **args):
392 """Generate the page content. Default value comes from self.pageConfig['content']."""
393 return self.pageConfig['content'] % self.pageConfig
394
395 def footer(self, **args):
396 """Generate the footer. Default value comes from self.pageConfig['footer']."""
397 return self.pageConfig['footer'] % self.pageConfig
398
399
400# Example configuration file on how to run CherryPy with Apache mod_python:
401#
402# <Location "/">
403# PythonPath "sys.path+['/whatever']"
404# SetHandler python-program
405# PythonHandler cherrypy._cpmodpy::handler
406# PythonOption cherrypy.setup WebPage::setup_server
407# PythonDebug On
408# </Location>
409#
410#import cherrypy
411#def setup_server():
412# cherrypy.config.update({'environment': 'production',
413# 'log.screen': False,
414# 'server.socket_host': '127.0.0.1',
415# 'log.error_file': '/tmp/site.log',
416# 'show_tracebacks': False})
417# cherrypy.tree.mount(WebPage())
418
419
420# Test code
421if __name__ == '__main__':
422
424 def content(self):
425 return "Hello, world!"
426
427 # For plain CGI
428 #import cgi
429 #import cgitb; cgitb.enable()
430 #p = HelloWorld(pageTitle = 'HelloWorld Test', contentType='Content-type: text/html\n\n')
431 #print p.index()
432
433 # For local CherryPy (run as `python WebPage.py')
434 # NOTE: see http://www.cherrypy.org/wiki/StaticContent on how to
435 # serve static content such as the css file
436 import cherrypy
437 cherrypy.quickstart(HelloWorld(pageTitle='HelloWorld Test',contentType=''))
configureLinks(self)
Definition WebPage.py:353
__init__(self, **args)
Definition WebPage.py:270
navigation(self, **args)
Definition WebPage.py:387
footer(self, **args)
Definition WebPage.py:395
content(self, **args)
Definition WebPage.py:391
addToPageHeader(self, snippet)
Definition WebPage.py:320
header(self, **args)
Definition WebPage.py:383
bool isConfigured
Definition WebPage.py:291
addLink(self, name, alias, **attrs)
Definition WebPage.py:309
configure(self)
Definition WebPage.py:347
addPage(self, name, page, **attrs)
Definition WebPage.py:300
appendRow(self, cellData, rowAttr='')
Definition WebPage.py:108
__init__(self, tableAttr='', defaultCellAttr=[], useTableSorter=False, escapeText=False)
Definition WebPage.py:98
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition hcg.cxx:130
htmlForm(contents, action='', method='post', attr='')
Definition WebPage.py:168
htmlLink(text, link, attr='', escapeText=False)
Definition WebPage.py:77
htmlCheckbox(labelText, parName, args, labelAttr='', attr='')
Definition WebPage.py:206
sep(s)
Definition WebPage.py:42
htmlLabel(labelText, parName, attr='')
Definition WebPage.py:175
htmlTextInput(labelText, parName, args, size=None, maxLength=None, labelAttr='', attr='')
Definition WebPage.py:213
htmlText(text, attr='', escapeText=False)
Definition WebPage.py:58
htmlLI(text, attr='', escapeText=False)
Definition WebPage.py:85
htmlList(contents, attr='', listType='ul')
Definition WebPage.py:80
htmlFoldingSection(header, content, isClosed=True, headerClass='section-header', contentClass='section-content')
Definition WebPage.py:156
htmlSubmit(text, parName, attr='', onlyOnce=False)
Definition WebPage.py:227
htmlDiv(id, contents='', attr='', keepEmptyDiv=True)
Definition WebPage.py:46
htmlSelect(labelText, parName, args, choiceList, hint=None, descriptionSeparator='::', labelAttr='', attr='')
Definition WebPage.py:184
htmlPara(text='', attr='', escapeText=False)
Definition WebPage.py:70
htmlPre(text, attr='', escapeText=False)
Definition WebPage.py:64
Definition index.py:1