amee-data-persistence 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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