Project

General

Profile

Feature #44458 » 0001-Add-filter-form-to-project-members-settings-tab.patch

Marius BĂLTEANU, 2026-09-16 23:08

View differences:

app/controllers/members_controller.rb
143 143

  
144 144
  def members_settings_url_params
145 145
    query = {:tab => 'members'}
146
    query[:members_page] = params[:members_page] if params[:members_page].present?
147 146
    query[:per_page] = params[:per_page] if params[:per_page].present?
148
    query
147
    query.merge(members_list_params)
149 148
  end
150 149
end
app/helpers/members_helper.rb
48 48
    s + content_tag('span', links, :class => 'pagination')
49 49
  end
50 50

  
51
  # Returns the scope of the members of project matching the filters
52
  # set in the request params
53
  def members_scope(project)
54
    project.memberships.like(params[:member_name]).with_role(params[:member_role_id])
55
  end
56

  
51 57
  # limit/offset on Member.sorted would paginate role join rows, not members
52
  def paginate_members(project)
53
    ordered_ids = project.memberships.sorted.pluck("#{Member.table_name}.id").uniq
58
  def paginate_members(scope)
59
    ordered_ids = scope.sorted.pluck("#{Member.table_name}.id").uniq
54 60
    member_count = ordered_ids.size
55 61
    member_pages = Redmine::Pagination::Paginator.new(member_count, per_page_option, params['members_page'], 'members_page')
56 62
    page_ids = ordered_ids[member_pages.offset, member_pages.per_page] || []
57
    members_by_id = project.memberships.where(:id => page_ids).preload(:project, :principal, :roles).index_by(&:id)
63
    members_by_id = scope.where(:id => page_ids).preload(:project, :principal, :roles).index_by(&:id)
58 64
    members = page_ids.filter_map {|id| members_by_id[id]}
59 65
    [members, member_pages, member_count]
60 66
  end
61 67

  
68
  # Returns the params that define the members list currently displayed
69
  # (pagination and filters), so that they can be preserved across the member
70
  # creation, edition and deletion requests
71
  def members_list_params
72
    {
73
      :members_page => params[:members_page],
74
      :member_name => params[:member_name],
75
      :member_role_id => params[:member_role_id]
76
    }.select {|_, value| value.present?}
77
  end
78

  
62 79
  # Returns inheritance information for an inherited member role
63 80
  def render_role_inheritance(member, role)
64 81
    content = member.role_inheritance(role).filter_map do |h|
app/helpers/projects_helper.rb
24 24
        {:name => 'info', :action => :edit_project,
25 25
         :partial => 'projects/edit', :label => :label_project},
26 26
        {:name => 'members', :action => :manage_members,
27
         :partial => 'projects/settings/members', :label => :label_member_plural},
27
         :partial => 'projects/settings/members', :label => :label_member_plural,
28
         :url => {:tab => 'members'}.merge(members_list_params)},
28 29
        {:name => 'issues', :action => :edit_project, :module => :issue_tracking,
29 30
         :partial => 'projects/settings/issues', :label => :label_issue_tracking},
30 31
        {:name => 'versions', :action => :manage_versions,
app/models/member.rb
33 33
  scope :active, (lambda do
34 34
    joins(:principal).where(:users => {:status => Principal::STATUS_ACTIVE})
35 35
  end)
36
  # Members whose principal matches the given string
37
  scope :like, (lambda do |arg|
38
    if arg.present?
39
      where(:user_id => Principal.like(arg).select(:id))
40
    end
41
  end)
42
  # Members having the given role, inherited or not
43
  scope :with_role, (lambda do |arg|
44
    if arg.present?
45
      where(:id => MemberRole.where(:role_id => arg.to_i).select(:member_id))
46
    end
47
  end)
36 48
  # Sort by first role and principal
37 49
  scope :sorted, (lambda do
38 50
    includes(:member_roles, :roles, :principal).
app/views/members/_edit.html.erb
1
<%= form_for(@member, :url => membership_path(@member, :members_page => params[:members_page]),
1
<%= form_for(@member, :url => membership_path(@member, members_list_params),
2 2
                      :as => :membership,
3 3
                      :remote => request.xhr?,
4 4
                      :method => :put) do |f| %>
app/views/members/_new_modal.html.erb
1 1
<h3 class="title"><%= l(:label_member_new) %></h3>
2 2

  
3
<%= form_for @member, :as => :membership, :url => project_memberships_path(@project, :members_page => params[:members_page]), :remote => true, :method => :post do |f| %>
3
<%= form_for @member, :as => :membership, :url => project_memberships_path(@project, members_list_params), :remote => true, :method => :post do |f| %>
4 4
  <%= render :partial => 'new_form' %>
5 5
  <p class="buttons">
6 6
    <%= submit_tag l(:button_add), :id => 'member-add-submit' %>
app/views/projects/settings/_members.html.erb
1
<% members, member_pages, member_count = paginate_members(@project) %>
1
<% members, member_pages, member_count = paginate_members(members_scope(@project)) %>
2 2

  
3 3
<% if User.current.admin? %>
4 4
  <div class="contextual"><%= link_to sprite_icon('settings', l(:label_administration)), users_path, :class => "icon icon-settings" %></div>
5 5
<% end %>
6
<p><%= link_to sprite_icon('add', l(:label_member_new)), new_project_membership_path(@project, :members_page => params[:members_page]), :remote => true, :class => "icon icon-add" %></p>
6
<p><%= link_to sprite_icon('add', l(:label_member_new)), new_project_membership_path(@project, members_list_params), :remote => true, :class => "icon icon-add" %></p>
7

  
8
<%= form_tag(settings_project_path(@project, :tab => 'members'), :method => :get, :id => 'members-filter-form') do %>
9
<fieldset><legend><%= l(:label_filter_plural) %></legend>
10
<label for='member_name'><%= l(:label_user) %> / <%= l(:label_group) %>:</label>
11
<%= text_field_tag 'member_name', params[:member_name], :size => 20 %>
12
<label for='member_role_id'><%= l(:label_role) %>:</label>
13
<%= select_tag 'member_role_id',
14
               options_for_select([[l(:label_all), '']] + Role.givable.pluck(:name, :id), params[:member_role_id]),
15
               :onchange => "this.form.submit(); return false;" %>
16
<%= submit_tag l(:button_apply), :class => "small", :name => nil %>
17
<%= link_to sprite_icon('reload', l(:button_clear)), settings_project_path(@project, :tab => 'members'), :class => 'icon icon-reload' %>
18
</fieldset>
19
<% end %>
20
&nbsp;
7 21

  
8 22
<% if member_count > 0 %>
9 23
<div class="autoscroll">
......
33 47
  </td>
34 48
  <td class="buttons">
35 49
      <%= link_to sprite_icon('edit', l(:button_edit)),
36
                  edit_membership_path(member, :members_page => params[:members_page]),
50
                  edit_membership_path(member, members_list_params),
37 51
                  :remote => true,
38 52
                  :class => 'icon icon-edit' %>
39
      <%= remove_link membership_path(member, :members_page => params[:members_page]),
53
      <%= remove_link membership_path(member, members_list_params),
40 54
                      :remote => true,
41 55
                      :data => (!User.current.admin? && member.include?(User.current) ? {:confirm => l(:text_own_membership_delete_confirmation)} : {}) if member.deletable? %>
42 56
  </td>
test/functional/members_controller_test.rb
274 274
    assert_match %r{settings/members\?members_page=}, response.body
275 275
  end
276 276

  
277
  def test_edit_xhr_should_keep_current_filters_in_form_action
278
    @request.session[:user_id] = 2
279
    get(:edit, :params => {:id => 2, :member_name => 'Smith', :member_role_id => 1}, :xhr => true)
280
    assert_response :success
281
    assert_match %r{/memberships/2\?member_name=Smith&amp;member_role_id=1}, response.body
282
  end
283

  
284
  def test_new_xhr_should_keep_current_filters_in_form_action
285
    @request.session[:user_id] = 2
286
    get(:new, :params => {:project_id => 1, :member_name => 'Smith', :member_role_id => 1}, :xhr => true)
287
    assert_response :success
288
    assert_match %r{/projects/ecookbook/memberships\?member_name=Smith&amp;member_role_id=1}, response.body
289
  end
290

  
291
  def test_update_xhr_should_keep_the_members_list_filtered
292
    @request.session[:user_id] = 2
293
    put(
294
      :update,
295
      :params => {
296
        :id => 2,
297
        :membership => {:role_ids => [2]},
298
        :member_role_id => 1
299
      },
300
      :xhr => true
301
    )
302
    assert_response :success
303
    # The updated member does not match the filter anymore, but the list
304
    # must still be filtered by role 1
305
    assert_match %r{/memberships/1\?member_role_id=1}, response.body
306
    assert_no_match %r{member-4}, response.body
307
  end
308

  
309
  def test_destroy_xhr_should_keep_the_members_list_filtered
310
    @request.session[:user_id] = 2
311
    assert_difference 'Member.count', -1 do
312
      delete(:destroy, :params => {:id => 2, :member_name => 'Smith'}, :xhr => true)
313
    end
314
    assert_response :success
315
    assert_match %r{/memberships/1\?member_name=Smith}, response.body
316
    assert_no_match %r{member-4}, response.body
317
  end
318

  
319
  def test_destroy_should_redirect_to_the_filtered_members_list
320
    @request.session[:user_id] = 2
321
    assert_difference 'Member.count', -1 do
322
      delete(:destroy, :params => {:id => 2, :member_name => 'Smith', :member_role_id => 1})
323
    end
324
    assert_redirected_to '/projects/ecookbook/settings/members?member_name=Smith&member_role_id=1'
325
  end
326

  
277 327
  def test_update_locked_member_should_be_allowed
278 328
    User.find(3).lock!
279 329

  
test/functional/projects_controller_test.rb
1112 1112
    @request.session[:user_id] = 1
1113 1113
    get(:settings, :params => {:id => project.id, :tab => 'members'})
1114 1114
    assert_response :success
1115
    assert_select 'div#tab-content-members form#members-filter-form'
1115 1116
    assert_select 'div#tab-content-members p.nodata'
1116 1117
    assert_select 'div#tab-content-members table.list.members', :count => 0
1117 1118
  end
1118 1119

  
1120
  def test_settings_members_should_display_filter_form
1121
    @request.session[:user_id] = 2
1122
    get(:settings, :params => {:id => 'ecookbook', :tab => 'members'})
1123
    assert_response :success
1124
    assert_select 'div#tab-content-members form#members-filter-form[action=?]', '/projects/ecookbook/settings/members' do
1125
      assert_select 'input[name=member_name]'
1126
      assert_select 'select[name=member_role_id][onchange=?]', 'this.form.submit(); return false;'
1127
      assert_select 'input[type=submit]'
1128
      assert_select 'a[href=?]', '/projects/ecookbook/settings/members'
1129
    end
1130
  end
1131

  
1132
  def test_settings_members_should_filter_by_name
1133
    @request.session[:user_id] = 2
1134
    get(
1135
      :settings,
1136
      :params => {
1137
        :id => 'ecookbook',
1138
        :tab => 'members',
1139
        :member_name => 'John'
1140
      }
1141
    )
1142
    assert_response :success
1143
    assert_select 'div#tab-content-members form#members-filter-form' do
1144
      assert_select 'input[name=member_name][value=?]', 'John'
1145
    end
1146
    assert_select 'div#tab-content-members tr#member-1'
1147
    assert_select 'div#tab-content-members tr#member-2', :count => 0
1148
    assert_select 'a#tab-members[href*=?]', 'member_name=John'
1149
  end
1150

  
1151
  def test_settings_members_should_filter_by_role
1152
    @request.session[:user_id] = 2
1153
    get(
1154
      :settings,
1155
      :params => {
1156
        :id => 'ecookbook',
1157
        :tab => 'members',
1158
        :member_role_id => '2'
1159
      }
1160
    )
1161
    assert_response :success
1162
    assert_select 'div#tab-content-members form#members-filter-form' do
1163
      assert_select 'select[name=member_role_id]' do
1164
        assert_select 'option[value="2"][selected=selected]'
1165
      end
1166
    end
1167
    assert_select 'div#tab-content-members tr#member-2'
1168
    assert_select 'div#tab-content-members tr#member-1', :count => 0
1169
    assert_select 'a#tab-members[href*=?]', 'member_role_id=2'
1170
  end
1171

  
1172
  def test_settings_members_filter_with_no_matches_should_show_no_data
1173
    @request.session[:user_id] = 2
1174
    get(
1175
      :settings,
1176
      :params => {
1177
        :id => 'ecookbook',
1178
        :tab => 'members',
1179
        :member_name => 'NonexistentPrincipal'
1180
      }
1181
    )
1182
    assert_response :success
1183
    assert_select 'div#tab-content-members form#members-filter-form'
1184
    assert_select 'div#tab-content-members p.nodata'
1185
    assert_select 'div#tab-content-members table.list.members', :count => 0
1186
  end
1187

  
1188
  def test_settings_members_pagination_should_preserve_filters
1189
    project = Project.find(1)
1190
    per_page = 25
1191
    (per_page + 5).times do
1192
      user = User.generate!
1193
      Member.create!(:project => project, :principal => user, :role_ids => [2])
1194
    end
1195
    @request.session[:user_id] = 2
1196
    with_settings :per_page_options => '25,50,100' do
1197
      get(
1198
        :settings,
1199
        :params => {
1200
          :id => 'ecookbook',
1201
          :tab => 'members',
1202
          :member_role_id => '2'
1203
        }
1204
      )
1205
    end
1206
    assert_response :success
1207
    assert_select 'div#tab-content-members span.pagination a[href*=?]', 'member_role_id=2'
1208
  end
1209

  
1119 1210
  def test_settings_should_show_tabs_depending_on_permission
1120 1211
    @request.session[:user_id] = 3
1121 1212
    project = Project.find(1)
test/helpers/members_helper_test.rb
46 46
    project = Project.generate!
47 47
    5.times { User.add_to_project(User.generate!, project) }
48 48

  
49
    members, member_pages, member_count = paginate_members(project)
49
    members, member_pages, member_count = paginate_members(project.memberships)
50 50

  
51 51
    assert_equal 3, members.size
52 52
    assert_equal 3, member_pages.per_page
......
59 59
    3.times { User.add_to_project(User.generate!, project) }
60 60
    Member.where(:project_id => project.id).first.update!(:role_ids => [1, 2])
61 61

  
62
    members, _member_pages, member_count = paginate_members(project)
62
    members, _member_pages, member_count = paginate_members(project.memberships)
63 63

  
64 64
    member_ids = members.map(&:id)
65 65
    assert_equal member_ids.uniq, member_ids
66 66
    assert_equal member_count, members.size
67 67
    assert_equal project.memberships.count, member_count
68 68
  end
69

  
70
  def test_paginate_members_should_paginate_the_given_scope
71
    stubs(:per_page_option).returns(25)
72
    project = Project.find(1)
73

  
74
    members, _member_pages, member_count = paginate_members(project.memberships.with_role(2))
75

  
76
    assert_equal [2, 4], members.map(&:id).sort
77
    assert_equal 2, member_count
78
  end
69 79
end
test/unit/member_test.rb
33 33
    assert_equal roles, roles.sort
34 34
  end
35 35

  
36
  def test_like_scope_should_match_users_and_groups
37
    assert_equal [1], Project.find(1).memberships.like('Smith').ids
38
    assert_equal [9], Project.find(2).memberships.like('B Team').ids
39
  end
40

  
41
  def test_like_scope_with_blank_value_should_return_all_the_members
42
    project = Project.find(1)
43
    assert_equal project.memberships.ids.sort, project.memberships.like('').ids.sort
44
  end
45

  
46
  def test_with_role_scope_should_return_the_members_having_the_role
47
    assert_equal [2, 4], Project.find(1).memberships.with_role(2).ids.sort
48
  end
49

  
50
  def test_with_role_scope_should_return_the_members_having_the_role_inherited
51
    # Member 7 has the role 1 inherited from the member 6
52
    assert_include 7, Project.find(5).memberships.with_role(1).ids
53
  end
54

  
55
  def test_with_role_scope_with_blank_value_should_return_all_the_members
56
    project = Project.find(1)
57
    assert_equal project.memberships.ids.sort, project.memberships.with_role(nil).ids.sort
58
  end
59

  
36 60
  def test_create
37 61
    member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2])
38 62
    assert member.save
(2-2/2)