Mercurial > p > roundup > code
changeset 1041:c28603c9f831
Class help and generic class editing done.
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Wed, 04 Sep 2002 04:31:51 +0000 |
| parents | 4316d1432db4 |
| children | adcfdeac4e76 |
| files | TODO.txt roundup/cgi/client.py roundup/cgi/templating.py roundup/templates/classic/html/_generic.help roundup/templates/classic/html/_generic.index roundup/templates/classic/html/home.classlist |
| diffstat | 6 files changed, 158 insertions(+), 55 deletions(-) [+] |
line wrap: on
line diff
--- a/TODO.txt Wed Sep 04 04:30:58 2002 +0000 +++ b/TODO.txt Wed Sep 04 04:31:51 2002 +0000 @@ -49,8 +49,6 @@ 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
--- a/roundup/cgi/client.py Wed Sep 04 04:30:58 2002 +0000 +++ b/roundup/cgi/client.py Wed Sep 04 04:31:51 2002 +0000 @@ -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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 <a href="%s">%s</a>' % (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 <a href="%s">%s</a>'%(designator, designator) try: # do the create @@ -654,7 +660,7 @@ 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 @@ 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 @@ cl = self.db.classes[self.classname] idlessprops = cl.getprops(protected=0).keys() + idlessprops.sort() props = ['id'] + idlessprops # do the edit @@ -716,19 +723,24 @@ 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 @@ 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.
--- a/roundup/cgi/templating.py Wed Sep 04 04:30:58 2002 +0000 +++ b/roundup/cgi/templating.py Wed Sep 04 04:31:51 2002 +0000 @@ -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 @@ 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. ''' + # default the name to "home" + if name is None: + name = 'home' + # 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] + 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, name) + 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 @@ 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 @@ You may optionally override the label displayed, the width and height. The popup window will be resizable and scrollable. ''' - return '<a href="javascript:help_window(\'classhelp?classname=%s&' \ + return '<a href="javascript:help_window(\'%s?:template=help&' \ 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(self.classname, properties, width, height, label) @@ -307,8 +364,7 @@ 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 @@ "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 @@ # 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 @@ 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/roundup/templates/classic/html/_generic.help Wed Sep 04 04:31:51 2002 +0000 @@ -0,0 +1,11 @@ +<table tal:define="props python:request.form['properties'].value.split(',')" + border=1 cellspacing=0 cellpadding=2> +<tr> + <th align=left tal:repeat="prop props" tal:content="prop"></th> +</tr> +<tr tal:repeat="item klass/list"> + <td align="left" valign="top" tal:repeat="prop props" + tal:content="python:item[prop]"></td> +</tr> +</table> +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/roundup/templates/classic/html/_generic.index Wed Sep 04 04:31:51 2002 +0000 @@ -0,0 +1,27 @@ +<!-- dollarId: issue.index,v 1.2 2001/07/29 04:07:37 richard Exp dollar--> + +<p class="form-help"> + You may edit the contents of the <span tal:replace="request/classname" /> + class using this form. Commas, newlines and double quotes (") must be + handled delicately. You may include commas and newlines by enclosing the + values in double-quotes ("). Double quotes themselves must be quoted by + doubling (""). +</p> + +<p class="form-help"> + Multilink properties have their multiple values colon (":") separated + (... ,"one:two:three", ...) +</p> + +<p class="form-help"> + Remove entries by deleting their line. Add new entries by appending + them to the table - put an X in the id column. +</p> + +<form onSubmit="return submit_once()" method="POST"> +<textarea rows="15" cols="60" name="rows" tal:content="klass/csv"></textarea> +<br> +<input type="hidden" name=":action" value="editCSV"> +<input type="submit" value="Edit Items"> +</form> +
--- a/roundup/templates/classic/html/home.classlist Wed Sep 04 04:30:58 2002 +0000 +++ b/roundup/templates/classic/html/home.classlist Wed Sep 04 04:31:51 2002 +0000 @@ -3,7 +3,7 @@ <tal:block tal:repeat="cl db/classes"> <tr class="list-header"> <th colspan="2" align="left"> - <a tal:attributes="href string:${cl/classname}?:template=genericedit" + <a tal:attributes="href string:${cl/classname}" tal:content="python:cl.classname.capitalize()">classname</a> </th> </tr>
