Browse Source

[MIG] web_widget_float_formula: v9 with i18n

* Update JS to use v9 module system
* Fix non-functioning logic for obtaining the value of an input element
* Eliminate redundant calls (e.g. there were multiple calls to eval)
* Modify formula cleanup to use localized decimal point and thousands separator
 characters
* Add JS unit tests
pull/425/head
Oleg Bulkin 8 years ago
parent
commit
d02ce7f647
  1. 75
      web_widget_float_formula/README.rst
  2. 4
      web_widget_float_formula/__init__.py
  3. 23
      web_widget_float_formula/__openerp__.py
  4. 132
      web_widget_float_formula/static/src/js/models.js
  5. 103
      web_widget_float_formula/static/src/js/web_widget_float_formula.js
  6. 161
      web_widget_float_formula/static/tests/js/test_web_widget_float_formula.js
  7. 5
      web_widget_float_formula/tests/__init__.py
  8. 15
      web_widget_float_formula/tests/test_js.py
  9. 9
      web_widget_float_formula/views/qweb.xml
  10. 21
      web_widget_float_formula/views/web_widget_float_formula.xml

75
web_widget_float_formula/README.rst

@ -1,36 +1,61 @@
Allow to write simple mathematic formulas in Integer / Float fields
===================================================================
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
* Possibility to tip a text like "=45 + 4/3 - 5 * (2 +1)";
* if the formula is correct, The result will be computed and displayed;
* if the formula is not correct, the initial text is displayed;
========================
Formulas in Float Fields
========================
Technical informations
----------------------
This module allows the use of simple math formulas in integer/float fields
(e.g. "=45 + 4/3 - 5 * (2 + 1)").
* Overloads "instance.web.form.FieldFloat"; (so works for fields.integer &
fields.float);
* To compute, the module simply use the eval() javascript function;
* Rounding computation is not done by this module (The module has the same
behaviour if the user tips "=1/3" or if he tips "0.33[...]");
* avoid code injonction by regexpr test: "=alert('security')" is not valid;
* Only supports parentheses, decimal points, thousands separators, and the
operators "+", "-", "*", and "/"
* Will use the decimal point and thousands separator characters associated
with your language
* If the formula is valid, the result will be computed and displayed, and the
formula will be stored for editing
* If the formula is not valid, it's retained in the field as text
**Technical Details**
* Overloads web.form_widgets.FieldFloat (so it works for fields.integer &
fields.float)
* Uses the eval() JS function to evaluate the formula
* Does not do any rounding (this is handled elsewhere)
* Avoids code injection by applying strict regex to formula prior to eval()
(e.g. "=alert('security')" would not get evaluated)
Installation
============
To install this module, simply follow the standard install process.
Configuration
=============
No configuration is needed or possible.
Usage Usage
===== =====
See demo here Video: http://www.youtube.com/watch?v=jQGdD34WYrA&hd=1
Install and enjoy. A short demo video can be found at
http://www.youtube.com/watch?v=jQGdD34WYrA.
Roadmap / Limit
===============
* Only supports the four operators: "+" "-" "*" "/" and parenthesis;
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/162/9.0
Known Issues / Roadmap
======================
Bug Tracker Bug Tracker
=========== ===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_. Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
In case of trouble, please check there if your issue has already been reported. In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/web/issues/new?body=module:%20web_widget_float_formula%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
If you spotted it first, help us smash it by providing detailed and welcomed
feedback.
Credits Credits
======= =======
@ -39,17 +64,19 @@ Contributors
------------ ------------
* Sylvain Le Gal (https://twitter.com/legalsylvain) * Sylvain Le Gal (https://twitter.com/legalsylvain)
* Oleg Bulkin <o.bulkin@gmail.com>
Maintainer Maintainer
---------- ----------
.. image:: http://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: http://odoo-community.org
.. image:: http://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: http://odoo-community.org
This module is maintained by the OCA. This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit http://odoo-community.org. To contribute to this module, please visit http://odoo-community.org.

4
web_widget_float_formula/__init__.py

@ -1,4 +0,0 @@
# -*- encoding: utf-8 -*-
###############################################################################
# See __openerp__.py file for Copyright and Licence Informations.
###############################################################################

23
web_widget_float_formula/__openerp__.py

@ -1,19 +1,22 @@
# -*- encoding: utf-8 -*-
###############################################################################
# See Copyright and Licence Informations undermentioned.
###############################################################################
# -*- coding: utf-8 -*-
# Copyright GRAP
# Copyright 2016 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{ {
'name': 'Web Widget - Formulas in Float fields',
'version': '8.0.1.0.0',
'category': 'web',
'author': 'GRAP,Odoo Community Association (OCA)',
'name': 'Web Widget - Formulas in Float Fields',
'summary': 'Allow use of simple formulas in float fields',
'version': '9.0.1.0.0',
'category': 'Web',
'author': 'GRAP, LasLabs, Odoo Community Association (OCA)',
'website': 'http://www.grap.coop', 'website': 'http://www.grap.coop',
'license': 'AGPL-3', 'license': 'AGPL-3',
'depends': [ 'depends': [
'web', 'web',
], ],
'data': [ 'data': [
'views/qweb.xml',
'views/web_widget_float_formula.xml',
], ],
'installable': False,
'installable': True,
'application': False,
} }

132
web_widget_float_formula/static/src/js/models.js

@ -1,132 +0,0 @@
/*******************************************************************************
See __openerp__.py file for Copyright and Licence Informations.
*******************************************************************************/
openerp.web_widget_float_formula = function (instance) {
instance.web.FormView = instance.web.FormView.extend({
/***********************************************************************
Overload section
***********************************************************************/
/**
* Overload : '_process_save' function
1: to force computation of formula if the user realize a keydown directly after the formula input in a tree view ;
2: to clean up the '_formula_text' value in all case to avoid bugs in tree view ;
*/
_process_save: function(save_obj) {
for (var f in this.fields) {
if (!this.fields.hasOwnProperty(f)) { continue; }
f = this.fields[f];
if (f.hasOwnProperty('_formula_text')){
currentval = f.$('input').attr('value')
if (typeof currentval != 'undefined'){
formula = f._get_valid_expression(currentval);
if (formula){
f._compute_result();
}
}
f._clean_formula_text();
}
}
return this._super(save_obj);
},
});
instance.web.form.FieldFloat = instance.web.form.FieldFloat.extend({
/***********************************************************************
Overload section
***********************************************************************/
/**
* Overload : 'start' function to catch 'blur' and 'focus' events.
*/
start: function() {
this.on("blurred", this, this._compute_result);
this.on("focused", this, this._display_formula);
return this._super();
},
/**
* Overload : 'initialize_content' function to clean '_formula_text' value.
*/
initialize_content: function() {
this._clean_formula_text();
return this._super();
},
/***********************************************************************
Custom section
***********************************************************************/
/**
* keep in memory the formula to allow user to edit it again.
The formula has to be keeped in memory until a 'save' action.
*/
_formula_text: '',
/**
* Clean '_formula_text' value.
*/
_clean_formula_text: function() {
this._formula_text = '';
},
/**
* Return a valid formula from a val, if possible.
Otherwise, return false.
*/
_get_valid_expression: function(val) {
// Trim the value
currenttxt = val.toString().replace(/^\s+|\s+$/g, '');
// Test if the value is a formula
if (currenttxt[0] == '=') {
// allowed chars : [0-9] .,+-/*() and spaces
myreg = RegExp('[0-9]|\\s|\\.|,|\\(|\\)|\\+|\\-|\\*|\\/','g')
// Test to avoid code injonction in eval function.
if (currenttxt.substring(1).replace(myreg, '') == ''){
try {
// Try to compute
formula = currenttxt.substring(1).replace(/,/g,'.');
var floatval = eval(formula);
}catch (e) {}
if (typeof (floatval) != 'undefined'){
return formula;
}
}
}
return false;
},
/**
* test if the content of the field is a valid formula,
* compute the result, and replace the current value by the final result.
*/
_compute_result: function() {
var formula
// Erase old formula
this._formula_text = '';
formula = this._get_valid_expression(this.$el.find('input').attr('value'));
if (formula){
// Store new formula
this._formula_text = "=" + formula;
// put the result in the field
this.set_value(eval(formula));
// Force rendering anyway to avoid format loss if no change
this.render_value();
}
},
/**
* Display the stored formula in the field, to allow modification.
*/
_display_formula: function() {
if (this._formula_text != ''){
this.$el.find('input').val(this._formula_text);
}
},
});
};

103
web_widget_float_formula/static/src/js/web_widget_float_formula.js

@ -0,0 +1,103 @@
/**
* Copyright GRAP
* Copyright 2016 LasLabs Inc.
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
**/
odoo.define('web_widget_float_formula', function(require) {
"use strict";
var form_view = require('web.FormView');
form_view.include({
// Ensure that formula is computed even if user saves right away and
// clean up '_formula_text' value to avoid bugs in tree view
_process_save: function(save_obj) {
for (var f in this.fields) {
if (!this.fields.hasOwnProperty(f)) { continue; }
f = this.fields[f];
if (f.hasOwnProperty('_formula_text')) {
f._compute_result();
f._clean_formula_text();
}
}
return this._super(save_obj);
},
});
var core = require('web.core');
core.bus.on('web_client_ready', null, function () {
// Import localization values used to eval formula
var translation_params = core._t.database.parameters;
var decimal_point = translation_params.decimal_point;
var thousands_sep = translation_params.thousands_sep;
var field_float = require('web.form_widgets').FieldFloat;
field_float.include({
start: function() {
this.on('blurred', this, this._compute_result);
this.on('focused', this, this._display_formula);
return this._super();
},
initialize_content: function() {
this._clean_formula_text();
return this._super();
},
_formula_text: '',
_clean_formula_text: function() {
this._formula_text = '';
},
_process_formula: function(formula) {
var clean_formula = formula.toString().replace(/^\s+|\s+$/g, '');
if (clean_formula[0] == '=') {
clean_formula = clean_formula.substring(1);
var myreg = RegExp('[0-9]|\\s|\\.|,|\\(|\\)|\\+|\\-|\\*|\\/','g');
if (clean_formula.replace(myreg, '') === '') {
return clean_formula;
}
}
return false;
},
_eval_formula: function(formula) {
var value;
formula = formula.replace(thousands_sep, '').replace(decimal_point, '.');
try {
value = eval(formula);
}
catch(e) {}
if (typeof value != 'undefined') {
return value;
}
return false;
},
_compute_result: function() {
this._clean_formula_text();
var formula = this._process_formula(this.$el.find('input').val());
if (formula !== false) {
var value = this._eval_formula(formula);
if (value !== false) {
this._formula_text = "=" + formula;
this.set_value(value);
// Force rendering to avoid format loss if there's no change
this.render_value();
}
}
},
// Display the formula stored in the field to allow modification
_display_formula: function() {
if (this._formula_text !== '') {
this.$el.find('input').val(this._formula_text);
}
},
});
});
});

161
web_widget_float_formula/static/tests/js/test_web_widget_float_formula.js

@ -0,0 +1,161 @@
/**
* Copyright 2016 LasLabs Inc.
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
**/
odoo.define_section('web_widget_float_formula', ['web.form_common', 'web.form_widgets', 'web.core'], function(test) {
'use strict';
window.test_setup = function(self, form_common, form_widgets, core) {
core.bus.trigger('web_client_ready');
var field_manager = new form_common.DefaultFieldManager(null, {});
var filler = {'attrs': {}}; // Needed to instantiate FieldFloat
self.field = new form_widgets.FieldFloat(field_manager, filler);
self.$element = $('<input>');
self.field.$el.append(self.$element);
};
test('Float fields should have a _formula_text property that defaults to an empty string',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
assert.strictEqual(this.field._formula_text, '');
});
test('.initialize_content() on float fields should clear the _formula_text property',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
this.field._formula_text = 'test';
this.field.initialize_content();
assert.strictEqual(this.field._formula_text, '');
});
test('._clean_formula_text() on float fields should clear the _formula_text property',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
this.field._formula_text = 'test';
this.field._clean_formula_text();
assert.strictEqual(this.field._formula_text, '');
});
test('._process_formula() on float fields should return false when given invalid formulas',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
assert.strictEqual(this.field._process_formula('2*3'), false);
assert.strictEqual(this.field._process_formula('=2*3a'), false);
});
test('._process_formula() on float fields should properly process a valid formula',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
assert.strictEqual(this.field._process_formula(' =2*3\n'), '2*3');
});
test('._eval_formula() on float fields should properly evaluate a valid formula',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
assert.equal(this.field._eval_formula('2*3'), 6);
});
test('._eval_formula() on float fields should properly handle alternative decimal points and thousands seps',
function(assert, form_common, form_widgets, core) {
var translation_params = core._t.database.parameters;
translation_params.decimal_point = ',';
translation_params.thousands_sep = '.';
window.test_setup(this, form_common, form_widgets, core);
assert.equal(this.field._eval_formula('2.000*3,5'), 7000);
});
test('._eval_formula() on float fields should return false when given an input that evals to undefined',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
assert.equal(this.field._eval_formula(''), false);
});
test('._eval_formula() on float fields should return false when given an input that cannot be evaluated',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
assert.equal(this.field._eval_formula('*/'), false);
});
test('._compute_result() on float fields should always clean up _formula_text',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
this.field._formula_text = 'test';
this.field._compute_result();
assert.strictEqual(this.field._formula_text, '');
});
test('._compute_result() should not change the value of the associated input when it is not a valid formula',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
this.$element.val('=2*3a');
this.field._compute_result();
assert.strictEqual(this.$element.val(), '=2*3a');
});
test('._compute_result() should not change the value of the associated input when it cannot be evaled',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
this.$element.val('=*/');
this.field._compute_result();
assert.strictEqual(this.$element.val(), '=*/');
});
test('._compute_result() should behave properly when the current value of the input element is a valid formula',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
this.$element.val('=2*3');
this.field._compute_result();
assert.equal(this.$element.val(), '6');
assert.strictEqual(this.field._formula_text, '=2*3');
});
test('._display_formula() should update the value of the input element when there is a stored formula',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
this.field._formula_text = "test";
this.field._display_formula();
assert.equal(this.$element.val(), 'test');
});
test('.start() on float fields should add a handler that calls ._compute_result() when the field is blurred',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
this.field.called = false;
this.field._compute_result = function() {
this.called = true;
};
this.field.start();
this.field.trigger('blurred');
assert.strictEqual(this.field.called, true);
});
test('.start() on float fields should add a handler that calls ._display_formula() when the field is focused',
function(assert, form_common, form_widgets, core) {
window.test_setup(this, form_common, form_widgets, core);
this.field.called = false;
this.field._display_formula = function() {
this.called = true;
};
this.field.start();
this.field.trigger('focused');
assert.strictEqual(this.field.called, true);
});
});

5
web_widget_float_formula/tests/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_js

15
web_widget_float_formula/tests/test_js.py

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from openerp.tests import HttpCase
class TestJS(HttpCase):
def test_js(self):
self.phantom_js(
"/web/tests?module=web_widget_float_formula",
"",
login="admin",
)

9
web_widget_float_formula/views/qweb.xml

@ -1,9 +0,0 @@
<openerp>
<data>
<template id="assets_backend" name="web_widget_float_formula assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/web_widget_float_formula/static/src/js/models.js"></script>
</xpath>
</template>
</data>
</openerp>

21
web_widget_float_formula/views/web_widget_float_formula.xml

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright GRAP
Copyright 2016 LasLabs Inc.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo>
<template id="assets_backend" name="web_widget_float_formula Assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/web_widget_float_formula/static/src/js/web_widget_float_formula.js"/>
</xpath>
</template>
<template id="qunit_suite" name="web_widget_float_formula Test Assets" inherit_id="web.qunit_suite">
<xpath expr="//html/head" position="inside">
<script type="text/javascript" src="/web_widget_float_formula/static/tests/js/test_web_widget_float_formula.js"/>
</xpath>
</template>
</odoo>
Loading…
Cancel
Save