From dfa6a36380240155b412ba1a2d2bb0b1689b41f6 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 4 Sep 2002 04:31:51 +0000 Subject: [PATCH] Class help and generic class editing done. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1060 57a73879-2fb5-44c3-a270-3262357dd7e2 --- TODO.txt | 2 - roundup/cgi/client.py | 93 +++++++++++-------- roundup/cgi/templating.py | 80 +++++++++++++--- roundup/templates/classic/html/_generic.help | 11 +++ roundup/templates/classic/html/_generic.index | 27 ++++++ roundup/templates/classic/html/home.classlist | 2 +- 6 files changed, 159 insertions(+), 56 deletions(-) create mode 100644 roundup/templates/classic/html/_generic.help create mode 100644 roundup/templates/classic/html/_generic.index diff --git a/TODO.txt b/TODO.txt index 1a8feb9..027a6a8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -49,8 +49,6 @@ pending web: search "refinement" pending web: have roundup.cgi pick up instance config from the environment New templating TODO: -. generic class editing -. classhelp . rewritten documentation (can come after the beta though so stuff is settled) ongoing: any bugs diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py index b3f8628..a944376 100644 --- a/roundup/cgi/client.py +++ b/roundup/cgi/client.py @@ -1,4 +1,4 @@ -# $Id: client.py,v 1.10 2002-09-03 07:42:38 richard Exp $ +# $Id: client.py,v 1.11 2002-09-04 04:31:51 richard Exp $ __doc__ = """ WWW request handler (also used in the stand-alone server). @@ -110,11 +110,11 @@ class Client: self.determine_user() # figure out the context and desired content template self.determine_context() - # possibly handle a form submit action (may change self.message - # and self.template_name) + # possibly handle a form submit action (may change self.message, + # self.classname and self.template) self.handle_action() # now render the page - self.write(self.template('page', ok_message=self.ok_message, + self.write(self.renderTemplate('page', '', ok_message=self.ok_message, error_message=self.error_message)) except Redirect, url: # let's redirect - if the url isn't None, then we need to do @@ -127,8 +127,7 @@ class Client: except SendStaticFile, file: self.serve_static_file(str(file)) except Unauthorised, message: - self.write(self.template('page.unauthorised', - error_message=message)) + self.write(self.renderTemplate('page', '', error_message=message)) except: # everything else self.write(cgitb.html()) @@ -207,9 +206,9 @@ class Client: full item designator supplied: "item" We set: - self.classname - self.nodeid - self.template_name + self.classname - the class to display, can be None + self.template - the template to render the current context with + self.nodeid - the nodeid of the class we're displaying ''' # default the optional variables self.classname = None @@ -219,11 +218,9 @@ class Client: path = self.split_path if not path or path[0] in ('', 'home', 'index'): if self.form.has_key(':template'): - self.template_type = self.form[':template'].value - self.template_name = 'home' + '.' + self.template_type + self.template = self.form[':template'].value else: - self.template_type = '' - self.template_name = 'home' + self.template = '' return elif path[0] == '_file': raise SendStaticFile, path[1] @@ -239,14 +236,14 @@ class Client: self.classname = m.group(1) self.nodeid = m.group(2) # with a designator, we default to item view - self.template_type = 'item' + self.template = 'item' else: # with only a class, we default to index view - self.template_type = 'index' + self.template = 'index' # see if we have a template override if self.form.has_key(':template'): - self.template_type = self.form[':template'].value + self.template = self.form[':template'].value # see if we were passed in a message @@ -255,9 +252,6 @@ class Client: if self.form.has_key(':error_message'): self.error_message.append(self.form[':error_message'].value) - # we have the template name now - self.template_name = self.classname + '.' + self.template_type - def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')): ''' Serve the file from the content property of the designated item. ''' @@ -279,10 +273,10 @@ class Client: self.header({'Content-Type': mt}) self.write(open(os.path.join(self.instance.TEMPLATES, file)).read()) - def template(self, name, **kwargs): + def renderTemplate(self, name, extension, **kwargs): ''' Return a PageTemplate for the named page ''' - pt = getTemplate(self.instance.TEMPLATES, name) + pt = getTemplate(self.instance.TEMPLATES, name, extension) # XXX handle PT rendering errors here more nicely try: # let the template render figure stuff out @@ -297,14 +291,23 @@ class Client: def content(self): ''' Callback used by the page template to render the content of the page. + + If we don't have a specific class to display, that is none was + determined in determine_context(), then we display a "home" + template. ''' # now render the page content using the template we determined in # determine_context - return self.template(self.template_name) + if self.classname is None: + name = 'home' + else: + name = self.classname + return self.renderTemplate(self.classname, self.template) # these are the actions that are available actions = { 'edit': 'editItemAction', + 'editCSV': 'editCSVAction', 'new': 'newItemAction', 'register': 'registerAction', 'login': 'login_action', @@ -631,14 +634,17 @@ class Client: self.error_message.append( _('You do not have permission to create %s' %self.classname)) - # XXX -# 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 = '' + # create a little extra message for anticipated :link / :multilink + if self.form.has_key(':multilink'): + link = self.form[':multilink'].value + elif self.form.has_key(':link'): + link = self.form[':multilink'].value + else: + link = None + xtra = '' + if link: + designator, linkprop = link.split(':') + xtra = ' for %s'%(designator, designator) try: # do the create @@ -654,7 +660,7 @@ class Client: self.nodeid = nid # and some nice feedback for the user - message = _('%(classname)s created ok')%self.__dict__ + message = _('%(classname)s created ok')%self.__dict__ + xtra except (ValueError, KeyError), message: self.error_message.append(_('Error: ') + str(message)) return @@ -686,15 +692,15 @@ class Client: return 1 return 0 - def genericEditAction(self): + def editCSVAction(self): ''' Performs an edit of all of a class' items in one go. The "rows" CGI var defines the CSV-formatted entries for the class. New nodes are identified by the ID 'X' (or any other non-existent ID) and removed lines are retired. ''' - # generic edit is per-class only - if not self.genericEditPermission(): + # this is per-class only + if not self.editCSVPermission(): self.error_message.append( _('You do not have permission to edit %s' %self.classname)) @@ -709,6 +715,7 @@ class Client: cl = self.db.classes[self.classname] idlessprops = cl.getprops(protected=0).keys() + idlessprops.sort() props = ['id'] + idlessprops # do the edit @@ -716,19 +723,24 @@ class Client: p = csv.parser() found = {} line = 0 - for row in rows: + for row in rows[1:]: line += 1 values = p.parse(row) # not a complete row, keep going if not values: continue + # skip property names header + if values == props: + continue + # extract the nodeid nodeid, values = values[0], values[1:] found[nodeid] = 1 # confirm correct weight if len(idlessprops) != len(values): - message=(_('Not enough values on line %(line)s'%{'line':line})) + self.error_message.append( + _('Not enough values on line %(line)s')%{'line':line}) return # extract the new values @@ -755,13 +767,12 @@ class Client: if not found.has_key(nodeid): cl.retire(nodeid) - message = _('items edited OK') + # all OK + self.db.commit() - # redirect to the class' edit page - raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname, - urllib.quote(message)) + self.ok_message.append(_('Items edited OK')) - def genericEditPermission(self): + def editCSVPermission(self): ''' Determine whether the user has permission to edit this class. Base behaviour is to check the user can edit this class. diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py index dfc662b..aea8651 100644 --- a/roundup/cgi/templating.py +++ b/roundup/cgi/templating.py @@ -1,4 +1,4 @@ -import sys, cgi, urllib, os, re, os.path, time +import sys, cgi, urllib, os, re, os.path, time, errno from roundup import hyperdb, date from roundup.i18n import _ @@ -80,14 +80,37 @@ import ZTUtils templates = {} -def getTemplate(dir, name, classname=None, request=None): +def getTemplate(dir, name, extension, classname=None, request=None): ''' Interface to get a template, possibly loading a compiled template. + + "name" and "extension" indicate the template we're after, which in + most cases will be "name.extension". If "extension" is None, then + we look for a template just called "name" with no extension. + + If the file "name.extension" doesn't exist, we look for + "_generic.extension" as a fallback. ''' - # find the source, figure the time it was last modified - src = os.path.join(dir, name) - stime = os.stat(src)[os.path.stat.ST_MTIME] + # default the name to "home" + if name is None: + name = 'home' - key = (dir, name) + # find the source, figure the time it was last modified + if extension: + filename = '%s.%s'%(name, extension) + else: + filename = name + src = os.path.join(dir, filename) + try: + stime = os.stat(src)[os.path.stat.ST_MTIME] + except os.error, error: + if error.errno != errno.ENOENT or not extension: + raise + # try for a generic template + filename = '_generic.%s'%extension + src = os.path.join(dir, filename) + stime = os.stat(src)[os.path.stat.ST_MTIME] + + key = (dir, filename) if templates.has_key(key) and stime < templates[key].mtime: # compiled template is up to date return templates[key] @@ -262,6 +285,40 @@ class HTMLClass: l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()] return l + def csv(self): + ''' Return the items of this class as a chunk of CSV text. + ''' + # get the CSV module + try: + import csv + except ImportError: + return 'Sorry, you need the csv module to use this function.\n'\ + 'Get it from: http://www.object-craft.com.au/projects/csv/' + + props = self.propnames() + p = csv.parser() + s = StringIO.StringIO() + s.write(p.join(props) + '\n') + for nodeid in self.klass.list(): + l = [] + for name in props: + value = self.klass.get(nodeid, name) + if value is None: + l.append('') + elif isinstance(value, type([])): + l.append(':'.join(map(str, value))) + else: + l.append(str(self.klass.get(nodeid, name))) + s.write(p.join(l) + '\n') + return s.getvalue() + + def propnames(self): + ''' Return the list of the names of the properties of this class. + ''' + idlessprops = self.klass.getprops(protected=0).keys() + idlessprops.sort() + return ['id'] + idlessprops + def filter(self, request=None): ''' Return a list of items from this class, filtered and sorted by the current requested filterspec/filter/sort/group args @@ -285,7 +342,7 @@ class HTMLClass: You may optionally override the label displayed, the width and height. The popup window will be resizable and scrollable. ''' - return '(%s)'%(self.classname, properties, width, height, label) @@ -307,8 +364,7 @@ class HTMLClass: req.update(kwargs) # new template, using the specified classname and request - name = self.classname + '.' + name - pt = getTemplate(self.db.config.TEMPLATES, name) + pt = getTemplate(self.db.config.TEMPLATES, self.classname, name) # XXX handle PT rendering errors here nicely try: @@ -946,7 +1002,7 @@ class HTMLRequest: "base" the base URL for this instance "user" a HTMLUser instance for this user "classname" the current classname (possibly None) - "template_type" the current template type (suffix, also possibly None) + "template" the current template (suffix, also possibly None) Index args: "columns" dictionary of the columns to display in an index page @@ -971,7 +1027,7 @@ class HTMLRequest: # store the current class name and action self.classname = client.classname - self.template_type = client.template_type + self.template = client.template # extract the index display information from the form self.columns = [] @@ -1055,7 +1111,7 @@ form: %(form)s url: %(url)r base: %(base)r classname: %(classname)r -template_type: %(template_type)r +template: %(template)r columns: %(columns)r sort: %(sort)r group: %(group)r diff --git a/roundup/templates/classic/html/_generic.help b/roundup/templates/classic/html/_generic.help new file mode 100644 index 0000000..af36b03 --- /dev/null +++ b/roundup/templates/classic/html/_generic.help @@ -0,0 +1,11 @@ + + + + + + + +
+ diff --git a/roundup/templates/classic/html/_generic.index b/roundup/templates/classic/html/_generic.index new file mode 100644 index 0000000..298a282 --- /dev/null +++ b/roundup/templates/classic/html/_generic.index @@ -0,0 +1,27 @@ + + +

+ You may edit the contents of the + 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. +

+ +
+ +
+ + +
+ diff --git a/roundup/templates/classic/html/home.classlist b/roundup/templates/classic/html/home.classlist index bcabfea..f79ce54 100644 --- a/roundup/templates/classic/html/home.classlist +++ b/roundup/templates/classic/html/home.classlist @@ -3,7 +3,7 @@ - classname -- 2.30.2