amee-data-abstraction 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +1 -0
- data/CHANGELOG.txt +4 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +41 -0
- data/LICENSE.txt +27 -0
- data/README.txt +188 -0
- data/Rakefile +102 -0
- data/VERSION +1 -0
- data/amee-data-abstraction.gemspec +115 -0
- data/examples/_calculator_form.erb +27 -0
- data/examples/calculation_controller.rb +16 -0
- data/init.rb +4 -0
- data/lib/amee-data-abstraction.rb +30 -0
- data/lib/amee-data-abstraction/calculation.rb +236 -0
- data/lib/amee-data-abstraction/calculation_set.rb +101 -0
- data/lib/amee-data-abstraction/drill.rb +63 -0
- data/lib/amee-data-abstraction/exceptions.rb +47 -0
- data/lib/amee-data-abstraction/input.rb +197 -0
- data/lib/amee-data-abstraction/metadatum.rb +58 -0
- data/lib/amee-data-abstraction/ongoing_calculation.rb +545 -0
- data/lib/amee-data-abstraction/output.rb +16 -0
- data/lib/amee-data-abstraction/profile.rb +108 -0
- data/lib/amee-data-abstraction/prototype_calculation.rb +350 -0
- data/lib/amee-data-abstraction/term.rb +506 -0
- data/lib/amee-data-abstraction/terms_list.rb +150 -0
- data/lib/amee-data-abstraction/usage.rb +90 -0
- data/lib/config/amee_units.rb +129 -0
- data/lib/core-extensions/class.rb +27 -0
- data/lib/core-extensions/hash.rb +43 -0
- data/lib/core-extensions/ordered_hash.rb +21 -0
- data/lib/core-extensions/proc.rb +15 -0
- data/rails/init.rb +32 -0
- data/spec/amee-data-abstraction/calculation_set_spec.rb +54 -0
- data/spec/amee-data-abstraction/calculation_spec.rb +75 -0
- data/spec/amee-data-abstraction/drill_spec.rb +38 -0
- data/spec/amee-data-abstraction/input_spec.rb +77 -0
- data/spec/amee-data-abstraction/metadatum_spec.rb +17 -0
- data/spec/amee-data-abstraction/ongoing_calculation_spec.rb +494 -0
- data/spec/amee-data-abstraction/profile_spec.rb +39 -0
- data/spec/amee-data-abstraction/prototype_calculation_spec.rb +256 -0
- data/spec/amee-data-abstraction/term_spec.rb +385 -0
- data/spec/amee-data-abstraction/terms_list_spec.rb +53 -0
- data/spec/config/amee_units_spec.rb +71 -0
- data/spec/core-extensions/class_spec.rb +25 -0
- data/spec/core-extensions/hash_spec.rb +44 -0
- data/spec/core-extensions/ordered_hash_spec.rb +12 -0
- data/spec/core-extensions/proc_spec.rb +12 -0
- data/spec/fixtures/electricity.rb +35 -0
- data/spec/fixtures/electricity_and_transport.rb +55 -0
- data/spec/fixtures/transport.rb +26 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +244 -0
- metadata +262 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
# Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
|
2
|
+
# Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
|
3
|
+
|
4
|
+
# :title: Module: AMEE::DataAbstraction::Exceptions
|
5
|
+
|
6
|
+
module AMEE
|
7
|
+
module DataAbstraction
|
8
|
+
module Exceptions
|
9
|
+
# Throw this exception when there is a general syntax error in a DSL block.
|
10
|
+
class DSL < Exception; end
|
11
|
+
|
12
|
+
# Throw this exception when user specifies a suggested UI for a term which
|
13
|
+
# is not supported.
|
14
|
+
class InvalidInterface < Exception ; end
|
15
|
+
|
16
|
+
# Throw this exception when user tries to set a value for a term with a
|
17
|
+
#read-only value.
|
18
|
+
class FixedValueInterference < Exception; end
|
19
|
+
|
20
|
+
# Throw this exception when someone tries to access a term which is not
|
21
|
+
# defined for a calculation.
|
22
|
+
class NoSuchTerm < Exception; end
|
23
|
+
|
24
|
+
# Throw this exception when trying to create a PI for a calculation which
|
25
|
+
# already has a corresponding PI.
|
26
|
+
class AlreadyHaveProfileItem < Exception; end
|
27
|
+
|
28
|
+
# Throw this exception when a locally stored calculation and the information
|
29
|
+
# on the AMEE server have got out of sync.
|
30
|
+
class Syncronization < Exception; end
|
31
|
+
|
32
|
+
# Throw this exception if something went wrong making a profile item.
|
33
|
+
class DidNotCreateProfileItem < Exception; end
|
34
|
+
|
35
|
+
# Throw this exception when someone tries to specify a "usage" twice when
|
36
|
+
# defining a PrototypeCalculation using the DSL.
|
37
|
+
class TwoUsages < Exception; end
|
38
|
+
|
39
|
+
# Throw this exception when an invalid value is set for a term
|
40
|
+
class ChoiceValidation < Exception; end
|
41
|
+
|
42
|
+
# Throw this exception when inappropriate units are set for a term.
|
43
|
+
class InvalidUnits < Exception; end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
|
2
|
+
# Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
|
3
|
+
|
4
|
+
# :title: Class: AMEE::DataAbstraction::Input
|
5
|
+
|
6
|
+
module AMEE
|
7
|
+
module DataAbstraction
|
8
|
+
|
9
|
+
# Subclass of <tt>Term</tt> providing methods and attributes appropriate for
|
10
|
+
# representing calculation inputs specifically
|
11
|
+
#
|
12
|
+
class Input < Term
|
13
|
+
|
14
|
+
# Returns the valid choices for this input
|
15
|
+
# (Abstract, implemented only for subclasses of input.)
|
16
|
+
def choices
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns an ppropriate data structure for a rails select list form helper.
|
21
|
+
def options_for_select
|
22
|
+
[[nil,nil]]+choices.map{|x|[x.underscore.humanize,x] unless x.nil? }.compact
|
23
|
+
end
|
24
|
+
|
25
|
+
# Initialization of <i>Input</i> objects follows that of the parent
|
26
|
+
# <i>Term</i> class.
|
27
|
+
#
|
28
|
+
def initialize(options={},&block)
|
29
|
+
@validation = nil
|
30
|
+
validation_message {"#{name} is invalid."}
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
# Configures the value of <tt>self</tt> to be fixed to <tt>val</tt>, i.e.
|
35
|
+
# the value is read-only.
|
36
|
+
#
|
37
|
+
def fixed val
|
38
|
+
value(val)
|
39
|
+
@fixed=true
|
40
|
+
@optional=false
|
41
|
+
end
|
42
|
+
|
43
|
+
# Block to define custom complaint message for an invalid value.
|
44
|
+
def validation_message(&block)
|
45
|
+
@validation_block=block
|
46
|
+
end
|
47
|
+
|
48
|
+
# Set a default validation message appropriate for input terms which have
|
49
|
+
# a list of choices.
|
50
|
+
def choice_validation_message
|
51
|
+
validation_message {"#{name} is invalid because #{value} is not one of #{choices.join(', ')}."}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Represents the value of <tt>self</tt>. Set a value by passing an argument.
|
55
|
+
# Retrieve a value by calling without an argument, e.g.,
|
56
|
+
#
|
57
|
+
# my_term.value 12345
|
58
|
+
#
|
59
|
+
# my_term.value #=> 12345
|
60
|
+
#
|
61
|
+
# If <tt>self</tt> is configured to have a fixed value and a value is passed
|
62
|
+
# which does not correspond to the fixed value, a <i>FixedValueInterference</i>
|
63
|
+
# exception is raised.
|
64
|
+
#
|
65
|
+
def value(*args)
|
66
|
+
unless args.empty?
|
67
|
+
if args.first.to_s != @value.to_s
|
68
|
+
raise Exceptions::FixedValueInterference if fixed?
|
69
|
+
parent.dirty! if parent and parent.is_a? OngoingCalculation
|
70
|
+
end
|
71
|
+
end
|
72
|
+
super
|
73
|
+
end
|
74
|
+
|
75
|
+
# Represents a custom object, symbol or pattern (to be called via ===) to
|
76
|
+
# determine the whether value set for <tt>self</tt> should be considered
|
77
|
+
# acceptable. The following symbols are acceptable :numeric, :date or
|
78
|
+
# :datetime If validation is specified using a <i>Proc</i> object, the term
|
79
|
+
# value should be initialized as the block variable. E.g.,
|
80
|
+
#
|
81
|
+
# my_input.validation 20
|
82
|
+
#
|
83
|
+
# my_input.valid? #=> true
|
84
|
+
#
|
85
|
+
# my_input.value 'some string'
|
86
|
+
# my_input.valid? #=> false
|
87
|
+
#
|
88
|
+
# my_input.value 21
|
89
|
+
# my_input.valid? #=> false
|
90
|
+
#
|
91
|
+
# my_input.value 20
|
92
|
+
# my_input.valid? #=> true
|
93
|
+
#
|
94
|
+
# ---
|
95
|
+
#
|
96
|
+
# my_input.validation lambda{ |value| value.is_a? Numeric }
|
97
|
+
#
|
98
|
+
# my_input.valid? #=> true
|
99
|
+
#
|
100
|
+
# my_input.value 'some string'
|
101
|
+
# my_input.valid? #=> false
|
102
|
+
#
|
103
|
+
# my_input.value 12345
|
104
|
+
# my_input.valid? #=> true
|
105
|
+
#
|
106
|
+
# ---
|
107
|
+
#
|
108
|
+
# my_input.validation :numeric
|
109
|
+
#
|
110
|
+
# my_input.valid? #=> false
|
111
|
+
#
|
112
|
+
# my_input.value 21
|
113
|
+
# my_input.valid? #=> true
|
114
|
+
#
|
115
|
+
# my_input.value "20"
|
116
|
+
# my_input.valid? #=> true
|
117
|
+
#
|
118
|
+
# my_input.value "e"
|
119
|
+
# my_input.valid? #=> false
|
120
|
+
def validation(*args)
|
121
|
+
unless args.empty?
|
122
|
+
if args.first.is_a?(Symbol)
|
123
|
+
@validation = case args.first
|
124
|
+
when :numeric then lambda{|v| v.is_a?(Fixnum) || v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(BigDecimal) || (v.is_a?(String) && v.match(/^[\d\.]+$/))}
|
125
|
+
when :date then lambda{|v| v.is_a?(Date) || v.is_a?(DateTime) || Date.parse(v) rescue nil}
|
126
|
+
when :datetime then lambda{|v| v.is_a?(Time) || v.is_a?(DateTime) || DateTime.parse(v) rescue nil}
|
127
|
+
end
|
128
|
+
else
|
129
|
+
@validation = args.first
|
130
|
+
end
|
131
|
+
end
|
132
|
+
@validation
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns true if <tt>self</tt> is configured to contain a fixed (read-only)
|
136
|
+
# value
|
137
|
+
#
|
138
|
+
def fixed?
|
139
|
+
@fixed
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns <tt>true</tt> if the value of <tt>self</tt> does not need to be
|
143
|
+
# specified for the parent calculation to calculate a result. Otherwise,
|
144
|
+
# returns <tt>false</tt>
|
145
|
+
#
|
146
|
+
def optional?(usage=nil)
|
147
|
+
@optional
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns <tt>true</tt> if the value of <tt>self</tt> is required in order
|
151
|
+
# for the parent calculation to calculate a result. Otherwise, returns
|
152
|
+
# <tt>false</tt>
|
153
|
+
#
|
154
|
+
def compulsory?(usage=nil)
|
155
|
+
!optional?(usage)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Check that the value of <tt>self</tt> is valid. If invalid, and is defined
|
159
|
+
# as part of a calculation, add the invalidity message to the parent
|
160
|
+
# calculation's error list. Otherwise, raise a <i>ChoiceValidation</i>
|
161
|
+
# exception.
|
162
|
+
#
|
163
|
+
def validate!
|
164
|
+
# Typically, you just wipe yourself if supplied value not valid, but
|
165
|
+
# deriving classes might want to raise an exception
|
166
|
+
#
|
167
|
+
invalid unless fixed? || valid?
|
168
|
+
end
|
169
|
+
|
170
|
+
# Declare the calculation invalid, reporting to the parent calculation or
|
171
|
+
# raising an exception, as appropriate.
|
172
|
+
#
|
173
|
+
def invalid
|
174
|
+
if parent
|
175
|
+
parent.invalid(label,instance_eval(&@validation_block))
|
176
|
+
else
|
177
|
+
raise AMEE::DataAbstraction::Exceptions::ChoiceValidation.new(instance_eval(&@validation_block))
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns <tt>true</tt> if the UI element of <tt>self</tt> is disabled.
|
182
|
+
# Otherwise, returns <tt>false</tt>.
|
183
|
+
#
|
184
|
+
def disabled?
|
185
|
+
super || fixed?
|
186
|
+
end
|
187
|
+
|
188
|
+
protected
|
189
|
+
# Returns <tt>true</tt> if the value set for <tt>self</tt> is either blank
|
190
|
+
# or passes custom validation criteria. Otherwise, returns <tt>false</tt>.
|
191
|
+
#
|
192
|
+
def valid?
|
193
|
+
validation.blank? || validation === @value_before_cast
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
|
2
|
+
# Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
|
3
|
+
|
4
|
+
# :title: Class: AMEE::DataAbstraction::Metadatum
|
5
|
+
|
6
|
+
module AMEE
|
7
|
+
module DataAbstraction
|
8
|
+
|
9
|
+
# Subclass of <tt>Input</tt> providing methods and attributes appropriate for
|
10
|
+
# representing arbitrary metadata which does not correspond to any AMEE profile
|
11
|
+
# item value or drill.
|
12
|
+
#
|
13
|
+
class Metadatum < Input
|
14
|
+
|
15
|
+
# Initialization of <i>Metadatum</i> objects follows that of the parent
|
16
|
+
# <i>Input</i> class. The <tt>interface</tt> attribute of <tt>self</tt> is
|
17
|
+
# set to <tt>:drop_down</tt> by default, but can be manually configured if
|
18
|
+
# required.
|
19
|
+
#
|
20
|
+
def initialize(options={},&block)
|
21
|
+
super
|
22
|
+
interface :drop_down unless interface
|
23
|
+
end
|
24
|
+
|
25
|
+
# Represents a list of acceptable choices for the value of <tt>self</tt>.
|
26
|
+
# Set the list of choices by passing an argument. Retrieve the choices by
|
27
|
+
# calling without an argument, e.g.,
|
28
|
+
#
|
29
|
+
# my_metadatum.choices 'London', 'New York', 'Tokyo'
|
30
|
+
#
|
31
|
+
# my_metadatum.choices #=> [ 'London',
|
32
|
+
# 'New York',
|
33
|
+
# 'Tokyo' ]
|
34
|
+
#
|
35
|
+
# A single value of <tt>nil</tt> represents an unrestricted value
|
36
|
+
# .
|
37
|
+
attr_property :choices
|
38
|
+
|
39
|
+
# Returns <tt>false</tt> as all metadatum are arbitrarily defined and
|
40
|
+
# therefore not directly involved in any AMEE calculation.
|
41
|
+
#
|
42
|
+
def compulsory?(usage=nil)
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns <tt>true</tt> if the value set for <tt>self</tt> is valid. If
|
47
|
+
# <tt>self</tt> contains neither a custom validation pattern nor any
|
48
|
+
# defined choices, <tt>true</tt> is returned. Otherwise, validity depends
|
49
|
+
# on the custom validation being successful (if present) and the the value
|
50
|
+
# of <tt>self</tt> matching one of the entries in the <tt>choices</tt>
|
51
|
+
# attribute (if defined). Otherwise, returns <tt>false</tt>.
|
52
|
+
#
|
53
|
+
def valid?
|
54
|
+
super && (choices.blank? || choices.include?(value))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,545 @@
|
|
1
|
+
# Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
|
2
|
+
# Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
|
3
|
+
|
4
|
+
# :title: Class: AMEE::DataAbstraction::OngoingCalculation
|
5
|
+
|
6
|
+
module AMEE
|
7
|
+
module DataAbstraction
|
8
|
+
|
9
|
+
# Instances of the <i>OngoingCalculation</i> class represent actual
|
10
|
+
# calculations made via the AMEE platform.
|
11
|
+
#
|
12
|
+
# The class inherits from the <i>Calculation</i> class and is therefore
|
13
|
+
# primarly characterised by the <tt>label</tt>, <tt>name</tt>, and <tt>path</tt>
|
14
|
+
# attributes, as well as an associated instance of the <tt>TermsList</tt>
|
15
|
+
# class which represents each of the values (input, outputs, metdata) involved
|
16
|
+
# in the calculation.
|
17
|
+
#
|
18
|
+
# Instances of <i>OngoingCalcualtion</i> are typically instantiated from an
|
19
|
+
# instance of <i>PrototypeCalculation</i> using the <tt>#begin_calculation</tt>
|
20
|
+
# method, e.g.
|
21
|
+
#
|
22
|
+
# my_prototype.begin_calculation #=> <AMEE::DataAbstraction::OngoingCalculation ...>
|
23
|
+
#
|
24
|
+
# In this case, the new instance inherits all of the attributes and terms
|
25
|
+
# defined on the <i>PrototypeCalculation</i> template.
|
26
|
+
#
|
27
|
+
# In contrast to instances of <i>PrototypeCalculation</i>, instances of the
|
28
|
+
# <i>OngoingCalculation</i> class will typically have their term values
|
29
|
+
# explicitly set according to the specific calculation scenario being
|
30
|
+
# represented. Other term attributes, e.g. units, may also be modified on a
|
31
|
+
# calculation-by-calculation basis. These values and other attributes form
|
32
|
+
# (at least some of) the data which is passed to the AMEE platform in order
|
33
|
+
# to make caluclations.
|
34
|
+
#
|
35
|
+
class OngoingCalculation < Calculation
|
36
|
+
|
37
|
+
public
|
38
|
+
|
39
|
+
# String representing the AMEE platform profile UID assocaited with <tt>self</tt>
|
40
|
+
attr_accessor :profile_uid
|
41
|
+
|
42
|
+
# String representing the AMEE platform profile item UID assocaited with <tt>self</tt>
|
43
|
+
attr_accessor :profile_item_uid
|
44
|
+
|
45
|
+
# Hash of invalidity messages. Keys are represent the labels of terms
|
46
|
+
# assocaited with <tt>self</tt>. Values are string error message reports associated
|
47
|
+
# with the keyed term.
|
48
|
+
#
|
49
|
+
attr_accessor :invalidity_messages
|
50
|
+
|
51
|
+
# Construct an Ongoing Calculation. Should be called only via
|
52
|
+
# <tt>PrototypeCalculation#begin_calculation</tt>. Not intended for external
|
53
|
+
# use.
|
54
|
+
#
|
55
|
+
def initialize
|
56
|
+
super
|
57
|
+
dirty!
|
58
|
+
reset_invalidity_messages
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns true if the value of a term associated with <tt>self</tt> has been changed
|
62
|
+
# since the calculation was last synchronized with the AMEE platform.
|
63
|
+
# Otherwise, return false.
|
64
|
+
#
|
65
|
+
def dirty?
|
66
|
+
@dirty
|
67
|
+
end
|
68
|
+
|
69
|
+
# Declare that the calculation is dirty, i.e. that changes to term values
|
70
|
+
# have been made since <tt>self</tt> was last synchronized with the AMEE platform,
|
71
|
+
# in which case a synchonization with with AMEE must occur for <tt>self</tt> to be
|
72
|
+
# valid.
|
73
|
+
#
|
74
|
+
def dirty!
|
75
|
+
@dirty=true
|
76
|
+
end
|
77
|
+
|
78
|
+
# Declare that the calculation is not dirty, and need not be sent to AMEE
|
79
|
+
# for results to be valid.
|
80
|
+
#
|
81
|
+
def clean!
|
82
|
+
@dirty=false
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns true if all compulsory terms are set, i.e. ahve non-nil values.
|
86
|
+
# This inidicates that <tt>self</tt> is ready to be sent to the AMEE platform for
|
87
|
+
# outputs to be calculated
|
88
|
+
#
|
89
|
+
def satisfied?
|
90
|
+
inputs.compulsory.unset.empty?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Mass assignment of (one or more) term attributes (value, unit, per_unit)
|
94
|
+
# based on data defined in <tt>choice</tt>. <tt>choice</tt> should be
|
95
|
+
# a hash with keys representing the labels of terms which are to be updated.
|
96
|
+
# Hash values can represent either the the value to be assigned explicitly,
|
97
|
+
# or, alternatively, a hash representing any or all of the term value, unit
|
98
|
+
# and per_unit attributes (keyed as :value, :unit and :per_unit).
|
99
|
+
#
|
100
|
+
# Unit attributes can be represented by any form which is accepted by the
|
101
|
+
# <i>Quantify::Unit#for</i> method (either an instance of
|
102
|
+
# <i>Quantify::Unit::Base</i> (or subclass) or a symbolized or string
|
103
|
+
# representation of the a unit symbol, name or label).
|
104
|
+
#
|
105
|
+
# Nil values are ignored. Term attributes can be intentionally blanked by
|
106
|
+
# passing a blank string as the respective hash value.
|
107
|
+
#
|
108
|
+
# Examples of options hash which modify only term values:
|
109
|
+
#
|
110
|
+
# options = { :type => 'van' }
|
111
|
+
#
|
112
|
+
# options = { :type => 'van',
|
113
|
+
# :distance => 100 }
|
114
|
+
#
|
115
|
+
# options = { :type => 'van',
|
116
|
+
# :distance => "" }
|
117
|
+
#
|
118
|
+
# Examples of options hash which modify other term attributes:
|
119
|
+
#
|
120
|
+
# options = { :type => 'van',
|
121
|
+
# :distance => { :value => 100 }}
|
122
|
+
#
|
123
|
+
# options = { :type => 'van',
|
124
|
+
# :distance => { :value => 100,
|
125
|
+
# :unit => :mi }}
|
126
|
+
#
|
127
|
+
# options = { :type => 'van',
|
128
|
+
# :distance => { :value => 100,
|
129
|
+
# :unit => 'feet' }}
|
130
|
+
#
|
131
|
+
# my_distance_unit = <Quantify::Unit::NonSI:0xb71cac48 @label="mi" ... >
|
132
|
+
# my_time_unit = <Quantify::Unit::NonSI:0xb71c67b0 @label="h" ... >
|
133
|
+
#
|
134
|
+
# options = { :type => 'van',
|
135
|
+
# :distance => { :value => 100,
|
136
|
+
# :unit => my_unit,
|
137
|
+
# :per_unit => my_time_unit }}
|
138
|
+
#
|
139
|
+
# my_calculation.choose_without_validation!(options)
|
140
|
+
#
|
141
|
+
# Do not attempt to check that the values specified are acceptable.
|
142
|
+
#
|
143
|
+
def choose_without_validation!(choice)
|
144
|
+
# Make sure choice keys are symbols since they are mapped to term labels
|
145
|
+
# Uses extension methods for Hash defined in /core_extensions
|
146
|
+
choice.recursive_symbolize_keys!
|
147
|
+
|
148
|
+
new_profile_uid= choice.delete(:profile_uid)
|
149
|
+
self.profile_uid=new_profile_uid if new_profile_uid
|
150
|
+
new_profile_item_uid= choice.delete(:profile_item_uid)
|
151
|
+
self.profile_item_uid=new_profile_item_uid if new_profile_item_uid
|
152
|
+
choice.each do |k,v|
|
153
|
+
next unless self[k]
|
154
|
+
if v.is_a? Hash
|
155
|
+
# <tt>if has_key?</tt> clause included so that single attributes can
|
156
|
+
# be updated without nullifying others if their values are not
|
157
|
+
# explicitly passed. Intentional blanking of values is enabled by
|
158
|
+
# passing nil or "".
|
159
|
+
#
|
160
|
+
self[k].value v[:value] if v.has_key?(:value)
|
161
|
+
self[k].unit v[:unit] if v.has_key?(:unit)
|
162
|
+
self[k].per_unit v[:per_unit] if v.has_key?(:per_unit)
|
163
|
+
else
|
164
|
+
self[k].value v
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Similar to <tt>#choose_without_validation!</tt> but performs validation
|
170
|
+
# on the modified input terms and raises a <i>ChoiceValidation</i>
|
171
|
+
# exception if any of the values supplied is invalid
|
172
|
+
#
|
173
|
+
def choose!(choice)
|
174
|
+
choose_without_validation!(choice)
|
175
|
+
validate!
|
176
|
+
raise AMEE::DataAbstraction::Exceptions::ChoiceValidation.new(invalidity_messages) unless
|
177
|
+
invalidity_messages.empty?
|
178
|
+
end
|
179
|
+
|
180
|
+
# Similar to <tt>#choose!</tt> but returns <tt>false</tt> if any term
|
181
|
+
# attributes are invalid, rather than raising an exception. Returns
|
182
|
+
# <tt>true</tt> if validation is successful.
|
183
|
+
#
|
184
|
+
def choose(choice)
|
185
|
+
begin
|
186
|
+
choose!(choice)
|
187
|
+
return true
|
188
|
+
rescue AMEE::DataAbstraction::Exceptions::ChoiceValidation
|
189
|
+
return false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Synchonizes the current term values and attributes with the AMEE platform
|
194
|
+
# if <tt>self</tt> is <tt>dirty?</tt>, and subsequently calls <tt>clean!</tt>
|
195
|
+
# on <tt>self</tt>
|
196
|
+
#
|
197
|
+
def calculate!
|
198
|
+
return unless dirty?
|
199
|
+
syncronize_with_amee
|
200
|
+
clean!
|
201
|
+
end
|
202
|
+
|
203
|
+
# Check that the values set for all terms are acceptable, and raise a
|
204
|
+
# <i>ChoiceValidation</i> exception if not. Error messages are available
|
205
|
+
# via the <tt>self.invalidity_messages</tt> hash.
|
206
|
+
#
|
207
|
+
def validate!
|
208
|
+
return unless dirty?
|
209
|
+
reset_invalidity_messages
|
210
|
+
inputs.each do |d|
|
211
|
+
d.validate! unless d.unset?
|
212
|
+
end
|
213
|
+
autodrill
|
214
|
+
end
|
215
|
+
|
216
|
+
# Declare that the term labelled by <tt>label</tt> has an unnaceptable value
|
217
|
+
# and load the <tt>message</tt> into the <tt>invalidity_messages</tt> hash.
|
218
|
+
#
|
219
|
+
def invalid(label,message)
|
220
|
+
@invalidity_messages[label]=message
|
221
|
+
end
|
222
|
+
|
223
|
+
# Set the values of any invalid terms to nil. Can be called following any
|
224
|
+
# of the <tt>#choose...</tt> methods so that invalid terms resulting from
|
225
|
+
# modification of a previously valid calculation can be cleared. This is
|
226
|
+
# particularly useful in cases where drill down choices have been changed
|
227
|
+
# thus invalidating the choices for subsequent drills.
|
228
|
+
#
|
229
|
+
def clear_invalid_terms!
|
230
|
+
terms.select do |term|
|
231
|
+
invalidity_messages.keys.include?(term.label)
|
232
|
+
end.each do |term|
|
233
|
+
term.value nil
|
234
|
+
end
|
235
|
+
reset_invalidity_messages
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
# Empty the hash of error messages for term choices.
|
241
|
+
def reset_invalidity_messages
|
242
|
+
@invalidity_messages={}
|
243
|
+
end
|
244
|
+
|
245
|
+
# Obtain from the AMEE platform the results of a calculation, and set these
|
246
|
+
# to the output terms of <tt>self</tt>.
|
247
|
+
#
|
248
|
+
def load_outputs
|
249
|
+
outputs.each do |output|
|
250
|
+
res=nil
|
251
|
+
if output.path==:default
|
252
|
+
res= profile_item.amounts.find{|x| x[:default] == true}
|
253
|
+
else
|
254
|
+
res= profile_item.amounts.find{|x| x[:type] == output.path}
|
255
|
+
end
|
256
|
+
if res
|
257
|
+
output.value res[:value]
|
258
|
+
output.unit res[:unit]
|
259
|
+
output.per_unit res[:per_unit]
|
260
|
+
else
|
261
|
+
# If no platform result, then no outputs should be set. Added so that
|
262
|
+
# nullifying a compulsory PIV wipes output value, otherwise previous
|
263
|
+
# output values persist after PIVs have been removed.
|
264
|
+
#
|
265
|
+
output.value nil
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Load any metadata stored in the AMEE profile which can be used to set
|
271
|
+
# metadata for <tt>self</tt>. Not implemented in AMEE yet.
|
272
|
+
#
|
273
|
+
def load_metadata
|
274
|
+
end
|
275
|
+
|
276
|
+
# If a profile item exists for <tt>self</tt>, load the corresponding values
|
277
|
+
# and units for any unset <i>Profile</i> terms (profile item values) from
|
278
|
+
# the AMEE platform
|
279
|
+
#
|
280
|
+
def load_profile_item_values
|
281
|
+
return unless profile_item
|
282
|
+
profiles.unset.each do |term|
|
283
|
+
ameeval=profile_item.values.find { |value| value[:path] == term.path }
|
284
|
+
term.value ameeval[:value]
|
285
|
+
term.unit ameeval[:unit]
|
286
|
+
term.per_unit ameeval[:per_unit]
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Load drill values from the AMEE platform. If the remote drills selections
|
291
|
+
# are different than locally set values, raise a <i>Syncronization</i>
|
292
|
+
# exception.
|
293
|
+
#
|
294
|
+
# If an exception is raised, typical practice would be to delete the profile
|
295
|
+
# item associated with <tt>self</tt> and create a new one with the current
|
296
|
+
# selection of drill down choices (see, for example, the
|
297
|
+
# <tt>#synchronize_with_amee</tt> method
|
298
|
+
#
|
299
|
+
def load_drills
|
300
|
+
return unless profile_item
|
301
|
+
drills.each do |term|
|
302
|
+
ameeval=data_item.value(term.path)
|
303
|
+
raise Exceptions::Syncronization if term.set? && ameeval!=term.value
|
304
|
+
term.value ameeval
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Dispatch the calculation to AMEE. If necessary, delete an out of date
|
309
|
+
# AMEE profile item and create a new one. Fetch any values which are stored
|
310
|
+
# in the AMEE platform and not stored locally. Send any values stored locally
|
311
|
+
# and not stored in the AMEE platform. Fetch calculation results from the
|
312
|
+
# AMEE platform and update outputs assocaited with <tt>self</tt>
|
313
|
+
#
|
314
|
+
def syncronize_with_amee
|
315
|
+
new_memoize_pass
|
316
|
+
find_profile
|
317
|
+
load_profile_item_values
|
318
|
+
begin
|
319
|
+
load_drills
|
320
|
+
rescue Exceptions::Syncronization
|
321
|
+
delete_profile_item
|
322
|
+
end
|
323
|
+
load_metadata
|
324
|
+
# We could create an unsatisfied PI, and just check drilled? here
|
325
|
+
if satisfied?
|
326
|
+
if profile_item
|
327
|
+
set_profile_item_values
|
328
|
+
else
|
329
|
+
create_profile_item
|
330
|
+
end
|
331
|
+
load_outputs
|
332
|
+
end
|
333
|
+
rescue AMEE::UnknownError
|
334
|
+
# Tidy up, only if we created a "Bad" profile item.
|
335
|
+
# Need to check this condition
|
336
|
+
delete_profile_item
|
337
|
+
raise DidNotCreateProfileItem
|
338
|
+
end
|
339
|
+
|
340
|
+
# Returns a <i>String</i> representation of drill down choices appropriate for
|
341
|
+
# submitting to the AMEE platform. An optional hash argument can be provided,
|
342
|
+
# with the key <tt>:before</tt> in order to specify the drill down choice
|
343
|
+
# to which the representation is required, e.g.
|
344
|
+
#
|
345
|
+
# my_calc.drill_options #=> "type=van&fuel=petrol&size=2.0+litres"
|
346
|
+
#
|
347
|
+
# my_calc.drill_options(:before => :size) #=> "type=van&fuel=petrol"
|
348
|
+
#
|
349
|
+
def drill_options(options={})
|
350
|
+
to=options.delete(:before)
|
351
|
+
drills_to_use=to ? drills.before(to).set : drills.set
|
352
|
+
drills_to_use.map{|x| "#{CGI.escape(x.path)}=#{CGI.escape(x.value)}"}.join("&")
|
353
|
+
end
|
354
|
+
|
355
|
+
# Returns a <i>Hash</i> representation of <i>Profile</i> term attributes
|
356
|
+
# appropriate for submitting to the AMEE platform via the AMEE rubygem.
|
357
|
+
#
|
358
|
+
def profile_options
|
359
|
+
result={}
|
360
|
+
profiles.set.each do |piv|
|
361
|
+
result[piv.path]=piv.value
|
362
|
+
result["#{piv.path}Unit"]=piv.unit.label unless piv.unit.nil?
|
363
|
+
result["#{piv.path}PerUnit"]=piv.per_unit.label unless piv.per_unit.nil?
|
364
|
+
end
|
365
|
+
if contents[:start_date] && !contents[:start_date].value.blank?
|
366
|
+
result[:start_date] = contents[:start_date].value
|
367
|
+
end
|
368
|
+
if contents[:end_date] && !contents[:end_date].value.blank?
|
369
|
+
result[:end_date] = contents[:end_date].value
|
370
|
+
end
|
371
|
+
return result
|
372
|
+
end
|
373
|
+
|
374
|
+
# Returns a <i>Hash</i> of options for profile item GET requests to the AMEE
|
375
|
+
# platform.
|
376
|
+
#
|
377
|
+
# This is where is where return unit convertion requests can be handled
|
378
|
+
# if/when theseare implemented.
|
379
|
+
#
|
380
|
+
def get_options
|
381
|
+
# Specify unit options here based on the contents
|
382
|
+
# getopts={}
|
383
|
+
# getopts[:returnUnit] = params[:unit] if params[:unit]
|
384
|
+
# getopts[:returnPerUnit] = params[:perUnit] if params[:perUnit]
|
385
|
+
return {}
|
386
|
+
end
|
387
|
+
|
388
|
+
# Return the <i>AMEE::Profile::Profile</i> object under which the AMEE profile
|
389
|
+
# item associated with <tt>self</tt> belongs and and update
|
390
|
+
# <tt>self.profile_uid</tt> to make the appropriate reference.
|
391
|
+
#
|
392
|
+
def find_profile
|
393
|
+
unless self.profile_uid
|
394
|
+
prof ||= AMEE::Profile::ProfileList.new(connection).first
|
395
|
+
prof ||= AMEE::Profile::Profile.create(connection)
|
396
|
+
self.profile_uid=prof.uid
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# Generate a unique name for the profile item assocaited with <tt>self</tt>.
|
401
|
+
# This is required in order to make similarly drilled profile items within
|
402
|
+
# the same profile distinguishable.
|
403
|
+
#
|
404
|
+
# This is random at present but could be improved to generate more meaningful
|
405
|
+
# name by interrogating metadata according to specifications of an
|
406
|
+
# organisational model.
|
407
|
+
#
|
408
|
+
def amee_name
|
409
|
+
UUIDTools::UUID.timestamp_create
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
# Create a profile item in the AMEE platform to be associated with
|
414
|
+
# <tt>self</tt>. Raises <i>AlreadyHaveProfileItem</i> exception if a
|
415
|
+
# profile item value is already associated with <tt>self</tt>
|
416
|
+
#
|
417
|
+
def create_profile_item
|
418
|
+
raise Exceptions::AlreadyHaveProfileItem unless profile_item_uid.blank?
|
419
|
+
location = AMEE::Profile::Item.create(profile_category,
|
420
|
+
# call <tt>#data_item_uid</tt> on drill object rather than <tt>self</tt>
|
421
|
+
# since there exists no profile item value yet
|
422
|
+
amee_drill.data_item_uid,
|
423
|
+
profile_options.merge(:get_item=>false,:name=>amee_name))
|
424
|
+
self.profile_item_uid=location.split('/').last
|
425
|
+
end
|
426
|
+
|
427
|
+
# Methods which should be memoized once per interaction with AMEE to minimise
|
428
|
+
# API calls. These require wiping at every pass, because otherwise, they might
|
429
|
+
# change, e.g. if metadata changes change the profile
|
430
|
+
#
|
431
|
+
MemoizedProfileInformation=[:profile_item,:data_item,:profile_category]
|
432
|
+
|
433
|
+
# Clear the memoized values.
|
434
|
+
def new_memoize_pass
|
435
|
+
MemoizedProfileInformation.each do |prop|
|
436
|
+
instance_variable_set("@#{prop.to_s}",nil)
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
# Return the <i>AMEE::Profile::Item</i> object associated with self. If
|
441
|
+
# not set, instantiates via the AMEE platform and assigns to <tt>self</tt>
|
442
|
+
#
|
443
|
+
def profile_item
|
444
|
+
@profile_item||=AMEE::Profile::Item.get(connection, profile_item_path, get_options) unless profile_item_uid.blank?
|
445
|
+
end
|
446
|
+
|
447
|
+
# Update the associated profile item in the AMEE platform with the current
|
448
|
+
# <i>Profile</i> term values and attributes
|
449
|
+
#
|
450
|
+
def set_profile_item_values
|
451
|
+
AMEE::Profile::Item.update(connection,profile_item_path,
|
452
|
+
profile_options.merge(:get_item=>false))
|
453
|
+
#Clear the memoised profile item, to reload with updated values
|
454
|
+
@profile_item=nil
|
455
|
+
end
|
456
|
+
|
457
|
+
# Delete the profile item which is associated with <tt>self</tt> from the
|
458
|
+
# AMEE platform and nullify the local references (i.e.
|
459
|
+
# <tt>@profile_item</tt> and <tt>#profile_item_uid</tt>)
|
460
|
+
#
|
461
|
+
def delete_profile_item
|
462
|
+
AMEE::Profile::Item.delete(connection,profile_item_path)
|
463
|
+
self.profile_item_uid=false
|
464
|
+
@profile_item=nil
|
465
|
+
end
|
466
|
+
|
467
|
+
# Returns a string representing the AMEE platform path to the profile
|
468
|
+
# category associated with <tt>self</self>
|
469
|
+
#
|
470
|
+
def profile_category_path
|
471
|
+
"/profiles/#{profile_uid}#{path}"
|
472
|
+
end
|
473
|
+
|
474
|
+
# Returns a string representing the AMEE platform path to the profile
|
475
|
+
# item associated with <tt>self</self>
|
476
|
+
#
|
477
|
+
def profile_item_path
|
478
|
+
"#{profile_category_path}/#{profile_item_uid}"
|
479
|
+
end
|
480
|
+
|
481
|
+
# Returns a string representing the AMEE platform path to the data item
|
482
|
+
# associated with <tt>self</self>
|
483
|
+
#
|
484
|
+
def data_item_path
|
485
|
+
"/data#{path}/#{data_item_uid}"
|
486
|
+
end
|
487
|
+
|
488
|
+
# Returns a string representing the AMEE platform UID for the data item
|
489
|
+
# associated with <tt>self</self>
|
490
|
+
#
|
491
|
+
def data_item_uid
|
492
|
+
profile_item.data_item_uid
|
493
|
+
end
|
494
|
+
|
495
|
+
# Return the <i>AMEE::Data::Item</i> object associated with self. If
|
496
|
+
# not set, instantiates via the AMEE platform and assigns to <tt>self</tt>
|
497
|
+
#
|
498
|
+
def data_item
|
499
|
+
@data_item||=AMEE::Data::Item.get(connection, data_item_path, get_options)
|
500
|
+
end
|
501
|
+
|
502
|
+
# Return the <i>AMEE::Profile::Category</i> object associated with self. If
|
503
|
+
# not set, instantiates via the AMEE platform and assigns to <tt>self</tt>
|
504
|
+
#
|
505
|
+
def profile_category
|
506
|
+
@profile_category||=AMEE::Profile::Category.get(connection, profile_category_path)
|
507
|
+
end
|
508
|
+
|
509
|
+
# Automatically set the value of a drill term if there is only one choice
|
510
|
+
def autodrill
|
511
|
+
|
512
|
+
picks=amee_drill.selections
|
513
|
+
picks.each do |path,value|
|
514
|
+
# If drill term does not exist, initialize a dummy instance.
|
515
|
+
#
|
516
|
+
# This is useful in those cases where some drills selections are unecessary
|
517
|
+
# (i.e. not all choices require selection for data items to be uniquely
|
518
|
+
# identified) and removes the need to explicitly specify the blank drills
|
519
|
+
# in configuration. This doesn't matter if calculations are auto configured.
|
520
|
+
#
|
521
|
+
if drill = drill_by_path(path)
|
522
|
+
drill.value value
|
523
|
+
else
|
524
|
+
drills << Drill.new {path path; value value}
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
public
|
530
|
+
|
531
|
+
# Instantiate an <tt>AMEE::Data::DrillDown</tt> object representing the
|
532
|
+
# drill down sequence defined by the drill terms associated with
|
533
|
+
# <tt>self</tt>. As with <tt>#drill_options</tt>, An optional hash argument
|
534
|
+
# can be provided, with the key <tt>:before</tt> in order to specify the
|
535
|
+
# drill down choice to which the representation is required, e.g.
|
536
|
+
#
|
537
|
+
# my_calc.amee_drill(:before => :size)
|
538
|
+
#
|
539
|
+
def amee_drill(options={})
|
540
|
+
AMEE::Data::DrillDown.get(connection,"/data#{path}/drill?#{drill_options(options)}")
|
541
|
+
end
|
542
|
+
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|