rolify 3.1.0 → 3.2.0.rc2

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.
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