Skip to content

Commit 14d8691

Browse files
committed
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.
1 parent 5672a52 commit 14d8691

File tree

6 files changed

+181
-1
lines changed

6 files changed

+181
-1
lines changed

doc/customizing.txt

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,20 @@ A Class is comprised of one or more properties of the following types:
741741
properties are for storing encoded arbitrary-length strings.
742742
The default encoding is defined on the ``roundup.password.Password``
743743
class.
744+
Computed
745+
properties invoke a python function. The return value of the
746+
function is the value of the property. Unlike other properties,
747+
the property is read only and can not be changed. Use cases:
748+
ask a remote interface for a value (e.g. retrieve user's office
749+
location from ldap, query state of a related ticket from
750+
another roundup instance). It can be used to compute a value
751+
(e.g. count the number of messages for an issue). The type
752+
returned by the function is the type of the value. (Note it is
753+
coerced to a string when displayed in the html interface.) At
754+
this time it's a partial implementation. It can be
755+
displayed/queried only. It can not be searched or used for
756+
sorting or grouping as it does not exist in the back end
757+
database.
744758
Date
745759
properties store date-and-time stamps. Their values are Timestamp
746760
objects.
@@ -4046,6 +4060,72 @@ caches the schema).
40464060
columns string:id,activity,due_date,title,creator,status;
40474061
columns_showall string:id,activity,due_date,title,creator,assignedto,status;
40484062

4063+
Adding a new Computed field to the schema
4064+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4065+
4066+
Computed properties are a bit different from other properties. They do
4067+
not actually change the database. Computed fields are not contained in
4068+
the database and can not be searched or used for sorting or
4069+
grouping. (Patches to add this capability are welcome.)
4070+
4071+
In this example we will add a count of the number of files attached to
4072+
the issue. This could be done using an auditor (see below) to update
4073+
an integer field called ``filecount``. But we will implement this in a
4074+
different way.
4075+
4076+
We have two changes to make:
4077+
4078+
1. add a new python method to the hyperdb.Computed class. It will
4079+
count the number of files attached to the issue. This method will
4080+
be added in the (possibly new) interfaces.py file in the top level
4081+
of the tracker directory. (See interfaces.py above for more
4082+
information.)
4083+
2. add a new ``filecount`` property to the issue class calling the
4084+
new function.
4085+
4086+
A Computed method receives three arguments when called:
4087+
4088+
1. the computed object (self)
4089+
2. the id of the item in the class
4090+
3. the database object
4091+
4092+
To add the method to the Computed class, modify the trackers
4093+
interfaces.py adding::
4094+
4095+
import roundup.hyperdb as hyperdb
4096+
def filecount(self, nodeid, db):
4097+
return len(db.issue.get(nodeid, 'files'))
4098+
setattr(hyperdb.Computed, 'filecount', filecount)
4099+
4100+
Then add::
4101+
4102+
filecount = Computed(Computed.filecount),
4103+
4104+
to the existing IssueClass call.
4105+
4106+
Now you can retrieve the value of the ``filecount`` property and it
4107+
will be computed on the fly from the existing list of attached files.
4108+
4109+
This example was done with the IssueClass, but you could add a
4110+
Computed property to any class. E.G.::
4111+
4112+
user = Class(db, "user",
4113+
username=String(),
4114+
password=Password(),
4115+
address=String(),
4116+
realname=String(),
4117+
phone=String(),
4118+
office=Computed(Computed.getOfficeFromLdap), # new prop
4119+
organisation=String(),
4120+
alternate_addresses=String(),
4121+
queries=Multilink('query'),
4122+
roles=String(),
4123+
timezone=String())
4124+
4125+
where the method ``getOfficeFromLdap`` queries the local ldap server to
4126+
get the current office location information. The method will be called
4127+
with the Computed instance, the user id and the database object.
4128+
40494129
Adding a new constrained field to the classic schema
40504130
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40514131

roundup/backends/back_anydbm.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,9 @@ def get(self, nodeid, propname, default=_marker, cache=1):
11461146
# get the property (raises KeyErorr if invalid)
11471147
prop = self.properties[propname]
11481148

1149+
if isinstance(prop, Computed):
1150+
return prop.function(prop, nodeid, self.db)
1151+
11491152
if isinstance(prop, hyperdb.Multilink) and prop.computed:
11501153
cls = self.db.getclass(prop.rev_classname)
11511154
ids = cls.find(**{prop.rev_propname: nodeid})

roundup/backends/rdbms_common.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
# roundup modules
5858
from roundup import hyperdb, date, password, roundupdb, security, support
5959
from roundup.hyperdb import String, Password, Date, Interval, Link, \
60-
Multilink, DatabaseError, Boolean, Number, Integer
60+
Multilink, DatabaseError, Boolean, Computed, Number, Integer
6161
from roundup.i18n import _
6262

6363
# support
@@ -1780,6 +1780,9 @@ def get(self, nodeid, propname, default=_marker, cache=1):
17801780
# get the property (raises KeyError if invalid)
17811781
prop = self.properties[propname]
17821782

1783+
if isinstance(prop, Computed):
1784+
return prop.function(prop, nodeid, self.db)
1785+
17831786
# lazy evaluation of Multilink
17841787
if propname not in d and isinstance(prop, Multilink):
17851788
self.db._materialize_multilink(self.classname, nodeid, d, propname)

roundup/cgi/templating.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,6 +1882,33 @@ def email(self, escape=1):
18821882
value = html_escape(value)
18831883
return value
18841884

1885+
class ComputedHTMLProperty(HTMLProperty):
1886+
def plain(self, escape=0):
1887+
""" Render a "plain" representation of the property
1888+
"""
1889+
if not self.is_view_ok():
1890+
return self._('[hidden]')
1891+
1892+
if self._value is None:
1893+
return ''
1894+
try:
1895+
if isinstance(self._value, str):
1896+
value = self._value
1897+
else:
1898+
value = str(self._value)
1899+
1900+
except AttributeError:
1901+
value = self._('[hidden]')
1902+
if escape:
1903+
value = html_escape(value)
1904+
return value
1905+
1906+
def field(self):
1907+
""" Computed properties are not editable so
1908+
just display the value via plain().
1909+
"""
1910+
return self.plain(escape=1)
1911+
18851912
class PasswordHTMLProperty(HTMLProperty):
18861913
def plain(self, escape=0):
18871914
""" Render a "plain" representation of the property
@@ -2768,6 +2795,7 @@ def menu(self, size=None, height=None, showid=0, additional=[],
27682795
(hyperdb.Boolean, BooleanHTMLProperty),
27692796
(hyperdb.Date, DateHTMLProperty),
27702797
(hyperdb.Interval, IntervalHTMLProperty),
2798+
(hyperdb.Computed, ComputedHTMLProperty),
27712799
(hyperdb.Password, PasswordHTMLProperty),
27722800
(hyperdb.Link, LinkHTMLProperty),
27732801
(hyperdb.Multilink, MultilinkHTMLProperty),

roundup/hyperdb.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,71 @@ def sort_repr(self, cls, val, name):
7373
"""
7474
return val
7575

76+
class Computed(object):
77+
"""A roundup computed property type. Not inherited from _Type
78+
as the value is never changed. It is defined by running a
79+
method.
80+
81+
This property has some restrictions. It is read only and can
82+
not be set. It can not (currently) be searched or used for
83+
sorting or grouping.
84+
"""
85+
86+
def __init__(self, function, default_value=None, cachefor=None):
87+
""" function: a callable method on this class.
88+
default_value: default value to be returned by the method
89+
cachefor: an interval property used to determine how long to
90+
cache the value from the function. Not yet
91+
implemented.
92+
"""
93+
self.function = function
94+
self.__default_value = default_value
95+
self.computed = True
96+
97+
# alert the user that cachefor is not valid yet.
98+
if cachefor is not None:
99+
raise NotImplementedError
100+
101+
def __repr__(self):
102+
' more useful for dumps '
103+
return '<%s.%s computed %s>' % (self.__class__.__module__,
104+
self.__class__.__name__,
105+
self.function.__name__)
106+
107+
def get_default_value(self):
108+
"""The default value when creating a new instance of this property."""
109+
return self.__default_value
110+
111+
def register (self, cls, propname):
112+
"""Register myself to the class of which we are a property
113+
the given propname is the name we have in our class.
114+
"""
115+
assert not getattr(self, 'cls', None)
116+
self.name = propname
117+
self.cls = cls
118+
119+
def sort_repr(self, cls, val, name):
120+
"""Representation used for sorting. This should be a python
121+
built-in type, otherwise sorting will take ages. Note that
122+
individual backends may chose to use something different for
123+
sorting as long as the outcome is the same.
124+
"""
125+
return val
126+
127+
def message_count(self, nodeid, db):
128+
"""Example method that counts the number of messages for an issue.
129+
130+
Adding a property to the IssueClass like:
131+
132+
msgcount = Computed(Computed.message_count)
133+
134+
allows querying for the msgcount property to get a count of
135+
the number of messages on the issue. Note that you can not
136+
currently search, sort or group using a computed property
137+
like msgcount.
138+
"""
139+
140+
return len(db.issue.get(nodeid, 'messages'))
76141

77142
class String(_Type):
78143
"""An object designating a String property."""

roundup/instance.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def open(self, name=None):
132132
'Multilink': hyperdb.Multilink,
133133
'Interval': hyperdb.Interval,
134134
'Boolean': hyperdb.Boolean,
135+
'Computed': hyperdb.Computed,
135136
'Number': hyperdb.Number,
136137
'Integer': hyperdb.Integer,
137138
'db': backend.Database(self.config, name)

0 commit comments

Comments
 (0)