##// END OF EJS Templates
Fixed that the proposed users list may be empty when adding a project member (#10374)....
Jean-Philippe Lang -
r9008:6aad82e524c1
parent child
Show More
@@ -1,142 +1,142
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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, :create, :autocomplete]
21 21 before_filter :find_project_from_association, :except => [:index, :create, :autocomplete]
22 22 before_filter :find_project_by_project_id, :only => [:index, :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 self, @member_count, @limit, params['page']
30 30 @offset ||= @member_pages.current.offset
31 31 @members = @project.member_principals.all(
32 32 :order => "#{Member.table_name}.id",
33 33 :limit => @limit,
34 34 :offset => @offset
35 35 )
36 36
37 37 respond_to do |format|
38 38 format.html { head 406 }
39 39 format.api
40 40 end
41 41 end
42 42
43 43 def show
44 44 respond_to do |format|
45 45 format.html { head 406 }
46 46 format.api
47 47 end
48 48 end
49 49
50 50 def create
51 51 members = []
52 52 if params[:membership] && params[:membership][:user_ids]
53 53 attrs = params[:membership].dup
54 54 user_ids = attrs.delete(:user_ids)
55 55 user_ids.each do |user_id|
56 56 members << Member.new(attrs.merge(:user_id => user_id))
57 57 end
58 58 else
59 59 members << Member.new(params[:membership])
60 60 end
61 61 @project.members << members
62 62
63 63 respond_to do |format|
64 64 if members.present? && members.all? {|m| m.valid? }
65 65 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
66 66 format.js {
67 67 render(:update) {|page|
68 68 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
69 69 page << 'hideOnLoad()'
70 70 members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
71 71 }
72 72 }
73 73 format.api {
74 74 @member = members.first
75 75 render :action => 'show', :status => :created, :location => membership_url(@member)
76 76 }
77 77 else
78 78 format.js {
79 79 render(:update) {|page|
80 80 errors = members.collect {|m|
81 81 m.errors.full_messages
82 82 }.flatten.uniq
83 83
84 84 page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', ')))
85 85 }
86 86 }
87 87 format.api { render_validation_errors(members.first) }
88 88 end
89 89 end
90 90 end
91 91
92 92 def update
93 93 if params[:membership]
94 94 @member.role_ids = params[:membership][:role_ids]
95 95 end
96 96 saved = @member.save
97 97 respond_to do |format|
98 98 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
99 99 format.js {
100 100 render(:update) {|page|
101 101 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
102 102 page << 'hideOnLoad()'
103 103 page.visual_effect(:highlight, "member-#{@member.id}")
104 104 }
105 105 }
106 106 format.api {
107 107 if saved
108 108 head :ok
109 109 else
110 110 render_validation_errors(@member)
111 111 end
112 112 }
113 113 end
114 114 end
115 115
116 116 def destroy
117 117 if request.delete? && @member.deletable?
118 118 @member.destroy
119 119 end
120 120 respond_to do |format|
121 121 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
122 122 format.js { render(:update) {|page|
123 123 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
124 124 page << 'hideOnLoad()'
125 125 }
126 126 }
127 127 format.api {
128 128 if @member.destroyed?
129 129 head :ok
130 130 else
131 131 head :unprocessable_entity
132 132 end
133 133 }
134 134 end
135 135 end
136 136
137 137 def autocomplete
138 @principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals
138 @principals = Principal.active.not_member_of(@project).like(params[:q]).all(:limit => 100)
139 139 render :layout => false
140 140 end
141 141
142 142 end
@@ -1,84 +1,95
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 Principal < ActiveRecord::Base
19 19 set_table_name "#{table_name_prefix}users#{table_name_suffix}"
20 20
21 21 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
22 22 has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
23 23 has_many :projects, :through => :memberships
24 24 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
25 25
26 26 # Groups and active users
27 27 named_scope :active, :conditions => "#{Principal.table_name}.status = 1"
28 28
29 29 named_scope :like, lambda {|q|
30 30 if q.blank?
31 31 {}
32 32 else
33 33 q = q.to_s.downcase
34 34 pattern = "%#{q}%"
35 35 sql = "LOWER(login) LIKE :p OR LOWER(firstname) LIKE :p OR LOWER(lastname) LIKE :p OR LOWER(mail) LIKE :p"
36 36 params = {:p => pattern}
37 37 if q =~ /^(.+)\s+(.+)$/
38 38 a, b = "#{$1}%", "#{$2}%"
39 39 sql << " OR (LOWER(firstname) LIKE :a AND LOWER(lastname) LIKE :b) OR (LOWER(firstname) LIKE :b AND LOWER(lastname) LIKE :a)"
40 40 params.merge!(:a => a, :b => b)
41 41 end
42 42 {:conditions => [sql, params]}
43 43 end
44 44 }
45 45
46 46 # Principals that are members of a collection of projects
47 47 named_scope :member_of, lambda {|projects|
48 projects = [projects] unless projects.is_a?(Array)
48 49 if projects.empty?
49 50 {:conditions => "1=0"}
50 51 else
51 52 ids = projects.map(&:id)
52 53 {:conditions => ["#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids]}
53 54 end
54 55 }
56 # Principals that are not members of projects
57 named_scope :not_member_of, lambda {|projects|
58 projects = [projects] unless projects.is_a?(Array)
59 if projects.empty?
60 {:conditions => "1=0"}
61 else
62 ids = projects.map(&:id)
63 {:conditions => ["#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids]}
64 end
65 }
55 66
56 67 before_create :set_default_empty_values
57 68
58 69 def name(formatter = nil)
59 70 to_s
60 71 end
61 72
62 73 def <=>(principal)
63 74 if principal.nil?
64 75 -1
65 76 elsif self.class.name == principal.class.name
66 77 self.to_s.downcase <=> principal.to_s.downcase
67 78 else
68 79 # groups after users
69 80 principal.class.name <=> self.class.name
70 81 end
71 82 end
72 83
73 84 protected
74 85
75 86 # Make sure we don't try to insert NULL values (see #4632)
76 87 def set_default_empty_values
77 88 self.login ||= ''
78 89 self.hashed_password ||= ''
79 90 self.firstname ||= ''
80 91 self.lastname ||= ''
81 92 self.mail ||= ''
82 93 true
83 94 end
84 95 end
@@ -1,91 +1,91
1 1 <%= error_messages_for 'member' %>
2 2 <% roles = Role.find_all_givable
3 3 members = @project.member_principals.find(:all, :include => [:roles, :principal]).sort %>
4 4
5 5 <div class="splitcontentleft">
6 6 <% if members.any? %>
7 7 <table class="list members">
8 8 <thead><tr>
9 9 <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
10 10 <th><%= l(:label_role_plural) %></th>
11 11 <th style="width:15%"></th>
12 12 <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
13 13 </tr></thead>
14 14 <tbody>
15 15 <% members.each do |member| %>
16 16 <% next if member.new_record? %>
17 17 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
18 18 <td class="<%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td>
19 19 <td class="roles">
20 20 <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
21 21 <% remote_form_for(:membership, member, :url => membership_path(member),
22 22 :method => :put,
23 23 :html => { :id => "member-#{member.id}-roles-form", :class => 'hol' }) do |f| %>
24 24 <p><% roles.each do |role| %>
25 25 <label><%= check_box_tag 'membership[role_ids][]', role.id, member.roles.include?(role),
26 26 :disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
27 27 <% end %></p>
28 28 <%= hidden_field_tag 'membership[role_ids][]', '' %>
29 29 <p><%= submit_tag l(:button_change), :class => "small" %>
30 30 <%= link_to_function l(:button_cancel),
31 31 "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;"
32 32 %></p>
33 33 <% end %>
34 34 </td>
35 35 <td class="buttons">
36 36 <%= link_to_function l(:button_edit),
37 37 "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;",
38 38 :class => 'icon icon-edit' %>
39 39 <%= link_to_remote(
40 40 l(:button_delete),
41 41 { :url => membership_path(member),
42 42 :method => :delete,
43 43 :confirm => (!User.current.admin? && member.include?(User.current) ? l(:text_own_membership_delete_confirmation) : nil) },
44 44 :title => l(:button_delete),
45 45 :class => 'icon icon-del'
46 46 ) if member.deletable? %>
47 47 </td>
48 48 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
49 49 </tr>
50 50 <% end; reset_cycle %>
51 51 </tbody>
52 52 </table>
53 53 <% else %>
54 54 <p class="nodata"><%= l(:label_no_data) %></p>
55 55 <% end %>
56 56 </div>
57 57
58 <% principals = Principal.active.find(:all, :limit => 100, :order => 'type, login, lastname ASC') - @project.principals %>
58 <% principals = Principal.active.not_member_of(@project).all(:limit => 100, :order => 'type, login, lastname ASC') %>
59 59
60 60 <div class="splitcontentright">
61 61 <% if roles.any? && principals.any? %>
62 62 <% remote_form_for(:membership, @member, :url => project_memberships_path(@project), :method => :post,
63 63 :loading => '$(\'member-add-submit\').disable();',
64 64 :complete => 'if($(\'member-add-submit\')) $(\'member-add-submit\').enable();') do |f| %>
65 65 <fieldset><legend><%=l(:label_member_new)%></legend>
66 66
67 67 <p><%= label_tag "principal_search", l(:label_principal_search) %><%= text_field_tag 'principal_search', nil %></p>
68 68 <%= observe_field(:principal_search,
69 69 :frequency => 0.5,
70 70 :update => :principals,
71 71 :url => autocomplete_project_memberships_path(@project),
72 72 :method => :get,
73 73 :before => '$("principal_search").addClassName("ajax-loading")',
74 74 :complete => '$("principal_search").removeClassName("ajax-loading")',
75 75 :with => 'q')
76 76 %>
77 77
78 78 <div id="principals">
79 79 <%= principals_check_box_tags 'membership[user_ids][]', principals %>
80 80 </div>
81 81
82 82 <p><%= l(:label_role_plural) %>:
83 83 <% roles.each do |role| %>
84 84 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
85 85 <% end %></p>
86 86
87 87 <p><%= submit_tag l(:button_add), :id => 'member-add-submit' %></p>
88 88 </fieldset>
89 89 <% end %>
90 90 <% end %>
91 91 </div>
@@ -1,94 +1,101
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 PrincipalTest < ActiveSupport::TestCase
21 fixtures :users, :projects, :members, :member_roles
21 22
22 23 def test_active_scope_should_return_groups_and_active_users
23 24 result = Principal.active.all
24 25 assert_include Group.first, result
25 26 assert_not_nil result.detect {|p| p.is_a?(User)}
26 27 assert_nil result.detect {|p| p.is_a?(User) && !p.active?}
27 28 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
28 29 end
29 30
30 31 def test_member_of_scope_should_return_the_union_of_all_members
31 32 projects = Project.find_all_by_id(1, 2)
32 33 assert_equal projects.map(&:principals).flatten.sort, Principal.member_of(projects).sort
33 34 end
34 35
36 def test_not_member_of_scope_should_return_users_that_have_no_memberships
37 projects = Project.find_all_by_id(1, 2)
38 expected = (Principal.all - projects.map(&:memberships).flatten.map(&:principal)).sort
39 assert_equal expected, Principal.not_member_of(projects).sort
40 end
41
35 42 context "#like" do
36 43 setup do
37 44 Principal.generate!(:login => 'login')
38 45 Principal.generate!(:login => 'login2')
39 46
40 47 Principal.generate!(:firstname => 'firstname')
41 48 Principal.generate!(:firstname => 'firstname2')
42 49
43 50 Principal.generate!(:lastname => 'lastname')
44 51 Principal.generate!(:lastname => 'lastname2')
45 52
46 53 Principal.generate!(:mail => 'mail@example.com')
47 54 Principal.generate!(:mail => 'mail2@example.com')
48 55
49 56 @palmer = Principal.generate!(:firstname => 'David', :lastname => 'Palmer')
50 57 end
51 58
52 59 should "search login" do
53 60 results = Principal.like('login')
54 61
55 62 assert_equal 2, results.count
56 63 assert results.all? {|u| u.login.match(/login/) }
57 64 end
58 65
59 66 should "search firstname" do
60 67 results = Principal.like('firstname')
61 68
62 69 assert_equal 2, results.count
63 70 assert results.all? {|u| u.firstname.match(/firstname/) }
64 71 end
65 72
66 73 should "search lastname" do
67 74 results = Principal.like('lastname')
68 75
69 76 assert_equal 2, results.count
70 77 assert results.all? {|u| u.lastname.match(/lastname/) }
71 78 end
72 79
73 80 should "search mail" do
74 81 results = Principal.like('mail')
75 82
76 83 assert_equal 2, results.count
77 84 assert results.all? {|u| u.mail.match(/mail/) }
78 85 end
79 86
80 87 should "search firstname and lastname" do
81 88 results = Principal.like('david palm')
82 89
83 90 assert_equal 1, results.count
84 91 assert_equal @palmer, results.first
85 92 end
86 93
87 94 should "search lastname and firstname" do
88 95 results = Principal.like('palmer davi')
89 96
90 97 assert_equal 1, results.count
91 98 assert_equal @palmer, results.first
92 99 end
93 100 end
94 101 end
General Comments 0
You need to be logged in to leave comments. Login now