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