Feature #44601 » 0001-Show-the-first-members-of-each-role-with-avatars-and.patch
| app/assets/stylesheets/application.css | ||
|---|---|---|
| 795 | 795 | |
| 796 | 796 |
td.center {text-align:center;}
|
| 797 | 797 | |
| 798 |
.members.box .avatar {margin-block: 0 2px; margin-inline: 0 4px;}
|
|
| 799 |
.members.box .badge {inset-block-end: 0;}
|
|
| 800 |
.members.box a.badge-count, .members.box a.badge-count:hover {color: var(--oc-white); text-decoration: none;}
|
|
| 798 | 801 |
#watchers select {inline-size: 95%; display: block;}
|
| 799 | 802 |
#watchers .avatar {margin-block: 0 2px; margin-inline: 0 4px;}
|
| 800 | 803 |
#watchers svg.icon-svg {margin-block: 0 2px; margin-inline: 0 2px;}
|
| app/controllers/projects_controller.rb | ||
|---|---|---|
| 175 | 175 | |
| 176 | 176 |
respond_to do |format| |
| 177 | 177 |
format.html do |
| 178 |
@principals_by_role = @project.principals_by_role
|
|
| 178 |
@roles_with_principals = @project.roles_with_principals
|
|
| 179 | 179 |
@subprojects = @project.leaf? ? [] : @project.children.visible.to_a |
| 180 | 180 |
@news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
|
| 181 | 181 |
with_subprojects = Setting.display_subprojects_issues? |
| app/helpers/application_helper.rb | ||
|---|---|---|
| 76 | 76 |
end |
| 77 | 77 | |
| 78 | 78 |
css_classes += " #{options[:class]}" if css_classes && options[:class].present?
|
| 79 |
url ? link_to(principal_icon(principal).to_s + name, url, :class => css_classes) : h(name) |
|
| 79 |
return h(name) unless url |
|
| 80 | ||
| 81 |
icon = |
|
| 82 |
if options[:avatar] |
|
| 83 |
avatar(principal, :size => (options[:avatar] == true ? 16 : options[:avatar])) |
|
| 84 |
else |
|
| 85 |
principal_icon(principal) |
|
| 86 |
end |
|
| 87 |
link_to(safe_join([icon, name].compact), url, :class => css_classes) |
|
| 80 | 88 |
end |
| 81 | 89 | |
| 82 | 90 |
def link_to_mention(user, object, options={})
|
| app/helpers/projects_helper.rb | ||
|---|---|---|
| 18 | 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 19 | 19 | |
| 20 | 20 |
module ProjectsHelper |
| 21 |
# Returns a badge with the number of members of the role that are not listed |
|
| 22 |
# in the project members box (ie. beyond +displayed_count+ out of |
|
| 23 |
# +total_count+), linked to the members settings tab filtered on that role |
|
| 24 |
# for the users allowed to manage the members of the project. Returns nil if |
|
| 25 |
# every member of the role is already displayed. |
|
| 26 |
def members_not_displayed_badge(project, role, total_count, displayed_count) |
|
| 27 |
remaining_count = total_count - displayed_count |
|
| 28 |
return if remaining_count <= 0 |
|
| 29 | ||
| 30 |
options = {:class => 'badge badge-count', :title => l(:label_x_more_members, :count => remaining_count)}
|
|
| 31 |
if User.current.allowed_to?(:manage_members, project) |
|
| 32 |
link_to("+#{remaining_count}", settings_project_path(project, :tab => 'members', :member_role_id => role.id), options)
|
|
| 33 |
else |
|
| 34 |
content_tag('span', "+#{remaining_count}", options)
|
|
| 35 |
end |
|
| 36 |
end |
|
| 37 | ||
| 21 | 38 |
def project_settings_tabs |
| 22 | 39 |
tabs = |
| 23 | 40 |
[ |
| app/models/project.rb | ||
|---|---|---|
| 590 | 590 |
end |
| 591 | 591 |
end |
| 592 | 592 | |
| 593 |
# Returns an array of [Role, principals, count] for the active members of the |
|
| 594 |
# project, ordered by role position. Only the first +limit+ principals are |
|
| 595 |
# returned for each role, count being the total number of members with the role. |
|
| 596 |
def roles_with_principals(limit: 10) |
|
| 597 |
# One row per member and role: enough to count the members of each role and |
|
| 598 |
# to pick the first ones of each, without loading every member of the project |
|
| 599 |
role_and_user_ids = |
|
| 600 |
memberships.active.joins(:member_roles). |
|
| 601 |
reorder(Principal.fields_for_order_statement). |
|
| 602 |
pluck("#{MemberRole.table_name}.role_id", "#{Member.table_name}.user_id").uniq
|
|
| 603 |
return [] if role_and_user_ids.empty? |
|
| 604 | ||
| 605 |
user_ids_by_role_id = role_and_user_ids.group_by(&:first).transform_values {|rows| rows.map(&:last)}
|
|
| 606 |
principals = |
|
| 607 |
Principal.where(:id => user_ids_by_role_id.values.flat_map {|ids| ids.first(limit)}).index_by(&:id)
|
|
| 608 | ||
| 609 |
Role.where(:id => user_ids_by_role_id.keys).sorted.map do |role| |
|
| 610 |
user_ids = user_ids_by_role_id[role.id] |
|
| 611 |
[role, user_ids.first(limit).filter_map {|user_id| principals[user_id]}, user_ids.size]
|
|
| 612 |
end |
|
| 613 |
end |
|
| 614 | ||
| 593 | 615 |
# Adds user as a project member with the default role |
| 594 | 616 |
# Used for when a non-admin user creates a project |
| 595 | 617 |
def add_default_member(user) |
| app/views/projects/_members_box.html.erb | ||
|---|---|---|
| 1 |
<% if @principals_by_role.any? %>
|
|
| 1 |
<% if @roles_with_principals.any? %>
|
|
| 2 | 2 |
<div class="members box"> |
| 3 | 3 |
<h3 class="icon icon-group"><%= sprite_icon('group', l(:label_member_plural)) %></h3>
|
| 4 |
<% @principals_by_role.keys.sort.each do |role| %> |
|
| 5 |
<p><span class="label"><%= role %>:</span> <%= @principals_by_role[role].sort.collect{|p| link_to_principal(p, :class => p.is_a?(Group) ? 'icon icon-group' : nil)}.join(", ").html_safe %></p>
|
|
| 4 |
<% @roles_with_principals.each do |role, principals, count| %> |
|
| 5 |
<p> |
|
| 6 |
<span class="label"><%= role %>:</span> |
|
| 7 |
<%= safe_join(principals.collect {|principal| link_to_principal(principal, :avatar => true)}, ', ') %>
|
|
| 8 |
<%= members_not_displayed_badge(@project, role, count, principals.size) %> |
|
| 9 |
</p> |
|
| 6 | 10 |
<% end %> |
| 7 | 11 |
<% if User.current.allowed_to?(:view_members, @project) %> |
| 8 | 12 |
<p><%= link_to l(:label_member_view_all), project_members_path(@project) %></p> |
| config/locales/en.yml | ||
|---|---|---|
| 675 | 675 |
label_member_new: New member |
| 676 | 676 |
label_member_plural: Members |
| 677 | 677 |
label_member_view_all: View all members |
| 678 |
label_x_more_members: |
|
| 679 |
one: 1 more member |
|
| 680 |
other: "%{count} more members"
|
|
| 678 | 681 |
label_tracker: Tracker |
| 679 | 682 |
label_tracker_plural: Trackers |
| 680 | 683 |
label_tracker_all: All trackers |
| test/functional/projects_controller_test.rb | ||
|---|---|---|
| 802 | 802 |
assert_select '#main.nosidebar' |
| 803 | 803 |
end |
| 804 | 804 | |
| 805 |
def test_show_should_display_members_box_with_avatars |
|
| 806 |
@request.session[:user_id] = 2 |
|
| 807 |
get(:show, :params => {:id => 'ecookbook'})
|
|
| 808 |
assert_response :success |
|
| 809 | ||
| 810 |
assert_select 'div.members.box' do |
|
| 811 |
assert_select 'p span.label', :text => 'Manager:' |
|
| 812 |
assert_select 'p a.user[href=?]', '/users/2' do |
|
| 813 |
assert_select '.avatar' |
|
| 814 |
end |
|
| 815 |
end |
|
| 816 |
end |
|
| 817 | ||
| 818 |
def test_show_should_link_the_number_of_members_not_displayed_of_a_role |
|
| 819 |
# 12 active members with the role, 10 of them displayed |
|
| 820 |
11.times {User.add_to_project(User.generate!, Project.find(1), Role.find(2))}
|
|
| 821 |
@request.session[:user_id] = 2 |
|
| 822 |
get(:show, :params => {:id => 'ecookbook'})
|
|
| 823 |
assert_response :success |
|
| 824 | ||
| 825 |
assert_select 'div.members.box' do |
|
| 826 |
assert_select 'p a.user', :count => 10 + 1 |
|
| 827 |
assert_select( |
|
| 828 |
'p a.badge[href=?][title=?]', |
|
| 829 |
'/projects/ecookbook/settings/members?member_role_id=2', '2 more members', |
|
| 830 |
:text => '+2' |
|
| 831 |
) |
|
| 832 |
end |
|
| 833 |
end |
|
| 834 | ||
| 835 |
def test_show_should_not_link_the_number_of_members_not_displayed_without_manage_members_permission |
|
| 836 |
11.times {User.add_to_project(User.generate!, Project.find(1), Role.find(2))}
|
|
| 837 |
@request.session[:user_id] = 7 |
|
| 838 |
get(:show, :params => {:id => 'ecookbook'})
|
|
| 839 |
assert_response :success |
|
| 840 | ||
| 841 |
assert_select 'div.members.box' do |
|
| 842 |
assert_select 'p span.badge[title=?]', '2 more members', :text => '+2' |
|
| 843 |
assert_select 'p a[href*=?]', 'settings/members', :count => 0 |
|
| 844 |
end |
|
| 845 |
end |
|
| 846 | ||
| 805 | 847 |
def test_show_should_display_visible_custom_fields |
| 806 | 848 |
ProjectCustomField.find_by_name('Development status').update_attribute :visible, true
|
| 807 | 849 |
get(:show, :params => {:id => 'ecookbook'})
|
| test/helpers/application_helper_test.rb | ||
|---|---|---|
| 1808 | 1808 |
assert_equal result, link_to_principal(user) |
| 1809 | 1809 |
end |
| 1810 | 1810 | |
| 1811 |
def test_link_to_principal_with_avatar_should_replace_the_icon_with_an_avatar |
|
| 1812 |
html = link_to_principal(User.find(2), :avatar => true) |
|
| 1813 |
assert_select_in html, 'a.user.active[href=?]', '/users/2' do |
|
| 1814 |
assert_select '.avatar' |
|
| 1815 |
assert_select 'svg', :count => 0 |
|
| 1816 |
end |
|
| 1817 |
end |
|
| 1818 | ||
| 1819 |
def test_link_to_principal_with_avatar_size |
|
| 1820 |
html = link_to_principal(Group.find(10), :avatar => 24) |
|
| 1821 |
assert_select_in html, 'a.group[href=?]', '/groups/10' do |
|
| 1822 |
assert_select 'img.avatar[width=?]', '24' |
|
| 1823 |
end |
|
| 1824 |
end |
|
| 1825 | ||
| 1811 | 1826 |
def test_link_to_principal_should_link_to_group |
| 1812 | 1827 |
group = Group.find(10) |
| 1813 | 1828 |
result = %r{<a class="group" href="/groups/10"><svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-\w+.svg#icon--group"></use></svg>A Team</a>}
|
| test/unit/project_test.rb | ||
|---|---|---|
| 480 | 480 |
assert_not principals_by_role.values.flatten.include?(locked_user) |
| 481 | 481 |
end |
| 482 | 482 | |
| 483 |
def test_roles_with_principals |
|
| 484 |
group = Group.find(10) |
|
| 485 |
Member.create!(:principal => group, :project_id => 1, :role_ids => [1]) |
|
| 486 | ||
| 487 |
roles_with_principals = Project.find(1).roles_with_principals |
|
| 488 |
assert_kind_of Array, roles_with_principals |
|
| 489 | ||
| 490 |
role, principals, count = roles_with_principals.detect {|r, _, _| r == Role.find(1)}
|
|
| 491 |
assert_equal Role.find(1), role |
|
| 492 |
# The group, and the users it gives the role to, are members with the role too |
|
| 493 |
assert_equal 3, count |
|
| 494 |
assert_include User.find(2), principals |
|
| 495 |
assert_include group, principals |
|
| 496 |
# Groups are listed after the users |
|
| 497 |
assert_equal group, principals.last |
|
| 498 |
end |
|
| 499 | ||
| 500 |
def test_roles_with_principals_should_be_ordered_by_role_position |
|
| 501 |
roles = Project.find(1).roles_with_principals.map(&:first) |
|
| 502 |
assert_equal roles.sort_by(&:position), roles |
|
| 503 |
end |
|
| 504 | ||
| 505 |
def test_roles_with_principals_should_only_return_active_principals |
|
| 506 |
locked_user = User.find(5) |
|
| 507 |
assert_include locked_user, Project.find(1).memberships.map(&:principal) |
|
| 508 | ||
| 509 |
roles_with_principals = Project.find(1).roles_with_principals |
|
| 510 |
assert_not_include locked_user, roles_with_principals.flat_map {|_, principals, _| principals}
|
|
| 511 |
end |
|
| 512 | ||
| 513 |
def test_roles_with_principals_should_limit_the_principals_but_not_the_count |
|
| 514 |
project = Project.find(1) |
|
| 515 |
role = Role.find(2) |
|
| 516 |
11.times {User.add_to_project(User.generate!, project, role)}
|
|
| 517 | ||
| 518 |
_, principals, count = project.roles_with_principals.detect {|r, _, _| r == role}
|
|
| 519 |
assert_equal 10, principals.size |
|
| 520 |
assert_equal 12, count |
|
| 521 |
end |
|
| 522 | ||
| 523 |
def test_roles_with_principals_should_list_a_member_under_each_of_its_roles |
|
| 524 |
member = Member.find(1) |
|
| 525 |
member.update!(:role_ids => [1, 2]) |
|
| 526 | ||
| 527 |
roles_with_principals = Project.find(1).roles_with_principals |
|
| 528 |
[1, 2].each do |role_id| |
|
| 529 |
_, principals, = roles_with_principals.detect {|r, _, _| r == Role.find(role_id)}
|
|
| 530 |
assert_include member.principal, principals |
|
| 531 |
end |
|
| 532 |
end |
|
| 533 | ||
| 534 |
def test_roles_with_principals_should_not_include_the_members_of_other_projects |
|
| 535 |
principals = Project.find(1).roles_with_principals.flat_map {|_, p, _| p}
|
|
| 536 |
assert_not_include User.find(8), principals |
|
| 537 |
end |
|
| 538 | ||
| 539 |
def test_roles_with_principals_should_return_an_empty_array_without_members |
|
| 540 |
assert_equal [], Project.generate!.roles_with_principals |
|
| 541 |
end |
|
| 542 | ||
| 483 | 543 |
def test_rolled_up_trackers |
| 484 | 544 |
parent = Project.find(1) |
| 485 | 545 |
parent.trackers = Tracker.find([1, 2]) |
- « Previous
- 1
- 2
- Next »