rolify 3.1.0 → 3.2.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/.gitignore +2 -1
  2. data/.travis.yml +2 -4
  3. data/CHANGELOG.rdoc +17 -0
  4. data/README.md +42 -6
  5. data/lib/generators/rolify/role/role_generator.rb +11 -3
  6. data/lib/generators/rolify/role/templates/initializer.rb +2 -1
  7. data/lib/generators/rolify/role/templates/role-active_record.rb +2 -0
  8. data/lib/generators/rolify/role/templates/role-mongoid.rb +11 -9
  9. data/lib/rolify.rb +13 -5
  10. data/lib/rolify/adapters/active_record/resource_adapter.rb +11 -4
  11. data/lib/rolify/adapters/active_record/role_adapter.rb +11 -4
  12. data/lib/rolify/adapters/active_record/scopes.rb +27 -0
  13. data/lib/rolify/adapters/base.rb +5 -0
  14. data/lib/rolify/adapters/mongoid/resource_adapter.rb +9 -5
  15. data/lib/rolify/adapters/mongoid/role_adapter.rb +14 -2
  16. data/lib/rolify/adapters/mongoid/scopes.rb +27 -0
  17. data/lib/rolify/finders.rb +39 -0
  18. data/lib/rolify/railtie.rb +1 -1
  19. data/lib/rolify/resource.rb +2 -1
  20. data/lib/rolify/role.rb +12 -2
  21. data/lib/rolify/version.rb +1 -1
  22. data/rolify.gemspec +4 -4
  23. data/spec/generators/rolify/role/role_generator_spec.rb +17 -61
  24. data/spec/rolify/custom_spec.rb +12 -0
  25. data/spec/rolify/resource_spec.rb +31 -0
  26. data/spec/rolify/role_spec.rb +12 -0
  27. data/spec/rolify/shared_contexts.rb +12 -0
  28. data/spec/rolify/shared_examples/shared_examples_for_callbacks.rb +57 -0
  29. data/spec/rolify/shared_examples/shared_examples_for_finders.rb +77 -0
  30. data/spec/rolify/shared_examples/shared_examples_for_roles.rb +39 -27
  31. data/spec/rolify/shared_examples/shared_examples_for_scopes.rb +38 -0
  32. data/spec/spec_helper.rb +13 -0
  33. data/spec/support/adapters/active_record.rb +4 -0
  34. data/spec/support/adapters/mongoid.rb +23 -3
  35. data/spec/support/adapters/mongoid.yml +6 -0
  36. metadata +82 -30
data/.gitignore CHANGED
@@ -3,5 +3,6 @@
3
3
  Gemfile.lock
4
4
  pkg/*
5
5
  tmp/*
6
- logs/*
6
+ log*/*
7
7
  .rbx/*
8
+ .rspec
data/.travis.yml CHANGED
@@ -1,10 +1,8 @@
1
1
  rvm:
2
- - 1.8.7
3
2
  - 1.9.3
4
3
  - ruby-head
5
- - rbx
6
- - jruby
7
- - ree
4
+ - rbx-19mode
5
+ - jruby-19mode
8
6
 
9
7
  env:
10
8
  - ADAPTER=active_record
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,20 @@
1
+ = 3.2 (unreleased yet)
2
+ * <b>DEPRECATION NOTICE:</b>Ruby 1.8 support dropped ! Mongoid 3.0 only supports MRI 1.9.3, and HEAD, and JRuby 1.6.0+ in 1.9 mode
3
+ * removed <tt>dynamic_shortcuts</tt> arguments from the generator
4
+ * to use dynamic shortcuts feature when you're using ActiveRecord, you have to enable it _after_ running rake db:migrate as it relies on the roles table
5
+ * support for Mongoid 3.x (thanks to @Leonas)
6
+ * new class methods on the User class to find users depending on roles they have
7
+ * added scopes to Role class to be able to fetch global, class scoped and instance scoped roles for a specific user
8
+ * deletions of n-n relation are unreliable with Mongoid. Removing ids instead (thanks to @nfo)
9
+ * <tt>has_role?</tt> method now supports new instance (i.e. record not saved in the database yet) (thanks to @demental)
10
+ * added association callbacks <tt>(before|after)_add</tt>, <tt>(before|after)_remove</tt> on <tt>rolify</tt> method (thanks to @shekibobo)
11
+ * added ability to pass an array of roles to <tt>Resource.with_role()</tt>, aliased by <tt>Resource.with_roles()</tt> (thanks to @lukes)
12
+ * added option to have roles be destroyed by default if parent resource is destroyed (thanks to @treydock)
13
+ * better edge cases covering in the specs
14
+ * fixed a bug regarding the loading order of the railtie when using Mongoid ORM and other gems using initializer files (thanks to @stigi)
15
+ * fixed double quote syntax when using MySQL
16
+ * documentation improvement
17
+
1
18
  = 3.1 (Apr 6, 2012)
2
19
  * Mongoid adapter optimization
3
20
  * adapter code refactoring
data/README.md CHANGED
@@ -13,9 +13,10 @@ This library was intended to be used with [CanCan](https://github.com/ryanb/canc
13
13
 
14
14
  ## Requirements
15
15
 
16
- * >= Rails 3.1
17
- * ActiveRecord ORM <b>or</b> Mongoid
18
- * supports ruby 1.8/1.9, REE, JRuby and Rubinius
16
+ * Rails >= 3.1
17
+ * ActiveRecord >= 3.1 <b>or</b> Mongoid >= 3.0
18
+ * supports ruby 1.9, JRuby 1.6.0+ (in 1.9 mode) and Rubinius 2.0.0dev (in 1.9 mode)
19
+ * support of ruby 1.8 has been dropped due to Mongoid 3.0 that only supports 1.9 new hash syntax
19
20
 
20
21
  ## Installation
21
22
 
@@ -54,7 +55,41 @@ Let's migrate !
54
55
  rake db:migrate
55
56
  ```
56
57
 
57
- ### 3. Add a role to a user
58
+ ### 3.1 Configure your user model
59
+
60
+ This gem adds the `rolify` method to your User class. You can also specify optional callbacks* on the user for when roles are added or removed:
61
+
62
+ ```ruby
63
+ class User < ActiveRecord::Base
64
+ rolify :before_add => :before_add_method
65
+
66
+ def :before_add_method(role)
67
+ # do something before it gets added
68
+ end
69
+ end
70
+ ```
71
+
72
+ The `rolify` method accepts the following callback* options:
73
+
74
+ - `before_add`
75
+ - `after_add`
76
+ - `before_remove`
77
+ - `after_remove`
78
+
79
+ *PLEASE NOTE: callbacks are currently only supported using ActiveRecord ORM. Mongoid will support association callbacks in its 3.1 release (Mongoid current version is 3.0.x)
80
+
81
+ ### 3.2 Configure your resource models
82
+
83
+ In the resource models you want to apply roles on, just add ``resourcify`` method.
84
+ For example, on this ActiveRecord class:
85
+
86
+ ```ruby
87
+ class Forum < ActiveRecord::Base
88
+ resourcify
89
+ end
90
+ ```
91
+
92
+ ### 4. Add a role to a user
58
93
 
59
94
  To define a global role:
60
95
 
@@ -79,7 +114,7 @@ To define a role scoped to a resource class
79
114
 
80
115
  That's it !
81
116
 
82
- ### 4. Check roles
117
+ ### 5. Role queries
83
118
 
84
119
  To check if a user has a global role:
85
120
 
@@ -125,7 +160,7 @@ A global role overrides resource role request:
125
160
  => true
126
161
  ```
127
162
 
128
- ### 5. Resource roles querying
163
+ ### 6. Resource roles querying
129
164
 
130
165
  Starting from rolify 3.0, you can search roles on instance level or class level resources.
131
166
 
@@ -160,6 +195,7 @@ Starting from rolify 3.0, you can search roles on instance level or class level
160
195
  * [Wiki](https://github.com/EppO/rolify/wiki)
161
196
  * [Usage](https://github.com/EppO/rolify/wiki/Usage): all the available commands
162
197
  * [Tutorial](https://github.com/EppO/rolify/wiki/Tutorial): how to use [rolify](http://eppo.github.com/rolify) with [Devise](https://github.com/plataformatec/devise) and [CanCan](https://github.com/ryanb/cancan).
198
+ * [Amazing tutorial](http://railsapps.github.com/tutorial-rails-bootstrap-devise-cancan.html) provided by [RailsApps](http://railsapps.github.com/)
163
199
 
164
200
  ## Questions or Problems?
165
201
 
@@ -9,14 +9,14 @@ module Rolify
9
9
  argument :role_cname, :type => :string, :default => "Role"
10
10
  argument :user_cname, :type => :string, :default => "User"
11
11
  argument :orm_adapter, :type => :string, :default => "active_record"
12
- class_option :dynamic_shortcuts, :type => :boolean, :default => false
12
+ #class_option :dynamic_shortcuts, :type => :boolean, :default => false
13
13
 
14
14
  desc "Generates a model with the given NAME and a migration file."
15
15
 
16
16
  def generate_role
17
17
  template "role-#{orm_adapter}.rb", "app/models/#{role_cname.underscore}.rb"
18
- inject_into_class(model_path, user_cname.camelize) do
19
- "\trolify" + (role_cname == "Role" ? "" : ":role_cname => '#{role_cname.camelize}'") + "\n"
18
+ inject_into_file(model_path, :after => inject_rolify_method) do
19
+ " rolify" + (role_cname == "Role" ? "" : " :role_cname => '#{role_cname.camelize}'") + "\n"
20
20
  end
21
21
  end
22
22
 
@@ -36,6 +36,14 @@ module Rolify
36
36
  def show_readme
37
37
  readme "README-#{orm_adapter}" if behavior == :invoke
38
38
  end
39
+
40
+ def inject_rolify_method
41
+ if orm_adapter == "active_record"
42
+ /class #{user_cname.camelize}\n|class #{user_cname.camelize} .*\n/
43
+ else
44
+ /include Mongoid::Document\n|include Mongoid::Document .*\n/
45
+ end
46
+ end
39
47
  end
40
48
  end
41
49
  end
@@ -3,5 +3,6 @@ Rolify.configure do |config|
3
3
  <%= "# " if orm_adapter == "active_record" %>config.use_mongoid
4
4
 
5
5
  # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false
6
- <%= "# " if !options[:dynamic_shortcuts] %>config.use_dynamic_shortcuts
6
+ # Enable this feature _after_ running rake db:migrate as it relies on the roles table
7
+ # config.use_dynamic_shortcuts
7
8
  end
@@ -1,4 +1,6 @@
1
1
  class <%= role_cname.camelize %> < ActiveRecord::Base
2
2
  has_and_belongs_to_many :<%= user_cname.tableize %>, :join_table => :<%= "#{user_cname.tableize}_#{role_cname.tableize}" %>
3
3
  belongs_to :resource, :polymorphic => true
4
+
5
+ scopify
4
6
  end
@@ -5,13 +5,15 @@ class <%= role_cname.camelize %>
5
5
  belongs_to :resource, :polymorphic => true
6
6
 
7
7
  field :name, :type => String
8
- index :name, unique: true
9
- index(
10
- [
11
- [:name, Mongo::ASCENDING],
12
- [:resource_type, Mongo::ASCENDING],
13
- [:resource_id, Mongo::ASCENDING]
14
- ],
15
- unique: true
16
- )
8
+ index({ :name => 1 }, { :unique => true })
9
+
10
+
11
+ index({
12
+ :name => 1,
13
+ :resource_type => 1,
14
+ :resource_id => 1
15
+ },
16
+ { :unique => true})
17
+
18
+ scopify
17
19
  end
data/lib/rolify.rb CHANGED
@@ -11,12 +11,16 @@ module Rolify
11
11
 
12
12
  attr_accessor :role_cname, :adapter
13
13
 
14
- def rolify(options = { :role_cname => 'Role' })
14
+ def rolify(options = {})
15
15
  include Role
16
16
  extend Dynamic if Rolify.dynamic_shortcuts
17
17
 
18
+ options.reverse_merge!({:role_cname => 'Role'})
19
+
18
20
  rolify_options = { :class_name => options[:role_cname].camelize }
19
21
  rolify_options.merge!({ :join_table => "#{self.to_s.tableize}_#{options[:role_cname].tableize}" }) if Rolify.orm == "active_record"
22
+ rolify_options.merge!(options.select{ |k,v| [:before_add, :after_add, :before_remove, :after_remove].include? k.to_sym }) if Rolify.orm == "active_record"
23
+
20
24
  has_and_belongs_to_many :roles, rolify_options
21
25
 
22
26
  self.adapter = Rolify::Adapter::Base.create("role_adapter", options[:role_cname], self.name)
@@ -25,18 +29,22 @@ module Rolify
25
29
  load_dynamic_methods if Rolify.dynamic_shortcuts
26
30
  end
27
31
 
28
- def resourcify(options = { :role_cname => 'Role' })
32
+ def resourcify(options = { :role_cname => 'Role', :dependent => :destroy })
29
33
  include Resource
30
34
 
31
- resourcify_options = { :class_name => options[:role_cname].camelize }
32
- resourcify_options.merge!({ :as => :resource })
35
+ resourcify_options = { :class_name => options[:role_cname].camelize, :as => :resource, :dependent => options[:dependent] }
33
36
  has_many :roles, resourcify_options
34
37
 
35
38
  self.adapter = Rolify::Adapter::Base.create("resource_adapter", options[:role_cname], self.name)
36
39
  self.role_cname = options[:role_cname]
37
40
  end
38
41
 
42
+ def scopify
43
+ require "rolify/adapters/#{Rolify.orm}/scopes.rb"
44
+ extend Rolify::Adapter::Scopes
45
+ end
46
+
39
47
  def role_class
40
48
  self.role_cname.constantize
41
49
  end
42
- end
50
+ end
@@ -4,13 +4,20 @@ module Rolify
4
4
  module Adapter
5
5
  class ResourceAdapter < ResourceAdapterBase
6
6
  def resources_find(roles_table, relation, role_name)
7
- resources = relation.joins("INNER JOIN \"#{roles_table}\" ON \"#{roles_table}\".\"resource_type\" = '#{relation.to_s}'")
8
- resources = resources.where("#{roles_table}.name = ? AND #{roles_table}.resource_type = ?", role_name, relation.to_s)
7
+ resources = relation.joins("INNER JOIN #{quote(roles_table)} ON #{quote(roles_table)}.resource_type = '#{relation.to_s}'")
8
+ resources = resources.where("#{quote(roles_table)}.name IN (?) AND #{quote(roles_table)}.resource_type = ?", Array(role_name), relation.to_s)
9
9
  resources
10
10
  end
11
11
 
12
- def in(relation, roles)
13
- relation.where("#{role_class.to_s.tableize}.id IN (?) AND ((resource_id = #{relation.table_name}.id) OR (resource_id IS NULL))", roles)
12
+ def in(relation, user, role_names)
13
+ roles = user.roles.where(:name => role_names)
14
+ relation.where("#{quote(role_class.to_s.tableize)}.id IN (?) AND ((resource_id = #{quote(relation.table_name)}.id) OR (resource_id IS NULL))", roles)
15
+ end
16
+
17
+ private
18
+
19
+ def quote(column)
20
+ ActiveRecord::Base.connection.quote_column_name column
14
21
  end
15
22
  end
16
23
  end
@@ -32,6 +32,13 @@ module Rolify
32
32
  def exists?(relation, column)
33
33
  relation.where("#{column} IS NOT NULL")
34
34
  end
35
+
36
+ def scope(relation, conditions)
37
+ query = relation.scoped
38
+ query = query.joins(:roles)
39
+ query = where(query, conditions)
40
+ query
41
+ end
35
42
 
36
43
  private
37
44
 
@@ -54,15 +61,15 @@ module Rolify
54
61
  end
55
62
 
56
63
  def build_query(role, resource = nil)
57
- return [ "name = ?", [ role ] ] if resource == :any
58
- query = "((name = ?) AND (resource_type IS NULL) AND (resource_id IS NULL))"
64
+ return [ "#{role_table}.name = ?", [ role ] ] if resource == :any
65
+ query = "((#{role_table}.name = ?) AND (#{role_table}.resource_type IS NULL) AND (#{role_table}.resource_id IS NULL))"
59
66
  values = [ role ]
60
67
  if resource
61
68
  query.insert(0, "(")
62
- query += " OR ((name = ?) AND (resource_type = ?) AND (resource_id IS NULL))"
69
+ query += " OR ((#{role_table}.name = ?) AND (#{role_table}.resource_type = ?) AND (#{role_table}.resource_id IS NULL))"
63
70
  values << role << (resource.is_a?(Class) ? resource.to_s : resource.class.name)
64
71
  if !resource.is_a? Class
65
- query += " OR ((name = ?) AND (resource_type = ?) AND (resource_id = ?))"
72
+ query += " OR ((#{role_table}.name = ?) AND (#{role_table}.resource_type = ?) AND (#{role_table}.resource_id = ?))"
66
73
  values << role << resource.class.name << resource.id
67
74
  end
68
75
  query += ")"
@@ -0,0 +1,27 @@
1
+ module Rolify
2
+ module Adapter
3
+ module Scopes
4
+ def global
5
+ where(:resource_type => nil, :resource_id => nil)
6
+ end
7
+
8
+ def class_scoped(resource_type = nil)
9
+ where_conditions = "resource_type IS NOT NULL AND resource_id IS NULL"
10
+ where_conditions = [ "resource_type = ? AND resource_id IS NULL", resource_type.name ] if resource_type
11
+ where(where_conditions)
12
+ end
13
+
14
+ def instance_scoped(resource_type = nil)
15
+ where_conditions = "resource_type IS NOT NULL AND resource_id IS NOT NULL"
16
+ if resource_type
17
+ if resource_type.is_a? Class
18
+ where_conditions = [ "resource_type = ? AND resource_id IS NOT NULL", resource_type.name ]
19
+ else
20
+ where_conditions = [ "resource_type = ? AND resource_id = ?", resource_type.class.name, resource_type.id ]
21
+ end
22
+ end
23
+ where(where_conditions)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -14,8 +14,13 @@ module Rolify
14
14
  @user_cname.constantize
15
15
  end
16
16
 
17
+ def role_table
18
+ @role_cname.tableize
19
+ end
20
+
17
21
  def self.create(adapter, role_cname, user_cname)
18
22
  load "rolify/adapters/#{Rolify.orm}/#{adapter}.rb"
23
+ load "rolify/adapters/#{Rolify.orm}/scopes.rb"
19
24
  Rolify::Adapter.const_get(adapter.camelize.to_sym).new(role_cname, user_cname)
20
25
  end
21
26
  end
@@ -4,16 +4,20 @@ module Rolify
4
4
  module Adapter
5
5
  class ResourceAdapter < ResourceAdapterBase
6
6
  def resources_find(roles_table, relation, role_name)
7
- roles = roles_table.classify.constantize.where(:name => role_name, :resource_type => relation.to_s)
7
+ roles = roles_table.classify.constantize.where(:name.in => Array(role_name), :resource_type => relation.to_s)
8
8
  resources = []
9
9
  roles.each do |role|
10
- return relation.all if role.resource_id.nil?
11
- resources << role.resource
10
+ if role.resource_id.nil?
11
+ resources += relation.all
12
+ else
13
+ resources << role.resource
14
+ end
12
15
  end
13
- resources
16
+ resources.uniq
14
17
  end
15
18
 
16
- def in(resources, roles)
19
+ def in(resources, user, role_names)
20
+ roles = user.roles.where(:name.in => Array(role_names))
17
21
  return [] if resources.empty? || roles.empty?
18
22
  resources.delete_if { |resource| (resource.applied_roles & roles).empty? }
19
23
  resources
@@ -24,8 +24,13 @@ module Rolify
24
24
  roles.merge!({ :resource_id => resource.id }) if resource && !resource.is_a?(Class)
25
25
  roles_to_remove = relation.roles.where(roles)
26
26
  roles_to_remove.each do |role|
27
- relation.roles.delete(role)
28
- role.reload
27
+ # Deletion in n-n relations is unreliable. Sometimes it works, sometimes not.
28
+ # So, this does not work all the time: `relation.roles.delete(role)`
29
+ # @see http://stackoverflow.com/questions/9132596/rails3-mongoid-many-to-many-relation-and-delete-operation
30
+ # We instead remove ids from the Role object and the relation object.
31
+ relation.role_ids.delete(role.id)
32
+ role.send((user_class.to_s.underscore + '_ids').to_sym).delete(relation.id)
33
+
29
34
  role.destroy if role.send(user_class.to_s.tableize.to_sym).empty?
30
35
  end
31
36
  end
@@ -33,6 +38,13 @@ module Rolify
33
38
  def exists?(relation, column)
34
39
  relation.where(column.to_sym.ne => nil)
35
40
  end
41
+
42
+ def scope(relation, conditions)
43
+ roles = where(role_class, conditions).map { |role| role.id }
44
+ return [] if roles.size.zero?
45
+ query = relation.any_in(:role_ids => roles)
46
+ query
47
+ end
36
48
 
37
49
  private
38
50
 
@@ -0,0 +1,27 @@
1
+ module Rolify
2
+ module Adapter
3
+ module Scopes
4
+ def global
5
+ where(:resource_type => nil, :resource_id => nil)
6
+ end
7
+
8
+ def class_scoped(resource_type = nil)
9
+ where_conditions = { :resource_type.ne => nil, :resource_id => nil }
10
+ where_conditions = { :resource_type => resource_type.name, :resource_id => nil } if resource_type
11
+ where(where_conditions)
12
+ end
13
+
14
+ def instance_scoped(resource_type = nil)
15
+ where_conditions = { :resource_type.ne => nil, :resource_id.ne => nil }
16
+ if resource_type
17
+ if resource_type.is_a? Class
18
+ where_conditions = { :resource_type => resource_type.name, :resource_id.ne => nil }
19
+ else
20
+ where_conditions = { :resource_type => resource_type.class.name, :resource_id => resource_type.id }
21
+ end
22
+ end
23
+ where(where_conditions)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ module Rolify
2
+ module Finders
3
+ def with_role(role_name, resource = nil)
4
+ self.adapter.scope(self, :name => role_name, :resource => resource)
5
+ end
6
+
7
+ def with_all_roles(*args)
8
+ users = []
9
+ args.each do |arg|
10
+ if arg.is_a? Hash
11
+ users_to_add = self.with_role(arg[:name], arg[:resource])
12
+ elsif arg.is_a?(String) || arg.is_a?(Symbol)
13
+ users_to_add = self.with_role(arg)
14
+ else
15
+ raise ArgumentError, "Invalid argument type: only hash or string or symbol allowed"
16
+ end
17
+ users = users_to_add if users.empty?
18
+ users &= users_to_add
19
+ return [] if users.empty?
20
+ end
21
+ users
22
+ end
23
+
24
+ def with_any_role(*args)
25
+ users = []
26
+ args.each do |arg|
27
+ if arg.is_a? Hash
28
+ users_to_add = self.with_role(arg[:name], arg[:resource])
29
+ elsif arg.is_a?(String) || arg.is_a?(Symbol)
30
+ users_to_add = self.with_role(arg)
31
+ else
32
+ raise ArgumentError, "Invalid argument type: only hash or string or symbol allowed"
33
+ end
34
+ users += users_to_add
35
+ end
36
+ users.uniq
37
+ end
38
+ end
39
+ end