From 91c39e79772ca11bc2c365a1d50b3688fe6a3e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C4=82LTEANU?= Date: Mon, 23 Jun 2025 23:17:28 +0300 Subject: [PATCH] Render user's initals by default when Gravatar services is disabled (#29824). --- app/assets/stylesheets/application.css | 123 +++++++++++++++--- app/helpers/avatars_helper.rb | 5 + app/models/group.rb | 4 + app/views/issues/show.html.erb | 4 +- app/views/layouts/base.html.erb | 8 +- lib/plugins/gravatar/lib/gravatar.rb | 2 +- lib/redmine/helpers/gantt.rb | 6 +- test/functional/calendars_controller_test.rb | 2 +- test/functional/gantts_controller_test.rb | 2 +- test/functional/issues_controller_test.rb | 27 ++-- test/functional/messages_controller_test.rb | 2 +- test/functional/news_controller_test.rb | 2 +- test/helpers/application_helper_test.rb | 14 -- test/helpers/avatars_helper_test.rb | 10 +- .../api_test/authentication_test.rb | 8 +- 15 files changed, 148 insertions(+), 71 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 833e998f8..58e34bbbf 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -413,7 +413,7 @@ tr.entry.file td.filename a { margin-left: 26px; } tr.entry.file td.filename_no_report a { margin-left: 16px; } tr span.expander, .gantt_subjects div > span.expander {margin-left: 0; cursor: pointer;} -.gantt_subjects div > span .icon-gravatar {float: none;} +.gantt_subjects .avatar {margin-right: 4px;} .gantt_subjects div.project-name a, .gantt_subjects div.version-name a {margin-left: 4px;} tr.changeset { height: 20px } @@ -445,7 +445,7 @@ tr.version:not(.shared) td.name { padding-left: 20px; } tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; } #principals_for_new_member .icon-user, #users_for_watcher .icon-user {background:transparent;} -#principals_for_new_member svg, #principals_for_new_member img {margin-right: 4px;} +#principals_for_new_member svg, #principals_for_new_member .avatar {margin-right: 4px;} tr.user td {width:13%;white-space: nowrap;} td.username, td.firstname, td.lastname, td.email {text-align:left !important;} @@ -549,9 +549,9 @@ body.controller-gantts fieldset#options > div > div { td.center {text-align:center;} #watchers select {width: 95%; display: block;} -#watchers img.gravatar {margin: 0 4px 2px 0;} +#watchers .avatar {margin: 0 4px 2px 0;} #watchers svg.icon-svg {margin: 0 2px 2px 0;} -#users_for_watcher img.gravatar {padding-bottom: 2px; margin-right: 4px;} +#users_for_watcher .avatar {padding-bottom: 2px; margin-right: 4px;} #users_for_watcher svg {margin-right: 4px;} #users_for_watcher span.icon-user {display: inline;} @@ -1445,7 +1445,7 @@ p.cal.legend span {display:flex;} .tooltip span.tip{display: none; text-align:left;} .tooltip span.tip a { color: #169 !important; } -.tooltip span.tip img.gravatar { +.tooltip span.tip .avatar { float: none; margin: 0; } @@ -1799,9 +1799,6 @@ table.gantt-table td { } .gantt_subjects div.issue-subject:hover { background-color:#ffffdd; } .gantt_selected_column_content { padding-left: 3px; padding-right: 3px;} -.gantt_subjects .issue-subject img.icon-gravatar { - margin: 2px 5px 0px 2px; -} .gantt_hdr_selected_column_name { position: absolute; @@ -2112,21 +2109,16 @@ tr.ui-sortable-helper { border:1px solid #e4e4e4; } .contextual>*:not(:first-child), .buttons>.icon:not(:first-child), .contextual .journal-actions>*:not(:first-child) { margin-left: 5px; } -img.gravatar { - vertical-align: middle; - border-radius: 20%; -} - -div.issue img.gravatar { +div.issue .avatar { float: left; margin: 0 12px 6px 0; } -div.gravatar-with-child { +div.avatar-with-child { position: relative; } -div.gravatar-with-child > img.gravatar:nth-child(2) { +div.avatar-with-child > .avatar:nth-child(2) { position: absolute; top: 30px; left: 30px; @@ -2134,12 +2126,11 @@ div.gravatar-with-child > img.gravatar:nth-child(2) { border: 2px solid rgba(255, 255, 255, 0.9); } -h2 img.gravatar, h3 img.gravatar {margin-right: 4px;} +h2 .avatar, h3 .avatar {margin-right: 4px;} h4 img.gravatar {margin: -2px 4px -4px 0;} +/*# TODO: check where this rule is still used*/ td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;} -#activity dt img.gravatar {margin: 0 1em 0 0;} -/* Used on 12px Gravatar img tags without the icon background */ -.icon-gravatar {float: left; margin-right: 4px;} +#activity dt .avatar {margin: 0 1em 0 0;} #activity dt, .journal {clear: left;} @@ -2162,6 +2153,98 @@ color: #555; text-shadow: 1px 1px 0 #fff; img.filecontent.image {background-image: url(/transparent.png);} +/* Avatar styles */ +.avatar { + border-radius: 20%; + display: inline-flex; + vertical-align: middle; +} + +span[role="img"].avatar { + font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; + align-items: center; + display: inline-flex; + font-size: calc(24px * .4); + justify-content: center; + user-select: none; + font-weight: 700; +} +.avatar.s13 { + block-size: 13px; + inline-size: 13px; +} +span[role="img"].avatar.s13 { + font-size: calc(16px * .3); +} +.avatar.s16 { + block-size: 16px; + inline-size: 16px; +} +span[role="img"].avatar.s16 { + font-size: calc(16px * .4); +} +.avatar.s22 { + block-size: 22px; + inline-size: 22px; +} +span[role="img"].avatar.s22 { + font-size: calc(22px * .4); +} +.avatar.s24 { + block-size: 24px; + inline-size: 24px; +} +span[role="img"].avatar.s24 { + font-size: calc(24px * .4); +} +.avatar.s40 { + block-size: 40px; + inline-size: 40px; +} +span[role="img"].avatar.s40 { + font-size: calc(40px * .4); +} +.avatar.s50 { + block-size: 50px; + inline-size: 50px; +} +span[role="img"].avatar.s50 { + font-size: calc(50px * .4); +} + +.avatar-color-0 { + background-color: #880000; + color: #FFFFFF; +} +.avatar-color-1 { + background-color: #ff0000; + color: #000000; +} +.avatar-color-2 { + background-color: #00ff00; + color: #000000; +} +.avatar-color-3 { + background-color: #008800; + color: #FFFFFF; +} +.avatar-color-4 { + background-color: #0000ff; + color: #FFFFFF; +} +.avatar-color-5 { + background-color: #000088; + color: #FFFFFF; +} +.avatar-color-6 { + background-color: #ff8800; + color: #000000; +} +.avatar-color-7 { + background-color: #ff0088; + color: #000000; +} + /* Reaction styles */ .reaction-button:hover, .reaction-button:active { text-decoration: none; diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 88a571b62..1c7fb23a4 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -57,6 +57,11 @@ module AvatarsHelper else nil end + elsif user.respond_to?(:initials) + size = options.delete(:size) || GravatarHelper::DEFAULT_OPTIONS[:size] + css_class = "avatar-color-#{user.id % 8} avatar s#{size}" + css_class += " #{options[:class]}" if options[:class] + content_tag('span', user.initials, role: 'img', class: css_class) else '' end diff --git a/app/models/group.rb b/app/models/group.rb index 300b59b46..1ba876f3c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -53,6 +53,10 @@ class Group < Principal name.to_s end + def initials + name[0, 1] + end + def builtin_type nil end diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb index e41a91bb3..462b33984 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -32,9 +32,9 @@ <% end %> -
+
<%= author_avatar(@issue.author, :size => "50") %> - <%= assignee_avatar(@issue.assigned_to, :size => "22", :class => "gravatar-child") if @issue.assigned_to %> + <%= assignee_avatar(@issue.assigned_to, :size => "22", :class => "avatar-child") if @issue.assigned_to %>
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index e982f534c..9e2ef51b5 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -36,11 +36,9 @@ <% end %> <% if User.current.logged? %> -
- <% if Setting.gravatar_enabled? %> - <%= link_to(avatar(User.current, :size => "80"), user_path(User.current)) %> - <% end %> - <%= link_to_user(User.current, :format => :username) %> +
+ <%= link_to(avatar(User.current, :size => "40"), user_path(User.current)) %> + <%= link_to_user(User.current, :format => :username) %>
<% end %> diff --git a/lib/plugins/gravatar/lib/gravatar.rb b/lib/plugins/gravatar/lib/gravatar.rb index 316a01b19..43820008f 100644 --- a/lib/plugins/gravatar/lib/gravatar.rb +++ b/lib/plugins/gravatar/lib/gravatar.rb @@ -32,7 +32,7 @@ module GravatarHelper :title => '', # The class to assign to the img tag for the gravatar. - :class => 'gravatar', + :class => 'gravatar avatar', } # The methods that will be made available to your views. diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb index 523ae3188..d23c40b38 100644 --- a/lib/redmine/helpers/gantt.rb +++ b/lib/redmine/helpers/gantt.rb @@ -731,7 +731,7 @@ module Redmine css_classes = +'' css_classes << ' issue-overdue' if issue.overdue? css_classes << ' issue-behind-schedule' if issue.behind_schedule? - css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to + css_classes << ' icon icon-issue' unless issue.assigned_to css_classes << ' issue-closed' if issue.closed? if issue.start_date && issue.due_before && issue.done_ratio progress_date = calc_progress_date(issue.start_date, @@ -740,8 +740,8 @@ module Redmine css_classes << ' over-end-date' if progress_date > self.date_to && issue.done_ratio > 0 end s = (+"").html_safe - s << view.sprite_icon('issue').html_safe unless Setting.gravatar_enabled? && issue.assigned_to - s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-gravatar') + s << view.sprite_icon('issue').html_safe unless issue.assigned_to + s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-avatar') s << view.link_to_issue(issue).html_safe s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => issue.id, :style => 'display:none;', diff --git a/test/functional/calendars_controller_test.rb b/test/functional/calendars_controller_test.rb index 227919435..202c076ac 100644 --- a/test/functional/calendars_controller_test.rb +++ b/test/functional/calendars_controller_test.rb @@ -57,7 +57,7 @@ class CalendarsControllerTest < Redmine::ControllerTest ) do assert_select 'a.issue[href=?]', '/issues/2', :text => 'Feature request #2' assert_select 'span.tip' do - assert_select 'img[class="gravatar"]' + assert_select 'img[class="gravatar avatar"]' end assert_select 'input[name=?][type=?][value=?]', 'ids[]', 'checkbox', '2' end diff --git a/test/functional/gantts_controller_test.rb b/test/functional/gantts_controller_test.rb index daba816b8..73a74ba65 100644 --- a/test/functional/gantts_controller_test.rb +++ b/test/functional/gantts_controller_test.rb @@ -58,7 +58,7 @@ class GanttsControllerTest < Redmine::ControllerTest # Assert context menu on issues subject and gantt bar assert_select 'div[class=?]', 'issue-subject hascontextmenu' assert_select 'div.tooltip.hascontextmenu' do - assert_select 'img[class="gravatar"]' + assert_select 'img[class="gravatar avatar"]' end assert_select "form[data-cm-url=?]", '/issues/context_menu' diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 48304c868..90cf1ecd7 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -2816,7 +2816,7 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'h3', {text: /Watchers \(\d*\)/, count: 0} end - def test_show_should_display_watchers_with_gravatars + def test_show_should_display_watchers_with_avatars @request.session[:user_id] = 2 issue = Issue.find(1) issue.add_watcher User.find(2) @@ -2824,9 +2824,10 @@ class IssuesControllerTest < Redmine::ControllerTest with_settings :gravatar_enabled => '1' do get(:show, :params => {:id => 1}) end + assert_select 'div#watchers ul' do assert_select 'li.user-2' do - assert_select 'img.gravatar[title=?]', 'John Smith' + assert_select '.avatar[title=?]', 'John Smith' assert_select 'a[href="/users/2"]' assert_select 'a[class*=delete]' end @@ -8786,31 +8787,27 @@ class IssuesControllerTest < Redmine::ControllerTest assert_select 'a[href=?][onclick=?]', "/issues/1", "", :text => 'Cancel' end - def test_show_should_display_author_gravatar_only_when_not_assigned + def test_show_should_display_author_avatar_only_when_not_assigned issue = Issue.find(1) assert_nil issue.assigned_to_id @request.session[:user_id] = 1 - with_settings :gravatar_enabled => '1' do - get :show, :params => {:id => issue.id} - assert_select 'div.gravatar-with-child' do - assert_select 'img.gravatar', 1 - end + get :show, :params => {:id => issue.id} + assert_select 'div.avatar-with-child' do + assert_select '.avatar', 1 end end - def test_show_should_display_author_and_assignee_gravatars_when_assigned + def test_show_should_display_author_and_assignee_avatars_when_assigned issue = Issue.find(1) issue.assigned_to_id = 2 issue.save! @request.session[:user_id] = 1 - with_settings :gravatar_enabled => '1' do - get :show, :params => {:id => issue.id} - assert_select 'div.gravatar-with-child' do - assert_select 'img.gravatar', 2 - assert_select 'img.gravatar-child', 1 - end + get :show, :params => {:id => issue.id} + assert_select 'div.avatar-with-child' do + assert_select '.avatar', 2 + assert_select '.avatar-child', 1 end end diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb index 997b2263a..cec58ff5f 100644 --- a/test/functional/messages_controller_test.rb +++ b/test/functional/messages_controller_test.rb @@ -28,7 +28,7 @@ class MessagesControllerTest < Redmine::ControllerTest get(:show, :params => {:board_id => 1, :id => 1}) assert_response :success - assert_select 'h2', :text => 'First post' + assert_select 'h2', :text => "RAFirst post" end def test_show_should_contain_reply_field_tags_for_quoting diff --git a/test/functional/news_controller_test.rb b/test/functional/news_controller_test.rb index 536814c9d..686fada25 100644 --- a/test/functional/news_controller_test.rb +++ b/test/functional/news_controller_test.rb @@ -75,7 +75,7 @@ class NewsControllerTest < Redmine::ControllerTest get(:show, :params => {:id => 1}) assert_response :success assert_select 'p.breadcrumb a[href=?]', '/projects/ecookbook/news', :text => 'News' - assert_select 'h2', :text => 'eCookbook first release !' + assert_select 'h2', :text => 'JS eCookbook first release !' end def test_show_should_show_attachments diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb index 2e2e8b933..1f60bbbe2 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -2053,20 +2053,6 @@ class ApplicationHelperTest < Redmine::HelperTest end end - def test_principals_check_box_tag_without_avatar - principals = [User.find(1), Group.find(10)] - Setting.gravatar_enabled = '1' - avatar_tags = principals.collect{|p| avatar(p, :size => 16)} - - with_settings :gravatar_enabled => '0' do - tags = principals_check_box_tags(name, principals) - principals.each_with_index do |principal, i| - assert_not_include avatar_tags[i], tags - assert_include content_tag('span', principal_icon(principal), :class => "name icon icon-#{principal.class.name.downcase}"), tags - end - end - end - def test_principals_options_for_select_with_users User.current = nil users = [User.find(2), User.find(4)] diff --git a/test/helpers/avatars_helper_test.rb b/test/helpers/avatars_helper_test.rb index baa64a653..6b426bc98 100644 --- a/test/helpers/avatars_helper_test.rb +++ b/test/helpers/avatars_helper_test.rb @@ -63,9 +63,9 @@ class AvatarsHelperTest < Redmine::HelperTest end def test_avatar_css_class - # The default class of the img tag should be gravatar - assert_include 'class="gravatar"', avatar('jsmith ') - assert_include 'class="gravatar picture"', avatar('jsmith ', :class => 'picture') + # The default classes of the img tag should be gravatar and avatar + assert_include 'class="gravatar avatar"', avatar('jsmith ') + assert_include 'class="gravatar avatar picture"', avatar('jsmith ', :class => 'picture') end def test_avatar_with_initials @@ -80,9 +80,9 @@ class AvatarsHelperTest < Redmine::HelperTest end end - def test_avatar_disabled + def test_avatar_disabled_should_display_user_initials with_settings :gravatar_enabled => '0' do - assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo')) + assert_equal "JS", avatar(User.find_by_mail('jsmith@somenet.foo')) end end diff --git a/test/integration/api_test/authentication_test.rb b/test/integration/api_test/authentication_test.rb index 23641b53a..4145fb969 100644 --- a/test/integration/api_test/authentication_test.rb +++ b/test/integration/api_test/authentication_test.rb @@ -127,20 +127,23 @@ class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base assert_response :unauthorized end + # TODO: check why this test does not use the API endpoint def test_api_should_accept_switch_user_header_for_admin_user user = User.find(1) su = User.find(4) get '/users/current', :headers => {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login} assert_response :success - assert_select 'h2', :text => su.name + assert_select 'h2', :text => "#{su.initials} #{su.name}" end + # TODO: check why this test does not use the API endpoint def test_api_should_respond_with_412_when_trying_to_switch_to_a_invalid_user get '/users/current', :headers => {'X-Redmine-API-Key' => User.find(1).api_key, 'X-Redmine-Switch-User' => 'foobar'} assert_response :precondition_failed end + # TODO: check why this test does not use the API endpoint def test_api_should_respond_with_412_when_trying_to_switch_to_a_locked_user user = User.find(5) assert user.locked? @@ -149,12 +152,13 @@ class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base assert_response :precondition_failed end + # TODO: check why this test does not use the API endpoint def test_api_should_not_accept_switch_user_header_for_non_admin_user user = User.find(2) su = User.find(4) get '/users/current', :headers => {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login} assert_response :success - assert_select 'h2', :text => user.name + assert_select 'h2', :text => "#{user.initials} #{user.name}" end end -- 2.39.5 (Apple Git-154)