##// END OF EJS Templates
Ability to limit member management to certain roles (#19707)....
Jean-Philippe Lang -
r13911:ed9f00178c65
parent child
Show More
@@ -0,0 +1,5
1 class AddRolesAllRolesManaged < ActiveRecord::Migration
2 def change
3 add_column :roles, :all_roles_managed, :boolean, :default => true, :null => false
4 end
5 end
@@ -0,0 +1,8
1 class CreateRolesManagedRoles < ActiveRecord::Migration
2 def change
3 create_table :roles_managed_roles, :id => false do |t|
4 t.integer :role_id, :null => false
5 t.integer :managed_role_id, :null => false
6 end
7 end
8 end
@@ -0,0 +1,5
1 class AddUniqueIndexOnRolesManagedRoles < ActiveRecord::Migration
2 def change
3 add_index :roles_managed_roles, [:role_id, :managed_role_id], :unique => true
4 end
5 end
@@ -1,131 +1,129
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MembersController < ApplicationController
18 class MembersController < ApplicationController
19 model_object Member
19 model_object Member
20 before_filter :find_model_object, :except => [:index, :new, :create, :autocomplete]
20 before_filter :find_model_object, :except => [:index, :new, :create, :autocomplete]
21 before_filter :find_project_from_association, :except => [:index, :new, :create, :autocomplete]
21 before_filter :find_project_from_association, :except => [:index, :new, :create, :autocomplete]
22 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :autocomplete]
22 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :autocomplete]
23 before_filter :authorize
23 before_filter :authorize
24 accept_api_auth :index, :show, :create, :update, :destroy
24 accept_api_auth :index, :show, :create, :update, :destroy
25
25
26 def index
26 def index
27 @offset, @limit = api_offset_and_limit
27 @offset, @limit = api_offset_and_limit
28 @member_count = @project.member_principals.count
28 @member_count = @project.member_principals.count
29 @member_pages = Paginator.new @member_count, @limit, params['page']
29 @member_pages = Paginator.new @member_count, @limit, params['page']
30 @offset ||= @member_pages.offset
30 @offset ||= @member_pages.offset
31 @members = @project.member_principals.
31 @members = @project.member_principals.
32 order("#{Member.table_name}.id").
32 order("#{Member.table_name}.id").
33 limit(@limit).
33 limit(@limit).
34 offset(@offset).
34 offset(@offset).
35 to_a
35 to_a
36 respond_to do |format|
36 respond_to do |format|
37 format.html { head 406 }
37 format.html { head 406 }
38 format.api
38 format.api
39 end
39 end
40 end
40 end
41
41
42 def show
42 def show
43 respond_to do |format|
43 respond_to do |format|
44 format.html { head 406 }
44 format.html { head 406 }
45 format.api
45 format.api
46 end
46 end
47 end
47 end
48
48
49 def new
49 def new
50 @member = Member.new
50 @member = Member.new
51 end
51 end
52
52
53 def create
53 def create
54 members = []
54 members = []
55 if params[:membership]
55 if params[:membership]
56 if params[:membership][:user_ids]
56 user_ids = Array.wrap(params[:membership][:user_id] || params[:membership][:user_ids])
57 attrs = params[:membership].dup
57 user_ids << nil if user_ids.empty?
58 user_ids = attrs.delete(:user_ids)
59 user_ids.each do |user_id|
58 user_ids.each do |user_id|
60 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => user_id)
59 member = Member.new(:project => @project, :user_id => user_id)
61 end
60 member.set_editable_role_ids(params[:membership][:role_ids])
62 else
61 members << member
63 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id])
64 end
62 end
65 @project.members << members
63 @project.members << members
66 end
64 end
67
65
68 respond_to do |format|
66 respond_to do |format|
69 format.html { redirect_to_settings_in_projects }
67 format.html { redirect_to_settings_in_projects }
70 format.js {
68 format.js {
71 @members = members
69 @members = members
72 @member = Member.new
70 @member = Member.new
73 }
71 }
74 format.api {
72 format.api {
75 @member = members.first
73 @member = members.first
76 if @member.valid?
74 if @member.valid?
77 render :action => 'show', :status => :created, :location => membership_url(@member)
75 render :action => 'show', :status => :created, :location => membership_url(@member)
78 else
76 else
79 render_validation_errors(@member)
77 render_validation_errors(@member)
80 end
78 end
81 }
79 }
82 end
80 end
83 end
81 end
84
82
85 def update
83 def update
86 if params[:membership]
84 if params[:membership]
87 @member.role_ids = params[:membership][:role_ids]
85 @member.set_editable_role_ids(params[:membership][:role_ids])
88 end
86 end
89 saved = @member.save
87 saved = @member.save
90 respond_to do |format|
88 respond_to do |format|
91 format.html { redirect_to_settings_in_projects }
89 format.html { redirect_to_settings_in_projects }
92 format.js
90 format.js
93 format.api {
91 format.api {
94 if saved
92 if saved
95 render_api_ok
93 render_api_ok
96 else
94 else
97 render_validation_errors(@member)
95 render_validation_errors(@member)
98 end
96 end
99 }
97 }
100 end
98 end
101 end
99 end
102
100
103 def destroy
101 def destroy
104 if request.delete? && @member.deletable?
102 if @member.deletable?
105 @member.destroy
103 @member.destroy
106 end
104 end
107 respond_to do |format|
105 respond_to do |format|
108 format.html { redirect_to_settings_in_projects }
106 format.html { redirect_to_settings_in_projects }
109 format.js
107 format.js
110 format.api {
108 format.api {
111 if @member.destroyed?
109 if @member.destroyed?
112 render_api_ok
110 render_api_ok
113 else
111 else
114 head :unprocessable_entity
112 head :unprocessable_entity
115 end
113 end
116 }
114 }
117 end
115 end
118 end
116 end
119
117
120 def autocomplete
118 def autocomplete
121 respond_to do |format|
119 respond_to do |format|
122 format.js
120 format.js
123 end
121 end
124 end
122 end
125
123
126 private
124 private
127
125
128 def redirect_to_settings_in_projects
126 def redirect_to_settings_in_projects
129 redirect_to settings_project_path(@project, :tab => 'members')
127 redirect_to settings_project_path(@project, :tab => 'members')
130 end
128 end
131 end
129 end
@@ -1,139 +1,196
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Member < ActiveRecord::Base
18 class Member < ActiveRecord::Base
19 belongs_to :user
19 belongs_to :user
20 belongs_to :principal, :foreign_key => 'user_id'
20 belongs_to :principal, :foreign_key => 'user_id'
21 has_many :member_roles, :dependent => :destroy
21 has_many :member_roles, :dependent => :destroy
22 has_many :roles, lambda {uniq}, :through => :member_roles
22 has_many :roles, lambda {uniq}, :through => :member_roles
23 belongs_to :project
23 belongs_to :project
24
24
25 validates_presence_of :principal, :project
25 validates_presence_of :principal, :project
26 validates_uniqueness_of :user_id, :scope => :project_id
26 validates_uniqueness_of :user_id, :scope => :project_id
27 validate :validate_role
27 validate :validate_role
28 attr_protected :id
28 attr_protected :id
29
29
30 before_destroy :set_issue_category_nil
30 before_destroy :set_issue_category_nil
31
31
32 alias :base_reload :reload
33 def reload(*args)
34 @managed_roles = nil
35 base_reload(*args)
36 end
37
32 def role
38 def role
33 end
39 end
34
40
35 def role=
41 def role=
36 end
42 end
37
43
38 def name
44 def name
39 self.user.name
45 self.user.name
40 end
46 end
41
47
42 alias :base_role_ids= :role_ids=
48 alias :base_role_ids= :role_ids=
43 def role_ids=(arg)
49 def role_ids=(arg)
44 ids = (arg || []).collect(&:to_i) - [0]
50 ids = (arg || []).collect(&:to_i) - [0]
45 # Keep inherited roles
51 # Keep inherited roles
46 ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
52 ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
47
53
48 new_role_ids = ids - role_ids
54 new_role_ids = ids - role_ids
49 # Add new roles
55 # Add new roles
50 new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id, :member => self) }
56 new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id, :member => self) }
51 # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
57 # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
52 member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)}
58 member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)}
53 if member_roles_to_destroy.any?
59 if member_roles_to_destroy.any?
54 member_roles_to_destroy.each(&:destroy)
60 member_roles_to_destroy.each(&:destroy)
55 end
61 end
56 end
62 end
57
63
58 def <=>(member)
64 def <=>(member)
59 a, b = roles.sort.first, member.roles.sort.first
65 a, b = roles.sort.first, member.roles.sort.first
60 if a == b
66 if a == b
61 if principal
67 if principal
62 principal <=> member.principal
68 principal <=> member.principal
63 else
69 else
64 1
70 1
65 end
71 end
66 elsif a
72 elsif a
67 a <=> b
73 a <=> b
68 else
74 else
69 1
75 1
70 end
76 end
71 end
77 end
72
78
73 def deletable?
79 # Set member role ids ignoring any change to roles that
74 member_roles.detect {|mr| mr.inherited_from}.nil?
80 # user is not allowed to manage
81 def set_editable_role_ids(ids, user=User.current)
82 ids = (ids || []).collect(&:to_i) - [0]
83 editable_role_ids = user.managed_roles(project).map(&:id)
84 untouched_role_ids = self.role_ids - editable_role_ids
85 touched_role_ids = ids & editable_role_ids
86 self.role_ids = untouched_role_ids + touched_role_ids
75 end
87 end
76
88
77 def destroy
89 # Returns true if one of the member roles is inherited
78 if member_roles.reload.present?
90 def any_inherited_role?
79 # destroying the last role will destroy another instance
91 member_roles.any? {|mr| mr.inherited_from}
80 # of the same Member record, #super would then trigger callbacks twice
92 end
81 member_roles.destroy_all
93
82 @destroyed = true
94 # Returns true if the member has the role and if it's inherited
83 freeze
95 def has_inherited_role?(role)
96 member_roles.any? {|mr| mr.role_id == role.id && mr.inherited_from.present?}
97 end
98
99 # Returns true if the member's role is editable by user
100 def role_editable?(role, user=User.current)
101 if has_inherited_role?(role)
102 false
84 else
103 else
85 super
104 user.managed_roles(project).include?(role)
105 end
106 end
107
108 # Returns true if the member is deletable by user
109 def deletable?(user=User.current)
110 if any_inherited_role?
111 false
112 else
113 roles & user.managed_roles(project) == roles
86 end
114 end
87 end
115 end
88
116
117 # Destroys the member
118 def destroy
119 member_roles.reload.each(&:destroy_without_member_removal)
120 super
121 end
122
123 # Returns true if the member is user or is a group
124 # that includes user
89 def include?(user)
125 def include?(user)
90 if principal.is_a?(Group)
126 if principal.is_a?(Group)
91 !user.nil? && user.groups.include?(principal)
127 !user.nil? && user.groups.include?(principal)
92 else
128 else
93 self.user == user
129 self.user == user
94 end
130 end
95 end
131 end
96
132
97 def set_issue_category_nil
133 def set_issue_category_nil
98 if user_id && project_id
134 if user_id && project_id
99 # remove category based auto assignments for this member
135 # remove category based auto assignments for this member
100 IssueCategory.where(["project_id = ? AND assigned_to_id = ?", project_id, user_id]).
136 IssueCategory.where(["project_id = ? AND assigned_to_id = ?", project_id, user_id]).
101 update_all("assigned_to_id = NULL")
137 update_all("assigned_to_id = NULL")
102 end
138 end
103 end
139 end
104
140
141 # Returns the roles that the member is allowed to manage
142 # in the project the member belongs to
143 def managed_roles
144 @managed_roles ||= begin
145 if principal.try(:admin?)
146 Role.givable.to_a
147 else
148 members_management_roles = roles.select do |role|
149 role.has_permission?(:manage_members)
150 end
151 if members_management_roles.empty?
152 []
153 elsif members_management_roles.any?(&:all_roles_managed?)
154 Role.givable.to_a
155 else
156 members_management_roles.map(&:managed_roles).reduce(&:|)
157 end
158 end
159 end
160 end
161
105 # Creates memberships for principal with the attributes
162 # Creates memberships for principal with the attributes
106 # * project_ids : one or more project ids
163 # * project_ids : one or more project ids
107 # * role_ids : ids of the roles to give to each membership
164 # * role_ids : ids of the roles to give to each membership
108 #
165 #
109 # Example:
166 # Example:
110 # Member.create_principal_memberships(user, :project_ids => [2, 5], :role_ids => [1, 3]
167 # Member.create_principal_memberships(user, :project_ids => [2, 5], :role_ids => [1, 3]
111 def self.create_principal_memberships(principal, attributes)
168 def self.create_principal_memberships(principal, attributes)
112 members = []
169 members = []
113 if attributes
170 if attributes
114 project_ids = Array.wrap(attributes[:project_ids] || attributes[:project_id])
171 project_ids = Array.wrap(attributes[:project_ids] || attributes[:project_id])
115 role_ids = attributes[:role_ids]
172 role_ids = attributes[:role_ids]
116 project_ids.each do |project_id|
173 project_ids.each do |project_id|
117 members << Member.new(:principal => principal, :role_ids => role_ids, :project_id => project_id)
174 members << Member.new(:principal => principal, :role_ids => role_ids, :project_id => project_id)
118 end
175 end
119 principal.members << members
176 principal.members << members
120 end
177 end
121 members
178 members
122 end
179 end
123
180
124 # Finds or initilizes a Member for the given project and principal
181 # Finds or initilizes a Member for the given project and principal
125 def self.find_or_new(project, principal)
182 def self.find_or_new(project, principal)
126 project_id = project.is_a?(Project) ? project.id : project
183 project_id = project.is_a?(Project) ? project.id : project
127 principal_id = principal.is_a?(Principal) ? principal.id : principal
184 principal_id = principal.is_a?(Principal) ? principal.id : principal
128
185
129 member = Member.find_by_project_id_and_user_id(project_id, principal_id)
186 member = Member.find_by_project_id_and_user_id(project_id, principal_id)
130 member ||= Member.new(:project_id => project_id, :user_id => principal_id)
187 member ||= Member.new(:project_id => project_id, :user_id => principal_id)
131 member
188 member
132 end
189 end
133
190
134 protected
191 protected
135
192
136 def validate_role
193 def validate_role
137 errors.add_on_empty :role if member_roles.empty? && roles.empty?
194 errors.add_on_empty :role if member_roles.empty? && roles.empty?
138 end
195 end
139 end
196 end
@@ -1,73 +1,80
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MemberRole < ActiveRecord::Base
18 class MemberRole < ActiveRecord::Base
19 belongs_to :member
19 belongs_to :member
20 belongs_to :role
20 belongs_to :role
21
21
22 after_destroy :remove_member_if_empty
22 after_destroy :remove_member_if_empty
23
23
24 after_create :add_role_to_group_users, :add_role_to_subprojects
24 after_create :add_role_to_group_users, :add_role_to_subprojects
25 after_destroy :remove_inherited_roles
25 after_destroy :remove_inherited_roles
26
26
27 validates_presence_of :role
27 validates_presence_of :role
28 validate :validate_role_member
28 validate :validate_role_member
29 attr_protected :id
29 attr_protected :id
30
30
31 def validate_role_member
31 def validate_role_member
32 errors.add :role_id, :invalid if role && !role.member?
32 errors.add :role_id, :invalid if role && !role.member?
33 end
33 end
34
34
35 def inherited?
35 def inherited?
36 !inherited_from.nil?
36 !inherited_from.nil?
37 end
37 end
38
38
39 # Destroys the MemberRole without destroying its Member if it doesn't have
40 # any other roles
41 def destroy_without_member_removal
42 @member_removal = false
43 destroy
44 end
45
39 private
46 private
40
47
41 def remove_member_if_empty
48 def remove_member_if_empty
42 if member.roles.empty?
49 if @member_removal != false && member.roles.empty?
43 member.destroy
50 member.destroy
44 end
51 end
45 end
52 end
46
53
47 def add_role_to_group_users
54 def add_role_to_group_users
48 if member.principal.is_a?(Group) && !inherited?
55 if member.principal.is_a?(Group) && !inherited?
49 member.principal.users.each do |user|
56 member.principal.users.each do |user|
50 user_member = Member.find_or_new(member.project_id, user.id)
57 user_member = Member.find_or_new(member.project_id, user.id)
51 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
58 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
52 user_member.save!
59 user_member.save!
53 end
60 end
54 end
61 end
55 end
62 end
56
63
57 def add_role_to_subprojects
64 def add_role_to_subprojects
58 member.project.children.each do |subproject|
65 member.project.children.each do |subproject|
59 if subproject.inherit_members?
66 if subproject.inherit_members?
60 child_member = Member.find_or_new(subproject.id, member.user_id)
67 child_member = Member.find_or_new(subproject.id, member.user_id)
61 child_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
68 child_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
62 child_member.save!
69 child_member.save!
63 end
70 end
64 end
71 end
65 end
72 end
66
73
67 def remove_inherited_roles
74 def remove_inherited_roles
68 MemberRole.where(:inherited_from => id).group_by(&:member).
75 MemberRole.where(:inherited_from => id).group_by(&:member).
69 each do |member, member_roles|
76 each do |member, member_roles|
70 member_roles.each(&:destroy)
77 member_roles.each(&:destroy)
71 end
78 end
72 end
79 end
73 end
80 end
@@ -1,229 +1,233
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Role < ActiveRecord::Base
18 class Role < ActiveRecord::Base
19 # Custom coder for the permissions attribute that should be an
19 # Custom coder for the permissions attribute that should be an
20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
21 # slow on some platforms (eg. mingw32).
21 # slow on some platforms (eg. mingw32).
22 class PermissionsAttributeCoder
22 class PermissionsAttributeCoder
23 def self.load(str)
23 def self.load(str)
24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
25 end
25 end
26
26
27 def self.dump(value)
27 def self.dump(value)
28 YAML.dump(value)
28 YAML.dump(value)
29 end
29 end
30 end
30 end
31
31
32 # Built-in roles
32 # Built-in roles
33 BUILTIN_NON_MEMBER = 1
33 BUILTIN_NON_MEMBER = 1
34 BUILTIN_ANONYMOUS = 2
34 BUILTIN_ANONYMOUS = 2
35
35
36 ISSUES_VISIBILITY_OPTIONS = [
36 ISSUES_VISIBILITY_OPTIONS = [
37 ['all', :label_issues_visibility_all],
37 ['all', :label_issues_visibility_all],
38 ['default', :label_issues_visibility_public],
38 ['default', :label_issues_visibility_public],
39 ['own', :label_issues_visibility_own]
39 ['own', :label_issues_visibility_own]
40 ]
40 ]
41
41
42 TIME_ENTRIES_VISIBILITY_OPTIONS = [
42 TIME_ENTRIES_VISIBILITY_OPTIONS = [
43 ['all', :label_time_entries_visibility_all],
43 ['all', :label_time_entries_visibility_all],
44 ['own', :label_time_entries_visibility_own]
44 ['own', :label_time_entries_visibility_own]
45 ]
45 ]
46
46
47 USERS_VISIBILITY_OPTIONS = [
47 USERS_VISIBILITY_OPTIONS = [
48 ['all', :label_users_visibility_all],
48 ['all', :label_users_visibility_all],
49 ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects]
49 ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects]
50 ]
50 ]
51
51
52 scope :sorted, lambda { order(:builtin, :position) }
52 scope :sorted, lambda { order(:builtin, :position) }
53 scope :givable, lambda { order(:position).where(:builtin => 0) }
53 scope :givable, lambda { order(:position).where(:builtin => 0) }
54 scope :builtin, lambda { |*args|
54 scope :builtin, lambda { |*args|
55 compare = (args.first == true ? 'not' : '')
55 compare = (args.first == true ? 'not' : '')
56 where("#{compare} builtin = 0")
56 where("#{compare} builtin = 0")
57 }
57 }
58
58
59 before_destroy :check_deletable
59 before_destroy :check_deletable
60 has_many :workflow_rules, :dependent => :delete_all do
60 has_many :workflow_rules, :dependent => :delete_all do
61 def copy(source_role)
61 def copy(source_role)
62 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
62 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
63 end
63 end
64 end
64 end
65 has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
65 has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
66
66
67 has_and_belongs_to_many :managed_roles, :class_name => 'Role',
68 :join_table => "#{table_name_prefix}roles_managed_roles#{table_name_suffix}",
69 :association_foreign_key => "managed_role_id"
70
67 has_many :member_roles, :dependent => :destroy
71 has_many :member_roles, :dependent => :destroy
68 has_many :members, :through => :member_roles
72 has_many :members, :through => :member_roles
69 acts_as_list
73 acts_as_list
70
74
71 serialize :permissions, ::Role::PermissionsAttributeCoder
75 serialize :permissions, ::Role::PermissionsAttributeCoder
72 attr_protected :builtin
76 attr_protected :builtin
73
77
74 validates_presence_of :name
78 validates_presence_of :name
75 validates_uniqueness_of :name
79 validates_uniqueness_of :name
76 validates_length_of :name, :maximum => 30
80 validates_length_of :name, :maximum => 30
77 validates_inclusion_of :issues_visibility,
81 validates_inclusion_of :issues_visibility,
78 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
82 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
79 :if => lambda {|role| role.respond_to?(:issues_visibility) && role.issues_visibility_changed?}
83 :if => lambda {|role| role.respond_to?(:issues_visibility) && role.issues_visibility_changed?}
80 validates_inclusion_of :users_visibility,
84 validates_inclusion_of :users_visibility,
81 :in => USERS_VISIBILITY_OPTIONS.collect(&:first),
85 :in => USERS_VISIBILITY_OPTIONS.collect(&:first),
82 :if => lambda {|role| role.respond_to?(:users_visibility) && role.users_visibility_changed?}
86 :if => lambda {|role| role.respond_to?(:users_visibility) && role.users_visibility_changed?}
83 validates_inclusion_of :time_entries_visibility,
87 validates_inclusion_of :time_entries_visibility,
84 :in => TIME_ENTRIES_VISIBILITY_OPTIONS.collect(&:first),
88 :in => TIME_ENTRIES_VISIBILITY_OPTIONS.collect(&:first),
85 :if => lambda {|role| role.respond_to?(:time_entries_visibility) && role.time_entries_visibility_changed?}
89 :if => lambda {|role| role.respond_to?(:time_entries_visibility) && role.time_entries_visibility_changed?}
86
90
87 # Copies attributes from another role, arg can be an id or a Role
91 # Copies attributes from another role, arg can be an id or a Role
88 def copy_from(arg, options={})
92 def copy_from(arg, options={})
89 return unless arg.present?
93 return unless arg.present?
90 role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
94 role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
91 self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
95 self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
92 self.permissions = role.permissions.dup
96 self.permissions = role.permissions.dup
93 self
97 self
94 end
98 end
95
99
96 def permissions=(perms)
100 def permissions=(perms)
97 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
101 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
98 write_attribute(:permissions, perms)
102 write_attribute(:permissions, perms)
99 end
103 end
100
104
101 def add_permission!(*perms)
105 def add_permission!(*perms)
102 self.permissions = [] unless permissions.is_a?(Array)
106 self.permissions = [] unless permissions.is_a?(Array)
103
107
104 permissions_will_change!
108 permissions_will_change!
105 perms.each do |p|
109 perms.each do |p|
106 p = p.to_sym
110 p = p.to_sym
107 permissions << p unless permissions.include?(p)
111 permissions << p unless permissions.include?(p)
108 end
112 end
109 save!
113 save!
110 end
114 end
111
115
112 def remove_permission!(*perms)
116 def remove_permission!(*perms)
113 return unless permissions.is_a?(Array)
117 return unless permissions.is_a?(Array)
114 permissions_will_change!
118 permissions_will_change!
115 perms.each { |p| permissions.delete(p.to_sym) }
119 perms.each { |p| permissions.delete(p.to_sym) }
116 save!
120 save!
117 end
121 end
118
122
119 # Returns true if the role has the given permission
123 # Returns true if the role has the given permission
120 def has_permission?(perm)
124 def has_permission?(perm)
121 !permissions.nil? && permissions.include?(perm.to_sym)
125 !permissions.nil? && permissions.include?(perm.to_sym)
122 end
126 end
123
127
124 def consider_workflow?
128 def consider_workflow?
125 has_permission?(:add_issues) || has_permission?(:edit_issues)
129 has_permission?(:add_issues) || has_permission?(:edit_issues)
126 end
130 end
127
131
128 def <=>(role)
132 def <=>(role)
129 if role
133 if role
130 if builtin == role.builtin
134 if builtin == role.builtin
131 position <=> role.position
135 position <=> role.position
132 else
136 else
133 builtin <=> role.builtin
137 builtin <=> role.builtin
134 end
138 end
135 else
139 else
136 -1
140 -1
137 end
141 end
138 end
142 end
139
143
140 def to_s
144 def to_s
141 name
145 name
142 end
146 end
143
147
144 def name
148 def name
145 case builtin
149 case builtin
146 when 1; l(:label_role_non_member, :default => read_attribute(:name))
150 when 1; l(:label_role_non_member, :default => read_attribute(:name))
147 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
151 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
148 else; read_attribute(:name)
152 else; read_attribute(:name)
149 end
153 end
150 end
154 end
151
155
152 # Return true if the role is a builtin role
156 # Return true if the role is a builtin role
153 def builtin?
157 def builtin?
154 self.builtin != 0
158 self.builtin != 0
155 end
159 end
156
160
157 # Return true if the role is the anonymous role
161 # Return true if the role is the anonymous role
158 def anonymous?
162 def anonymous?
159 builtin == 2
163 builtin == 2
160 end
164 end
161
165
162 # Return true if the role is a project member role
166 # Return true if the role is a project member role
163 def member?
167 def member?
164 !self.builtin?
168 !self.builtin?
165 end
169 end
166
170
167 # Return true if role is allowed to do the specified action
171 # Return true if role is allowed to do the specified action
168 # action can be:
172 # action can be:
169 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
173 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
170 # * a permission Symbol (eg. :edit_project)
174 # * a permission Symbol (eg. :edit_project)
171 def allowed_to?(action)
175 def allowed_to?(action)
172 if action.is_a? Hash
176 if action.is_a? Hash
173 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
177 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
174 else
178 else
175 allowed_permissions.include? action
179 allowed_permissions.include? action
176 end
180 end
177 end
181 end
178
182
179 # Return all the permissions that can be given to the role
183 # Return all the permissions that can be given to the role
180 def setable_permissions
184 def setable_permissions
181 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
185 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
182 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
186 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
183 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
187 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
184 setable_permissions
188 setable_permissions
185 end
189 end
186
190
187 # Find all the roles that can be given to a project member
191 # Find all the roles that can be given to a project member
188 def self.find_all_givable
192 def self.find_all_givable
189 Role.givable.to_a
193 Role.givable.to_a
190 end
194 end
191
195
192 # Return the builtin 'non member' role. If the role doesn't exist,
196 # Return the builtin 'non member' role. If the role doesn't exist,
193 # it will be created on the fly.
197 # it will be created on the fly.
194 def self.non_member
198 def self.non_member
195 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
199 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
196 end
200 end
197
201
198 # Return the builtin 'anonymous' role. If the role doesn't exist,
202 # Return the builtin 'anonymous' role. If the role doesn't exist,
199 # it will be created on the fly.
203 # it will be created on the fly.
200 def self.anonymous
204 def self.anonymous
201 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
205 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
202 end
206 end
203
207
204 private
208 private
205
209
206 def allowed_permissions
210 def allowed_permissions
207 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
211 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
208 end
212 end
209
213
210 def allowed_actions
214 def allowed_actions
211 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
215 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
212 end
216 end
213
217
214 def check_deletable
218 def check_deletable
215 raise "Cannot delete role" if members.any?
219 raise "Cannot delete role" if members.any?
216 raise "Cannot delete builtin role" if builtin?
220 raise "Cannot delete builtin role" if builtin?
217 end
221 end
218
222
219 def self.find_or_create_system_role(builtin, name)
223 def self.find_or_create_system_role(builtin, name)
220 role = where(:builtin => builtin).first
224 role = where(:builtin => builtin).first
221 if role.nil?
225 if role.nil?
222 role = create(:name => name, :position => 0) do |r|
226 role = create(:name => name, :position => 0) do |r|
223 r.builtin = builtin
227 r.builtin = builtin
224 end
228 end
225 raise "Unable to create the #{name} role." if role.new_record?
229 raise "Unable to create the #{name} role." if role.new_record?
226 end
230 end
227 role
231 role
228 end
232 end
229 end
233 end
@@ -1,852 +1,861
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstinitial_lastname => {
35 :firstinitial_lastname => {
36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 :order => %w(firstname lastname id),
37 :order => %w(firstname lastname id),
38 :setting_order => 2
38 :setting_order => 2
39 },
39 },
40 :firstname => {
40 :firstname => {
41 :string => '#{firstname}',
41 :string => '#{firstname}',
42 :order => %w(firstname id),
42 :order => %w(firstname id),
43 :setting_order => 3
43 :setting_order => 3
44 },
44 },
45 :lastname_firstname => {
45 :lastname_firstname => {
46 :string => '#{lastname} #{firstname}',
46 :string => '#{lastname} #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 4
48 :setting_order => 4
49 },
49 },
50 :lastname_coma_firstname => {
50 :lastname_coma_firstname => {
51 :string => '#{lastname}, #{firstname}',
51 :string => '#{lastname}, #{firstname}',
52 :order => %w(lastname firstname id),
52 :order => %w(lastname firstname id),
53 :setting_order => 5
53 :setting_order => 5
54 },
54 },
55 :lastname => {
55 :lastname => {
56 :string => '#{lastname}',
56 :string => '#{lastname}',
57 :order => %w(lastname id),
57 :order => %w(lastname id),
58 :setting_order => 6
58 :setting_order => 6
59 },
59 },
60 :username => {
60 :username => {
61 :string => '#{login}',
61 :string => '#{login}',
62 :order => %w(login id),
62 :order => %w(login id),
63 :setting_order => 7
63 :setting_order => 7
64 },
64 },
65 }
65 }
66
66
67 MAIL_NOTIFICATION_OPTIONS = [
67 MAIL_NOTIFICATION_OPTIONS = [
68 ['all', :label_user_mail_option_all],
68 ['all', :label_user_mail_option_all],
69 ['selected', :label_user_mail_option_selected],
69 ['selected', :label_user_mail_option_selected],
70 ['only_my_events', :label_user_mail_option_only_my_events],
70 ['only_my_events', :label_user_mail_option_only_my_events],
71 ['only_assigned', :label_user_mail_option_only_assigned],
71 ['only_assigned', :label_user_mail_option_only_assigned],
72 ['only_owner', :label_user_mail_option_only_owner],
72 ['only_owner', :label_user_mail_option_only_owner],
73 ['none', :label_user_mail_option_none]
73 ['none', :label_user_mail_option_none]
74 ]
74 ]
75
75
76 has_and_belongs_to_many :groups,
76 has_and_belongs_to_many :groups,
77 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
77 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
78 :after_add => Proc.new {|user, group| group.user_added(user)},
78 :after_add => Proc.new {|user, group| group.user_added(user)},
79 :after_remove => Proc.new {|user, group| group.user_removed(user)}
79 :after_remove => Proc.new {|user, group| group.user_removed(user)}
80 has_many :changesets, :dependent => :nullify
80 has_many :changesets, :dependent => :nullify
81 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
81 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
82 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
82 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
83 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
83 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
84 has_one :email_address, lambda {where :is_default => true}, :autosave => true
84 has_one :email_address, lambda {where :is_default => true}, :autosave => true
85 has_many :email_addresses, :dependent => :delete_all
85 has_many :email_addresses, :dependent => :delete_all
86 belongs_to :auth_source
86 belongs_to :auth_source
87
87
88 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
88 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
89 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
89 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
90
90
91 acts_as_customizable
91 acts_as_customizable
92
92
93 attr_accessor :password, :password_confirmation, :generate_password
93 attr_accessor :password, :password_confirmation, :generate_password
94 attr_accessor :last_before_login_on
94 attr_accessor :last_before_login_on
95 # Prevents unauthorized assignments
95 # Prevents unauthorized assignments
96 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
96 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
97
97
98 LOGIN_LENGTH_LIMIT = 60
98 LOGIN_LENGTH_LIMIT = 60
99 MAIL_LENGTH_LIMIT = 60
99 MAIL_LENGTH_LIMIT = 60
100
100
101 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
101 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
102 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
102 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
103 # Login must contain letters, numbers, underscores only
103 # Login must contain letters, numbers, underscores only
104 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
104 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
105 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
105 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
106 validates_length_of :firstname, :lastname, :maximum => 30
106 validates_length_of :firstname, :lastname, :maximum => 30
107 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
107 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
108 validate :validate_password_length
108 validate :validate_password_length
109 validate do
109 validate do
110 if password_confirmation && password != password_confirmation
110 if password_confirmation && password != password_confirmation
111 errors.add(:password, :confirmation)
111 errors.add(:password, :confirmation)
112 end
112 end
113 end
113 end
114
114
115 before_validation :instantiate_email_address
115 before_validation :instantiate_email_address
116 before_create :set_mail_notification
116 before_create :set_mail_notification
117 before_save :generate_password_if_needed, :update_hashed_password
117 before_save :generate_password_if_needed, :update_hashed_password
118 before_destroy :remove_references_before_destroy
118 before_destroy :remove_references_before_destroy
119 after_save :update_notified_project_ids, :destroy_tokens
119 after_save :update_notified_project_ids, :destroy_tokens
120
120
121 scope :in_group, lambda {|group|
121 scope :in_group, lambda {|group|
122 group_id = group.is_a?(Group) ? group.id : group.to_i
122 group_id = group.is_a?(Group) ? group.id : group.to_i
123 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
123 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
124 }
124 }
125 scope :not_in_group, lambda {|group|
125 scope :not_in_group, lambda {|group|
126 group_id = group.is_a?(Group) ? group.id : group.to_i
126 group_id = group.is_a?(Group) ? group.id : group.to_i
127 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
127 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
128 }
128 }
129 scope :sorted, lambda { order(*User.fields_for_order_statement)}
129 scope :sorted, lambda { order(*User.fields_for_order_statement)}
130 scope :having_mail, lambda {|arg|
130 scope :having_mail, lambda {|arg|
131 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
131 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
132 if addresses.any?
132 if addresses.any?
133 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq
133 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq
134 else
134 else
135 none
135 none
136 end
136 end
137 }
137 }
138
138
139 def set_mail_notification
139 def set_mail_notification
140 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
140 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
141 true
141 true
142 end
142 end
143
143
144 def update_hashed_password
144 def update_hashed_password
145 # update hashed_password if password was set
145 # update hashed_password if password was set
146 if self.password && self.auth_source_id.blank?
146 if self.password && self.auth_source_id.blank?
147 salt_password(password)
147 salt_password(password)
148 end
148 end
149 end
149 end
150
150
151 alias :base_reload :reload
151 alias :base_reload :reload
152 def reload(*args)
152 def reload(*args)
153 @name = nil
153 @name = nil
154 @projects_by_role = nil
154 @projects_by_role = nil
155 @membership_by_project_id = nil
155 @membership_by_project_id = nil
156 @notified_projects_ids = nil
156 @notified_projects_ids = nil
157 @notified_projects_ids_changed = false
157 @notified_projects_ids_changed = false
158 @builtin_role = nil
158 @builtin_role = nil
159 @visible_project_ids = nil
159 @visible_project_ids = nil
160 base_reload(*args)
160 base_reload(*args)
161 end
161 end
162
162
163 def mail
163 def mail
164 email_address.try(:address)
164 email_address.try(:address)
165 end
165 end
166
166
167 def mail=(arg)
167 def mail=(arg)
168 email = email_address || build_email_address
168 email = email_address || build_email_address
169 email.address = arg
169 email.address = arg
170 end
170 end
171
171
172 def mail_changed?
172 def mail_changed?
173 email_address.try(:address_changed?)
173 email_address.try(:address_changed?)
174 end
174 end
175
175
176 def mails
176 def mails
177 email_addresses.pluck(:address)
177 email_addresses.pluck(:address)
178 end
178 end
179
179
180 def self.find_or_initialize_by_identity_url(url)
180 def self.find_or_initialize_by_identity_url(url)
181 user = where(:identity_url => url).first
181 user = where(:identity_url => url).first
182 unless user
182 unless user
183 user = User.new
183 user = User.new
184 user.identity_url = url
184 user.identity_url = url
185 end
185 end
186 user
186 user
187 end
187 end
188
188
189 def identity_url=(url)
189 def identity_url=(url)
190 if url.blank?
190 if url.blank?
191 write_attribute(:identity_url, '')
191 write_attribute(:identity_url, '')
192 else
192 else
193 begin
193 begin
194 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
194 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
195 rescue OpenIdAuthentication::InvalidOpenId
195 rescue OpenIdAuthentication::InvalidOpenId
196 # Invalid url, don't save
196 # Invalid url, don't save
197 end
197 end
198 end
198 end
199 self.read_attribute(:identity_url)
199 self.read_attribute(:identity_url)
200 end
200 end
201
201
202 # Returns the user that matches provided login and password, or nil
202 # Returns the user that matches provided login and password, or nil
203 def self.try_to_login(login, password, active_only=true)
203 def self.try_to_login(login, password, active_only=true)
204 login = login.to_s
204 login = login.to_s
205 password = password.to_s
205 password = password.to_s
206
206
207 # Make sure no one can sign in with an empty login or password
207 # Make sure no one can sign in with an empty login or password
208 return nil if login.empty? || password.empty?
208 return nil if login.empty? || password.empty?
209 user = find_by_login(login)
209 user = find_by_login(login)
210 if user
210 if user
211 # user is already in local database
211 # user is already in local database
212 return nil unless user.check_password?(password)
212 return nil unless user.check_password?(password)
213 return nil if !user.active? && active_only
213 return nil if !user.active? && active_only
214 else
214 else
215 # user is not yet registered, try to authenticate with available sources
215 # user is not yet registered, try to authenticate with available sources
216 attrs = AuthSource.authenticate(login, password)
216 attrs = AuthSource.authenticate(login, password)
217 if attrs
217 if attrs
218 user = new(attrs)
218 user = new(attrs)
219 user.login = login
219 user.login = login
220 user.language = Setting.default_language
220 user.language = Setting.default_language
221 if user.save
221 if user.save
222 user.reload
222 user.reload
223 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
223 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
224 end
224 end
225 end
225 end
226 end
226 end
227 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
227 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
228 user
228 user
229 rescue => text
229 rescue => text
230 raise text
230 raise text
231 end
231 end
232
232
233 # Returns the user who matches the given autologin +key+ or nil
233 # Returns the user who matches the given autologin +key+ or nil
234 def self.try_to_autologin(key)
234 def self.try_to_autologin(key)
235 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
235 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
236 if user
236 if user
237 user.update_column(:last_login_on, Time.now)
237 user.update_column(:last_login_on, Time.now)
238 user
238 user
239 end
239 end
240 end
240 end
241
241
242 def self.name_formatter(formatter = nil)
242 def self.name_formatter(formatter = nil)
243 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
243 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
244 end
244 end
245
245
246 # Returns an array of fields names than can be used to make an order statement for users
246 # Returns an array of fields names than can be used to make an order statement for users
247 # according to how user names are displayed
247 # according to how user names are displayed
248 # Examples:
248 # Examples:
249 #
249 #
250 # User.fields_for_order_statement => ['users.login', 'users.id']
250 # User.fields_for_order_statement => ['users.login', 'users.id']
251 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
251 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
252 def self.fields_for_order_statement(table=nil)
252 def self.fields_for_order_statement(table=nil)
253 table ||= table_name
253 table ||= table_name
254 name_formatter[:order].map {|field| "#{table}.#{field}"}
254 name_formatter[:order].map {|field| "#{table}.#{field}"}
255 end
255 end
256
256
257 # Return user's full name for display
257 # Return user's full name for display
258 def name(formatter = nil)
258 def name(formatter = nil)
259 f = self.class.name_formatter(formatter)
259 f = self.class.name_formatter(formatter)
260 if formatter
260 if formatter
261 eval('"' + f[:string] + '"')
261 eval('"' + f[:string] + '"')
262 else
262 else
263 @name ||= eval('"' + f[:string] + '"')
263 @name ||= eval('"' + f[:string] + '"')
264 end
264 end
265 end
265 end
266
266
267 def active?
267 def active?
268 self.status == STATUS_ACTIVE
268 self.status == STATUS_ACTIVE
269 end
269 end
270
270
271 def registered?
271 def registered?
272 self.status == STATUS_REGISTERED
272 self.status == STATUS_REGISTERED
273 end
273 end
274
274
275 def locked?
275 def locked?
276 self.status == STATUS_LOCKED
276 self.status == STATUS_LOCKED
277 end
277 end
278
278
279 def activate
279 def activate
280 self.status = STATUS_ACTIVE
280 self.status = STATUS_ACTIVE
281 end
281 end
282
282
283 def register
283 def register
284 self.status = STATUS_REGISTERED
284 self.status = STATUS_REGISTERED
285 end
285 end
286
286
287 def lock
287 def lock
288 self.status = STATUS_LOCKED
288 self.status = STATUS_LOCKED
289 end
289 end
290
290
291 def activate!
291 def activate!
292 update_attribute(:status, STATUS_ACTIVE)
292 update_attribute(:status, STATUS_ACTIVE)
293 end
293 end
294
294
295 def register!
295 def register!
296 update_attribute(:status, STATUS_REGISTERED)
296 update_attribute(:status, STATUS_REGISTERED)
297 end
297 end
298
298
299 def lock!
299 def lock!
300 update_attribute(:status, STATUS_LOCKED)
300 update_attribute(:status, STATUS_LOCKED)
301 end
301 end
302
302
303 # Returns true if +clear_password+ is the correct user's password, otherwise false
303 # Returns true if +clear_password+ is the correct user's password, otherwise false
304 def check_password?(clear_password)
304 def check_password?(clear_password)
305 if auth_source_id.present?
305 if auth_source_id.present?
306 auth_source.authenticate(self.login, clear_password)
306 auth_source.authenticate(self.login, clear_password)
307 else
307 else
308 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
308 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
309 end
309 end
310 end
310 end
311
311
312 # Generates a random salt and computes hashed_password for +clear_password+
312 # Generates a random salt and computes hashed_password for +clear_password+
313 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
313 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
314 def salt_password(clear_password)
314 def salt_password(clear_password)
315 self.salt = User.generate_salt
315 self.salt = User.generate_salt
316 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
316 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
317 self.passwd_changed_on = Time.now.change(:usec => 0)
317 self.passwd_changed_on = Time.now.change(:usec => 0)
318 end
318 end
319
319
320 # Does the backend storage allow this user to change their password?
320 # Does the backend storage allow this user to change their password?
321 def change_password_allowed?
321 def change_password_allowed?
322 return true if auth_source.nil?
322 return true if auth_source.nil?
323 return auth_source.allow_password_changes?
323 return auth_source.allow_password_changes?
324 end
324 end
325
325
326 # Returns true if the user password has expired
326 # Returns true if the user password has expired
327 def password_expired?
327 def password_expired?
328 period = Setting.password_max_age.to_i
328 period = Setting.password_max_age.to_i
329 if period.zero?
329 if period.zero?
330 false
330 false
331 else
331 else
332 changed_on = self.passwd_changed_on || Time.at(0)
332 changed_on = self.passwd_changed_on || Time.at(0)
333 changed_on < period.days.ago
333 changed_on < period.days.ago
334 end
334 end
335 end
335 end
336
336
337 def must_change_password?
337 def must_change_password?
338 (must_change_passwd? || password_expired?) && change_password_allowed?
338 (must_change_passwd? || password_expired?) && change_password_allowed?
339 end
339 end
340
340
341 def generate_password?
341 def generate_password?
342 generate_password == '1' || generate_password == true
342 generate_password == '1' || generate_password == true
343 end
343 end
344
344
345 # Generate and set a random password on given length
345 # Generate and set a random password on given length
346 def random_password(length=40)
346 def random_password(length=40)
347 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
347 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
348 chars -= %w(0 O 1 l)
348 chars -= %w(0 O 1 l)
349 password = ''
349 password = ''
350 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
350 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
351 self.password = password
351 self.password = password
352 self.password_confirmation = password
352 self.password_confirmation = password
353 self
353 self
354 end
354 end
355
355
356 def pref
356 def pref
357 self.preference ||= UserPreference.new(:user => self)
357 self.preference ||= UserPreference.new(:user => self)
358 end
358 end
359
359
360 def time_zone
360 def time_zone
361 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
361 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
362 end
362 end
363
363
364 def force_default_language?
364 def force_default_language?
365 Setting.force_default_language_for_loggedin?
365 Setting.force_default_language_for_loggedin?
366 end
366 end
367
367
368 def language
368 def language
369 if force_default_language?
369 if force_default_language?
370 Setting.default_language
370 Setting.default_language
371 else
371 else
372 super
372 super
373 end
373 end
374 end
374 end
375
375
376 def wants_comments_in_reverse_order?
376 def wants_comments_in_reverse_order?
377 self.pref[:comments_sorting] == 'desc'
377 self.pref[:comments_sorting] == 'desc'
378 end
378 end
379
379
380 # Return user's RSS key (a 40 chars long string), used to access feeds
380 # Return user's RSS key (a 40 chars long string), used to access feeds
381 def rss_key
381 def rss_key
382 if rss_token.nil?
382 if rss_token.nil?
383 create_rss_token(:action => 'feeds')
383 create_rss_token(:action => 'feeds')
384 end
384 end
385 rss_token.value
385 rss_token.value
386 end
386 end
387
387
388 # Return user's API key (a 40 chars long string), used to access the API
388 # Return user's API key (a 40 chars long string), used to access the API
389 def api_key
389 def api_key
390 if api_token.nil?
390 if api_token.nil?
391 create_api_token(:action => 'api')
391 create_api_token(:action => 'api')
392 end
392 end
393 api_token.value
393 api_token.value
394 end
394 end
395
395
396 # Return an array of project ids for which the user has explicitly turned mail notifications on
396 # Return an array of project ids for which the user has explicitly turned mail notifications on
397 def notified_projects_ids
397 def notified_projects_ids
398 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
398 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
399 end
399 end
400
400
401 def notified_project_ids=(ids)
401 def notified_project_ids=(ids)
402 @notified_projects_ids_changed = true
402 @notified_projects_ids_changed = true
403 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
403 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
404 end
404 end
405
405
406 # Updates per project notifications (after_save callback)
406 # Updates per project notifications (after_save callback)
407 def update_notified_project_ids
407 def update_notified_project_ids
408 if @notified_projects_ids_changed
408 if @notified_projects_ids_changed
409 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
409 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
410 members.update_all(:mail_notification => false)
410 members.update_all(:mail_notification => false)
411 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
411 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
412 end
412 end
413 end
413 end
414 private :update_notified_project_ids
414 private :update_notified_project_ids
415
415
416 def valid_notification_options
416 def valid_notification_options
417 self.class.valid_notification_options(self)
417 self.class.valid_notification_options(self)
418 end
418 end
419
419
420 # Only users that belong to more than 1 project can select projects for which they are notified
420 # Only users that belong to more than 1 project can select projects for which they are notified
421 def self.valid_notification_options(user=nil)
421 def self.valid_notification_options(user=nil)
422 # Note that @user.membership.size would fail since AR ignores
422 # Note that @user.membership.size would fail since AR ignores
423 # :include association option when doing a count
423 # :include association option when doing a count
424 if user.nil? || user.memberships.length < 1
424 if user.nil? || user.memberships.length < 1
425 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
425 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
426 else
426 else
427 MAIL_NOTIFICATION_OPTIONS
427 MAIL_NOTIFICATION_OPTIONS
428 end
428 end
429 end
429 end
430
430
431 # Find a user account by matching the exact login and then a case-insensitive
431 # Find a user account by matching the exact login and then a case-insensitive
432 # version. Exact matches will be given priority.
432 # version. Exact matches will be given priority.
433 def self.find_by_login(login)
433 def self.find_by_login(login)
434 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
434 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
435 if login.present?
435 if login.present?
436 # First look for an exact match
436 # First look for an exact match
437 user = where(:login => login).detect {|u| u.login == login}
437 user = where(:login => login).detect {|u| u.login == login}
438 unless user
438 unless user
439 # Fail over to case-insensitive if none was found
439 # Fail over to case-insensitive if none was found
440 user = where("LOWER(login) = ?", login.downcase).first
440 user = where("LOWER(login) = ?", login.downcase).first
441 end
441 end
442 user
442 user
443 end
443 end
444 end
444 end
445
445
446 def self.find_by_rss_key(key)
446 def self.find_by_rss_key(key)
447 Token.find_active_user('feeds', key)
447 Token.find_active_user('feeds', key)
448 end
448 end
449
449
450 def self.find_by_api_key(key)
450 def self.find_by_api_key(key)
451 Token.find_active_user('api', key)
451 Token.find_active_user('api', key)
452 end
452 end
453
453
454 # Makes find_by_mail case-insensitive
454 # Makes find_by_mail case-insensitive
455 def self.find_by_mail(mail)
455 def self.find_by_mail(mail)
456 having_mail(mail).first
456 having_mail(mail).first
457 end
457 end
458
458
459 # Returns true if the default admin account can no longer be used
459 # Returns true if the default admin account can no longer be used
460 def self.default_admin_account_changed?
460 def self.default_admin_account_changed?
461 !User.active.find_by_login("admin").try(:check_password?, "admin")
461 !User.active.find_by_login("admin").try(:check_password?, "admin")
462 end
462 end
463
463
464 def to_s
464 def to_s
465 name
465 name
466 end
466 end
467
467
468 CSS_CLASS_BY_STATUS = {
468 CSS_CLASS_BY_STATUS = {
469 STATUS_ANONYMOUS => 'anon',
469 STATUS_ANONYMOUS => 'anon',
470 STATUS_ACTIVE => 'active',
470 STATUS_ACTIVE => 'active',
471 STATUS_REGISTERED => 'registered',
471 STATUS_REGISTERED => 'registered',
472 STATUS_LOCKED => 'locked'
472 STATUS_LOCKED => 'locked'
473 }
473 }
474
474
475 def css_classes
475 def css_classes
476 "user #{CSS_CLASS_BY_STATUS[status]}"
476 "user #{CSS_CLASS_BY_STATUS[status]}"
477 end
477 end
478
478
479 # Returns the current day according to user's time zone
479 # Returns the current day according to user's time zone
480 def today
480 def today
481 if time_zone.nil?
481 if time_zone.nil?
482 Date.today
482 Date.today
483 else
483 else
484 Time.now.in_time_zone(time_zone).to_date
484 Time.now.in_time_zone(time_zone).to_date
485 end
485 end
486 end
486 end
487
487
488 # Returns the day of +time+ according to user's time zone
488 # Returns the day of +time+ according to user's time zone
489 def time_to_date(time)
489 def time_to_date(time)
490 if time_zone.nil?
490 if time_zone.nil?
491 time.to_date
491 time.to_date
492 else
492 else
493 time.in_time_zone(time_zone).to_date
493 time.in_time_zone(time_zone).to_date
494 end
494 end
495 end
495 end
496
496
497 def logged?
497 def logged?
498 true
498 true
499 end
499 end
500
500
501 def anonymous?
501 def anonymous?
502 !logged?
502 !logged?
503 end
503 end
504
504
505 # Returns user's membership for the given project
505 # Returns user's membership for the given project
506 # or nil if the user is not a member of project
506 # or nil if the user is not a member of project
507 def membership(project)
507 def membership(project)
508 project_id = project.is_a?(Project) ? project.id : project
508 project_id = project.is_a?(Project) ? project.id : project
509
509
510 @membership_by_project_id ||= Hash.new {|h, project_id|
510 @membership_by_project_id ||= Hash.new {|h, project_id|
511 h[project_id] = memberships.where(:project_id => project_id).first
511 h[project_id] = memberships.where(:project_id => project_id).first
512 }
512 }
513 @membership_by_project_id[project_id]
513 @membership_by_project_id[project_id]
514 end
514 end
515
515
516 # Returns the user's bult-in role
516 # Returns the user's bult-in role
517 def builtin_role
517 def builtin_role
518 @builtin_role ||= Role.non_member
518 @builtin_role ||= Role.non_member
519 end
519 end
520
520
521 # Return user's roles for project
521 # Return user's roles for project
522 def roles_for_project(project)
522 def roles_for_project(project)
523 # No role on archived projects
523 # No role on archived projects
524 return [] if project.nil? || project.archived?
524 return [] if project.nil? || project.archived?
525 if membership = membership(project)
525 if membership = membership(project)
526 membership.roles.dup
526 membership.roles.dup
527 elsif project.is_public?
527 elsif project.is_public?
528 project.override_roles(builtin_role)
528 project.override_roles(builtin_role)
529 else
529 else
530 []
530 []
531 end
531 end
532 end
532 end
533
533
534 # Returns a hash of user's projects grouped by roles
534 # Returns a hash of user's projects grouped by roles
535 def projects_by_role
535 def projects_by_role
536 return @projects_by_role if @projects_by_role
536 return @projects_by_role if @projects_by_role
537
537
538 hash = Hash.new([])
538 hash = Hash.new([])
539
539
540 group_class = anonymous? ? GroupAnonymous : GroupNonMember
540 group_class = anonymous? ? GroupAnonymous : GroupNonMember
541 members = Member.joins(:project, :principal).
541 members = Member.joins(:project, :principal).
542 where("#{Project.table_name}.status <> 9").
542 where("#{Project.table_name}.status <> 9").
543 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
543 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
544 preload(:project, :roles).
544 preload(:project, :roles).
545 to_a
545 to_a
546
546
547 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
547 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
548 members.each do |member|
548 members.each do |member|
549 if member.project
549 if member.project
550 member.roles.each do |role|
550 member.roles.each do |role|
551 hash[role] = [] unless hash.key?(role)
551 hash[role] = [] unless hash.key?(role)
552 hash[role] << member.project
552 hash[role] << member.project
553 end
553 end
554 end
554 end
555 end
555 end
556
556
557 hash.each do |role, projects|
557 hash.each do |role, projects|
558 projects.uniq!
558 projects.uniq!
559 end
559 end
560
560
561 @projects_by_role = hash
561 @projects_by_role = hash
562 end
562 end
563
563
564 # Returns the ids of visible projects
564 # Returns the ids of visible projects
565 def visible_project_ids
565 def visible_project_ids
566 @visible_project_ids ||= Project.visible(self).pluck(:id)
566 @visible_project_ids ||= Project.visible(self).pluck(:id)
567 end
567 end
568
568
569 # Returns the roles that the user is allowed to manage for the given project
570 def managed_roles(project)
571 if admin?
572 Role.givable.to_a
573 else
574 membership(project).try(:managed_roles) || []
575 end
576 end
577
569 # Returns true if user is arg or belongs to arg
578 # Returns true if user is arg or belongs to arg
570 def is_or_belongs_to?(arg)
579 def is_or_belongs_to?(arg)
571 if arg.is_a?(User)
580 if arg.is_a?(User)
572 self == arg
581 self == arg
573 elsif arg.is_a?(Group)
582 elsif arg.is_a?(Group)
574 arg.users.include?(self)
583 arg.users.include?(self)
575 else
584 else
576 false
585 false
577 end
586 end
578 end
587 end
579
588
580 # Return true if the user is allowed to do the specified action on a specific context
589 # Return true if the user is allowed to do the specified action on a specific context
581 # Action can be:
590 # Action can be:
582 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
591 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
583 # * a permission Symbol (eg. :edit_project)
592 # * a permission Symbol (eg. :edit_project)
584 # Context can be:
593 # Context can be:
585 # * a project : returns true if user is allowed to do the specified action on this project
594 # * a project : returns true if user is allowed to do the specified action on this project
586 # * an array of projects : returns true if user is allowed on every project
595 # * an array of projects : returns true if user is allowed on every project
587 # * nil with options[:global] set : check if user has at least one role allowed for this action,
596 # * nil with options[:global] set : check if user has at least one role allowed for this action,
588 # or falls back to Non Member / Anonymous permissions depending if the user is logged
597 # or falls back to Non Member / Anonymous permissions depending if the user is logged
589 def allowed_to?(action, context, options={}, &block)
598 def allowed_to?(action, context, options={}, &block)
590 if context && context.is_a?(Project)
599 if context && context.is_a?(Project)
591 return false unless context.allows_to?(action)
600 return false unless context.allows_to?(action)
592 # Admin users are authorized for anything else
601 # Admin users are authorized for anything else
593 return true if admin?
602 return true if admin?
594
603
595 roles = roles_for_project(context)
604 roles = roles_for_project(context)
596 return false unless roles
605 return false unless roles
597 roles.any? {|role|
606 roles.any? {|role|
598 (context.is_public? || role.member?) &&
607 (context.is_public? || role.member?) &&
599 role.allowed_to?(action) &&
608 role.allowed_to?(action) &&
600 (block_given? ? yield(role, self) : true)
609 (block_given? ? yield(role, self) : true)
601 }
610 }
602 elsif context && context.is_a?(Array)
611 elsif context && context.is_a?(Array)
603 if context.empty?
612 if context.empty?
604 false
613 false
605 else
614 else
606 # Authorize if user is authorized on every element of the array
615 # Authorize if user is authorized on every element of the array
607 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
616 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
608 end
617 end
609 elsif context
618 elsif context
610 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
619 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
611 elsif options[:global]
620 elsif options[:global]
612 # Admin users are always authorized
621 # Admin users are always authorized
613 return true if admin?
622 return true if admin?
614
623
615 # authorize if user has at least one role that has this permission
624 # authorize if user has at least one role that has this permission
616 roles = memberships.collect {|m| m.roles}.flatten.uniq
625 roles = memberships.collect {|m| m.roles}.flatten.uniq
617 roles << (self.logged? ? Role.non_member : Role.anonymous)
626 roles << (self.logged? ? Role.non_member : Role.anonymous)
618 roles.any? {|role|
627 roles.any? {|role|
619 role.allowed_to?(action) &&
628 role.allowed_to?(action) &&
620 (block_given? ? yield(role, self) : true)
629 (block_given? ? yield(role, self) : true)
621 }
630 }
622 else
631 else
623 false
632 false
624 end
633 end
625 end
634 end
626
635
627 # Is the user allowed to do the specified action on any project?
636 # Is the user allowed to do the specified action on any project?
628 # See allowed_to? for the actions and valid options.
637 # See allowed_to? for the actions and valid options.
629 #
638 #
630 # NB: this method is not used anywhere in the core codebase as of
639 # NB: this method is not used anywhere in the core codebase as of
631 # 2.5.2, but it's used by many plugins so if we ever want to remove
640 # 2.5.2, but it's used by many plugins so if we ever want to remove
632 # it it has to be carefully deprecated for a version or two.
641 # it it has to be carefully deprecated for a version or two.
633 def allowed_to_globally?(action, options={}, &block)
642 def allowed_to_globally?(action, options={}, &block)
634 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
643 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
635 end
644 end
636
645
637 def allowed_to_view_all_time_entries?(context)
646 def allowed_to_view_all_time_entries?(context)
638 allowed_to?(:view_time_entries, context) do |role, user|
647 allowed_to?(:view_time_entries, context) do |role, user|
639 role.time_entries_visibility == 'all'
648 role.time_entries_visibility == 'all'
640 end
649 end
641 end
650 end
642
651
643 # Returns true if the user is allowed to delete the user's own account
652 # Returns true if the user is allowed to delete the user's own account
644 def own_account_deletable?
653 def own_account_deletable?
645 Setting.unsubscribe? &&
654 Setting.unsubscribe? &&
646 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
655 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
647 end
656 end
648
657
649 safe_attributes 'login',
658 safe_attributes 'login',
650 'firstname',
659 'firstname',
651 'lastname',
660 'lastname',
652 'mail',
661 'mail',
653 'mail_notification',
662 'mail_notification',
654 'notified_project_ids',
663 'notified_project_ids',
655 'language',
664 'language',
656 'custom_field_values',
665 'custom_field_values',
657 'custom_fields',
666 'custom_fields',
658 'identity_url'
667 'identity_url'
659
668
660 safe_attributes 'status',
669 safe_attributes 'status',
661 'auth_source_id',
670 'auth_source_id',
662 'generate_password',
671 'generate_password',
663 'must_change_passwd',
672 'must_change_passwd',
664 :if => lambda {|user, current_user| current_user.admin?}
673 :if => lambda {|user, current_user| current_user.admin?}
665
674
666 safe_attributes 'group_ids',
675 safe_attributes 'group_ids',
667 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
676 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
668
677
669 # Utility method to help check if a user should be notified about an
678 # Utility method to help check if a user should be notified about an
670 # event.
679 # event.
671 #
680 #
672 # TODO: only supports Issue events currently
681 # TODO: only supports Issue events currently
673 def notify_about?(object)
682 def notify_about?(object)
674 if mail_notification == 'all'
683 if mail_notification == 'all'
675 true
684 true
676 elsif mail_notification.blank? || mail_notification == 'none'
685 elsif mail_notification.blank? || mail_notification == 'none'
677 false
686 false
678 else
687 else
679 case object
688 case object
680 when Issue
689 when Issue
681 case mail_notification
690 case mail_notification
682 when 'selected', 'only_my_events'
691 when 'selected', 'only_my_events'
683 # user receives notifications for created/assigned issues on unselected projects
692 # user receives notifications for created/assigned issues on unselected projects
684 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
693 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
685 when 'only_assigned'
694 when 'only_assigned'
686 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
695 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
687 when 'only_owner'
696 when 'only_owner'
688 object.author == self
697 object.author == self
689 end
698 end
690 when News
699 when News
691 # always send to project members except when mail_notification is set to 'none'
700 # always send to project members except when mail_notification is set to 'none'
692 true
701 true
693 end
702 end
694 end
703 end
695 end
704 end
696
705
697 def self.current=(user)
706 def self.current=(user)
698 RequestStore.store[:current_user] = user
707 RequestStore.store[:current_user] = user
699 end
708 end
700
709
701 def self.current
710 def self.current
702 RequestStore.store[:current_user] ||= User.anonymous
711 RequestStore.store[:current_user] ||= User.anonymous
703 end
712 end
704
713
705 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
714 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
706 # one anonymous user per database.
715 # one anonymous user per database.
707 def self.anonymous
716 def self.anonymous
708 anonymous_user = AnonymousUser.first
717 anonymous_user = AnonymousUser.first
709 if anonymous_user.nil?
718 if anonymous_user.nil?
710 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
719 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
711 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
720 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
712 end
721 end
713 anonymous_user
722 anonymous_user
714 end
723 end
715
724
716 # Salts all existing unsalted passwords
725 # Salts all existing unsalted passwords
717 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
726 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
718 # This method is used in the SaltPasswords migration and is to be kept as is
727 # This method is used in the SaltPasswords migration and is to be kept as is
719 def self.salt_unsalted_passwords!
728 def self.salt_unsalted_passwords!
720 transaction do
729 transaction do
721 User.where("salt IS NULL OR salt = ''").find_each do |user|
730 User.where("salt IS NULL OR salt = ''").find_each do |user|
722 next if user.hashed_password.blank?
731 next if user.hashed_password.blank?
723 salt = User.generate_salt
732 salt = User.generate_salt
724 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
733 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
725 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
734 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
726 end
735 end
727 end
736 end
728 end
737 end
729
738
730 protected
739 protected
731
740
732 def validate_password_length
741 def validate_password_length
733 return if password.blank? && generate_password?
742 return if password.blank? && generate_password?
734 # Password length validation based on setting
743 # Password length validation based on setting
735 if !password.nil? && password.size < Setting.password_min_length.to_i
744 if !password.nil? && password.size < Setting.password_min_length.to_i
736 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
745 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
737 end
746 end
738 end
747 end
739
748
740 def instantiate_email_address
749 def instantiate_email_address
741 email_address || build_email_address
750 email_address || build_email_address
742 end
751 end
743
752
744 private
753 private
745
754
746 def generate_password_if_needed
755 def generate_password_if_needed
747 if generate_password? && auth_source.nil?
756 if generate_password? && auth_source.nil?
748 length = [Setting.password_min_length.to_i + 2, 10].max
757 length = [Setting.password_min_length.to_i + 2, 10].max
749 random_password(length)
758 random_password(length)
750 end
759 end
751 end
760 end
752
761
753 # Delete all outstanding password reset tokens on password change.
762 # Delete all outstanding password reset tokens on password change.
754 # Delete the autologin tokens on password change to prohibit session leakage.
763 # Delete the autologin tokens on password change to prohibit session leakage.
755 # This helps to keep the account secure in case the associated email account
764 # This helps to keep the account secure in case the associated email account
756 # was compromised.
765 # was compromised.
757 def destroy_tokens
766 def destroy_tokens
758 if hashed_password_changed?
767 if hashed_password_changed?
759 tokens = ['recovery', 'autologin']
768 tokens = ['recovery', 'autologin']
760 Token.where(:user_id => id, :action => tokens).delete_all
769 Token.where(:user_id => id, :action => tokens).delete_all
761 end
770 end
762 end
771 end
763
772
764 # Removes references that are not handled by associations
773 # Removes references that are not handled by associations
765 # Things that are not deleted are reassociated with the anonymous user
774 # Things that are not deleted are reassociated with the anonymous user
766 def remove_references_before_destroy
775 def remove_references_before_destroy
767 return if self.id.nil?
776 return if self.id.nil?
768
777
769 substitute = User.anonymous
778 substitute = User.anonymous
770 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
779 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
771 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
780 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
772 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
781 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
773 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
782 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
774 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
783 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
775 JournalDetail.
784 JournalDetail.
776 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
785 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
777 update_all(['old_value = ?', substitute.id.to_s])
786 update_all(['old_value = ?', substitute.id.to_s])
778 JournalDetail.
787 JournalDetail.
779 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
788 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
780 update_all(['value = ?', substitute.id.to_s])
789 update_all(['value = ?', substitute.id.to_s])
781 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
790 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
782 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
791 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
783 # Remove private queries and keep public ones
792 # Remove private queries and keep public ones
784 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
793 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
785 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
794 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
786 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
795 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
787 Token.delete_all ['user_id = ?', id]
796 Token.delete_all ['user_id = ?', id]
788 Watcher.delete_all ['user_id = ?', id]
797 Watcher.delete_all ['user_id = ?', id]
789 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
798 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
790 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
799 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
791 end
800 end
792
801
793 # Return password digest
802 # Return password digest
794 def self.hash_password(clear_password)
803 def self.hash_password(clear_password)
795 Digest::SHA1.hexdigest(clear_password || "")
804 Digest::SHA1.hexdigest(clear_password || "")
796 end
805 end
797
806
798 # Returns a 128bits random salt as a hex string (32 chars long)
807 # Returns a 128bits random salt as a hex string (32 chars long)
799 def self.generate_salt
808 def self.generate_salt
800 Redmine::Utils.random_hex(16)
809 Redmine::Utils.random_hex(16)
801 end
810 end
802
811
803 end
812 end
804
813
805 class AnonymousUser < User
814 class AnonymousUser < User
806 validate :validate_anonymous_uniqueness, :on => :create
815 validate :validate_anonymous_uniqueness, :on => :create
807
816
808 def validate_anonymous_uniqueness
817 def validate_anonymous_uniqueness
809 # There should be only one AnonymousUser in the database
818 # There should be only one AnonymousUser in the database
810 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
819 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
811 end
820 end
812
821
813 def available_custom_fields
822 def available_custom_fields
814 []
823 []
815 end
824 end
816
825
817 # Overrides a few properties
826 # Overrides a few properties
818 def logged?; false end
827 def logged?; false end
819 def admin; false end
828 def admin; false end
820 def name(*args); I18n.t(:label_user_anonymous) end
829 def name(*args); I18n.t(:label_user_anonymous) end
821 def mail=(*args); nil end
830 def mail=(*args); nil end
822 def mail; nil end
831 def mail; nil end
823 def time_zone; nil end
832 def time_zone; nil end
824 def rss_key; nil end
833 def rss_key; nil end
825
834
826 def pref
835 def pref
827 UserPreference.new(:user => self)
836 UserPreference.new(:user => self)
828 end
837 end
829
838
830 # Returns the user's bult-in role
839 # Returns the user's bult-in role
831 def builtin_role
840 def builtin_role
832 @builtin_role ||= Role.anonymous
841 @builtin_role ||= Role.anonymous
833 end
842 end
834
843
835 def membership(*args)
844 def membership(*args)
836 nil
845 nil
837 end
846 end
838
847
839 def member_of?(*args)
848 def member_of?(*args)
840 false
849 false
841 end
850 end
842
851
843 # Anonymous user can not be destroyed
852 # Anonymous user can not be destroyed
844 def destroy
853 def destroy
845 false
854 false
846 end
855 end
847
856
848 protected
857 protected
849
858
850 def instantiate_email_address
859 def instantiate_email_address
851 end
860 end
852 end
861 end
@@ -1,16 +1,16
1 <fieldset class="box">
1 <fieldset class="box">
2 <legend><%= label_tag("principal_search", l(:label_principal_search)) %></legend>
2 <legend><%= label_tag("principal_search", l(:label_principal_search)) %></legend>
3 <p><%= text_field_tag('principal_search', nil) %></p>
3 <p><%= text_field_tag('principal_search', nil) %></p>
4 <%= javascript_tag "observeSearchfield('principal_search', null, '#{ escape_javascript autocomplete_project_memberships_path(@project, :format => 'js') }')" %>
4 <%= javascript_tag "observeSearchfield('principal_search', null, '#{ escape_javascript autocomplete_project_memberships_path(@project, :format => 'js') }')" %>
5 <div id="principals_for_new_member">
5 <div id="principals_for_new_member">
6 <%= render_principals_for_new_members(@project) %>
6 <%= render_principals_for_new_members(@project) %>
7 </div>
7 </div>
8 </fieldset>
8 </fieldset>
9 <fieldset class="box">
9 <fieldset class="box">
10 <legend><%= l(:label_role_plural) %> <%= toggle_checkboxes_link('.roles-selection input') %></legend>
10 <legend><%= l(:label_role_plural) %> <%= toggle_checkboxes_link('.roles-selection input') %></legend>
11 <div class="roles-selection">
11 <div class="roles-selection">
12 <% Role.givable.all.each do |role| %>
12 <% User.current.managed_roles(@project).each do |role| %>
13 <label><%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %> <%= role %></label>
13 <label><%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %> <%= role %></label>
14 <% end %>
14 <% end %>
15 </div>
15 </div>
16 </fieldset>
16 </fieldset>
@@ -1,64 +1,62
1 <% roles = Role.find_all_givable
1 <% roles = Role.find_all_givable
2 members = @project.member_principals.includes(:member_roles, :roles, :principal).to_a.sort %>
2 members = @project.member_principals.includes(:member_roles, :roles, :principal).to_a.sort %>
3
3
4 <p><%= link_to l(:label_member_new), new_project_membership_path(@project), :remote => true, :class => "icon icon-add" %></p>
4 <p><%= link_to l(:label_member_new), new_project_membership_path(@project), :remote => true, :class => "icon icon-add" %></p>
5
5
6 <% if members.any? %>
6 <% if members.any? %>
7 <table class="list members">
7 <table class="list members">
8 <thead>
8 <thead>
9 <tr>
9 <tr>
10 <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
10 <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
11 <th><%= l(:label_role_plural) %></th>
11 <th><%= l(:label_role_plural) %></th>
12 <th style="width:15%"></th>
12 <th style="width:15%"></th>
13 <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
13 <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
14 </tr>
14 </tr>
15 </thead>
15 </thead>
16 <tbody>
16 <tbody>
17 <% members.each do |member| %>
17 <% members.each do |member| %>
18 <% next if member.new_record? %>
18 <% next if member.new_record? %>
19 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
19 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
20 <td class="name <%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td>
20 <td class="name <%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td>
21 <td class="roles">
21 <td class="roles">
22 <span id="member-<%= member.id %>-roles"><%= member.roles.sort.collect(&:to_s).join(', ') %></span>
22 <span id="member-<%= member.id %>-roles"><%= member.roles.sort.collect(&:to_s).join(', ') %></span>
23 <%= form_for(member,
23 <%= form_for(member,
24 {:as => :membership, :remote => true,
24 {:as => :membership, :remote => true,
25 :url => membership_path(member),
25 :url => membership_path(member),
26 :method => :put,
26 :method => :put,
27 :html => { :id => "member-#{member.id}-roles-form", :class => 'hol' }}
27 :html => { :id => "member-#{member.id}-roles-form", :class => 'hol' }}
28 ) do |f| %>
28 ) do |f| %>
29 <p>
29 <p>
30 <% roles.each do |role| %>
30 <% roles.each do |role| %>
31 <label>
31 <label>
32 <%= check_box_tag('membership[role_ids][]',
32 <%= check_box_tag('membership[role_ids][]',
33 role.id, member.roles.include?(role),
33 role.id, member.roles.include?(role),
34 :id => nil,
34 :id => nil,
35 :disabled => member.member_roles.detect {
35 :disabled => !member.role_editable?(role)) %> <%= role %>
36 |mr| mr.role_id == role.id && !mr.inherited_from.nil?
37 } ) %> <%= role %>
38 </label><br />
36 </label><br />
39 <% end %>
37 <% end %>
40 </p>
38 </p>
41 <%= hidden_field_tag 'membership[role_ids][]', '', :id => nil %>
39 <%= hidden_field_tag 'membership[role_ids][]', '', :id => nil %>
42 <p>
40 <p>
43 <%= submit_tag l(:button_save), :class => "small" %>
41 <%= submit_tag l(:button_save), :class => "small" %>
44 <%= link_to_function(l(:button_cancel),
42 <%= link_to_function(l(:button_cancel),
45 "$('#member-#{member.id}-roles').show(); $('#member-#{member.id}-roles-form').hide(); return false;") %>
43 "$('#member-#{member.id}-roles').show(); $('#member-#{member.id}-roles-form').hide(); return false;") %>
46 </p>
44 </p>
47 <% end %>
45 <% end %>
48 </td>
46 </td>
49 <td class="buttons">
47 <td class="buttons">
50 <%= link_to_function l(:button_edit),
48 <%= link_to_function l(:button_edit),
51 "$('#member-#{member.id}-roles').hide(); $('#member-#{member.id}-roles-form').show(); return false;",
49 "$('#member-#{member.id}-roles').hide(); $('#member-#{member.id}-roles-form').show(); return false;",
52 :class => 'icon icon-edit' %>
50 :class => 'icon icon-edit' %>
53 <%= delete_link membership_path(member),
51 <%= delete_link membership_path(member),
54 :remote => true,
52 :remote => true,
55 :data => (!User.current.admin? && member.include?(User.current) ? {:confirm => l(:text_own_membership_delete_confirmation)} : {}) if member.deletable? %>
53 :data => (!User.current.admin? && member.include?(User.current) ? {:confirm => l(:text_own_membership_delete_confirmation)} : {}) if member.deletable? %>
56 </td>
54 </td>
57 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
55 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
58 </tr>
56 </tr>
59 <% end; reset_cycle %>
57 <% end; reset_cycle %>
60 </tbody>
58 </tbody>
61 </table>
59 </table>
62 <% else %>
60 <% else %>
63 <p class="nodata"><%= l(:label_no_data) %></p>
61 <p class="nodata"><%= l(:label_no_data) %></p>
64 <% end %>
62 <% end %>
@@ -1,40 +1,71
1 <%= error_messages_for 'role' %>
1 <%= error_messages_for 'role' %>
2
2
3 <div class="box tabular">
3 <div class="box tabular">
4 <% unless @role.builtin? %>
4 <% unless @role.builtin? %>
5 <p><%= f.text_field :name, :required => true %></p>
5 <p><%= f.text_field :name, :required => true %></p>
6 <p><%= f.check_box :assignable %></p>
6 <p><%= f.check_box :assignable %></p>
7 <% end %>
7 <% end %>
8
8
9 <% unless @role.anonymous? %>
9 <% unless @role.anonymous? %>
10 <p><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
10 <p><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
11 <% end %>
11 <% end %>
12
12
13 <% unless @role.anonymous? %>
13 <% unless @role.anonymous? %>
14 <p><%= f.select :time_entries_visibility, Role::TIME_ENTRIES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
14 <p><%= f.select :time_entries_visibility, Role::TIME_ENTRIES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
15 <% end %>
15 <% end %>
16
16
17 <p><%= f.select :users_visibility, Role::USERS_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
17 <p><%= f.select :users_visibility, Role::USERS_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
18
18
19 <% unless @role.builtin? %>
20 <p id="manage_members_options">
21 <label>Gestion des membres</label>
22 <label class="block">
23 <%= radio_button_tag 'role[all_roles_managed]', 1, @role.all_roles_managed?, :id => 'role_all_roles_managed_on',
24 :data => {:disables => '.role_managed_role input'} %>
25 tous les rΓ΄les
26 </label>
27 <label class="block">
28 <%= radio_button_tag 'role[all_roles_managed]', 0, !@role.all_roles_managed?, :id => 'role_all_roles_managed_off',
29 :data => {:enables => '.role_managed_role input'} %>
30 ces rΓ΄les uniquement:
31 </label>
32 <% Role.givable.sorted.each do |role| %>
33 <label class="block role_managed_role" style="padding-left:2em;">
34 <%= check_box_tag 'role[managed_role_ids][]', role.id, @role.managed_roles.include?(role), :id => nil %>
35 <%= role.name %>
36 </label>
37 <% end %>
38 <%= hidden_field_tag 'role[managed_role_ids][]', '' %>
39 <% end %>
40
19 <% if @role.new_record? && @roles.any? %>
41 <% if @role.new_record? && @roles.any? %>
20 <p><label for="copy_workflow_from"><%= l(:label_copy_workflow_from) %></label>
42 <p><label for="copy_workflow_from"><%= l(:label_copy_workflow_from) %></label>
21 <%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name, params[:copy_workflow_from] || @copy_from.try(:id))) %></p>
43 <%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name, params[:copy_workflow_from] || @copy_from.try(:id))) %></p>
22 <% end %>
44 <% end %>
23 </div>
45 </div>
24
46
25 <h3><%= l(:label_permissions) %></h3>
47 <h3><%= l(:label_permissions) %></h3>
26 <div class="box tabular" id="permissions">
48 <div class="box tabular" id="permissions">
27 <% perms_by_module = @role.setable_permissions.group_by {|p| p.project_module.to_s} %>
49 <% perms_by_module = @role.setable_permissions.group_by {|p| p.project_module.to_s} %>
28 <% perms_by_module.keys.sort.each do |mod| %>
50 <% perms_by_module.keys.sort.each do |mod| %>
29 <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend>
51 <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend>
30 <% perms_by_module[mod].each do |permission| %>
52 <% perms_by_module[mod].each do |permission| %>
31 <label class="floating">
53 <label class="floating">
32 <%= check_box_tag 'role[permissions][]', permission.name, (@role.permissions.include? permission.name), :id => nil %>
54 <%= check_box_tag 'role[permissions][]', permission.name, (@role.permissions.include? permission.name),
55 :id => "role_permissions_#{permission.name}" %>
33 <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
56 <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
34 </label>
57 </label>
35 <% end %>
58 <% end %>
36 </fieldset>
59 </fieldset>
37 <% end %>
60 <% end %>
38 <br /><%= check_all_links 'permissions' %>
61 <br /><%= check_all_links 'permissions' %>
39 <%= hidden_field_tag 'role[permissions][]', '' %>
62 <%= hidden_field_tag 'role[permissions][]', '' %>
40 </div>
63 </div>
64
65 <%= javascript_tag do %>
66 $(document).ready(function(){
67 $("#role_permissions_manage_members").change(function(){
68 $("#manage_members_options").toggle($(this).is(":checked"));
69 }).change();
70 });
71 <% end %>
@@ -1,122 +1,201
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class MembersControllerTest < ActionController::TestCase
20 class MembersControllerTest < ActionController::TestCase
21 fixtures :projects, :members, :member_roles, :roles, :users
21 fixtures :projects, :members, :member_roles, :roles, :users
22
22
23 def setup
23 def setup
24 User.current = nil
24 User.current = nil
25 @request.session[:user_id] = 2
25 @request.session[:user_id] = 2
26 end
26 end
27
27
28 def test_new
28 def test_new
29 get :new, :project_id => 1
29 get :new, :project_id => 1
30 assert_response :success
30 assert_response :success
31 end
31 end
32
32
33 def test_new_should_propose_managed_roles_only
34 role = Role.find(1)
35 role.update! :all_roles_managed => false
36 role.managed_roles = Role.where(:id => [2, 3]).to_a
37
38 get :new, :project_id => 1
39 assert_response :success
40 assert_select 'div.roles-selection' do
41 assert_select 'label', :text => 'Manager', :count => 0
42 assert_select 'label', :text => 'Developer'
43 assert_select 'label', :text => 'Reporter'
44 end
45 end
46
33 def test_xhr_new
47 def test_xhr_new
34 xhr :get, :new, :project_id => 1
48 xhr :get, :new, :project_id => 1
35 assert_response :success
49 assert_response :success
36 assert_equal 'text/javascript', response.content_type
50 assert_equal 'text/javascript', response.content_type
37 end
51 end
38
52
39 def test_create
53 def test_create
40 assert_difference 'Member.count' do
54 assert_difference 'Member.count' do
41 post :create, :project_id => 1, :membership => {:role_ids => [1], :user_id => 7}
55 post :create, :project_id => 1, :membership => {:role_ids => [1], :user_id => 7}
42 end
56 end
43 assert_redirected_to '/projects/ecookbook/settings/members'
57 assert_redirected_to '/projects/ecookbook/settings/members'
44 assert User.find(7).member_of?(Project.find(1))
58 assert User.find(7).member_of?(Project.find(1))
45 end
59 end
46
60
47 def test_create_multiple
61 def test_create_multiple
48 assert_difference 'Member.count', 3 do
62 assert_difference 'Member.count', 3 do
49 post :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]}
63 post :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]}
50 end
64 end
51 assert_redirected_to '/projects/ecookbook/settings/members'
65 assert_redirected_to '/projects/ecookbook/settings/members'
52 assert User.find(7).member_of?(Project.find(1))
66 assert User.find(7).member_of?(Project.find(1))
53 end
67 end
54
68
69 def test_create_should_ignore_unmanaged_roles
70 role = Role.find(1)
71 role.update! :all_roles_managed => false
72 role.managed_roles = Role.where(:id => [2, 3]).to_a
73
74 assert_difference 'Member.count' do
75 post :create, :project_id => 1, :membership => {:role_ids => [1, 2], :user_id => 7}
76 end
77 member = Member.order(:id => :desc).first
78 assert_equal [2], member.role_ids
79 end
80
81 def test_create_should_be_allowed_for_admin_without_role
82 User.find(1).members.delete_all
83 @request.session[:user_id] = 1
84
85 assert_difference 'Member.count' do
86 post :create, :project_id => 1, :membership => {:role_ids => [1, 2], :user_id => 7}
87 end
88 member = Member.order(:id => :desc).first
89 assert_equal [1, 2], member.role_ids
90 end
91
55 def test_xhr_create
92 def test_xhr_create
56 assert_difference 'Member.count', 3 do
93 assert_difference 'Member.count', 3 do
57 xhr :post, :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]}
94 xhr :post, :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]}
58 assert_response :success
95 assert_response :success
59 assert_template 'create'
96 assert_template 'create'
60 assert_equal 'text/javascript', response.content_type
97 assert_equal 'text/javascript', response.content_type
61 end
98 end
62 assert User.find(7).member_of?(Project.find(1))
99 assert User.find(7).member_of?(Project.find(1))
63 assert User.find(8).member_of?(Project.find(1))
100 assert User.find(8).member_of?(Project.find(1))
64 assert User.find(9).member_of?(Project.find(1))
101 assert User.find(9).member_of?(Project.find(1))
65 assert_include 'tab-content-members', response.body
102 assert_include 'tab-content-members', response.body
66 end
103 end
67
104
68 def test_xhr_create_with_failure
105 def test_xhr_create_with_failure
69 assert_no_difference 'Member.count' do
106 assert_no_difference 'Member.count' do
70 xhr :post, :create, :project_id => 1, :membership => {:role_ids => [], :user_ids => [7, 8, 9]}
107 xhr :post, :create, :project_id => 1, :membership => {:role_ids => [], :user_ids => [7, 8, 9]}
71 assert_response :success
108 assert_response :success
72 assert_template 'create'
109 assert_template 'create'
73 assert_equal 'text/javascript', response.content_type
110 assert_equal 'text/javascript', response.content_type
74 end
111 end
75 assert_match /alert/, response.body, "Alert message not sent"
112 assert_match /alert/, response.body, "Alert message not sent"
76 end
113 end
77
114
78 def test_edit
115 def test_update
79 assert_no_difference 'Member.count' do
116 assert_no_difference 'Member.count' do
80 put :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3}
117 put :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3}
81 end
118 end
82 assert_redirected_to '/projects/ecookbook/settings/members'
119 assert_redirected_to '/projects/ecookbook/settings/members'
83 end
120 end
84
121
85 def test_xhr_edit
122 def test_update_should_not_add_unmanaged_roles
123 role = Role.find(1)
124 role.update! :all_roles_managed => false
125 role.managed_roles = Role.where(:id => [2, 3]).to_a
126 member = Member.create!(:user => User.find(9), :role_ids => [3], :project_id => 1)
127
128 put :update, :id => member.id, :membership => {:role_ids => [1, 2, 3]}
129 assert_equal [2, 3], member.reload.role_ids.sort
130 end
131
132 def test_update_should_not_remove_unmanaged_roles
133 role = Role.find(1)
134 role.update! :all_roles_managed => false
135 role.managed_roles = Role.where(:id => [2, 3]).to_a
136 member = Member.create!(:user => User.find(9), :role_ids => [1, 3], :project_id => 1)
137
138 put :update, :id => member.id, :membership => {:role_ids => [2]}
139 assert_equal [1, 2], member.reload.role_ids.sort
140 end
141
142 def test_xhr_update
86 assert_no_difference 'Member.count' do
143 assert_no_difference 'Member.count' do
87 xhr :put, :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3}
144 xhr :put, :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3}
88 assert_response :success
145 assert_response :success
89 assert_template 'update'
146 assert_template 'update'
90 assert_equal 'text/javascript', response.content_type
147 assert_equal 'text/javascript', response.content_type
91 end
148 end
92 member = Member.find(2)
149 member = Member.find(2)
93 assert_equal [1], member.role_ids
150 assert_equal [1], member.role_ids
94 assert_equal 3, member.user_id
151 assert_equal 3, member.user_id
95 assert_include 'tab-content-members', response.body
152 assert_include 'tab-content-members', response.body
96 end
153 end
97
154
98 def test_destroy
155 def test_destroy
99 assert_difference 'Member.count', -1 do
156 assert_difference 'Member.count', -1 do
100 delete :destroy, :id => 2
157 delete :destroy, :id => 2
101 end
158 end
102 assert_redirected_to '/projects/ecookbook/settings/members'
159 assert_redirected_to '/projects/ecookbook/settings/members'
103 assert !User.find(3).member_of?(Project.find(1))
160 assert !User.find(3).member_of?(Project.find(1))
104 end
161 end
105
162
163 def test_destroy_should_fail_with_unmanaged_roles
164 role = Role.find(1)
165 role.update! :all_roles_managed => false
166 role.managed_roles = Role.where(:id => [2, 3]).to_a
167 member = Member.create!(:user => User.find(9), :role_ids => [1, 3], :project_id => 1)
168
169 assert_no_difference 'Member.count' do
170 delete :destroy, :id => member.id
171 end
172 end
173
174 def test_destroy_should_succeed_with_managed_roles_only
175 role = Role.find(1)
176 role.update! :all_roles_managed => false
177 role.managed_roles = Role.where(:id => [2, 3]).to_a
178 member = Member.create!(:user => User.find(9), :role_ids => [3], :project_id => 1)
179
180 assert_difference 'Member.count', -1 do
181 delete :destroy, :id => member.id
182 end
183 end
184
106 def test_xhr_destroy
185 def test_xhr_destroy
107 assert_difference 'Member.count', -1 do
186 assert_difference 'Member.count', -1 do
108 xhr :delete, :destroy, :id => 2
187 xhr :delete, :destroy, :id => 2
109 assert_response :success
188 assert_response :success
110 assert_template 'destroy'
189 assert_template 'destroy'
111 assert_equal 'text/javascript', response.content_type
190 assert_equal 'text/javascript', response.content_type
112 end
191 end
113 assert_nil Member.find_by_id(2)
192 assert_nil Member.find_by_id(2)
114 assert_include 'tab-content-members', response.body
193 assert_include 'tab-content-members', response.body
115 end
194 end
116
195
117 def test_autocomplete
196 def test_autocomplete
118 xhr :get, :autocomplete, :project_id => 1, :q => 'mis', :format => 'js'
197 xhr :get, :autocomplete, :project_id => 1, :q => 'mis', :format => 'js'
119 assert_response :success
198 assert_response :success
120 assert_include 'User Misc', response.body
199 assert_include 'User Misc', response.body
121 end
200 end
122 end
201 end
@@ -1,162 +1,193
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class MemberTest < ActiveSupport::TestCase
20 class MemberTest < ActiveSupport::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :enumerations, :users, :issue_categories,
22 :enumerations, :users, :issue_categories,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules,
27 :enabled_modules,
28 :groups_users,
28 :groups_users,
29 :watchers,
29 :watchers,
30 :journals, :journal_details,
30 :journals, :journal_details,
31 :messages,
31 :messages,
32 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
32 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
33 :boards
33 :boards
34
34
35 include Redmine::I18n
35 include Redmine::I18n
36
36
37 def setup
37 def setup
38 @jsmith = Member.find(1)
38 @jsmith = Member.find(1)
39 end
39 end
40
40
41 def test_create
41 def test_create
42 member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2])
42 member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2])
43 assert member.save
43 assert member.save
44 member.reload
44 member.reload
45
45
46 assert_equal 2, member.roles.size
46 assert_equal 2, member.roles.size
47 assert_equal Role.find(1), member.roles.sort.first
47 assert_equal Role.find(1), member.roles.sort.first
48 end
48 end
49
49
50 def test_update
50 def test_update
51 assert_equal "eCookbook", @jsmith.project.name
51 assert_equal "eCookbook", @jsmith.project.name
52 assert_equal "Manager", @jsmith.roles.first.name
52 assert_equal "Manager", @jsmith.roles.first.name
53 assert_equal "jsmith", @jsmith.user.login
53 assert_equal "jsmith", @jsmith.user.login
54
54
55 @jsmith.mail_notification = !@jsmith.mail_notification
55 @jsmith.mail_notification = !@jsmith.mail_notification
56 assert @jsmith.save
56 assert @jsmith.save
57 end
57 end
58
58
59 def test_update_roles
59 def test_update_roles
60 assert_equal 1, @jsmith.roles.size
60 assert_equal 1, @jsmith.roles.size
61 @jsmith.role_ids = [1, 2]
61 @jsmith.role_ids = [1, 2]
62 assert @jsmith.save
62 assert @jsmith.save
63 assert_equal 2, @jsmith.reload.roles.size
63 assert_equal 2, @jsmith.reload.roles.size
64 end
64 end
65
65
66 def test_validate
66 def test_validate
67 member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [2])
67 member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [2])
68 # same use cannot have more than one membership for a project
68 # same use cannot have more than one membership for a project
69 assert !member.save
69 assert !member.save
70
70
71 # must have one role at least
71 # must have one role at least
72 user = User.new(:firstname => "new1", :lastname => "user1",
72 user = User.new(:firstname => "new1", :lastname => "user1",
73 :mail => "test_validate@somenet.foo")
73 :mail => "test_validate@somenet.foo")
74 user.login = "test_validate"
74 user.login = "test_validate"
75 user.password, user.password_confirmation = "password", "password"
75 user.password, user.password_confirmation = "password", "password"
76 assert user.save
76 assert user.save
77
77
78 set_language_if_valid 'fr'
78 set_language_if_valid 'fr'
79 member = Member.new(:project_id => 1, :user_id => user.id, :role_ids => [])
79 member = Member.new(:project_id => 1, :user_id => user.id, :role_ids => [])
80 assert !member.save
80 assert !member.save
81 assert_include I18n.translate('activerecord.errors.messages.empty'), member.errors[:role]
81 assert_include I18n.translate('activerecord.errors.messages.empty'), member.errors[:role]
82 assert_equal "R\xc3\xb4le doit \xc3\xaatre renseign\xc3\xa9(e)".force_encoding('UTF-8'),
82 assert_equal "R\xc3\xb4le doit \xc3\xaatre renseign\xc3\xa9(e)".force_encoding('UTF-8'),
83 [member.errors.full_messages].flatten.join
83 [member.errors.full_messages].flatten.join
84 end
84 end
85
85
86 def test_validate_member_role
86 def test_validate_member_role
87 user = User.new(:firstname => "new1", :lastname => "user1",
87 user = User.new(:firstname => "new1", :lastname => "user1",
88 :mail => "test_validate@somenet.foo")
88 :mail => "test_validate@somenet.foo")
89 user.login = "test_validate_member_role"
89 user.login = "test_validate_member_role"
90 user.password, user.password_confirmation = "password", "password"
90 user.password, user.password_confirmation = "password", "password"
91 assert user.save
91 assert user.save
92 member = Member.new(:project_id => 1, :user_id => user.id, :role_ids => [5])
92 member = Member.new(:project_id => 1, :user_id => user.id, :role_ids => [5])
93 assert !member.save
93 assert !member.save
94 end
94 end
95
95
96 def test_set_issue_category_nil_should_handle_nil_values
96 def test_set_issue_category_nil_should_handle_nil_values
97 m = Member.new
97 m = Member.new
98 assert_nil m.user
98 assert_nil m.user
99 assert_nil m.project
99 assert_nil m.project
100
100
101 assert_nothing_raised do
101 assert_nothing_raised do
102 m.set_issue_category_nil
102 m.set_issue_category_nil
103 end
103 end
104 end
104 end
105
105
106 def test_destroy
106 def test_destroy
107 category1 = IssueCategory.find(1)
107 category1 = IssueCategory.find(1)
108 assert_equal @jsmith.user.id, category1.assigned_to_id
108 assert_equal @jsmith.user.id, category1.assigned_to_id
109 assert_difference 'Member.count', -1 do
109 assert_difference 'Member.count', -1 do
110 assert_difference 'MemberRole.count', -1 do
110 assert_difference 'MemberRole.count', -1 do
111 @jsmith.destroy
111 @jsmith.destroy
112 end
112 end
113 end
113 end
114 assert_raise(ActiveRecord::RecordNotFound) { Member.find(@jsmith.id) }
114 assert_raise(ActiveRecord::RecordNotFound) { Member.find(@jsmith.id) }
115 category1.reload
115 category1.reload
116 assert_nil category1.assigned_to_id
116 assert_nil category1.assigned_to_id
117 end
117 end
118
118
119 def test_destroy_should_trigger_callbacks_only_once
119 def test_destroy_should_trigger_callbacks_only_once
120 Member.class_eval { def destroy_test_callback; end}
120 Member.class_eval { def destroy_test_callback; end}
121 Member.after_destroy :destroy_test_callback
121 Member.after_destroy :destroy_test_callback
122
122
123 m = Member.create!(:user_id => 1, :project_id => 1, :role_ids => [1,3])
123 m = Member.create!(:user_id => 1, :project_id => 1, :role_ids => [1,3])
124
124
125 Member.any_instance.expects(:destroy_test_callback).once
125 Member.any_instance.expects(:destroy_test_callback).once
126 assert_difference 'Member.count', -1 do
126 assert_difference 'Member.count', -1 do
127 assert_difference 'MemberRole.count', -2 do
127 assert_difference 'MemberRole.count', -2 do
128 m.destroy
128 m.destroy
129 end
129 end
130 end
130 end
131 assert m.destroyed?
131 assert m.destroyed?
132 ensure
132 ensure
133 Member._destroy_callbacks.delete(:destroy_test_callback)
133 Member._destroy_callbacks.delete(:destroy_test_callback)
134 end
134 end
135
135
136 def test_roles_should_be_unique
136 def test_roles_should_be_unique
137 m = Member.new(:user_id => 1, :project_id => 1)
137 m = Member.new(:user_id => 1, :project_id => 1)
138 m.member_roles.build(:role_id => 1)
138 m.member_roles.build(:role_id => 1)
139 m.member_roles.build(:role_id => 1)
139 m.member_roles.build(:role_id => 1)
140 m.save!
140 m.save!
141 m.reload
141 m.reload
142 assert_equal 1, m.roles.count
142 assert_equal 1, m.roles.count
143 assert_equal [1], m.roles.ids
143 assert_equal [1], m.roles.ids
144 end
144 end
145
145
146 def test_sort_without_roles
146 def test_sort_without_roles
147 a = Member.new(:roles => [Role.first])
147 a = Member.new(:roles => [Role.first])
148 b = Member.new
148 b = Member.new
149
149
150 assert_equal -1, a <=> b
150 assert_equal -1, a <=> b
151 assert_equal 1, b <=> a
151 assert_equal 1, b <=> a
152 end
152 end
153
153
154 def test_sort_without_principal
154 def test_sort_without_principal
155 role = Role.first
155 role = Role.first
156 a = Member.new(:roles => [role], :principal => User.first)
156 a = Member.new(:roles => [role], :principal => User.first)
157 b = Member.new(:roles => [role])
157 b = Member.new(:roles => [role])
158
158
159 assert_equal -1, a <=> b
159 assert_equal -1, a <=> b
160 assert_equal 1, b <=> a
160 assert_equal 1, b <=> a
161 end
161 end
162
163 def test_managed_roles_should_return_all_roles_for_role_with_all_roles_managed
164 member = Member.new
165 member.roles << Role.generate!(:permissions => [:manage_members], :all_roles_managed => true)
166 assert_equal Role.givable.all, member.managed_roles
167 end
168
169 def test_managed_roles_should_return_all_roles_for_admins
170 member = Member.new(:user => User.find(1))
171 member.roles << Role.generate!
172 assert_equal Role.givable.all, member.managed_roles
173 end
174
175 def test_managed_roles_should_return_limited_roles_for_role_without_all_roles_managed
176 member = Member.new
177 member.roles << Role.generate!(:permissions => [:manage_members], :all_roles_managed => false, :managed_role_ids => [2, 3])
178 assert_equal [2, 3], member.managed_roles.map(&:id).sort
179 end
180
181 def test_managed_roles_should_cumulated_managed_roles
182 member = Member.new
183 member.roles << Role.generate!(:permissions => [:manage_members], :all_roles_managed => false, :managed_role_ids => [3])
184 member.roles << Role.generate!(:permissions => [:manage_members], :all_roles_managed => false, :managed_role_ids => [2])
185 assert_equal [2, 3], member.managed_roles.map(&:id).sort
186 end
187
188 def test_managed_roles_should_return_no_roles_for_role_without_permission
189 member = Member.new
190 member.roles << Role.generate!(:all_roles_managed => true)
191 assert_equal [], member.managed_roles
192 end
162 end
193 end
General Comments 0
You need to be logged in to leave comments. Login now