Code

. Lots of cleanup in the classic html (stylesheet, search page, index page, ...)
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sun, 1 Sep 2002 04:32:30 +0000 (04:32 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sun, 1 Sep 2002 04:32:30 +0000 (04:32 +0000)
. Reinstated searching, but not query saving yet
. Filtering only allows sorting and grouping by one property - all backends
  now implement this behaviour.
. Nosy list journalling turned off by default, everything else is on.
. Added some convenience methods (reverse, propchanged, [item] accesses, ...)
. Did I mention the stylesheet is much cleaner now? :)

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1019 57a73879-2fb5-44c3-a270-3262357dd7e2

14 files changed:
TODO.txt
roundup/backends/back_anydbm.py
roundup/backends/back_gadfly.py
roundup/backends/back_metakit.py
roundup/cgi/client.py
roundup/cgi/templating.py
roundup/templates/classic/dbinit.py
roundup/templates/classic/html/home
roundup/templates/classic/html/issue.index
roundup/templates/classic/html/issue.item
roundup/templates/classic/html/issue.search
roundup/templates/classic/html/page
roundup/templates/classic/html/style.css
roundup/templates/classic/html/user.item

index e87933019fcfe00865cc7303ae14be1bc4c4f7cf..4e48e0ec77167889d3d62c072a104bfe666e10a4 100644 (file)
--- a/TODO.txt
+++ b/TODO.txt
@@ -14,7 +14,6 @@ pending hyperdb: range searching of values (dates in particular)
           [value, value, ...] implies "in"
 pending hyperdb: make creator, creation and activity available pre-commit
 pending hyperdb: migrate "id" property to be Number type
-pending hyperdb: allow classes to define their ordering (properties, asc/desc)
 pending instance: including much simpler upgrade path and the use of
                   non-Python configuration files (ConfigParser)
 pending instance: split instance.open() into open() and login()
@@ -34,25 +33,27 @@ pending project: have the demo allow anonymous login
 pending security: at least an LDAP user database implementation
 pending security: authenticate over a secure connection
 pending security: use digital signatures in mailgw
+pending security: submission protection
 pending web: I18N
 pending web: Better message summary display (feature request #520244)
 pending web: Navigating around the issues (feature request #559149)
-pending web: Re-enable link backrefs from messages (feature request #568714)
 pending web: Quick help links next to the property labels giving a
              description of the property. Combine with help for the actual
              form element too, eg. how to use the nosy list edit box.
 pending web: clicking on a group header should filter for that type of entry
 pending web: re-enable auth by basic http auth
 
-New templating TODO
+New templating TODO:
+. generic class editing
 . classhelp
 . query saving
-. search page is a shambles
-. view customisation is a shambles
-. style is a shambles
+. search "refinement" (pre-fill the search page with the current search
+  parameters)
+. security on actions (only allows/enforces generic Edit perm on the class :()
 
 ongoing: any bugs
 
+done web: Re-enable link backrefs from messages (feature request #568714) (RJ)
 done web: have the page layout (header/footer) be templatable (RJ)
 done web: fixing the templating so it works (RJ)
 done web: re-work cgi interface to abstract out the explicit "issue"
index fee6d95dd6d9f967886ba3938a5cbb919cb2e7a8..0f0faa017f947a0af214fa4fcab2f4d5cb7b366e 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_anydbm.py,v 1.65 2002-08-30 08:35:45 richard Exp $
+#$Id: back_anydbm.py,v 1.66 2002-09-01 04:32:30 richard Exp $
 '''
 This module defines a backend that saves the hyperdatabase in a database
 chosen by anydbm. It is guaranteed to always be available in python
@@ -1471,8 +1471,8 @@ class Class(hyperdb.Class):
             sort spec.
 
             "filterspec" is {propname: value(s)}
-            "sort" is ['+propname', '-propname', 'propname', ...]
-            "group is ['+propname', '-propname', 'propname', ...]
+            "sort" and "group" are (dir, prop) where dir is '+', '-' or None
+                               and prop is a prop name or None
             "search_matches" is {nodeid: marker}
         '''
         cn = self.classname
@@ -1591,126 +1591,109 @@ class Class(hyperdb.Class):
                     k.append(v)
             l = k
 
-        # optimise sort
-        m = []
-        for entry in sort:
-            if entry[0] != '-':
-                m.append(('+', entry))
-            else:
-                m.append((entry[0], entry[1:]))
-        sort = m
-
-        # optimise group
-        m = []
-        for entry in group:
-            if entry[0] != '-':
-                m.append(('+', entry))
-            else:
-                m.append((entry[0], entry[1:]))
-        group = m
         # now, sort the result
         def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
                 db = self.db, cl=self):
             a_id, an = a
             b_id, bn = b
             # sort by group and then sort
-            for list in group, sort:
-                for dir, prop in list:
-                    # sorting is class-specific
-                    propclass = properties[prop]
+            for dir, prop in group, sort:
+                if dir is None: continue
 
-                    # handle the properties that might be "faked"
-                    # also, handle possible missing properties
-                    try:
-                        if not an.has_key(prop):
-                            an[prop] = cl.get(a_id, prop)
-                        av = an[prop]
-                    except KeyError:
-                        # the node doesn't have a value for this property
-                        if isinstance(propclass, Multilink): av = []
-                        else: av = ''
+                # sorting is class-specific
+                propclass = properties[prop]
+
+                # handle the properties that might be "faked"
+                # also, handle possible missing properties
+                try:
+                    if not an.has_key(prop):
+                        an[prop] = cl.get(a_id, prop)
+                    av = an[prop]
+                except KeyError:
+                    # the node doesn't have a value for this property
+                    if isinstance(propclass, Multilink): av = []
+                    else: av = ''
+                try:
+                    if not bn.has_key(prop):
+                        bn[prop] = cl.get(b_id, prop)
+                    bv = bn[prop]
+                except KeyError:
+                    # the node doesn't have a value for this property
+                    if isinstance(propclass, Multilink): bv = []
+                    else: bv = ''
+
+                # String and Date values are sorted in the natural way
+                if isinstance(propclass, String):
+                    # clean up the strings
+                    if av and av[0] in string.uppercase:
+                        av = an[prop] = av.lower()
+                    if bv and bv[0] in string.uppercase:
+                        bv = bn[prop] = bv.lower()
+                if (isinstance(propclass, String) or
+                        isinstance(propclass, Date)):
+                    # it might be a string that's really an integer
                     try:
-                        if not bn.has_key(prop):
-                            bn[prop] = cl.get(b_id, prop)
-                        bv = bn[prop]
-                    except KeyError:
-                        # the node doesn't have a value for this property
-                        if isinstance(propclass, Multilink): bv = []
-                        else: bv = ''
-
-                    # String and Date values are sorted in the natural way
-                    if isinstance(propclass, String):
-                        # clean up the strings
-                        if av and av[0] in string.uppercase:
-                            av = an[prop] = av.lower()
-                        if bv and bv[0] in string.uppercase:
-                            bv = bn[prop] = bv.lower()
-                    if (isinstance(propclass, String) or
-                            isinstance(propclass, Date)):
-                        # it might be a string that's really an integer
-                        try:
-                            av = int(av)
-                            bv = int(bv)
-                        except:
-                            pass
+                        av = int(av)
+                        bv = int(bv)
+                    except:
+                        pass
+                    if dir == '+':
+                        r = cmp(av, bv)
+                        if r != 0: return r
+                    elif dir == '-':
+                        r = cmp(bv, av)
+                        if r != 0: return r
+
+                # Link properties are sorted according to the value of
+                # the "order" property on the linked nodes if it is
+                # present; or otherwise on the key string of the linked
+                # nodes; or finally on  the node ids.
+                elif isinstance(propclass, Link):
+                    link = db.classes[propclass.classname]
+                    if av is None and bv is not None: return -1
+                    if av is not None and bv is None: return 1
+                    if av is None and bv is None: continue
+                    if link.getprops().has_key('order'):
                         if dir == '+':
-                            r = cmp(av, bv)
+                            r = cmp(link.get(av, 'order'),
+                                link.get(bv, 'order'))
                             if r != 0: return r
                         elif dir == '-':
-                            r = cmp(bv, av)
+                            r = cmp(link.get(bv, 'order'),
+                                link.get(av, 'order'))
                             if r != 0: return r
-
-                    # Link properties are sorted according to the value of
-                    # the "order" property on the linked nodes if it is
-                    # present; or otherwise on the key string of the linked
-                    # nodes; or finally on  the node ids.
-                    elif isinstance(propclass, Link):
-                        link = db.classes[propclass.classname]
-                        if av is None and bv is not None: return -1
-                        if av is not None and bv is None: return 1
-                        if av is None and bv is None: continue
-                        if link.getprops().has_key('order'):
-                            if dir == '+':
-                                r = cmp(link.get(av, 'order'),
-                                    link.get(bv, 'order'))
-                                if r != 0: return r
-                            elif dir == '-':
-                                r = cmp(link.get(bv, 'order'),
-                                    link.get(av, 'order'))
-                                if r != 0: return r
-                        elif link.getkey():
-                            key = link.getkey()
-                            if dir == '+':
-                                r = cmp(link.get(av, key), link.get(bv, key))
-                                if r != 0: return r
-                            elif dir == '-':
-                                r = cmp(link.get(bv, key), link.get(av, key))
-                                if r != 0: return r
-                        else:
-                            if dir == '+':
-                                r = cmp(av, bv)
-                                if r != 0: return r
-                            elif dir == '-':
-                                r = cmp(bv, av)
-                                if r != 0: return r
-
-                    # Multilink properties are sorted according to how many
-                    # links are present.
-                    elif isinstance(propclass, Multilink):
+                    elif link.getkey():
+                        key = link.getkey()
                         if dir == '+':
-                            r = cmp(len(av), len(bv))
+                            r = cmp(link.get(av, key), link.get(bv, key))
                             if r != 0: return r
                         elif dir == '-':
-                            r = cmp(len(bv), len(av))
+                            r = cmp(link.get(bv, key), link.get(av, key))
                             if r != 0: return r
-                    elif isinstance(propclass, Number) or isinstance(propclass, Boolean):
+                    else:
                         if dir == '+':
                             r = cmp(av, bv)
+                            if r != 0: return r
                         elif dir == '-':
                             r = cmp(bv, av)
-                        
-                # end for dir, prop in list:
-            # end for list in sort, group:
+                            if r != 0: return r
+
+                # Multilink properties are sorted according to how many
+                # links are present.
+                elif isinstance(propclass, Multilink):
+                    if dir == '+':
+                        r = cmp(len(av), len(bv))
+                        if r != 0: return r
+                    elif dir == '-':
+                        r = cmp(len(bv), len(av))
+                        if r != 0: return r
+                elif isinstance(propclass, Number) or isinstance(propclass, Boolean):
+                    if dir == '+':
+                        r = cmp(av, bv)
+                    elif dir == '-':
+                        r = cmp(bv, av)
+                    
+            # end for dir, prop in sort, group:
             # if all else fails, compare the ids
             return cmp(a[0], b[0])
 
@@ -1909,13 +1892,18 @@ class IssueClass(Class, roundupdb.IssueClass):
         if not properties.has_key('files'):
             properties['files'] = hyperdb.Multilink("file")
         if not properties.has_key('nosy'):
-            properties['nosy'] = hyperdb.Multilink("user")
+            # note: journalling is turned off as it really just wastes
+            # space. this behaviour may be overridden in an instance
+            properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
         if not properties.has_key('superseder'):
             properties['superseder'] = hyperdb.Multilink(classname)
         Class.__init__(self, db, classname, **properties)
 
 #
 #$Log: not supported by cvs2svn $
+#Revision 1.65  2002/08/30 08:35:45  richard
+#minor edits
+#
 #Revision 1.64  2002/08/22 07:57:11  richard
 #Consistent quoting
 #
index 7ac724bb4fac5bd3644811d981cef3fc1f51cb9b..95b5befbd676548c9bad8542e2b5308a900babd2 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: back_gadfly.py,v 1.6 2002-08-30 08:35:16 richard Exp $
+# $Id: back_gadfly.py,v 1.7 2002-09-01 04:32:30 richard Exp $
 __doc__ = '''
 About Gadfly
 ============
@@ -1469,8 +1469,8 @@ class Class(hyperdb.Class):
             sort spec
 
             "filterspec" is {propname: value(s)}
-            "sort" is ['+propname', '-propname', 'propname', ...]
-            "group is ['+propname', '-propname', 'propname', ...]
+            "sort" and "group" are (dir, prop) where dir is '+', '-' or None
+                               and prop is a prop name or None
             "search_matches" is {nodeid: marker}
         '''
         cn = self.classname
@@ -1511,26 +1511,24 @@ class Class(hyperdb.Class):
         # figure the order by clause
         orderby = []
         ordercols = []
-        if sort:
-            for entry in sort:
-                if entry[0] != '-':
-                    orderby.append('_'+entry)
-                    ordercols.append(entry)
-                else:
-                    orderby.append('_'+entry[1:]+' desc')
-                    ordercols.append(entry)
+        if sort is not None:
+            if sort[0] != '-':
+                orderby.append('_'+sort[1])
+                ordercols.append(sort[1])
+            else:
+                orderby.append('_'+sort[1]+' desc')
+                ordercols.append(sort[1])
 
         # figure the group by clause
         groupby = []
         groupcols = []
-        if group:
-            for entry in group:
-                if entry[0] != '-':
-                    groupby.append('_'+entry)
-                    groupcols.append(entry)
-                else:
-                    groupby.append('_'+entry[1:]+' desc')
-                    groupcols.append(entry[1:])
+        if group is not None:
+            if group[0] != '-':
+                groupby.append('_'+group[1])
+                groupcols.append(group[1])
+            else:
+                groupby.append('_'+group[1]+' desc')
+                groupcols.append(group[1])
 
         # construct the SQL
         frum = ','.join(frum)
@@ -1743,13 +1741,18 @@ class IssueClass(Class, roundupdb.IssueClass):
         if not properties.has_key('files'):
             properties['files'] = hyperdb.Multilink("file")
         if not properties.has_key('nosy'):
-            properties['nosy'] = hyperdb.Multilink("user")
+            # note: journalling is turned off as it really just wastes
+            # space. this behaviour may be overridden in an instance
+            properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
         if not properties.has_key('superseder'):
             properties['superseder'] = hyperdb.Multilink(classname)
         Class.__init__(self, db, classname, **properties)
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.6  2002/08/30 08:35:16  richard
+# very basic filter support
+#
 # Revision 1.5  2002/08/23 05:33:32  richard
 # implemented multilink changes (and a unit test)
 #
index d78b27f9f8034f176cf7c29718d4c42938faad02..89193822f499a5be15aacfbdd4082f98087052f7 100755 (executable)
@@ -638,7 +638,9 @@ class Class:
         # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
         # filterspec is a dict {propname:value}
         # sort and group are lists of propnames
-        
+        # sort and group are (dir, prop) where dir is '+', '-' or None
+        #                    and prop is a prop name or None
+
         where = {'_isdel':0}
         mlcriteria = {}
         regexes = {}
@@ -745,10 +747,10 @@ class Class:
         if sort or group:
             sortspec = []
             rev = []
-            for propname in group + sort:
+            for dir, propname in group, sort:
+                if propname is None: continue
                 isreversed = 0
-                if propname[0] == '-':
-                    propname = propname[1:]
+                if dir == '-':
                     isreversed = 1
                 try:
                     prop = getattr(v, propname)
@@ -1013,7 +1015,9 @@ class IssueClass(Class, roundupdb.IssueClass):
         if not properties.has_key('files'):
             properties['files'] = hyperdb.Multilink("file")
         if not properties.has_key('nosy'):
-            properties['nosy'] = hyperdb.Multilink("user")
+            # note: journalling is turned off as it really just wastes
+            # space. this behaviour may be overridden in an instance
+            properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
         if not properties.has_key('superseder'):
             properties['superseder'] = hyperdb.Multilink(classname)
         Class.__init__(self, db, classname, **properties)
index bcc0a16157a5dcbbf2ddb29d142c120b7da7d0ef..81695009a719c6a80a4aa59eb6077e4a4d51882d 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.1 2002-08-30 08:28:44 richard Exp $
+# $Id: client.py,v 1.2 2002-09-01 04:32:30 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -296,6 +296,7 @@ class Client:
         'login':    'login_action',
         'logout':   'logout_action',
         'register': 'register_action',
+        'search':   'search_action',
     }
     def handle_action(self):
         ''' Determine whether there should be an _action called.
@@ -308,6 +309,7 @@ class Client:
              "login"     -> self.login_action
              "logout"    -> self.logout_action
              "register"  -> self.register_action
+             "search"    -> self.search_action
 
         '''
         if not self.form.has_key(':action'):
@@ -512,23 +514,29 @@ class Client:
              "files" property. Attach the file to the message created from
              the __note if it's supplied.
         '''
-        cn = self.classname
-        cl = self.db.classes[cn]
+        cl = self.db.classes[self.classname]
 
         # check permission
         userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Edit', userid, cn):
+        if not self.db.security.hasPermission('Edit', userid, self.classname):
             self.error_message.append(
-                _('You do not have permission to edit %s' %cn))
+                _('You do not have permission to edit %(classname)s' %
+                self.__dict__))
+            return
 
         # perform the edit
-        props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
+        try:
+            props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
 
-        # make changes to the node
-        props = self._changenode(props)
+            # make changes to the node
+            props = self._changenode(props)
 
-        # handle linked nodes 
-        self._post_editnode(self.nodeid)
+            # handle linked nodes 
+            self._post_editnode(self.nodeid)
+
+        except (ValueError, KeyError), message:
+            self.error_message.append(_('Error: ') + str(message))
+            return
 
         # commit now that all the tricky stuff is done
         self.db.commit()
@@ -545,8 +553,8 @@ class Client:
             message = _('nothing changed')
 
         # redirect to the item's edit page
-        raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, cn, self.nodeid,  
-            urllib.quote(message))
+        raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
+            self.nodeid,  urllib.quote(message))
 
     def newitem_action(self):
         ''' Add a new item to the database.
@@ -554,11 +562,10 @@ class Client:
             This follows the same form as the edititem_action
         '''
         # check permission
-        cn = self.classname
         userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Edit', userid, cn):
+        if not self.db.security.hasPermission('Edit', userid, self.classname):
             self.error_message.append(
-                _('You do not have permission to create %s' %cn))
+                _('You do not have permission to create %s' %self.classname))
 
         # XXX
 #        cl = self.db.classes[cn]
@@ -583,17 +590,21 @@ class Client:
             self.nodeid = nid
 
             # and some nice feedback for the user
-            message = _('%(classname)s created ok')%{'classname': cn}
+            message = _('%(classname)s created ok')%self.__dict__
+        except (ValueError, KeyError), message:
+            self.error_message.append(_('Error: ') + str(message))
+            return
         except:
             # oops
             self.db.rollback()
             s = StringIO.StringIO()
             traceback.print_exc(None, s)
             self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
+            return
 
         # redirect to the new item's page
-        raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, cn, nid,  
-            urllib.quote(message))
+        raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
+            nid,  urllib.quote(message))
 
     def genericedit_action(self):
         ''' Performs an edit of all of a class' items in one go.
@@ -603,12 +614,10 @@ class Client:
             non-existent ID) and removed lines are retired.
         '''
         userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Edit', userid):
+        if not self.db.security.hasPermission('Edit', userid, self.classname):
             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]
+        cl = self.db.classes[self.classname]
         idlessprops = cl.getprops(protected=0).keys()
         props = ['id'] + idlessprops
 
@@ -638,7 +647,7 @@ class Client:
 
             # confirm correct weight
             if len(idlessprops) != len(values):
-                w(_('Not enough values on line %(line)s'%{'line':line}))
+                message=(_('Not enough values on line %(line)s'%{'line':line}))
                 return
 
             # extract the new values
@@ -668,7 +677,7 @@ class Client:
         message = _('items edited OK')
 
         # redirect to the class' edit page
-        raise Redirect, '%s/%s?:ok_message=%s'%(self.base, cn
+        raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname
             urllib.quote(message))
 
     def _changenode(self, props):
@@ -797,6 +806,19 @@ class Client:
                     link = self.db.classes[link]
                     link.set(nodeid, **{property: nid})
 
+    def search_action(self):
+        ''' Mangle some of the form variables.
+
+            Set the form ":filter" variable based on the values of the
+            filter variables - if they're set to anything other than
+            "dontcare" then add them to :filter.
+        '''
+        # add a faked :filter form variable for each filtering prop
+        props = self.db.classes[self.classname].getprops()
+        for key in self.form.keys():
+            if not props.has_key(key): continue
+            if not self.form[key].value: continue
+            self.form.value.append(cgi.MiniFieldStorage(':filter', key))
 
     def remove_action(self,  dre=re.compile(r'([^\d]+)(\d+)')):
         # XXX handle this !
@@ -838,7 +860,11 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
         if isinstance(proptype, hyperdb.String):
             value = form[key].value.strip()
         elif isinstance(proptype, hyperdb.Password):
-            value = password.Password(form[key].value.strip())
+            value = form[key].value.strip()
+            if not value:
+                # ignore empty password values
+                continue
+            value = password.Password(value)
         elif isinstance(proptype, hyperdb.Date):
             value = form[key].value.strip()
             if value:
index a01fa7c768b2740d13e176a2e2eb12b0e22989f2..2a04f5ab44ea1eba69522dccd351f10bd4587ae7 100644 (file)
@@ -1,4 +1,4 @@
-import sys, cgi, urllib, os
+import sys, cgi, urllib, os, re
 
 from roundup import hyperdb, date
 from roundup.i18n import _
@@ -136,15 +136,15 @@ class HTMLClass:
     def __repr__(self):
         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
 
-    def __getattr__(self, attr):
+    def __getitem__(self, item):
         ''' return an HTMLItem instance'''
-        #print 'getattr', (self, attr)
-        if attr == 'creator':
+        #print 'getitem', (self, attr)
+        if item == 'creator':
             return HTMLUser(self.client)
 
-        if not self.props.has_key(attr):
-            raise AttributeError, attr
-        prop = self.props[attr]
+        if not self.props.has_key(item):
+            raise KeyError, item
+        prop = self.props[item]
 
         # look up the correct HTMLProperty class
         for klass, htmlklass in propclasses:
@@ -153,10 +153,17 @@ class HTMLClass:
             else:
                 value = None
             if isinstance(prop, klass):
-                return htmlklass(self.db, '', prop, attr, value)
+                return htmlklass(self.db, '', prop, item, value)
 
         # no good
-        raise AttributeError, attr
+        raise KeyError, item
+
+    def __getattr__(self, attr):
+        ''' convenience access '''
+        try:
+            return self[attr]
+        except KeyError:
+            raise AttributeError, attr
 
     def properties(self):
         ''' Return HTMLProperty for all props
@@ -248,29 +255,33 @@ class HTMLItem:
     def __repr__(self):
         return '<HTMLItem(0x%x) %s %s>'%(id(self), self.classname, self.nodeid)
 
-    def __getattr__(self, attr):
+    def __getitem__(self, item):
         ''' return an HTMLItem instance'''
-        #print 'getattr', (self, attr)
-        if attr == 'id':
+        if item == 'id':
             return self.nodeid
-
-        if not self.props.has_key(attr):
-            raise AttributeError, attr
-        prop = self.props[attr]
+        if not self.props.has_key(item):
+            raise KeyError, item
+        prop = self.props[item]
 
         # get the value, handling missing values
-        value = self.klass.get(self.nodeid, attr, None)
+        value = self.klass.get(self.nodeid, item, None)
         if value is None:
-            if isinstance(self.props[attr], hyperdb.Multilink):
+            if isinstance(self.props[item], hyperdb.Multilink):
                 value = []
 
         # look up the correct HTMLProperty class
         for klass, htmlklass in propclasses:
             if isinstance(prop, klass):
-                return htmlklass(self.db, self.nodeid, prop, attr, value)
+                return htmlklass(self.db, self.nodeid, prop, item, value)
 
-        # no good
-        raise AttributeError, attr
+        raise KeyErorr, item
+
+    def __getattr__(self, attr):
+        ''' convenience access to properties '''
+        try:
+            return self[attr]
+        except KeyError:
+            raise AttributeError, attr
     
     def submit(self, label="Submit Changes"):
         ''' Generate a submit button (and action hidden element)
@@ -613,7 +624,7 @@ class LinkHTMLProperty(HTMLProperty):
     def plain(self, escape=0):
         if self.value is None:
             return _('[unselected]')
-        linkcl = self.db.classes[self.klass.classname]
+        linkcl = self.db.classes[self.prop.classname]
         k = linkcl.labelprop(1)
         value = str(linkcl.get(self.value, k))
         if escape:
@@ -688,10 +699,10 @@ class LinkHTMLProperty(HTMLProperty):
             s = 'selected '
         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
         if linkcl.getprops().has_key('order'):  
-            sort_on = 'order'  
+            sort_on = ('+', 'order')
         else:  
-            sort_on = linkcl.labelprop() 
-        options = linkcl.filter(None, conditions, [sort_on], []) 
+            sort_on = ('+', linkcl.labelprop())
+        options = linkcl.filter(None, conditions, sort_on, (None, None))
         for optionid in options:
             option = linkcl.get(optionid, k)
             s = ''
@@ -735,6 +746,12 @@ class MultilinkHTMLProperty(HTMLProperty):
         value = self.value[num]
         return HTMLItem(self.db, self.prop.classname, value)
 
+    def reverse(self):
+        ''' return the list in reverse order '''
+        l = self.value[:]
+        l.reverse()
+        return [HTMLItem(self.db, self.prop.classname, value) for value in l]
+
     def plain(self, escape=0):
         linkcl = self.db.classes[self.prop.classname]
         k = linkcl.labelprop(1)
@@ -772,10 +789,10 @@ class MultilinkHTMLProperty(HTMLProperty):
 
         linkcl = self.db.getclass(self.prop.classname)
         if linkcl.getprops().has_key('order'):  
-            sort_on = 'order'  
+            sort_on = ('+', 'order')
         else:  
-            sort_on = linkcl.labelprop()
-        options = linkcl.filter(None, conditions, [sort_on], []
+            sort_on = ('+', linkcl.labelprop())
+        options = linkcl.filter(None, conditions, sort_on, (None,None)
         height = height or min(len(options), 7)
         l = ['<select multiple name="%s" size="%s">'%(self.name, height)]
         k = linkcl.labelprop(1)
@@ -858,19 +875,48 @@ class HTMLRequest:
         if self.form.has_key(':columns'):
             for entry in handleListCGIValue(self.form[':columns']):
                 self.columns[entry] = 1
-        self.sort = []
+
+        # sorting
+        self.sort = (None, None)
         if self.form.has_key(':sort'):
-            self.sort = handleListCGIValue(self.form[':sort'])
-        self.group = []
+            sort = self.form[':sort'].value
+            if sort.startswith('-'):
+                self.sort = ('-', sort[1:])
+            else:
+                self.sort = ('+', sort)
+        if self.form.has_key(':sortdir'):
+            self.sort = ('-', self.sort[1])
+
+        # grouping
+        self.group = (None, None)
         if self.form.has_key(':group'):
-            self.group = handleListCGIValue(self.form[':group'])
+            group = self.form[':group'].value
+            if group.startswith('-'):
+                self.group = ('-', group[1:])
+            else:
+                self.group = ('+', group)
+        if self.form.has_key(':groupdir'):
+            self.group = ('-', self.group[1])
+
+        # filtering
         self.filter = []
         if self.form.has_key(':filter'):
             self.filter = handleListCGIValue(self.form[':filter'])
         self.filterspec = {}
+        props = self.client.db.getclass(self.classname).getprops()
         for name in self.filter:
             if self.form.has_key(name):
-                self.filterspec[name]=handleListCGIValue(self.form[name])
+                prop = props[name]
+                if (isinstance(prop, hyperdb.Link) or
+                        isinstance(prop, hyperdb.Multilink)):
+                    self.filterspec[name] = handleListCGIValue(self.form[name])
+                else:
+                    self.filterspec[name] = self.form[name].value
+
+        # full-text search argument
+        self.search_text = None
+        if self.form.has_key(':search_text'):
+            self.search_text = self.form[':search_text'].value
 
     def __str__(self):
         d = {}
@@ -896,30 +942,32 @@ filterspec: %(filterspec)r
 env: %(env)s
 '''%d
 
-    def indexargs_form(self):
+    def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
+            filterspec=1):
         ''' return the current index args as form elements '''
         l = []
         s = '<input type="hidden" name="%s" value="%s">'
-        if self.columns:
+        if columns and self.columns:
             l.append(s%(':columns', ','.join(self.columns.keys())))
-        if self.sort:
-            l.append(s%(':sort', ','.join(self.sort)))
-        if self.group:
-            l.append(s%(':group', ','.join(self.group)))
-        if self.filter:
+        if sort and self.sort is not None:
+            l.append(s%(':sort', self.sort))
+        if group and self.group is not None:
+            l.append(s%(':group', self.group))
+        if filter and self.filter:
             l.append(s%(':filter', ','.join(self.filter)))
-        for k,v in self.filterspec.items():
-            l.append(s%(k, ','.join(v)))
+        if filterspec:
+            for k,v in self.filterspec.items():
+                l.append(s%(k, ','.join(v)))
         return '\n'.join(l)
 
     def indexargs_href(self, url, args):
         l = ['%s=%s'%(k,v) for k,v in args.items()]
         if self.columns:
             l.append(':columns=%s'%(','.join(self.columns.keys())))
-        if self.sort:
-            l.append(':sort=%s'%(','.join(self.sort)))
-        if self.group:
-            l.append(':group=%s'%(','.join(self.group)))
+        if self.sort is not None:
+            l.append(':sort=%s'%self.sort)
+        if self.group is not None:
+            l.append(':group=%s'%self.group)
         if self.filter:
             l.append(':filter=%s'%(','.join(self.filter)))
         for k,v in self.filterspec.items():
@@ -954,7 +1002,12 @@ function help_window(helpurl, width, height) {
 
         # get the list of ids we're batching over
         klass = self.client.db.getclass(self.classname)
-        l = klass.filter(None, filterspec, sort, group)
+        if self.search_text:
+            matches = self.client.db.indexer.search(
+                re.findall(r'\b\w{2,25}\b', self.search_text), klass)
+        else:
+            matches = None
+        l = klass.filter(matches, filterspec, sort, group)
 
         # figure batch args
         if self.form.has_key(':pagesize'):
@@ -973,6 +1026,8 @@ class Batch(ZTUtils.Batch):
     def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
         self.client = client
         self.classname = classname
+        self.last_index = self.last_item = None
+        self.current_item = None
         ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
 
     # overwrite so we can late-instantiate the HTMLItem instance
@@ -983,13 +1038,28 @@ class Batch(ZTUtils.Batch):
         
         if index >= self.length: raise IndexError, index
 
+        # move the last_item along - but only if the fetched index changes
+        # (for some reason, index 0 is fetched twice)
+        if index != self.last_index:
+            self.last_item = self.current_item
+            self.last_index = index
+
         # wrap the return in an HTMLItem
-        return HTMLItem(self.client.db, self.classname,
+        self.current_item = HTMLItem(self.client.db, self.classname,
             self._sequence[index+self.first])
+        return self.current_item
+
+    def propchanged(self, property):
+        ''' Detect if the property marked as being the group property
+            changed in the last iteration fetch
+        '''
+        if (self.last_item is None or
+                self.last_item[property] != self.current_item[property]):
+            return 1
+        return 0
 
     # override these 'cos we don't have access to acquisition
     def previous(self):
-        print self.start
         if self.start == 1:
             return None
         return Batch(self.client, self.classname, self._sequence, self._size,
index 29242df27c0ede9e4125657637e0e049af8f1928..c5a8ccf3354296e87b30b1a9d470bd66da0f6c87 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: dbinit.py,v 1.23 2002-08-30 08:30:45 richard Exp $
+# $Id: dbinit.py,v 1.24 2002-09-01 04:32:30 richard Exp $
 
 import os
 
@@ -111,6 +111,11 @@ def open(name=None):
     p = db.security.getPermission('Email Access')
     db.security.addPermissionToRole('User', p)
 
+    # May users view other user information? Comment these lines out
+    # if you don't want them to
+    p = db.security.getPermission('View', 'user')
+    db.security.addPermissionToRole('User', p)
+
     # Assign the appropriate permissions to the anonymous user's Anonymous
     # Role. Choices here are:
     # - Allow anonymous users to register through the web
@@ -185,6 +190,9 @@ def init(adminpw):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.23  2002/08/30 08:30:45  richard
+# allow perms on user class
+#
 # Revision 1.22  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).
index b8e7ab0c14d9cfaf08058a6ef7a052925c4ff241..1d5d38c61136bb260013667a696644f2762e7f0f 100644 (file)
@@ -5,7 +5,8 @@
  whatever. It's a good idea to have the issues on the front page though
 -->
 <span tal:replace="structure python:db.issue.renderWith('index',
-    sort=['-activity'], group=['priority'], filter=['status'],
-    columns={'id':1,'activity':1,'title':1,'creator':1,'assignedto':1},
+    sort=('-', 'activity'), group=('+', 'priority'), filter=['status'],
+    columns={'id':1,'activity':1,'title':1,'creator':1,'assignedto':1,
+             'priority':1},
     filterspec={'status':['-1','1','2','3','4','5','6','7']})" />
 
index f726492161f143f5db59bcec15b2ba15d944f602..13b316fbb6377ae919e563dad4fb1a398bfeae06 100644 (file)
 <!-- dollarId: issue.index,v 1.2 2001/07/29 04:07:37 richard Exp dollar-->
 <tal:block tal:define="batch request/batch">
- <table width="100%" border=0 cellspacing=0 cellpadding=2 class="list">
- <tr class="list-header">
-  <th align="left" tal:condition="exists:request/columns/id">ID</th>
-  <th align="left" tal:condition="exists:request/columns/activity">Activity</th>
-  <th align="left" tal:condition="exists:request/columns/priority">Priority</th>
-  <th align="left" tal:condition="exists:request/columns/title">Title</th>
-  <th align="left" tal:condition="exists:request/columns/status">Status</th>
-  <th align="left" tal:condition="exists:request/columns/assignedto">Assigned To</th>
- </tr>
- <tr tal:repeat="i batch"
-    tal:attributes="class python:['row-normal', 'row-alt'][repeat['i'].even()]">
-  <td valign="top" tal:condition="exists:request/columns/id"
-     tal:content="i/id"></td>
-  <td valign="top" tal:condition="exists:request/columns/activity"
-     tal:content="i/activity/reldate"></td>
-  <td valign="top" tal:condition="exists:request/columns/priority"
-     tal:content="i/priority"></td>
-  <td valign="top" tal:condition="exists:request/columns/title">
-   <a tal:attributes="href string:issue${i/id}"
-      tal:content="python:i.title.value or '[no title]'">title</a>
+ <table class="list">
+  <tr>
+   <th tal:condition="exists:request/columns/priority">Priority</th>
+   <th tal:condition="exists:request/columns/id">ID</th>
+   <th tal:condition="exists:request/columns/activity">Activity</th>
+   <th tal:condition="exists:request/columns/title">Title</th>
+   <th tal:condition="exists:request/columns/status">Status</th>
+   <th tal:condition="exists:request/columns/creator">Created&nbsp;By</th>
+   <th tal:condition="exists:request/columns/assignedto">Assigned&nbsp;To</th>
+  </tr>
+ <tal:block tal:repeat="i batch">
+  <tr tal:condition="python:batch.propchanged(request.group[1])">
+   <th tal:attributes="colspan python:len(request.columns)"
+       tal:content="python:i[request.group[1]]" class="group">
+   </th>
+  </tr>
+  <tr tal:attributes="class python:['normal', 'alt'][repeat['i'].even()]">
+   <td tal:condition="exists:request/columns/priority"
+      tal:content="i/priority"></td>
+   <td tal:condition="exists:request/columns/id"
+      tal:content="i/id"></td>
+   <td tal:condition="exists:request/columns/activity"
+      tal:content="i/activity/reldate"></td>
+   <td tal:condition="exists:request/columns/title">
+    <a tal:attributes="href string:issue${i/id}"
+       tal:content="python:i.title.value or '[no title]'">title</a>
+   </td>
+   <td tal:condition="exists:request/columns/status"
+       tal:content="i/status"></td>
+   <td tal:condition="exists:request/columns/creator"
+       tal:content="i/creator"></td>
+   <td tal:condition="exists:request/columns/assignedto"
+       tal:content="i/assignedto"></td>
+  </tr>
+ </tal:block>
+ <tr>
+  <td style="padding: 0" tal:attributes="colspan python:len(request.columns)">
+   <table class="list">
+    <tr><th style="text-align: left">
+     <a tal:define="prev batch/previous" tal:condition="prev"
+        tal:attributes="href python:request.indexargs_href(request.classname,
+        {':startwith':prev.start, ':pagesize':prev.size})">&lt;&lt; previous</a>
+     &nbsp;
+    </th>
+    <th style="text-align: right">
+     <a tal:define="next batch/next" tal:condition="next"
+        tal:attributes="href python:request.indexargs_href(request.classname,
+         {':startwith':next.start, ':pagesize':next.size})">next &gt;&gt;</a>
+     &nbsp;
+    </th></tr>
+   </table>
   </td>
-  <td valign="top" tal:condition="exists:request/columns/status"
-      tal:content="i/status"></td>
-  <td valign="top" tal:condition="exists:request/columns/assignedto"
-      tal:content="i/assignedto/username | default"></td>
  </tr>
- </table>
- <table width="100%" border=0 cellspacing=0 cellpadding=2>
-  <tr class="list-header">
-   <td width="50%" align="left">
-<a tal:define="prev batch/previous" tal:condition="prev"
-   tal:attributes="href python:request.indexargs_href(request.classname,
-    {':startwith':prev.start, ':pagesize':prev.size})">&lt;&lt; previous</a>
-   </td><td width="50%" align="right">
-<a tal:define="next batch/next" tal:condition="next"
-   tal:attributes="href python:request.indexargs_href(request.classname,
-    {':startwith':next.start, ':pagesize':next.size})">next &gt;&gt;</a>
-  </tr>
-  <tr class="list-header" tal:condition="batch">
-   <td colspan="2">
-    <form method="GET" tal:attributes="action request/classname">
-     Pagesize: <input type="text" name=":pagesize" size="3" value="50">
-     Starting at: <input type="text" name=":startwith" size="3" value="0">
-     <br>
-     <xml:block tal:condition="python:'status' in request.filter">
-      Status:
-      <input type="hidden" name=":filter" value="status">
-      <select name="status">
-       <option value="-1,1,2,3,4,5,6,7"
-         tal:attributes="selected python:'-1,1,2,3,4,5,6,7'.split(',')==request.filterspec['status']">not resolved</option>
-       <option value="-1"
-         tal:attributes="selected python:['-1']==request.filterspec['status']">not selected</option>
-       <option tal:repeat="s db/status/list"
-         tal:attributes="value s/id;
-                         selected python:[s]==request.filterspec['status']"
-         tal:content="s/name">status to filter on</option>
-      </select>
-     </xml:block>
+</table>
 
-     <xml:block tal:condition="python:'assignedto' in request.filter">
-      Assigned To:
-      <input type="hidden" name=":filter" value="assignedto">
-      <select name="assignedto">
-       <option value="-1"
-         tal:attributes="selected python:['-1']==request.filterspec['assignedto']">not selected</option>
-       <option tal:repeat="s db/user/list"
-         tal:attributes="value s/id;
-                         selected python:[s]==request.filterspec['assignedto']"
-         tal:content="s/username">person to filter on</option>
-      </select>
-     </xml:block>
-
-     <br>
-     Show:
-     <tal:block tal:repeat="col python:'id activity priority title status assignedto'.split()">
-      <span tal:replace="col" /><input type="checkbox" name=":columns" tal:attributes="value col;
-        checked python:request.columns.has_key(col)">
-     </tal:block>
-     <br>
-     Sort on:
-     <select name=":sort">
-      <option tal:repeat="col python:request.columns.keys()"
-             tal:attributes="value col; selected python:col in request.sort"
+<form method="GET" tal:attributes="action request/classname">
+ <tal:block tal:replace="structure python:request.indexargs_form(sort=0, group=0)" />
+ <table class="form">
+  <tr tal:condition="batch">
+   <th>Sort on:</th>
+   <td>
+    <select name=":sort">
+     <option value="">- nothing -</option>
+     <option tal:repeat="col python:request.columns.keys()"
+             tal:attributes="value col; selected python:col == request.sort[1]"
              tal:content="col">column</option>
      </select>
-     Group on:
+   </td>
+   <th>Descending:</th>
+   <td><input type="checkbox" name=":sortdir"
+              tal:attributes="checked python:request.sort[0] == '-'"> 
+   </td>
+  </tr>
+  <tr>
+   <th>Group on:</th>
+   <td>
      <select name=":group">
+      <option value="">- nothing -</option>
       <option tal:repeat="col python:request.columns.keys()"
-             tal:attributes="value col; selected python:col in request.group"
+             tal:attributes="value col; selected python:col == request.group[1]"
              tal:content="col">column</option>
      </select>
-     <input type="submit" value="Redisplay">
-    </form>
+   </td>
+   <th>Descending:</th>
+   <td><input type="checkbox" name=":groupdir"
+              tal:attributes="checked python:request.group[0] == '-'"> 
    </td>
   </tr>
+  <tr><td colspan="4"><input type="submit" value="Redisplay"></td></tr>
  </table>
+</form>
+
 </tal:block>
 
index d07fbec972987567b7d27b302ccba443f0dd6241..750f35cd44eb4bcef153e54c0d0cc2ddbaf49a62 100644 (file)
@@ -1,47 +1,44 @@
 <!-- dollarId: issue.item,v 1.4 2001/08/03 01:19:43 richard Exp dollar-->
 <form method="POST" onSubmit="return submit_once()"
       enctype="multipart/form-data">
-<table border=0 cellspacing=0 cellpadding=2>
-
-<tr class="form">
- <td width=1% nowrap align=right><span class="form-label">Title</span></td>
+<table border=0 cellspacing=0 cellpadding=2 class="form">
+<tr>
+ <th nowrap>Title</th>
  <td colspan=3 class="form-text" tal:content="structure python:issue.title.field(size=60)">title</td>
 </tr>
 
 <tr class="form">
- <td width=1% nowrap align=right><span class="form-label">Created</span></td>
+ <th nowrap>Created</th>
  <td class="form-text" tal:content="string:${issue/creation} (${issue/creator/username})">creation (creator)</td>
- <td width=1% nowrap align=right><span class="form-label">Last activity</span></td>
+ <th nowrap>Last activity</th>
  <td class="form-text" tal:content="issue/activity">activity</td>
 </tr>
 
 <tr class="form">
- <td width=1% nowrap align=right><span class="form-label">Priority</span></td>
+ <th nowrap>Priority</th>
  <td class="form-text" tal:content="structure issue/priority/menu">priority</td>
- <td width=1% nowrap align=right><span class="form-label">Status</span></td>
+ <th nowrap>Status</th>
  <td class="form-text" tal:content="structure issue/status/menu">status</td>
 </tr>
 
 <tr class="form">
- <td width=1% nowrap align=right><span class="form-label">Superseder</span></td>
- <td class="form-text">
+ <th nowrap>Superseder</th>
+ <td>
   <span tal:replace="structure python:issue.superseder.field(showid=1)" />
   <span tal:replace="structure python:db.issue.classhelp('id,title', label='list', width=500)" />
   <span tal:condition="issue/superseder">
    <br>View: <span tal:replace="structure python:issue.superseder.link(showid=1)" />
   </span>
  </td>
- <td width=1% nowrap align=right><span class="form-label">Nosy List</span></td>
- <td class="form-text">
+ <th nowrap>Nosy List</th>
+ <td>
   <span tal:replace="structure issue/nosy/field" />
   <span tal:replace="structure python:db.user.classhelp('username,realname,address,phone', label='list', width=500)" />
  </td>
 </tr>
 
 <tr class="form">
- <td width=1% nowrap align=right>
-  <span class="form-label">Assigned To</span>
- </td>
+ <th nowrap>Assigned To</th>
  <td class="form-text" tal:content="structure issue/assignedto/menu">
   assignedto menu
  </td>
 </tr>
 
 <tr class="form">
- <td width=1% nowrap align=right> 
-  <span class="form-label">Change Note</span>
- </td>
+ <th nowrap>Change Note</th>
  <td colspan=3 class="form-text">
   <textarea name="__note" wrap="hard" rows="5" cols="60"></textarea>
  </td>
 </tr>
 
 <tr class="form">
- <td width=1% nowrap align=right>
-  <span class="form-label">File</span>
- </td>
+ <th nowrap>File</th>
  <td colspan=3 class="form-text">
   <input type="file" name="__file" size="60">
  </td>
   submit button will go here
  </td>
 </tr>
-
 </table>
 
-<table width="100%" cellspacing=0 cellpadding=0 border=0 tal:condition="exists:item">
- <tr class="msg-header">
-  <td colspan=2><b>Messages</b></td>
</tr>
<tal:block tal:condition="issue/messages" tal:repeat="msg issue/messages">
-  <tr>
-   <td class="msg-header" tal:content="msg/date">date</td>
-   <td class="msg-header" tal:content="msg/author">author</td>
-  </tr>
-  <tr>
-   <td class="msg-content" colspan="2" tal:content="msg/content">content</td>
-  </tr>
- </tal:block>
+<tal:block tal:condition="exists:item">
+ <table class="messages" tal:condition="issue/messages">
+  <tr><th colspan=2 class="header">Messages</th></tr>
 <tal:block tal:repeat="msg issue/messages/reverse">
  <tr>
+    <th tal:content="string:Author: ${msg/author}">author</th>
+    <th tal:content="string:Date: ${msg/date}">date</th>
+   </tr>
+   <tr>
+    <td colspan="2"><pre tal:content="msg/content">content</pre></td>
+   </tr>
+  </tal:block>
+ </table>
 
- <tr class="file-header">
-  <td colspan="2"><b>Files</b></td>
- </tr>
- <tal:block tal:condition="issue/files">
-  <tr>
-   <td class="file-header">File name</td>
-   <td class="file-header">Uploaded</td>
-  </tr>
+ <table class="files" tal:condition="issue/files">
+  <tr><th colspan="2" class="header">Files</th></tr>
+  <tr><th>File name</th><th>Uploaded</th></tr>
   <tr tal:repeat="file issue/files">
    <td>
     <a tal:attributes="href string:file${file/id}/${file/name}"
     <span tal:content="file/creation">creation date</span>
    </td>
   </tr>
- </tal:block>
+ </table>
 
- <tr class="history-header">
-  <td colspan="2"><b>History</b></td>
- </tr>
- <tr>
-  <td colspan="2" tal:content="structure issue/history">history</td>
- </tr>
-</table>
+ <table class="history">
+  <tr><th colspan="2" class="header">History</th></tr>
+  <tr><td colspan="2" tal:content="structure issue/history">history</td></tr>
+ </table>
+</tal:block>
 
 </form>
index accf7aed62b41b5035f4aa799de3a28437c3216d..30ed9884978fd2d8314a4a3343251a843ec66a63 100644 (file)
 <form method="GET" tal:attributes="action request/classname">
 <input type="hidden" name=":action" value="search">
 
-Columns you may display:
-<tal:block
-  tal:repeat="n python:'id activity priority title status assignedto'.split()">
-  <span tal:content="n">name</span>
-  <input type="checkbox" name=":columns"
-       tal:attributes="value n;
-                       checked python:request.columns.has_key(n)">
-</tal:block>
-<br>
-Sort on: <input type="radio" name=":sort" tal:repeat="n request/sort"
-       tal:attributes="value n;
-                       checked python:n in request.sort">
-<br>
-Group on:
-<input type="radio" name=":group" tal:repeat="n request/group"
-       tal:attributes="value n;
-                       checked python:n in request.group">
-
-<br>
-Priority:
-<span tal:replace="structure issue/priority/menu" />
-
-<br>
-Status:
-<select name="status">
- <option value="dontcare">don't care</option>
- <option value="-1,1,2,3,4,5,6,7">not resolved</option>
- <option value="-1">not selected</option>
- <option value="dontcare">------------</option>
- <option tal:repeat="s db/status/list" tal:attributes="value s/id"
-   tal:content="s/name">status to filter on</option>
-</select>
-
-<br>
-Assigned To:
-<select name="assignedto">
- <option value="dontcare">don't care</option>
- <option tal:attributes="value request/user/id">assigned to me</option>
- <option value="-1">unassigned</option>
- <option value="-1">------------</option>
- <option tal:repeat="s db/user/list" tal:attributes="value s/id"
-   tal:content="s/username">user to filter on</option>
-</select>
-
-<tal:block tal:repeat="n python:request.filterspec.keys()">
- Filter: <input tal:repeat="v python:request.filterspec[n]"
-                tal:attributes="name n; value v">
-</tal:block>
-
-<br>
-Pagesize:
- <input type="text" name=":pagesize" size="3" value="50">
-
-<br>
-Start With:
-<input type="text" name=":startwith" size="3" value="0">
-
-<br>
-<input type="submit" value="Search">
+<table class="form" tal:define="
+   cols python:'id activity priority title status assignedto'.split();
+   defsort python:['activity'];
+   defgroup python:['priority'];
+   defdisp python:'id activity title status assignedto'.split()">
+
+<tr class="form-header">
+ <th>&nbsp;</th>
+ <th>Filter on</th><th>Display</th><th>Sort on</th><th>Group on</th>
+</tr>
+
+<tr>
+ <th>All text*:</th>
+ <td><input name=":search_text"></td>
+ <td>&nbsp;</td>
+ <td>&nbsp;</td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <th>Title:</th>
+ <td><input name="title"></td>
+ <td><input type="checkbox" name=":columns" value="title" checked></td>
+ <td><input type="radio" name=":sort" value="title"></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <th>Created:</th>
+ <td><input name="activity"></td>
+ <td><input type="checkbox" name=":columns" value="created"></td>
+ <td><input type="radio" name=":sort" value="created"></td>
+ <td><input type="radio" name=":group" value="created"></td>
+</tr>
+
+<tr>
+ <th>Creator:</th>
+ <td>
+  <select name="creator">
+   <option value="">don't care</option>
+   <option tal:attributes="value request/user/id">created by me</option>
+   <option value="-1">------------</option>
+   <option tal:repeat="s db/user/list" tal:attributes="value s/id"
+     tal:content="s/username">user to filter on</option>
+  </select>
+ </td>
+ <td><input type="checkbox" name=":columns" value="creator" checked></td>
+ <td><input type="radio" name=":sort" value="creator"></td>
+ <td><input type="radio" name=":group" value="creator"></td>
+</tr>
+
+<tr>
+ <th>Activity:</th>
+ <td><input name="activity"></td>
+ <td><input type="checkbox" name=":columns" value="activity" checked></td>
+ <td><input type="radio" name=":sort" value="activity" checked></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr>
+ <th>Priority:</th>
+ <td>
+  <select name="priority">
+   <option value="">don't care</option>
+   <option value="-1">not selected</option>
+   <option value="">------------</option>
+   <option tal:repeat="s db/priority/list" tal:attributes="value s/id"
+     tal:content="s/name">priority to filter on</option>
+  </select>
+ </td>
+ <td><input type="checkbox" name=":columns" value="priority"></td>
+ <td><input type="radio" name=":sort" value="priority"></td>
+ <td><input type="radio" name=":group" value="priority" checked></td>
+</tr>
+
+<tr>
+ <th>Status:</th>
+ <td>
+  <select name="status">
+   <option value="">don't care</option>
+   <option value="-1,1,2,3,4,5,6,7">not resolved</option>
+   <option value="-1">not selected</option>
+   <option value="">------------</option>
+   <option tal:repeat="s db/status/list" tal:attributes="value s/id"
+           tal:content="s/name">status to filter on</option>
+  </select>
+ </td>
+ <td><input type="checkbox" name=":columns" value="status" checked></td>
+ <td><input type="radio" name=":sort" value="status"></td>
+ <td><input type="radio" name=":group" value="status"></td>
+</tr>
+
+<tr>
+ <th>Assigned To:</th>
+ <td>
+  <select name="assignedto">
+   <option value="">don't care</option>
+   <option tal:attributes="value request/user/id">assigned to me</option>
+   <option value="-1">unassigned</option>
+   <option value="">------------</option>
+   <option tal:repeat="s db/user/list" tal:attributes="value s/id"
+     tal:content="s/username">user to filter on</option>
+  </select>
+ </td>
+ <td><input type="checkbox" name=":columns" value="assignedto" checked></td>
+ <td><input type="radio" name=":sort" value="assignedto"></td>
+ <td><input type="radio" name=":group" value="assignedto"></td>
+</tr>
+
+<tr>
+<th>Pagesize:</th>
+<td><input type="text" name=":pagesize" size="3" value="50"></td>
+</tr>
+
+<tr>
+<th>Start With:</th>
+<td><input type="text" name=":startwith" size="3" value="0"></td>
+</tr>
+
+<tr>
+<th>Sort Descending:</th>
+<td><input type="checkbox" name=":sortdir" checked>
+</td>
+
+<tr>
+<th>Group Descending:</th>
+<td><input type="checkbox" name=":groupdir">
+</td>
+</tr>
+
+<tr><td>&nbsp;</td>
+<td><input type="submit" value="Search"></td>
+</tr>
+
+<tr><td>&nbsp;</td>
+ <td colspan="4" class="help">*: The "all text" field will look in message
+   bodies and issue titles</td>
+</tr>
+</table>
 
 </form>
index a1b0da853a280e2809fd0e281c504133781a01f2..c6d6f98fb5e45786ea7a2cdaa4e8f1196c296c74 100644 (file)
 <table border=0 cellspacing=0 cellpadding=0>
 
 <tr>
- <td colspan="2" class="page-header">
+ <td class="page-header-left">&nbsp;</td>
+ <td class="page-header-top">
   <h2 tal:content="title">name</h2>
  </td>
 </tr>
 
 <tr>
  <td rowspan="2" valign="top" nowrap class="sidebar">
-  <div class="classblock"
+  <p class="classblock"
        tal:condition="python:request.user.hasPermission('Edit', 'issue')">
-   <a href="issue?:sort=-activity&:group=priority&:filter=status,assignedto&:columns=id,activity,title,creator,assignedto&status=-1,1,2,3,4,5,6,7&assignedto=-1">Unassigned Issues</a><br>
-   <a href="issue?:sort=-activity&:group=priority&:filter=status&:columns=id,activity,title,creator,assignedto&status=-1,1,2,3,4,5,6,7">All Issues</a><br>
+   <b>Issues</b><br>
+   <a href="issue?:sort=-activity&:group=priority&:filter=status,assignedto&:columns=id,activity,title,creator,priority&status=-1,1,2,3,4,5,6,7&assignedto=-1">Unassigned Issues</a><br>
+   <a href="issue?:sort=-activity&:group=priority&:filter=status&:columns=id,activity,title,creator,assignedto,priority&status=-1,1,2,3,4,5,6,7">All Issues</a><br>
    <a href="issue?:template=search">Search Issues</a><br>
-   <a href="issue?:template=item">New Issue</a><br>
-  </div>
-  <div class="classblock"
+   <a href="issue?:template=item">New Issue</a>
+  </p>
+
+  <p class="classblock"
        tal:condition="python:request.user.hasPermission('Edit', None)">
+   <b>Admin</b><br>
    <a href="home?:template=classlist">Class List</a><br>
    <a href="user">User List</a><br>
-   <a href="user?:template=item">Add User</a><br>
-  </div>
-  <hr>
-  <b tal:content="request/user/username">username</b><br>
-  <div tal:condition="python:request.user.username=='anonymous'">
-   <form method="POST" action=''>
+   <a href="user?:template=item">Add User</a>
+  </p>
+
+  <p class="userblock">
+   <b>Logged in as</b><br><b tal:content="request/user/username">username</b><br>
+   <form method="POST" action=''
+         tal:condition="python:request.user.username=='anonymous'">
     <input size="10" name="__login_name"><br>
     <input size="10" type="password" name="__login_password"><br>
     <input type="submit" name=":action" value="login">
     <span tal:replace="structure request/indexargs_form" />
    </form>
-  </div>
-  <div tal:condition="python:request.user.username!='anonymous'">
-   <a tal:attributes="href string:issue?:sort=-activity&:group=priority&:filter=status,assignedto&:columns=id,activity,title,creator,assignedto&status=-1,1,2,3,4,5,6,7&assignedto=${request/user/id}">My Issues</a><br>
-   <a tal:attributes="href string:user${request/user/id}">My Details</a><br>
-   <a href="?:action=logout">Logout</a>
-  </div>
+   <tal:block tal:condition="python:request.user.username != 'anonymous'">
+    <a tal:attributes="href string:issue?:sort=-activity&:group=priority&:filter=status,assignedto&:columns=id,activity,title,creator,priority&status=-1,1,2,3,4,5,6,7&assignedto=${request/user/id}">My Issues</a><br>
+    <a tal:attributes="href string:user${request/user/id}">My Details</a><br>
+    <a href="?:action=logout">Logout</a>
+   </tal:block>
+  </p>
  </td>
  <td>
   <p tal:condition="options/error_message | nothing" class="error-message"
index da9d3eac88570e4528121790503ab844cc588e9f..c8bccc06b59eca6babcd7628b3682f80f94c201a 100644 (file)
+/* main page styles */
 .body {
   font-family: sans-serif, Arial, Helvetica;
+  color: #333333;
 }
 a:hover { text-decoration: underline; }
 a:link { text-decoration: none; }
 a { text-decoration: none; }
 
-p {
-  color: #333333;
+.page-header-left {
+  background-color: #ffffee;
+  padding: 5px;
 }
 
-td.sidebar {
-  font-family: sans-serif, Arial, Helvetica;
-  background-color: #efefef;
-  border: none;
+.page-header-top {
+  background-color: #ffffee;
+  border-bottom: 1px solid #ffffbb;
   padding: 5px;
 }
 
-.page-header {
-  font-family: sans-serif, Arial, Helvetica;
-  background-color: #efefef;
-  border: none;
+td.sidebar {
+  background-color: #ffffee;
+  border-right: 1px solid #ffffbb;
+  border-bottom: 1px solid #ffffbb;
   padding: 5px;
 }
 
-.strong-header {
-  font-family: sans-serif, Arial, Helvetica;
-  font-weight: bold;
-  background-color: #000000;
-  color: #ffffff;
+td.sidebar p.classblock {
+  border-top: 1px solid #ffffbb;
+  border-bottom: 1px solid #ffffbb;
+}
+
+td.sidebar p.userblock {
+  background-color: #eeffff;
+  border-top: 1px solid #bbffff;
+  border-bottom: 1px solid #bbffff;
 }
 
 td.content {
   padding: 1px;
-/*
-  border: 1px solid black;
-*/
 }
 
-table.list td {
-  border: 0 2 0 2;
-  border-left: 1px solid #404070;
-  border-right: 1px solid #404070;
+p.ok-message {
+  background-color: #22bb22;
+  padding: 5 5 5 5;
+  color: white;
+  font-weight: bold;
 }
-/*
-WAAAAAAH nothing seems to support last-child :(
-table.list td:first-child {
-  border-left: 2px solid #404070;
+p.error-message {
+  background-color: #bb2222;
+  padding: 5 5 5 5;
+  color: white;
+  font-weight: bold;
+}
+
+
+/* style for forms */
+table.form {
+  border-spacing: 0px;
+  border-collapse: separate;
+  /* width: 100%; */
+}
+
+.form th {
+  font-weight: bold;
+  color: #333388;
+  text-align: right;
+}
+
+.form-header th {
+  font-weight: bold;
+  color: #333388;
+  background-color: #eeeeff;
+  text-align: left;
+}
+
+.form td.optional {
+  font-weight: bold;
+  font-style: italic;
+  color: #333333;
+}
+
+.form td {
+  color: #333333;
 }
-table.list td:last-child {
-  border-right: 2px solid #404070;
+
+.form td.html {
+  color: #777777;
+}
+
+/* style for lists */
+table.list {
+  border-spacing: 0px;
+  border-collapse: separate;
+  width: 100%;
 }
+
+table.list th {
+  padding: 0 4 0 4;
+  color: #404070;
+  background-color: #eeeeff;
+/*
+  border-right: 1px solid #404070;
 */
-table.list tr:last-child td {
-  border-bottom: 2px solid #404070;
+  vertical-align: top;
 }
-.list-header {
-  background-color: #404070;
-  color: white;
-  border: none;
+table.list th a:hover { color: white }
+table.list th a:link { color: white }
+table.list th a { color: white }
+table.list th.group {
+  text-align: center;
 }
 
-.section-bar {
-  background-color: #e0e0e0;
+table.list td {
+  padding: 0 4 0 4;
+  border: 0 2 0 2;
+  border-right: 1px solid #404070;
+  color: #404070;
+  background-color: white;
+  vertical-align: top;
 }
 
-.row-normal {
-  background-color: #ffffff;
-  border: none;
+table.list td.normal {
 }
 
-.row-alt {
+table.list td.alt {
   background-color: #efefef;
-  border: none;
 }
 
-.file-header {
-  font-family: sans-serif, Arial, Helvetica;
-  font-weight: bold;
-  background-color: #41BE62;
-  color: #ffffff;
-}   
+table.list td:first-child {
+  border-left: 1px solid #404070;
+  border-right: 1px solid #404070;
+}
+/*
+table.list th:first-child {
+  border-left: 1px solid #404070;
+  border-right: 1px solid #404070;
+}
+*/
 
-.history-header {
-  font-family: sans-serif, Arial, Helvetica;
+/* style for message displays */
+table.messages {
+  border-spacing: 0px;
+  border-collapse: separate;
+  width: 100%;
+}
+
+table.messages th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
   font-weight: bold;
-  background-color: #739DEE;
-  color: #ffffff;
+  background-color: white;
+  color: #707040;
 }
 
-.msg-header {
-  font-family: Verdana, Helvetica, sans-serif;
+table.messages th {
   font-weight: bold;
-  background-color: #EE71AC;
-  color: #ffffff;
+  color: black;
+  text-align: left;
 }
 
-.msg-content {
+table.messages td {
   font-family: monospace;
-  background-color: #ffeaff;
-  color: #000000;
+  background-color: #efefef;
+  border-top: 1px solid #afafaf;
+  border-bottom: 1px solid #afafaf;
+  color: black;
 }
 
-p.ok-message {
-  background-color: #22bb22;
-  padding: 5 5 5 5;
-  color: white;
-  font-weight: bold;
-}
-p.error-message {
-  background-color: #bb2222;
-  padding: 5 5 5 5;
-  color: white;
-  font-weight: bold;
+/* style for file displays */
+table.files {
+  border-spacing: 0px;
+  border-collapse: separate;
+  width: 100%;
 }
 
-tr.form td {
-  background-color: #f0f0f0;
-}
-.form-title {
+table.files th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
   font-weight: bold;
-  color: #333333;
+  background-color: white;
+  color: #707040;
 }
 
-.form-label {
+table.files th {
+  border-bottom: 1px solid #afafaf;
   font-weight: bold;
-  color: #333333;
+  text-align: left;
 }
 
-.form-optional {
-  font-weight: bold;
-  font-style: italic;
-  color: #333333;
+table.files td {
+  font-family: monospace;
 }
 
-.form-element {
-  color: #000000;
+
+/* style for file displays */
+table.otherinfo {
+  border-spacing: 0px;
+  border-collapse: separate;
+  width: 100%;
 }
 
-.form-text {
-  color: #333333;
+table.otherinfo th.header{
+  padding-top: 10px;
+  border-bottom: 1px solid gray;
+  font-weight: bold;
+  background-color: white;
+  color: #707040;
 }
 
-.form-mono {
-  font-family: monospace;
+table.otherinfo th {
+  border-bottom: 1px solid #afafaf;
+  font-weight: bold;
+  text-align: left;
 }
 
index d9e84deb9a882f11c074853afdaeba646c7822d8..f44c23392cce2763a32e48be9096df3ee7196771 100644 (file)
@@ -7,88 +7,84 @@
 You are not allowed to view this page.
 </span>
 
-<form method="POST" onSubmit="return submit_once()"
-      enctype="multipart/form-data" tal:condition="editok">
+<tal:block tal:condition="editok">
+<form method="POST" onSubmit="return submit_once()" enctype="multipart/form-data">
 
-<table border=0 cellspacing=0 cellpadding=2>
- <tr class="strong-header">
-  <td colspan=2>Your Details</td>
- </tr>
- <tr class="form">
-  <td width=1% nowrap align=right><span class="form-label">Name</span></td>
-  <td class="form-text" tal:content="structure user/realname/field">realname</td>
+<table class="form">
+ <tr>
+  <th>Name</th>
+  <td tal:content="structure user/realname/field">realname</td>
  </tr>
- <tr class="form">
-  <td width=1% nowrap align=right><span class="form-label">Login Name</span></td>
-  <td class="form-text" tal:content="structure user/username/field">username</td>
+ <tr>
+  <th>Login Name</th>
+  <td tal:content="structure user/username/field">username</td>
  </tr>
- <tr class="form">
-  <td width=1% nowrap align=right><span class="form-label">Login Password</span></td>
-  <td class="form-text" tal:content="structure user/password/field">password</td>
+ <tr>
+  <th>Login Password</th>
+  <td tal:content="structure user/password/field">password</td>
  </tr>
- <tr tal:condition="python:request.user.hasPermission('Web Roles')" class="form">
-  <td width=1% nowrap align=right><span class="form-label">Roles</span></td>
-  <td class="form-text" tal:content="structure user/roles/field">roles</td>
+ <tr tal:condition="python:request.user.hasPermission('Web Roles')">
+  <th>Roles</th>
+  <td tal:content="structure user/roles/field">roles</td>
  </tr>
- <tr class="form">
-  <td width=1% nowrap align=right><span class="form-label">Phone</span></td>
-  <td class="form-text" tal:content="structure user/phone/field">phone</td>
+ <tr>
+  <th>Phone</th>
+  <td tal:content="structure user/phone/field">phone</td>
  </tr>
- <tr class="form">
-  <td width=1% nowrap align=right><span class="form-label">Organisation</span></td>
-  <td class="form-text" tal:content="structure user/organisation/field">organisation</td>
+ <tr>
+  <th>Organisation</th>
+  <td tal:content="structure user/organisation/field">organisation</td>
  </tr>
- <tr class="form">
-  <td width=1% nowrap align=right><span class="form-label">E-mail address</span></td>
-  <td class="form-text" tal:content="structure user/address/field">address</td>
+ <tr>
+  <th>E-mail address</th>
+  <td tal:content="structure user/address/field">address</td>
  </tr>
- <tr class="form">
-  <td width=1% nowrap align=right><span class="form-label">Alternate
-   E-mail addresses</span><br>
-  <span class="form-help">One address per line</span></td>
-  <td class="form-text" tal:content="structure user/alternate_addresses/multiline">alternate_addresses</td>
+ <tr>
+  <th>Alternate E-mail addresses<br>One address per line</th>
+  <td tal:content="structure user/alternate_addresses/multiline">alternate_addresses</td>
  </tr>
 
- <tr class="form">
+ <tr>
   <td>&nbsp;</td>
-  <td colspan=3 class="form-text" tal:content="structure user/submit">
-   submit button will go here
-  </td>
+  <td colspan=3 tal:content="structure user/submit">submit button here</td>
  </tr>
+</table>
+</form>
 
- <tr class="strong-header"><td colspan=2><b>Queries</b></td></tr>
- <tr class="form" tal:repeat="query user/queries">
-  <td colspan=2 tal:content="query">query</td>
+<table class="otherinfo" tal:condition="user/queries">
+ <tr><th class="header">Queries</th></tr>
+ <tr tal:repeat="query user/queries">
+  <td tal:content="query">query</td>
  </tr>
+</table>
 
- <tr class="strong-header"><td colspan=2><b>History</b></td></tr>
+<table class="otherinfo">
+ <tr><th class="header">History</th></tr>
  <tr>
-  <td colspan="2" tal:content="structure user/history">history</td>
+  <td tal:content="structure user/history">history</td>
  </tr>
 </table>
+</tal:block>
 
-</form>
-
-<table border=0 cellspacing=0 cellpadding=2
-       tal:condition="python:viewok and not editok">
- <tr class="strong-header">
-  <td colspan=2 tal:content="user/realname">realname</td>
+<table class="form" tal:condition="python:viewok and not editok">
+ <tr>
+  <th colspan=2 class="header" tal:content="user/realname">realname</th>
  </tr>
  <tr>
-  <td width=1% nowrap align=right><span class="form-label">Login Name</span></td>
-  <td class="form-text" tal:content="user/username">username</td>
+  <th>Login Name</th>
+  <td tal:content="user/username">username</td>
  </tr>
  <tr>
-  <td width=1% nowrap align=right><span class="form-label">Phone</span></td>
-  <td class="form-text" tal:content="user/phone">phone</td>
+  <th>Phone</th>
+  <td tal:content="user/phone">phone</td>
  </tr>
  <tr>
-  <td width=1% nowrap align=right><span class="form-label">Organisation</span></td>
-  <td class="form-text" tal:content="user/organisation">organisation</td>
+  <th>Organisation</th>
+  <td tal:content="user/organisation">organisation</td>
  </tr>
  <tr>
-  <td width=1% nowrap align=right><span class="form-label">E-mail address</span></td>
-  <td class="form-text" tal:content="user/address/email">address</td>
+  <th>E-mail address</th>
+  <td tal:content="user/address/email">address</td>
  </tr>
 </table>