Skip to content

Commit 5ac6e59

Browse files
committed
feat(web) - Use native number type input for Number() and Integer().
When editing hyperdb.Number() or hyperdb.Integer() properties, use a native number input. For Number you can enter digits, +/-, . and e/E for exponent (1E2 = 100). For integer we have the same keys as number, but also add step=1 to the input. This stops submitting 23.5 suggesting 23 or 24. It does allow 2E4 to be submitted that is rejected with an error from the backend. However if the spinner is used with 2E4 it is turned into 20000, a pure integer and incremented/decremented by the spinner. The upgrade happens automatically. Directions on going back to text input provided. User guide updated to describe addition of spinner. Tests added.
1 parent caf5612 commit 5ac6e59

File tree

5 files changed

+141
-4
lines changed

5 files changed

+141
-4
lines changed

CHANGES.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ Features:
122122
- Added new templating utils.set_http_response(integer) method to
123123
allow reporting an error to the user from a template. (John
124124
Rouillard)
125-
125+
- Use native number type input for Number() and Integer()
126+
properties. Integer() uses step=1 as well. (John Rouillard)
127+
126128
2024-07-13 2.4.0
127129

128130
Fixed:

doc/upgrading.txt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ defusedxml.
224224

225225
.. _defusedxml: https://pypi.org/project/defusedxml/
226226

227-
Use native date inputs (optional)
228-
---------------------------------
227+
Enable use of native date inputs (optional)
228+
-------------------------------------------
229229

230230
Roundup now uses native date or datetime-local inputs for Date()
231231
properties. These inputs take the place of the text input and
@@ -308,6 +308,8 @@ changing it to::
308308
will generate the input as in Roundup 2.4 or earlier without a
309309
popcal link.
310310

311+
.. _revert_input_to_text:
312+
311313
If you are using a path expression like::
312314

313315
tal:content="context/duedate/field"
@@ -366,6 +368,22 @@ There is no support for activating text mode using the
366368
keyboard. Tablet/touch support is mixed. Chrome supports
367369
double-tap to activate text mode input. Firefox does not.
368370

371+
Revert native number inputs for Number() and Integer() (optional)
372+
-----------------------------------------------------------------
373+
374+
Roundup's field() method for properties of type Number() or
375+
Integer() now use a native browser number input.
376+
377+
You can use the old style text inputs by calling the field
378+
method with ``type="text"``. You can :ref:`follow the example
379+
for setting the text argument on a Date()
380+
property. <revert_input_to_text>` for details.
381+
382+
Note that the Integer() type also uses ``step="1"`` by default to
383+
add a stepper control and try to constrain the input to
384+
integers. This can be overridden by passing a new step
385+
(e.g. ``step="50"``) to the field() method.
386+
369387
Change in REST response for invalid CORS requests (info)
370388
--------------------------------------------------------
371389

doc/user_guide.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ Integer Properties
131131
~~~~~~~~~~~~~~~~~~
132132

133133
These fields take a whole/integer number without decimal marker like
134-
``123``, ``+123``, ``-123``. Exponents are not supported.
134+
``123``, ``+123``, ``-123``. Exponents are not supported. Your browser
135+
may show an up/down arrow spinner to change the value.
135136

136137
Boolean properties
137138
~~~~~~~~~~~~~~~~~~

roundup/cgi/templating.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,6 +2066,7 @@ def field(self, size=30, **kwargs):
20662066
if value is None:
20672067
value = ''
20682068

2069+
kwargs.setdefault("type", "number")
20692070
return self.input(name=self._formname, value=value, size=size,
20702071
**kwargs)
20712072

@@ -2104,6 +2105,9 @@ def field(self, size=30, **kwargs):
21042105
if value is None:
21052106
value = ''
21062107

2108+
kwargs.setdefault("type", "number")
2109+
if kwargs["type"] == "number":
2110+
kwargs.setdefault("step", "1")
21072111
return self.input(name=self._formname, value=value, size=size,
21082112
**kwargs)
21092113

test/test_templating.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,118 @@ def test_string_rst(self):
12531253
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'A string with [email protected] *embedded* \u00df'))
12541254
self.assertEqual(p.rst(), u2s(u'A string with <a href="mailto:[email protected]">[email protected]</a> *embedded* \u00df'))
12551255

1256+
class NumberIntegerHTMLPropertyTestCase(HTMLPropertyTestClass):
1257+
1258+
def setUp(self):
1259+
super(NumberIntegerHTMLPropertyTestCase, self).setUp()
1260+
1261+
db = self.client.db
1262+
db.issue.addprop(numberval=hyperdb.Number())
1263+
db.issue.addprop(intval=hyperdb.Integer())
1264+
1265+
self.client.db.issue.create(title="title",
1266+
numberval = "3.14",
1267+
intval="314")
1268+
1269+
def tearDown(self):
1270+
self.client.db.close()
1271+
memorydb.db_nuke('')
1272+
1273+
def test_IntegerHTML(self):
1274+
test_hyperdbInteger = self.client.db.issue.getprops("1")['intval']
1275+
test_Integer = test_hyperdbInteger.from_raw(
1276+
self.client.db.issue.get("1", 'intval')
1277+
)
1278+
1279+
# client, classname, nodeid, prop, name, value,
1280+
# anonymous=0, offset=None
1281+
d = IntegerHTMLProperty(self.client, 'issue', '1',
1282+
test_hyperdbInteger,
1283+
'intval', test_Integer)
1284+
1285+
self.assertIsInstance(d._value, int)
1286+
1287+
self.assertEqual(d.plain(), "314")
1288+
1289+
input_expected = """<input id="issue1@intval" name="issue1@intval" size="30" step="1" type="number" value="314">"""
1290+
self.assertEqual(d.field(), input_expected)
1291+
1292+
input_expected = """<input id="issue1@intval" name="issue1@intval" size="30" step="50" type="number" value="314">"""
1293+
self.assertEqual(d.field(step="50"), input_expected)
1294+
1295+
input_expected = """<input id="issue1@intval" name="issue1@intval" size="30" type="text" value="314">"""
1296+
self.assertEqual(d.field(type="text"), input_expected)
1297+
1298+
1299+
# check permissions return
1300+
is_view_ok_orig = d.is_view_ok
1301+
is_edit_ok_orig = d.is_edit_ok
1302+
no_access = lambda : False
1303+
1304+
d.is_view_ok = no_access
1305+
self.assertEqual(d.plain(), "[hidden]")
1306+
1307+
d.is_edit_ok = no_access
1308+
self.assertEqual(d.field(), "[hidden]")
1309+
1310+
d.is_view_ok = is_view_ok_orig
1311+
self.assertEqual(d.field(), "314")
1312+
d.is_edit_ok = is_edit_ok_orig
1313+
1314+
def test_NumberHTML(self):
1315+
test_hyperdbNumber = self.client.db.issue.getprops("1")['numberval']
1316+
test_Number = test_hyperdbNumber.from_raw(
1317+
self.client.db.issue.get("1", 'numberval')
1318+
)
1319+
1320+
# client, classname, nodeid, prop, name, value,
1321+
# anonymous=0, offset=None
1322+
d = NumberHTMLProperty(self.client, 'issue', '1',
1323+
test_hyperdbNumber,
1324+
'numberval', test_Number)
1325+
1326+
# string needed for memorydb/anydbm backend. Float?? when
1327+
# running against sql backends.
1328+
self.assertIsInstance(d._value, float)
1329+
1330+
self.assertEqual(d._value, 3.14)
1331+
1332+
input_expected = """<input id="issue1@numberval" name="issue1@numberval" size="30" type="number" value="3.14">"""
1333+
self.assertEqual(d.field(), input_expected)
1334+
1335+
input_expected = """<input id="issue1@numberval" name="issue1@numberval" size="30" step="50" type="number" value="3.14">"""
1336+
self.assertEqual(d.field(step="50"), input_expected)
1337+
1338+
input_expected = """<input id="issue1@numberval" name="issue1@numberval" size="30" type="text" value="3.14">"""
1339+
self.assertEqual(d.field(type="text"), input_expected)
1340+
1341+
self.assertEqual(d.pretty("%0.3f"), "3.140")
1342+
self.assertEqual(d.pretty("%0.3d"), "003")
1343+
self.assertEqual(d.pretty("%2d"), " 3")
1344+
1345+
# see what happens if for other values
1346+
value = d._value
1347+
d._value = "1" # integer
1348+
self.assertEqual(d.pretty("%2d"), "1")
1349+
d._value = "I'mNotAFloat" # not a number
1350+
self.assertEqual(d.pretty("%2d"), "I'mNotAFloat")
1351+
d._value = value
1352+
1353+
# check permissions return
1354+
is_view_ok_orig = d.is_view_ok
1355+
is_edit_ok_orig = d.is_edit_ok
1356+
no_access = lambda : False
1357+
1358+
d.is_view_ok = no_access
1359+
self.assertEqual(d.plain(), "[hidden]")
1360+
1361+
d.is_edit_ok = no_access
1362+
self.assertEqual(d.field(), "[hidden]")
1363+
1364+
d.is_view_ok = is_view_ok_orig
1365+
self.assertEqual(d.field(), "3.14")
1366+
d.is_edit_ok = is_edit_ok_orig
1367+
12561368
class ZUtilsTestcase(TemplatingTestCase):
12571369

12581370
def test_Iterator(self):

0 commit comments

Comments
 (0)