amee_rails_layer 0.3.0

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