amee-data-abstraction 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/CHANGELOG.txt +3 -0
  2. data/README.txt +28 -15
  3. data/Rakefile +1 -16
  4. data/VERSION +1 -1
  5. data/amee-data-abstraction.gemspec +6 -5
  6. data/lib/amee-data-abstraction/calculation.rb +1 -1
  7. data/lib/amee-data-abstraction/calculation_set.rb +152 -10
  8. data/lib/amee-data-abstraction/drill.rb +8 -2
  9. data/lib/amee-data-abstraction/input.rb +24 -0
  10. data/lib/amee-data-abstraction/ongoing_calculation.rb +13 -9
  11. data/lib/amee-data-abstraction/term.rb +31 -13
  12. data/spec/amee-data-abstraction/calculation_set_spec.rb +244 -8
  13. data/spec/amee-data-abstraction/calculation_spec.rb +23 -18
  14. data/spec/amee-data-abstraction/drill_spec.rb +34 -7
  15. data/spec/amee-data-abstraction/input_spec.rb +113 -73
  16. data/spec/amee-data-abstraction/metadatum_spec.rb +1 -1
  17. data/spec/amee-data-abstraction/ongoing_calculation_spec.rb +45 -29
  18. data/spec/amee-data-abstraction/profile_spec.rb +2 -2
  19. data/spec/amee-data-abstraction/prototype_calculation_spec.rb +12 -8
  20. data/spec/amee-data-abstraction/term_spec.rb +49 -5
  21. data/spec/amee-data-abstraction/terms_list_spec.rb +16 -12
  22. data/spec/config/amee_units_spec.rb +1 -2
  23. data/spec/core-extensions/class_spec.rb +18 -18
  24. data/spec/core-extensions/hash_spec.rb +1 -2
  25. data/spec/core-extensions/ordered_hash_spec.rb +1 -2
  26. data/spec/core-extensions/proc_spec.rb +1 -1
  27. data/spec/fixtures/{electricity.rb → config/calculations/electricity.rb} +2 -2
  28. data/spec/fixtures/config/calculations/electricity_and_transport.rb +53 -0
  29. data/spec/fixtures/{transport.rb → config/calculations/transport.rb} +2 -2
  30. data/spec/fixtures/config/electricity.rb +35 -0
  31. data/spec/spec_helper.rb +38 -6
  32. metadata +8 -7
  33. data/spec/fixtures/electricity_and_transport.rb +0 -55
data/CHANGELOG.txt CHANGED
@@ -1,4 +1,7 @@
1
1
  = Changelog
2
2
 
3
+ == 1.3.0
4
+ * Reconfigure storage and API for Calculation Sets
5
+
3
6
  == 1.0.0
4
7
  * Initial public release
data/README.txt CHANGED
@@ -25,9 +25,9 @@ Documentation: http://rubydoc.info/gems/amee-data-abstraction
25
25
  All gem requirements should be installed as part of the rubygems installation process
26
26
  above, but are listed here for completeness.
27
27
 
28
- * amee ~> 3.0
28
+ * amee ~> 3.1
29
29
  * uuidtools = 2.1.2
30
- * quantify = 1.1.0
30
+ * quantify = 1.2.2
31
31
 
32
32
  == USAGE
33
33
 
@@ -117,16 +117,10 @@ Submit to AMEE for calculation
117
117
 
118
118
  Typical practice is initialize the calculation prototypes required for an
119
119
  application via a configuration file which creates the required calculation
120
- templates within an instance of CalculationSet. If the calculation set is assigned
121
- to a global variable or constant, the set of prototypes is available for
122
- initializing new calculations and templating view structures (e.g. tables, forms)
123
- from anywhere in the application.
120
+ templates within an instance of CalculationSet. Such a configuration file can
121
+ be structured the follow DSL:
124
122
 
125
- Adding a configuration to /config or /config/initializers may be appropriate
126
-
127
- # e.g. /config/initializers/calculations.rb
128
-
129
- Calculations = AMEE::DataAbstraction::CalculationSet {
123
+ # e.g. /config/calculations/my_emissions_calculations.rb
130
124
 
131
125
  calculation {
132
126
  label :electricity
@@ -148,17 +142,36 @@ Adding a configuration to /config or /config/initializers may be appropriate
148
142
  path "/some/fuel/associated/path/in/amee"
149
143
  terms_from_amee
150
144
  }
151
- }
145
+
146
+ The default location for such files within Rails applications is under
147
+ /config/calculations. If such an approach is taken, the configuration can be
148
+ read and a calculation set generated by using the filename, thus:
149
+
150
+ CalculationSet.find('my_emissions_calculations')
151
+
152
+ #=> <AMEE::DataAbstraction::CalculationSet ... >
153
+
154
+ Otherwise, the path to the configuration file can be provided:
155
+
156
+ CalculationSet.find('some/directory/my_emissions_calculations')
157
+
158
+ #=> <AMEE::DataAbstraction::CalculationSet ... >
159
+
160
+ The calculation set is accessible as follow using the same argument as used
161
+ by the Find method (filename if conventional Rails location, path otherwise):
162
+
163
+ CalculationSet.sets['my_emissions_calculations']
152
164
 
153
165
  #=> <AMEE::DataAbstraction::CalculationSet ... >
154
166
 
155
- From this global calculation set, initialize a new calculation
167
+ And a new calcualtion can be initialized by providing the prototype calculation
168
+ label:
156
169
 
157
- my_fuel_calculation = Calculations[:fuel].begin_calculation
170
+ my_fuel_calculation = CalculationSet.sets['my_emissions_calcualtions'][:fuel].begin_calculation
158
171
 
159
172
  #=> <AMEE::DataAbstraction::OngoingCalculation ... >
160
173
 
161
- a_different_transport_calculation = Calculations[:transport].begin_calculation
174
+ a_different_transport_calculation = CalculationSet.sets['my_emissions_calcualtions'][:transport].begin_calculation
162
175
 
163
176
  #=> <AMEE::DataAbstraction::OngoingCalculation ... >
164
177
 
data/Rakefile CHANGED
@@ -74,22 +74,7 @@ Jeweler::Tasks.new do |gem|
74
74
  end
75
75
  Jeweler::RubygemsDotOrgTasks.new
76
76
 
77
- require 'rake/testtask'
78
- Rake::TestTask.new(:test) do |test|
79
- test.libs << 'lib' << 'test'
80
- test.pattern = 'test/**/test_*.rb'
81
- test.verbose = true
82
- end
83
-
84
- require 'rcov/rcovtask'
85
- Rcov::RcovTask.new do |test|
86
- test.libs << 'test'
87
- test.pattern = 'test/**/test_*.rb'
88
- test.verbose = true
89
- test.rcov_opts << '--exclude "gems/*"'
90
- end
91
-
92
- task :default => :test
77
+ task :default => :spec
93
78
 
94
79
  require 'rake/rdoctask'
95
80
  Rake::RDocTask.new do |rdoc|
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
1
+ 1.3.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{amee-data-abstraction}
8
- s.version = "1.2.0"
8
+ s.version = "1.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["James Hetherington", "Andrew Berkeley", "James Smith", "George Palmer"]
12
- s.date = %q{2011-10-10}
12
+ s.date = %q{2011-10-18}
13
13
  s.description = %q{Part of the AMEEappkit this gem provides a data abstraction layer, decreasing the amount and detail of development required}
14
14
  s.email = %q{help@amee.com}
15
15
  s.extra_rdoc_files = [
@@ -64,9 +64,10 @@ Gem::Specification.new do |s|
64
64
  "spec/core-extensions/hash_spec.rb",
65
65
  "spec/core-extensions/ordered_hash_spec.rb",
66
66
  "spec/core-extensions/proc_spec.rb",
67
- "spec/fixtures/electricity.rb",
68
- "spec/fixtures/electricity_and_transport.rb",
69
- "spec/fixtures/transport.rb",
67
+ "spec/fixtures/config/calculations/electricity.rb",
68
+ "spec/fixtures/config/calculations/electricity_and_transport.rb",
69
+ "spec/fixtures/config/calculations/transport.rb",
70
+ "spec/fixtures/config/electricity.rb",
70
71
  "spec/spec.opts",
71
72
  "spec/spec_helper.rb"
72
73
  ]
@@ -132,7 +132,7 @@ module AMEE
132
132
  end
133
133
 
134
134
  def explorer_url
135
- Rails.logger.info "#explorer_url method deprecated. Use #discover_url" if defined? Rails
135
+ Rails.logger.info "#explorer_url method deprecated. Use #discover_url" if defined?(Rails) && Rails.logger.present?
136
136
  discover_url
137
137
  end
138
138
 
@@ -36,22 +36,95 @@ module AMEE
36
36
  #
37
37
  class CalculationSet
38
38
 
39
- # Initialize a <i>CalculationSet</i> with a "DSL" block, i.e. a block to be
40
- # evaluated in the context of the instantiated CalculationSet
39
+ # Class variable holding all instantiated calculation sets keyed on the set
40
+ # name.
41
41
  #
42
- def initialize(options={},&block)
43
- @calculations=ActiveSupport::OrderedHash.new
42
+ @@sets = {}
43
+
44
+ # Convenience method for accessing the @@sets class variable
45
+ def self.sets
46
+ @@sets
47
+ end
48
+
49
+ # Retrieve a calculation set on the basis of a configuration file name or
50
+ # relatiev/absolute file path. If configuration files are location within
51
+ # the default Rails location under '/config/calculations' then the path and
52
+ # the .rb extenstion can be omitted from the name.
53
+ #
54
+ def self.find(name)
55
+ @@sets[name.to_sym] or load_set(name)
56
+ end
57
+
58
+ # Regenerate a configuration lock file assocaited with the master
59
+ # configuration file <tt>name</tt>. Optionally set a custom path for the
60
+ # lock file as <tt>output_path</tt>, otherwise the lock file path and
61
+ # filename will be based upon the master file with the extension .lock.rb.
62
+ #
63
+ def self.regenerate_lock_file(name,output_path=nil)
64
+ set = CalculationSet.find(name)
65
+ set.generate_lock_file(output_path)
66
+ end
67
+
68
+ # Find a specific prototype calculation instance without specifying the set
69
+ # to which it belongs.
70
+ #
71
+ def self.find_prototype_calculation(label)
72
+ @@sets.each_pair do |name,set|
73
+ set = find(name)
74
+ return set[label] if set[label]
75
+ end
76
+ return nil
77
+ end
78
+
79
+ protected
80
+
81
+ # Load a calculation set based on a filename or full path.
82
+ def self.load_set(name)
83
+ CalculationSet.new(name,:file => name) do
84
+ instance_eval(File.open(self.config_path).read)
85
+ end
86
+ end
87
+
88
+ DEFFAULT_RAILS_CONFIG_DIR = "config/calculations"
89
+
90
+ # Find the config file assocaited with <tt>name</tt>. The method first checks
91
+ # the default Rails configuration location (config/calculations) then the
92
+ # file path described by <tt>name</tt> relative to the Rails root and by
93
+ # absolute path.
94
+ def self.find_config_file(name)
95
+ default_config_dir = defined?(::Rails) ? "#{::Rails.root}/#{DEFFAULT_RAILS_CONFIG_DIR}" : nil
96
+ if defined?(::Rails) && File.exists?("#{default_config_dir}/#{name.to_s}.rb")
97
+ "#{default_config_dir}/#{name.to_s}.rb"
98
+ elsif defined?(::Rails) && File.exists?("#{default_config_dir}/#{name.to_s}")
99
+ "#{default_config_dir}/#{name.to_s}"
100
+ elsif defined?(::Rails) && File.exists?("#{::Rails.root}/#{name}")
101
+ "#{::Rails.root}/#{name}"
102
+ elsif File.exists?(name)
103
+ name
104
+ else
105
+ raise ArgumentError, "The config file '#{name}' could not be located"
106
+ end
107
+ end
108
+
109
+ public
110
+
111
+ attr_accessor :calculations, :name, :file
112
+
113
+ # Initialise a new Calculation set. Specify the name of the calculation set
114
+ # as the first argument. This name is used as the set key within the class
115
+ # variable @@sets hash.
116
+ #
117
+ def initialize(name,options={},&block)
118
+ raise ArgumentError, "Calculation set must have a name" unless name
119
+ @name = name
120
+ @file = CalculationSet.find_config_file(options[:file]) if options[:file]
121
+ @calculations = ActiveSupport::OrderedHash.new
44
122
  @all_blocks=[]
45
123
  @all_options={}
46
124
  instance_eval(&block) if block
125
+ @@sets[@name.to_sym] = self
47
126
  end
48
127
 
49
- # Access the @calculations instance variable ordered hash. Keys are labels
50
- # assocaited with each prototype calculation; values are the instantiated
51
- # <i>PrototypeCalculation</i> objects
52
- #
53
- attr_accessor :calculations
54
-
55
128
  # Shorthand method for returning the prototype calculation which is represented
56
129
  # by a label matching <tt>sym</tt>
57
130
  #
@@ -94,8 +167,77 @@ module AMEE
94
167
  instance_exec(usage,&dsl_block)
95
168
  }
96
169
  end
170
+ end
97
171
 
172
+ # Returns the path to the configuration file for <tt>self</tt>. If a .lock
173
+ # file exists, this takes precedence, otherwise the master config file
174
+ # described by the <tt>#file</tt> attribute is returned.
175
+ #
176
+ def config_path
177
+ lock_file_exists? ? lock_file_path : @file
98
178
  end
179
+
180
+ # Returns the path to the configuration lock file
181
+ def lock_file_path
182
+ @file.gsub(".rb",".lock.rb") rescue nil
183
+ end
184
+
185
+ # Returns <tt>true</tt> if a configuration lock file exists. Otherwise,
186
+ # returns <tt>false</tt>.
187
+ #
188
+ def lock_file_exists?
189
+ File.exists?(lock_file_path)
190
+ end
191
+
192
+ # Generates a lock file for the calcuation set configuration. If no argument
193
+ # is provided the, the lock file is generated using the filename and path
194
+ # described by the <tt>#lock_file_path</tt> method. If a custom output
195
+ # location is required, this can be provided optionally as an argument.
196
+ #
197
+ def generate_lock_file(output_path=nil)
198
+ file = output_path || lock_file_path or raise ArgumentError,
199
+ "No path for lock file known. Either set path for the master config file using the #file accessor method or provide as an argument"
200
+ string = ""
201
+ @calculations.values.each do |prototype_calculation|
202
+ string += "calculation {\n\n"
203
+ string += " name \"#{prototype_calculation.name}\"\n"
204
+ string += " label :#{prototype_calculation.label}\n"
205
+ string += " path \"#{prototype_calculation.path}\"\n\n"
206
+ prototype_calculation.terms.each do |term|
207
+ string += " #{term.class.to_s.split("::").last.downcase} {\n"
208
+ string += " name \"#{term.name}\"\n" unless term.name.blank?
209
+ string += " label :#{term.label}\n" unless term.label.blank?
210
+ string += " path \"#{term.path}\"\n" unless term.path.blank?
211
+ string += " value \"#{term.value}\"\n" unless term.value.blank?
212
+
213
+ if term.is_a?(AMEE::DataAbstraction::Input)
214
+ string += " fixed \"#{term.value}\"\n" if term.fixed? && !term.value.blank?
215
+ if term.is_a?(AMEE::DataAbstraction::Drill)
216
+ string += " choices \"#{term.choices.join('\',\'')}\"\n" if term.instance_variable_defined?("@choices") && !term.choices.blank?
217
+ elsif term.is_a?(AMEE::DataAbstraction::Profile)
218
+ string += " choices [\"#{term.choices.join('\"','\"')}\"]\n" if term.instance_variable_defined?("@choices") && !term.choices.blank?
219
+ end
220
+ string += " optional!\n" if term.optional?
221
+ end
222
+
223
+ string += " default_unit :#{term.default_unit.label}\n" unless term.default_unit.blank?
224
+ string += " default_per_unit :#{term.default_per_unit.label}\n" unless term.default_per_unit.blank?
225
+ string += " alternative_units :#{term.alternative_units.map(&:label).join(', :')}\n" unless term.alternative_units.blank?
226
+ string += " alternative_per_units :#{term.alternative_per_units.map(&:label).join(', :')}\n" unless term.alternative_per_units.blank?
227
+ string += " unit :#{term.unit.label}\n" unless term.unit.blank?
228
+ string += " per_unit :#{term.per_unit.label}\n" unless term.per_unit.blank?
229
+ string += " type :#{term.type}\n" unless term.type.blank?
230
+ string += " interface :#{term.interface}\n" unless term.interface.blank?
231
+ string += " note \"#{term.note}\"\n" unless term.note.blank?
232
+ string += " disable!\n" if !term.is_a?(AMEE::DataAbstraction::Drill) && term.disabled?
233
+ string += " hide!\n" if term.hidden?
234
+ string += " }\n\n"
235
+ end
236
+ string += "}\n\n"
237
+ end
238
+ File.open(file,'w') { |f| f.write string }
239
+ end
240
+
99
241
  end
100
242
  end
101
243
  end
@@ -42,8 +42,14 @@ module AMEE
42
42
  def choices(*args)
43
43
  if args.empty?
44
44
  if @choices.blank?
45
- c=parent.amee_drill(:before=>label).choices
46
- c.length==1 ? [value] : c
45
+ drill_down = parent.amee_drill(:before=>label)
46
+ if single_choice = drill_down.selections[path]
47
+ disable!
48
+ [single_choice]
49
+ else
50
+ enable!
51
+ drill_down.choices
52
+ end
47
53
  else
48
54
  @choices
49
55
  end
@@ -11,6 +11,8 @@ module AMEE
11
11
  #
12
12
  class Input < Term
13
13
 
14
+ attr_accessor :dirty
15
+
14
16
  # Returns the valid choices for this input
15
17
  # (Abstract, implemented only for subclasses of input.)
16
18
  def choices
@@ -29,6 +31,7 @@ module AMEE
29
31
  @validation = nil
30
32
  validation_message {"#{name} is invalid."}
31
33
  super
34
+ @dirty = false
32
35
  end
33
36
 
34
37
  # Configures the value of <tt>self</tt> to be fixed to <tt>val</tt>, i.e.
@@ -67,6 +70,7 @@ module AMEE
67
70
  if args.first.to_s != @value.to_s
68
71
  raise Exceptions::FixedValueInterference if fixed?
69
72
  parent.dirty! if parent and parent.is_a? OngoingCalculation
73
+ mark_as_dirty
70
74
  end
71
75
  end
72
76
  super
@@ -155,6 +159,16 @@ module AMEE
155
159
  !optional?(usage)
156
160
  end
157
161
 
162
+ # Manually set the term as optional
163
+ def optional!
164
+ @optional=true
165
+ end
166
+
167
+ # Manually set the term as compuslory
168
+ def compulsory!
169
+ @optional=false
170
+ end
171
+
158
172
  # Check that the value of <tt>self</tt> is valid. If invalid, and is defined
159
173
  # as part of a calculation, add the invalidity message to the parent
160
174
  # calculation's error list. Otherwise, raise a <i>ChoiceValidation</i>
@@ -185,6 +199,10 @@ module AMEE
185
199
  super || fixed?
186
200
  end
187
201
 
202
+ def dirty?
203
+ @dirty
204
+ end
205
+
188
206
  protected
189
207
  # Returns <tt>true</tt> if the value set for <tt>self</tt> is either blank
190
208
  # or passes custom validation criteria. Otherwise, returns <tt>false</tt>.
@@ -192,6 +210,12 @@ module AMEE
192
210
  def valid?
193
211
  validation.blank? || validation === @value_before_cast
194
212
  end
213
+
214
+ def mark_as_dirty
215
+ @dirty = true
216
+ parent.dirty! if parent and parent.is_a? OngoingCalculation
217
+ end
218
+
195
219
  end
196
220
  end
197
221
  end
@@ -235,6 +235,10 @@ module AMEE
235
235
  reset_invalidity_messages
236
236
  end
237
237
 
238
+ def clear_outputs
239
+ outputs.each {|output| output.value nil }
240
+ end
241
+
238
242
  def ==(other_calc)
239
243
  !terms.inject(false) do |boolean,term|
240
244
  boolean || term != other_calc[term.label]
@@ -254,7 +258,7 @@ module AMEE
254
258
  def load_outputs
255
259
  outputs.each do |output|
256
260
  res=nil
257
- if output.path==:default
261
+ if output.path.to_s=='default'
258
262
  res= profile_item.amounts.find{|x| x[:default] == true}
259
263
  else
260
264
  res= profile_item.amounts.find{|x| x[:type] == output.path}
@@ -283,7 +287,7 @@ module AMEE
283
287
  # and units for any unset <i>Profile</i> terms (profile item values) from
284
288
  # the AMEE platform
285
289
  #
286
- def load_profile_item_values
290
+ def load_profile_item_values
287
291
  return unless profile_item
288
292
  profiles.unset.each do |term|
289
293
  ameeval=profile_item.values.find { |value| value[:path] == term.path }
@@ -305,6 +309,7 @@ module AMEE
305
309
  def load_drills
306
310
  return unless profile_item
307
311
  drills.each do |term|
312
+ next unless term.value.nil? || term.dirty?
308
313
  ameeval=data_item.value(term.path)
309
314
  raise Exceptions::Syncronization if term.set? && ameeval!=term.value
310
315
  term.value ameeval
@@ -325,6 +330,7 @@ module AMEE
325
330
  load_drills
326
331
  rescue Exceptions::Syncronization
327
332
  delete_profile_item
333
+ clear_outputs
328
334
  end
329
335
  load_metadata
330
336
  # We could create an unsatisfied PI, and just check drilled? here
@@ -451,10 +457,8 @@ module AMEE
451
457
  # <i>Profile</i> term values and attributes
452
458
  #
453
459
  def set_profile_item_values
454
- AMEE::Profile::Item.update(connection,profile_item_path,
455
- profile_options.merge(:get_item=>false))
456
- #Clear the memoised profile item, to reload with updated values
457
- @profile_item=nil
460
+ @profile_item = AMEE::Profile::Item.update(connection,profile_item_path,
461
+ profile_options.merge(:get_item=>true))
458
462
  end
459
463
 
460
464
  # Delete the profile item which is associated with <tt>self</tt> from the
@@ -463,7 +467,7 @@ module AMEE
463
467
  #
464
468
  def delete_profile_item
465
469
  AMEE::Profile::Item.delete(connection,profile_item_path)
466
- self.profile_item_uid=false
470
+ self.profile_item_uid=nil
467
471
  @profile_item=nil
468
472
  end
469
473
 
@@ -509,6 +513,8 @@ module AMEE
509
513
  @profile_category||=AMEE::Profile::Category.get(connection, profile_category_path)
510
514
  end
511
515
 
516
+ public
517
+
512
518
  # Automatically set the value of a drill term if there is only one choice
513
519
  def autodrill
514
520
 
@@ -529,8 +535,6 @@ module AMEE
529
535
  end
530
536
  end
531
537
 
532
- public
533
-
534
538
  # Instantiate an <tt>AMEE::Data::DrillDown</tt> object representing the
535
539
  # drill down sequence defined by the drill terms associated with
536
540
  # <tt>self</tt>. As with <tt>#drill_options</tt>, An optional hash argument