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