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 +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +89 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/amee_rails_layer.gemspec +58 -0
- data/lib/amee_rails_layer/amee_carbon_store.rb +264 -0
- data/lib/amee_rails_layer/amee_category.rb +162 -0
- data/lib/amee_rails_layer/unit.rb +77 -0
- data/lib/amee_rails_layer.rb +5 -0
- data/rails/init.rb +3 -0
- data/test/helper.rb +10 -0
- data/test/test_amee-abstraction-layer-gem.rb +7 -0
- metadata +88 -0
data/.document
ADDED
data/.gitignore
ADDED
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
|
data/rails/init.rb
ADDED
data/test/helper.rb
ADDED
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
|