amee_rails_layer 0.3.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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 AMEE UK Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,89 @@
1
+ The rails abstraction layer gem provides a series of common abstractions that are typically used when
2
+ creating rails models that store carbon data in AMEE. This is an opinionated gem and whilst it will
3
+ suit a lot of projects well it will not be right for every project. In those cases use the lower level
4
+ amee-ruby gem (http://github.com/floppy/amee-ruby).
5
+
6
+ == Installation
7
+
8
+ Command line installation:
9
+
10
+ sudo gem install amee_rails_layer
11
+
12
+ == Configuration
13
+
14
+ For models that store carbon data in AMEE, several additional fields required in the database. Normally
15
+ these would be:
16
+ * name (string) - a name for the object, often set by user but can be assigned automatically
17
+ * amee_profile_item_id (string) - the AMEE profile Item ID used to store the carbon data
18
+ * carbon_output_cache (float) - the amount of carbon produced
19
+ * units (string) - the units the amount field is in
20
+ * amount (float) - the amount of the thing being recorded, eg 6 (kg), 9 (litres)
21
+ with either:
22
+ * amee_profile (string) - the AMEE profile identifier under which all data is stored
23
+ * or profile_id (integer) - the record id of the model holding the amee_profile (and of course that model to have an amee_profile field)
24
+ Extra fields may also be required depending on the options used. See AmeeCarbonStore for full details
25
+
26
+ == Usage
27
+
28
+ All data in AMEE is stored under a profile and there are two ways to encapsulate this knowledge in your
29
+ application:
30
+
31
+ 1) Models belong_to another object that has the amee_profile. Exactly what this parent will be called
32
+ will depend on the application, but common examples will be Project or User. In this case it is up to
33
+ the developer to add the has_amee_profile declaration to this class.
34
+
35
+ 2) Each model has its own profile. The has_amee_profile declaration is handled automatically.
36
+
37
+ The best way to determine which approach to take is to read the AMEE documentation (link at end of
38
+ README) and see which unit in the application logically maps to an AMEE profile. In either case the
39
+ model will require an amee_profile field to store the profile identifier.
40
+
41
+ The models that are to store carbon data in AMEE need the has_carbon_data_stored_in_amee declaration with
42
+ any appropriate options and must implement the amee_category method. For this, there are two main
43
+ options:
44
+
45
+ 1) If the model is always the same type just return a AmeeCategory with relevant options for where the
46
+ data should be stored in AMEE
47
+
48
+ 2) If the model has multiple types, for example a Journey that can be a Car, Bus Journey etc, a pattern
49
+ like the following can be used:
50
+
51
+ class Journey < ActiveRecord::Base
52
+ TYPE = {
53
+ :bus => AmeeCategory.new("Bus", :journey_distance, "/transport/bus/generic/defra", :type => "typical"),
54
+ :car => AmeeCategory.new("Car", :distance, "/transport/car/generic/defra/bysize", :fuel => "average", :size => "average"),
55
+ ...
56
+ }
57
+
58
+ def amee_category
59
+ TYPE[journey_type.to_sym]
60
+ end
61
+ end
62
+
63
+ Note the need for a column called journey_type in the database to store the type of the Journey.
64
+
65
+ The database caches the carbon value returned by AMEE to keep the local application fast when
66
+ displaying this data. As a result of this a cronjob should be run at a regular interval to update
67
+ the carbon value. This is because AMEE alters the underlying calculations as more accurate data
68
+ and formulas becomes available. See AmeeCarbonStore#update_carbon_caches for more details.
69
+
70
+ Further information on AMEE can be found at: http://my.amee.com/developers
71
+
72
+ == Note on Patches/Pull Requests
73
+
74
+ * Fork the project.
75
+ * Make your feature addition or bug fix.
76
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
77
+ * Commit, do not mess with rakefile, version, or history (if you want to have your own version, that is fine
78
+ but bump version in a commit by itself I can ignore when I pull)
79
+ * Send me a pull request. Bonus points for topic branches.
80
+
81
+ == TODO
82
+ * Integrate rails.rb from amee-ruby In AmeeCarbonStore has_amee_profile call can be removed from
83
+ the has_carbon_data_stored_in_amee method and connection_to_amee can be named back to the more
84
+ logically amee_connection
85
+ * Full test coverage
86
+
87
+ == Copyright
88
+
89
+ Copyright (c) 2010 AMEE UK ltd. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "amee_rails_layer"
8
+ gem.summary = %Q{An abstraction layer for building applications around the AMEE API}
9
+ gem.description = %Q{We need a longer description of your gem}
10
+ gem.email = "george.palmer@gmail.com"
11
+ gem.homepage = "http://github.com/georgepalmer/amee-abstraction-layer-gem"
12
+ gem.authors = ["George Palmer"]
13
+ gem.add_development_dependency "amee-ruby", ">= 0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "amee-abstraction-layer-gem #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -0,0 +1,58 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{amee_rails_layer}
8
+ s.version = "0.3.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["George Palmer"]
12
+ s.date = %q{2010-03-30}
13
+ s.description = %q{We need a longer description of your gem}
14
+ s.email = %q{george.palmer@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "amee_rails_layer.gemspec",
27
+ "lib/amee_rails_layer.rb",
28
+ "lib/amee_rails_layer/amee_carbon_store.rb",
29
+ "lib/amee_rails_layer/amee_category.rb",
30
+ "lib/amee_rails_layer/unit.rb",
31
+ "rails/init.rb",
32
+ "test/helper.rb",
33
+ "test/test_amee-abstraction-layer-gem.rb"
34
+ ]
35
+ s.homepage = %q{http://github.com/georgepalmer/amee-abstraction-layer-gem}
36
+ s.rdoc_options = ["--charset=UTF-8"]
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = %q{1.3.6}
39
+ s.summary = %q{An abstraction layer for building applications around the AMEE API}
40
+ s.test_files = [
41
+ "test/helper.rb",
42
+ "test/test_amee-abstraction-layer-gem.rb"
43
+ ]
44
+
45
+ if s.respond_to? :specification_version then
46
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
47
+ s.specification_version = 3
48
+
49
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
50
+ s.add_development_dependency(%q<amee-ruby>, [">= 0"])
51
+ else
52
+ s.add_dependency(%q<amee-ruby>, [">= 0"])
53
+ end
54
+ else
55
+ s.add_dependency(%q<amee-ruby>, [">= 0"])
56
+ end
57
+ end
58
+
@@ -0,0 +1,264 @@
1
+ # Module that abstracts some common patterns for data storage/retrieval in AMEE. This is
2
+ # automatically included in ActiveRecord::Base object when used with Rails. See the gem
3
+ # README for more information on the application structure this gem assumes.
4
+ #
5
+ # Classes that have the has_carbon_data_stored_in_amee decleartion require the following
6
+ # database columns:
7
+ # * name (string) - a name for the object, often set by user but can be assigned automatically
8
+ # * amee_profile_item_id (string) - the AMEE profile Item ID used to store the carbon data
9
+ # * carbon_output_cache (float) - the amount of carbon produced
10
+ # * units (string) - the units the amount field is in
11
+ # * amount (float) - the amount of the thing being recorded, eg 6 (kg), 9 (litres)
12
+ # * amee_profile (string) - optional. The amee profile identifier under which all the data
13
+ # is stored. Although this field is optional, either this or profile_id is required
14
+ # * profile_id (integer) - optional. Used when the model belongs_to a parent that has the
15
+ # amee_profile identifier. Name is changed according to the :profile option passed into
16
+ # the has_carbon_data_stored_in_amee. Although this field is optional, either this or
17
+ # amee_profile is required.
18
+ # * repetitions (integer) - optional. Used when the model object is composed of several
19
+ # repetitions - for example 6 x 3 miles would make the repetitions 6
20
+ # * start_date (date) - optional. Used in combination with the has_date_range option on
21
+ # has_carbon_data_stored_in_amee to store the start date for the data
22
+ # * end_date (date) - optional. Used in combination with the has_date_range option on
23
+ # has_carbon_data_stored_in_amee to store the end date for the data
24
+ module AmeeCarbonStore
25
+ def self.included(base)
26
+ base.extend ClassMethods
27
+ end
28
+
29
+ module ClassMethods
30
+ # Class method that configures a class for storing carbon data in amee. Options are as follows:
31
+ # * profile - if set will use this model (through a belongs_to relationship) to access the amee
32
+ # profile to store the data under rather than the model itself. Pass the model to use as symbol
33
+ # - eg :user or :project. Introduces the need for the profile_id field as described in header
34
+ # and the referenced model must store the amee profile key under the field amee_profile (as
35
+ # would be provided by the has_amee_profile decleration)
36
+ # * nameless - if set then automatically assign the name field so the user doesn't have to
37
+ # (still requires the name field in the database as a name must be set to store in amee)
38
+ # * has_date_range - will check for the presence of a start_date and end_date on the object
39
+ # and pass that through to AMEE to store with the data. Will also check the name is unique
40
+ # given the dates unless used in conjunction with :nameless option (the two together are
41
+ # therefore not recommended as it will be easy to create overlapping data). Requires the
42
+ # start_date and end_date database fields as described in header.
43
+ # * repetitions - allows repetitions of the data at a database level where AMEE doesn't support
44
+ # it natively. For example multiple journeys can be setup with this option. The value stored
45
+ # in AMEE will be the total for all journeys. Requires the repetitions database field as
46
+ # described in header.
47
+ # * singular_types - if using a structure where multiple types are available for a model and
48
+ # the type is stored in the field "#{model}_type", this option enforces that only one instance
49
+ # of each type may exist in the database (for a given project if using a project structure)
50
+ def has_carbon_data_stored_in_amee(options = {})
51
+
52
+ if options[:profile]
53
+ belongs_to options[:profile]
54
+ else
55
+ has_amee_profile
56
+ end
57
+
58
+ validates_numericality_of :amount
59
+ validate_on_create :units_are_valid
60
+ validates_presence_of :start_date, :end_date if options[:has_date_range]
61
+ unless options[:nameless]
62
+ if options[:has_date_range]
63
+ validate :name_is_unique_given_date_range
64
+ else
65
+ uniqueness_options = options[:profile] ? {:scope => "#{options[:profile]}_id".to_sym} : {}
66
+ validates_uniqueness_of :name, uniqueness_options
67
+ end
68
+ validates_format_of :name, :with => /\A[\w -]+\Z/,
69
+ :message => "must be letters, numbers, spaces or underscores only"
70
+ validates_length_of :name, :maximum => 250
71
+ end
72
+ if options[:singular_types]
73
+ validate_on_create :maximum_one_instance_for_each_type
74
+ end
75
+ if options[:repetitions]
76
+ validates_numericality_of :repetitions, :only_integer => true
77
+ end
78
+
79
+ before_create :add_to_amee
80
+ before_update :update_amee
81
+ after_destroy :delete_from_amee
82
+
83
+ write_inheritable_attribute(:amee_profile_class, options[:profile]) if options[:profile]
84
+ write_inheritable_attribute(:repetitions, true) if options[:repetitions]
85
+ write_inheritable_attribute(:nameless_entries, true) if options[:nameless]
86
+ write_inheritable_attribute(:has_date_range, true) if options[:has_date_range]
87
+
88
+ include AmeeCarbonStore::InstanceMethods
89
+ end
90
+
91
+ # This method updates all the carbon caches for instances of this model. Be aware this may
92
+ # take some time depending on the number of rows.
93
+ def update_carbon_caches
94
+ find(:all).each do |item|
95
+ item.update_carbon_output_cache
96
+ end.size
97
+ end
98
+ end
99
+
100
+ module InstanceMethods
101
+ # Updates the carbon cache for this instance
102
+ def update_carbon_output_cache
103
+ update_attributes(:carbon_output_cache => amee_profile_item.total_amount)
104
+ end
105
+
106
+ # Returns whether the passed date lies between this instances start and end date. This is only
107
+ # useful when using with date ranges - ie has_date_range is passed as option into
108
+ # has_carbon_data_stored_in_amee
109
+ def covers_date?(date)
110
+ start_date <= date && end_date > date
111
+ end
112
+
113
+ protected
114
+ # The AmeeCategory the model instance is associated with must be returned by this method. The
115
+ # version in this module raises an exception so this method must be overriden in the class
116
+ # including this module. See README for an examples of how to do this for models that are just
117
+ # one type and models that can be many types.
118
+ def amee_category
119
+ raise "Must be implemented in model"
120
+ end
121
+
122
+ # Override this method to pass additional options to AMEE on creation of the AMEE profile item
123
+ # for the model instance. For example if you were creating an item in /home/waste/lifecycle
124
+ # and wanted to set disposalEmissionsOnly as true you could override this method in the model
125
+ # with {:disposalEmissionsOnly => true}
126
+ def additional_options
127
+ nil
128
+ end
129
+
130
+ # Override this method when the category type used in AmeeCategory does not result in the
131
+ # correct key to store the data against in the AMEE API (the key is looked up from category
132
+ # type in AmeeCategory::CATEGORY_TYPES). Normally this lookup would result in values such
133
+ # as distancePerJourney, mass etc but in some cases it won't be directly inferable from the
134
+ # category type you want. If using this for multiple types in a model be sure to only
135
+ # override when the model type is the correct one and not for all types.
136
+ #
137
+ # For example if you were using /home/waste/lifecyclewaste with waste type "other waste" and
138
+ # category_type :weight, you might want to specify a landfill amount rather than a mass. This
139
+ # could be achieved by overriding this method to return "quantityLandfill".
140
+ def amount_symbol
141
+ amee_category.category_type_from_amee_api_unit(get_units)
142
+ end
143
+
144
+ private
145
+ def units_are_valid
146
+ errors.add("units", "are not valid") if amount_symbol.nil?
147
+ end
148
+
149
+ def name_is_unique_given_date_range
150
+ conditions = amee_profile_class_scoping.merge(:name => self.name)
151
+ self.class.find(:all, :conditions => conditions).each do |record|
152
+ next if record.id == self.id
153
+ unless (self.start_date < record.start_date && self.end_date <= record.start_date) ||
154
+ (self.start_date >= record.end_date && self.end_date > record.end_date)
155
+ errors.add_to_base("Entry already added covering dates within that range")
156
+ return false
157
+ end
158
+ end
159
+ end
160
+
161
+ def maximum_one_instance_for_each_type
162
+ model_type = "#{self.class.name.underscore}_type".to_sym
163
+ conditions = amee_profile_class_scoping.merge(model_type => send(model_type))
164
+ if self.class.send(:find, :first, :conditions => conditions)
165
+ errors.add_to_base "Only one #{amee_category.name} entry allowed"
166
+ end
167
+ end
168
+
169
+ def add_to_amee
170
+ profile = create_amee_profile
171
+ self.amee_profile_item_id = profile.uid
172
+ self.carbon_output_cache = profile.total_amount
173
+ return true
174
+ end
175
+
176
+ def update_amee
177
+ result = AMEE::Profile::Item.update(connection_to_amee, amee_profile_item_path,
178
+ :name => get_name, amount_symbol => get_amount, :get_item => true)
179
+ self.carbon_output_cache = result.total_amount
180
+ return true
181
+ end
182
+
183
+ def delete_from_amee
184
+ AMEE::Profile::Item.delete(connection_to_amee, amee_profile_item_path)
185
+ rescue Exception => e
186
+ logger.error "Unable to remove '#{amee_profile_item_path}' from AMEE"
187
+ end
188
+
189
+ def create_amee_profile
190
+ options = {:name => get_name, amount_symbol => get_amount,
191
+ amount_unit_symbol => get_units, :get_item => true}
192
+ if self.class.read_inheritable_attribute(:has_date_range)
193
+ options.merge!(:start_date => self.start_date, :end_date => self.end_date)
194
+ end
195
+ options.merge!(additional_options) if additional_options
196
+ AMEE::Profile::Item.create(amee_profile_category, amee_data_category_uid, options)
197
+ end
198
+
199
+ # TODO can be renamed to amee_connection once ruby-amee rails lib merged in
200
+ def connection_to_amee
201
+ method = self.class.read_inheritable_attribute(:amee_profile_class)
202
+ method ? send(method).amee_connection : amee_connection
203
+ end
204
+
205
+ def amee_profile_path
206
+ method = self.class.read_inheritable_attribute(:amee_profile_class)
207
+ method ? "/profiles/#{send(method).amee_profile}" : "/profiles/#{amee_profile}"
208
+ end
209
+
210
+ def amee_profile_class_scoping
211
+ method = self.class.read_inheritable_attribute(:amee_profile_class)
212
+ method ? {"#{method}_id".to_sym => send(method).id} : {}
213
+ end
214
+
215
+ def amee_profile_item
216
+ @amee_profile_item_cache ||= AMEE::Profile::Item.get(connection_to_amee,
217
+ amee_profile_item_path)
218
+ end
219
+
220
+ def amee_profile_item_path
221
+ "#{amee_profile_path}#{amee_category.path}/#{amee_profile_item_id}"
222
+ end
223
+
224
+ def amee_profile_category
225
+ AMEE::Profile::Category.get(connection_to_amee, "#{amee_profile_path}#{amee_category.path}")
226
+ end
227
+
228
+ def amee_data_category_uid
229
+ Rails.cache.fetch("#{DRILLDOWN_CACHE_PREFIX}_#{amee_category.drill_down_path.gsub(/[^\w]/, '')}") do
230
+ AMEE::Data::DrillDown.get(connection_to_amee, amee_category.drill_down_path).choices.first
231
+ end
232
+ end
233
+
234
+ def amount_unit_symbol
235
+ (amount_symbol.to_s + "Unit").to_sym
236
+ end
237
+
238
+ def get_name
239
+ self.class.read_inheritable_attribute(:nameless_entries) ? "#{self.class.name}_#{Time.now.to_i}" : self.name
240
+ end
241
+
242
+ def get_amount
243
+ if self.class.read_inheritable_attribute(:repetitions)
244
+ result = self.amount * self.repetitions
245
+ else
246
+ result = self.amount
247
+ end
248
+
249
+ if amee_category.has_alternative_unit?(self.units)
250
+ result * amee_category.alternative_unit_conversion_factor(self.units)
251
+ else
252
+ result
253
+ end
254
+ end
255
+
256
+ def get_units
257
+ if amee_category.has_alternative_unit?(self.units)
258
+ amee_category.alternative_unit_converts_to(self.units).to_s
259
+ else
260
+ self.units
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,162 @@
1
+ require 'amee_rails_layer/unit'
2
+
3
+ # Encapsulates information on the AMEE category used to store data against. For example, in an
4
+ # application modelling Car Trips the model would have an AmeeCategory representing the chosen Car
5
+ # Category in AMEE. In an application with a more generic Journey model with multiple possible
6
+ # types, the Journey model would have an AmeeCategory for each type that Journey could take - ie
7
+ # Car, Bus, Van... In all cases the model will need the amee_category method to return the correct
8
+ # AmeeCategory object as described in AmeeCarbonStore. See the README for more information on the
9
+ # application structure this gem assumes and examples of how to use it.
10
+ #
11
+ # There are several types the AmeeCategory can be constructed with:
12
+ # * :distance - the total distance (units available are km or mile)
13
+ # * :journey_distance - the distance of the journey (units available are km or miles)
14
+ # * :weight - the total weight (units available are kg or tonned)
15
+ # * :energy - the energy consumed (units available are kWh)
16
+ # * :volumable_energy - the energy consumed specified either as a volume of fuel or energy unit
17
+ # (units available are litres or kWh)
18
+ #
19
+ # The CATEGORY_TYPES constant maps these symbols to the corresponding field names used in amee to
20
+ # store the amount of whatever is being specified. So for a given data item path you must ensure
21
+ # the item value supports the field named there. For example: assuming we've gone with
22
+ # :journey_distance as the type and a path of /transport/bus/generic/defra then this would work as
23
+ # the amount of miles is specified in the distancePerJourney field in amee. For
24
+ # /transport/car/generic/defra/bysize however it would not work as the only available field in amee
25
+ # is distance (so the type :distance must be used).
26
+ #
27
+ # The type also determines the units that instances of the model can be created with. The available
28
+ # units are specified in the constant CATEGORY_TYPES_TO_UNITS but are also listed in the bullets
29
+ # above above for convience. Developers can provide additional units by (manually) passing in a
30
+ # unit conversion factor in the constructor. See the constructor documentation for more on how to
31
+ # do this.
32
+ class AmeeCategory
33
+
34
+ attr_accessor :name, :path
35
+
36
+ CATEGORY_TYPES = {
37
+ :distance => [:distance],
38
+ :journey_distance => [:distancePerJourney],
39
+ :weight => [:mass],
40
+ :energy => [:energyConsumption],
41
+ :volumable_energy => [:volumePerTime, :energyConsumption]
42
+ }
43
+
44
+ CATEGORY_TYPES_TO_UNITS = {
45
+ :distance => [Unit.km, Unit.miles],
46
+ :distancePerJourney => [Unit.km, Unit.miles],
47
+ :mass => [Unit.kg, Unit.tonnes],
48
+ :energyConsumption => [Unit.kwh],
49
+ :volumePerTime => [Unit.litres],
50
+ }
51
+
52
+ # Create an AmeeCategory. The initializer takes the following parameters:
53
+ # * name - a human readable name to refer to the category by. This is not used in the storing or
54
+ # retrieving of data in amee but is useful for exposing in the view where a user chooses the
55
+ # type they would like for the model
56
+ # * category_type - either :distance, :journey_distance, :weight, :energy or :volumable_energy. See
57
+ # notes in class header for more on this
58
+ # * profile_category_path - the path to the amee category - eg "/transport/car/generic/defra/bysize"
59
+ # * options - any additional options required with the profile_category_path to make the path
60
+ # refer to just one amee categorgy (typically these are passed in as a query string URL in amee
61
+ # explorer). For example: {:fuel => "average", :size => "average"}
62
+ #
63
+ # This option can also take an optional hash for unit conversions, for example:
64
+ # :unit_conversions => {:kg => [:m3 => 2.5, :abc => 0.3], :xyz => [:efg => 0.6]}
65
+ # which would make m3 available as a unit (converted to kg by * 2.5). The hash keys, :kg and
66
+ # :xyz in this case, must map to the unit types provided by the corresponding category_type
67
+ # option - ie :kg would work if this option was :weight but not :energy
68
+ def initialize(name, category_type, profile_category_path, options = {}, *args)
69
+ @name = name
70
+ @category_type = category_type
71
+ @path = profile_category_path
72
+ @conversions = options.delete(:unit_conversions)
73
+ @path_options = options
74
+ end
75
+
76
+ # The drill down path as derived from the path and options arguments in the constructor
77
+ def drill_down_path
78
+ "/data#{@path}/drill?#{@path_options.to_query}"
79
+ end
80
+
81
+ # Returns an array of the available unit names and amee api unit string representations. This
82
+ # will also include any units provided by the user through the unit_conversions option. The
83
+ # resulting array can be passed straight through to a options_for_select view helper.
84
+ #
85
+ # For example if the instance is constructed with the :weight category_type option then this
86
+ # will produce: [["kg", "kg"], ["tonnes", "t"]]
87
+ def unit_options
88
+ unit_options = category_units.map{|unit| [unit.name, unit.amee_api_unit]}
89
+ unit_options += alternative_unit_options if has_alternative_units?
90
+ unit_options
91
+ end
92
+
93
+ # Given an AMEE API unit string return the category type
94
+ def category_type_from_amee_api_unit(amee_unit)
95
+ category_types.each do |type|
96
+ return type if amee_api_units(type).include?(amee_unit)
97
+ end
98
+ return nil
99
+ end
100
+
101
+ # For a given unit returns true if the passed unit is an alternative one - ie conversion
102
+ # factor supplied by user in the constructor
103
+ def has_alternative_unit?(unit)
104
+ return false unless has_alternative_units?
105
+ units = @conversions.values.map(&:first).map(&:keys).flatten
106
+ units.include?(unit.to_sym)
107
+ end
108
+
109
+ # Given an alternative unit, returns the units it converts to
110
+ def alternative_unit_converts_to(unit_name)
111
+ units_to_alternates = merge_hashes(@conversions.map {|k,v| {k => v.first.keys}})
112
+ units_to_alternates.each do |amee_unit, alt_units|
113
+ return amee_unit if alt_units.include?(unit_name.to_sym)
114
+ end
115
+ return nil
116
+ end
117
+
118
+ # Given an alternative unit, returns the factor needed to convert it to the unit it
119
+ # can be derived from
120
+ def alternative_unit_conversion_factor(unit_name)
121
+ alternative_units_to_conversions.each do |alt_unit, conversion|
122
+ return conversion if alt_unit == unit_name.to_sym
123
+ end
124
+ return nil
125
+ end
126
+
127
+ private
128
+ def category_types
129
+ CATEGORY_TYPES[@category_type]
130
+ end
131
+
132
+ def category_units
133
+ category_types.map{|t| CATEGORY_TYPES_TO_UNITS[t]}.flatten
134
+ end
135
+
136
+ def has_alternative_units?
137
+ !@conversions.nil?
138
+ end
139
+
140
+ def amee_api_units(name)
141
+ CATEGORY_TYPES_TO_UNITS[name].map{|u| u.amee_api_unit}
142
+ end
143
+
144
+ def alternative_units_to_conversions
145
+ merge_hashes(@conversions.map {|k,v| v.first})
146
+ end
147
+
148
+ def alternative_unit_options
149
+ alternative_units_to_conversions.keys.map {|unit| [unit.to_s, unit.to_s]}
150
+ end
151
+
152
+ # Does [{:a => 1, :b => 2}, {:c => 3}, {:d => 4}] to {:a => 1, :b => 2, :c => 3, :d => 4}
153
+ def merge_hashes(array_of_hashes)
154
+ result = {}
155
+ array_of_hashes.each do |item|
156
+ item.keys.each do |k|
157
+ result[k] = item[k]
158
+ end
159
+ end
160
+ result
161
+ end
162
+ end
@@ -0,0 +1,77 @@
1
+ # Encapsulates the Units used by the AMEE API. Possible types are currently
2
+ # :km, :miles, :kg, :tonnes, :kwh, :litres and :uk_gallons Convience class
3
+ # methods are provided to construct an object of each of these types
4
+ class Unit
5
+
6
+ NAME = {
7
+ :km => "km",
8
+ :miles => "miles",
9
+ :kg => "kg",
10
+ :tonnes => "tonnes",
11
+ :kwh => "kWh",
12
+ :litres => "litres",
13
+ :uk_gallons => "UK Gallons"
14
+ }
15
+
16
+ AMEE_API_UNITS = {
17
+ :km => "km",
18
+ :miles => "mi",
19
+ :kg => "kg",
20
+ :tonnes => "t",
21
+ :kwh => "kWh",
22
+ :litres => "L",
23
+ :uk_gallons => "gal_uk"
24
+ }
25
+
26
+ # Creates a new Unit object from the symbol representing the unit (see class doc)
27
+ def initialize(type, *args)
28
+ @type = type
29
+ end
30
+
31
+ # Creates a new Unit class from the string used by AMEE to represent the unit. For example
32
+ # pass in "t" to initialize an Unit object for tonnes
33
+ def self.from_amee_unit(unit)
34
+ AMEE_API_UNITS.each do |key, value|
35
+ return new(key) if value == unit
36
+ end
37
+ return nil
38
+ end
39
+
40
+ # A human readable form of the unit
41
+ def name
42
+ NAME[@type]
43
+ end
44
+
45
+ # The string used by the AMEE API to represent the unit
46
+ def amee_api_unit
47
+ AMEE_API_UNITS[@type]
48
+ end
49
+
50
+ def self.km
51
+ new(:km)
52
+ end
53
+
54
+ def self.miles
55
+ new(:miles)
56
+ end
57
+
58
+ def self.kg
59
+ new(:kg)
60
+ end
61
+
62
+ def self.tonnes
63
+ new(:tonnes)
64
+ end
65
+
66
+ def self.kwh
67
+ new(:kwh)
68
+ end
69
+
70
+ def self.litres
71
+ new(:litres)
72
+ end
73
+
74
+ def self.uk_gallons
75
+ new(:uk_gallons)
76
+ end
77
+ end
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'amee'
4
+ require 'amee_rails_layer/amee_category'
5
+ require 'amee_rails_layer/amee_carbon_store'
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ ActiveRecord::Base.class_eval do
2
+ include AmeeCarbonStore
3
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'amee-abstraction-layer-gem'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestAmeeAbstractionLayerGem < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amee_rails_layer
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 3
8
+ - 0
9
+ version: 0.3.0
10
+ platform: ruby
11
+ authors:
12
+ - George Palmer
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-30 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: amee-ruby
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description: We need a longer description of your gem
33
+ email: george.palmer@gmail.com
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files:
39
+ - LICENSE
40
+ - README.rdoc
41
+ files:
42
+ - .document
43
+ - .gitignore
44
+ - LICENSE
45
+ - README.rdoc
46
+ - Rakefile
47
+ - VERSION
48
+ - amee_rails_layer.gemspec
49
+ - lib/amee_rails_layer.rb
50
+ - lib/amee_rails_layer/amee_carbon_store.rb
51
+ - lib/amee_rails_layer/amee_category.rb
52
+ - lib/amee_rails_layer/unit.rb
53
+ - rails/init.rb
54
+ - test/helper.rb
55
+ - test/test_amee-abstraction-layer-gem.rb
56
+ has_rdoc: true
57
+ homepage: http://github.com/georgepalmer/amee-abstraction-layer-gem
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options:
62
+ - --charset=UTF-8
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ requirements: []
80
+
81
+ rubyforge_project:
82
+ rubygems_version: 1.3.6
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: An abstraction layer for building applications around the AMEE API
86
+ test_files:
87
+ - test/helper.rb
88
+ - test/test_amee-abstraction-layer-gem.rb