Mercurial > p > roundup > code
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)
