changeset 6292:1e5ed659e8ca issue2550923_computed_property

Initial implementation of Computed property It supports query/display in html, rest and xml interfaces. You can specify a cache parameter, but using it raises NotImplementedError. It does not support: search, sort or grouping by the computed field. Checking in on a branch to get more eyeballs on it and maybe some people to help.
author John Rouillard <rouilj@ieee.org>
date Fri, 27 Nov 2020 18:09:00 -0500
parents a67e2b559e8d
children 325beb81c89d
files doc/customizing.txt roundup/backends/back_anydbm.py roundup/backends/rdbms_common.py roundup/cgi/templating.py roundup/hyperdb.py roundup/instance.py
diffstat 6 files changed, 181 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/doc/customizing.txt	Fri Nov 27 17:55:52 2020 -0500
+++ b/doc/customizing.txt	Fri Nov 27 18:09:00 2020 -0500
@@ -741,6 +741,20 @@
        properties are for storing encoded arbitrary-length strings.
        The default encoding is defined on the ``roundup.password.Password``
        class.
+  Computed
+       properties invoke a python function. The return value of the
+       function is the value of the property. Unlike other properties,
+       the property is read only and can not be changed. Use cases:
+       ask a remote interface for a value (e.g. retrieve user's office
+       location from ldap, query state of a related ticket from
+       another roundup instance). It can be used to compute a value
+       (e.g. count the number of messages for an issue). The type
+       returned by the function is the type of the value. (Note it is
+       coerced to a string when displayed in the html interface.) At
+       this time it's a partial implementation. It can be
+       displayed/queried only. It can not be searched or used for
+       sorting or grouping as it does not exist in the back end
+       database.
   Date
        properties store date-and-time stamps. Their values are Timestamp
        objects.
@@ -4046,6 +4060,72 @@
     columns string:id,activity,due_date,title,creator,status;
     columns_showall string:id,activity,due_date,title,creator,assignedto,status;
 
+Adding a new Computed field to the schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Computed properties are a bit different from other properties. They do
+not actually change the database. Computed fields are not contained in
+the database and can not be searched or used for sorting or
+grouping. (Patches to add this capability are welcome.)
+
+In this example we will add a count of the number of files attached to
+the issue. This could be done using an auditor (see below) to update
+an integer field called ``filecount``. But we will implement this in a
+different way.
+
+We have two changes to make:
+
+1. add a new python method to the hyperdb.Computed class. It will
+   count the number of files attached to the issue. This method will
+   be added in the (possibly new) interfaces.py file in the top level
+   of the tracker directory.  (See interfaces.py above for more
+   information.)
+2. add a new ``filecount`` property to the issue class calling the
+   new function.
+
+A Computed method receives three arguments when called:
+
+  1. the computed object (self)
+  2. the id of the item in the class
+  3. the database object
+
+To add the method to the Computed class, modify the trackers
+interfaces.py adding::
+
+    import roundup.hyperdb as hyperdb
+    def filecount(self, nodeid, db):
+        return len(db.issue.get(nodeid, 'files'))
+    setattr(hyperdb.Computed, 'filecount', filecount)
+
+Then add::
+
+   filecount = Computed(Computed.filecount),
+
+to the existing IssueClass call.
+
+Now you can retrieve the value of the ``filecount`` property and it 
+will be computed on the fly from the existing list of attached files.
+
+This example was done with the IssueClass, but you could add a
+Computed property to any class. E.G.::
+
+     user = Class(db, "user",
+                username=String(),
+                password=Password(),
+                address=String(),
+                realname=String(),
+                phone=String(),
+                office=Computed(Computed.getOfficeFromLdap), # new prop
+                organisation=String(),
+                alternate_addresses=String(),
+                queries=Multilink('query'),
+                roles=String(),
+                timezone=String())
+
+where the method ``getOfficeFromLdap`` queries the local ldap server to
+get the current office location information. The method will be called
+with the Computed instance, the user id and the database object.
+
 Adding a new constrained field to the classic schema
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
--- a/roundup/backends/back_anydbm.py	Fri Nov 27 17:55:52 2020 -0500
+++ b/roundup/backends/back_anydbm.py	Fri Nov 27 18:09:00 2020 -0500
@@ -1146,6 +1146,9 @@
         # get the property (raises KeyErorr if invalid)
         prop = self.properties[propname]
 
+        if isinstance(prop, Computed):
+            return prop.function(prop, nodeid, self.db)
+
         if isinstance(prop, hyperdb.Multilink) and prop.computed:
             cls = self.db.getclass(prop.rev_classname)
             ids = cls.find(**{prop.rev_propname: nodeid})
--- a/roundup/backends/rdbms_common.py	Fri Nov 27 17:55:52 2020 -0500
+++ b/roundup/backends/rdbms_common.py	Fri Nov 27 18:09:00 2020 -0500
@@ -57,7 +57,7 @@
 # roundup modules
 from roundup import hyperdb, date, password, roundupdb, security, support
 from roundup.hyperdb import String, Password, Date, Interval, Link, \
-    Multilink, DatabaseError, Boolean, Number, Integer
+    Multilink, DatabaseError, Boolean, Computed, Number, Integer
 from roundup.i18n import _
 
 # support
@@ -1780,6 +1780,9 @@
         # get the property (raises KeyError if invalid)
         prop = self.properties[propname]
 
+        if isinstance(prop, Computed):
+            return prop.function(prop, nodeid, self.db)
+
         # lazy evaluation of Multilink
         if propname not in d and isinstance(prop, Multilink):
             self.db._materialize_multilink(self.classname, nodeid, d, propname)
--- a/roundup/cgi/templating.py	Fri Nov 27 17:55:52 2020 -0500
+++ b/roundup/cgi/templating.py	Fri Nov 27 18:09:00 2020 -0500
@@ -1882,6 +1882,33 @@
             value = html_escape(value)
         return value
 
+class ComputedHTMLProperty(HTMLProperty):
+    def plain(self, escape=0):
+        """ Render a "plain" representation of the property
+        """
+        if not self.is_view_ok():
+            return self._('[hidden]')
+
+        if self._value is None:
+            return ''
+        try:
+            if isinstance(self._value, str):
+                value = self._value
+            else:
+                value = str(self._value)
+
+        except AttributeError:
+            value = self._('[hidden]')
+        if escape:
+            value = html_escape(value)
+        return value
+
+    def field(self):
+        """ Computed properties are not editable so
+            just display the value via plain().
+        """
+        return self.plain(escape=1)
+
 class PasswordHTMLProperty(HTMLProperty):
     def plain(self, escape=0):
         """ Render a "plain" representation of the property
@@ -2768,6 +2795,7 @@
     (hyperdb.Boolean, BooleanHTMLProperty),
     (hyperdb.Date, DateHTMLProperty),
     (hyperdb.Interval, IntervalHTMLProperty),
+    (hyperdb.Computed, ComputedHTMLProperty),
     (hyperdb.Password, PasswordHTMLProperty),
     (hyperdb.Link, LinkHTMLProperty),
     (hyperdb.Multilink, MultilinkHTMLProperty),
--- a/roundup/hyperdb.py	Fri Nov 27 17:55:52 2020 -0500
+++ b/roundup/hyperdb.py	Fri Nov 27 18:09:00 2020 -0500
@@ -73,6 +73,71 @@
         """
         return val
 
+class Computed(object):
+    """A roundup computed property type. Not inherited from _Type
+       as the value is never changed. It is defined by running a
+       method.
+
+       This property has some restrictions. It is read only and can
+       not be set. It can not (currently) be searched or used for
+       sorting or grouping.
+    """
+
+    def __init__(self, function, default_value=None, cachefor=None):
+        """ function: a callable method on this class.
+            default_value: default value to be returned by the method
+            cachefor: an interval property used to determine how long to
+                      cache the value from the function. Not yet
+                       implemented.
+        """
+        self.function = function
+        self.__default_value = default_value
+        self.computed = True
+
+        # alert the user that cachefor is not valid yet.
+        if cachefor is not None:
+            raise NotImplementedError
+
+    def __repr__(self):
+        ' more useful for dumps '
+        return '<%s.%s computed %s>' % (self.__class__.__module__,
+                                        self.__class__.__name__,
+                                        self.function.__name__)
+
+    def get_default_value(self):
+        """The default value when creating a new instance of this property."""
+        return self.__default_value
+
+    def register (self, cls, propname):
+        """Register myself to the class of which we are a property
+           the given propname is the name we have in our class.
+        """
+        assert not getattr(self, 'cls', None)
+        self.name = propname
+        self.cls  = cls
+
+    def sort_repr(self, cls, val, name):
+        """Representation used for sorting. This should be a python
+        built-in type, otherwise sorting will take ages. Note that
+        individual backends may chose to use something different for
+        sorting as long as the outcome is the same.
+        """
+        return val
+
+    def message_count(self, nodeid, db):
+        """Example method that counts the number of messages for an issue.
+
+           Adding a property to the IssueClass like:
+
+                msgcount = Computed(Computed.message_count)
+
+           allows querying for the msgcount property to get a count of
+           the number of messages on the issue. Note that you can not
+           currently search, sort or group using a computed property
+           like msgcount.
+        """
+
+        return len(db.issue.get(nodeid, 'messages'))
 
 class String(_Type):
     """An object designating a String property."""
--- a/roundup/instance.py	Fri Nov 27 17:55:52 2020 -0500
+++ b/roundup/instance.py	Fri Nov 27 18:09:00 2020 -0500
@@ -132,6 +132,7 @@
             'Multilink': hyperdb.Multilink,
             'Interval': hyperdb.Interval,
             'Boolean': hyperdb.Boolean,
+            'Computed': hyperdb.Computed,
             'Number': hyperdb.Number,
             'Integer': hyperdb.Integer,
             'db': backend.Database(self.config, name)

Roundup Issue Tracker: http://roundup-tracker.org/