summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: c0026a5)
raw | patch | inline | side by side (parent: c0026a5)
author | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Fri, 30 Aug 2002 08:49:15 +0000 (08:49 +0000) | ||
committer | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Fri, 30 Aug 2002 08:49:15 +0000 (08:49 +0000) |
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1014 57a73879-2fb5-44c3-a270-3262357dd7e2
roundup/cgi_client.py | [deleted file] | patch | blob | history |
roundup/cgitb.py | [deleted file] | patch | blob | history |
roundup/template_funcs.py | [deleted file] | patch | blob | history |
roundup/template_parser.py | [deleted file] | patch | blob | history |
diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py
--- a/roundup/cgi_client.py
+++ /dev/null
@@ -1,2486 +0,0 @@
-#
-# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
-# This module is free software, and you may redistribute it and/or modify
-# under the same terms as Python, so long as this copyright message and
-# disclaimer are retained in their original form.
-#
-# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
-# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
-# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-#
-# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
-# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
-# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
-# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-#
-# $Id: cgi_client.py,v 1.162 2002-08-23 04:42:30 richard Exp $
-
-__doc__ = """
-WWW request handler (also used in the stand-alone server).
-"""
-
-import os, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib
-import binascii, Cookie, time, random
-
-import roundupdb, htmltemplate, date, hyperdb, password
-from roundup.i18n import _
-
-class Unauthorised(ValueError):
- pass
-
-class NotFound(ValueError):
- pass
-
-def initialiseSecurity(security):
- ''' Create some Permissions and Roles on the security object
-
- This function is directly invoked by security.Security.__init__()
- as a part of the Security object instantiation.
- '''
- security.addPermission(name="Web Registration",
- description="User may register through the web")
- p = security.addPermission(name="Web Access",
- description="User may access the web interface")
- security.addPermissionToRole('Admin', p)
-
- # doing Role stuff through the web - make sure Admin can
- p = security.addPermission(name="Web Roles",
- description="User may manipulate user Roles through the web")
- security.addPermissionToRole('Admin', p)
-
-class Client:
- '''
- A note about login
- ------------------
-
- If the user has no login cookie, then they are anonymous. There
- are two levels of anonymous use. If there is no 'anonymous' user, there
- is no login at all and the database is opened in read-only mode. If the
- 'anonymous' user exists, the user is logged in using that user (though
- there is no cookie). This allows them to modify the database, and all
- modifications are attributed to the 'anonymous' user.
-
- Once a user logs in, they are assigned a session. The Client instance
- keeps the nodeid of the session as the "session" attribute.
- '''
-
- def __init__(self, instance, request, env, form=None):
- hyperdb.traceMark()
- self.instance = instance
- self.request = request
- self.env = env
- self.path = env['PATH_INFO']
- self.split_path = self.path.split('/')
- self.instance_path_name = env['INSTANCE_NAME']
- url = self.env['SCRIPT_NAME'] + '/'
- machine = self.env['SERVER_NAME']
- port = self.env['SERVER_PORT']
- if port != '80': machine = machine + ':' + port
- self.base = urlparse.urlunparse(('http', env['HTTP_HOST'], url,
- None, None, None))
-
- if form is None:
- self.form = cgi.FieldStorage(environ=env)
- else:
- self.form = form
- self.headers_done = 0
- try:
- self.debug = int(env.get("ROUNDUP_DEBUG", 0))
- except ValueError:
- # someone gave us a non-int debug level, turn it off
- self.debug = 0
-
- def getuid(self):
- try:
- return self.db.user.lookup(self.user)
- except KeyError:
- if self.user is None:
- # user is not logged in and username 'anonymous' doesn't
- # exist in the database
- err = _('anonymous users have read-only access only')
- else:
- err = _("sanity check: unknown user name `%s'")%self.user
- raise Unauthorised, errmsg
-
- def header(self, headers=None, response=200):
- '''Put up the appropriate header.
- '''
- if headers is None:
- headers = {'Content-Type':'text/html'}
- if not headers.has_key('Content-Type'):
- headers['Content-Type'] = 'text/html'
- self.request.send_response(response)
- for entry in headers.items():
- self.request.send_header(*entry)
- self.request.end_headers()
- self.headers_done = 1
- if self.debug:
- self.headers_sent = headers
-
- global_javascript = '''
-<script language="javascript">
-submitted = false;
-function submit_once() {
- if (submitted) {
- alert("Your request is being processed.\\nPlease be patient.");
- return 0;
- }
- submitted = true;
- return 1;
-}
-
-function help_window(helpurl, width, height) {
- HelpWin = window.open('%(base)s%(instance_path_name)s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
-}
-
-</script>
-'''
- def make_index_link(self, name):
- '''Turn a configuration entry into a hyperlink...
- '''
- # get the link label and spec
- spec = getattr(self.instance, name+'_INDEX')
-
- d = {}
- d[':sort'] = ','.join(map(urllib.quote, spec['SORT']))
- d[':group'] = ','.join(map(urllib.quote, spec['GROUP']))
- d[':filter'] = ','.join(map(urllib.quote, spec['FILTER']))
- d[':columns'] = ','.join(map(urllib.quote, spec['COLUMNS']))
- d[':pagesize'] = spec.get('PAGESIZE','50')
-
- # snarf the filterspec
- filterspec = spec['FILTERSPEC'].copy()
-
- # now format the filterspec
- for k, l in filterspec.items():
- # fix up the CURRENT USER if needed (handle None too since that's
- # the old flag value)
- if l in (None, 'CURRENT USER'):
- if not self.user:
- continue
- l = [self.db.user.lookup(self.user)]
-
- # add
- d[urllib.quote(k)] = ','.join(map(urllib.quote, l))
-
- # finally, format the URL
- return '<a href="%s?%s">%s</a>'%(spec['CLASS'],
- '&'.join([k+'='+v for k,v in d.items()]), spec['LABEL'])
-
-
- def pagehead(self, title, message=None):
- '''Display the page heading, with information about the tracker and
- links to more information
- '''
-
- # include any important message
- if message is not None:
- message = _('<div class="system-msg">%(message)s</div>')%locals()
- else:
- message = ''
-
- # style sheet (CSS)
- style = open(os.path.join(self.instance.TEMPLATES, 'style.css')).read()
-
- # figure who the user is
- user_name = self.user
- userid = self.db.user.lookup(user_name)
- default_queries = 1
- links = []
- if user_name != 'anonymous':
- try:
- default_queries = self.db.user.get(userid, 'defaultqueries')
- except KeyError:
- pass
-
- # figure all the header links
- if default_queries:
- if hasattr(self.instance, 'HEADER_INDEX_LINKS'):
- for name in self.instance.HEADER_INDEX_LINKS:
- spec = getattr(self.instance, name + '_INDEX')
- # skip if we need to fill in the logged-in user id and
- # we're anonymous
- if (spec['FILTERSPEC'].has_key('assignedto') and
- spec['FILTERSPEC']['assignedto'] in ('CURRENT USER',
- None) and user_name == 'anonymous'):
- continue
- links.append(self.make_index_link(name))
- else:
- # no config spec - hard-code
- links = [
- _('All <a href="issue?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>'),
- _('Unassigned <a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>')
- ]
-
- user_info = _('<a href="login">Login</a>')
- add_links = ''
- if user_name != 'anonymous':
- # add any personal queries to the menu
- try:
- queries = self.db.getclass('query')
- except KeyError:
- # no query class
- queries = self.instance.dbinit.Class(self.db, "query",
- klass=hyperdb.String(), name=hyperdb.String(),
- url=hyperdb.String())
- queries.setkey('name')
- #queries.disableJournalling()
- try:
- qids = self.db.getclass('user').get(userid, 'queries')
- except KeyError, e:
- #self.db.getclass('user').addprop(queries=hyperdb.Multilink('query'))
- qids = []
- for qid in qids:
- links.append('<a href=%s?%s>%s</a>'%(queries.get(qid, 'klass'),
- queries.get(qid, 'url'), queries.get(qid, 'name')))
-
- # if they're logged in, include links to their information,
- # and the ability to add an issue
- user_info = _('''
-<a href="user%(userid)s">My Details</a> | <a href="logout">Logout</a>
-''')%locals()
-
- # figure the "add class" links
- if hasattr(self.instance, 'HEADER_ADD_LINKS'):
- classes = self.instance.HEADER_ADD_LINKS
- else:
- classes = ['issue']
- l = []
- for class_name in classes:
- # make sure the user has permission to add
- if not self.db.security.hasPermission('Edit', userid, class_name):
- continue
- cap_class = class_name.capitalize()
- links.append(_('Add <a href="new%(class_name)s">'
- '%(cap_class)s</a>')%locals())
-
- # if the user can edit everything, include the links
- admin_links = ''
- userid = self.db.user.lookup(user_name)
- if self.db.security.hasPermission('Edit', userid):
- links.append(_('<a href="list_classes">Class List</a>'))
- links.append(_('<a href="user?:sort=username&:group=roles">User List</a>'))
- links.append(_('<a href="newuser">Add User</a>'))
-
- # add the search links
- if hasattr(self.instance, 'HEADER_SEARCH_LINKS'):
- classes = self.instance.HEADER_SEARCH_LINKS
- else:
- classes = ['issue']
- l = []
- for class_name in classes:
- # make sure the user has permission to view
- if not self.db.security.hasPermission('View', userid, class_name):
- continue
- cap_class = class_name.capitalize()
- links.append(_('Search <a href="search%(class_name)s">'
- '%(cap_class)s</a>')%locals())
-
- # now we have all the links, join 'em
- links = '\n | '.join(links)
-
- # include the javascript bit
- global_javascript = self.global_javascript%self.__dict__
-
- # finally, format the header
- self.write(_('''<html><head>
-<title>%(title)s</title>
-<style type="text/css">%(style)s</style>
-</head>
-%(global_javascript)s
-<body bgcolor=#ffffff>
-%(message)s
-<table width=100%% border=0 cellspacing=0 cellpadding=2>
- <tr class="location-bar">
- <td><big><strong>%(title)s</strong></big></td>
- <td align=right valign=bottom>%(user_name)s</td>
- </tr>
- <tr class="location-bar">
- <td align=left>%(links)s</td>
- <td align=right>%(user_info)s</td>
- </tr>
-</table><br>
-''')%locals())
-
- def pagefoot(self):
- if self.debug:
- self.write(_('<hr><small><dl><dt><b>Path</b></dt>'))
- self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
- keys = self.form.keys()
- keys.sort()
- if keys:
- self.write(_('<dt><b>Form entries</b></dt>'))
- for k in self.form.keys():
- v = self.form.getvalue(k, "<empty>")
- if type(v) is type([]):
- # Multiple username fields specified
- v = "|".join(v)
- self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
- keys = self.headers_sent.keys()
- keys.sort()
- self.write(_('<dt><b>Sent these HTTP headers</b></dt>'))
- for k in keys:
- v = self.headers_sent[k]
- self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
- keys = self.env.keys()
- keys.sort()
- self.write(_('<dt><b>CGI environment</b></dt>'))
- for k in keys:
- v = self.env[k]
- self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
- self.write('</dl></small>')
- self.write('</body></html>')
-
- def write(self, content):
- if not self.headers_done:
- self.header()
- self.request.wfile.write(content)
-
- def index_arg(self, arg):
- ''' handle the args to index - they might be a list from the form
- (ie. submitted from a form) or they might be a command-separated
- single string (ie. manually constructed GET args)
- '''
- if self.form.has_key(arg):
- arg = self.form[arg]
- if type(arg) == type([]):
- return [arg.value for arg in arg]
- return arg.value.split(',')
- return []
-
- def index_sort(self):
- # first try query string / simple form
- x = self.index_arg(':sort')
- if x:
- if self.index_arg(':descending'):
- return ['-'+x[0]]
- return x
- # nope - get the specs out of the form
- specs = []
- for colnm in self.db.getclass(self.classname).getprops().keys():
- desc = ''
- try:
- spec = self.form[':%s_ss' % colnm]
- except KeyError:
- continue
- spec = spec.value
- if spec:
- if spec[-1] == '-':
- desc='-'
- spec = spec[0]
- specs.append((int(spec), colnm, desc))
- specs.sort()
- x = []
- for _, colnm, desc in specs:
- x.append('%s%s' % (desc, colnm))
- return x
-
- def index_filterspec(self, filter, classname=None):
- ''' pull the index filter spec from the form
-
- Links and multilinks want to be lists - the rest are straight
- strings.
- '''
- if classname is None:
- classname = self.classname
- klass = self.db.getclass(classname)
- filterspec = {}
- props = klass.getprops()
- for colnm in filter:
- widget = ':%s_fs' % colnm
- try:
- val = self.form[widget]
- except KeyError:
- try:
- val = self.form[colnm]
- except KeyError:
- # they checked the filter box but didn't enter a value
- continue
- propdescr = props.get(colnm, None)
- if propdescr is None:
- print "huh? %r is in filter & form, but not in Class!" % colnm
- raise "butthead programmer"
- if (isinstance(propdescr, hyperdb.Link) or
- isinstance(propdescr, hyperdb.Multilink)):
- if type(val) == type([]):
- val = [arg.value for arg in val]
- else:
- val = val.value.split(',')
- l = filterspec.get(colnm, [])
- l = l + val
- filterspec[colnm] = l
- else:
- filterspec[colnm] = val.value
-
- return filterspec
-
- def customization_widget(self):
- ''' The customization widget is visible by default. The widget
- visibility is remembered by show_customization. Visibility
- is not toggled if the action value is "Redisplay"
- '''
- if not self.form.has_key('show_customization'):
- visible = 1
- else:
- visible = int(self.form['show_customization'].value)
- if self.form.has_key('action'):
- if self.form['action'].value != 'Redisplay':
- visible = self.form['action'].value == '+'
-
- return visible
-
- # TODO: make this go away some day...
- default_index_sort = ['-activity']
- default_index_group = ['priority']
- default_index_filter = ['status']
- default_index_columns = ['id','activity','title','status','assignedto']
- default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
- default_pagesize = '50'
-
- def _get_customisation_info(self):
- # see if the web has supplied us with any customisation info
- for key in ':sort', ':group', ':filter', ':columns', ':pagesize':
- if self.form.has_key(key):
- # make list() extract the info from the CGI environ
- self.classname = 'issue'
- sort = group = filter = columns = filterspec = pagesize = None
- break
- else:
- # TODO: look up the session first
- # try the instance config first
- if hasattr(self.instance, 'DEFAULT_INDEX'):
- d = self.instance.DEFAULT_INDEX
- self.classname = d['CLASS']
- sort = d['SORT']
- group = d['GROUP']
- filter = d['FILTER']
- columns = d['COLUMNS']
- filterspec = d['FILTERSPEC']
- pagesize = d.get('PAGESIZE', '50')
- else:
- # nope - fall back on the old way of doing it
- self.classname = 'issue'
- sort = self.default_index_sort
- group = self.default_index_group
- filter = self.default_index_filter
- columns = self.default_index_columns
- filterspec = self.default_index_filterspec
- pagesize = self.default_pagesize
- return columns, filter, group, sort, filterspec, pagesize
-
- def index(self):
- ''' put up an index - no class specified
- '''
- columns, filter, group, sort, filterspec, pagesize = \
- self._get_customisation_info()
- return self.list(columns=columns, filter=filter, group=group,
- sort=sort, filterspec=filterspec, pagesize=pagesize)
-
- def searchnode(self):
- columns, filter, group, sort, filterspec, pagesize = \
- self._get_customisation_info()
- cn = self.classname
- self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
- 'classname': cn, 'instancename': self.instance.INSTANCE_NAME})
- index = htmltemplate.IndexTemplate(self, self.instance.TEMPLATES, cn)
- self.write('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
- all_columns = self.db.getclass(cn).getprops().keys()
- all_columns.sort()
- index.filter_section('', filter, columns, group, all_columns, sort,
- filterspec, pagesize, 0, 0)
- self.pagefoot()
-
- # XXX deviates from spec - loses the '+' (that's a reserved character
- # in URLS
- def list(self, sort=None, group=None, filter=None, columns=None,
- filterspec=None, show_customization=None, show_nodes=1,
- pagesize=None):
- ''' call the template index with the args
-
- :sort - sort by prop name, optionally preceeded with '-'
- to give descending or nothing for ascending sorting.
- :group - group by prop name, optionally preceeded with '-' or
- to sort in descending or nothing for ascending order.
- :filter - selects which props should be displayed in the filter
- section. Default is all.
- :columns - selects the columns that should be displayed.
- Default is all.
-
- '''
- cn = self.classname
- cl = self.db.classes[cn]
- if sort is None: sort = self.index_sort()
- if group is None: group = self.index_arg(':group')
- if filter is None: filter = self.index_arg(':filter')
- if columns is None: columns = self.index_arg(':columns')
- if filterspec is None: filterspec = self.index_filterspec(filter)
- if show_customization is None:
- show_customization = self.customization_widget()
- if self.form.has_key('search_text'):
- search_text = self.form['search_text'].value
- else:
- search_text = ''
- if pagesize is None:
- if self.form.has_key(':pagesize'):
- pagesize = self.form[':pagesize'].value
- else:
- pagesize = '50'
- pagesize = int(pagesize)
- if self.form.has_key(':startwith'):
- startwith = int(self.form[':startwith'].value)
- else:
- startwith = 0
- simpleform = 1
- if self.form.has_key(':advancedsearch'):
- simpleform = 0
-
- if self.form.has_key('Query') and self.form['Query'].value == 'Save':
- # format a query string
- qd = {}
- qd[':sort'] = ','.join(map(urllib.quote, sort))
- qd[':group'] = ','.join(map(urllib.quote, group))
- qd[':filter'] = ','.join(map(urllib.quote, filter))
- qd[':columns'] = ','.join(map(urllib.quote, columns))
- for k, l in filterspec.items():
- qd[urllib.quote(k)] = ','.join(map(urllib.quote, l))
- url = '&'.join([k+'='+v for k,v in qd.items()])
- url += '&:pagesize=%s' % pagesize
- if search_text:
- url += '&search_text=%s' % search_text
-
- # create a query
- d = {}
- d['name'] = nm = self.form[':name'].value
- if not nm:
- d['name'] = nm = 'New Query'
- d['klass'] = self.form[':classname'].value
- d['url'] = url
- qid = self.db.getclass('query').create(**d)
-
- # and add it to the user's query multilink
- uid = self.getuid()
- usercl = self.db.getclass('user')
- queries = usercl.get(uid, 'queries')
- queries.append(qid)
- usercl.set(uid, queries=queries)
-
- self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
- 'classname': cn, 'instancename': self.instance.INSTANCE_NAME})
-
- index = htmltemplate.IndexTemplate(self, self.instance.TEMPLATES, cn)
- try:
- index.render(filterspec=filterspec, search_text=search_text,
- filter=filter, columns=columns, sort=sort, group=group,
- show_customization=show_customization,
- show_nodes=show_nodes, pagesize=pagesize, startwith=startwith,
- simple_search=simpleform)
- except htmltemplate.MissingTemplateError:
- self.basicClassEditPage()
- self.pagefoot()
-
- def basicClassEditPage(self):
- '''Display a basic edit page that allows simple editing of the
- nodes of the current class
- '''
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Edit', userid):
- raise Unauthorised, _("You do not have permission to access"\
- " %(action)s.")%{'action': self.classname}
- w = self.write
- cn = self.classname
- cl = self.db.classes[cn]
- idlessprops = cl.getprops(protected=0).keys()
- props = ['id'] + idlessprops
-
- # get the CSV module
- try:
- import csv
- except ImportError:
- w(_('Sorry, you need the csv module to use this function.<br>\n'
- 'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
- return
-
- # do the edit
- if self.form.has_key('rows'):
- rows = self.form['rows'].value.splitlines()
- p = csv.parser()
- found = {}
- line = 0
- for row in rows:
- line += 1
- values = p.parse(row)
- # not a complete row, keep going
- if not values: continue
-
- # extract the nodeid
- nodeid, values = values[0], values[1:]
- found[nodeid] = 1
-
- # confirm correct weight
- if len(idlessprops) != len(values):
- w(_('Not enough values on line %(line)s'%{'line':line}))
- return
-
- # extract the new values
- d = {}
- for name, value in zip(idlessprops, values):
- value = value.strip()
- # only add the property if it has a value
- if value:
- # if it's a multilink, split it
- if isinstance(cl.properties[name], hyperdb.Multilink):
- value = value.split(':')
- d[name] = value
-
- # perform the edit
- if cl.hasnode(nodeid):
- # edit existing
- cl.set(nodeid, **d)
- else:
- # new node
- found[cl.create(**d)] = 1
-
- # retire the removed entries
- for nodeid in cl.list():
- if not found.has_key(nodeid):
- cl.retire(nodeid)
-
- w(_('''<p class="form-help">You may edit the contents of the
- "%(classname)s" class using this form. Commas, newlines and double
- quotes (") must be handled delicately. You may include commas and
- newlines by enclosing the values in double-quotes ("). Double
- quotes themselves must be quoted by doubling ("").</p>
- <p class="form-help">Multilink properties have their multiple
- values colon (":") separated (... ,"one:two:three", ...)</p>
- <p class="form-help">Remove entries by deleting their line. Add
- new entries by appending
- them to the table - put an X in the id column.</p>''')%{'classname':cn})
-
- l = []
- for name in props:
- l.append(name)
- w('<tt>')
- w(', '.join(l) + '\n')
- w('</tt>')
-
- w('<form onSubmit="return submit_once()" method="POST">')
- w('<textarea name="rows" cols=80 rows=15>')
- p = csv.parser()
- for nodeid in cl.list():
- l = []
- for name in props:
- value = cl.get(nodeid, name)
- if value is None:
- l.append('')
- elif isinstance(value, type([])):
- l.append(cgi.escape(':'.join(map(str, value))))
- else:
- l.append(cgi.escape(str(cl.get(nodeid, name))))
- w(p.join(l) + '\n')
-
- w(_('</textarea><br><input type="submit" value="Save Changes"></form>'))
-
- def classhelp(self):
- '''Display a table of class info
- '''
- w = self.write
- cn = self.form['classname'].value
- cl = self.db.classes[cn]
- props = self.form['properties'].value.split(',')
- if cl.labelprop(1) in props:
- sort = [cl.labelprop(1)]
- else:
- sort = props[0]
-
- w('<table border=1 cellspacing=0 cellpaddin=2>')
- w('<tr>')
- for name in props:
- w('<th align=left>%s</th>'%name)
- w('</tr>')
- for nodeid in cl.filter(None, {}, sort, []):
- w('<tr>')
- for name in props:
- value = cgi.escape(str(cl.get(nodeid, name)))
- w('<td align="left" valign="top">%s</td>'%value)
- w('</tr>')
- w('</table>')
-
- def shownode(self, message=None, num_re=re.compile('^\d+$')):
- ''' display an item
- '''
- cn = self.classname
- cl = self.db.classes[cn]
- keys = self.form.keys()
- fromremove = 0
- if self.form.has_key(':multilink'):
- # is the multilink there because we came from remove()?
- if self.form.has_key(':target'):
- xtra = ''
- fromremove = 1
- message = _('%s removed' % self.index_arg(":target")[0])
- else:
- link = self.form[':multilink'].value
- designator, linkprop = link.split(':')
- xtra = ' for <a href="%s">%s</a>' % (designator, designator)
- else:
- xtra = ''
-
- # possibly perform an edit
- # don't try to set properties if the user has just logged in
- if keys and not fromremove and not self.form.has_key('__login_name'):
- try:
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Edit', userid, cn):
- message = _('You do not have permission to edit %s' %cn)
- else:
- props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
- # make changes to the node
- props = self._changenode(props)
- # handle linked nodes
- self._post_editnode(self.nodeid)
- # and some nice feedback for the user
- if props:
- message = _('%(changes)s edited ok')%{'changes':
- ', '.join(props.keys())}
- elif self.form.has_key('__note') and self.form['__note'].value:
- message = _('note added')
- elif (self.form.has_key('__file') and
- self.form['__file'].filename):
- message = _('file added')
- else:
- message = _('nothing changed')
- except:
- self.db.rollback()
- s = StringIO.StringIO()
- traceback.print_exc(None, s)
- message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
-
- # now the display
- id = self.nodeid
- if cl.getkey():
- id = cl.get(id, cl.getkey())
- self.pagehead('%s: %s %s'%(self.classname.capitalize(), id, xtra),
- message)
-
- nodeid = self.nodeid
-
- # use the template to display the item
- item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES,
- self.classname)
- item.render(nodeid)
-
- self.pagefoot()
- showissue = shownode
- showmsg = shownode
- searchissue = searchnode
-
- def showquery(self):
- queries = self.db.getclass(self.classname)
- if self.form.keys():
- sort = self.index_sort()
- group = self.index_arg(':group')
- filter = self.index_arg(':filter')
- columns = self.index_arg(':columns')
- filterspec = self.index_filterspec(filter, queries.get(self.nodeid, 'klass'))
- if self.form.has_key('search_text'):
- search_text = self.form['search_text'].value
- search_text = urllib.quote(search_text)
- else:
- search_text = ''
- if self.form.has_key(':pagesize'):
- pagesize = int(self.form[':pagesize'].value)
- else:
- pagesize = 50
- # format a query string
- qd = {}
- qd[':sort'] = ','.join(map(urllib.quote, sort))
- qd[':group'] = ','.join(map(urllib.quote, group))
- qd[':filter'] = ','.join(map(urllib.quote, filter))
- qd[':columns'] = ','.join(map(urllib.quote, columns))
- for k, l in filterspec.items():
- qd[urllib.quote(k)] = ','.join(map(urllib.quote, l))
- url = '&'.join([k+'='+v for k,v in qd.items()])
- url += '&:pagesize=%s' % pagesize
- if search_text:
- url += '&search_text=%s' % search_text
- if url != queries.get(self.nodeid, 'url'):
- queries.set(self.nodeid, url=url)
- message = _('url edited ok')
- else:
- message = _('nothing changed')
- else:
- message = None
- nm = queries.get(self.nodeid, 'name')
- self.pagehead('%s: %s'%(self.classname.capitalize(), nm), message)
-
- # use the template to display the item
- item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES,
- self.classname)
- item.render(self.nodeid)
- self.pagefoot()
-
- def _changenode(self, props):
- ''' change the node based on the contents of the form
- '''
- cl = self.db.classes[self.classname]
-
- # create the message
- message, files = self._handle_message()
- if message:
- props['messages'] = cl.get(self.nodeid, 'messages') + [message]
- if files:
- props['files'] = cl.get(self.nodeid, 'files') + files
-
- # make the changes
- return cl.set(self.nodeid, **props)
-
- def _createnode(self):
- ''' create a node based on the contents of the form
- '''
- cl = self.db.classes[self.classname]
- props = parsePropsFromForm(self.db, cl, self.form)
-
- # check for messages and files
- message, files = self._handle_message()
- if message:
- props['messages'] = [message]
- if files:
- props['files'] = files
- # create the node and return it's id
- return cl.create(**props)
-
- def _handle_message(self):
- ''' generate an edit message
- '''
- # handle file attachments
- files = []
- if self.form.has_key('__file'):
- file = self.form['__file']
- if file.filename:
- filename = file.filename.split('\\')[-1]
- mime_type = mimetypes.guess_type(filename)[0]
- if not mime_type:
- mime_type = "application/octet-stream"
- # create the new file entry
- files.append(self.db.file.create(type=mime_type,
- name=filename, content=file.file.read()))
-
- # we don't want to do a message if none of the following is true...
- cn = self.classname
- cl = self.db.classes[self.classname]
- props = cl.getprops()
- note = None
- # in a nutshell, don't do anything if there's no note or there's no
- # NOSY
- if self.form.has_key('__note'):
- note = self.form['__note'].value.strip()
- if not note:
- return None, files
- if not props.has_key('messages'):
- return None, files
- if not isinstance(props['messages'], hyperdb.Multilink):
- return None, files
- if not props['messages'].classname == 'msg':
- return None, files
- if not (self.form.has_key('nosy') or note):
- return None, files
-
- # handle the note
- if '\n' in note:
- summary = re.split(r'\n\r?', note)[0]
- else:
- summary = note
- m = ['%s\n'%note]
-
- # handle the messageid
- # TODO: handle inreplyto
- messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
- self.classname, self.instance.MAIL_DOMAIN)
-
- # now create the message, attaching the files
- content = '\n'.join(m)
- message_id = self.db.msg.create(author=self.getuid(),
- recipients=[], date=date.Date('.'), summary=summary,
- content=content, files=files, messageid=messageid)
-
- # update the messages property
- return message_id, files
-
- def _post_editnode(self, nid):
- '''Do the linking part of the node creation.
-
- If a form element has :link or :multilink appended to it, its
- value specifies a node designator and the property on that node
- to add _this_ node to as a link or multilink.
-
- This is typically used on, eg. the file upload page to indicated
- which issue to link the file to.
-
- TODO: I suspect that this and newfile will go away now that
- there's the ability to upload a file using the issue __file form
- element!
- '''
- cn = self.classname
- cl = self.db.classes[cn]
- # link if necessary
- keys = self.form.keys()
- for key in keys:
- if key == ':multilink':
- value = self.form[key].value
- if type(value) != type([]): value = [value]
- for value in value:
- designator, property = value.split(':')
- link, nodeid = hyperdb.splitDesignator(designator)
- link = self.db.classes[link]
- # take a dupe of the list so we're not changing the cache
- value = link.get(nodeid, property)[:]
- value.append(nid)
- link.set(nodeid, **{property: value})
- elif key == ':link':
- value = self.form[key].value
- if type(value) != type([]): value = [value]
- for value in value:
- designator, property = value.split(':')
- link, nodeid = hyperdb.splitDesignator(designator)
- link = self.db.classes[link]
- link.set(nodeid, **{property: nid})
-
- def newnode(self, message=None):
- ''' Add a new node to the database.
-
- The form works in two modes: blank form and submission (that is,
- the submission goes to the same URL). **Eventually this means that
- the form will have previously entered information in it if
- submission fails.
-
- The new node will be created with the properties specified in the
- form submission. For multilinks, multiple form entries are handled,
- as are prop=value,value,value. You can't mix them though.
-
- If the new node is to be referenced from somewhere else immediately
- (ie. the new node is a file that is to be attached to a support
- issue) then supply one of these arguments in addition to the usual
- form entries:
- :link=designator:property
- :multilink=designator:property
- ... which means that once the new node is created, the "property"
- on the node given by "designator" should now reference the new
- node's id. The node id will be appended to the multilink.
- '''
- cn = self.classname
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('View', userid, cn):
- raise Unauthorised, _("You do not have permission to access"\
- " %(action)s.")%{'action': self.classname}
- cl = self.db.classes[cn]
- if self.form.has_key(':multilink'):
- link = self.form[':multilink'].value
- designator, linkprop = link.split(':')
- xtra = ' for <a href="%s">%s</a>' % (designator, designator)
- else:
- xtra = ''
-
- # possibly perform a create
- keys = self.form.keys()
- if [i for i in keys if i[0] != ':']:
- # no dice if you can't edit!
- if not self.db.security.hasPermission('Edit', userid, cn):
- raise Unauthorised, _("You do not have permission to access"\
- " %(action)s.")%{'action': 'new'+self.classname}
- props = {}
- try:
- nid = self._createnode()
- # handle linked nodes
- self._post_editnode(nid)
- # and some nice feedback for the user
- message = _('%(classname)s created ok')%{'classname': cn}
-
- # render the newly created issue
- self.db.commit()
- self.nodeid = nid
- self.pagehead('%s: %s'%(self.classname.capitalize(), nid),
- message)
- item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES,
- self.classname)
- item.render(nid)
- self.pagefoot()
- return
- except:
- self.db.rollback()
- s = StringIO.StringIO()
- traceback.print_exc(None, s)
- message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
- self.pagehead(_('New %(classname)s %(xtra)s')%{
- 'classname': self.classname.capitalize(),
- 'xtra': xtra }, message)
-
- # call the template
- newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
- self.classname)
- newitem.render(self.form)
-
- self.pagefoot()
- newissue = newnode
-
- def newuser(self, message=None):
- ''' Add a new user to the database.
-
- Don't do any of the message or file handling, just create the node.
- '''
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Edit', userid, 'user'):
- raise Unauthorised, _("You do not have permission to access"\
- " %(action)s.")%{'action': 'newuser'}
-
- cn = self.classname
- cl = self.db.classes[cn]
-
- # possibly perform a create
- keys = self.form.keys()
- if [i for i in keys if i[0] != ':']:
- try:
- props = parsePropsFromForm(self.db, cl, self.form)
- nid = cl.create(**props)
- # handle linked nodes
- self._post_editnode(nid)
- # and some nice feedback for the user
- message = _('%(classname)s created ok')%{'classname': cn}
- except:
- self.db.rollback()
- s = StringIO.StringIO()
- traceback.print_exc(None, s)
- message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
- self.pagehead(_('New %(classname)s')%{'classname':
- self.classname.capitalize()}, message)
-
- # call the template
- newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
- self.classname)
- newitem.render(self.form)
-
- self.pagefoot()
-
- def newfile(self, message=None):
- ''' Add a new file to the database.
-
- This form works very much the same way as newnode - it just has a
- file upload.
- '''
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Edit', userid, 'file'):
- raise Unauthorised, _("You do not have permission to access"\
- " %(action)s.")%{'action': 'newfile'}
- cn = self.classname
- cl = self.db.classes[cn]
- props = parsePropsFromForm(self.db, cl, self.form)
- if self.form.has_key(':multilink'):
- link = self.form[':multilink'].value
- designator, linkprop = link.split(':')
- xtra = ' for <a href="%s">%s</a>' % (designator, designator)
- else:
- xtra = ''
-
- # possibly perform a create
- keys = self.form.keys()
- if [i for i in keys if i[0] != ':']:
- try:
- file = self.form['content']
- mime_type = mimetypes.guess_type(file.filename)[0]
- if not mime_type:
- mime_type = "application/octet-stream"
- # save the file
- props['type'] = mime_type
- props['name'] = file.filename
- props['content'] = file.file.read()
- nid = cl.create(**props)
- # handle linked nodes
- self._post_editnode(nid)
- # and some nice feedback for the user
- message = _('%(classname)s created ok')%{'classname': cn}
- except:
- self.db.rollback()
- s = StringIO.StringIO()
- traceback.print_exc(None, s)
- message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
-
- self.pagehead(_('New %(classname)s %(xtra)s')%{
- 'classname': self.classname.capitalize(),
- 'xtra': xtra }, message)
- newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
- self.classname)
- newitem.render(self.form)
- self.pagefoot()
-
- def showuser(self, message=None, num_re=re.compile('^\d+$')):
- '''Display a user page for editing. Make sure the user is allowed
- to edit this node, and also check for password changes.
-
- Note: permission checks for this node are handled in the template.
- '''
- user = self.db.user
-
- # get the username of the node being edited
- try:
- node_user = user.get(self.nodeid, 'username')
- except IndexError:
- raise NotFound, 'user%s'%self.nodeid
-
- #
- # perform any editing
- #
- keys = self.form.keys()
- if keys:
- try:
- props = parsePropsFromForm(self.db, user, self.form,
- self.nodeid)
- set_cookie = 0
- if props.has_key('password'):
- password = self.form['password'].value.strip()
- if not password:
- # no password was supplied - don't change it
- del props['password']
- elif self.nodeid == self.getuid():
- # this is the logged-in user's password
- set_cookie = password
- user.set(self.nodeid, **props)
- # and some feedback for the user
- message = _('%(changes)s edited ok')%{'changes':
- ', '.join(props.keys())}
- except:
- self.db.rollback()
- s = StringIO.StringIO()
- traceback.print_exc(None, s)
- message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
- else:
- set_cookie = 0
-
- # fix the cookie if the password has changed
- if set_cookie:
- self.set_cookie(self.user, set_cookie)
-
- #
- # now the display
- #
- self.pagehead(_('User: %(user)s')%{'user': node_user}, message)
-
- # use the template to display the item
- item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES, 'user')
- item.render(self.nodeid)
- self.pagefoot()
-
- def showfile(self):
- ''' display a file
- '''
- # nothing in xtrapath - edit the file's metadata
- if self.xtrapath is None:
- return self.shownode()
-
- # something in xtrapath - download the file
- nodeid = self.nodeid
- cl = self.db.classes[self.classname]
- try:
- mime_type = cl.get(nodeid, 'type')
- except IndexError:
- raise NotFound, 'file%s'%nodeid
- if mime_type == 'message/rfc822':
- mime_type = 'text/plain'
- self.header(headers={'Content-Type': mime_type})
- self.write(cl.get(nodeid, 'content'))
-
- def permission(self):
- '''
- '''
-
- def classes(self, message=None):
- ''' display a list of all the classes in the database
- '''
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Edit', userid):
- raise Unauthorised, _("You do not have permission to access"\
- " %(action)s.")%{'action': 'all classes'}
-
- self.pagehead(_('Table of classes'), message)
- classnames = self.db.classes.keys()
- classnames.sort()
- self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
- for cn in classnames:
- cl = self.db.getclass(cn)
- self.write('<tr class="list-header"><th colspan=2 align=left>'
- '<a href="%s">%s</a></th></tr>'%(cn, cn.capitalize()))
- for key, value in cl.properties.items():
- if value is None: value = ''
- else: value = str(value)
- self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
- key, cgi.escape(value)))
- self.write('</table>')
- self.pagefoot()
-
- def unauthorised(self, message):
- ''' The user is not authorised to do something. If they're
- anonymous, throw up a login box. If not, just tell them they
- can't do whatever it was they were trying to do.
-
- Bot cases print up the message, which is most likely the
- argument to the Unauthorised exception.
- '''
- self.header(response=403)
- if self.desired_action is None or self.desired_action == 'login':
- if not message:
- message=_("You do not have permission.")
- action = 'index'
- else:
- if not message:
- message=_("You do not have permission to access"\
- " %(action)s.")%{'action': self.desired_action}
- action = self.desired_action
- if self.user == 'anonymous':
- self.login(action=action, message=message)
- else:
- self.pagehead(_('Not Authorised'))
- self.write('<p class="system-msg">%s</p>'%message)
- self.pagefoot()
-
- def login(self, message=None, newuser_form=None, action='index'):
- '''Display a login page.
- '''
- self.pagehead(_('Login to roundup'))
- if message:
- self.write('<p class="system-msg">%s</p>'%message)
- self.write(_('''
-<table>
-<tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
-<form onSubmit="return submit_once()" action="login_action" method=POST>
-<input type="hidden" name="__destination_url" value="%(action)s">
-<tr><td align=right>Login name: </td>
- <td><input name="__login_name"></td></tr>
-<tr><td align=right>Password: </td>
- <td><input type="password" name="__login_password"></td></tr>
-<tr><td></td>
- <td><input type="submit" value="Log In"></td></tr>
-</form>
-''')%locals())
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Web Registration', userid):
- self.write('</table>')
- self.pagefoot()
- return
- values = {'realname': '', 'organisation': '', 'address': '',
- 'phone': '', 'username': '', 'password': '', 'confirm': '',
- 'action': action, 'alternate_addresses': ''}
- if newuser_form is not None:
- for key in newuser_form.keys():
- values[key] = newuser_form[key].value
- self.write(_('''
-<p>
-<tr><td colspan=2 class="strong-header">New User Registration</td></tr>
-<tr><td colspan=2><em>marked items</em> are optional...</td></tr>
-<form onSubmit="return submit_once()" action="newuser_action" method=POST>
-<input type="hidden" name="__destination_url" value="%(action)s">
-<tr><td align=right><em>Name: </em></td>
- <td><input name="realname" value="%(realname)s" size=40></td></tr>
-<tr><td align=right><em>Organisation: </em></td>
- <td><input name="organisation" value="%(organisation)s" size=40></td></tr>
-<tr><td align=right>E-Mail Address: </td>
- <td><input name="address" value="%(address)s" size=40></td></tr>
-<tr><td align=right><em>Alternate E-mail Addresses: </em></td>
- <td><textarea name="alternate_addresses" rows=5 cols=40>%(alternate_addresses)s</textarea></td></tr>
-<tr><td align=right><em>Phone: </em></td>
- <td><input name="phone" value="%(phone)s"></td></tr>
-<tr><td align=right>Preferred Login name: </td>
- <td><input name="username" value="%(username)s"></td></tr>
-<tr><td align=right>Password: </td>
- <td><input type="password" name="password" value="%(password)s"></td></tr>
-<tr><td align=right>Password Again: </td>
- <td><input type="password" name="confirm" value="%(confirm)s"></td></tr>
-<tr><td></td>
- <td><input type="submit" value="Register"></td></tr>
-</form>
-</table>
-''')%values)
- self.pagefoot()
-
- def login_action(self, message=None):
- '''Attempt to log a user in and set the cookie
-
- returns 0 if a page is generated as a result of this call, and
- 1 if not (ie. the login is successful
- '''
- if not self.form.has_key('__login_name'):
- self.login(message=_('Username required'))
- return 0
- self.user = self.form['__login_name'].value
- # re-open the database for real, using the user
- self.opendb(self.user)
- if self.form.has_key('__login_password'):
- password = self.form['__login_password'].value
- else:
- password = ''
- # make sure the user exists
- try:
- uid = self.db.user.lookup(self.user)
- except KeyError:
- name = self.user
- self.make_user_anonymous()
- action = self.form['__destination_url'].value
- self.login(message=_('No such user "%(name)s"')%locals(),
- action=action)
- return 0
-
- # and that the password is correct
- pw = self.db.user.get(uid, 'password')
- if password != pw:
- self.make_user_anonymous()
- action = self.form['__destination_url'].value
- self.login(message=_('Incorrect password'), action=action)
- return 0
-
- self.set_cookie(self.user, password)
- return 1
-
- def newuser_action(self, message=None):
- '''Attempt to create a new user based on the contents of the form
- and then set the cookie.
-
- return 1 on successful login
- '''
- # make sure we're allowed to register
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Web Registration', userid):
- raise Unauthorised, _("You do not have permission to access"\
- " %(action)s.")%{'action': 'registration'}
-
- # re-open the database as "admin"
- if self.user != 'admin':
- self.opendb('admin')
-
- # create the new user
- cl = self.db.user
- try:
- props = parsePropsFromForm(self.db, cl, self.form)
- props['roles'] = self.instance.NEW_WEB_USER_ROLES
- uid = cl.create(**props)
- self.db.commit()
- except ValueError, message:
- action = self.form['__destination_url'].value
- self.login(message, action=action)
- return 0
-
- # log the new user in
- self.user = cl.get(uid, 'username')
- # re-open the database for real, using the user
- self.opendb(self.user)
- password = cl.get(uid, 'password')
- self.set_cookie(self.user, password)
- return 1
-
- def set_cookie(self, user, password):
- # TODO generate a much, much stronger session key ;)
- self.session = binascii.b2a_base64(repr(time.time())).strip()
-
- # clean up the base64
- if self.session[-1] == '=':
- if self.session[-2] == '=':
- self.session = self.session[:-2]
- else:
- self.session = self.session[:-1]
-
- # insert the session in the sessiondb
- self.db.sessions.set(self.session, user=user, last_use=time.time())
-
- # and commit immediately
- self.db.sessions.commit()
-
- # expire us in a long, long time
- expire = Cookie._getdate(86400*365)
-
- # generate the cookie path - make sure it has a trailing '/'
- path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
- ''))
- self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;'%(
- self.session, expire, path)})
-
- def make_user_anonymous(self):
- ''' Make us anonymous
-
- This method used to handle non-existence of the 'anonymous'
- user, but that user is mandatory now.
- '''
- self.db.user.lookup('anonymous')
- self.user = 'anonymous'
-
- def logout(self, message=None):
- ''' Make us really anonymous - nuke the cookie too
- '''
- self.make_user_anonymous()
-
- # construct the logout cookie
- now = Cookie._getdate()
- path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
- ''))
- self.header({'Set-Cookie':
- 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now,
- path)})
- self.login()
-
- def opendb(self, user):
- ''' Open the database.
- '''
- # open the db if the user has changed
- if not hasattr(self, 'db') or user != self.db.journaltag:
- self.db = self.instance.open(user)
-
- def main(self):
- ''' Wrap the request and handle unauthorised requests
- '''
- self.desired_action = None
- try:
- self.main_action()
- except Unauthorised, message:
- self.unauthorised(message)
-
- def main_action(self):
- '''Wrap the database accesses so we can close the database cleanly
- '''
- # determine the uid to use
- self.opendb('admin')
-
- # make sure we have the session Class
- sessions = self.db.sessions
-
- # age sessions, remove when they haven't been used for a week
- # TODO: this shouldn't be done every access
- week = 60*60*24*7
- now = time.time()
- for sessid in sessions.list():
- interval = now - sessions.get(sessid, 'last_use')
- if interval > week:
- sessions.destroy(sessid)
-
- # look up the user session cookie
- cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
- user = 'anonymous'
-
- if (cookie.has_key('roundup_user') and
- cookie['roundup_user'].value != 'deleted'):
-
- # get the session key from the cookie
- self.session = cookie['roundup_user'].value
- # get the user from the session
- try:
- # update the lifetime datestamp
- sessions.set(self.session, last_use=time.time())
- sessions.commit()
- user = sessions.get(self.session, 'user')
- except KeyError:
- user = 'anonymous'
-
- # sanity check on the user still being valid
- try:
- self.db.user.lookup(user)
- except (KeyError, TypeError):
- user = 'anonymous'
-
- # make sure the anonymous user is valid if we're using it
- if user == 'anonymous':
- self.make_user_anonymous()
- else:
- self.user = user
-
- # now figure which function to call
- path = self.split_path
- self.xtrapath = None
-
- # default action to index if the path has no information in it
- if not path or path[0] in ('', 'index'):
- action = 'index'
- else:
- action = path[0]
- if len(path) > 1:
- self.xtrapath = path[1:]
- self.desired_action = action
-
- # everyone is allowed to try to log in
- if action == 'login_action':
- # try to login
- if not self.login_action():
- return
- # figure the resulting page
- action = self.form['__destination_url'].value
-
- # allow anonymous people to register
- elif action == 'newuser_action':
- # try to add the user
- if not self.newuser_action():
- return
- # figure the resulting page
- action = self.form['__destination_url'].value
-
- # ok, now we have figured out who the user is, make sure the user
- # has permission to use this interface
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Web Access', userid):
- raise Unauthorised, \
- _("You do not have permission to access this interface.")
-
- # re-open the database for real, using the user
- self.opendb(self.user)
-
- # make sure we have a sane action
- if not action:
- action = 'index'
-
- # just a regular action
- try:
- self.do_action(action)
- except Unauthorised, message:
- # if unauth is raised here, then a page header will have
- # been displayed
- self.write('<p class="system-msg">%s</p>'%message)
- else:
- # commit all changes to the database
- self.db.commit()
-
- def do_action(self, action, dre=re.compile(r'([^\d]+)(\d+)'),
- nre=re.compile(r'new(\w+)'), sre=re.compile(r'search(\w+)')):
- '''Figure the user's action and do it.
- '''
- # here be the "normal" functionality
- if action == 'index':
- self.index()
- return
- if action == 'list_classes':
- self.classes()
- return
- if action == 'classhelp':
- self.classhelp()
- return
- if action == 'login':
- self.login()
- return
- if action == 'logout':
- self.logout()
- return
- if action == 'remove':
- self.remove()
- return
-
- # see if we're to display an existing node
- m = dre.match(action)
- if m:
- self.classname = m.group(1)
- self.nodeid = m.group(2)
- try:
- cl = self.db.classes[self.classname]
- except KeyError:
- raise NotFound, self.classname
- try:
- cl.get(self.nodeid, 'id')
- except IndexError:
- raise NotFound, self.nodeid
- try:
- func = getattr(self, 'show%s'%self.classname)
- except AttributeError:
- raise NotFound, 'show%s'%self.classname
- func()
- return
-
- # see if we're to put up the new node page
- m = nre.match(action)
- if m:
- self.classname = m.group(1)
- try:
- func = getattr(self, 'new%s'%self.classname)
- except AttributeError:
- raise NotFound, 'new%s'%self.classname
- func()
- return
-
- # see if we're to put up the new node page
- m = sre.match(action)
- if m:
- self.classname = m.group(1)
- try:
- func = getattr(self, 'search%s'%self.classname)
- except AttributeError:
- raise NotFound
- func()
- return
-
- # otherwise, display the named class
- self.classname = action
- try:
- self.db.getclass(self.classname)
- except KeyError:
- raise NotFound, self.classname
- self.list()
-
- def remove(self, dre=re.compile(r'([^\d]+)(\d+)')):
- target = self.index_arg(':target')[0]
- m = dre.match(target)
- if m:
- classname = m.group(1)
- nodeid = m.group(2)
- cl = self.db.getclass(classname)
- cl.retire(nodeid)
- # now take care of the reference
- parentref = self.index_arg(':multilink')[0]
- parent, prop = parentref.split(':')
- m = dre.match(parent)
- if m:
- self.classname = m.group(1)
- self.nodeid = m.group(2)
- cl = self.db.getclass(self.classname)
- value = cl.get(self.nodeid, prop)
- value.remove(nodeid)
- cl.set(self.nodeid, **{prop:value})
- func = getattr(self, 'show%s'%self.classname)
- return func()
- else:
- raise NotFound, parent
- else:
- raise NotFound, target
-
-class ExtendedClient(Client):
- '''Includes pages and page heading information that relate to the
- extended schema.
- '''
- showsupport = Client.shownode
- showtimelog = Client.shownode
- newsupport = Client.newnode
- newtimelog = Client.newnode
- searchsupport = Client.searchnode
-
- default_index_sort = ['-activity']
- default_index_group = ['priority']
- default_index_filter = ['status']
- default_index_columns = ['activity','status','title','assignedto']
- default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
- default_pagesize = '50'
-
-def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
- '''Pull properties for the given class out of the form.
- '''
- props = {}
- keys = form.keys()
- for key in keys:
- if not cl.properties.has_key(key):
- continue
- proptype = cl.properties[key]
- if isinstance(proptype, hyperdb.String):
- value = form[key].value.strip()
- elif isinstance(proptype, hyperdb.Password):
- value = password.Password(form[key].value.strip())
- elif isinstance(proptype, hyperdb.Date):
- value = form[key].value.strip()
- if value:
- value = date.Date(form[key].value.strip())
- else:
- value = None
- elif isinstance(proptype, hyperdb.Interval):
- value = form[key].value.strip()
- if value:
- value = date.Interval(form[key].value.strip())
- else:
- value = None
- elif isinstance(proptype, hyperdb.Link):
- value = form[key].value.strip()
- # see if it's the "no selection" choice
- if value == '-1':
- value = None
- else:
- # handle key values
- link = cl.properties[key].classname
- if not num_re.match(value):
- try:
- value = db.classes[link].lookup(value)
- except KeyError:
- raise ValueError, _('property "%(propname)s": '
- '%(value)s not a %(classname)s')%{'propname':key,
- 'value': value, 'classname': link}
- elif isinstance(proptype, hyperdb.Multilink):
- value = form[key]
- if hasattr(value, 'value'):
- # Quite likely to be a FormItem instance
- value = value.value
- if not isinstance(value, type([])):
- value = [i.strip() for i in value.split(',')]
- else:
- value = [i.strip() for i in value]
- link = cl.properties[key].classname
- l = []
- for entry in map(str, value):
- if entry == '': continue
- if not num_re.match(entry):
- try:
- entry = db.classes[link].lookup(entry)
- except KeyError:
- raise ValueError, _('property "%(propname)s": '
- '"%(value)s" not an entry of %(classname)s')%{
- 'propname':key, 'value': entry, 'classname': link}
- l.append(entry)
- l.sort()
- value = l
- elif isinstance(proptype, hyperdb.Boolean):
- value = form[key].value.strip()
- props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
- elif isinstance(proptype, hyperdb.Number):
- value = form[key].value.strip()
- props[key] = value = int(value)
-
- # get the old value
- if nodeid:
- try:
- existing = cl.get(nodeid, key)
- except KeyError:
- # this might be a new property for which there is no existing
- # value
- if not cl.properties.has_key(key): raise
-
- # if changed, set it
- if value != existing:
- props[key] = value
- else:
- props[key] = value
- return props
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.161 2002/08/19 00:21:10 richard
-# removed debug prints
-#
-# Revision 1.160 2002/08/19 00:20:34 richard
-# grant web access to admin ;)
-#
-# Revision 1.159 2002/08/16 04:29:41 richard
-# bugfix
-#
-# Revision 1.158 2002/08/15 00:40:10 richard
-# cleanup
-#
-# Revision 1.157 2002/08/13 20:16:09 gmcm
-# Use a real parser for templates.
-# Rewrite htmltemplate to use the parser (hack, hack).
-# Move the "do_XXX" methods to template_funcs.py.
-# Redo the funcion tests (but not Template tests - they're hopeless).
-# Simplified query form in cgi_client.
-# Ability to delete msgs, files, queries.
-# Ability to edit the metadata on files.
-#
-# Revision 1.156 2002/08/01 15:06:06 gmcm
-# Use same regex to split search terms as used to index text.
-# Fix to back_metakit for not changing journaltag on reopen.
-# Fix htmltemplate's do_link so [No <whatever>] strings are href'd.
-# Fix bogus "nosy edited ok" msg - the **d syntax does NOT share d between caller and callee.
-#
-# Revision 1.155 2002/08/01 00:56:22 richard
-# Added the web access and email access permissions, so people can restrict
-# access to users who register through the email interface (for example).
-# Also added "security" command to the roundup-admin interface to display the
-# Role/Permission config for an instance.
-#
-# Revision 1.154 2002/07/31 23:57:36 richard
-# . web forms may now unset Link values (like assignedto)
-#
-# Revision 1.153 2002/07/31 22:40:50 gmcm
-# Fixes to the search form and saving queries.
-# Fixes to sorting in back_metakit.py.
-#
-# Revision 1.152 2002/07/31 22:04:14 richard
-# cleanup
-#
-# Revision 1.151 2002/07/30 21:37:43 richard
-# oops, thanks Duncan Booth for spotting this one
-#
-# Revision 1.150 2002/07/30 20:43:18 gmcm
-# Oops, fix the permission check!
-#
-# Revision 1.149 2002/07/30 20:04:38 gmcm
-# Adapt metakit backend to new security scheme.
-# Put some more permission checks in cgi_client.
-#
-# Revision 1.148 2002/07/30 16:09:11 gmcm
-# Simple optimization.
-#
-# Revision 1.147 2002/07/30 08:22:38 richard
-# Session storage in the hyperdb was horribly, horribly inefficient. We use
-# a simple anydbm wrapper now - which could be overridden by the metakit
-# backend or RDB backend if necessary.
-# Much, much better.
-#
-# Revision 1.146 2002/07/30 05:27:30 richard
-# nicer error messages, and a bugfix
-#
-# Revision 1.145 2002/07/26 08:26:59 richard
-# Very close now. The cgi and mailgw now use the new security API. The two
-# templates have been migrated to that setup. Lots of unit tests. Still some
-# issue in the web form for editing Roles assigned to users.
-#
-# Revision 1.144 2002/07/25 07:14:05 richard
-# Bugger it. Here's the current shape of the new security implementation.
-# Still to do:
-# . call the security funcs from cgi and mailgw
-# . change shipped templates to include correct initialisation and remove
-# the old config vars
-# ... that seems like a lot. The bulk of the work has been done though. Honest :)
-#
-# Revision 1.143 2002/07/20 19:29:10 gmcm
-# Fixes/improvements to the search form & saved queries.
-#
-# Revision 1.142 2002/07/18 11:17:30 gmcm
-# Add Number and Boolean types to hyperdb.
-# Add conversion cases to web, mail & admin interfaces.
-# Add storage/serialization cases to back_anydbm & back_metakit.
-#
-# Revision 1.141 2002/07/17 12:39:10 gmcm
-# Saving, running & editing queries.
-#
-# Revision 1.140 2002/07/14 23:17:15 richard
-# cleaned up structure
-#
-# Revision 1.139 2002/07/14 06:14:40 richard
-# Some more TODOs
-#
-# Revision 1.138 2002/07/14 04:03:13 richard
-# Implemented a switch to disable journalling for a Class. CGI session
-# database now uses it.
-#
-# Revision 1.137 2002/07/10 07:00:30 richard
-# removed debugging
-#
-# Revision 1.136 2002/07/10 06:51:08 richard
-# . #576241 ] MultiLink problems in parsePropsFromForm
-#
-# Revision 1.135 2002/07/10 00:22:34 richard
-# . switched to using a session-based web login
-#
-# Revision 1.134 2002/07/09 04:19:09 richard
-# Added reindex command to roundup-admin.
-# Fixed reindex on first access.
-# Also fixed reindexing of entries that change.
-#
-# Revision 1.133 2002/07/08 15:32:05 gmcm
-# Pagination of index pages.
-# New search form.
-#
-# Revision 1.132 2002/07/08 07:26:14 richard
-# ehem
-#
-# Revision 1.131 2002/07/08 06:53:57 richard
-# Not sure why the cgi_client had an indexer argument.
-#
-# Revision 1.130 2002/06/27 12:01:53 gmcm
-# If the form has a :multilink, put a back href in the pageheader (back to the linked-to node).
-# Some minor optimizations (only compile regexes once).
-#
-# Revision 1.129 2002/06/20 23:52:11 richard
-# Better handling of unauth attempt to edit stuff
-#
-# Revision 1.128 2002/06/12 21:28:25 gmcm
-# Allow form to set user-properties on a Fileclass.
-# Don't assume that a Fileclass is named "files".
-#
-# Revision 1.127 2002/06/11 06:38:24 richard
-# . #565996 ] The "Attach a File to this Issue" fails
-#
-# Revision 1.126 2002/05/29 01:16:17 richard
-# Sorry about this huge checkin! It's fixing a lot of related stuff in one go
-# though.
-#
-# . #541941 ] changing multilink properties by mail
-# . #526730 ] search for messages capability
-# . #505180 ] split MailGW.handle_Message
-# - also changed cgi client since it was duplicating the functionality
-# . build htmlbase if tests are run using CVS checkout (removed note from
-# installation.txt)
-# . don't create an empty message on email issue creation if the email is empty
-#
-# Revision 1.125 2002/05/25 07:16:24 rochecompaan
-# Merged search_indexing-branch with HEAD
-#
-# Revision 1.124 2002/05/24 02:09:24 richard
-# Nothing like a live demo to show up the bugs ;)
-#
-# Revision 1.123 2002/05/22 05:04:13 richard
-# Oops
-#
-# Revision 1.122 2002/05/22 04:12:05 richard
-# . applied patch #558876 ] cgi client customization
-# ... with significant additions and modifications ;)
-# - extended handling of ML assignedto to all places it's handled
-# - added more NotFound info
-#
-# Revision 1.121 2002/05/21 06:08:10 richard
-# Handle migration
-#
-# Revision 1.120 2002/05/21 06:05:53 richard
-# . #551483 ] assignedto in Client.make_index_link
-#
-# Revision 1.119 2002/05/15 06:21:21 richard
-# . node caching now works, and gives a small boost in performance
-#
-# As a part of this, I cleaned up the DEBUG output and implemented TRACE
-# output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
-# CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
-# (using if __debug__ which is compiled out with -O)
-#
-# Revision 1.118 2002/05/12 23:46:33 richard
-# ehem, part 2
-#
-# Revision 1.117 2002/05/12 23:42:29 richard
-# ehem
-#
-# Revision 1.116 2002/05/02 08:07:49 richard
-# Added the ADD_AUTHOR_TO_NOSY handling to the CGI interface.
-#
-# Revision 1.115 2002/04/02 01:56:10 richard
-# . stop sending blank (whitespace-only) notes
-#
-# Revision 1.114.2.4 2002/05/02 11:49:18 rochecompaan
-# Allow customization of the search filters that should be displayed
-# on the search page.
-#
-# Revision 1.114.2.3 2002/04/20 13:23:31 rochecompaan
-# We now have a separate search page for nodes. Search links for
-# different classes can be customized in instance_config similar to
-# index links.
-#
-# Revision 1.114.2.2 2002/04/19 19:54:42 rochecompaan
-# cgi_client.py
-# removed search link for the time being
-# moved rendering of matches to htmltemplate
-# hyperdb.py
-# filtering of nodes on full text search incorporated in filter method
-# roundupdb.py
-# added paramater to call of filter method
-# roundup_indexer.py
-# added search method to RoundupIndexer class
-#
-# Revision 1.114.2.1 2002/04/03 11:55:57 rochecompaan
-# . Added feature #526730 - search for messages capability
-#
-# Revision 1.114 2002/03/17 23:06:05 richard
-# oops
-#
-# Revision 1.113 2002/03/14 23:59:24 richard
-# . #517734 ] web header customisation is obscure
-#
-# Revision 1.112 2002/03/12 22:52:26 richard
-# more pychecker warnings removed
-#
-# Revision 1.111 2002/02/25 04:32:21 richard
-# ahem
-#
-# Revision 1.110 2002/02/21 07:19:08 richard
-# ... and label, width and height control for extra flavour!
-#
-# Revision 1.109 2002/02/21 07:08:19 richard
-# oops
-#
-# Revision 1.108 2002/02/21 07:02:54 richard
-# The correct var is "HTTP_HOST"
-#
-# Revision 1.107 2002/02/21 06:57:38 richard
-# . Added popup help for classes using the classhelp html template function.
-# - add <display call="classhelp('priority', 'id,name,description')">
-# to an item page, and it generates a link to a popup window which displays
-# the id, name and description for the priority class. The description
-# field won't exist in most installations, but it will be added to the
-# default templates.
-#
-# Revision 1.106 2002/02/21 06:23:00 richard
-# *** empty log message ***
-#
-# Revision 1.105 2002/02/20 05:52:10 richard
-# better error handling
-#
-# Revision 1.104 2002/02/20 05:45:17 richard
-# Use the csv module for generating the form entry so it's correct.
-# [also noted the sf.net feature request id in the change log]
-#
-# Revision 1.103 2002/02/20 05:05:28 richard
-# . Added simple editing for classes that don't define a templated interface.
-# - access using the admin "class list" interface
-# - limited to admin-only
-# - requires the csv module from object-craft (url given if it's missing)
-#
-# Revision 1.102 2002/02/15 07:08:44 richard
-# . Alternate email addresses are now available for users. See the MIGRATION
-# file for info on how to activate the feature.
-#
-# Revision 1.101 2002/02/14 23:39:18 richard
-# . All forms now have "double-submit" protection when Javascript is enabled
-# on the client-side.
-#
-# Revision 1.100 2002/01/16 07:02:57 richard
-# . lots of date/interval related changes:
-# - more relaxed date format for input
-#
-# Revision 1.99 2002/01/16 03:02:42 richard
-# #503793 ] changing assignedto resets nosy list
-#
-# Revision 1.98 2002/01/14 02:20:14 richard
-# . changed all config accesses so they access either the instance or the
-# config attriubute on the db. This means that all config is obtained from
-# instance_config instead of the mish-mash of classes. This will make
-# switching to a ConfigParser setup easier too, I hope.
-#
-# At a minimum, this makes migration a _little_ easier (a lot easier in the
-# 0.5.0 switch, I hope!)
-#
-# Revision 1.97 2002/01/11 23:22:29 richard
-# . #502437 ] rogue reactor and unittest
-# in short, the nosy reactor was modifying the nosy list. That code had
-# been there for a long time, and I suspsect it was there because we
-# weren't generating the nosy list correctly in other places of the code.
-# We're now doing that, so the nosy-modifying code can go away from the
-# nosy reactor.
-#
-# Revision 1.96 2002/01/10 05:26:10 richard
-# missed a parsePropsFromForm in last update
-#
-# Revision 1.95 2002/01/10 03:39:45 richard
-# . fixed some problems with web editing and change detection
-#
-# Revision 1.94 2002/01/09 13:54:21 grubert
-# _add_assignedto_to_nosy did set nosy to assignedto only, no adding.
-#
-# Revision 1.93 2002/01/08 11:57:12 richard
-# crying out for real configuration handling... :(
-#
-# Revision 1.92 2002/01/08 04:12:05 richard
-# Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822
-#
-# Revision 1.91 2002/01/08 04:03:47 richard
-# I mucked the intent of the code up.
-#
-# Revision 1.90 2002/01/08 03:56:55 richard
-# Oops, missed this before the beta:
-# . #495392 ] empty nosy -patch
-#
-# Revision 1.89 2002/01/07 20:24:45 richard
-# *mutter* stupid cutnpaste
-#
-# Revision 1.88 2002/01/02 02:31:38 richard
-# Sorry for the huge checkin message - I was only intending to implement #496356
-# but I found a number of places where things had been broken by transactions:
-# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
-# for _all_ roundup-generated smtp messages to be sent to.
-# . the transaction cache had broken the roundupdb.Class set() reactors
-# . newly-created author users in the mailgw weren't being committed to the db
-#
-# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
-# on when I found that stuff :):
-# . #496356 ] Use threading in messages
-# . detectors were being registered multiple times
-# . added tests for mailgw
-# . much better attaching of erroneous messages in the mail gateway
-#
-# Revision 1.87 2001/12/23 23:18:49 richard
-# We already had an admin-specific section of the web heading, no need to add
-# another one :)
-#
-# Revision 1.86 2001/12/20 15:43:01 rochecompaan
-# Features added:
-# . Multilink properties are now displayed as comma separated values in
-# a textbox
-# . The add user link is now only visible to the admin user
-# . Modified the mail gateway to reject submissions from unknown
-# addresses if ANONYMOUS_ACCESS is denied
-#
-# Revision 1.85 2001/12/20 06:13:24 rochecompaan
-# Bugs fixed:
-# . Exception handling in hyperdb for strings-that-look-like numbers got
-# lost somewhere
-# . Internet Explorer submits full path for filename - we now strip away
-# the path
-# Features added:
-# . Link and multilink properties are now displayed sorted in the cgi
-# interface
-#
-# Revision 1.84 2001/12/18 15:30:30 rochecompaan
-# Fixed bugs:
-# . Fixed file creation and retrieval in same transaction in anydbm
-# backend
-# . Cgi interface now renders new issue after issue creation
-# . Could not set issue status to resolved through cgi interface
-# . Mail gateway was changing status back to 'chatting' if status was
-# omitted as an argument
-#
-# Revision 1.83 2001/12/15 23:51:01 richard
-# Tested the changes and fixed a few problems:
-# . files are now attached to the issue as well as the message
-# . newuser is a real method now since we don't want to do the message/file
-# stuff for it
-# . added some documentation
-# The really big changes in the diff are a result of me moving some code
-# around to keep like methods together a bit better.
-#
-# Revision 1.82 2001/12/15 19:24:39 rochecompaan
-# . Modified cgi interface to change properties only once all changes are
-# collected, files created and messages generated.
-# . Moved generation of change note to nosyreactors.
-# . We now check for changes to "assignedto" to ensure it's added to the
-# nosy list.
-#
-# Revision 1.81 2001/12/12 23:55:00 richard
-# Fixed some problems with user editing
-#
-# Revision 1.80 2001/12/12 23:27:14 richard
-# Added a Zope frontend for roundup.
-#
-# Revision 1.79 2001/12/10 22:20:01 richard
-# Enabled transaction support in the bsddb backend. It uses the anydbm code
-# where possible, only replacing methods where the db is opened (it uses the
-# btree opener specifically.)
-# Also cleaned up some change note generation.
-# Made the backends package work with pydoc too.
-#
-# Revision 1.78 2001/12/07 05:59:27 rochecompaan
-# Fixed small bug that prevented adding issues through the web.
-#
-# Revision 1.77 2001/12/06 22:48:29 richard
-# files multilink was being nuked in post_edit_node
-#
-# Revision 1.76 2001/12/05 14:26:44 rochecompaan
-# Removed generation of change note from "sendmessage" in roundupdb.py.
-# The change note is now generated when the message is created.
-#
-# Revision 1.75 2001/12/04 01:25:08 richard
-# Added some rollbacks where we were catching exceptions that would otherwise
-# have stopped committing.
-#
-# Revision 1.74 2001/12/02 05:06:16 richard
-# . We now use weakrefs in the Classes to keep the database reference, so
-# the close() method on the database is no longer needed.
-# I bumped the minimum python requirement up to 2.1 accordingly.
-# . #487480 ] roundup-server
-# . #487476 ] INSTALL.txt
-#
-# I also cleaned up the change message / post-edit stuff in the cgi client.
-# There's now a clearly marked "TODO: append the change note" where I believe
-# the change note should be added there. The "changes" list will obviously
-# have to be modified to be a dict of the changes, or somesuch.
-#
-# More testing needed.
-#
-# Revision 1.73 2001/12/01 07:17:50 richard
-# . We now have basic transaction support! Information is only written to
-# the database when the commit() method is called. Only the anydbm
-# backend is modified in this way - neither of the bsddb backends have been.
-# The mail, admin and cgi interfaces all use commit (except the admin tool
-# doesn't have a commit command, so interactive users can't commit...)
-# . Fixed login/registration forwarding the user to the right page (or not,
-# on a failure)
-#
-# Revision 1.72 2001/11/30 20:47:58 rochecompaan
-# Links in page header are now consistent with default sort order.
-#
-# Fixed bugs:
-# - When login failed the list of issues were still rendered.
-# - User was redirected to index page and not to his destination url
-# if his first login attempt failed.
-#
-# Revision 1.71 2001/11/30 20:28:10 rochecompaan
-# Property changes are now completely traceable, whether changes are
-# made through the web or by email
-#
-# Revision 1.70 2001/11/30 00:06:29 richard
-# Converted roundup/cgi_client.py to use _()
-# Added the status file, I18N_PROGRESS.txt
-#
-# Revision 1.69 2001/11/29 23:19:51 richard
-# Removed the "This issue has been edited through the web" when a valid
-# change note is supplied.
-#
-# Revision 1.68 2001/11/29 04:57:23 richard
-# a little comment
-#
-# Revision 1.67 2001/11/28 21:55:35 richard
-# . login_action and newuser_action return values were being ignored
-# . Woohoo! Found that bloody re-login bug that was killing the mail
-# gateway.
-# (also a minor cleanup in hyperdb)
-#
-# Revision 1.66 2001/11/27 03:00:50 richard
-# couple of bugfixes from latest patch integration
-#
-# Revision 1.65 2001/11/26 23:00:53 richard
-# This config stuff is getting to be a real mess...
-#
-# Revision 1.64 2001/11/26 22:56:35 richard
-# typo
-#
-# Revision 1.63 2001/11/26 22:55:56 richard
-# Feature:
-# . Added INSTANCE_NAME to configuration - used in web and email to identify
-# the instance.
-# . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
-# signature info in e-mails.
-# . Some more flexibility in the mail gateway and more error handling.
-# . Login now takes you to the page you back to the were denied access to.
-#
-# Fixed:
-# . Lots of bugs, thanks Roché and others on the devel mailing list!
-#
-# Revision 1.62 2001/11/24 00:45:42 jhermann
-# typeof() instead of type(): avoid clash with database field(?) "type"
-#
-# Fixes this traceback:
-#
-# Traceback (most recent call last):
-# File "roundup\cgi_client.py", line 535, in newnode
-# self._post_editnode(nid)
-# File "roundup\cgi_client.py", line 415, in _post_editnode
-# if type(value) != type([]): value = [value]
-# UnboundLocalError: local variable 'type' referenced before assignment
-#
-# Revision 1.61 2001/11/22 15:46:42 jhermann
-# Added module docstrings to all modules.
-#
-# Revision 1.60 2001/11/21 22:57:28 jhermann
-# Added dummy hooks for I18N and some preliminary (test) markup of
-# translatable messages
-#
-# Revision 1.59 2001/11/21 03:21:13 richard
-# oops
-#
-# Revision 1.58 2001/11/21 03:11:28 richard
-# Better handling of new properties.
-#
-# Revision 1.57 2001/11/15 10:24:27 richard
-# handle the case where there is no file attached
-#
-# Revision 1.56 2001/11/14 21:35:21 richard
-# . users may attach files to issues (and support in ext) through the web now
-#
-# Revision 1.55 2001/11/07 02:34:06 jhermann
-# Handling of damaged login cookies
-#
-# Revision 1.54 2001/11/07 01:16:12 richard
-# Remove the '=' padding from cookie value so quoting isn't an issue.
-#
-# Revision 1.53 2001/11/06 23:22:05 jhermann
-# More IE fixes: it does not like quotes around cookie values; in the
-# hope this does not break anything for other browser; if it does, we
-# need to check HTTP_USER_AGENT
-#
-# Revision 1.52 2001/11/06 23:11:22 jhermann
-# Fixed debug output in page footer; added expiry date to the login cookie
-# (expires 1 year in the future) to prevent probs with certain versions
-# of IE
-#
-# Revision 1.51 2001/11/06 22:00:34 jhermann
-# Get debug level from ROUNDUP_DEBUG env var
-#
-# Revision 1.50 2001/11/05 23:45:40 richard
-# Fixed newuser_action so it sets the cookie with the unencrypted password.
-# Also made it present nicer error messages (not tracebacks).
-#
-# Revision 1.49 2001/11/04 03:07:12 richard
-# Fixed various cookie-related bugs:
-# . bug #477685 ] base64.decodestring breaks
-# . bug #477837 ] lynx does not like the cookie
-# . bug #477892 ] Password edit doesn't fix login cookie
-# Also closed a security hole - a logged-in user could edit another user's
-# details.
-#
-# Revision 1.48 2001/11/03 01:30:18 richard
-# Oops. uses pagefoot now.
-#
-# Revision 1.47 2001/11/03 01:29:28 richard
-# Login page didn't have all close tags.
-#
-# Revision 1.46 2001/11/03 01:26:55 richard
-# possibly fix truncated base64'ed user:pass
-#
-# Revision 1.45 2001/11/01 22:04:37 richard
-# Started work on supporting a pop3-fetching server
-# Fixed bugs:
-# . bug #477104 ] HTML tag error in roundup-server
-# . bug #477107 ] HTTP header problem
-#
-# Revision 1.44 2001/10/28 23:03:08 richard
-# Added more useful header to the classic schema.
-#
-# Revision 1.43 2001/10/24 00:01:42 richard
-# More fixes to lockout logic.
-#
-# Revision 1.42 2001/10/23 23:56:03 richard
-# HTML typo
-#
-# Revision 1.41 2001/10/23 23:52:35 richard
-# Fixed lock-out logic, thanks Roch'e for pointing out the problems.
-#
-# Revision 1.40 2001/10/23 23:06:39 richard
-# Some cleanup.
-#
-# Revision 1.39 2001/10/23 01:00:18 richard
-# Re-enabled login and registration access after lopping them off via
-# disabling access for anonymous users.
-# Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
-# a couple of bugs while I was there. Probably introduced a couple, but
-# things seem to work OK at the moment.
-#
-# Revision 1.38 2001/10/22 03:25:01 richard
-# Added configuration for:
-# . anonymous user access and registration (deny/allow)
-# . filter "widget" location on index page (top, bottom, both)
-# Updated some documentation.
-#
-# Revision 1.37 2001/10/21 07:26:35 richard
-# feature #473127: Filenames. I modified the file.index and htmltemplate
-# source so that the filename is used in the link and the creation
-# information is displayed.
-#
-# Revision 1.36 2001/10/21 04:44:50 richard
-# bug #473124: UI inconsistency with Link fields.
-# This also prompted me to fix a fairly long-standing usability issue -
-# that of being able to turn off certain filters.
-#
-# Revision 1.35 2001/10/21 00:17:54 richard
-# CGI interface view customisation section may now be hidden (patch from
-# Roch'e Compaan.)
-#
-# Revision 1.34 2001/10/20 11:58:48 richard
-# Catch errors in login - no username or password supplied.
-# Fixed editing of password (Password property type) thanks Roch'e Compaan.
-#
-# Revision 1.33 2001/10/17 00:18:41 richard
-# Manually constructing cookie headers now.
-#
-# Revision 1.32 2001/10/16 03:36:21 richard
-# CGI interface wasn't handling checkboxes at all.
-#
-# Revision 1.31 2001/10/14 10:55:00 richard
-# Handle empty strings in HTML template Link function
-#
-# Revision 1.30 2001/10/09 07:38:58 richard
-# Pushed the base code for the extended schema CGI interface back into the
-# code cgi_client module so that future updates will be less painful.
-# Also removed a debugging print statement from cgi_client.
-#
-# Revision 1.29 2001/10/09 07:25:59 richard
-# Added the Password property type. See "pydoc roundup.password" for
-# implementation details. Have updated some of the documentation too.
-#
-# Revision 1.28 2001/10/08 00:34:31 richard
-# Change message was stuffing up for multilinks with no key property.
-#
-# Revision 1.27 2001/10/05 02:23:24 richard
-# . roundup-admin create now prompts for property info if none is supplied
-# on the command-line.
-# . hyperdb Class getprops() method may now return only the mutable
-# properties.
-# . Login now uses cookies, which makes it a whole lot more flexible. We can
-# now support anonymous user access (read-only, unless there's an
-# "anonymous" user, in which case write access is permitted). Login
-# handling has been moved into cgi_client.Client.main()
-# . The "extended" schema is now the default in roundup init.
-# . The schemas have had their page headings modified to cope with the new
-# login handling. Existing installations should copy the interfaces.py
-# file from the roundup lib directory to their instance home.
-# . Incorrectly had a Bizar Software copyright on the cgitb.py module from
-# Ping - has been removed.
-# . Fixed a whole bunch of places in the CGI interface where we should have
-# been returning Not Found instead of throwing an exception.
-# . Fixed a deviation from the spec: trying to modify the 'id' property of
-# an item now throws an exception.
-#
-# Revision 1.26 2001/09/12 08:31:42 richard
-# handle cases where mime type is not guessable
-#
-# Revision 1.25 2001/08/29 05:30:49 richard
-# change messages weren't being saved when there was no-one on the nosy list.
-#
-# Revision 1.24 2001/08/29 04:49:39 richard
-# didn't clean up fully after debugging :(
-#
-# Revision 1.23 2001/08/29 04:47:18 richard
-# Fixed CGI client change messages so they actually include the properties
-# changed (again).
-#
-# Revision 1.22 2001/08/17 00:08:10 richard
-# reverted back to sending messages always regardless of who is doing the web
-# edit. change notes weren't being saved. bleah. hackish.
-#
-# Revision 1.21 2001/08/15 23:43:18 richard
-# Fixed some isFooTypes that I missed.
-# Refactored some code in the CGI code.
-#
-# Revision 1.20 2001/08/12 06:32:36 richard
-# using isinstance(blah, Foo) now instead of isFooType
-#
-# Revision 1.19 2001/08/07 00:24:42 richard
-# stupid typo
-#
-# Revision 1.18 2001/08/07 00:15:51 richard
-# Added the copyright/license notice to (nearly) all files at request of
-# Bizar Software.
-#
-# Revision 1.17 2001/08/02 06:38:17 richard
-# Roundupdb now appends "mailing list" information to its messages which
-# include the e-mail address and web interface address. Templates may
-# override this in their db classes to include specific information (support
-# instructions, etc).
-#
-# Revision 1.16 2001/08/02 05:55:25 richard
-# Web edit messages aren't sent to the person who did the edit any more. No
-# message is generated if they are the only person on the nosy list.
-#
-# Revision 1.15 2001/08/02 00:34:10 richard
-# bleah syntax error
-#
-# Revision 1.14 2001/08/02 00:26:16 richard
-# Changed the order of the information in the message generated by web edits.
-#
-# Revision 1.13 2001/07/30 08:12:17 richard
-# Added time logging and file uploading to the templates.
-#
-# Revision 1.12 2001/07/30 06:26:31 richard
-# Added some documentation on how the newblah works.
-#
-# Revision 1.11 2001/07/30 06:17:45 richard
-# Features:
-# . Added ability for cgi newblah forms to indicate that the new node
-# should be linked somewhere.
-# Fixed:
-# . Fixed the agument handling for the roundup-admin find command.
-# . Fixed handling of summary when no note supplied for newblah. Again.
-# . Fixed detection of no form in htmltemplate Field display.
-#
-# Revision 1.10 2001/07/30 02:37:34 richard
-# Temporary measure until we have decent schema migration...
-#
-# Revision 1.9 2001/07/30 01:25:07 richard
-# Default implementation is now "classic" rather than "extended" as one would
-# expect.
-#
-# Revision 1.8 2001/07/29 08:27:40 richard
-# Fixed handling of passed-in values in form elements (ie. during a
-# drill-down)
-#
-# Revision 1.7 2001/07/29 07:01:39 richard
-# Added vim command to all source so that we don't get no steenkin' tabs :)
-#
-# Revision 1.6 2001/07/29 04:04:00 richard
-# Moved some code around allowing for subclassing to change behaviour.
-#
-# Revision 1.5 2001/07/28 08:16:52 richard
-# New issue form handles lack of note better now.
-#
-# Revision 1.4 2001/07/28 00:34:34 richard
-# Fixed some non-string node ids.
-#
-# Revision 1.3 2001/07/23 03:56:30 richard
-# oops, missed a config removal
-#
-# Revision 1.2 2001/07/22 12:09:32 richard
-# Final commit of Grande Splite
-#
-# Revision 1.1 2001/07/22 11:58:35 richard
-# More Grande Splite
-#
-#
-# vim: set filetype=python ts=4 sw=4 et si
diff --git a/roundup/cgitb.py b/roundup/cgitb.py
--- a/roundup/cgitb.py
+++ /dev/null
@@ -1,157 +0,0 @@
-#
-# This module was written by Ka-Ping Yee, <ping@lfw.org>.
-#
-# $Id: cgitb.py,v 1.10 2002-01-16 04:49:45 richard Exp $
-
-__doc__ = """
-Extended CGI traceback handler by Ka-Ping Yee, <ping@lfw.org>.
-"""
-
-import sys, os, types, string, keyword, linecache, tokenize, inspect, pydoc
-
-from i18n import _
-
-def breaker():
- return ('<body bgcolor="#f0f0ff">' +
- '<font color="#f0f0ff" size="-5"> > </font> ' +
- '</table>' * 5)
-
-def html(context=5):
- etype, evalue = sys.exc_type, sys.exc_value
- if type(etype) is types.ClassType:
- etype = etype.__name__
- pyver = 'Python ' + string.split(sys.version)[0] + '<br>' + sys.executable
- head = pydoc.html.heading(
- '<font size=+1><strong>%s</strong>: %s</font>'%(str(etype), str(evalue)),
- '#ffffff', '#aa55cc', pyver)
-
- head = head + (_('<p>A problem occurred while running a Python script. '
- 'Here is the sequence of function calls leading up to '
- 'the error, with the most recent (innermost) call first. '
- 'The exception attributes are:'))
-
- indent = '<tt><small>%s</small> </tt>' % (' ' * 5)
- traceback = []
- for frame, file, lnum, func, lines, index in inspect.trace(context):
- if file is None:
- link = '<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>'
- else:
- file = os.path.abspath(file)
- link = '<a href="file:%s">%s</a>' % (file, pydoc.html.escape(file))
- args, varargs, varkw, locals = inspect.getargvalues(frame)
- if func == '?':
- call = ''
- else:
- call = 'in <strong>%s</strong>' % func + inspect.formatargvalues(
- args, varargs, varkw, locals,
- formatvalue=lambda value: '=' + pydoc.html.repr(value))
-
- level = '''
-<table width="100%%" bgcolor="#d8bbff" cellspacing=0 cellpadding=2 border=0>
-<tr><td>%s %s</td></tr></table>''' % (link, call)
-
- if index is None or file is None:
- traceback.append('<p>' + level)
- continue
-
- # do a fil inspection
- names = []
- def tokeneater(type, token, start, end, line, names=names):
- if type == tokenize.NAME and token not in keyword.kwlist:
- if token not in names:
- names.append(token)
- if type == tokenize.NEWLINE: raise IndexError
- def linereader(file=file, lnum=[lnum]):
- line = linecache.getline(file, lnum[0])
- lnum[0] = lnum[0] + 1
- return line
-
- try:
- tokenize.tokenize(linereader, tokeneater)
- except IndexError: pass
- lvals = []
- for name in names:
- if name in frame.f_code.co_varnames:
- if locals.has_key(name):
- value = pydoc.html.repr(locals[name])
- else:
- value = _('<em>undefined</em>')
- name = '<strong>%s</strong>' % name
- else:
- if frame.f_globals.has_key(name):
- value = pydoc.html.repr(frame.f_globals[name])
- else:
- value = _('<em>undefined</em>')
- name = '<em>global</em> <strong>%s</strong>' % name
- lvals.append('%s = %s' % (name, value))
- if lvals:
- lvals = string.join(lvals, ', ')
- lvals = indent + '''
-<small><font color="#909090">%s</font></small><br>''' % lvals
- else:
- lvals = ''
-
- excerpt = []
- i = lnum - index
- for line in lines:
- number = ' ' * (5-len(str(i))) + str(i)
- number = '<small><font color="#909090">%s</font></small>' % number
- line = '<tt>%s %s</tt>' % (number, pydoc.html.preformat(line))
- if i == lnum:
- line = '''
-<table width="100%%" bgcolor="#ffccee" cellspacing=0 cellpadding=0 border=0>
-<tr><td>%s</td></tr></table>''' % line
- excerpt.append('\n' + line)
- if i == lnum:
- excerpt.append(lvals)
- i = i + 1
- traceback.append('<p>' + level + string.join(excerpt, '\n'))
-
- traceback.reverse()
-
- exception = '<p><strong>%s</strong>: %s' % (str(etype), str(evalue))
- attribs = []
- if type(evalue) is types.InstanceType:
- for name in dir(evalue):
- value = pydoc.html.repr(getattr(evalue, name))
- attribs.append('<br>%s%s = %s' % (indent, name, value))
-
- return head + string.join(attribs) + string.join(traceback) + '<p> </p>'
-
-def handler():
- print breaker()
- print html()
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.9 2002/01/08 11:56:24 richard
-# missed an import _
-#
-# Revision 1.8 2002/01/05 02:22:32 richard
-# i18n'ification
-#
-# Revision 1.7 2001/11/22 15:46:42 jhermann
-# Added module docstrings to all modules.
-#
-# Revision 1.6 2001/09/29 13:27:00 richard
-# CGI interfaces now spit up a top-level index of all the instances they can
-# serve.
-#
-# Revision 1.5 2001/08/07 00:24:42 richard
-# stupid typo
-#
-# Revision 1.4 2001/08/07 00:15:51 richard
-# Added the copyright/license notice to (nearly) all files at request of
-# Bizar Software.
-#
-# Revision 1.3 2001/07/29 07:01:39 richard
-# Added vim command to all source so that we don't get no steenkin' tabs :)
-#
-# Revision 1.2 2001/07/22 12:09:32 richard
-# Final commit of Grande Splite
-#
-# Revision 1.1 2001/07/22 11:58:35 richard
-# More Grande Splite
-#
-#
-# vim: set filetype=python ts=4 sw=4 et si
diff --git a/roundup/template_funcs.py b/roundup/template_funcs.py
+++ /dev/null
@@ -1,826 +0,0 @@
-#
-# $Id: template_funcs.py,v 1.3 2002-08-19 00:22:47 richard Exp $
-#
-import hyperdb, date, password
-from i18n import _
-import htmltemplate
-import cgi, os, StringIO, urllib, types
-
-def do_plain(client, classname, cl, props, nodeid, filterspec, property,
- escape=0, lookup=1):
- ''' display a String property directly;
-
- display a Date property in a specified time zone with an option to
- omit the time from the date stamp;
-
- for a Link or Multilink property, display the key strings of the
- linked nodes (or the ids if the linked class has no key property)
- when the lookup argument is true, otherwise just return the
- linked ids
- '''
- if not nodeid and client.form is None:
- return _('[Field: not called from item]')
- propclass = props[property]
- value = determine_value(cl, props, nodeid, filterspec, property)
-
- if isinstance(propclass, hyperdb.Password):
- value = _('*encrypted*')
- elif isinstance(propclass, hyperdb.Boolean):
- value = value and "Yes" or "No"
- elif isinstance(propclass, hyperdb.Link):
- if value:
- if lookup:
- linkcl = client.db.classes[propclass.classname]
- k = linkcl.labelprop(1)
- value = linkcl.get(value, k)
- else:
- value = _('[unselected]')
- elif isinstance(propclass, hyperdb.Multilink):
- if value:
- if lookup:
- linkcl = client.db.classes[propclass.classname]
- k = linkcl.labelprop(1)
- labels = []
- for v in value:
- labels.append(linkcl.get(v, k))
- value = ', '.join(labels)
- else:
- value = ', '.join(value)
- else:
- value = ''
- else:
- value = str(value)
-
- if escape:
- value = cgi.escape(value)
- return value
-
-def do_stext(client, classname, cl, props, nodeid, filterspec, property,
- escape=0):
- '''Render as structured text using the StructuredText module
- (see above for details)
- '''
- s = do_plain(client, classname, cl, props, nodeid, filterspec, property,
- escape=escape)
- if not StructuredText:
- return s
- return StructuredText(s,level=1,header=0)
-
-def determine_value(cl, props, nodeid, filterspec, property):
- '''determine the value of a property using the node, form or
- filterspec
- '''
- if nodeid:
- value = cl.get(nodeid, property, None)
- if value is None:
- if isinstance(props[property], hyperdb.Multilink):
- return []
- return ''
- return value
- elif filterspec is not None:
- if isinstance(props[property], hyperdb.Multilink):
- return filterspec.get(property, [])
- else:
- return filterspec.get(property, '')
- # TODO: pull the value from the form
- if isinstance(props[property], hyperdb.Multilink):
- return []
- else:
- return ''
-
-def make_sort_function(client, filterspec, classname):
- '''Make a sort function for a given class
- '''
- linkcl = client.db.getclass(classname)
- if linkcl.getprops().has_key('order'):
- sort_on = 'order'
- else:
- sort_on = linkcl.labelprop()
- def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
- return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
- return sortfunc
-
-def do_field(client, classname, cl, props, nodeid, filterspec, property,
- size=None, showid=0):
- ''' display a property like the plain displayer, but in a text field
- to be edited
-
- Note: if you would prefer an option list style display for
- link or multilink editing, use menu().
- '''
- if not nodeid and client.form is None and filterspec is None:
- return _('[Field: not called from item]')
- if size is None:
- size = 30
-
- propclass = props[property]
-
- # get the value
- value = determine_value(cl, props, nodeid, filterspec, property)
- # now display
- if (isinstance(propclass, hyperdb.String) or
- isinstance(propclass, hyperdb.Date) or
- isinstance(propclass, hyperdb.Interval)):
- if value is None:
- value = ''
- else:
- value = cgi.escape(str(value))
- value = '"'.join(value.split('"'))
- s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
- elif isinstance(propclass, hyperdb.Boolean):
- checked = value and "checked" or ""
- s = '<input type="radio" name="%s" value="yes" %s>Yes'%(property,
- checked)
- if checked:
- checked = ""
- else:
- checked = "checked"
- s += '<input type="radio" name="%s" value="no" %s>No'%(property,
- checked)
- elif isinstance(propclass, hyperdb.Number):
- s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
- elif isinstance(propclass, hyperdb.Password):
- s = '<input type="password" name="%s" size="%s">'%(property, size)
- elif isinstance(propclass, hyperdb.Link):
- linkcl = client.db.getclass(propclass.classname)
- if linkcl.getprops().has_key('order'):
- sort_on = 'order'
- else:
- sort_on = linkcl.labelprop()
- options = linkcl.filter(None, {}, [sort_on], [])
- # TODO: make this a field display, not a menu one!
- l = ['<select name="%s">'%property]
- k = linkcl.labelprop(1)
- if value is None:
- s = 'selected '
- else:
- s = ''
- l.append(_('<option %svalue="-1">- no selection -</option>')%s)
- for optionid in options:
- option = linkcl.get(optionid, k)
- s = ''
- if optionid == value:
- s = 'selected '
- if showid:
- lab = '%s%s: %s'%(propclass.classname, optionid, option)
- else:
- lab = option
- if size is not None and len(lab) > size:
- lab = lab[:size-3] + '...'
- lab = cgi.escape(lab)
- l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
- l.append('</select>')
- s = '\n'.join(l)
- elif isinstance(propclass, hyperdb.Multilink):
- sortfunc = make_sort_function(client, filterspec, propclass.classname)
- linkcl = client.db.getclass(propclass.classname)
- if value:
- value.sort(sortfunc)
- # map the id to the label property
- if not showid:
- k = linkcl.labelprop(1)
- value = [linkcl.get(v, k) for v in value]
- value = cgi.escape(','.join(value))
- s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
- else:
- s = _('Plain: bad propclass "%(propclass)s"')%locals()
- return s
-
-def do_multiline(client, classname, cl, props, nodeid, filterspec, property,
- rows=5, cols=40):
- ''' display a string property in a multiline text edit field
- '''
- if not nodeid and client.form is None and filterspec is None:
- return _('[Multiline: not called from item]')
-
- propclass = props[property]
-
- # make sure this is a link property
- if not isinstance(propclass, hyperdb.String):
- return _('[Multiline: not a string]')
-
- # get the value
- value = determine_value(cl, props, nodeid, filterspec, property)
- if value is None:
- value = ''
-
- # display
- return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
- property, rows, cols, value)
-
-def do_menu(client, classname, cl, props, nodeid, filterspec, property,
- size=None, height=None, showid=0, additional=[], **conditions):
- ''' For a Link/Multilink property, display a menu of the available
- choices
-
- If the additional properties are specified, they will be
- included in the text of each option in (brackets, with, commas).
- '''
- if not nodeid and client.form is None and filterspec is None:
- return _('[Field: not called from item]')
-
- propclass = props[property]
-
- # make sure this is a link property
- if not (isinstance(propclass, hyperdb.Link) or
- isinstance(propclass, hyperdb.Multilink)):
- return _('[Menu: not a link]')
-
- # sort function
- sortfunc = make_sort_function(client, filterspec, propclass.classname)
-
- # get the value
- value = determine_value(cl, props, nodeid, filterspec, property)
-
- # display
- if isinstance(propclass, hyperdb.Multilink):
- linkcl = client.db.getclass(propclass.classname)
- if linkcl.getprops().has_key('order'):
- sort_on = 'order'
- else:
- sort_on = linkcl.labelprop()
- options = linkcl.filter(None, conditions, [sort_on], [])
- height = height or min(len(options), 7)
- l = ['<select multiple name="%s" size="%s">'%(property, height)]
- k = linkcl.labelprop(1)
- for optionid in options:
- option = linkcl.get(optionid, k)
- s = ''
- if optionid in value or option in value:
- s = 'selected '
- if showid:
- lab = '%s%s: %s'%(propclass.classname, optionid, option)
- else:
- lab = option
- if size is not None and len(lab) > size:
- lab = lab[:size-3] + '...'
- if additional:
- m = []
- for propname in additional:
- m.append(linkcl.get(optionid, propname))
- lab = lab + ' (%s)'%', '.join(m)
- lab = cgi.escape(lab)
- l.append('<option %svalue="%s">%s</option>'%(s, optionid,
- lab))
- l.append('</select>')
- return '\n'.join(l)
- if isinstance(propclass, hyperdb.Link):
- # force the value to be a single choice
- if type(value) is types.ListType:
- value = value[0]
- linkcl = client.db.getclass(propclass.classname)
- l = ['<select name="%s">'%property]
- k = linkcl.labelprop(1)
- s = ''
- if value is None:
- s = 'selected '
- l.append(_('<option %svalue="-1">- no selection -</option>')%s)
- if linkcl.getprops().has_key('order'):
- sort_on = 'order'
- else:
- sort_on = linkcl.labelprop()
- options = linkcl.filter(None, conditions, [sort_on], [])
- for optionid in options:
- option = linkcl.get(optionid, k)
- s = ''
- if value in [optionid, option]:
- s = 'selected '
- if showid:
- lab = '%s%s: %s'%(propclass.classname, optionid, option)
- else:
- lab = option
- if size is not None and len(lab) > size:
- lab = lab[:size-3] + '...'
- if additional:
- m = []
- for propname in additional:
- m.append(linkcl.get(optionid, propname))
- lab = lab + ' (%s)'%', '.join(map(str, m))
- lab = cgi.escape(lab)
- l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
- l.append('</select>')
- return '\n'.join(l)
- return _('[Menu: not a link]')
-
-#XXX deviates from spec
-def do_link(client, classname, cl, props, nodeid, filterspec, property=None,
- is_download=0, showid=0):
- '''For a Link or Multilink property, display the names of the linked
- nodes, hyperlinked to the item views on those nodes.
- For other properties, link to this node with the property as the
- text.
-
- If is_download is true, append the property value to the generated
- URL so that the link may be used as a download link and the
- downloaded file name is correct.
- '''
- if not nodeid and client.form is None:
- return _('[Link: not called from item]')
-
- # get the value
- value = determine_value(cl, props, nodeid, filterspec, property)
- propclass = props[property]
- if isinstance(propclass, hyperdb.Boolean):
- value = value and "Yes" or "No"
- elif isinstance(propclass, hyperdb.Link):
- if value in ('', None, []):
- return _('[no %(propname)s]')%{'propname':property.capitalize()}
- linkname = propclass.classname
- linkcl = client.db.getclass(linkname)
- k = linkcl.labelprop(1)
- linkvalue = cgi.escape(str(linkcl.get(value, k)))
- if showid:
- label = value
- title = ' title="%s"'%linkvalue
- # note ... this should be urllib.quote(linkcl.get(value, k))
- else:
- label = linkvalue
- title = ''
- if is_download:
- return '<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
- linkvalue, title, label)
- else:
- return '<a href="%s%s"%s>%s</a>'%(linkname, value, title, label)
- elif isinstance(propclass, hyperdb.Multilink):
- if value in ('', None, []):
- return _('[no %(propname)s]')%{'propname':property.capitalize()}
- linkname = propclass.classname
- linkcl = client.db.getclass(linkname)
- k = linkcl.labelprop(1)
- l = []
- for value in value:
- linkvalue = cgi.escape(str(linkcl.get(value, k)))
- if showid:
- label = value
- title = ' title="%s"'%linkvalue
- # note ... this should be urllib.quote(linkcl.get(value, k))
- else:
- label = linkvalue
- title = ''
- if is_download:
- l.append('<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
- linkvalue, title, label))
- else:
- l.append('<a href="%s%s"%s>%s</a>'%(linkname, value,
- title, label))
- return ', '.join(l)
- if is_download:
- if value in ('', None, []):
- return _('[no %(propname)s]')%{'propname':property.capitalize()}
- return '<a href="%s%s/%s">%s</a>'%(classname, nodeid,
- value, value)
- else:
- if value in ('', None, []):
- value = _('[no %(propname)s]')%{'propname':property.capitalize()}
- return '<a href="%s%s">%s</a>'%(classname, nodeid, value)
-
-def do_count(client, classname, cl, props, nodeid, filterspec, property,
- **args):
- ''' for a Multilink property, display a count of the number of links in
- the list
- '''
- if not nodeid:
- return _('[Count: not called from item]')
-
- propclass = props[property]
- if not isinstance(propclass, hyperdb.Multilink):
- return _('[Count: not a Multilink]')
-
- # figure the length then...
- value = cl.get(nodeid, property)
- return str(len(value))
-
-# XXX pretty is definitely new ;)
-def do_reldate(client, classname, cl, props, nodeid, filterspec, property,
- pretty=0):
- ''' display a Date property in terms of an interval relative to the
- current date (e.g. "+ 3w", "- 2d").
-
- with the 'pretty' flag, make it pretty
- '''
- if not nodeid and client.form is None:
- return _('[Reldate: not called from item]')
-
- propclass = props[property]
- if not isinstance(propclass, hyperdb.Date):
- return _('[Reldate: not a Date]')
-
- if nodeid:
- value = cl.get(nodeid, property)
- else:
- return ''
- if not value:
- return ''
-
- # figure the interval
- interval = date.Date('.') - value
- if pretty:
- if not nodeid:
- return _('now')
- return interval.pretty()
- return str(interval)
-
-def do_download(client, classname, cl, props, nodeid, filterspec, property,
- **args):
- ''' show a Link("file") or Multilink("file") property using links that
- allow you to download files
- '''
- if not nodeid:
- return _('[Download: not called from item]')
- return do_link(client, classname, cl, props, nodeid, filterspec, property,
- is_download=1)
-
-def do_checklist(client, classname, cl, props, nodeid, filterspec, property,
- sortby=None):
- ''' for a Link or Multilink property, display checkboxes for the
- available choices to permit filtering
-
- sort the checklist by the argument (+/- property name)
- '''
- propclass = props[property]
- if (not isinstance(propclass, hyperdb.Link) and not
- isinstance(propclass, hyperdb.Multilink)):
- return _('[Checklist: not a link]')
-
- # get our current checkbox state
- if nodeid:
- # get the info from the node - make sure it's a list
- if isinstance(propclass, hyperdb.Link):
- value = [cl.get(nodeid, property)]
- else:
- value = cl.get(nodeid, property)
- elif filterspec is not None:
- # get the state from the filter specification (always a list)
- value = filterspec.get(property, [])
- else:
- # it's a new node, so there's no state
- value = []
-
- # so we can map to the linked node's "lable" property
- linkcl = client.db.getclass(propclass.classname)
- l = []
- k = linkcl.labelprop(1)
-
- # build list of options and then sort it, either
- # by id + label or <sortby>-value + label;
- # a minus reverses the sort order, while + or no
- # prefix sort in increasing order
- reversed = 0
- if sortby:
- if sortby[0] == '-':
- reversed = 1
- sortby = sortby[1:]
- elif sortby[0] == '+':
- sortby = sortby[1:]
- options = []
- for optionid in linkcl.list():
- if sortby:
- sortval = linkcl.get(optionid, sortby)
- else:
- sortval = int(optionid)
- option = cgi.escape(str(linkcl.get(optionid, k)))
- options.append((sortval, option, optionid))
- options.sort()
- if reversed:
- options.reverse()
-
- # build checkboxes
- for sortval, option, optionid in options:
- if optionid in value or option in value:
- checked = 'checked'
- else:
- checked = ''
- l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
- option, checked, property, option))
-
- # for Links, allow the "unselected" option too
- if isinstance(propclass, hyperdb.Link):
- if value is None or '-1' in value:
- checked = 'checked'
- else:
- checked = ''
- l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
- 'value="-1">')%(checked, property))
- return '\n'.join(l)
-
-def do_note(client, classname, cl, props, nodeid, filterspec, rows=5, cols=80):
- ''' display a "note" field, which is a text area for entering a note to
- go along with a change.
- '''
- # TODO: pull the value from the form
- return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
- '</textarea>'%(rows, cols)
-
-# XXX new function
-def do_list(client, classname, cl, props, nodeid, filterspec, property,
- reverse=0, xtracols=None):
- ''' list the items specified by property using the standard index for
- the class
- '''
- propcl = props[property]
- if not isinstance(propcl, hyperdb.Multilink):
- return _('[List: not a Multilink]')
-
- value = determine_value(cl, props, nodeid, filterspec, property)
- if not value:
- return ''
-
- # sort, possibly revers and then re-stringify
- value = map(int, value)
- value.sort()
- if reverse:
- value.reverse()
- value = map(str, value)
-
- # render the sub-index into a string
- fp = StringIO.StringIO()
- try:
- write_save = client.write
- client.write = fp.write
- client.listcontext = ('%s%s' % (classname, nodeid), property)
- index = htmltemplate.IndexTemplate(client, client.instance.TEMPLATES,
- propcl.classname)
- index.render(nodeids=value, show_display_form=0, xtracols=xtracols)
- finally:
- client.listcontext = None
- client.write = write_save
-
- return fp.getvalue()
-
-# XXX new function
-def do_history(client, classname, cl, props, nodeid, filterspec,
- direction='descending'):
- ''' list the history of the item
-
- If "direction" is 'descending' then the most recent event will
- be displayed first. If it is 'ascending' then the oldest event
- will be displayed first.
- '''
- if nodeid is None:
- return _("[History: node doesn't exist]")
-
- l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
- '<tr class="list-header">',
- _('<th align=left><span class="list-item">Date</span></th>'),
- _('<th align=left><span class="list-item">User</span></th>'),
- _('<th align=left><span class="list-item">Action</span></th>'),
- _('<th align=left><span class="list-item">Args</span></th>'),
- '</tr>']
- comments = {}
- history = cl.history(nodeid)
- history.sort()
- if direction == 'descending':
- history.reverse()
- for id, evt_date, user, action, args in history:
- date_s = str(evt_date).replace("."," ")
- arg_s = ''
- if action == 'link' and type(args) == type(()):
- if len(args) == 3:
- linkcl, linkid, key = args
- arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
- linkcl, linkid, key)
- else:
- arg_s = str(args)
-
- elif action == 'unlink' and type(args) == type(()):
- if len(args) == 3:
- linkcl, linkid, key = args
- arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
- linkcl, linkid, key)
- else:
- arg_s = str(args)
-
- elif type(args) == type({}):
- cell = []
- for k in args.keys():
- # try to get the relevant property and treat it
- # specially
- try:
- prop = props[k]
- except:
- prop = None
- if prop is not None:
- if args[k] and (isinstance(prop, hyperdb.Multilink) or
- isinstance(prop, hyperdb.Link)):
- # figure what the link class is
- classname = prop.classname
- try:
- linkcl = client.db.getclass(classname)
- except KeyError:
- labelprop = None
- comments[classname] = _('''The linked class
- %(classname)s no longer exists''')%locals()
- labelprop = linkcl.labelprop(1)
- hrefable = os.path.exists(
- os.path.join(client.instance.TEMPLATES, classname+'.item'))
-
- if isinstance(prop, hyperdb.Multilink) and \
- len(args[k]) > 0:
- ml = []
- for linkid in args[k]:
- if isinstance(linkid, type(())):
- sublabel = linkid[0] + ' '
- linkids = linkid[1]
- else:
- sublabel = ''
- linkids = [linkid]
- subml = []
- for linkid in linkids:
- label = classname + linkid
- # if we have a label property, try to use it
- # TODO: test for node existence even when
- # there's no labelprop!
- try:
- if labelprop is not None:
- label = linkcl.get(linkid, labelprop)
- except IndexError:
- comments['no_link'] = _('''<strike>The
- linked node no longer
- exists</strike>''')
- subml.append('<strike>%s</strike>'%label)
- else:
- if hrefable:
- subml.append('<a href="%s%s">%s</a>'%(
- classname, linkid, label))
- else:
- subml.append(label)
- ml.append(sublabel + ', '.join(subml))
- cell.append('%s:\n %s'%(k, ', '.join(ml)))
- elif isinstance(prop, hyperdb.Link) and args[k]:
- label = classname + args[k]
- # if we have a label property, try to use it
- # TODO: test for node existence even when
- # there's no labelprop!
- if labelprop is not None:
- try:
- label = linkcl.get(args[k], labelprop)
- except IndexError:
- comments['no_link'] = _('''<strike>The
- linked node no longer
- exists</strike>''')
- cell.append(' <strike>%s</strike>,\n'%label)
- # "flag" this is done .... euwww
- label = None
- if label is not None:
- if hrefable:
- cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
- classname, args[k], label))
- else:
- cell.append('%s: %s' % (k,label))
-
- elif isinstance(prop, hyperdb.Date) and args[k]:
- d = date.Date(args[k])
- cell.append('%s: %s'%(k, str(d)))
-
- elif isinstance(prop, hyperdb.Interval) and args[k]:
- d = date.Interval(args[k])
- cell.append('%s: %s'%(k, str(d)))
-
- elif isinstance(prop, hyperdb.String) and args[k]:
- cell.append('%s: %s'%(k, cgi.escape(args[k])))
-
- elif not args[k]:
- cell.append('%s: (no value)\n'%k)
-
- else:
- cell.append('%s: %s\n'%(k, str(args[k])))
- else:
- # property no longer exists
- comments['no_exist'] = _('''<em>The indicated property
- no longer exists</em>''')
- cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
- arg_s = '<br />'.join(cell)
- else:
- # unkown event!!
- comments['unknown'] = _('''<strong><em>This event is not
- handled by the history display!</em></strong>''')
- arg_s = '<strong><em>' + str(args) + '</em></strong>'
- date_s = date_s.replace(' ', ' ')
- l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
- '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
- user, action, arg_s))
- if comments:
- l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
- for entry in comments.values():
- l.append('<tr><td colspan=4>%s</td></tr>'%entry)
- l.append('</table>')
- return '\n'.join(l)
-
-# XXX new function
-def do_submit(client, classname, cl, props, nodeid, filterspec, value=None):
- ''' add a submit button for the item
- '''
- if value is None:
- if nodeid:
- value = "Submit Changes"
- else:
- value = "Submit New Entry"
- if nodeid or client.form is not None:
- return _('<input type="submit" name="submit" value="%s">' % value)
- else:
- return _('[Submit: not called from item]')
-
-def do_classhelp(client, classname, cl, props, nodeid, filterspec, clname,
- properties, label='?', width='400', height='400'):
- '''pop up a javascript window with class help
-
- This generates a link to a popup window which displays the
- properties indicated by "properties" of the class named by
- "classname". The "properties" should be a comma-separated list
- (eg. 'id,name,description').
-
- You may optionally override the label displayed, the width and
- height. The popup window will be resizable and scrollable.
- '''
- return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
- 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(clname,
- properties, width, height, label)
-
-def do_email(client, classname, cl, props, nodeid, filterspec, property,
- escape=0):
- '''display the property as one or more "fudged" email addrs
- '''
-
- if not nodeid and client.form is None:
- return _('[Email: not called from item]')
- propclass = props[property]
- if nodeid:
- # get the value for this property
- try:
- value = cl.get(nodeid, property)
- except KeyError:
- # a KeyError here means that the node doesn't have a value
- # for the specified property
- value = ''
- else:
- value = ''
- if isinstance(propclass, hyperdb.String):
- if value is None: value = ''
- else: value = str(value)
- value = value.replace('@', ' at ')
- value = value.replace('.', ' ')
- else:
- value = _('[Email: not a string]')%locals()
- if escape:
- value = cgi.escape(value)
- return value
-
-def do_filterspec(client, classname, cl, props, nodeid, filterspec, classprop,
- urlprop):
-
- qs = cl.get(nodeid, urlprop)
- classname = cl.get(nodeid, classprop)
- filterspec = {}
- query = cgi.parse_qs(qs)
- for k,v in query.items():
- query[k] = v[0].split(',')
- pagesize = query.get(':pagesize',['25'])[0]
- search_text = query.get('search_text', [''])[0]
- search_text = urllib.unquote(search_text)
- for k,v in query.items():
- if k[0] != ':':
- filterspec[k] = v
- ixtmplt = htmltemplate.IndexTemplate(client, client.instance.TEMPLATES,
- classname)
- qform = '<form onSubmit="return submit_once()" action="%s%s">\n'%(
- classname,nodeid)
- qform += ixtmplt.filter_form(search_text,
- query.get(':filter', []),
- query.get(':columns', []),
- query.get(':group', []),
- [],
- query.get(':sort',[]),
- filterspec,
- pagesize)
- return qform + '</table>\n'
-
-def do_href(client, classname, cl, props, nodeid, filterspec, property,
- prefix='', suffix='', label=''):
- ''' Generate a link to the value of the property, with the form:
-
- <a href="[prefix][value][suffix]">[label]</a>
-
- where the [value] is the specified property value.
- '''
- value = determine_value(cl, props, nodeid, filterspec, property)
- return '<a href="%s%s%s">%s</a>'%(prefix, value, suffix, label)
-
-def do_remove(client, classname, cl, props, nodeid, filterspec):
- ''' put a remove href for an item in a list '''
- if not nodeid:
- return _('[Remove not called from item]')
- try:
- parentdesignator, mlprop = client.listcontext
- except (AttributeError, TypeError):
- return _('[Remove not called form listing of multilink]')
- return '<a href="remove?:target=%s%s&:multilink=%s:%s">[Remove]</a>'%(
- classname, nodeid, parentdesignator, mlprop)
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.2 2002/08/15 00:40:10 richard
-# cleanup
-#
-#
-#
-# vim: set filetype=python ts=4 sw=4 et si
diff --git a/roundup/template_parser.py b/roundup/template_parser.py
+++ /dev/null
@@ -1,150 +0,0 @@
-import htmllib, formatter
-
-class Require:
- ''' Encapsulates a parsed <require attributes>...[<else>...]</require>
- '''
- def __init__(self, attributes):
- self.attributes = attributes
- self.current = self.ok = []
- self.fail = []
- def __len__(self):
- return len(self.current)
- def __getitem__(self, n):
- return self.current[n]
- def __setitem__(self, n, data):
- self.current[n] = data
- def append(self, data):
- self.current.append(data)
- def elseMode(self):
- self.current = self.fail
- def __repr__(self):
- return '<Require %r ok:%r fail:%r>'%(self.attributes, self.ok,
- self.fail)
-
-class Display:
- ''' Encapsulates a parsed <display attributes>
- '''
- def __init__(self, attributes):
- self.attributes = attributes
- def __repr__(self):
- return '<Display %r>'%self.attributes
-
-class Property:
- ''' Encapsulates a parsed <property attributes>
- '''
- def __init__(self, attributes):
- self.attributes = attributes
- self.current = self.ok = []
- def __len__(self):
- return len(self.current)
- def __getitem__(self, n):
- return self.current[n]
- def __setitem__(self, n, data):
- self.current[n] = data
- def append(self, data):
- self.current.append(data)
- def __repr__(self):
- return '<Property %r %r>'%(self.attributes, self.structure)
-
-class RoundupTemplate(htmllib.HTMLParser):
- ''' Parse Roundup's HTML template structure into a list of components:
-
- 'string': this is just plain data to be displayed
- Display : instances indicate that display functions are to be called
- Require : if/else style check using the conditions in the attributes,
- displaying the "ok" list of components or "fail" list
-
- '''
- def __init__(self):
- htmllib.HTMLParser.__init__(self, formatter.NullFormatter())
- self.current = self.structure = []
- self.stack = []
-
- def handle_data(self, data):
- self.append_data(data)
-
- def append_data(self, data):
- if self.current and isinstance(self.current[-1], type('')):
- self.current[-1] = self.current[-1] + data
- else:
- self.current.append(data)
-
- def unknown_starttag(self, tag, attributes):
- self.append_data('<%s' % tag)
- closeit = 1
- for name, value in attributes:
- pos = value.find('<')
- if pos > -1:
- self.append_data(' %s="%s' % (name, value[:pos]))
- closeit = 0
- else:
- self.append_data(' %s="%s"' % (name, value))
- if closeit:
- self.append_data('>')
-
- def handle_starttag(self, tag, method, attributes):
- if tag in ('require', 'else', 'display', 'property'):
- method(attributes)
- else:
- self.unknown_starttag(tag, attributes)
-
- def unknown_endtag(self, tag):
- if tag in ('require','property'):
- self.current = self.stack.pop()
- else:
- self.append_data('</%s>'%tag)
-
- def handle_endtag(self, tag, method):
- self.unknown_endtag(tag)
-
- def close(self):
- htmllib.HTMLParser.close(self)
-
- def do_display(self, attributes):
- self.current.append(Display(attributes))
-
- def do_property(self, attributes):
- p = Property(attributes)
- self.current.append(p)
- self.stack.append(self.current)
- self.current = p
-
- def do_require(self, attributes):
- r = Require(attributes)
- self.current.append(r)
- self.stack.append(self.current)
- self.current = r
-
- def do_else(self, attributes):
- self.current.elseMode()
-
- def __repr__(self):
- return '<RoundupTemplate %r>'%self.structure
-
-def display(structure, indent=''):
- ''' Pretty-print the parsed structure for debugging
- '''
- l = []
- for entry in structure:
- if isinstance(entry, type('')):
- l.append("%s%s"%(indent, entry))
- elif isinstance(entry, Require):
- l.append('%sTEST: %r\n'%(indent, entry.attributes))
- l.append('%sOK...'%indent)
- l.append(display(entry.ok, indent+' '))
- if entry.fail:
- l.append('%sFAIL...'%indent)
- l.append(display(entry.fail, indent+' '))
- elif isinstance(entry, Display):
- l.append('%sDISPLAY: %r'%(indent, entry.attributes))
- elif isinstance(entry, Property):
- l.append('%sPROPERTY: %r'%(indent, entry.attributes))
- l.append(display(entry.ok, indent+' '))
- return ''.join(l)
-
-if __name__ == '__main__':
- import sys
- parser = RoundupTemplate()
- parser.feed(open(sys.argv[1], 'r').read())
- print display(parser.structure)
-