Code

Class help and generic class editing done.
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Wed, 4 Sep 2002 04:31:51 +0000 (04:31 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Wed, 4 Sep 2002 04:31:51 +0000 (04:31 +0000)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1060 57a73879-2fb5-44c3-a270-3262357dd7e2

TODO.txt
roundup/cgi/client.py
roundup/cgi/templating.py
roundup/templates/classic/html/_generic.help [new file with mode: 0644]
roundup/templates/classic/html/_generic.index [new file with mode: 0644]
roundup/templates/classic/html/home.classlist

index 1a8feb9674ce64bfc7ca3dd03e5a82ab1608435f..027a6a8fe6fc709fbf0d4c3d9b61bf58e2eedba4 100644 (file)
--- 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
index b3f862842f650825eda6a6ebd94b4dd68e6ffd2d..a9443765c1c6bfc7479336cd1b97021457bb20f4 100644 (file)
@@ -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 <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 @@ 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.
index dfc662b2dcb7df7526077cd2ad24aa415265ecd5..aea865120c74e74ecc7e398f55453e46c2ceac06 100644 (file)
@@ -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 '<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 @@ 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 (file)
index 0000000..af36b03
--- /dev/null
@@ -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>
+
diff --git a/roundup/templates/classic/html/_generic.index b/roundup/templates/classic/html/_generic.index
new file mode 100644 (file)
index 0000000..298a282
--- /dev/null
@@ -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>
+
index bcabfea114dff31b24b301fca1227ab437d4fedc..f79ce5480152f0e490819e78f72bb180eedc14b7 100644 (file)
@@ -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>