Code

- added a favicon
[roundup.git] / roundup / cgi / templating.py
index 1a807f9416279121a083ac406424767d25d201d3..324e75f47ac5114833fbdeeb51b05fb6139f2706 100644 (file)
@@ -1,5 +1,15 @@
 """Implements the API used in the HTML templating for the web interface.
 """
+
+todo = '''
+- Most methods should have a "default" arg to supply a value 
+  when none appears in the hyperdb or request. 
+- Multilink property additions: change_note and new_upload
+- Add class.find() too
+- NumberHTMLProperty should support numeric operations
+- HTMLProperty should have an isset() method
+'''
+
 __docformat__ = 'restructuredtext'
 
 from __future__ import nested_scopes
@@ -124,7 +134,7 @@ class Templates:
                 raise
 
         if self.templates.has_key(src) and \
-                stime < self.templates[src].mtime:
+                stime <= self.templates[src].mtime:
             # compiled template is up to date
             return self.templates[src]
 
@@ -134,7 +144,7 @@ class Templates:
         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
         pt.pt_edit(open(src).read(), content_type)
         pt.id = filename
-        pt.mtime = time.time()
+        pt.mtime = stime
         return pt
 
     def __getitem__(self, name):
@@ -195,6 +205,7 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
              'tracker': client.instance,
              'utils': utils(client),
              'templates': Templates(client.instance.config.TEMPLATES),
+             'template': self,
         }
         # add in the item if there is one
         if client.nodeid:
@@ -252,8 +263,13 @@ class HTMLDatabase:
         # check to see if we're actually accessing an item
         m = desre.match(item)
         if m:
-            self._client.db.getclass(m.group('cl'))
-            return HTMLItem(self._client, m.group('cl'), m.group('id'))
+            cl = m.group('cl')
+            self._client.db.getclass(cl)
+            if cl == 'user':
+                klass = HTMLUser
+            else:
+                klass = HTMLItem
+            return klass(self._client, cl, m.group('id'))
         else:
             self._client.db.getclass(item)
             if item == 'user':
@@ -269,14 +285,17 @@ class HTMLDatabase:
     def classes(self):
         l = self._client.db.classes.keys()
         l.sort()
-        r = []
+        m = []
         for item in l:
             if item == 'user':
                 m.append(HTMLUserClass(self._client, item))
             m.append(HTMLClass(self._client, item))
-        return r
+        return m
 
-def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
+def lookupIds(db, prop, ids, fail_ok=0, num_re=re.compile('-?\d+')):
+    ''' "fail_ok" should be specified if we wish to pass through bad values
+        (most likely form values that we wish to represent back to the user)
+    '''
     cl = db.getclass(prop.classname)
     l = []
     for entry in ids:
@@ -285,9 +304,22 @@ def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
         else:
             try:
                 l.append(cl.lookup(entry))
-            except KeyError:
-                # ignore invalid keys
-                pass
+            except (TypeError, KeyError):
+                if fail_ok:
+                    # pass through the bad value
+                    l.append(entry)
+    return l
+
+def lookupKeys(linkcl, key, ids, num_re=re.compile('-?\d+')):
+    ''' Look up the "key" values for "ids" list - though some may already
+    be key values, not ids.
+    '''
+    l = []
+    for entry in ids:
+        if num_re.match(entry):
+            l.append(linkcl.get(entry, key))
+        else:
+            l.append(entry)
     return l
 
 class HTMLPermissions:
@@ -372,7 +404,10 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
             return None
 
         # get the property
-        prop = self._props[item]
+        try:
+            prop = self._props[item]
+        except KeyError:
+            raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
 
         # look up the correct HTMLProperty class
         form = self._client.form
@@ -382,11 +417,12 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
             if form.has_key(item):
                 if isinstance(prop, hyperdb.Multilink):
                     value = lookupIds(self._db, prop,
-                        handleListCGIValue(form[item]))
+                        handleListCGIValue(form[item]), fail_ok=1)
                 elif isinstance(prop, hyperdb.Link):
                     value = form[item].value.strip()
                     if value:
-                        value = lookupIds(self._db, prop, [value])[0]
+                        value = lookupIds(self._db, prop, [value],
+                            fail_ok=1)[0]
                     else:
                         value = None
                 else:
@@ -624,6 +660,10 @@ class HTMLItem(HTMLInputMixin, HTMLPermissions):
     def designator(self):
         """Return this item's designator (classname + id)."""
         return '%s%s'%(self._classname, self._nodeid)
+
+    def is_retired(self):
+        """Is this item retired?"""
+        return self._klass.is_retired(self._nodeid)
     
     def submit(self, label="Submit Changes"):
         """Generate a submit button.
@@ -659,25 +699,27 @@ class HTMLItem(HTMLInputMixin, HTMLPermissions):
         timezone = self._db.getUserTimezone()
         if direction == 'descending':
             history.reverse()
+            # pre-load the history with the current state
             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
-                        try:
-                            template = find_template(self._db.config.TEMPLATES,
-                                classname, 'item')
-                            if template[1].startswith('_generic'):
-                                raise NoTemplate, 'not really...'
-                        except NoTemplate:
-                            pass
-                        else:
-                            id = self._klass.get(self._nodeid, prop_n, None)
-                            current[prop_n] = '<a href="%s%s">%s</a>'%(
-                                classname, id, current[prop_n])
+                if not isinstance(prop, HTMLProperty):
+                    continue
+                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
+                    try:
+                        template = find_template(self._db.config.TEMPLATES,
+                            classname, 'item')
+                        if template[1].startswith('_generic'):
+                            raise NoTemplate, 'not really...'
+                    except NoTemplate:
+                        pass
+                    else:
+                        id = self._klass.get(self._nodeid, prop_n, None)
+                        current[prop_n] = '<a href="%s%s">%s</a>'%(
+                            classname, id, current[prop_n])
  
         for id, evt_date, user, action, args in history:
             date_s = str(evt_date.local(timezone)).replace("."," ")
@@ -799,17 +841,25 @@ class HTMLItem(HTMLInputMixin, HTMLPermissions):
                             current[k] = str(d)
 
                     elif isinstance(prop, hyperdb.Interval) and args[k]:
-                        d = date.Interval(args[k])
-                        cell.append('%s: %s'%(k, str(d)))
+                        val = str(date.Interval(args[k]))
+                        cell.append('%s: %s'%(k, val))
                         if current.has_key(k):
                             cell[-1] += ' -> %s'%current[k]
-                            current[k] = str(d)
+                            current[k] = val
 
                     elif isinstance(prop, hyperdb.String) and args[k]:
-                        cell.append('%s: %s'%(k, cgi.escape(args[k])))
+                        val = cgi.escape(args[k])
+                        cell.append('%s: %s'%(k, val))
                         if current.has_key(k):
                             cell[-1] += ' -> %s'%current[k]
-                            current[k] = cgi.escape(args[k])
+                            current[k] = val
+
+                    elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
+                        val = args[k] and 'Yes' or 'No'
+                        cell.append('%s: %s'%(k, val))
+                        if current.has_key(k):
+                            cell[-1] += ' -> %s'%current[k]
+                            current[k] = val
 
                     elif not args[k]:
                         if current.has_key(k):
@@ -860,6 +910,15 @@ class HTMLItem(HTMLInputMixin, HTMLPermissions):
         # use our fabricated request
         return pt.render(self._client, req.classname, req)
 
+    def download_url(self):
+        ''' Assume that this item is a FileClass and that it has a name
+        and content. Construct a URL for the download of the content.
+        '''
+        name = self._klass.get(self._nodeid, 'name')
+        url = '%s%s/%s'%(self._classname, self._nodeid, name)
+        return urllib.quote(url)
+
+
 class HTMLUserPermission:
 
     def is_edit_ok(self):
@@ -955,6 +1014,10 @@ class HTMLProperty(HTMLInputMixin, HTMLPermissions):
             return cmp(self._value, other._value)
         return cmp(self._value, other)
 
+    def isset(self):
+        '''Is my _value None?'''
+        return self._value is None
+
     def is_edit_ok(self):
         ''' Is the user allowed to Edit the current class?
         '''
@@ -1185,7 +1248,7 @@ class BooleanHTMLProperty(HTMLProperty):
         '''
         self.view_check()
 
-        if not is_edit_ok():
+        if not self.is_edit_ok():
             return self.plain()
 
         checked = self._value and "checked" or ""
@@ -1221,8 +1284,8 @@ class DateHTMLProperty(HTMLProperty):
         '''
         self.view_check()
 
-        return DateHTMLProperty(self._client, self._nodeid, self._prop,
-            self._formname, date.Date('.'))
+        return DateHTMLProperty(self._client, self._classname, self._nodeid,
+            self._prop, self._formname, date.Date('.'))
 
     def field(self, size = 30):
         ''' Render a form edit field for the property
@@ -1237,7 +1300,7 @@ class DateHTMLProperty(HTMLProperty):
             tz = self._db.getUserTimezone()
             value = cgi.escape(str(self._value.local(tz)))
 
-        if is_edit_ok():
+        if self.is_edit_ok():
             value = '&quot;'.join(value.split('"'))
             return self.input(name=self._formname,value=value,size=size)
         
@@ -1280,8 +1343,8 @@ class DateHTMLProperty(HTMLProperty):
         '''
         self.view_check()
 
-        return DateHTMLProperty(self._client, self._nodeid, self._prop,
-            self._formname, self._value.local(offset))
+        return DateHTMLProperty(self._client, self._classname, self._nodeid,
+            self._prop, self._formname, self._value.local(offset))
 
 class IntervalHTMLProperty(HTMLProperty):
     def plain(self):
@@ -1378,13 +1441,13 @@ class LinkHTMLProperty(HTMLProperty):
         else:
             k = linkcl.getkey()
             if k:
-                label = linkcl.get(self._value, k)
+                value = linkcl.get(self._value, k)
             else:
-                label = self._value
-            value = cgi.escape(str(self._value))
+                value = self._value
+            value = cgi.escape(str(value))
             value = '&quot;'.join(value.split('"'))
         return '<input name="%s" value="%s" size="%s">'%(self._formname,
-            label, size)
+            value, size)
 
     def menu(self, size=None, height=None, showid=0, additional=[],
             sort_on=None, **conditions):
@@ -1487,6 +1550,10 @@ class MultilinkHTMLProperty(HTMLProperty):
         '''
         return str(value) in self._value
 
+    def isset(self):
+        '''Is my _value []?'''
+        return self._value == []
+
     def reverse(self):
         ''' return the list in reverse order
         '''
@@ -1530,7 +1597,7 @@ class MultilinkHTMLProperty(HTMLProperty):
             showid=1
         if not showid:
             k = linkcl.labelprop(1)
-            value = [linkcl.get(v, k) for v in value]
+            value = lookupKeys(linkcl, k, value)
         value = cgi.escape(','.join(value))
         return self.input(name=self._formname,size=size,value=value)
 
@@ -2044,3 +2111,11 @@ class TemplatingUtils:
         return Batch(self.client, sequence, size, start, end, orphan,
             overlap)
 
+    def url_quote(self, url):
+        '''URL-quote the supplied text.'''
+        return urllib.quote(url)
+
+    def html_quote(self, html):
+        '''HTML-quote the supplied text.'''
+        return cgi.escape(url)
+