Quantity formula assignment and validation
This commit is contained in:
parent
fd24bced60
commit
fce6ee57b0
@ -60,10 +60,11 @@ class QuantitiesController < ApplicationController
|
|||||||
|
|
||||||
def quantity_params
|
def quantity_params
|
||||||
params.require(:quantity).permit(
|
params.require(:quantity).permit(
|
||||||
:name,
|
|
||||||
:description,
|
|
||||||
:domain,
|
:domain,
|
||||||
:parent_id,
|
:parent_id,
|
||||||
|
:name,
|
||||||
|
:description,
|
||||||
|
:formula,
|
||||||
:primary
|
:primary
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
class Quantity < ActiveRecord::Base
|
class Quantity < ActiveRecord::Base
|
||||||
|
require 'ripper'
|
||||||
|
|
||||||
enum domain: {
|
enum domain: {
|
||||||
diet: 0,
|
diet: 0,
|
||||||
measurement: 1,
|
measurement: 1,
|
||||||
@ -13,6 +15,41 @@ class Quantity < ActiveRecord::Base
|
|||||||
validate if: -> { parent.present? } do
|
validate if: -> { parent.present? } do
|
||||||
errors.add(:parent, :parent_domain_mismatch) unless domain == parent.domain
|
errors.add(:parent, :parent_domain_mismatch) unless domain == parent.domain
|
||||||
end
|
end
|
||||||
|
validate if: -> { formula.present? } do
|
||||||
|
# 1st: check if formula is valid Ruby code
|
||||||
|
tokenized_length = Ripper.tokenize(formula).join.length
|
||||||
|
unless tokenized_length == formula.length
|
||||||
|
errors.add(:formula, :invalid_formula, {part: formula[0...tokenized_length]})
|
||||||
|
end
|
||||||
|
|
||||||
|
# 2nd: check if formula contains only allowed token types
|
||||||
|
identifiers = []
|
||||||
|
Ripper.lex(formula).each do |location, ttype, token|
|
||||||
|
case
|
||||||
|
when [:on_ident, :on_tstring_content, :on_const].include?(ttype)
|
||||||
|
identifiers << token
|
||||||
|
when [:on_sp, :on_int, :on_rational, :on_float, :on_tstring_beg, :on_tstring_end,
|
||||||
|
:on_lparen, :on_rparen].include?(ttype)
|
||||||
|
when :on_op == ttype && '+-*/'.include?(token)
|
||||||
|
else
|
||||||
|
errors.add(:formula, :disallowed_token,
|
||||||
|
{token: token, ttype: ttype, location: location})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# 3rd: check for disallowed function calls (they are not detected by Ripper.lex)
|
||||||
|
# FIXME: this is unreliable (?) detection of function calls, should be replaced
|
||||||
|
# with parsing Ripper.sexp if necessary
|
||||||
|
function = Ripper.slice(formula, 'ident [sp]* lparen')
|
||||||
|
errors.add(:formula, :disallowed_function_call, {function: function}) if function
|
||||||
|
|
||||||
|
# 4th: check if identifiers used in formula correspond to existing quantities
|
||||||
|
identifiers.uniq!
|
||||||
|
quantities = self.project.quantities.where(name: identifiers).pluck(:name)
|
||||||
|
if quantities.length != identifiers.length
|
||||||
|
errors.add(:formula, :unknown_quantity, {quantities: identifiers - quantities})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
after_initialize do
|
after_initialize do
|
||||||
if new_record?
|
if new_record?
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<p><%= f.text_field :name, size: 25, required: true %></p>
|
<p><%= f.text_field :name, size: 25, required: true %></p>
|
||||||
<p><%= f.text_field :description, size: 200 %></p>
|
<p><%= f.text_field :description, size: 200 %></p>
|
||||||
|
<p><%= f.text_field :formula, size: 200, placeholder: t('.formula_placeholder') %></p>
|
||||||
<p><%= f.check_box :primary %></p>
|
<p><%= f.check_box :primary %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ en:
|
|||||||
field_nutrients: 'Nutrients:'
|
field_nutrients: 'Nutrients:'
|
||||||
field_domain: 'Domain'
|
field_domain: 'Domain'
|
||||||
field_parent_quantity: 'Parent'
|
field_parent_quantity: 'Parent'
|
||||||
|
field_formula: 'Formula'
|
||||||
field_primary: 'Primary'
|
field_primary: 'Primary'
|
||||||
field_shortname: 'Short name'
|
field_shortname: 'Short name'
|
||||||
button_primary: 'Primary'
|
button_primary: 'Primary'
|
||||||
@ -24,6 +25,12 @@ en:
|
|||||||
attributes:
|
attributes:
|
||||||
parent:
|
parent:
|
||||||
parent_domain_mismatch: 'parent quantity has to be in the same domain'
|
parent_domain_mismatch: 'parent quantity has to be in the same domain'
|
||||||
|
formula:
|
||||||
|
invalid_formula: 'interpretation failed after: %{part}'
|
||||||
|
disallowed_token: 'includes disallowed token "%{token}"
|
||||||
|
(of type %{ttype}) at %{location}'
|
||||||
|
disallowed_function_call: 'includes disallowed function call %{function}'
|
||||||
|
unknown_quantity: 'contains undefined quantities: %{quantities}'
|
||||||
body_trackers:
|
body_trackers:
|
||||||
index:
|
index:
|
||||||
heading: 'Summary'
|
heading: 'Summary'
|
||||||
@ -82,6 +89,8 @@ en:
|
|||||||
measurement: 'measurement'
|
measurement: 'measurement'
|
||||||
exercise: 'exercise'
|
exercise: 'exercise'
|
||||||
null_parent: '- none -'
|
null_parent: '- none -'
|
||||||
|
formula_placeholder: 'provide if value of quantity has to be computed in terms of
|
||||||
|
other quantities'
|
||||||
units:
|
units:
|
||||||
index:
|
index:
|
||||||
heading: 'Units'
|
heading: 'Units'
|
||||||
|
Reference in New Issue
Block a user