From 14d869147a41f89e7a5a4bc2eccbcedd6ab9eb10 Mon Sep 17 00:00:00 2001 From: John Rouillard Date: Fri, 27 Nov 2020 18:09:00 -0500 Subject: [PATCH 1/4] 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. --- doc/customizing.txt | 80 ++++++++++++++++++++++++++++++++ roundup/backends/back_anydbm.py | 3 ++ roundup/backends/rdbms_common.py | 5 +- roundup/cgi/templating.py | 28 +++++++++++ roundup/hyperdb.py | 65 ++++++++++++++++++++++++++ roundup/instance.py | 1 + 6 files changed, 181 insertions(+), 1 deletion(-) diff --git a/doc/customizing.txt b/doc/customizing.txt index d6d34d720..97adcbda2 100644 --- a/doc/customizing.txt +++ b/doc/customizing.txt @@ -741,6 +741,20 @@ A Class is comprised of one or more properties of the following types: 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 @@ caches the schema). 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/roundup/backends/back_anydbm.py b/roundup/backends/back_anydbm.py index 4ea00c468..c6c4f77af 100644 --- a/roundup/backends/back_anydbm.py +++ b/roundup/backends/back_anydbm.py @@ -1146,6 +1146,9 @@ def get(self, nodeid, propname, default=_marker, cache=1): # 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}) diff --git a/roundup/backends/rdbms_common.py b/roundup/backends/rdbms_common.py index cb548a226..519691113 100644 --- a/roundup/backends/rdbms_common.py +++ b/roundup/backends/rdbms_common.py @@ -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 @@ def get(self, nodeid, propname, default=_marker, cache=1): # 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) diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py index 51e2bc170..a434cd7f1 100644 --- a/roundup/cgi/templating.py +++ b/roundup/cgi/templating.py @@ -1882,6 +1882,33 @@ def email(self, escape=1): 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 @@ def menu(self, size=None, height=None, showid=0, additional=[], (hyperdb.Boolean, BooleanHTMLProperty), (hyperdb.Date, DateHTMLProperty), (hyperdb.Interval, IntervalHTMLProperty), + (hyperdb.Computed, ComputedHTMLProperty), (hyperdb.Password, PasswordHTMLProperty), (hyperdb.Link, LinkHTMLProperty), (hyperdb.Multilink, MultilinkHTMLProperty), diff --git a/roundup/hyperdb.py b/roundup/hyperdb.py index ad4435587..6f98ba9ff 100644 --- a/roundup/hyperdb.py +++ b/roundup/hyperdb.py @@ -73,6 +73,71 @@ def sort_repr(self, cls, val, name): """ 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.""" diff --git a/roundup/instance.py b/roundup/instance.py index 4542252ce..9addd4fe8 100644 --- a/roundup/instance.py +++ b/roundup/instance.py @@ -132,6 +132,7 @@ def open(self, name=None): 'Multilink': hyperdb.Multilink, 'Interval': hyperdb.Interval, 'Boolean': hyperdb.Boolean, + 'Computed': hyperdb.Computed, 'Number': hyperdb.Number, 'Integer': hyperdb.Integer, 'db': backend.Database(self.config, name) From 7a035a8cca0a1c4ab109a90c9658b2cff4b76026 Mon Sep 17 00:00:00 2001 From: John Rouillard Date: Mon, 21 Dec 2020 00:46:35 -0500 Subject: [PATCH 2/4] Computed needs to be hyperdb.Computed. Symbol not imported in anydbm like it is in rdbms. --- roundup/backends/back_anydbm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roundup/backends/back_anydbm.py b/roundup/backends/back_anydbm.py index c6c4f77af..ea7c7684b 100644 --- a/roundup/backends/back_anydbm.py +++ b/roundup/backends/back_anydbm.py @@ -1146,7 +1146,7 @@ def get(self, nodeid, propname, default=_marker, cache=1): # get the property (raises KeyErorr if invalid) prop = self.properties[propname] - if isinstance(prop, Computed): + if isinstance(prop, hyperdb.Computed): return prop.function(prop, nodeid, self.db) if isinstance(prop, hyperdb.Multilink) and prop.computed: From e1826c5e07b09d8aa895fb9cffd0eb0486e0128e Mon Sep 17 00:00:00 2001 From: John Rouillard Date: Fri, 15 Jan 2021 16:34:30 -0500 Subject: [PATCH 3/4] issue2551109 - improve keyword editing in jinja2 template. --- CHANGES.txt | 2 +- .../templates/jinja2/html/keyword.item.html | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3a571ec45..8701c19b6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -73,7 +73,7 @@ Features: available. (John Rouillard) - Added explanation for modifying Fileclass content files to customizing.txt. Result of mailing list question. (John Rouillard) - +- issue2551109 - improve keyword editing in jinja2 template. 2020-07-13 2.0.0 diff --git a/share/roundup/templates/jinja2/html/keyword.item.html b/share/roundup/templates/jinja2/html/keyword.item.html index 8d66bcd98..f7b118189 100644 --- a/share/roundup/templates/jinja2/html/keyword.item.html +++ b/share/roundup/templates/jinja2/html/keyword.item.html @@ -11,28 +11,32 @@ {% block content %}

Existing Keywords

-

- {% trans %}To edit an existing keyword (for spelling or typing errors), - click on its entry above.{% endtrans %} -

- +

+ {% trans %}To edit an existing keyword (for spelling or typing errors), + click on its entry above.{% endtrans %} +

+
- + - - - + {{ context.submit(html_kwargs={ 'class': 'btn btn-primary' })|u|safe }}
{% endblock %} From 331867989e21a7d2ee59de6d2ebac8d09c8b75aa Mon Sep 17 00:00:00 2001 From: John Rouillard Date: Sun, 9 Jul 2023 17:42:40 -0400 Subject: [PATCH 4/4] limit markdown2 to a working version 2.4.9 broke [text](page) relative links. Also reduce tested versions on travisci. --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 56c8c1dae..2cdb0479e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,9 +25,9 @@ python: - 3.10.4 # - 3.9 # - 3.8 - - 3.6 - - 3.11-dev - - nightly +# - 3.6 +# - 3.11-dev +# - nightly # - pypy3 services: @@ -135,7 +135,7 @@ install: - if [[ $TRAVIS_PYTHON_VERSION != "3.4"* ]]; then pip install docutils; fi - if [[ $TRAVIS_PYTHON_VERSION != "3.4"* ]]; then pip install mistune==0.8.4; fi - if [[ $TRAVIS_PYTHON_VERSION != "3.4"* && $TRAVIS_PYTHON_VERSION != "2."* ]]; then pip install Markdown; fi - - pip install markdown2 + - pip install 'markdown2<=2.4.8' - pip install brotli # zstd fails to build under python nightly aborting test. # allow testing to still happen if the optional package doesn't install.