amee-data-abstraction 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.rvmrc +1 -0
  2. data/CHANGELOG.txt +4 -0
  3. data/Gemfile +16 -0
  4. data/Gemfile.lock +41 -0
  5. data/LICENSE.txt +27 -0
  6. data/README.txt +188 -0
  7. data/Rakefile +102 -0
  8. data/VERSION +1 -0
  9. data/amee-data-abstraction.gemspec +115 -0
  10. data/examples/_calculator_form.erb +27 -0
  11. data/examples/calculation_controller.rb +16 -0
  12. data/init.rb +4 -0
  13. data/lib/amee-data-abstraction.rb +30 -0
  14. data/lib/amee-data-abstraction/calculation.rb +236 -0
  15. data/lib/amee-data-abstraction/calculation_set.rb +101 -0
  16. data/lib/amee-data-abstraction/drill.rb +63 -0
  17. data/lib/amee-data-abstraction/exceptions.rb +47 -0
  18. data/lib/amee-data-abstraction/input.rb +197 -0
  19. data/lib/amee-data-abstraction/metadatum.rb +58 -0
  20. data/lib/amee-data-abstraction/ongoing_calculation.rb +545 -0
  21. data/lib/amee-data-abstraction/output.rb +16 -0
  22. data/lib/amee-data-abstraction/profile.rb +108 -0
  23. data/lib/amee-data-abstraction/prototype_calculation.rb +350 -0
  24. data/lib/amee-data-abstraction/term.rb +506 -0
  25. data/lib/amee-data-abstraction/terms_list.rb +150 -0
  26. data/lib/amee-data-abstraction/usage.rb +90 -0
  27. data/lib/config/amee_units.rb +129 -0
  28. data/lib/core-extensions/class.rb +27 -0
  29. data/lib/core-extensions/hash.rb +43 -0
  30. data/lib/core-extensions/ordered_hash.rb +21 -0
  31. data/lib/core-extensions/proc.rb +15 -0
  32. data/rails/init.rb +32 -0
  33. data/spec/amee-data-abstraction/calculation_set_spec.rb +54 -0
  34. data/spec/amee-data-abstraction/calculation_spec.rb +75 -0
  35. data/spec/amee-data-abstraction/drill_spec.rb +38 -0
  36. data/spec/amee-data-abstraction/input_spec.rb +77 -0
  37. data/spec/amee-data-abstraction/metadatum_spec.rb +17 -0
  38. data/spec/amee-data-abstraction/ongoing_calculation_spec.rb +494 -0
  39. data/spec/amee-data-abstraction/profile_spec.rb +39 -0
  40. data/spec/amee-data-abstraction/prototype_calculation_spec.rb +256 -0
  41. data/spec/amee-data-abstraction/term_spec.rb +385 -0
  42. data/spec/amee-data-abstraction/terms_list_spec.rb +53 -0
  43. data/spec/config/amee_units_spec.rb +71 -0
  44. data/spec/core-extensions/class_spec.rb +25 -0
  45. data/spec/core-extensions/hash_spec.rb +44 -0
  46. data/spec/core-extensions/ordered_hash_spec.rb +12 -0
  47. data/spec/core-extensions/proc_spec.rb +12 -0
  48. data/spec/fixtures/electricity.rb +35 -0
  49. data/spec/fixtures/electricity_and_transport.rb +55 -0
  50. data/spec/fixtures/transport.rb +26 -0
  51. data/spec/spec.opts +2 -0
  52. data/spec/spec_helper.rb +244 -0
  53. 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