Code

oops, fubared the confirm password field
[roundup.git] / roundup / cgi / templating.py
index e7802dc78d496580d04df746444e3157164c6b0a..54bf622e8d0ae9316c43a20323f79ca81f25f76e 100644 (file)
@@ -84,13 +84,13 @@ class Templates:
                     extension, filename, generic)
             filename = generic
 
-        if self.templates.has_key(filename) and \
-                stime < self.templates[filename].mtime:
+        if self.templates.has_key(src) and \
+                stime < self.templates[src].mtime:
             # compiled template is up to date
-            return self.templates[filename]
+            return self.templates[src]
 
         # compile the template
-        self.templates[filename] = pt = RoundupPageTemplate()
+        self.templates[src] = pt = RoundupPageTemplate()
         pt.write(open(src).read())
         pt.id = filename
         pt.mtime = time.time()
@@ -391,6 +391,10 @@ class HTMLClass(HTMLPermissions):
             filterspec = request.filterspec
             sort = request.sort
             group = request.group
+        else:
+            filterspec = {}
+            sort = (None,None)
+            group = (None,None)
         if self.classname == 'user':
             klass = HTMLUser
         else:
@@ -480,7 +484,7 @@ class HTMLItem(HTMLPermissions):
             if isinstance(prop, klass):
                 return htmlklass(self._client, self._nodeid, prop, item, value)
 
-        raise KeyErorr, item
+        raise KeyError, item
 
     def __getattr__(self, attr):
         ''' convenience access to properties '''
@@ -511,13 +515,27 @@ class HTMLItem(HTMLPermissions):
              _('<th>Action</th>'),
              _('<th>Args</th>'),
             '</tr>']
+        current = {}
         comments = {}
         history = self._klass.history(self._nodeid)
         history.sort()
+        timezone = self._db.getUserTimezone()
         if direction == 'descending':
             history.reverse()
+            for prop_n in self._props.keys():
+                prop = self[prop_n]
+                if isinstance(prop, HTMLProperty):
+                    current[prop_n] = prop.plain()
+                    # make link if hrefable
+                    if (self._props.has_key(prop_n) and
+                            isinstance(self._props[prop_n], hyperdb.Link)):
+                        classname = self._props[prop_n].classname
+                        if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')):
+                            current[prop_n] = '<a href="%s%s">%s</a>'%(classname,
+                                self._klass.get(self._nodeid, prop_n, None), current[prop_n])
         for id, evt_date, user, action, args in history:
-            date_s = str(evt_date).replace("."," ")
+            date_s = str(evt_date.local(timezone)).replace("."," ")
             arg_s = ''
             if action == 'link' and type(args) == type(()):
                 if len(args) == 3:
@@ -610,27 +628,48 @@ class HTMLItem(HTMLPermissions):
                                     label = None
                             if label is not None:
                                 if hrefable:
-                                    cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
-                                        classname, args[k], label))
+                                    old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
                                 else:
-                                    cell.append('%s: %s' % (k,label))
+                                    old = label;
+                                cell.append('%s: %s' % (k,old))
+                                if current.has_key(k):
+                                    cell[-1] += ' -> %s'%current[k]
+                                    current[k] = old
 
                         elif isinstance(prop, hyperdb.Date) and args[k]:
-                            d = date.Date(args[k])
+                            d = date.Date(args[k]).local(timezone)
                             cell.append('%s: %s'%(k, str(d)))
+                            if current.has_key(k):
+                                if not current[k] == '(no value)' and current[k]:
+                                    current[k] = date.Date(current[k]).local(timezone)
+                                cell[-1] += ' -> %s' % current[k]
+                                current[k] = str(d)
 
                         elif isinstance(prop, hyperdb.Interval) and args[k]:
                             d = date.Interval(args[k])
                             cell.append('%s: %s'%(k, str(d)))
+                            if current.has_key(k):
+                                cell[-1] += ' -> %s'%current[k]
+                                current[k] = str(d)
 
                         elif isinstance(prop, hyperdb.String) and args[k]:
                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
+                            if current.has_key(k):
+                                cell[-1] += ' -> %s'%current[k]
+                                current[k] = cgi.escape(args[k])
 
                         elif not args[k]:
-                            cell.append('%s: (no value)\n'%k)
+                            if current.has_key(k):
+                                cell.append('%s: %s'%(k, current[k]))
+                                current[k] = '(no value)'
+                            else:
+                                cell.append('%s: (no value)'%k)
 
                         else:
-                            cell.append('%s: %s\n'%(k, str(args[k])))
+                            cell.append('%s: %s'%(k, str(args[k])))
+                            if current.has_key(k):
+                                cell[-1] += ' -> %s'%current[k]
+                                current[k] = str(args[k])
                     else:
                         # property no longer exists
                         comments['no_exist'] = _('''<em>The indicated property
@@ -662,7 +701,9 @@ class HTMLItem(HTMLPermissions):
         # create a new request and override the specified args
         req = HTMLRequest(self._client)
         req.classname = self._klass.get(self._nodeid, 'klass')
-        req.updateFromURL(self._klass.get(self._nodeid, 'url'))
+        name = self._klass.get(self._nodeid, 'name')
+        req.updateFromURL(self._klass.get(self._nodeid, 'url') +
+            '&:queryname=%s'%urllib.quote(name))
 
         # new template, using the specified classname and request
         pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
@@ -732,14 +773,45 @@ class HTMLProperty:
         return cmp(self._value, other)
 
 class StringHTMLProperty(HTMLProperty):
-    def plain(self, escape=0):
+    hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
+                          r'(?P<email>[\w\.]+@[\w\.\-]+)|'
+                          r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
+    def _hyper_repl(self, match):
+        if match.group('url'):
+            s = match.group('url')
+            return '<a href="%s">%s</a>'%(s, s)
+        elif match.group('email'):
+            s = match.group('email')
+            return '<a href="mailto:%s">%s</a>'%(s, s)
+        else:
+            s = match.group('item')
+            s1 = match.group('class')
+            s2 = match.group('id')
+            try:
+                # make sure s1 is a valid tracker classname
+                self._db.getclass(s1)
+                return '<a href="%s">%s %s</a>'%(s, s1, s2)
+            except KeyError:
+                return '%s%s'%(s1, s2)
+
+    def plain(self, escape=0, hyperlink=0):
         ''' Render a "plain" representation of the property
+            
+            "escape" turns on/off HTML quoting
+            "hyperlink" turns on/off in-text hyperlinking of URLs, email
+                addresses and designators
         '''
         if self._value is None:
             return ''
         if escape:
-            return cgi.escape(str(self._value))
-        return str(self._value)
+            s = cgi.escape(str(self._value))
+        else:
+            s = str(self._value)
+        if hyperlink:
+            if not escape:
+                s = cgi.escape(s)
+            s = self.hyper_re.sub(self._hyper_repl, s)
+        return s
 
     def stext(self, escape=0):
         ''' Render the value of the property as StructuredText.
@@ -804,9 +876,9 @@ class PasswordHTMLProperty(HTMLProperty):
     def confirm(self, size = 30):
         ''' Render a second form edit field for the property, used for 
             confirmation that the user typed the password correctly. Generates
-            a field with name "name:confirm".
+            a field with name ":confirm:name".
         '''
-        return '<input type="password" name="%s:confirm" size="%s">'%(
+        return '<input type="password" name=":confirm:%s" size="%s">'%(
             self._name, size)
 
 class NumberHTMLProperty(HTMLProperty):
@@ -829,7 +901,7 @@ class BooleanHTMLProperty(HTMLProperty):
     def plain(self):
         ''' Render a "plain" representation of the property
         '''
-        if self.value is None:
+        if self._value is None:
             return ''
         return self._value and "Yes" or "No"
 
@@ -853,7 +925,16 @@ class DateHTMLProperty(HTMLProperty):
         '''
         if self._value is None:
             return ''
-        return str(self._value)
+        return str(self._value.local(self._db.getUserTimezone()))
+
+    def now(self):
+        ''' Return the current time.
+
+            This is useful for defaulting a new value. Returns a
+            DateHTMLProperty.
+        '''
+        return DateHTMLProperty(self._client, self._nodeid, self._prop,
+            self._name, date.Date('.'))
 
     def field(self, size = 30):
         ''' Render a form edit field for the property
@@ -861,7 +942,7 @@ class DateHTMLProperty(HTMLProperty):
         if self._value is None:
             value = ''
         else:
-            value = cgi.escape(str(self._value))
+            value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
             value = '&quot;'.join(value.split('"'))
         return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
 
@@ -889,6 +970,12 @@ class DateHTMLProperty(HTMLProperty):
         '''
         return self._value.pretty()
 
+    def local(self, offset):
+        ''' Return the date/time as a local (timezone offset) date/time.
+        '''
+        return DateHTMLProperty(self._client, self._nodeid, self._prop,
+            self._name, self._value.local(offset))
+
 class IntervalHTMLProperty(HTMLProperty):
     def plain(self):
         ''' Render a "plain" representation of the property
@@ -922,6 +1009,13 @@ class LinkHTMLProperty(HTMLProperty):
         entry identified by the assignedto property on item, and then the
         name property of that user)
     '''
+    def __init__(self, *args):
+        HTMLProperty.__init__(self, *args)
+        # if we're representing a form value, then the -1 from the form really
+        # should be a None
+        if str(self._value) == '-1':
+            self._value = None
+
     def __getattr__(self, attr):
         ''' return a new HTMLItem '''
        #print 'Link.getattr', (self, attr, self._value)
@@ -963,6 +1057,11 @@ class LinkHTMLProperty(HTMLProperty):
         else:
             s = ''
         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
+
+        # make sure we list the current value if it's retired
+        if self._value and self._value not in options:
+            options.insert(0, self._value)
+
         for optionid in options:
             # get the option value, and if it's None use an empty string
             option = linkcl.get(optionid, k) or ''
@@ -1009,6 +1108,11 @@ class LinkHTMLProperty(HTMLProperty):
         else:  
             sort_on = ('+', linkcl.labelprop())
         options = linkcl.filter(None, conditions, sort_on, (None, None))
+
+        # make sure we list the current value if it's retired
+        if self._value and self._value not in options:
+            options.insert(0, self._value)
+
         for optionid in options:
             # get the option value, and if it's None use an empty string
             option = linkcl.get(optionid, k) or ''
@@ -1066,9 +1170,10 @@ class MultilinkHTMLProperty(HTMLProperty):
         return klass(self._client, self._prop.classname, value)
 
     def __contains__(self, value):
-        ''' Support the "in" operator
+        ''' Support the "in" operator. We have to make sure the passed-in
+            value is a string first, not a *HTMLProperty.
         '''
-        return value in self._value
+        return str(value) in self._value
 
     def reverse(self):
         ''' return the list in reverse order
@@ -1129,6 +1234,12 @@ class MultilinkHTMLProperty(HTMLProperty):
         height = height or min(len(options), 7)
         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
         k = linkcl.labelprop(1)
+
+        # make sure we list the current values if they're retired
+        for val in value:
+            if val not in options:
+                options.insert(0, val)
+
         for optionid in options:
             # get the option value, and if it's None use an empty string
             option = linkcl.get(optionid, k) or ''
@@ -1239,6 +1350,9 @@ class HTMLRequest:
         self.classname = client.classname
         self.template = client.template
 
+        # the special char to use for special vars
+        self.special_char = '@'
+
         self._post_init()
 
     def _post_init(self):
@@ -1246,36 +1360,46 @@ class HTMLRequest:
         '''
         # extract the index display information from the form
         self.columns = []
-        if self.form.has_key(':columns'):
-            self.columns = handleListCGIValue(self.form[':columns'])
+        for name in ':columns @columns'.split():
+            if self.form.has_key(name):
+                self.special_char = name[0]
+                self.columns = handleListCGIValue(self.form[name])
+                break
         self.show = ShowDict(self.columns)
 
         # sorting
         self.sort = (None, None)
-        if self.form.has_key(':sort'):
-            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])
+        for name in ':sort @sort'.split():
+            if self.form.has_key(name):
+                self.special_char = name[0]
+                sort = self.form[name].value
+                if sort.startswith('-'):
+                    self.sort = ('-', sort[1:])
+                else:
+                    self.sort = ('+', sort)
+                if self.form.has_key(self.special_char+'sortdir'):
+                    self.sort = ('-', self.sort[1])
 
         # grouping
         self.group = (None, None)
-        if self.form.has_key(':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])
+        for name in ':group @group'.split():
+            if self.form.has_key(name):
+                self.special_char = name[0]
+                group = self.form[name].value
+                if group.startswith('-'):
+                    self.group = ('-', group[1:])
+                else:
+                    self.group = ('+', group)
+                if self.form.has_key(self.special_char+'groupdir'):
+                    self.group = ('-', self.group[1])
 
         # filtering
         self.filter = []
-        if self.form.has_key(':filter'):
-            self.filter = handleListCGIValue(self.form[':filter'])
+        for name in ':filter @filter'.split():
+            if self.form.has_key(name):
+                self.special_char = name[0]
+                self.filter = handleListCGIValue(self.form[name])
+
         self.filterspec = {}
         db = self.client.db
         if self.classname is not None:
@@ -1293,19 +1417,24 @@ class HTMLRequest:
 
         # full-text search argument
         self.search_text = None
-        if self.form.has_key(':search_text'):
-            self.search_text = self.form[':search_text'].value
+        for name in ':search_text @search_text'.split():
+            if self.form.has_key(name):
+                self.special_char = name[0]
+                self.search_text = self.form[name].value
 
         # pagination - size and start index
         # figure batch args
-        if self.form.has_key(':pagesize'):
-            self.pagesize = int(self.form[':pagesize'].value)
-        else:
-            self.pagesize = 50
-        if self.form.has_key(':startwith'):
-            self.startwith = int(self.form[':startwith'].value)
-        else:
-            self.startwith = 0
+        self.pagesize = 50
+        for name in ':pagesize @pagesize'.split():
+            if self.form.has_key(name):
+                self.special_char = name[0]
+                self.pagesize = int(self.form[name].value)
+
+        self.startwith = 0
+        for name in ':startwith @startwith'.split():
+            if self.form.has_key(name):
+                self.special_char = name[0]
+                self.startwith = int(self.form[name].value)
 
     def updateFromURL(self, url):
         ''' Parse the URL for query args, and update my attributes using the
@@ -1378,60 +1507,68 @@ env: %(env)s
             filterspec=1):
         ''' return the current index args as form elements '''
         l = []
+        sc = self.special_char
         s = '<input type="hidden" name="%s" value="%s">'
         if columns and self.columns:
-            l.append(s%(':columns', ','.join(self.columns)))
+            l.append(s%(sc+'columns', ','.join(self.columns)))
         if sort and self.sort[1] is not None:
             if self.sort[0] == '-':
                 val = '-'+self.sort[1]
             else:
                 val = self.sort[1]
-            l.append(s%(':sort', val))
+            l.append(s%(sc+'sort', val))
         if group and self.group[1] is not None:
             if self.group[0] == '-':
                 val = '-'+self.group[1]
             else:
                 val = self.group[1]
-            l.append(s%(':group', val))
+            l.append(s%(sc+'group', val))
         if filter and self.filter:
-            l.append(s%(':filter', ','.join(self.filter)))
+            l.append(s%(sc+'filter', ','.join(self.filter)))
         if filterspec:
             for k,v in self.filterspec.items():
-                l.append(s%(k, ','.join(v)))
+                if type(v) == type([]):
+                    l.append(s%(k, ','.join(v)))
+                else:
+                    l.append(s%(k, v))
         if self.search_text:
-            l.append(s%(':search_text', self.search_text))
-        l.append(s%(':pagesize', self.pagesize))
-        l.append(s%(':startwith', self.startwith))
+            l.append(s%(sc+'search_text', self.search_text))
+        l.append(s%(sc+'pagesize', self.pagesize))
+        l.append(s%(sc+'startwith', self.startwith))
         return '\n'.join(l)
 
     def indexargs_url(self, url, args):
         ''' embed the current index args in a URL '''
+        sc = self.special_char
         l = ['%s=%s'%(k,v) for k,v in args.items()]
         if self.columns and not args.has_key(':columns'):
-            l.append(':columns=%s'%(','.join(self.columns)))
+            l.append(sc+'columns=%s'%(','.join(self.columns)))
         if self.sort[1] is not None and not args.has_key(':sort'):
             if self.sort[0] == '-':
                 val = '-'+self.sort[1]
             else:
                 val = self.sort[1]
-            l.append(':sort=%s'%val)
+            l.append(sc+'sort=%s'%val)
         if self.group[1] is not None and not args.has_key(':group'):
             if self.group[0] == '-':
                 val = '-'+self.group[1]
             else:
                 val = self.group[1]
-            l.append(':group=%s'%val)
-        if self.filter and not args.has_key(':columns'):
-            l.append(':filter=%s'%(','.join(self.filter)))
+            l.append(sc+'group=%s'%val)
+        if self.filter and not args.has_key(':filter'):
+            l.append(sc+'filter=%s'%(','.join(self.filter)))
         for k,v in self.filterspec.items():
             if not args.has_key(k):
-                l.append('%s=%s'%(k, ','.join(v)))
+                if type(v) == type([]):
+                    l.append('%s=%s'%(k, ','.join(v)))
+                else:
+                    l.append('%s=%s'%(k, v))
         if self.search_text and not args.has_key(':search_text'):
-            l.append(':search_text=%s'%self.search_text)
+            l.append(sc+'search_text=%s'%self.search_text)
         if not args.has_key(':pagesize'):
-            l.append(':pagesize=%s'%self.pagesize)
+            l.append(sc+'pagesize=%s'%self.pagesize)
         if not args.has_key(':startwith'):
-            l.append(':startwith=%s'%self.startwith)
+            l.append(sc+'startwith=%s'%self.startwith)
         return '%s?%s'%(url, '&'.join(l))
     indexargs_href = indexargs_url