Project

General

Profile

Feature #43355 » 0001-Add-pagination-to-the-project-members-management-list.patch

Takenori TAKAKI, 2026-09-10 19:23

View differences:

app/controllers/application_controller.rb
682 682
    end
683 683
    per_page
684 684
  end
685
  helper_method :per_page_option
685 686

  
686 687
  # Returns offset and limit used to retrieve objects
687 688
  # for an API response based on offset, limit and page parameters
app/controllers/members_controller.rb
138 138
  private
139 139

  
140 140
  def redirect_to_settings_in_projects
141
    redirect_to settings_project_path(@project, :tab => 'members')
141
    redirect_to settings_project_path(@project, members_settings_url_params)
142
  end
143

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

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

  
51 67
  # Returns inheritance information for an inherited member role
52 68
  def render_role_inheritance(member, role)
53 69
    content = member.role_inheritance(role).filter_map do |h|
app/views/members/_edit.html.erb
1
<%= form_for(@member, :url => membership_path(@member),
1
<%= form_for(@member, :url => membership_path(@member, :members_page => params[:members_page]),
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), :remote => true, :method => :post do |f| %>
3
<%= form_for @member, :as => :membership, :url => project_memberships_path(@project, :members_page => params[:members_page]), :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 = @project.memberships.preload(:project).sorted.to_a %>
1
<% members, member_pages, member_count = paginate_members(@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), :remote => true, :class => "icon icon-add" %></p>
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>
7 7

  
8
<% if members.any? %>
8
<% if member_count > 0 %>
9
<div class="autoscroll">
9 10
<table class="list members">
10 11
  <thead>
11 12
    <tr>
......
32 33
  </td>
33 34
  <td class="buttons">
34 35
      <%= link_to sprite_icon('edit', l(:button_edit)),
35
                  edit_membership_path(member),
36
                  edit_membership_path(member, :members_page => params[:members_page]),
36 37
                  :remote => true,
37 38
                  :class => 'icon icon-edit' %>
38
      <%= remove_link membership_path(member),
39
      <%= remove_link membership_path(member, :members_page => params[:members_page]),
39 40
                      :remote => true,
40 41
                      :data => (!User.current.admin? && member.include?(User.current) ? {:confirm => l(:text_own_membership_delete_confirmation)} : {}) if member.deletable? %>
41 42
  </td>
......
44 45
<% end %>
45 46
  </tbody>
46 47
</table>
48
</div>
49
<span class="pagination">
50
  <%= pagination_links_full(member_pages, member_count) do |text, parameters, options|
51
        link_to text, settings_project_path(@project, 'members', request.query_parameters.merge(parameters)), options
52
      end %>
53
</span>
47 54
<% other_formats_links do |f| %>
48 55
  <%= f.link_to_with_query_parameters "CSV", {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
49 56
<% end %>
test/functional/members_controller_test.rb
224 224
    assert_redirected_to '/projects/ecookbook/settings/members'
225 225
  end
226 226

  
227
  def test_update_xhr_should_keep_members_tab_in_pagination_links
228
    project = Project.find(1)
229
    26.times { User.add_to_project(User.generate!, project) }
230
    @request.session[:user_id] = 2
231
    with_settings :per_page_options => '25,50,100' do
232
      put(
233
        :update,
234
        :params => {
235
          :id => 2,
236
          :membership => {:role_ids => [1]}
237
        },
238
        :xhr => true
239
      )
240
    end
241
    assert_response :success
242
    assert_match %r{settings/members\?members_page=}, response.body
243
  end
244

  
245
  def test_edit_xhr_should_keep_current_page_in_form_action
246
    @request.session[:user_id] = 2
247
    get(:edit, :params => {:id => 2, :members_page => 2}, :xhr => true)
248
    assert_response :success
249
    assert_match %r{/memberships/2\?members_page=2}, response.body
250
  end
251

  
252
  def test_new_xhr_should_keep_current_page_in_form_action
253
    @request.session[:user_id] = 2
254
    get(:new, :params => {:project_id => 1, :members_page => 2}, :xhr => true)
255
    assert_response :success
256
    assert_match %r{/projects/ecookbook/memberships\?members_page=2}, response.body
257
  end
258

  
227 259
  def test_update_locked_member_should_be_allowed
228 260
    User.find(3).lock!
229 261

  
test/functional/projects_controller_test.rb
1041 1041
    assert_select "tr#member-#{group_member.id} td.name a[href=?]", '/groups/10', :text => 'A Team'
1042 1042
  end
1043 1043

  
1044
  def test_settings_members_should_be_paginated
1045
    project = Project.find(1)
1046
    per_page = 25
1047
    (per_page + 5).times { User.add_to_project(User.generate!, project) }
1048
    @request.session[:user_id] = 2
1049
    with_settings :per_page_options => '25,50,100' do
1050
      get(
1051
        :settings,
1052
        :params => {:id => 'ecookbook', :tab => 'members'}
1053
      )
1054
    end
1055
    assert_response :success
1056
    assert_select 'div#tab-content-members table.list.members tbody tr.member', :count => per_page
1057
    assert_select 'div#tab-content-members span.pagination'
1058
    assert_select 'div#tab-content-members span.pagination a[href*=?]', 'settings/members?members_page='
1059
  end
1060

  
1061
  def test_settings_members_with_multiple_roles_should_not_appear_on_two_pages
1062
    project = Project.find(1)
1063
    30.times { User.add_to_project(User.generate!, project) }
1064
    Member.where(:project_id => project.id).first.update!(:role_ids => [1, 2])
1065
    @request.session[:user_id] = 2
1066
    page1 = page2 = nil
1067
    with_settings :per_page_options => '25,50,100' do
1068
      get(:settings, :params => {:id => 'ecookbook', :tab => 'members'})
1069
      page1 = css_select('div#tab-content-members tr.member').pluck('id')
1070
      get(:settings, :params => {:id => 'ecookbook', :tab => 'members', :members_page => 2})
1071
      page2 = css_select('div#tab-content-members tr.member').pluck('id')
1072
    end
1073
    assert_not page1.intersect?(page2), 'a member must not appear on more than one page'
1074
  end
1075

  
1076
  def test_settings_members_should_show_requested_page
1077
    project = Project.find(1)
1078
    per_page = 25
1079
    (per_page + 5).times { User.add_to_project(User.generate!, project) }
1080
    @request.session[:user_id] = 2
1081
    with_settings :per_page_options => '25,50,100' do
1082
      get(
1083
        :settings,
1084
        :params => {:id => 'ecookbook', :tab => 'members', :members_page => 2}
1085
      )
1086
    end
1087
    assert_response :success
1088
    assert_select 'div#tab-content-members table.list.members tbody tr.member'
1089
    assert_select 'div#tab-content-members span.pagination'
1090
  end
1091

  
1092
  def test_settings_members_with_out_of_range_page_should_keep_pagination
1093
    project = Project.find(1)
1094
    per_page = 25
1095
    (per_page + 5).times { User.add_to_project(User.generate!, project) }
1096
    @request.session[:user_id] = 2
1097
    with_settings :per_page_options => '25,50,100' do
1098
      get(
1099
        :settings,
1100
        :params => {:id => 'ecookbook', :tab => 'members', :members_page => 99}
1101
      )
1102
    end
1103
    assert_response :success
1104
    assert_select 'div#tab-content-members p.nodata', :count => 0
1105
    assert_select 'div#tab-content-members table.list.members'
1106
    assert_select 'div#tab-content-members span.pagination a[href*=?]', 'settings/members?members_page='
1107
  end
1108

  
1109
  def test_settings_members_without_members_should_show_no_data
1110
    project = Project.generate!
1111
    @request.session[:user_id] = 1
1112
    get(:settings, :params => {:id => project.id, :tab => 'members'})
1113
    assert_response :success
1114
    assert_select 'div#tab-content-members p.nodata'
1115
    assert_select 'div#tab-content-members table.list.members', :count => 0
1116
  end
1117

  
1044 1118
  def test_settings_should_show_tabs_depending_on_permission
1045 1119
    @request.session[:user_id] = 3
1046 1120
    project = Project.find(1)
test/helpers/members_helper_test.rb
39 39
    assert_select_in result, 'span.pagination li.current span', :text => '1'
40 40
    assert_select_in result, 'a[href=?]', "/projects/#{project.identifier}/memberships/autocomplete.js?page=2", :text => '2'
41 41
  end
42

  
43
  def test_paginate_members_returns_only_the_requested_page
44
    # per_page_option is provided by ApplicationController in the running app
45
    stubs(:per_page_option).returns(3)
46
    project = Project.generate!
47
    5.times { User.add_to_project(User.generate!, project) }
48

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

  
51
    assert_equal 3, members.size
52
    assert_equal 3, member_pages.per_page
53
    assert_equal project.memberships.count, member_count
54
  end
55

  
56
  def test_paginate_members_lists_a_member_with_several_roles_once
57
    stubs(:per_page_option).returns(3)
58
    project = Project.generate!
59
    3.times { User.add_to_project(User.generate!, project) }
60
    Member.where(:project_id => project.id).first.update!(:role_ids => [1, 2])
61

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

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