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 +2 -0
- data/README.rdoc +108 -0
- data/active_restrictors.gemspec +15 -0
- data/lib/active_restrictors/active_restrictor.rb +211 -0
- data/lib/active_restrictors/active_restrictor_views.rb +70 -0
- data/lib/active_restrictors/version.rb +17 -0
- data/lib/active_restrictors.rb +8 -0
- metadata +74 -0
data/CHANGELOG.rdoc
ADDED
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
|
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
|
+
|