changeset 5067:e424987d294a

Add support for an integer type to join the existing number type. Commit patch supplied for issue2550886. This can be used for properties used for ordering, counts etc. where a decimal point isn't needed. Developed by Anthony (antmail). Doc updates written by John Rouillard.
author John Rouillard <rouilj@ieee.org>
date Sun, 05 Jun 2016 00:17:26 -0400
parents d2256fcfd81f
children 5b2ce5723abb
files CHANGES.txt doc/FAQ.txt doc/customizing.txt doc/design.txt roundup/backends/back_anydbm.py roundup/backends/back_mysql.py roundup/backends/back_sqlite.py roundup/backends/rdbms_common.py roundup/cgi/actions.py roundup/cgi/form_parser.py roundup/cgi/templating.py roundup/hyperdb.py roundup/instance.py test/db_test_base.py test/test_cgi.py test/test_hyperdbvals.py test/test_templating.py
diffstat 17 files changed, 211 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Tue May 31 09:16:09 2016 +0200
+++ b/CHANGES.txt	Sun Jun 05 00:17:26 2016 -0400
@@ -28,6 +28,11 @@
 - Allow multiple file uploads: If the html template specifies
   multiple="multiple" for a file upload the user can attach multiple
   files and the form parser now handles this. (Ralf Schlatterbeck)
+- issue2550886: Add support for an integer type to join the existing
+  number type. This can be used for properties used for ordering,
+  counts etc. where a decimal point isn't needed. Developed by
+  Anthony (antmail). Doc updates written by John Rouillard. (applied
+  by John Rouillard)
 
 Fixed:
 
--- a/doc/FAQ.txt	Tue May 31 09:16:09 2016 +0200
+++ b/doc/FAQ.txt	Sun Jun 05 00:17:26 2016 -0400
@@ -204,8 +204,9 @@
 When we sort items in the hyperdb, we use one of a number of methods,
 depending on the properties being sorted on:
 
-1. If it's a String, Number, Date or Interval property, we just sort the
-   scalar value of the property. Strings are sorted case-sensitively.
+1. If it's a String, Integer, Number, Date or Interval property, we
+   just sort the scalar value of the property. Strings are sorted
+   case-sensitively.
 2. If it's a Link property, we sort by either the linked item's "order"
    property (if it has one) or the linked item's "id".
 3. Mulitlinks sort similar to #2, but we start with the first
--- a/doc/customizing.txt	Tue May 31 09:16:09 2016 +0200
+++ b/doc/customizing.txt	Sun Jun 05 00:17:26 2016 -0400
@@ -655,6 +655,7 @@
   class.
 * Date properties store date-and-time stamps. Their values are Timestamp
   objects.
+* Integer properties store integer values. (Number can store real/float values.)
 * Number properties store numeric values.
 * Boolean properties store on/off, yes/no, true/false values.
 * A Link property refers to a single other item selected from a
@@ -814,8 +815,9 @@
 When we sort items in the hyperdb, we use one of a number of methods,
 depending on the properties being sorted on:
 
-1. If it's a String, Number, Date or Interval property, we just sort the
-   scalar value of the property. Strings are sorted case-sensitively.
+1. If it's a String, Integer, Number, Date or Interval property, we
+   just sort the scalar value of the property. Strings are sorted
+   case-sensitively.
 2. If it's a Link property, we sort by either the linked item's "order"
    property (if it has one) or the linked item's "id".
 3. Mulitlinks sort similar to #2, but we start with the first Multilink
@@ -1660,7 +1662,7 @@
     they are valid for the class).  Otherwise, the property
     is set to the form value.
 
-    For Date(), Interval(), Boolean(), and Number()
+    For Date(), Interval(), Boolean(), Integer() and Number()
     properties, the form value is converted to the
     appropriate
 
--- a/doc/design.txt	Tue May 31 09:16:09 2016 +0200
+++ b/doc/design.txt	Sun Jun 05 00:17:26 2016 -0400
@@ -227,6 +227,8 @@
 
 - Boolean properties are for storing true/false, or yes/no values.
 
+- Integer properties are for storing Integer (non real) numeric values.
+
 - Number properties are for storing numeric values.
 
 - Date properties store date-and-time stamps. Their values are Timestamp
@@ -263,6 +265,10 @@
         def __init__(self):
             """An object designating a Boolean property."""
 
+    class Integer:
+        def __init__(self):
+            """An object designating an Integer property."""
+
     class Number:
         def __init__(self):
             """An object designating a Number property."""
@@ -1024,7 +1030,7 @@
 
 - Strings are, well, strings.
 
-- Numbers are displayed the same as strings.
+- Integers/Numbers are displayed the same as strings.
 
 - Booleans are displayed as 'Yes' or 'No'.
 
@@ -1646,7 +1652,7 @@
 Changes to this document
 ------------------------
 
-- Added Boolean and Number types
+- Added Boolean, Integer and Number types
 - Added section Hyperdatabase Implementations
 - "Item" has been renamed to "Issue" to account for the more specific
   nature of the Class.
--- a/roundup/backends/back_anydbm.py	Tue May 31 09:16:09 2016 +0200
+++ b/roundup/backends/back_anydbm.py	Sun Jun 05 00:17:26 2016 -0400
@@ -1001,6 +1001,12 @@
                 except ValueError:
                     raise TypeError('new property "%s" not numeric'%key)
 
+            elif value is not None and isinstance(prop, hyperdb.Integer):
+                try:
+                    int(value)
+                except ValueError:
+                    raise TypeError('new property "%s" not an integer'%key)
+
             elif value is not None and isinstance(prop, hyperdb.Boolean):
                 try:
                     int(value)
@@ -1342,6 +1348,13 @@
                     raise TypeError('new property "%s" not '
                         'numeric'%propname)
 
+            elif value is not None and isinstance(prop, hyperdb.Integer):
+                try:
+                    int(value)
+                except ValueError:
+                    raise TypeError('new property "%s" not '
+                        'numeric'%propname)
+
             elif value is not None and isinstance(prop, hyperdb.Boolean):
                 try:
                     int(value)
@@ -1748,6 +1761,14 @@
                         v = [v]
                 l.append((OTHER, k, [float(val) for val in v]))
 
+            elif isinstance(propclass, hyperdb.Integer):
+                if type(v) != type([]):
+                    try :
+                        v = v.split(',')
+                    except AttributeError :
+                        v = [v]
+                l.append((OTHER, k, [int(val) for val in v]))
+
         filterspec = l
 
         # now, find all the nodes that are active and pass filtering
--- a/roundup/backends/back_mysql.py	Tue May 31 09:16:09 2016 +0200
+++ b/roundup/backends/back_mysql.py	Sun Jun 05 00:17:26 2016 -0400
@@ -129,6 +129,7 @@
         hyperdb.Password  : 'VARCHAR(255)',
         hyperdb.Boolean   : 'BOOL',
         hyperdb.Number    : 'REAL',
+        hyperdb.Integer   : 'INTEGER',
     }
 
     hyperdb_to_sql_value = {
@@ -140,6 +141,7 @@
         hyperdb.Password  : str,
         hyperdb.Boolean   : int,
         hyperdb.Number    : lambda x: x,
+        hyperdb.Integer   : int,
         hyperdb.Multilink : lambda x: x,    # used in journal marshalling
     }
 
@@ -299,6 +301,8 @@
                         v = date.Interval(v)
                     elif isinstance(prop, Password) and v is not None:
                         v = password.Password(encrypted=v)
+                    elif isinstance(prop, Integer) and v is not None:
+                        v = int(v)
                     elif (isinstance(prop, Boolean) or
                             isinstance(prop, Number)) and v is not None:
                         v = float(v)
--- a/roundup/backends/back_sqlite.py	Tue May 31 09:16:09 2016 +0200
+++ b/roundup/backends/back_sqlite.py	Sun Jun 05 00:17:26 2016 -0400
@@ -51,6 +51,7 @@
         hyperdb.Password  : 'VARCHAR(255)',
         hyperdb.Boolean   : 'BOOLEAN',
         hyperdb.Number    : 'REAL',
+        hyperdb.Integer   : 'INTEGER',
     }
     hyperdb_to_sql_value = {
         hyperdb.String : str,
@@ -59,6 +60,7 @@
         hyperdb.Interval  : str,
         hyperdb.Password  : str,
         hyperdb.Boolean   : int,
+        hyperdb.Integer   : int,
         hyperdb.Number    : lambda x: x,
         hyperdb.Multilink : lambda x: x,    # used in journal marshalling
     }
@@ -69,6 +71,7 @@
         hyperdb.Interval  : date.Interval,
         hyperdb.Password  : lambda x: password.Password(encrypted=x),
         hyperdb.Boolean   : int,
+        hyperdb.Integer   : int,
         hyperdb.Number    : rdbms_common._num_cvt,
         hyperdb.Multilink : lambda x: x,    # used in journal marshalling
     }
--- a/roundup/backends/rdbms_common.py	Tue May 31 09:16:09 2016 +0200
+++ b/roundup/backends/rdbms_common.py	Sun Jun 05 00:17:26 2016 -0400
@@ -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, Node
+    Multilink, DatabaseError, Boolean, Number, Integer, Node
 from roundup.backends import locking
 from roundup.i18n import _
 
@@ -464,6 +464,7 @@
         hyperdb.Password  : 'VARCHAR(255)',
         hyperdb.Boolean   : 'BOOLEAN',
         hyperdb.Number    : 'REAL',
+        hyperdb.Integer   : 'INTEGER',
     }
 
     def hyperdb_to_sql_datatype(self, propclass):
@@ -871,6 +872,7 @@
         hyperdb.Password  : str,
         hyperdb.Boolean   : lambda x: x and 'TRUE' or 'FALSE',
         hyperdb.Number    : lambda x: x,
+        hyperdb.Integer   : lambda x: x,
         hyperdb.Multilink : lambda x: x,    # used in journal marshalling
     }
 
@@ -1084,6 +1086,7 @@
         hyperdb.Password  : lambda x: password.Password(encrypted=x),
         hyperdb.Boolean   : _bool_cvt,
         hyperdb.Number    : _num_cvt,
+        hyperdb.Integer   : int,
         hyperdb.Multilink : lambda x: x,    # used in journal marshalling
     }
 
@@ -1632,6 +1635,12 @@
                 except ValueError:
                     raise TypeError('new property "%s" not numeric'%key)
 
+            elif value is not None and isinstance(prop, Integer):
+                try:
+                    int(value)
+                except ValueError:
+                    raise TypeError('new property "%s" not integer'%key)
+
             elif value is not None and isinstance(prop, Boolean):
                 try:
                     int(value)
@@ -1931,6 +1940,12 @@
                 except ValueError:
                     raise TypeError('new property "%s" not numeric'%propname)
 
+            elif value is not None and isinstance(prop, Integer):
+                try:
+                    int(value)
+                except ValueError:
+                    raise TypeError('new property "%s" not integer'%propname)
+
             elif value is not None and isinstance(prop, Boolean):
                 try:
                     int(value)
--- a/roundup/cgi/actions.py	Tue May 31 09:16:09 2016 +0200
+++ b/roundup/cgi/actions.py	Sun Jun 05 00:17:26 2016 -0400
@@ -363,6 +363,8 @@
                         value = value.lower() in ('yes', 'true', 'on', '1')
                     elif isinstance(prop, hyperdb.Number):
                         value = float(value)
+                    elif isinstance(prop, hyperdb.Integer):
+                        value = int(value)
                     d[name] = value
                 elif exists:
                     # nuke the existing value
--- a/roundup/cgi/form_parser.py	Tue May 31 09:16:09 2016 +0200
+++ b/roundup/cgi/form_parser.py	Sun Jun 05 00:17:26 2016 -0400
@@ -135,7 +135,7 @@
                 they are valid for the class).  Otherwise, the property
                 is set to the form value.
 
-                For Date(), Interval(), Boolean(), and Number()
+                For Date(), Interval(), Boolean(), and Number(), Integer()
                 properties, the form value is converted to the
                 appropriate
 
@@ -519,7 +519,7 @@
                         # some backends store "missing" Strings as empty strings
                         if existing == self.db.BACKEND_MISSING_STRING:
                             existing = None
-                    elif isinstance(proptype, hyperdb.Number):
+                    elif isinstance(proptype, hyperdb.Number) or isinstance(proptype, hyperdb.Integer):
                         # some backends store "missing" Numbers as 0 :(
                         if existing == self.db.BACKEND_MISSING_NUMBER:
                             existing = None
--- a/roundup/cgi/templating.py	Tue May 31 09:16:09 2016 +0200
+++ b/roundup/cgi/templating.py	Sun Jun 05 00:17:26 2016 -0400
@@ -1201,7 +1201,7 @@
         return _HTMLItem(client, classname, nodeid, anonymous)
 
 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
-    """ String, Number, Date, Interval HTMLProperty
+    """ String, Integer, Number, Date, Interval HTMLProperty
 
         Has useful attributes:
 
@@ -1605,6 +1605,38 @@
         """
         return float(self._value)
 
+class IntegerHTMLProperty(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 ''
+
+        return str(self._value)
+
+    def field(self, size=30, **kwargs):
+        """ Render a form edit field for the property.
+
+            If not editable, just display the value via plain().
+        """
+        if not self.is_edit_ok():
+            return self.plain(escape=1)
+
+        value = self._value
+        if value is None:
+            value = ''
+
+        return self.input(name=self._formname, value=value, size=size,
+                          **kwargs)
+
+    def __int__(self):
+        """ Return an int of me
+        """
+        return int(self._value)
+
 
 class BooleanHTMLProperty(HTMLProperty):
     def plain(self, escape=0):
@@ -2352,6 +2384,7 @@
 propclasses = [
     (hyperdb.String, StringHTMLProperty),
     (hyperdb.Number, NumberHTMLProperty),
+    (hyperdb.Integer, IntegerHTMLProperty),
     (hyperdb.Boolean, BooleanHTMLProperty),
     (hyperdb.Date, DateHTMLProperty),
     (hyperdb.Interval, IntervalHTMLProperty),
--- a/roundup/hyperdb.py	Tue May 31 09:16:09 2016 +0200
+++ b/roundup/hyperdb.py	Sun Jun 05 00:17:26 2016 -0400
@@ -296,6 +296,17 @@
             raise HyperdbValueError, _('property %s: %r is not a number')%(
                 kw['propname'], value)
         return value
+
+class Integer(_Type):
+    """An object designating an integer property"""
+    def from_raw(self, value, **kw):
+        value = value.strip()
+        try:
+            value = int(value)
+        except ValueError:
+            raise HyperdbValueError, _('property %s: %r is not an integer')%(
+                kw['propname'], value)
+        return value
 #
 # Support for splitting designators
 #
--- a/roundup/instance.py	Tue May 31 09:16:09 2016 +0200
+++ b/roundup/instance.py	Sun Jun 05 00:17:26 2016 -0400
@@ -121,6 +121,7 @@
             'Interval': hyperdb.Interval,
             'Boolean': hyperdb.Boolean,
             'Number': hyperdb.Number,
+            'Integer': hyperdb.Integer,
             'db': backend.Database(self.config, name)
         }
 
--- a/test/db_test_base.py	Tue May 31 09:16:09 2016 +0200
+++ b/test/db_test_base.py	Sun Jun 05 00:17:26 2016 -0400
@@ -22,7 +22,7 @@
 
 import pytest
 from roundup.hyperdb import String, Password, Link, Multilink, Date, \
-    Interval, DatabaseError, Boolean, Number, Node
+    Interval, DatabaseError, Boolean, Number, Node, Integer
 from roundup.mailer import Mailer
 from roundup import date, password, init, instance, configuration, \
     roundupdb, i18n
@@ -82,7 +82,7 @@
     priority.setkey("name")
     user = module.Class(db, "user", username=String(), password=Password(),
         assignable=Boolean(), age=Number(), roles=String(), address=String(),
-        supervisor=Link('user'),realname=String())
+        rating=Integer(), supervisor=Link('user'),realname=String())
     user.setkey("username")
     file = module.FileClass(db, "file", name=String(), type=String(),
         comment=String(indexme="yes"), fooz=Password())
@@ -534,6 +534,25 @@
         self.db.user.set(nid, age=None)
         self.assertEqual(self.db.user.get(nid, "age"), None)
 
+    # Integer
+    def testIntegerChange(self):
+        nid = self.db.user.create(username='foo', rating=100)
+        self.assertEqual(100, self.db.user.get(nid, 'rating'))
+        self.db.user.set(nid, rating=300)
+        self.assertNotEqual(self.db.user.get(nid, 'rating'), 100)
+        self.db.user.set(nid, rating=-1)
+        self.assertEqual(self.db.user.get(nid, 'rating'), -1)
+        self.db.user.set(nid, rating=0)
+        self.assertEqual(self.db.user.get(nid, 'rating'), 0)
+
+        nid = self.db.user.create(username='bar', rating=0)
+        self.assertEqual(self.db.user.get(nid, 'rating'), 0)
+
+    def testIntegerUnset(self):
+        nid = self.db.user.create(username='foo', rating=1)
+        self.db.user.set(nid, rating=None)
+        self.assertEqual(self.db.user.get(nid, "rating"), None)
+
     # Password
     def testPasswordChange(self):
         x = password.Password('x')
--- a/test/test_cgi.py	Tue May 31 09:16:09 2016 +0200
+++ b/test/test_cgi.py	Sun Jun 05 00:17:26 2016 -0400
@@ -107,9 +107,10 @@
 
         test = self.instance.backend.Class(self.db, "test",
             string=hyperdb.String(), number=hyperdb.Number(),
-            boolean=hyperdb.Boolean(), link=hyperdb.Link('test'),
-            multilink=hyperdb.Multilink('test'), date=hyperdb.Date(),
-            messages=hyperdb.Multilink('msg'), interval=hyperdb.Interval())
+            intval=hyperdb.Integer(), boolean=hyperdb.Boolean(),
+            link=hyperdb.Link('test'), multilink=hyperdb.Multilink('test'),
+            date=hyperdb.Date(), messages=hyperdb.Multilink('msg'),
+            interval=hyperdb.Interval())
 
         # compile the labels re
         classes = '|'.join(self.db.classes.keys())
@@ -623,6 +624,62 @@
             self.fail('number "no" raised "required missing"')
 
     #
+    # Integer
+    #
+    def testEmptyInteger(self):
+        self.assertEqual(self.parseForm({'intval': ''}),
+            ({('test', None): {}}, []))
+        self.assertEqual(self.parseForm({'intval': ' '}),
+            ({('test', None): {}}, []))
+        self.assertRaises(FormError, self.parseForm, {'intval': ['', '']})
+
+    def testInvalidInteger(self):
+        self.assertRaises(FormError, self.parseForm, {'intval': 'hi, mum!'})
+
+    def testSetInteger(self):
+        self.assertEqual(self.parseForm({'intval': '1'}),
+            ({('test', None): {'intval': 1}}, []))
+        self.assertEqual(self.parseForm({'intval': '0'}),
+            ({('test', None): {'intval': 0}}, []))
+        self.assertEqual(self.parseForm({'intval': '\n0\n'}),
+            ({('test', None): {'intval': 0}}, []))
+
+    def testSetIntegerReplaceOne(self):
+        nodeid = self.db.test.create(intval=1)
+        self.assertEqual(self.parseForm({'intval': '1'}, 'test', nodeid),
+            ({('test', nodeid): {}}, []))
+        self.assertEqual(self.parseForm({'intval': '0'}, 'test', nodeid),
+            ({('test', nodeid): {'intval': 0}}, []))
+
+    def testSetIntegerReplaceZero(self):
+        nodeid = self.db.test.create(intval=0)
+        self.assertEqual(self.parseForm({'intval': '0'}, 'test', nodeid),
+            ({('test', nodeid): {}}, []))
+
+    def testSetIntegerReplaceNone(self):
+        nodeid = self.db.test.create()
+        self.assertEqual(self.parseForm({'intval': '0'}, 'test', nodeid),
+            ({('test', nodeid): {'intval': 0}}, []))
+        self.assertEqual(self.parseForm({'intval': '1'}, 'test', nodeid),
+            ({('test', nodeid): {'intval': 1}}, []))
+
+    def testEmptyIntegerSet(self):
+        nodeid = self.db.test.create(intval=0)
+        self.assertEqual(self.parseForm({'intval': ''}, 'test', nodeid),
+            ({('test', nodeid): {'intval': None}}, []))
+        nodeid = self.db.test.create(intval=1)
+        self.assertEqual(self.parseForm({'intval': ' '}, 'test', nodeid),
+            ({('test', nodeid): {'intval': None}}, []))
+
+    def testRequiredInteger(self):
+        self.assertRaises(FormError, self.parseForm, {'intval': '',
+            ':required': 'intval'})
+        try:
+            self.parseForm({'intval': '0', ':required': 'intval'})
+        except FormError:
+            self.fail('intval "no" raised "required missing"')
+
+    #
     # Date
     #
     def testEmptyDate(self):
--- a/test/test_hyperdbvals.py	Tue May 31 09:16:09 2016 +0200
+++ b/test/test_hyperdbvals.py	Sun Jun 05 00:17:26 2016 -0400
@@ -18,6 +18,7 @@
         return {
             'string': hyperdb.String(),
             'number': hyperdb.Number(),
+            'integer': hyperdb.Integer(),
             'boolean': hyperdb.Boolean(),
             'password': hyperdb.Password(),
             'date': hyperdb.Date(),
@@ -65,6 +66,15 @@
         self.assertEqual(self._test('password', ''), None)
         self.assertEqual(self._test('number', '  10 '), 10)
         self.assertEqual(self._test('number', '  1.5 '), 1.5)
+        self.assertEqual(self._test('number', '  -1022.5 '), -1022.5)
+    def testInteger(self):
+        self.assertEqual(self._test('integer', '  100 '), 100)
+        self.assertEqual(self._test('integer', '  0 '), 0)
+        self.assertEqual(self._test('integer', '  -100 '), -100)
+        # make sure error raised on string
+        self.assertRaises(hyperdb.HyperdbValueError, self._test, 'integer', 'a string', 'a string')
+        # make sure error raised on real number
+        self.assertRaises(hyperdb.HyperdbValueError, self._test, 'integer', '  -100.2 ')
     def testBoolean(self):
         self.assertEqual(self._test('password', ''), None)
         for true in 'yes true on 1'.split():
--- a/test/test_templating.py	Tue May 31 09:16:09 2016 +0200
+++ b/test/test_templating.py	Sun Jun 05 00:17:26 2016 -0400
@@ -263,6 +263,11 @@
     def __int__(self):
     def __float__(self):
 
+class IntegerHTMLProperty(HTMLProperty):
+    def plain(self):
+    def field(self, size = 30):
+    def __int__(self):
+
 class BooleanHTMLProperty(HTMLProperty):
     def plain(self):
     def field(self):

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