active_restrictors 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,2 @@
1
+ == v0.1.0
2
+ * Initial release
data/README.rdoc ADDED
@@ -0,0 +1,108 @@
1
+ == ActiveRestrictors
2
+
3
+ Chainable ActiveRecord restriction chaining.
4
+
5
+ === Overview
6
+
7
+ Restrictions are made via join tables between two models and a User object. Imagine these models:
8
+
9
+ +-------+ +---------------+ +------------+ +--------------+ +------+
10
+ | Fubar |<*----- FubarPermission|<-----| Permission |--->|UserPermission|-----*>| User |
11
+ +-------+ +---------------+ +------------+ +--------------+ +------+
12
+
13
+ Our model definitions would look something like:
14
+
15
+ class Permission < ActiveRecord::Base
16
+ has_many :fubar_permissions, :dependent => :destroy
17
+ has_many :fubars, :through => :fubar_permissions
18
+ has_many :user_permissions, :dependent => :destroy
19
+ has_many :users, :through => :user_permissions
20
+ end
21
+
22
+ class FubarPermission < ActiveRecord::Base
23
+ belongs_to :fubar
24
+ belongs_to :permission
25
+ end
26
+
27
+ class Fubar < ActiveRecord::Base
28
+ has_many :fubar_permissions, :dependent => :destroy
29
+ has_many :permissions, :through => :fubar_permissions
30
+ end
31
+
32
+ class UserPermission < ActiveRecord::Base
33
+ belongs_to :user
34
+ belongs_to :permission
35
+ end
36
+
37
+ class User < ActiveRecord::Base
38
+ has_many :user_permissions, :dependent => :destroy
39
+ has_many :users, :through => :user_permissions
40
+ end
41
+
42
+ Now, suppose a User should only be allowed to to see a Fubar instance if the Fubar instance and the User both have the same permission assigned to them. We modify Fubar like so:
43
+
44
+ class Fubar < ActiveRecord::Base
45
+ ...
46
+ include ActiveRestrictor
47
+
48
+ add_restrictor(:permissions,
49
+ :enabled => lambda{ User.current_user.fubars_enabled? },
50
+ :value => :name,
51
+ :multiple => true,
52
+ :default_view_all => true,
53
+ :user_values_only => lambda{ User.current_user }
54
+ )
55
+ end
56
+
57
+ A quick overview of what these options are doing.
58
+
59
+ * :enabled -> Restrictor is applied/not applied. This can be a static value or it can be a callable block to allow dynamic enabling
60
+ * :value -> This is the attribute on the Permission model that is displayed to the user
61
+ * :multiple -> Allows multiple Permissions to be applied on the restriction
62
+ * :default_view_all -> If Fubar has no Permissions applied, it is viewable by all
63
+ * :user_values_only -> Only Permissions assigned to the user will be viewable in edit mode
64
+
65
+ With the inclusion of the restrictor, we now have two new methods available. The first is on User instances:
66
+
67
+ User.first.allowed_fubars -> Returns scoping of Fubars the given user instance has access to
68
+
69
+ The second is on Fubar instances:
70
+
71
+ Fubar.first.allowed_users -> Returns scoping of the Users allowed to acces this instance
72
+
73
+ == View Helpers
74
+
75
+ === Details
76
+
77
+ %table
78
+ %tr
79
+ %td= 'Name'
80
+ %td= @fubar.name
81
+ - display_full_restictors(@fubar).each do |pair|
82
+ %tr
83
+ %td= "#{pair.first}:"
84
+ %td= pair.last
85
+
86
+ === Edit
87
+
88
+ - form_for(@fubar) do |f|
89
+ %table
90
+ %tr
91
+ %td= 'Name:'
92
+ %td= f.text_field :name
93
+ - edit_full_restrictors(@fubar, f).each do |pair|
94
+ %tr
95
+ %td= "#{pair.first}:"
96
+ %td= pair.last
97
+
98
+ == Advanced Restrictor
99
+
100
+ === User custom
101
+
102
+ - TODO
103
+
104
+ === Model custom
105
+
106
+ - TODO
107
+
108
+
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
2
+ require 'active_restrictors/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'active_restrictors'
6
+ s.version = ActiveRestrictors::VERSION.to_s
7
+ s.summary = 'Restrictors for Models'
8
+ s.author = 'Chris Roberts'
9
+ s.email = 'chrisroberts.code@gmail.com'
10
+ s.homepage = 'http://bitbucket.org/chrisroberts/active_restrictors'
11
+ s.description = 'Restrictors for Models'
12
+ s.require_path = 'lib'
13
+ s.extra_rdoc_files = ['README.rdoc', 'CHANGELOG.rdoc']
14
+ s.files = %w(README.rdoc CHANGELOG.rdoc active_restrictors.gemspec) + Dir.glob("{lib}/**/*")
15
+ end
@@ -0,0 +1,211 @@
1
+ module ActiveRestrictor
2
+ module ClassMethods
3
+
4
+ # name:: Name of restrictor. For non-basic types this must be the name of the association.
5
+ # opts:: Options hash. Valid options:
6
+ # :type:: Should be set to :basic when applying simple condition only
7
+ # :condition:: Condition string applied to User
8
+ # :class:: Class restriction is based on
9
+ # :value:: Attribute name of value to display
10
+ # :multiple:: Allow user to select multiple items for restriction
11
+ # :include_blank:: Include blank option in restriction selection
12
+ # :user_custom:: Block that returns User scope (passed: User scope, self instance)
13
+ # :user_association:: Name of association on user
14
+ # :model_custom:: Block that returns Model scope (passed: self instance, User instance)
15
+ # :enabled:: If restrictor is enabled. Must be boolean value or a callable object that returns boolean value
16
+ # :default_view_all:: If user has not been assigned a restrictor, they see all unless set to false
17
+ # :user_values_only:: User instance. Set this if you only want values set against user to be selectable
18
+ # Adds restrictions to Model
19
+ # NOTE: Basic type signifies that condition is forced without
20
+ # user interaction. This means no select options will be used
21
+ # as basic restrictors are never seen by the user. Basic restrictors
22
+ # are applied directly against the user model.
23
+ def add_restrictor(name, opts={})
24
+ self.restrictors ||= []
25
+ new_opts = {:name => name, :id => :id, :enabled => true, :type => :full, :include => []}.merge(opts)
26
+ new_opts[:include] = [new_opts[:include]] unless new_opts[:include].is_a?(Array)
27
+ new_opts[:include].push(name) unless new_opts[:include].map(&:to_s).include?(name.to_s)
28
+ self.restrictors.push(new_opts)
29
+ if(new_opts[:type] == :full)
30
+ self.class_eval do
31
+ # This just creates a helper that will grab activerecord
32
+ # instance from passed in IDs for applied restriction
33
+ alias_method "original_#{name}=".to_sym, "#{name}=".to_sym
34
+ define_method("#{name}=") do |args|
35
+ args = (args.is_a?(Array) ? args : Array(args)).find_all{|arg|arg.present?}
36
+ new_args = []
37
+ ids = []
38
+ args.each do |item|
39
+ if(item.is_a?(ActiveRecord::Base) || (defined?(ActiveRecord::Relation) && item.is_a?(ActiveRecord::Relation)))
40
+ new_args << item
41
+ else
42
+ ids << item.to_i
43
+ end
44
+ end
45
+ new_args += new_opts[:class].find(ids) unless ids.empty?
46
+ self.send("original_#{name}=".to_sym, new_args)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # Returns restrictors not of type :basic
53
+ def full_restrictors
54
+ self.restrictors.find_all{|restrictor| restrictor[:type] != :basic && check_enabled(restrictor[:enabled]) == true}
55
+ end
56
+
57
+ # Returns restrictors of type :basic
58
+ def basic_restrictors
59
+ self.restrictors.find_all{|restrictor| restrictor[:type] == :basic && check_enabled(restrictor[:enabled]) == true}
60
+ end
61
+
62
+ # Returns all restrictors that are currently in enabled state
63
+ def enabled_restrictors
64
+ self.restrictors.find_all{|restrictor| check_enabled(restrictor[:enabled]) == true}
65
+ end
66
+
67
+ # hash:: Restrictor hash
68
+ # Provides class of restrictor
69
+ def restrictor_class(hash)
70
+ if(restrictor[:class].present?)
71
+ restrictor[:class]
72
+ else
73
+ self.relect_on_association(restrictor[:name]).klass
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ # arg:: Enabled argument (generally: restrictor[:enabled])
80
+ # Test if enabled is true via value or block evaluation
81
+ def check_enabled(arg)
82
+ if(arg)
83
+ if(arg.respond_to?(:call))
84
+ arg.call
85
+ else
86
+ arg
87
+ end
88
+ else
89
+ false
90
+ end
91
+ end
92
+ end
93
+
94
+ module InstanceMethods
95
+
96
+ # hash:: Restrictor hash
97
+ # Provides class of restrictor
98
+ def restrictor_class(hash)
99
+ self.class.restrictor_class(hash)
100
+ end
101
+
102
+ # Returns restrictors not of type :basic
103
+ def full_restrictors
104
+ self.class.full_restrictors
105
+ end
106
+
107
+ # Returns restrictors of type :basic
108
+ def basic_restrictors
109
+ self.class.basic_restrictors
110
+ end
111
+
112
+ # Returns all restrictors taht are currently in enabled status
113
+ def enabled_restrictors
114
+ self.class.enabled_restrictors
115
+ end
116
+
117
+ # Returns User scope with all restrictors applied
118
+ def allowed_users
119
+ user_scope = User.scoped
120
+ enabled_restrictors.each do |restrictor|
121
+ next if restrictor[:user_custom]
122
+ if(restrictor[:include].is_a?(ActiveRecord::Relation))
123
+ user_scope = user_scope.merge(restrictor[:include])
124
+ elsif(restrictor[:include].present?)
125
+ user_scope = user_scope.joins(restrictor[:include])
126
+ end
127
+ if(restrictor[:condition].is_a?(ActiveRecord::Relation))
128
+ user_scope = user_scope.merge(restrictor[:condition])
129
+ elsif(restrictor[:condition].respond_to?(:call))
130
+ user_scope = user_scope.merge(restrictor[:condition].call)
131
+ elsif(restrictor[:condition].present?)
132
+ user_scope = user_scope.where(restrictor[:condition])
133
+ end
134
+ unless(restrictor[:type] == :basic)
135
+ user_scope = user_scope.where("#{restrictor[:table_name] || restrictor_class(restrictor).table_name}.id IN (#{self.send(restrictor[:name]).scoped.select(:id).to_sql})")
136
+ end
137
+ end
138
+ if((methods = enabled_restrictors.find_all{|res| res[:user_custom]}).size > 0)
139
+ user_scope = methods.inject(user_scope){|result,func| func.call(result, self)}
140
+ end
141
+ user_scope
142
+ end
143
+ end
144
+
145
+ def self.included(klass)
146
+ # Patch up the model we have been called on
147
+ ([klass] + klass.descendants).compact.each do |base|
148
+ cattr_accessor :restrictors
149
+
150
+ extend ClassMethods
151
+ include InstanceMethods
152
+
153
+ scope :allowed_for, lambda{|*args|
154
+ user = args.detect{|item|item.is_a?(User)}
155
+ where("#{table_name}.id IN (#{user.send("allowed_#{base.name.tableize}").select(:id).to_sql})")
156
+ }
157
+ end
158
+ # Patch up the user to provide restricted methods
159
+ ([User] + User.descendants).compact.each do
160
+ # This patches a method onto the User instance to
161
+ # provide access to the allowed instance of the model
162
+ # in use. For example, if the restrictor module is
163
+ # included into the Fubar model, it will
164
+ # provide User#allowed_fubars
165
+ define_method("allowed_#{klass.name.tableize}") do
166
+ # First we perform a basic check against the User to see
167
+ # if this user instance is even allowed by default
168
+ user_scope = User.scoped
169
+ klass.basic_restrictors.each do |restriction|
170
+ if(restriction[:condition].is_a?(ActiveRecord::Relation))
171
+ user_scope = user_scope.merge(restriction[:condition])
172
+ elsif(restriction[:condition].respond_to?(:call))
173
+ user_scope = user_scope.merge(restriction[:condition].call)
174
+ elsif(restriction[:condition].present?)
175
+ user_scope = user_scope.where(restriction[:condition])
176
+ end
177
+ if(restriction[:include].is_a?(ActiveRecord::Relation))
178
+ user_scope = user_scope.merge(restriction[:include])
179
+ elsif(restriction[:include].present?)
180
+ user_scope = user_scope.join(restriction[:include])
181
+ end
182
+ end
183
+ if(user_scope.count > 0)
184
+ scope = klass.scoped
185
+ klass.full_restrictors.each do |restrictor|
186
+ next if restrictor[:include].blank? || restrictor[:model_custom].present?
187
+ rtable_name = restrictor[:table_name] || restrictor_class(restrictor).table_name
188
+ r_scope = self.send(restrictor[:include]).scoped.select("#{rtable_name}.id")
189
+ r_scope.arel.ast.cores.first.projections.delete_at(0) # gets rid of the association_name.* rails insists upon
190
+ if(restrictor[:default_view_all])
191
+ scope = scope.includes(restrictor[:name]) if restrictor[:name].present?
192
+ else
193
+ scope = scope.joins(restrictor[:name]) if restrictor[:name].present?
194
+ end
195
+ scope = scope.where(
196
+ "#{rtable_name}.id IN (#{r_scope.to_sql})#{
197
+ " OR #{rtable_name}.id IS NULL" if restrictor[:default_view_all]
198
+ }"
199
+ )
200
+ if((methods = klass.restrictors.find_all{|res| res[:model_custom]}).size > 0)
201
+ scope = methods.inject(scope){|result,func| func.call(result, self)}
202
+ end
203
+ end
204
+ scope
205
+ else
206
+ klass.where('false')
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,70 @@
1
+ module ActiveRestrictors
2
+ module View
3
+ # obj:: Instance with restrictors enabled
4
+ # val_join:: String to join restictor values together
5
+ # Provides array of enabled restrictors in the form of:
6
+ # [[restrictor_name_label, string_of_restriction_values]]
7
+ def display_full_restrictors(obj, val_join = '<br />')
8
+ if(obj.class.respond_to?(:full_restrictors))
9
+ obj.class.full_restrictors.map do |restrictor|
10
+ [
11
+ label(obj.class.name.camelize, restrictor[:name]),
12
+ obj.send(restrictor[:name]).map(&restrictor[:value].to_sym).join(val_join).html_safe
13
+ ]
14
+ end
15
+ else
16
+ []
17
+ end
18
+ end
19
+
20
+ def display_custom_restrictors(klass)
21
+ # Stub for now
22
+ []
23
+ end
24
+
25
+ # obj:: Instance with restrictors enabled
26
+ # form:: Form object to attach restrictor fields to
27
+ # Provides form items for restrictors in an array of format:
28
+ # [[restrictor_name_label, form_selection_string]]
29
+ def edit_full_restrictors(obj, form)
30
+ if(obj.class.respond_to?(:full_restrictors))
31
+ obj.class.full_restrictors.map do |restrictor|
32
+ if(restrictor[:user_values_only])
33
+ if(restrictor[:user_values_only].respond_to?(:call))
34
+ user = restrictor[:user_values_only].call
35
+ if(user)
36
+ values = user.send(restrictor[:name].to_sym).find(:all, :order => restrictor[:value])
37
+ else
38
+ values = restrictor[:user_values_only].send(restrictor[:name].to_sym).find(:all, :order => restrictor[:value])
39
+ end
40
+ end
41
+ end
42
+ values = restrictor[:class].find(:all, :order => restrictor[:value]) unless values
43
+ [
44
+ form.label(restrictor[:name]),
45
+ form.collection_select(
46
+ restrictor[:name],
47
+ values,
48
+ restrictor[:id],
49
+ restrictor[:value],
50
+ {
51
+ :include_blank => restrictor[:include_blank],
52
+ :selected => Array(obj.send(restrictor[:name])).map(&restrictor[:id].to_sym)
53
+ },
54
+ :multiple => restrictor[:multiple]
55
+ )
56
+ ]
57
+ end
58
+ else
59
+ []
60
+ end
61
+ end
62
+
63
+ def edit_custom_restrictors(klass)
64
+ # Stub for now
65
+ []
66
+ end
67
+ end
68
+ end
69
+
70
+ ActionView::Base.send :include, ActiveRestrictors::View
@@ -0,0 +1,17 @@
1
+ module ActiveRestrictors
2
+ class Version
3
+
4
+ attr_reader :major, :minor, :tiny
5
+
6
+ def initialize(version)
7
+ version = version.split('.')
8
+ @major, @minor, @tiny = [version[0].to_i, version[1].to_i, version[2].to_i]
9
+ end
10
+
11
+ def to_s
12
+ "#{@major}.#{@minor}.#{@tiny}"
13
+ end
14
+ end
15
+
16
+ VERSION = Version.new('0.1.0')
17
+ end
@@ -0,0 +1,8 @@
1
+ require 'active_restrictors/version'
2
+
3
+ if(defined?(ActiveRecord::Relation))
4
+ require 'active_restrictors/active_restrictor'
5
+ end
6
+ if(defined?(ActionView))
7
+ require 'active_restrictors/active_restrictor_views'
8
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_restrictors
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Chris Roberts
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-01-02 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Restrictors for Models
23
+ email: chrisroberts.code@gmail.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README.rdoc
30
+ - CHANGELOG.rdoc
31
+ files:
32
+ - README.rdoc
33
+ - CHANGELOG.rdoc
34
+ - active_restrictors.gemspec
35
+ - lib/active_restrictors/active_restrictor_views.rb
36
+ - lib/active_restrictors/version.rb
37
+ - lib/active_restrictors/active_restrictor.rb
38
+ - lib/active_restrictors.rb
39
+ has_rdoc: true
40
+ homepage: http://bitbucket.org/chrisroberts/active_restrictors
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ hash: 3
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.4.2
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Restrictors for Models
73
+ test_files: []
74
+