Project

General

Profile

Feature #29824 » 0001-Render-user-s-initals-by-default-when-Gravatar-servi.patch

Marius BĂLTEANU, 2025-06-23 22:18

View differences:

app/assets/stylesheets/application.css
413 413
tr.entry.file td.filename_no_report a { margin-left: 16px; }
414 414

  
415 415
tr span.expander, .gantt_subjects div > span.expander {margin-left: 0; cursor: pointer;}
416
.gantt_subjects div > span .icon-gravatar {float: none;}
416
.gantt_subjects .avatar {margin-right: 4px;}
417 417
.gantt_subjects div.project-name a, .gantt_subjects div.version-name a {margin-left: 4px;}
418 418

  
419 419
tr.changeset { height: 20px }
......
445 445
tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
446 446

  
447 447
#principals_for_new_member .icon-user, #users_for_watcher .icon-user {background:transparent;}
448
#principals_for_new_member svg, #principals_for_new_member img {margin-right: 4px;}
448
#principals_for_new_member svg, #principals_for_new_member .avatar  {margin-right: 4px;}
449 449

  
450 450
tr.user td {width:13%;white-space: nowrap;}
451 451
td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
......
549 549
td.center {text-align:center;}
550 550

  
551 551
#watchers select {width: 95%; display: block;}
552
#watchers img.gravatar {margin: 0 4px 2px 0;}
552
#watchers .avatar {margin: 0 4px 2px 0;}
553 553
#watchers svg.icon-svg {margin: 0 2px 2px 0;}
554
#users_for_watcher img.gravatar {padding-bottom: 2px; margin-right: 4px;}
554
#users_for_watcher .avatar {padding-bottom: 2px; margin-right: 4px;}
555 555
#users_for_watcher svg {margin-right: 4px;}
556 556
#users_for_watcher span.icon-user {display: inline;}
557 557

  
......
1445 1445
.tooltip span.tip{display: none; text-align:left;}
1446 1446
.tooltip span.tip a { color: #169 !important; }
1447 1447

  
1448
.tooltip span.tip img.gravatar {
1448
.tooltip span.tip .avatar {
1449 1449
  float: none;
1450 1450
  margin: 0;
1451 1451
}
......
1799 1799
}
1800 1800
.gantt_subjects div.issue-subject:hover { background-color:#ffffdd; }
1801 1801
.gantt_selected_column_content { padding-left: 3px; padding-right: 3px;}
1802
.gantt_subjects .issue-subject img.icon-gravatar {
1803
  margin: 2px 5px 0px 2px;
1804
}
1805 1802

  
1806 1803
.gantt_hdr_selected_column_name {
1807 1804
  position: absolute;
......
2112 2109

  
2113 2110
.contextual>*:not(:first-child), .buttons>.icon:not(:first-child), .contextual .journal-actions>*:not(:first-child) { margin-left: 5px; }
2114 2111

  
2115
img.gravatar {
2116
  vertical-align: middle;
2117
  border-radius: 20%;
2118
}
2119

  
2120
div.issue img.gravatar {
2112
div.issue .avatar {
2121 2113
  float: left;
2122 2114
  margin: 0 12px 6px 0;
2123 2115
}
2124 2116

  
2125
div.gravatar-with-child {
2117
div.avatar-with-child {
2126 2118
  position: relative;
2127 2119
}
2128 2120

  
2129
div.gravatar-with-child > img.gravatar:nth-child(2) {
2121
div.avatar-with-child > .avatar:nth-child(2) {
2130 2122
  position: absolute;
2131 2123
  top: 30px;
2132 2124
  left: 30px;
......
2134 2126
  border: 2px solid rgba(255, 255, 255, 0.9);
2135 2127
}
2136 2128

  
2137
h2 img.gravatar, h3 img.gravatar {margin-right: 4px;}
2129
h2 .avatar, h3 .avatar {margin-right: 4px;}
2138 2130
h4 img.gravatar {margin: -2px 4px -4px 0;}
2131
/*# TODO: check where this rule is still used*/
2139 2132
td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
2140
#activity dt img.gravatar {margin: 0 1em 0 0;}
2141
/* Used on 12px Gravatar img tags without the icon background */
2142
.icon-gravatar {float: left; margin-right: 4px;}
2133
#activity dt .avatar {margin: 0 1em 0 0;}
2143 2134

  
2144 2135
#activity dt, .journal {clear: left;}
2145 2136

  
......
2162 2153

  
2163 2154
img.filecontent.image {background-image: url(/transparent.png);}
2164 2155

  
2156
/* Avatar styles */
2157
.avatar {
2158
  border-radius: 20%;
2159
  display: inline-flex;
2160
  vertical-align: middle;
2161
}
2162

  
2163
span[role="img"].avatar {
2164
  font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
2165
  align-items: center;
2166
  display: inline-flex;
2167
  font-size: calc(24px * .4);
2168
  justify-content: center;
2169
  user-select: none;
2170
  font-weight: 700;
2171
}
2172
.avatar.s13 {
2173
  block-size: 13px;
2174
  inline-size: 13px;
2175
}
2176
span[role="img"].avatar.s13 {
2177
  font-size: calc(16px * .3);
2178
}
2179
.avatar.s16 {
2180
  block-size: 16px;
2181
  inline-size: 16px;
2182
}
2183
span[role="img"].avatar.s16 {
2184
  font-size: calc(16px * .4);
2185
}
2186
.avatar.s22 {
2187
  block-size: 22px;
2188
  inline-size: 22px;
2189
}
2190
span[role="img"].avatar.s22 {
2191
  font-size: calc(22px * .4);
2192
}
2193
.avatar.s24 {
2194
  block-size: 24px;
2195
  inline-size: 24px;
2196
}
2197
span[role="img"].avatar.s24  {
2198
  font-size: calc(24px * .4);
2199
}
2200
.avatar.s40 {
2201
  block-size: 40px;
2202
  inline-size: 40px;
2203
}
2204
span[role="img"].avatar.s40  {
2205
  font-size: calc(40px * .4);
2206
}
2207
.avatar.s50 {
2208
  block-size: 50px;
2209
  inline-size: 50px;
2210
}
2211
span[role="img"].avatar.s50  {
2212
  font-size: calc(50px * .4);
2213
}
2214

  
2215
.avatar-color-0 {
2216
  background-color: #880000;
2217
  color: #FFFFFF;
2218
}
2219
.avatar-color-1 {
2220
  background-color: #ff0000;
2221
  color: #000000;
2222
}
2223
.avatar-color-2 {
2224
  background-color: #00ff00;
2225
  color: #000000;
2226
}
2227
.avatar-color-3 {
2228
  background-color: #008800;
2229
  color: #FFFFFF;
2230
}
2231
.avatar-color-4 {
2232
  background-color: #0000ff;
2233
  color: #FFFFFF;
2234
}
2235
.avatar-color-5 {
2236
  background-color: #000088;
2237
  color: #FFFFFF;
2238
}
2239
.avatar-color-6 {
2240
  background-color: #ff8800;
2241
  color: #000000;
2242
}
2243
.avatar-color-7 {
2244
  background-color: #ff0088;
2245
  color: #000000;
2246
}
2247

  
2165 2248
/* Reaction styles */
2166 2249
.reaction-button:hover, .reaction-button:active {
2167 2250
  text-decoration: none;
app/helpers/avatars_helper.rb
57 57
      else
58 58
        nil
59 59
      end
60
    elsif user.respond_to?(:initials)
61
      size = options.delete(:size) || GravatarHelper::DEFAULT_OPTIONS[:size]
62
      css_class = "avatar-color-#{user.id % 8} avatar s#{size}"
63
      css_class += " #{options[:class]}" if options[:class]
64
      content_tag('span', user.initials, role: 'img', class: css_class)
60 65
    else
61 66
      ''
62 67
    end
app/models/group.rb
53 53
    name.to_s
54 54
  end
55 55

  
56
  def initials
57
    name[0, 1]
58
  end
59

  
56 60
  def builtin_type
57 61
    nil
58 62
  end
app/views/issues/show.html.erb
32 32
    </div>
33 33
  <% end %>
34 34

  
35
  <div class="gravatar-with-child">
35
  <div class="avatar-with-child">
36 36
    <%= author_avatar(@issue.author, :size => "50") %>
37
    <%= assignee_avatar(@issue.assigned_to, :size => "22", :class => "gravatar-child") if @issue.assigned_to %>
37
    <%= assignee_avatar(@issue.assigned_to, :size => "22", :class => "avatar-child") if @issue.assigned_to %>
38 38
  </div>
39 39

  
40 40
<div data-controller="sticky-issue-header">
app/views/layouts/base.html.erb
36 36
    <% end %>
37 37

  
38 38
    <% if User.current.logged? %>
39
        <div class="flyout-menu__avatar <% if !Setting.gravatar_enabled? %>flyout-menu__avatar--no-avatar<% end %>">
40
            <% if Setting.gravatar_enabled? %>
41
                <%= link_to(avatar(User.current, :size => "80"), user_path(User.current)) %>
42
            <% end %>
43
            <%= link_to_user(User.current, :format => :username) %>
39
        <div class="flyout-menu__avatar">
40
          <%= link_to(avatar(User.current, :size => "40"), user_path(User.current)) %>
41
          <%= link_to_user(User.current, :format => :username) %>
44 42
        </div>
45 43
    <% end %>
46 44

  
lib/plugins/gravatar/lib/gravatar.rb
32 32
    :title => '',
33 33

  
34 34
    # The class to assign to the img tag for the gravatar.
35
    :class => 'gravatar',
35
    :class => 'gravatar avatar',
36 36
  }
37 37

  
38 38
  # The methods that will be made available to your views.
lib/redmine/helpers/gantt.rb
731 731
          css_classes = +''
732 732
          css_classes << ' issue-overdue' if issue.overdue?
733 733
          css_classes << ' issue-behind-schedule' if issue.behind_schedule?
734
          css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
734
          css_classes << ' icon icon-issue' unless issue.assigned_to
735 735
          css_classes << ' issue-closed' if issue.closed?
736 736
          if issue.start_date && issue.due_before && issue.done_ratio
737 737
            progress_date = calc_progress_date(issue.start_date,
......
740 740
            css_classes << ' over-end-date' if progress_date > self.date_to && issue.done_ratio > 0
741 741
          end
742 742
          s = (+"").html_safe
743
          s << view.sprite_icon('issue').html_safe unless Setting.gravatar_enabled? && issue.assigned_to
744
          s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-gravatar')
743
          s << view.sprite_icon('issue').html_safe unless issue.assigned_to
744
          s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-avatar')
745 745
          s << view.link_to_issue(issue).html_safe
746 746
          s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]',
747 747
                                :value => issue.id, :style => 'display:none;',
test/functional/calendars_controller_test.rb
57 57
        ) do
58 58
          assert_select 'a.issue[href=?]', '/issues/2', :text => 'Feature request #2'
59 59
          assert_select 'span.tip' do
60
            assert_select 'img[class="gravatar"]'
60
            assert_select 'img[class="gravatar avatar"]'
61 61
          end
62 62
          assert_select 'input[name=?][type=?][value=?]', 'ids[]', 'checkbox', '2'
63 63
        end
test/functional/gantts_controller_test.rb
58 58
    # Assert context menu on issues subject and gantt bar
59 59
    assert_select 'div[class=?]', 'issue-subject hascontextmenu'
60 60
    assert_select 'div.tooltip.hascontextmenu' do
61
      assert_select 'img[class="gravatar"]'
61
      assert_select 'img[class="gravatar avatar"]'
62 62
    end
63 63
    assert_select "form[data-cm-url=?]", '/issues/context_menu'
64 64

  
test/functional/issues_controller_test.rb
2816 2816
    assert_select 'h3', {text: /Watchers \(\d*\)/, count: 0}
2817 2817
  end
2818 2818

  
2819
  def test_show_should_display_watchers_with_gravatars
2819
  def test_show_should_display_watchers_with_avatars
2820 2820
    @request.session[:user_id] = 2
2821 2821
    issue = Issue.find(1)
2822 2822
    issue.add_watcher User.find(2)
......
2824 2824
    with_settings :gravatar_enabled => '1' do
2825 2825
      get(:show, :params => {:id => 1})
2826 2826
    end
2827

  
2827 2828
    assert_select 'div#watchers ul' do
2828 2829
      assert_select 'li.user-2' do
2829
        assert_select 'img.gravatar[title=?]', 'John Smith'
2830
        assert_select '.avatar[title=?]', 'John Smith'
2830 2831
        assert_select 'a[href="/users/2"]'
2831 2832
        assert_select 'a[class*=delete]'
2832 2833
      end
......
8786 8787
    assert_select 'a[href=?][onclick=?]', "/issues/1", "", :text => 'Cancel'
8787 8788
  end
8788 8789

  
8789
  def test_show_should_display_author_gravatar_only_when_not_assigned
8790
  def test_show_should_display_author_avatar_only_when_not_assigned
8790 8791
    issue = Issue.find(1)
8791 8792
    assert_nil issue.assigned_to_id
8792 8793
    @request.session[:user_id] = 1
8793 8794

  
8794
    with_settings :gravatar_enabled => '1' do
8795
      get :show, :params => {:id => issue.id}
8796
      assert_select 'div.gravatar-with-child' do
8797
        assert_select 'img.gravatar', 1
8798
      end
8795
    get :show, :params => {:id => issue.id}
8796
    assert_select 'div.avatar-with-child' do
8797
      assert_select '.avatar', 1
8799 8798
    end
8800 8799
  end
8801 8800

  
8802
  def test_show_should_display_author_and_assignee_gravatars_when_assigned
8801
  def test_show_should_display_author_and_assignee_avatars_when_assigned
8803 8802
    issue = Issue.find(1)
8804 8803
    issue.assigned_to_id = 2
8805 8804
    issue.save!
8806 8805
    @request.session[:user_id] = 1
8807 8806

  
8808
    with_settings :gravatar_enabled => '1' do
8809
      get :show, :params => {:id => issue.id}
8810
      assert_select 'div.gravatar-with-child' do
8811
        assert_select 'img.gravatar', 2
8812
        assert_select 'img.gravatar-child', 1
8813
      end
8807
    get :show, :params => {:id => issue.id}
8808
    assert_select 'div.avatar-with-child' do
8809
      assert_select '.avatar', 2
8810
      assert_select '.avatar-child', 1
8814 8811
    end
8815 8812
  end
8816 8813

  
test/functional/messages_controller_test.rb
28 28
    get(:show, :params => {:board_id => 1, :id => 1})
29 29
    assert_response :success
30 30

  
31
    assert_select 'h2', :text => 'First post'
31
    assert_select 'h2', :text => "RAFirst post"
32 32
  end
33 33

  
34 34
  def test_show_should_contain_reply_field_tags_for_quoting
test/functional/news_controller_test.rb
75 75
    get(:show, :params => {:id => 1})
76 76
    assert_response :success
77 77
    assert_select 'p.breadcrumb a[href=?]', '/projects/ecookbook/news', :text => 'News'
78
    assert_select 'h2', :text => 'eCookbook first release !'
78
    assert_select 'h2', :text => 'JS eCookbook first release !'
79 79
  end
80 80

  
81 81
  def test_show_should_show_attachments
test/helpers/application_helper_test.rb
2053 2053
    end
2054 2054
  end
2055 2055

  
2056
  def test_principals_check_box_tag_without_avatar
2057
    principals = [User.find(1), Group.find(10)]
2058
    Setting.gravatar_enabled = '1'
2059
    avatar_tags = principals.collect{|p| avatar(p, :size => 16)}
2060

  
2061
    with_settings :gravatar_enabled => '0' do
2062
      tags = principals_check_box_tags(name, principals)
2063
      principals.each_with_index do |principal, i|
2064
        assert_not_include avatar_tags[i], tags
2065
        assert_include content_tag('span', principal_icon(principal), :class => "name icon icon-#{principal.class.name.downcase}"), tags
2066
      end
2067
    end
2068
  end
2069

  
2070 2056
  def test_principals_options_for_select_with_users
2071 2057
    User.current = nil
2072 2058
    users = [User.find(2), User.find(4)]
test/helpers/avatars_helper_test.rb
63 63
  end
64 64

  
65 65
  def test_avatar_css_class
66
    # The default class of the img tag should be gravatar
67
    assert_include 'class="gravatar"', avatar('jsmith <jsmith@somenet.foo>')
68
    assert_include 'class="gravatar picture"', avatar('jsmith <jsmith@somenet.foo>', :class => 'picture')
66
    # The default classes of the img tag should be gravatar and avatar
67
    assert_include 'class="gravatar avatar"', avatar('jsmith <jsmith@somenet.foo>')
68
    assert_include 'class="gravatar avatar picture"', avatar('jsmith <jsmith@somenet.foo>', :class => 'picture')
69 69
  end
70 70

  
71 71
  def test_avatar_with_initials
......
80 80
    end
81 81
  end
82 82

  
83
  def test_avatar_disabled
83
  def test_avatar_disabled_should_display_user_initials
84 84
    with_settings :gravatar_enabled => '0' do
85
      assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
85
      assert_equal "<span role=\"img\" class=\"avatar-color-2 avatar s24\">JS</span>", avatar(User.find_by_mail('jsmith@somenet.foo'))
86 86
    end
87 87
  end
88 88

  
test/integration/api_test/authentication_test.rb
127 127
    assert_response :unauthorized
128 128
  end
129 129

  
130
  # TODO: check why this test does not use the API endpoint
130 131
  def test_api_should_accept_switch_user_header_for_admin_user
131 132
    user = User.find(1)
132 133
    su = User.find(4)
133 134

  
134 135
    get '/users/current', :headers => {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login}
135 136
    assert_response :success
136
    assert_select 'h2', :text => su.name
137
    assert_select 'h2', :text => "#{su.initials} #{su.name}"
137 138
  end
138 139

  
140
  # TODO: check why this test does not use the API endpoint
139 141
  def test_api_should_respond_with_412_when_trying_to_switch_to_a_invalid_user
140 142
    get '/users/current', :headers => {'X-Redmine-API-Key' => User.find(1).api_key, 'X-Redmine-Switch-User' => 'foobar'}
141 143
    assert_response :precondition_failed
142 144
  end
143 145

  
146
  # TODO: check why this test does not use the API endpoint
144 147
  def test_api_should_respond_with_412_when_trying_to_switch_to_a_locked_user
145 148
    user = User.find(5)
146 149
    assert user.locked?
......
149 152
    assert_response :precondition_failed
150 153
  end
151 154

  
155
  # TODO: check why this test does not use the API endpoint
152 156
  def test_api_should_not_accept_switch_user_header_for_non_admin_user
153 157
    user = User.find(2)
154 158
    su = User.find(4)
155 159

  
156 160
    get '/users/current', :headers => {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login}
157 161
    assert_response :success
158
    assert_select 'h2', :text => user.name
162
    assert_select 'h2', :text => "#{user.initials} #{user.name}"
159 163
  end
160 164
end
(5-5/5)