From: richard Date: Tue, 23 Oct 2001 01:00:18 +0000 (+0000) Subject: Re-enabled login and registration access after lopping them off via X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=23f1d7d802e8806fb76c7d976d4a0efadcaec7cc;p=roundup.git 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. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@328 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/roundup-admin b/roundup-admin index 176ab71..88849aa 100755 --- a/roundup-admin +++ b/roundup-admin @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundup-admin,v 1.36 2001-10-21 00:45:15 richard Exp $ +# $Id: roundup-admin,v 1.37 2001-10-23 01:00:18 richard Exp $ import sys if int(sys.version[0]) < 2: @@ -35,13 +35,13 @@ class AdminTool: def __init__(self): self.commands = {} - for k, v in AdminTool.__dict__.items(): + for k in AdminTool.__dict__.keys(): if k[:3] == 'do_': - self.commands[k[3:]] = v + self.commands[k[3:]] = getattr(self, k) self.help = {} - for k, v in AdminTool.__dict__.items(): + for k in AdminTool.__dict__.keys(): if k[:5] == 'help_': - self.help[k[5:]] = v + self.help[k[5:]] = getattr(self, k) def usage(message=''): if message: message = 'Problem: '+message+'\n' @@ -133,7 +133,7 @@ Command help: ''' help = self.help.get(args[0], None) if help: - help(self) + help() return help = self.commands.get(args[0], None) if help: @@ -212,7 +212,11 @@ Command help: designators = string.split(args[1], ',') l = [] for designator in designators: - classname, nodeid = roundupdb.splitDesignator(designator) + try: + classname, nodeid = roundupdb.splitDesignator(designator) + except roundupdb.DesignatorError, message: + print 'Error: %s'%message + return 1 if self.comma_sep: l.append(self.db.getclass(classname).get(nodeid, propname)) else: @@ -236,7 +240,11 @@ Command help: key, value = prop.split('=') props[key] = value for designator in designators: - classname, nodeid = roundupdb.splitDesignator(designator) + try: + classname, nodeid = roundupdb.splitDesignator(designator) + except roundupdb.DesignatorError, message: + print 'Error: %s'%message + return 1 cl = self.db.getclass(classname) properties = cl.getprops() for key, value in props.items(): @@ -427,7 +435,11 @@ Command help: Lists the journal entries for the node identified by the designator. ''' - classname, nodeid = roundupdb.splitDesignator(args[0]) + try: + classname, nodeid = roundupdb.splitDesignator(args[0]) + except roundupdb.DesignatorError, message: + print 'Error: %s'%message + return 1 # TODO: handle the -c option? print self.db.getclass(classname).history(nodeid) return 0 @@ -441,7 +453,11 @@ Command help: ''' designators = string.split(args[0], ',') for designator in designators: - classname, nodeid = roundupdb.splitDesignator(designator) + try: + classname, nodeid = roundupdb.splitDesignator(designator) + except roundupdb.DesignatorError, message: + print 'Error: %s'%message + return 1 self.db.getclass(classname).retire(nodeid) return 0 @@ -610,7 +626,7 @@ Command help: # do the command try: - return function(self, args[1:]) + return function(args[1:]) finally: self.db.close() @@ -671,6 +687,9 @@ if __name__ == '__main__': # # $Log: not supported by cvs2svn $ +# Revision 1.36 2001/10/21 00:45:15 richard +# Added author identification to e-mail messages from roundup. +# # Revision 1.35 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. diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py index 88fc686..3cd9a0c 100644 --- a/roundup/cgi_client.py +++ b/roundup/cgi_client.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: cgi_client.py,v 1.38 2001-10-22 03:25:01 richard Exp $ +# $Id: cgi_client.py,v 1.39 2001-10-23 01:00:18 richard Exp $ import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes import base64, Cookie, time @@ -240,8 +240,8 @@ class Client: if show_customization is None: show_customization = self.customization_widget() - htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec, - filter, columns, sort, group, + index = htmltemplate.IndexTemplate(self, self.TEMPLATES, cn) + index.render(filterspec, filter, columns, sort, group, show_customization=show_customization) self.pagefoot() @@ -276,7 +276,9 @@ class Client: nodeid = self.nodeid # use the template to display the item - htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid) + item = htmltemplate.ItemTemplate(self, self.TEMPLATES, self.classname) + item.render(nodeid) + self.pagefoot() showissue = shownode showmsg = shownode @@ -433,8 +435,12 @@ class Client: traceback.print_exc(None, s) message = '
%s
'%cgi.escape(s.getvalue()) self.pagehead('New %s'%self.classname.capitalize(), message) - htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname, - self.form) + + # call the template + newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES, + self.classname) + newitem.render(self.form) + self.pagefoot() newissue = newnode newuser = newnode @@ -466,8 +472,9 @@ class Client: message = '
%s
'%cgi.escape(s.getvalue()) self.pagehead('New %s'%self.classname.capitalize(), message) - htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname, - self.form) + newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES, + self.classname) + newitem.render(self.form) self.pagefoot() def classes(self, message=None): @@ -541,6 +548,7 @@ class Client: password = self.form['__login_password'].value else: password = '' + print self.user, password # make sure the user exists try: uid = self.db.user.lookup(self.user) @@ -585,6 +593,10 @@ class Client: ''' create a new user based on the contents of the form and then set the cookie ''' + # re-open the database as "admin" + self.db.close() + self.db = self.instance.open('admin') + # TODO: pre-check the required fields and username key property cl = self.db.classes['user'] props, dummy = parsePropsFromForm(self.db, cl, self.form) @@ -626,10 +638,6 @@ class Client: self.user = user self.db.close() - # make sure totally anonymous access is OK - if self.ANONYMOUS_ACCESS == 'deny' and self.user is None: - return self.login() - # re-open the database for real, using the user self.db = self.instance.open(self.user) @@ -647,18 +655,28 @@ class Client: # appends the name of the file to the URL so the download file name # is correct, but doesn't actually use it. action = path[0] - if action == 'list_classes': - self.classes() - return - if action == 'login': - self.login() - return if action == 'login_action': self.login_action() return + + # make sure anonymous are allowed to register + if self.ANONYMOUS_REGISTER == 'deny' and self.user is None: + return self.login() + if action == 'newuser_action': self.newuser_action() return + + # make sure totally anonymous access is OK + if self.ANONYMOUS_ACCESS == 'deny' and self.user is None: + return self.login() + + if action == 'list_classes': + self.classes() + return + if action == 'login': + self.login() + return if action == 'logout': self.logout() return @@ -834,6 +852,12 @@ def parsePropsFromForm(db, cl, form, nodeid=0): # # $Log: not supported by cvs2svn $ +# 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 diff --git a/roundup/htmltemplate.py b/roundup/htmltemplate.py index 0a78595..fc0baa4 100644 --- a/roundup/htmltemplate.py +++ b/roundup/htmltemplate.py @@ -15,32 +15,31 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: htmltemplate.py,v 1.32 2001-10-22 03:25:01 richard Exp $ +# $Id: htmltemplate.py,v 1.33 2001-10-23 01:00:18 richard Exp $ import os, re, StringIO, urllib, cgi, errno import hyperdb, date, password -class Base: - def __init__(self, db, templates, classname, nodeid=None, form=None, - filterspec=None): - # TODO: really not happy with the way templates is passed on here - self.db, self.templates = db, templates - self.classname, self.nodeid = classname, nodeid - self.form, self.filterspec = form, filterspec - self.cl = self.db.classes[self.classname] - self.properties = self.cl.getprops() - -class Plain(Base): - ''' 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) - ''' - def __call__(self, property, escape=0): +class TemplateFunctions: + def __init__(self): + self.form = None + self.nodeid = None + self.filterspec = None + self.globals = {} + for key in TemplateFunctions.__dict__.keys(): + if key[:3] == 'do_': + self.globals[key[3:]] = getattr(self, key) + + def do_plain(self, property, escape=0): + ''' 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) + ''' if not self.nodeid and self.form is None: return '[Field: not called from item]' propclass = self.properties[property] @@ -75,11 +74,10 @@ class Plain(Base): return cgi.escape(value) return value -class Field(Base): - ''' display a property like the plain displayer, but in a text field - to be edited - ''' - def __call__(self, property, size=None, height=None, showid=0): + def do_field(self, property, size=None, height=None, showid=0): + ''' display a property like the plain displayer, but in a text field + to be edited + ''' if not self.nodeid and self.form is None and self.filterspec is None: return '[Field: not called from item]' propclass = self.properties[property] @@ -159,10 +157,9 @@ class Field(Base): s = 'Plain: bad propclass "%s"'%propclass return s -class Menu(Base): - ''' for a Link property, display a menu of the available choices - ''' - def __call__(self, property, size=None, height=None, showid=0): + def do_menu(self, property, size=None, height=None, showid=0): + ''' for a Link property, display a menu of the available choices + ''' propclass = self.properties[property] if self.nodeid: value = self.cl.get(self.nodeid, property) @@ -208,18 +205,17 @@ class Menu(Base): return '\n'.join(l) return '[Menu: not a link]' -#XXX deviates from spec -class Link(Base): - '''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. - ''' - def __call__(self, property=None, is_download=0): + #XXX deviates from spec + def do_link(self, property=None, is_download=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 self.nodeid and self.form is None: return '[Link: not called from item]' propclass = self.properties[property] @@ -263,11 +259,10 @@ class Link(Base): else: return '%s'%(self.classname, self.nodeid, value) -class Count(Base): - ''' for a Multilink property, display a count of the number of links in - the list - ''' - def __call__(self, property, **args): + def do_count(self, property, **args): + ''' for a Multilink property, display a count of the number of links in + the list + ''' if not self.nodeid: return '[Count: not called from item]' propclass = self.properties[property] @@ -276,14 +271,13 @@ class Count(Base): return str(len(value)) return '[Count: not a Multilink]' -# XXX pretty is definitely new ;) -class Reldate(Base): - ''' display a Date property in terms of an interval relative to the - current date (e.g. "+ 3w", "- 2d"). + # XXX pretty is definitely new ;) + def do_reldate(self, 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 - ''' - def __call__(self, property, pretty=0): + with the 'pretty' flag, make it pretty + ''' if not self.nodeid and self.form is None: return '[Reldate: not called from item]' propclass = self.properties[property] @@ -303,11 +297,10 @@ class Reldate(Base): return pretty return str(interval) -class Download(Base): - ''' show a Link("file") or Multilink("file") property using links that - allow you to download files - ''' - def __call__(self, property, **args): + def do_download(self, property, **args): + ''' show a Link("file") or Multilink("file") property using links that + allow you to download files + ''' if not self.nodeid: return '[Download: not called from item]' propclass = self.properties[property] @@ -326,11 +319,10 @@ class Download(Base): return '[Download: not a link]' -class Checklist(Base): - ''' for a Link or Multilink property, display checkboxes for the available - choices to permit filtering - ''' - def __call__(self, property, **args): + def do_checklist(self, property, **args): + ''' for a Link or Multilink property, display checkboxes for the + available choices to permit filtering + ''' propclass = self.properties[property] if (not isinstance(propclass, hyperdb.Link) and not isinstance(propclass, hyperdb.Multilink)): @@ -373,38 +365,42 @@ class Checklist(Base): 'value="-1">'%(checked, property)) return '\n'.join(l) -class Note(Base): - ''' display a "note" field, which is a text area for entering a note to - go along with a change. - ''' - def __call__(self, rows=5, cols=80): - # TODO: pull the value from the form + def do_note(self, 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 -class List(Base): - ''' list the items specified by property using the standard index for - the class - ''' - def __call__(self, property, reverse=0): - propclass = self.properties[property] - if isinstance(not propclass, hyperdb.Multilink): + # XXX new function + def do_list(self, property, reverse=0): + ''' list the items specified by property using the standard index for + the class + ''' + propcl = self.properties[property] + if not isinstance(propcl, hyperdb.Multilink): return '[List: not a Multilink]' - fp = StringIO.StringIO() value = self.cl.get(self.nodeid, property) if reverse: value.reverse() - # TODO: really not happy with the way templates is passed on here - index(fp, self.templates, self.db, propclass.classname, nodeids=value, - show_display_form=0) + + # render the sub-index into a string + fp = StringIO.StringIO() + try: + write_save = self.client.write + self.client.write = fp.write + index = IndexTemplate(self.client, self.templates, propcl.classname) + index.render(nodeids=value, show_display_form=0) + finally: + self.client.write = write_save + return fp.getvalue() -# XXX new function -class History(Base): - ''' list the history of the item - ''' - def __call__(self, **args): + # XXX new function + def do_history(self, **args): + ''' list the history of the item + ''' if self.nodeid is None: return "[History: node doesn't exist]" @@ -421,11 +417,10 @@ class History(Base): l.append('') return '\n'.join(l) -# XXX new function -class Submit(Base): - ''' add a submit button for the item - ''' - def __call__(self): + # XXX new function + def do_submit(self): + ''' add a submit button for the item + ''' if self.nodeid: return '' elif self.form is not None: @@ -443,10 +438,11 @@ class IndexTemplateReplace: self.locals = locals self.props = props - def go(self, text, replace=re.compile( - r'(([^>]+)">(?P.+?))|' - r'(?P[^"]+)">))', re.I|re.S)): - return replace.sub(self, text) + replace=re.compile( + r'(([^>]+)">(?P.+?))|' + r'(?P[^"]+)">))', re.I|re.S) + def go(self, text): + return self.replace.sub(self, text) def __call__(self, m, filter=None, columns=None, sort=None, group=None): if m.group('name'): @@ -461,278 +457,280 @@ class IndexTemplateReplace: return eval(command, self.globals, self.locals) print '*** unhandled match', m.groupdict() -def sortby(sort_name, columns, filter, sort, group, filterspec): - l = [] - w = l.append - for k, v in filterspec.items(): - k = urllib.quote(k) - if type(v) == type([]): - w('%s=%s'%(k, ','.join(map(urllib.quote, v)))) - else: - w('%s=%s'%(k, urllib.quote(v))) - if columns: - w(':columns=%s'%','.join(map(urllib.quote, columns))) - if filter: - w(':filter=%s'%','.join(map(urllib.quote, filter))) - if group: - w(':group=%s'%','.join(map(urllib.quote, group))) - m = [] - s_dir = '' - for name in sort: - dir = name[0] - if dir == '-': - name = name[1:] +class IndexTemplate(TemplateFunctions): + def __init__(self, client, templates, classname): + self.client = client + self.templates = templates + self.classname = classname + + # derived + self.db = self.client.db + self.cl = self.db.classes[self.classname] + self.properties = self.cl.getprops() + + TemplateFunctions.__init__(self) + + col_re=re.compile(r']+)">') + def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[], + show_display_form=1, nodeids=None, show_customization=1): + self.filterspec = filterspec + + w = self.client.write + + # get the filter template + try: + filter_template = open(os.path.join(self.templates, + self.classname+'.filter')).read() + all_filters = self.col_re.findall(filter_template) + except IOError, error: + if error.errno != errno.ENOENT: raise + filter_template = None + all_filters = [] + + # display the filter section + if (hasattr(self.client, 'FILTER_POSITION') and + self.client.FILTER_POSITION in ('top and bottom', 'top')): + w('
\n') + self.filter_section(filter_template, filter, columns, group, + all_filters, all_columns, show_display_form, show_customization) + w('
\n') + + # make sure that the sorting doesn't get lost either + if sort: + w(''%','.join(sort)) + + # XXX deviate from spec here ... + # load the index section template and figure the default columns from it + template = open(os.path.join(self.templates, + self.classname+'.index')).read() + all_columns = self.col_re.findall(template) + if not columns: + columns = [] + for name in all_columns: + columns.append(name) else: - dir = '' - if sort_name == name: - if dir == '-': - s_dir = '' + # re-sort columns to be the same order as all_columns + l = [] + for name in all_columns: + if name in columns: + l.append(name) + columns = l + + # now display the index section + w('\n') + w('\n') + for name in columns: + cname = name.capitalize() + if show_display_form: + sb = self.sortby(name, filterspec, columns, filter, group, sort) + anchor = "%s?%s"%(self.classname, sb) + w('\n'%( + anchor, cname)) else: - s_dir = '-' - else: - m.append(dir+urllib.quote(name)) - m.insert(0, s_dir+urllib.quote(sort_name)) - # so things don't get completely out of hand, limit the sort to two columns - w(':sort=%s'%','.join(m[:2])) - return '&'.join(l) - -def index(client, templates, db, classname, filterspec={}, filter=[], - columns=[], sort=[], group=[], show_display_form=1, nodeids=None, - show_customization=1, - col_re=re.compile(r']+)">')): - globals = { - 'plain': Plain(db, templates, classname, filterspec=filterspec), - 'field': Field(db, templates, classname, filterspec=filterspec), - 'menu': Menu(db, templates, classname, filterspec=filterspec), - 'link': Link(db, templates, classname, filterspec=filterspec), - 'count': Count(db, templates, classname, filterspec=filterspec), - 'reldate': Reldate(db, templates, classname, filterspec=filterspec), - 'download': Download(db, templates, classname, filterspec=filterspec), - 'checklist': Checklist(db, templates, classname, filterspec=filterspec), - 'list': List(db, templates, classname, filterspec=filterspec), - 'history': History(db, templates, classname, filterspec=filterspec), - 'submit': Submit(db, templates, classname, filterspec=filterspec), - 'note': Note(db, templates, classname, filterspec=filterspec) - } - cl = db.classes[classname] - properties = cl.getprops() - w = client.write - w('
') - - try: - template = open(os.path.join(templates, classname+'.filter')).read() - all_filters = col_re.findall(template) - except IOError, error: - if error.errno != errno.ENOENT: raise - template = None - all_filters = [] - if template and filter: - # display the filter section - w('
%s
') - w('') - w(' ') - w('') - replace = IndexTemplateReplace(globals, locals(), filter) - w(replace.go(template)) - w('') - w('') + w('\n'%cname) + w('\n') + + # this stuff is used for group headings - optimise the group names + old_group = None + group_names = [] + if group: + for name in group: + if name[0] == '-': group_names.append(name[1:]) + else: group_names.append(name) + + # now actually loop through all the nodes we get from the filter and + # apply the template + if nodeids is None: + nodeids = self.cl.filter(filterspec, sort, group) + for nodeid in nodeids: + # check for a group heading + if group_names: + this_group = [self.cl.get(nodeid, name) for name in group_names] + if this_group != old_group: + l = [] + for name in group_names: + prop = self.properties[name] + if isinstance(prop, hyperdb.Link): + group_cl = self.db.classes[prop.classname] + key = group_cl.getkey() + value = self.cl.get(nodeid, name) + if value is None: + l.append('[unselected %s]'%prop.classname) + else: + l.append(group_cl.get(self.cl.get(nodeid, + name), key)) + elif isinstance(prop, hyperdb.Multilink): + group_cl = self.db.classes[prop.classname] + key = group_cl.getkey() + for value in self.cl.get(nodeid, name): + l.append(group_cl.get(value, key)) + else: + value = self.cl.get(nodeid, name) + if value is None: + value = '[empty %s]'%name + else: + value = str(value) + l.append(value) + w('' + ''%( + len(columns), ', '.join(l))) + old_group = this_group + + # display this node's row + replace = IndexTemplateReplace(self.globals, locals(), columns) + self.nodeid = nodeid + w(replace.go(template)) + self.nodeid = None + w('
Filter specification...
 
%s
%s
') - # If the filters aren't being displayed, then hide their current - # value in the form - if not filter: - for k, v in filterspec.items(): - if type(v) == type([]): v = ','.join(v) - w(''%(k, v)) - - # make sure that the sorting doesn't get lost either - if sort: - w(''%','.join(sort)) - - # XXX deviate from spec here ... - # load the index section template and figure the default columns from it - template = open(os.path.join(templates, classname+'.index')).read() - all_columns = col_re.findall(template) - if not columns: - columns = [] - for name in all_columns: - columns.append(name) - else: - # re-sort columns to be the same order as all_columns - l = [] - for name in all_columns: - if name in columns: - l.append(name) - columns = l - - # display the filter section - if hasattr(client, 'FILTER_POSITION') and client.FILTER_POSITION in ('top and bottom', 'top'): - filter_section(w, cl, filter, columns, group, all_filters, all_columns, - show_display_form, show_customization) - - # now display the index section - w('\n') - w('\n') - for name in columns: - cname = name.capitalize() - if show_display_form: - anchor = "%s?%s"%(classname, sortby(name, columns, filter, - sort, group, filterspec)) - w('\n'%( - anchor, cname)) - else: - w('\n'%cname) - w('\n') - - # this stuff is used for group headings - optimise the group names - old_group = None - group_names = [] - if group: - for name in group: - if name[0] == '-': group_names.append(name[1:]) - else: group_names.append(name) - - # now actually loop through all the nodes we get from the filter and - # apply the template - if nodeids is None: - nodeids = cl.filter(filterspec, sort, group) - for nodeid in nodeids: - # check for a group heading - if group_names: - this_group = [cl.get(nodeid, name) for name in group_names] - if this_group != old_group: - l = [] - for name in group_names: - prop = properties[name] - if isinstance(prop, hyperdb.Link): - group_cl = db.classes[prop.classname] - key = group_cl.getkey() - value = cl.get(nodeid, name) - if value is None: - l.append('[unselected %s]'%prop.classname) - else: - l.append(group_cl.get(cl.get(nodeid, name), key)) - elif isinstance(prop, hyperdb.Multilink): - group_cl = db.classes[prop.classname] - key = group_cl.getkey() - for value in cl.get(nodeid, name): - l.append(group_cl.get(value, key)) - else: - value = cl.get(nodeid, name) - if value is None: - value = '[empty %s]'%name - else: - value = str(value) - l.append(value) - w('' - ''%( - len(columns), ', '.join(l))) - old_group = this_group - - # display this node's row - for value in globals.values(): - if hasattr(value, 'nodeid'): - value.nodeid = nodeid - replace = IndexTemplateReplace(globals, locals(), columns) - w(replace.go(template)) - - w('
%s%s
%s
') - - # display the filter section - if hasattr(client, 'FILTER_POSITION') and client.FILTER_POSITION in ('top and bottom', 'bottom'): - filter_section(w, cl, filter, columns, group, all_filters, all_columns, - show_display_form, show_customization) - - -def filter_section(w, cl, filter, columns, group, all_filters, all_columns, - show_display_form, show_customization): - # now add in the filter/columns/group/etc config table form - w('' % - show_customization ) - w('\n') - names = [] - properties = cl.getprops() - for name in properties.keys(): - if name in all_filters or name in all_columns: - names.append(name) - w('') - if show_customization: - action = '-' - else: - action = '+' - # hide the values for filters, columns and grouping in the form - # if the customization widget is not visible - for name in names: - if all_filters and name in filter: - w('' % name) - if all_columns and name in columns: - w('' % name) - if all_columns and name in group: - w('' % name) - - if show_display_form: - # TODO: The widget style can go into the stylesheet - w('\n'%(len(names)+1, action)) + # display the filter section + if (hasattr(self.client, 'FILTER_POSITION') and + self.client.FILTER_POSITION in ('top and bottom', 'bottom')): + w('\n') + self.filter_section(filter_template, filter, columns, group, + all_filters, all_columns, show_display_form, show_customization) + w('\n') + + + def filter_section(self, template, filter, columns, group, all_filters, + all_columns, show_display_form, show_customization): + + w = self.client.write + + if template and filter: + # display the filter section + w('
' - ' View ' - 'customisation...
') + w('') + w(' ') + w('') + replace = IndexTemplateReplace(self.globals, locals(), filter) + w(replace.go(template)) + w('') + w('') + w('
Filter specification...
 
') + + # now add in the filter/columns/group/etc config table form + w('' % + show_customization ) + w('\n') + names = [] + for name in self.properties.keys(): + if name in all_filters or name in all_columns: + names.append(name) + w('') if show_customization: - w('') + action = '-' + else: + action = '+' + # hide the values for filters, columns and grouping in the form + # if the customization widget is not visible for name in names: - w(''%name.capitalize()) - w('\n') - - # Filter - if all_filters: - w('\n') - for name in names: - if name not in all_filters: - w('') - continue - if name in filter: checked=' checked' - else: checked='' - w('\n'%(name, checked)) - w('\n') + if all_filters and name in filter: + w('' % name) + if all_columns and name in columns: + w('' % name) + if all_columns and name in group: + w('' % name) - # Columns - if all_columns: - w('\n') + if show_display_form: + # TODO: The widget style can go into the stylesheet + w('\n'%(len(names)+1, action)) + if show_customization: + w('') for name in names: - if name not in all_columns: - w('') - continue - if name in columns: checked=' checked' - else: checked='' - w('\n'%(name, checked)) + w(''%name.capitalize()) w('\n') - # Grouping - w('\n') - for name in names: - prop = properties[name] - if name not in all_columns: - w('') - continue - if name in group: checked=' checked' - else: checked='' - w('\n'%(name, checked)) + # Filter + if all_filters: + w('\n') + for name in names: + if name not in all_filters: + w('') + continue + if name in filter: checked=' checked' + else: checked='' + w('\n'%(name, checked)) + w('\n') + + # Columns + if all_columns: + w('\n') + for name in names: + if name not in all_columns: + w('') + continue + if name in columns: checked=' checked' + else: checked='' + w('\n'%(name, checked)) + w('\n') + + # Grouping + w('\n') + for name in names: + prop = self.properties[name] + if name not in all_columns: + w('') + continue + if name in group: checked=' checked' + else: checked='' + w('\n'%(name, checked)) + w('\n') + + w('') + w('') w('\n') - w('') - w('') - w('\n') + w('
 %s
' - 'Filters \n') - w('
' - 'Columns' + ' View ' + 'customisation...
  \n') - w(' %s
' - 'Grouping \n') - w('
' + 'Filters \n') + w('
' + 'Columns \n') + w('
' + 'Grouping \n') + w('
 '%len(names)) + w('
 '%len(names)) - w('
\n') - w('\n') - w('\n') + def sortby(self, sort_name, filterspec, columns, filter, group, sort): + l = [] + w = l.append + for k, v in filterspec.items(): + k = urllib.quote(k) + if type(v) == type([]): + w('%s=%s'%(k, ','.join(map(urllib.quote, v)))) + else: + w('%s=%s'%(k, urllib.quote(v))) + if columns: + w(':columns=%s'%','.join(map(urllib.quote, columns))) + if filter: + w(':filter=%s'%','.join(map(urllib.quote, filter))) + if group: + w(':group=%s'%','.join(map(urllib.quote, group))) + m = [] + s_dir = '' + for name in sort: + dir = name[0] + if dir == '-': + name = name[1:] + else: + dir = '' + if sort_name == name: + if dir == '-': + s_dir = '' + else: + s_dir = '-' + else: + m.append(dir+urllib.quote(name)) + m.insert(0, s_dir+urllib.quote(sort_name)) + # so things don't get completely out of hand, limit the sort to + # two columns + w(':sort=%s'%','.join(m[:2])) + return '&'.join(l) # # ITEM TEMPLATES @@ -744,10 +742,11 @@ class ItemTemplateReplace: self.cl = cl self.nodeid = nodeid - def go(self, text, replace=re.compile( - r'(([^>]+)">(?P.+?))|' - r'(?P[^"]+)">))', re.I|re.S)): - return replace.sub(self, text) + replace=re.compile( + r'(([^>]+)">(?P.+?))|' + r'(?P[^"]+)">))', re.I|re.S) + def go(self, text): + return self.replace.sub(self, text) def __call__(self, m, filter=None, columns=None, sort=None, group=None): if m.group('name'): @@ -762,83 +761,78 @@ class ItemTemplateReplace: return eval(command, self.globals, self.locals) print '*** unhandled match', m.groupdict() -def item(client, templates, db, classname, nodeid, replace=re.compile( - r'((?P[^>]+)">)|' - r'(?P)|' - r'(?P[^"]+)">))', re.I)): - - globals = { - 'plain': Plain(db, templates, classname, nodeid), - 'field': Field(db, templates, classname, nodeid), - 'menu': Menu(db, templates, classname, nodeid), - 'link': Link(db, templates, classname, nodeid), - 'count': Count(db, templates, classname, nodeid), - 'reldate': Reldate(db, templates, classname, nodeid), - 'download': Download(db, templates, classname, nodeid), - 'checklist': Checklist(db, templates, classname, nodeid), - 'list': List(db, templates, classname, nodeid), - 'history': History(db, templates, classname, nodeid), - 'submit': Submit(db, templates, classname, nodeid), - 'note': Note(db, templates, classname, nodeid) - } - - cl = db.classes[classname] - properties = cl.getprops() - - if properties.has_key('type') and properties.has_key('content'): - pass - # XXX we really want to return this as a downloadable... - # currently I handle this at a higher level by detecting 'file' - # designators... - - w = client.write - w('
'%(classname, nodeid)) - s = open(os.path.join(templates, classname+'.item')).read() - replace = ItemTemplateReplace(globals, locals(), cl, nodeid) - w(replace.go(s)) - w('
') - - -def newitem(client, templates, db, classname, form, replace=re.compile( - r'((?P[^>]+)">)|' - r'(?P)|' - r'(?P[^"]+)">))', re.I)): - globals = { - 'plain': Plain(db, templates, classname, form=form), - 'field': Field(db, templates, classname, form=form), - 'menu': Menu(db, templates, classname, form=form), - 'link': Link(db, templates, classname, form=form), - 'count': Count(db, templates, classname, form=form), - 'reldate': Reldate(db, templates, classname, form=form), - 'download': Download(db, templates, classname, form=form), - 'checklist': Checklist(db, templates, classname, form=form), - 'list': List(db, templates, classname, form=form), - 'history': History(db, templates, classname, form=form), - 'submit': Submit(db, templates, classname, form=form), - 'note': Note(db, templates, classname, form=form) - } - - cl = db.classes[classname] - properties = cl.getprops() - - w = client.write - try: - s = open(os.path.join(templates, classname+'.newitem')).read() - except: - s = open(os.path.join(templates, classname+'.item')).read() - w('
'%classname) - for key in form.keys(): - if key[0] == ':': - value = form[key].value - if type(value) != type([]): value = [value] - for value in value: - w(''%(key, value)) - replace = ItemTemplateReplace(globals, locals(), None, None) - w(replace.go(s)) - w('
') + +class ItemTemplate(TemplateFunctions): + def __init__(self, client, templates, classname): + self.client = client + self.templates = templates + self.classname = classname + + # derived + self.db = self.client.db + self.cl = self.db.classes[self.classname] + self.properties = self.cl.getprops() + + TemplateFunctions.__init__(self) + + def render(self, nodeid): + self.nodeid = nodeid + + if (self.properties.has_key('type') and + self.properties.has_key('content')): + pass + # XXX we really want to return this as a downloadable... + # currently I handle this at a higher level by detecting 'file' + # designators... + + w = self.client.write + w('
'%(self.classname, nodeid)) + s = open(os.path.join(self.templates, self.classname+'.item')).read() + replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid) + w(replace.go(s)) + w('
') + + +class NewItemTemplate(TemplateFunctions): + def __init__(self, client, templates, classname): + self.client = client + self.templates = templates + self.classname = classname + + # derived + self.db = self.client.db + self.cl = self.db.classes[self.classname] + self.properties = self.cl.getprops() + + TemplateFunctions.__init__(self) + + def render(self, form): + self.form = form + w = self.client.write + c = self.classname + try: + s = open(os.path.join(self.templates, c+'.newitem')).read() + except: + s = open(os.path.join(self.templates, c+'.item')).read() + w('
'%c) + for key in form.keys(): + if key[0] == ':': + value = form[key].value + if type(value) != type([]): value = [value] + for value in value: + w(''%(key, value)) + replace = ItemTemplateReplace(self.globals, locals(), None, None) + w(replace.go(s)) + w('
') # # $Log: not supported by cvs2svn $ +# Revision 1.32 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.31 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 diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index 546e746..099c1c6 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -15,16 +15,20 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundupdb.py,v 1.14 2001-10-21 07:26:35 richard Exp $ +# $Id: roundupdb.py,v 1.15 2001-10-23 01:00:18 richard Exp $ import re, os, smtplib, socket import hyperdb, date +class DesignatorError(ValueError): + pass def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')): ''' Take a foo123 and return ('foo', 123) ''' m = dre.match(designator) + if m is None: + raise DesignatorError, '"%s" not a node designator'%designator return m.group(1), m.group(2) class Database: @@ -303,6 +307,11 @@ Roundup issue tracker # # $Log: not supported by cvs2svn $ +# Revision 1.14 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.13 2001/10/21 00:45:15 richard # Added author identification to e-mail messages from roundup. # diff --git a/roundup/templates/classic/instance_config.py b/roundup/templates/classic/instance_config.py index 176a981..f02335d 100644 --- a/roundup/templates/classic/instance_config.py +++ b/roundup/templates/classic/instance_config.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: instance_config.py,v 1.7 2001-10-22 03:25:01 richard Exp $ +# $Id: instance_config.py,v 1.8 2001-10-23 01:00:18 richard Exp $ MAIL_DOMAIN=MAILHOST=HTTP_HOST=None HTTP_PORT=0 @@ -62,14 +62,23 @@ ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN # Somewhere for roundup to log stuff internally sent to stdout or stderr LOG = os.path.join(INSTANCE_HOME, 'roundup.log') +# Where to place the web filtering HTML on the index page +FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom' + # Deny or allow anonymous access to the web interface -ANONYMOUS_ACCESS = 'deny' +ANONYMOUS_ACCESS = 'deny' # either 'deny' or 'allow' # Deny or allow anonymous users to register through the web interface -ANONYMOUS_REGISTER = 'deny' +ANONYMOUS_REGISTER = 'deny' # either 'deny' or 'allow' # # $Log: not supported by cvs2svn $ +# Revision 1.7 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.6 2001/10/01 06:10:42 richard # stop people setting up roundup with our addresses as default - need to # handle this better in the init diff --git a/roundup/templates/extended/instance_config.py b/roundup/templates/extended/instance_config.py index 643c409..3260bfa 100644 --- a/roundup/templates/extended/instance_config.py +++ b/roundup/templates/extended/instance_config.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: instance_config.py,v 1.7 2001-10-22 03:25:01 richard Exp $ +# $Id: instance_config.py,v 1.8 2001-10-23 01:00:18 richard Exp $ MAIL_DOMAIN=MAILHOST=HTTP_HOST=None HTTP_PORT=0 @@ -62,6 +62,9 @@ ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN # Somewhere for roundup to log stuff internally sent to stdout or stderr LOG = os.path.join(INSTANCE_HOME, 'roundup.log') +# Where to place the web filtering HTML on the index page +FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom' + # Deny or allow anonymous access to the web interface ANONYMOUS_ACCESS = 'deny' @@ -70,6 +73,12 @@ ANONYMOUS_REGISTER = 'deny' # # $Log: not supported by cvs2svn $ +# Revision 1.7 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.6 2001/10/01 06:10:42 richard # stop people setting up roundup with our addresses as default - need to # handle this better in the init