amee-data-persistence 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ method: <%= method %>
@@ -0,0 +1,29 @@
1
+ class CreatePersistenceTables < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :calculations do |t|
4
+ t.string "profile_uid"
5
+ t.string "profile_item_uid"
6
+ t.string "calculation_type"
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ create_table :terms do |t|
12
+ t.integer "calculation_id"
13
+ t.string "label"
14
+ t.string "value"
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :calculations, :calculation_type
20
+ add_index :calculations, :profile_item_uid
21
+ add_index :terms, [:calculation_id, :label], :name => "calc_id_label_index"
22
+ add_index :terms, [:label, :value, :calculation_id], :name => "label_name_calc_id_index"
23
+ end
24
+
25
+ def self.down
26
+ drop_table :calculations
27
+ drop_table :terms
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ class AddUnitColumns < ActiveRecord::Migration
2
+ def self.up
3
+
4
+ add_column :terms, :unit, :string
5
+ add_column :terms, :per_unit, :string
6
+
7
+ end
8
+
9
+ def self.down
10
+
11
+ remove_column :terms, :unit, :string
12
+ remove_column :terms, :per_unit
13
+
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ class AddValueTypes < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :terms, :value_type, :string
4
+ end
5
+
6
+ def self.down
7
+ remove_column :terms, :value_type
8
+ end
9
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,12 @@
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
+ require 'rubygems'
5
+ gem 'activerecord', "~> 2.3.9"
6
+ require 'active_record'
7
+ require 'quantify'
8
+
9
+ require 'amee/data_abstraction/calculation_collection'
10
+ require 'amee/db/calculation'
11
+ require 'amee/db/term'
12
+ require 'amee/db/config'
@@ -0,0 +1,22 @@
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::CalculationCollection
5
+
6
+ module AMEE
7
+ module DataAbstraction
8
+
9
+ # Class for containing a collection of instances of the class
10
+ # <i>OngoingCalculation</i>. This class is used to return mutliple
11
+ # ongoing calculation objects using the <i>OngoingCalculation.find</i>
12
+ # class method.
13
+ #
14
+ # This class is extended by the amee-reporting gem to provide analytical
15
+ # methods which can be applied across collections of caluclations (e.g.
16
+ # averages and summations of terms, sorting, etc.)
17
+ #
18
+ class CalculationCollection < Array
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,274 @@
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::PersistenceSupport
5
+
6
+ module AMEE
7
+ module DataAbstraction
8
+
9
+ # This module provides a number of class and instance methods which are
10
+ # added to the <i>AMEE::DataAbstraction::OngoingCalculation</i> class if
11
+ # the amee-data-persistence gem is required. These methods provide an
12
+ # interface between the <i>AMEE::DataAbstraction::OngoingCalculation</i>
13
+ # class (and its instances) and the the <i>AMEE::Db::Calculation</i> class
14
+ # which provides database persistence for calculations.
15
+ #
16
+ module PersistenceSupport
17
+
18
+ def self.included(base)
19
+ base.extend ClassMethods
20
+ end
21
+
22
+ # Represents the instance of <i>AMEE::Db::Calculation</i> which is
23
+ # associated with <tt>self</tt>.
24
+ #
25
+ attr_accessor :db_calculation
26
+
27
+ # Represents the primary key of the associated database record (instance of
28
+ # <i>AMEE::Db::Calculation</i>) if a database record for <tt>self</tt> is
29
+ # defined.
30
+ #
31
+ def id
32
+ db_calculation.nil? ? nil : db_calculation.id
33
+ end
34
+
35
+ # Same as <i>save</i> but raises an exception on error
36
+ def save!
37
+ validate!
38
+ record = db_calculation || get_db_calculation
39
+ record.update_calculation!(to_hash)
40
+ end
41
+
42
+ # Saves a representation of <tt>self<tt> to the database. Returns
43
+ # <tt>true</tt> if successful, otherwise <tt>false</tt>.
44
+ #
45
+ def save
46
+ save!
47
+ true
48
+ rescue ActiveRecord::RecordNotSaved
49
+ false
50
+ end
51
+
52
+ # Deletes the database record for <tt>self</tt> and any associated profile
53
+ # item value in the AMEE platform.
54
+ #
55
+ def delete
56
+ record = db_calculation || get_db_calculation
57
+ AMEE::Db::Calculation.delete record.id
58
+ self.db_calculation = nil
59
+ delete_profile_item
60
+ end
61
+
62
+ # As <i>calculate_and_save</i> but raises an exception on error
63
+ def calculate_and_save!
64
+ calculate!
65
+ save!
66
+ end
67
+
68
+ # Performs the calculation against AMEE and saves it to the database
69
+ def calculate_and_save
70
+ calculate_and_save!
71
+ true
72
+ rescue ActiveRecord::RecordNotFound, AMEE::DataAbstraction::Exceptions::DidNotCreateProfileItem
73
+ false
74
+ end
75
+
76
+ # Finds the instance of database record associated with <tt>self</tt> based
77
+ # on the <tt>profile_item_uid</tt> attribute of <tt>self</tt> and sets the
78
+ # <tt>db_calculation</tt> attribute of <tt>self</tt> to the associated
79
+ # instance of <i>AMEE::Db::Calculation</i>.
80
+ #
81
+ def get_db_calculation
82
+ self.db_calculation = AMEE::Db::Calculation.find_or_initialize_by_profile_item_uid(send :profile_item_uid)
83
+ end
84
+
85
+ # Returns the subset of terms associated with <tt>self</tt> which should be
86
+ # passed for database persistence, based on the configuration set in
87
+ # <i>AMEE::Db::Config#storage_method</i>.
88
+ #
89
+ def stored_terms
90
+ stored_terms = []
91
+ stored_terms += metadata if OngoingCalculation.store_metadata?
92
+ stored_terms += inputs if OngoingCalculation.store_inputs?
93
+ stored_terms += outputs if OngoingCalculation.store_outputs?
94
+ stored_terms
95
+ end
96
+
97
+ # Returns a hash representation of <tt>self</tt>. By default, only the terms
98
+ # which are configured for persistence (according to
99
+ # <i>AMEE::Db::Config#storage_method</i>) are included. All terms can be
100
+ # explicitly required by passing the symbol <tt>:full</tt> as an argument.
101
+ # E.g.
102
+ #
103
+ # # Set storage to include everything
104
+ # AMEE::Db::Config.storage_method=:everything
105
+ #
106
+ # my_calculation.to_hash #=> { :calculation_type => :fuel,
107
+ # :profile_item_uid => nil,
108
+ # :profile_uid => "A8D8R95EE7DH",
109
+ # :type => { :value => 'coal'},
110
+ # :location => { :value => 'facility' },
111
+ # :mass => { :value => 250,
112
+ # :unit => <Quantify::Unit ... > },
113
+ # :co2 => { :value => 60.5,
114
+ # :unit => <Quantify::Unit ... > }}
115
+ #
116
+ # # Set storage to include only oputputs and metadata
117
+ # AMEE::Db::Config.storage_method=:outputs
118
+ #
119
+ # my_calculation.to_hash #=> { :calculation_type => :fuel,
120
+ # :profile_item_uid => nil,
121
+ # :profile_uid => "A8D8R95EE7DH",
122
+ # :location => { :value => 'facility' },
123
+ # :co2 => { :value => 60.5,
124
+ # :unit => <Quantify::Unit ... > }}
125
+ #
126
+ # # Set storage to include only metadata
127
+ # AMEE::Db::Config.storage_method=:metadata
128
+ #
129
+ # my_calculation.to_hash #=> { :calculation_type => :fuel,
130
+ # :profile_item_uid => nil,
131
+ # :profile_uid => "A8D8R95EE7DH",
132
+ # :location => { :value => 'facility' },
133
+ #
134
+ # # Get full hash represenation regardless of storage level
135
+ # my_calculation.to_hash :full #=> { :calculation_type => :fuel,
136
+ # :profile_item_uid => nil,
137
+ # :profile_uid => "A8D8R95EE7DH",
138
+ # :type => { :value => 'coal'},
139
+ # :location => { :value => 'facility' },
140
+ # :mass => { :value => 250,
141
+ # :unit => <Quantify::Unit ... > },
142
+ # :co2 => { :value => 60.5,
143
+ # :unit => <Quantify::Unit ... > }}
144
+ #
145
+ def to_hash(representation=:stored_terms_only)
146
+ hash = {}
147
+ hash[:calculation_type] = label
148
+ hash[:profile_item_uid] = send :profile_item_uid
149
+ hash[:profile_uid] = send :profile_uid
150
+ (representation == :full ? terms : stored_terms ).each do |term|
151
+ sub_hash = {}
152
+ sub_hash[:value] = term.value
153
+ sub_hash[:unit] = term.unit if term.unit
154
+ sub_hash[:per_unit] = term.per_unit if term.per_unit
155
+ hash[term.label.to_sym] = sub_hash
156
+ end
157
+ return hash
158
+ end
159
+
160
+ module ClassMethods
161
+
162
+
163
+ # Find and initialize instance(s) of <i>OngoingCalculation</i> from the
164
+ # database using standard <i>ActiveRecord</i> <tt>find</tt> options.
165
+ # Returns <tt>nil</tt> if no records are found. If multiple records are
166
+ # found they are returned via an instance of the
167
+ # <i>CalculationCollection</i> class. E.g.,
168
+ #
169
+ # OngoingCalculation.find(:first)
170
+ #
171
+ # #=> <AMEE::DataAbstraction::OngoingCalculation ... >
172
+ #
173
+ # OngoingCalculation.find(:first,
174
+ # :conditions => {:profile_item_uid => "K588DH47SMN5"})
175
+ #
176
+ # #=> <AMEE::DataAbstraction::OngoingCalculation ... >
177
+ #
178
+ # OngoingCalculation.find(:all)
179
+ #
180
+ # #=> <AMEE::DataAbstraction::CalculationCollection ... >
181
+ #
182
+ def find(*args)
183
+ unless args.last.is_a? Symbol or args.last.is_a? Integer
184
+ raise ActiveRecord::ActiveRecordError.new("Using :include with terms and then conditioning on terms doesn't work due to rails caching. Use the :joins option instead.") if args.last[:include].to_s.match(/terms/) && args.last[:conditions].to_s.match(/terms/)
185
+ args.last[:include] = "terms" if args.last[:joins].to_s.match(/terms/)
186
+ end
187
+ result = AMEE::Db::Calculation.find(*args)
188
+ return nil unless result
189
+ if result.respond_to?(:map)
190
+ CalculationCollection.new(result.compact.map { |calc| initialize_from_db_record(calc) })
191
+ else
192
+ initialize_from_db_record(result)
193
+ end
194
+ end
195
+
196
+ # Find calculations of type <tt>type</tt> in the database and initialize
197
+ # as instances of <i>OngoingCalculation</i>. Returns <tt>nil</tt> if no
198
+ # records are found. If multiple records are found they are returned via
199
+ # an instance of the <i>CalculationCollection</i> class.
200
+ #
201
+ # Specify that either the first or all records should be returns by passing
202
+ # <tt>:first</tt> or <tt>:all</tt> as the first argument. The unique label
203
+ # of the calcualtion type required should be passed as the second argument.
204
+ # Standard options associated with the <i>ActiveRecord</i> <tt>find</tt>
205
+ # class method can be passed as the third argument. E.g.,
206
+ #
207
+ # OngoingCalculation.find_by_type(:first,:electricity)
208
+ #
209
+ # #=> <AMEE::DataAbstraction::OngoingCalculation ... >
210
+ #
211
+ # OngoingCalculation.find_by_type(:all,:fuel)
212
+ #
213
+ # #=> <AMEE::DataAbstraction::CalculationCollection ... >
214
+ #
215
+ def find_by_type(ordinality,type,options={})
216
+ OngoingCalculation.find(ordinality, options.merge(:conditions => {:calculation_type => type.to_s}))
217
+ end
218
+
219
+ # Initialize and return an instance of <i>OngoingCalculation</i> based
220
+ # on the database record represented by <tt>record</tt>.
221
+ #
222
+ def initialize_from_db_record(record)
223
+ unless record.is_a? AMEE::Db::Calculation
224
+ raise ArgumentError.new("Argument is not of class AMEE::Db::Calculation")
225
+ end
226
+ calc = Calculations.calculations[record.type].begin_calculation
227
+ calc.db_calculation = record
228
+ # Means that validation needs to occur before calcs are saved
229
+ calc.choose_without_validation!(record.to_hash)
230
+ return calc
231
+ end
232
+
233
+ # Returns a new instance of the <i>AMEE::Db::BaseConfig</i> class
234
+ def storage_config
235
+ AMEE::Db::BaseConfig.new
236
+ end
237
+
238
+ # Returns the currently configured storage level for database persistence,
239
+ # i.e. whether all terms should be persisted versus outputs and/or
240
+ # metadata only.
241
+ #
242
+ def storage_method
243
+ storage_config.storage_method
244
+ end
245
+
246
+ # Returns <tt>true</tt> if all terms should be persisted within the
247
+ # database according to the currently configured storage level (See
248
+ # <i>AMEE::Db::BaseConfig</i>). Otherwise, returns <tt>false</tt>.
249
+ #
250
+ def store_inputs?
251
+ storage_config.store_everything?
252
+ end
253
+
254
+ # Returns <tt>true</tt> if output terms should be persisted within the
255
+ # database according to the currently configured storage level (See
256
+ # <i>AMEE::Db::BaseConfig</i>). Otherwise, returns <tt>false</tt>.
257
+ #
258
+ def store_outputs?
259
+ storage_config.store_outputs?
260
+ end
261
+
262
+ # Returns <tt>true</tt> if metadata terms should be persisted within the
263
+ # database according to the currently configured storage level (See
264
+ # <i>AMEE::Db::BaseConfig</i>). Otherwise, returns <tt>false</tt>.
265
+ #
266
+ def store_metadata?
267
+ storage_config.store_metadata?
268
+ end
269
+
270
+ end
271
+
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,156 @@
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::Db::Calculation
5
+
6
+ module AMEE
7
+ module Db
8
+
9
+ # This class represents a database record for a calculation performed using
10
+ # the <i>AMEE:DataAbstraction::OngoingCalculation</i> class. This class stores
11
+ # the primary calculation level attributes such as the calculation
12
+ # <tt>calculation_type</tt>, <tt>profile_uid</tt> and <tt>profile_item_uid</tt>.
13
+ #
14
+ # The values and attributes of specific calculation terms are stored via the
15
+ # related class <i>AMEE::Db::Term</i>.
16
+ #
17
+ # This class is typically used by proxy, via the <tt>find</tt>,
18
+ # <tt>find_by_type</tt>, <tt>#save</tt>, <tt>#delete</tt>, and
19
+ # <tt>#get_db_calculation</tt> methods associated with the
20
+ # <i>AMEE:DataAbstraction::OngoingCalculation</i> class.
21
+ #
22
+ class Calculation < ActiveRecord::Base
23
+
24
+ has_many :terms, :class_name => "AMEE::Db::Term", :dependent => :destroy
25
+ validates_presence_of :calculation_type
26
+ validates_format_of :profile_item_uid, :with => /\A([A-Z0-9]{12})\z/, :allow_nil => true, :allow_blank => true
27
+ validates_format_of :profile_uid, :with => /\A([A-Z0-9]{12})\z/, :allow_nil => true, :allow_blank => true
28
+ before_save :validate_calculation_type
29
+
30
+ # Standardize the <tt>calculation_type</tt> attribute to <i>String</i>
31
+ # format. Called using before filter prior to record saving to ensure
32
+ # string serialization.
33
+ #
34
+ def validate_calculation_type
35
+ self.calculation_type = calculation_type.to_s
36
+ end
37
+
38
+ # Convenience method for returning the <tt>calculation_type</tt> attribute
39
+ # of <tt>self</tt> in canonical symbol form.
40
+ #
41
+ def type
42
+ calculation_type.to_sym
43
+ end
44
+
45
+ # Returns the subset of all instance attributes which should be editable via
46
+ # mass update methods and which should be included in hash representations of
47
+ # self, i.e. those passed in explcitly as data rather than added by
48
+ # <i>ActiveRecord</i> (e.g. <tt>id</tt>, <tt>created_at</tt>, etc.).
49
+ #
50
+ def primary_calculation_attributes
51
+ attributes.keys.reject {|attr| ['id','created_at','updated_at'].include? attr }
52
+ end
53
+
54
+ # Update the attributes of <tt>self</tt> and those of any related terms,
55
+ # according to the passed <tt>options</tt> hash. Any associated terms which
56
+ # are not represented in <tt>options</tt> are deleted.
57
+ #
58
+ # Term attributes provided in <tt>options</tt> should be keyed with the
59
+ # term label and include a sub-hash with keys represent one or more of
60
+ # :value, :unit and :per_unit. E.g.,
61
+ #
62
+ # options = { :profile_item_uid => "W93UEY573U4E8",
63
+ # :mass => { :value => 23 },
64
+ # :distance => { :value => 1400,
65
+ # :unit => <Quantify::Unit::SI ... > }}
66
+ #
67
+ # my_calculation.update_calculation!(options)
68
+ #
69
+ def update_calculation!(options)
70
+ primary_calculation_attributes.each do |attr|
71
+ if options.keys.include? attr.to_sym
72
+ update_calculation_attribute!(attr,options.delete(attr.to_sym),false)
73
+ end
74
+ end
75
+ save!
76
+ options.each_pair do |attribute,value|
77
+ add_or_update_term!(attribute,value)
78
+ end
79
+ delete_unspecified_terms(options)
80
+ reload
81
+ end
82
+
83
+ # Update the attribute of <tt>self</tt> represented by the label
84
+ # <tt>key</tt> with the value of <tt>value</tt>. By default,
85
+ # the <tt>#save!</tt> method is called, in turn calling the class
86
+ # validations.
87
+ #
88
+ # Specify that the record should not be saved by passing <tt>false</tt>
89
+ # as the final argument.
90
+ #
91
+ def update_calculation_attribute!(key,value,save=true)
92
+ # use attr_accessor (via #send) method rather than
93
+ # #update_attribute so that validations are performed
94
+ #
95
+ send("#{key}=", (value.nil? ? nil : value.to_s))
96
+ save! if save
97
+ end
98
+
99
+ # Add, or update an existing, associated term represented by the label
100
+ # <tt>label</tt> and value, unit and/or per_unit attributes defined by
101
+ # <tt>data</tt>. The <tt>data</tt> argument should be a hash with keys
102
+ # represent one or more of :value, :unit and :per_unit. E.g.,
103
+ #
104
+ # data = { :value => 1400,
105
+ # :unit => <Quantify::Unit::SI ... > }
106
+ #
107
+ # my_calculation.add_or_update_term!(:distance, data)
108
+ #
109
+ # This method is called as part of the <tt>#update_calculation!</tt>
110
+ # method
111
+ #
112
+ def add_or_update_term!(label,data)
113
+ term = Term.find_or_initialize_by_calculation_id_and_label(id,label.to_s)
114
+ term.update_attributes!(data)
115
+ end
116
+
117
+ # Delete all of the terms which are not explicitly referenced in the
118
+ # <tt>options</tt> hash.
119
+ #
120
+ # This method is called as part of the <tt>#update_calculation!</tt>
121
+ # method
122
+ #
123
+ def delete_unspecified_terms(options)
124
+ terms.each do |term|
125
+ Term.delete(term.id) unless options.keys.include? term.label.to_sym
126
+ end
127
+ end
128
+
129
+ # Returns a <i>Hash</i> representation of <tt>self</tt> including a only
130
+ # the data explicitly passed in (those added by <i>ActiveRecord</i> -
131
+ # <tt>created</tt>, <tt>updated</tt>, <tt>id</tt> - are ignored) as well
132
+ # as sub-hashes for all associated terms. E.g.,
133
+ #
134
+ # my_calculation.to_hash #=> { :profile_uid => "EYR758EY36WY",
135
+ # :profile_item_uid => "W83URT48DY3W",
136
+ # :type => { :value => 'car' },
137
+ # :distance => { :value => 1600,
138
+ # :unit => <Quantify::Unit::SI> },
139
+ # :co2 => { :value => 234.1,
140
+ # :unit => <Quantify::Unit::NonSI> }}
141
+ #
142
+ # This method can be used to initialize instances of the class
143
+ # <tt>OngoingCalculation</tt> by providing the hashed options for any of
144
+ # the <tt>choose...</tt> methods.
145
+ #
146
+ def to_hash
147
+ hash = {}
148
+ terms.each { |term| hash.merge!(term.to_hash) }
149
+ [ :profile_item_uid, :profile_uid ].each do |attr|
150
+ hash[attr] = self.send(attr)
151
+ end
152
+ return hash
153
+ end
154
+ end
155
+ end
156
+ end