From: richard Date: Fri, 30 Aug 2002 08:49:15 +0000 (+0000) Subject: removal of the old cgi stuff X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=45c37008181ca0743ed06b8de2a90832741c00de;p=roundup.git removal of the old cgi stuff git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1014 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py deleted file mode 100644 index 1f1c02d..0000000 --- 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 = ''' - -''' - 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 '%s'%(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 = _('
%(message)s
')%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 Issues'), - _('Unassigned Issues') - ] - - user_info = _('Login') - 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('%s'%(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 = _(''' -My Details | Logout -''')%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 ' - '%(cap_class)s')%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(_('Class List')) - links.append(_('User List')) - links.append(_('Add User')) - - # 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 ' - '%(cap_class)s')%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(_(''' -%(title)s - - -%(global_javascript)s - -%(message)s - - - - - - - - - -
%(title)s%(user_name)s
%(links)s%(user_info)s

-''')%locals()) - - def pagefoot(self): - if self.debug: - self.write(_('
Path
')) - self.write('
%s
'%(', '.join(map(repr, self.split_path)))) - keys = self.form.keys() - keys.sort() - if keys: - self.write(_('
Form entries
')) - for k in self.form.keys(): - v = self.form.getvalue(k, "") - if type(v) is type([]): - # Multiple username fields specified - v = "|".join(v) - self.write('
%s=%s
'%(k, cgi.escape(v))) - keys = self.headers_sent.keys() - keys.sort() - self.write(_('
Sent these HTTP headers
')) - for k in keys: - v = self.headers_sent[k] - self.write('
%s=%s
'%(k, cgi.escape(v))) - keys = self.env.keys() - keys.sort() - self.write(_('
CGI environment
')) - for k in keys: - v = self.env[k] - self.write('
%s=%s
'%(k, cgi.escape(v))) - self.write('
') - self.write('') - - 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('
\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.
\n' - 'Get it from: 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(_('''

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 ("").

-

Multilink properties have their multiple - values colon (":") separated (... ,"one:two:three", ...)

-

Remove entries by deleting their line. Add - new entries by appending - them to the table - put an X in the id column.

''')%{'classname':cn}) - - l = [] - for name in props: - l.append(name) - w('') - w(', '.join(l) + '\n') - w('') - - w('') - w('
')) - - 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('') - w('') - for name in props: - w(''%name) - w('') - for nodeid in cl.filter(None, {}, sort, []): - w('') - for name in props: - value = cgi.escape(str(cl.get(nodeid, name))) - w(''%value) - w('') - w('
%s
%s
') - - 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
%s' % (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 = '
%s
'%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 %s' % (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 = '
%s
'%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 = '
%s
'%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 %s' % (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 = '
%s
'%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 = '
%s
'%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('\n') - for cn in classnames: - cl = self.db.getclass(cn) - self.write(''%(cn, cn.capitalize())) - for key, value in cl.properties.items(): - if value is None: value = '' - else: value = str(value) - self.write(''%( - key, cgi.escape(value))) - self.write('
' - '%s
%s%s
') - 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('

%s

'%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('

%s

'%message) - self.write(_(''' - - - - - - - - - - - -''')%locals()) - userid = self.db.user.lookup(self.user) - if not self.db.security.hasPermission('Web Registration', userid): - self.write('
Existing User Login
Login name:
Password:
') - 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(_(''' -

-New User Registration -marked items are optional... -

- -Name: - -Organisation: - -E-Mail Address: - -Alternate E-mail Addresses: - -Phone: - -Preferred Login name: - -Password: - -Password Again: - - - -
- -''')%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('

%s

'%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 ] 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 -# 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 deleted file mode 100644 index d7837ca..0000000 --- a/roundup/cgitb.py +++ /dev/null @@ -1,157 +0,0 @@ -# -# This module was written by Ka-Ping Yee, . -# -# $Id: cgitb.py,v 1.10 2002-01-16 04:49:45 richard Exp $ - -__doc__ = """ -Extended CGI traceback handler by Ka-Ping Yee, . -""" - -import sys, os, types, string, keyword, linecache, tokenize, inspect, pydoc - -from i18n import _ - -def breaker(): - return ('' + - ' > ' + - '' * 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] + '
' + sys.executable - head = pydoc.html.heading( - '%s: %s'%(str(etype), str(evalue)), - '#ffffff', '#aa55cc', pyver) - - head = head + (_('

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 = '%s ' % (' ' * 5) - traceback = [] - for frame, file, lnum, func, lines, index in inspect.trace(context): - if file is None: - link = '<file is None - probably inside eval or exec>' - else: - file = os.path.abspath(file) - link = '%s' % (file, pydoc.html.escape(file)) - args, varargs, varkw, locals = inspect.getargvalues(frame) - if func == '?': - call = '' - else: - call = 'in %s' % func + inspect.formatargvalues( - args, varargs, varkw, locals, - formatvalue=lambda value: '=' + pydoc.html.repr(value)) - - level = ''' - -
%s %s
''' % (link, call) - - if index is None or file is None: - traceback.append('

' + 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 = _('undefined') - name = '%s' % name - else: - if frame.f_globals.has_key(name): - value = pydoc.html.repr(frame.f_globals[name]) - else: - value = _('undefined') - name = 'global %s' % name - lvals.append('%s = %s' % (name, value)) - if lvals: - lvals = string.join(lvals, ', ') - lvals = indent + ''' -%s
''' % lvals - else: - lvals = '' - - excerpt = [] - i = lnum - index - for line in lines: - number = ' ' * (5-len(str(i))) + str(i) - number = '%s' % number - line = '%s %s' % (number, pydoc.html.preformat(line)) - if i == lnum: - line = ''' - -
%s
''' % line - excerpt.append('\n' + line) - if i == lnum: - excerpt.append(lvals) - i = i + 1 - traceback.append('

' + level + string.join(excerpt, '\n')) - - traceback.reverse() - - exception = '

%s: %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('
%s%s = %s' % (indent, name, value)) - - return head + string.join(attribs) + string.join(traceback) + '

 

' - -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 deleted file mode 100755 index 65c4f69..0000000 --- a/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 = ''%(property, value, size) - elif isinstance(propclass, hyperdb.Boolean): - checked = value and "checked" or "" - s = 'Yes'%(property, - checked) - if checked: - checked = "" - else: - checked = "checked" - s += 'No'%(property, - checked) - elif isinstance(propclass, hyperdb.Number): - s = ''%(property, value, size) - elif isinstance(propclass, hyperdb.Password): - 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 = ['') - 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 = ''%(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 ''%( - 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 = ['') - 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 = ['') - 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 '%s'%(linkname, value, - linkvalue, title, label) - else: - return '%s'%(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('%s'%(linkname, value, - linkvalue, title, label)) - else: - l.append('%s'%(linkname, value, - title, label)) - return ', '.join(l) - if is_download: - if value in ('', None, []): - return _('[no %(propname)s]')%{'propname':property.capitalize()} - return '%s'%(classname, nodeid, - value, value) - else: - if value in ('', None, []): - value = _('[no %(propname)s]')%{'propname':property.capitalize()} - return '%s'%(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 -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:'%( - 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]:')%(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 ''%(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 = ['', - '', - _(''), - _(''), - _(''), - _(''), - ''] - 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 += '%s%s %s'%(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 += '%s%s %s'%(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'] = _('''The - linked node no longer - exists''') - subml.append('%s'%label) - else: - if hrefable: - subml.append('%s'%( - 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'] = _('''The - linked node no longer - exists''') - cell.append(' %s,\n'%label) - # "flag" this is done .... euwww - label = None - if label is not None: - if hrefable: - cell.append('%s: %s\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'] = _('''The indicated property - no longer exists''') - cell.append('%s: %s\n'%(k, str(args[k]))) - arg_s = '
'.join(cell) - else: - # unkown event!! - comments['unknown'] = _('''This event is not - handled by the history display!''') - arg_s = '' + str(args) + '' - date_s = date_s.replace(' ', ' ') - l.append('' - ''%(date_s, - user, action, arg_s)) - if comments: - l.append(_('')) - for entry in comments.values(): - l.append(''%entry) - l.append('
DateUserActionArgs
%s%s%s%s
Note:
%s
') - 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 _('' % 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 '(%s)'%(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 = '
\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 + '\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: - - [label] - - where the [value] is the specified property value. - ''' - value = determine_value(cl, props, nodeid, filterspec, property) - return '%s'%(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 '[Remove]'%( - 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 deleted file mode 100644 index e3e8ec4..0000000 --- a/roundup/template_parser.py +++ /dev/null @@ -1,150 +0,0 @@ -import htmllib, formatter - -class Require: - ''' Encapsulates a parsed ...[...] - ''' - 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 ''%(self.attributes, self.ok, - self.fail) - -class Display: - ''' Encapsulates a parsed - ''' - def __init__(self, attributes): - self.attributes = attributes - def __repr__(self): - return ''%self.attributes - -class Property: - ''' Encapsulates a parsed - ''' - 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 ''%(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(''%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 ''%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) -