property_sets 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +13 -0
- data/LICENSE +20 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +101 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/property_sets.rb +20 -0
- data/lib/property_sets/active_record_extension.rb +58 -0
- data/lib/property_sets/property_set_helper.rb +19 -0
- data/lib/property_sets/property_set_model.rb +97 -0
- data/test/database.yml +7 -0
- data/test/fixtures/account_settings.yml +14 -0
- data/test/fixtures/account_texts.yml +4 -0
- data/test/fixtures/accounts.yml +3 -0
- data/test/helper.rb +33 -0
- data/test/schema.rb +23 -0
- data/test/test.log +3672 -0
- data/test/test_property_sets.rb +148 -0
- metadata +162 -0
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "shoulda", ">= 0"
|
10
|
+
gem "bundler", "~> 1.0.0"
|
11
|
+
gem "jeweler", "~> 1.5.1"
|
12
|
+
gem "rcov", ">= 0"
|
13
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Morten Primdahl
|
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/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Morten Primdahl
|
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,101 @@
|
|
1
|
+
= Property sets
|
2
|
+
|
3
|
+
The property_set gem is an evolution of the has_settings gem which was an evolution of the features gem. This is getting old.
|
4
|
+
|
5
|
+
This gem is a way for you to use a basic "key/value" store for storing attributes for a given model in a relational fashion where there's a row per attribute. Alternatively you'd need to add a new column per attribute to your main table, or serialize the attributes and their values.
|
6
|
+
|
7
|
+
== Description
|
8
|
+
|
9
|
+
You configure the allowed stored properties by specifying these in an initializer:
|
10
|
+
|
11
|
+
class Account < ActiveRecord::Base
|
12
|
+
property_set :settings do
|
13
|
+
property :version, :default => "v1.0"
|
14
|
+
property :product
|
15
|
+
end
|
16
|
+
|
17
|
+
property_set :texts do
|
18
|
+
property :epilogue
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
The declared properties can then be accessed runtime via the defined association:
|
23
|
+
|
24
|
+
account.settings.ssl # returns the value of the setting
|
25
|
+
account.settings.ssl=(value) # immediately changes the value of the setting
|
26
|
+
account.settings.ssl? # coerces the setting to boolean AR style
|
27
|
+
|
28
|
+
If the value has never been set, a nil (or default) is returned. And that's pretty much it.
|
29
|
+
|
30
|
+
Stored properties can also be updated with the update_attributes and update_attributes! methods.
|
31
|
+
|
32
|
+
# Assuming params include { :account => { :settings => { :version => "v2.0", :forums => '0' }}}
|
33
|
+
# the below would set the respective values on the account
|
34
|
+
account.update_attributes(params[:account])
|
35
|
+
|
36
|
+
We also created a view helper to help you generate the UI for enabling and disabling properties:
|
37
|
+
|
38
|
+
<% form_for(:account, :html => { :method => :put }) do |f| %>
|
39
|
+
<h3>
|
40
|
+
<%= f.property_set_check_box(:settings) :forums %> Enable forums on your account
|
41
|
+
</h3>
|
42
|
+
<% end %>
|
43
|
+
|
44
|
+
== Security
|
45
|
+
|
46
|
+
We support protection from mass updates, the syntax is as follows:
|
47
|
+
|
48
|
+
class Account < ActiveRecord::Base
|
49
|
+
property_set :settings do
|
50
|
+
property :product, :protected => true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
== Installation
|
55
|
+
|
56
|
+
Install the gem in your rails project by putting it in your Gemfile:
|
57
|
+
|
58
|
+
gem 'property_set'
|
59
|
+
|
60
|
+
Also remember to create the storage table, if for example you are going to be using this with an accounts model and have, you can should the table like:
|
61
|
+
|
62
|
+
create_table :account_settings do |t|
|
63
|
+
t.integer :account_id, :null => false
|
64
|
+
t.string :name, :null => false
|
65
|
+
t.string :value
|
66
|
+
t.timestamps
|
67
|
+
end
|
68
|
+
|
69
|
+
add_index :account_settings, [ :account_id, :name ], :unique => true
|
70
|
+
|
71
|
+
== Requirements
|
72
|
+
|
73
|
+
* ActiveRecord
|
74
|
+
* ActionPack
|
75
|
+
|
76
|
+
== LICENSE:
|
77
|
+
|
78
|
+
(The MIT License)
|
79
|
+
|
80
|
+
Copyright (c) 2010 Zendesk
|
81
|
+
|
82
|
+
Permission is hereby granted, free of charge, to any person
|
83
|
+
obtaining a copy of this software and associated documentation
|
84
|
+
files (the "Software"), to deal in the Software without
|
85
|
+
restriction, including without limitation the rights to use,
|
86
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
87
|
+
copies of the Software, and to permit persons to whom the
|
88
|
+
Software is furnished to do so, subject to the following
|
89
|
+
conditions:
|
90
|
+
|
91
|
+
The above copyright notice and this permission notice shall be
|
92
|
+
included in all copies or substantial portions of the Software.
|
93
|
+
|
94
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
95
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
96
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
97
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
98
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
99
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
100
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
101
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
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 = "property_sets"
|
8
|
+
gem.summary = %Q{Property sets for ActiveRecord}
|
9
|
+
gem.description = %Q{This gem is an ActiveRecord extension which provides a convenient interface for managing per row properties}
|
10
|
+
gem.email = "morten@zendesk.com"
|
11
|
+
gem.homepage = "http://github.com/morten/property_sets"
|
12
|
+
gem.authors = ["Morten Primdahl"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 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
|
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 = "property_sets #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.6
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'property_sets/property_set_model'
|
2
|
+
require 'property_sets/active_record_extension'
|
3
|
+
require 'property_sets/property_set_helper'
|
4
|
+
|
5
|
+
module PropertySets
|
6
|
+
def self.ensure_property_set_class(association, owner_class)
|
7
|
+
const_name = "#{owner_class.name}#{association.to_s.singularize.capitalize}".to_sym
|
8
|
+
unless Object.const_defined?(const_name)
|
9
|
+
property_class = Object.const_set(const_name, Class.new(ActiveRecord::Base))
|
10
|
+
property_class.class_eval do
|
11
|
+
include PropertySets::PropertySetModel::InstanceMethods
|
12
|
+
extend PropertySets::PropertySetModel::ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
property_class.owner_class = owner_class
|
16
|
+
property_class.owner_assoc = association
|
17
|
+
end
|
18
|
+
Object.const_get(const_name)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module PropertySets
|
2
|
+
module ActiveRecordExtension
|
3
|
+
module ClassMethods
|
4
|
+
def property_set(association, &block)
|
5
|
+
raise "Invalid association name, letters only" unless association.to_s =~ /[a-z]+/
|
6
|
+
property_class = PropertySets.ensure_property_set_class(association, self)
|
7
|
+
property_class.instance_eval(&block)
|
8
|
+
|
9
|
+
has_many association.to_s.pluralize.to_sym, :class_name => property_class.name, :dependent => :destroy do
|
10
|
+
|
11
|
+
# Accepts a name value pair hash { :name => 'value', :pairs => true } and builds a property for each key
|
12
|
+
def build(property_pairs)
|
13
|
+
property_pairs.keys.each do |name|
|
14
|
+
value = property_pairs[name]
|
15
|
+
self << proxy_reflection.klass.new(:name => name.to_s, :value => value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Define the settings query methods, e.g. +account.settings.wiffle?+
|
20
|
+
property_class.keys.each do |key|
|
21
|
+
raise "Invalid key #{key}" if self.respond_to?(key)
|
22
|
+
|
23
|
+
# Reports the coerced truth valye of the property
|
24
|
+
define_method "#{key}?" do
|
25
|
+
lookup(key).true?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Assigns a new value to the property
|
29
|
+
define_method "#{key}=" do |value|
|
30
|
+
instance = lookup(key)
|
31
|
+
instance.value = value
|
32
|
+
instance.save
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the value of the property
|
36
|
+
define_method "#{key}" do
|
37
|
+
lookup(key)
|
38
|
+
end
|
39
|
+
|
40
|
+
# The finder method which returns the property if present, otherwise a new instance with defaults
|
41
|
+
define_method "lookup" do |key|
|
42
|
+
instance = detect { |property| property.name.to_sym == key }
|
43
|
+
instance ||= property_class.new(@owner.class.name.underscore.to_sym => @owner, :name => key.to_s, :value => property_class.default(key))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.included(receiver)
|
51
|
+
receiver.extend(ClassMethods)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
ActiveRecord::Base.class_eval do
|
57
|
+
include PropertySets::ActiveRecordExtension
|
58
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActionView
|
2
|
+
module Helpers
|
3
|
+
def setting_check_box(model_name, method, options = {}, checked_value = "1", unchecked_value = "0")
|
4
|
+
the_model = @template.instance_variable_get("@#{model_name}")
|
5
|
+
throw "No @#{model_name} in scope" if the_model.nil?
|
6
|
+
throw "The setting_check_box only works on models with settings" unless the_model.respond_to?(:settings)
|
7
|
+
options[:checked] = the_model.settings.send("#{method}?")
|
8
|
+
options[:id] ||= "#{model_name}_settings_#{method}"
|
9
|
+
options[:name] = "#{model_name}[settings][#{method}]"
|
10
|
+
@template.check_box(model_name, "settings_#{method}", options, checked_value, unchecked_value)
|
11
|
+
end
|
12
|
+
|
13
|
+
class FormBuilder
|
14
|
+
def setting_check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
|
15
|
+
@template.setting_check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module PropertySets
|
2
|
+
module PropertySetModel
|
3
|
+
module InstanceMethods
|
4
|
+
def protected?
|
5
|
+
self.class.protected?(self.name.to_sym)
|
6
|
+
end
|
7
|
+
|
8
|
+
def false?
|
9
|
+
[ "false", "0", "", "off", "n" ].member?(value.to_s.downcase)
|
10
|
+
end
|
11
|
+
|
12
|
+
def true?
|
13
|
+
!false?
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
value.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def validate_format_of_name
|
23
|
+
if name.blank?
|
24
|
+
errors.add(:name, :blank)
|
25
|
+
elsif !name.is_a?(String) || name !~ /^([a-z0-9]+_?)+$/
|
26
|
+
errors.add(:name, :invalid)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def owner_class_instance
|
31
|
+
send(self.class.owner_class_sym)
|
32
|
+
end
|
33
|
+
|
34
|
+
def update_owner_timestamp
|
35
|
+
owner_class_instance.update_attribute(:updated_at, Time.now) if owner_class_instance && !owner_class_instance.new_record?
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset_owner_association
|
39
|
+
owner_class_instance.send(self.class.owner_assoc).reload
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module ClassMethods
|
44
|
+
def self.extended(base)
|
45
|
+
base.after_create :reset_owner_association
|
46
|
+
base.after_destroy :reset_owner_association
|
47
|
+
base.validate :validate_format_of_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def property(key, options = nil)
|
51
|
+
@properties ||= {}
|
52
|
+
@properties[key] = options
|
53
|
+
end
|
54
|
+
|
55
|
+
def keys
|
56
|
+
@properties.keys
|
57
|
+
end
|
58
|
+
|
59
|
+
def protected?(key)
|
60
|
+
!!@properties[key][:protected]
|
61
|
+
end
|
62
|
+
|
63
|
+
def default(key)
|
64
|
+
@properties[key] && @properties[key].key?(:default) ? @properties[key][:default] : nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def owner_class=(owner_class)
|
68
|
+
@owner_class_sym = owner_class.name.underscore.to_sym
|
69
|
+
belongs_to owner_class_sym
|
70
|
+
validates_presence_of owner_class_sym
|
71
|
+
validates_uniqueness_of :name, :scope => owner_class_key_sym
|
72
|
+
|
73
|
+
if owner_class.table_exists? && owner_class.column_names.include?("updated_at")
|
74
|
+
before_create :update_owner_timestamp
|
75
|
+
before_destroy :update_owner_timestamp
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def owner_assoc=(association)
|
80
|
+
@owner_assoc = association
|
81
|
+
end
|
82
|
+
|
83
|
+
def owner_assoc
|
84
|
+
@owner_assoc
|
85
|
+
end
|
86
|
+
|
87
|
+
def owner_class_sym
|
88
|
+
@owner_class_sym
|
89
|
+
end
|
90
|
+
|
91
|
+
def owner_class_key_sym
|
92
|
+
"#{owner_class_sym}_id".to_sym
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
data/test/database.yml
ADDED