active_restrictors 0.1.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/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
+