Project

General

Profile

Feature #21026 » 0001-Add-an-option-to-allow-groups-in-user-custom-fields-.patch

Go MAEDA, 2026-09-14 10:48

View differences:

app/models/custom_field.rb
99 99
    'text_formatting',
100 100
    'edit_tag_style',
101 101
    'user_role',
102
    'possible_principals',
102 103
    'version_status',
103 104
    'extensions_allowed',
104 105
    'full_width_layout',
app/models/group.rb
118 118
    Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
119 119
    Watcher.where('user_id = ?', id).delete_all
120
    remove_user_custom_field_references
120 121
  end
121 122
end
app/models/principal.rb
211 211
    Project.where(default_assigned_to: self).update_all(default_assigned_to_id: nil)
212 212
  end
213
  # Removes the values of user custom fields that reference the principal
214
  def remove_user_custom_field_references
215
    user_custom_field_ids = CustomField.where(field_format: 'user').ids
216
    if user_custom_field_ids.any?
217
      CustomValue.where(custom_field_id: user_custom_field_ids, value: id.to_s).delete_all
218
    end
219
  end
220

  
213 221
  protected
214 222
  # Make sure we don't try to insert NULL values (see #4632)
app/models/query.rb
1173 1173
    if filter[:field].format.target_class && filter[:field].format.target_class <= User
1174 1174
      if value.delete('me')
1175 1175
        value.push User.current.id.to_s
1176
        if filter[:field].format.try(:selectable_principal_types, filter[:field])&.include?('Group')
1177
          value += User.current.group_ids.map(&:to_s)
1178
        end
1176 1179
      end
1177 1180
    end
1178 1181
    not_in = nil
app/models/user.rb
1016 1016
    Watcher.where('user_id = ?', id).delete_all
1017 1017
    WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
1018 1018
    WikiContentVersion.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
1019
    user_custom_field_ids = CustomField.where(field_format: 'user').ids
1020
    if user_custom_field_ids.any?
1021
      CustomValue.where(custom_field_id: user_custom_field_ids, value: self.id.to_s).delete_all
1022
    end
1019
    remove_user_custom_field_references
1023 1020
  end
1024 1021
  # Singleton class method is public
app/views/custom_fields/formats/_user.html.erb
1
<p><%= f.select :possible_principals,
2
      [
3
        [l(:label_user_plural), 'user'],
4
        [l(:label_users_and_groups), 'user_group'],
5
        [l(:label_group_plural), 'group']
6
      ],
7
      :label => :field_possible_values %></p>
1 8
<p>
2 9
  <label><%= l(:label_role) %></label>
3 10
  <label class="block">
config/locales/en.yml
633 633
  label_user: User
634 634
  label_user_plural: Users
635
  label_users_and_groups: Users and groups
635 636
  label_user_new: New user
636 637
  label_user_anonymous: Anonymous
637 638
  label_project: Project
config/locales/ja.yml
471 471
  label_user: ユーザー
472 472
  label_user_plural: ユーザー
473
  label_users_and_groups: ユーザーとグループ
473 474
  label_user_new: 新しいユーザー
474 475
  label_user_anonymous: 匿名ユーザー
475 476
  label_profile: プロフィール
lib/redmine/field_format.rb
613 613
        opts = []
614 614
        opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
615 615
        opts << [l(:label_none), '__none__'] unless custom_field.is_required?
616
        opts += possible_values_options(custom_field, objects)
617 616
        view.select_tag(
618 617
          tag_name,
619
          view.options_for_select(opts, value),
618
          view.options_for_select(opts, value) + bulk_edit_tag_options(view, custom_field, objects, value),
620 619
          options.merge(:multiple => custom_field.multiple?)
621 620
        )
622 621
      end
......
652 651
            blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
653 652
          end
654 653
        end
655
        options_tags =
656
          blank_option +
657
           view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
654
        options_tags = blank_option + select_edit_tag_options(view, custom_value)
658 655
        s =
659 656
          view.select_tag(
660 657
            tag_name, options_tags,
......
666 663
        s
667 664
      end
665
      # Returns the option tags of the select edit tag
666
      def select_edit_tag_options(view, custom_value)
667
        view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
668
      end
669

  
670
      # Returns the option tags of the bulk edit tag
671
      def bulk_edit_tag_options(view, custom_field, objects, value)
672
        view.options_for_select(possible_values_options(custom_field, objects), value)
673
      end
674

  
668 675
      # Renders the edit tag as check box or radio tags
669 676
      def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
670 677
        opts = []
......
871 878
    class UserFormat < RecordList
872 879
      add 'user'
873 880
      self.form_partial = 'custom_fields/formats/user'
874
      field_attributes :user_role
881
      field_attributes :user_role, :possible_principals
882

  
883
      # Maps the possible_principals option to the principal types that can be selected
884
      PRINCIPAL_TYPES = {
885
        'user' => ['User'],
886
        'user_group' => ['User', 'Group'],
887
        'group' => ['Group']
888
      }.freeze
875 889
      def possible_values_options(custom_field, object=nil)
876
        users = possible_values_records(custom_field, object)
877
        options = users.map {|u| [u.name, u.id.to_s]}
878
        if !custom_field.multiple? && users.include?(User.current)
879
          options = [["<< #{l(:label_me)} >>", User.current.id.to_s]] + options
880
        end
881
        options
890
        principals_options(custom_field, possible_values_records(custom_field, object))
882 891
      end
883 892
      def possible_values_records(custom_field, object=nil)
......
887 896
          projects = object.filter_map {|o| o.respond_to?(:project) ? o.project : nil}.uniq
888 897
          projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
889 898
        elsif object.respond_to?(:project) && object.project
890
          scope = object.project.users
899
          scope = object.project.principals.where(:type => selectable_principal_types(custom_field))
891 900
          if custom_field.user_role.is_a?(Array)
892 901
            role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
893 902
            if role_ids.any?
......
902 911
        end
903 912
      end
913
      # Returns the principal types (User and/or Group) that can be
914
      # selected for the given custom field
915
      def selectable_principal_types(custom_field)
916
        PRINCIPAL_TYPES[custom_field.possible_principals.to_s] || PRINCIPAL_TYPES['user']
917
      end
918

  
919
      def cast_single_value(custom_field, value, customized=nil)
920
        Principal.find_by_id(value.to_i) if value.present?
921
      end
922

  
923
      def possible_custom_value_options(custom_value)
924
        principals_options(custom_value.custom_field, possible_custom_value_records(custom_value))
925
      end
926

  
927
      # Returns the principals that can be selected for the given custom value,
928
      # including the principals that are currently set but no longer selectable
929
      def possible_custom_value_records(custom_value)
930
        principals = possible_values_records(custom_value.custom_field, custom_value.customized).to_a
931
        missing = [custom_value.value_was].flatten.reject(&:blank?).map(&:to_i) - principals.map(&:id)
932
        if missing.any?
933
          principals += Principal.where(:id => missing).to_a
934
        end
935
        principals
936
      end
937

  
904 938
      def value_from_keyword(custom_field, keyword, object)
905
        users = possible_values_records(custom_field, object).to_a
939
        principals = possible_values_records(custom_field, object).to_a
906 940
        parse_keyword(custom_field, keyword) do |k|
907
          Principal.detect_by_keyword(users, k).try(:id)
941
          Principal.detect_by_keyword(principals, k).try(:id)
908 942
        end
909 943
      end
......
916 950
        end
917 951
      end
952
      protected
953

  
954
      def select_edit_tag_options(view, custom_value)
955
        principals_option_tags(view, custom_value.custom_field,
956
                               possible_custom_value_records(custom_value), custom_value.value)
957
      end
958

  
959
      def bulk_edit_tag_options(view, custom_field, objects, value)
960
        principals_option_tags(view, custom_field, possible_values_records(custom_field, objects), value)
961
      end
962

  
963
      # Returns the [name, id] options for the given principals,
964
      # with the "me" option first when the current user is selectable
965
      def principals_options(custom_field, principals)
966
        me_options(custom_field, principals) + principals_to_options(principals)
967
      end
968

  
969
      # Renders the option tags for the given principals. When both users
970
      # and groups are present, they are listed in separate optgroups
971
      def principals_option_tags(view, custom_field, principals, selected)
972
        users, groups = principals.partition {|principal| principal.is_a?(User)}
973
        options = view.options_for_select(me_options(custom_field, principals), selected)
974
        if users.any? && groups.any?
975
          optgroups = [
976
            [l(:label_user_plural), principals_to_options(users)],
977
            [l(:label_group_plural), principals_to_options(groups)]
978
          ]
979
          options + view.grouped_options_for_select(optgroups, selected)
980
        else
981
          options + view.options_for_select(principals_to_options(principals), selected)
982
        end
983
      end
984

  
985
      def me_options(custom_field, principals)
986
        if !custom_field.multiple? && principals.include?(User.current)
987
          [["<< #{l(:label_me)} >>", User.current.id.to_s]]
988
        else
989
          []
990
        end
991
      end
992

  
993
      def principals_to_options(principals)
994
        principals.map {|principal| [principal.name, principal.id.to_s]}
995
      end
996

  
918 997
      def query_filter_values(custom_field, query)
919
        query.author_values
998
        types = selectable_principal_types(custom_field)
999
        values =
1000
          query.principals.select {|p| types.include?(p.type)}.
1001
            sort_by {|p| [p.status, p]}.
1002
            collect {|p| [p.name, p.id.to_s, l("status_#{User::LABEL_BY_STATUS[p.status]}")]}
1003
        values.unshift(["<< #{l(:label_me)} >>", "me"]) if User.current.logged?
1004
        values
920 1005
      end
921 1006
    end
test/functional/custom_fields_controller_test.rb
455 455
    assert_equal [1, 3], field.projects.map(&:id).sort
456 456
  end
457
  def test_create_user_custom_field
458
    assert_difference 'CustomField.count' do
459
      post(
460
        :create,
461
        :params => {
462
          :type => "IssueCustomField",
463
          :custom_field => {
464
            :name => "foo",
465
            :field_format => "user",
466
            :possible_principals => "user_group"
467
          }
468
        }
469
      )
470
      assert_response :found
471
    end
472
    field = IssueCustomField.order(id: :desc).first
473
    assert_equal 'user_group', field.possible_principals
474
  end
475

  
457 476
  def test_create_with_continue_params
458 477
    assert_difference 'CustomField.count' do
459 478
      post(
test/unit/group_test.rb
121 121
    assert !User.find(8).member_of?(Project.find(5))
122 122
  end
123
  def test_destroy_should_unassign_and_unwatch_issues
123
  def test_destroy_should_remove_references_from_issues
124 124
    group = Group.find(10)
125 125
    Issue.where(:id => 1).update_all(["assigned_to_id = ?", group.id])
126 126
    issue = Issue.find(2)
......
129 129
    issue.save
130 130
    issue.reload
131 131
    assert issue.watcher_user_ids.include?(10)
132
    cf = IssueCustomField.create!(:field_format => 'user', :possible_principals => 'user_group', :is_for_all => true, :name => 'User custom field', :tracker_ids => [1])
133
    issue_with_cf = Issue.generate!(:project_id => 5, :tracker_id => 1, :custom_field_values => {cf.id.to_s => group.id.to_s})
134
    assert_equal group.id.to_s, issue_with_cf.reload.custom_field_value(cf)
132 135
    assert group.destroy
133 136
    assert group.destroyed?
......
136 139
    assert_nil Issue.find(1).assigned_to_id
137 140
    issue.reload
138 141
    assert !issue.watcher_user_ids.include?(10)
142
    assert_equal '', issue_with_cf.reload.custom_field_value(cf).to_s
139 143
  end
140 144
  def test_builtin_groups_should_be_created_if_missing
test/unit/lib/redmine/field_format/user_field_format_test.rb
65 65
    field = IssueCustomField.new(:field_format => 'user')
66 66
    project = Project.find(1)
67
    assert_nil field.possible_principals
67 68
    assert_equal ['Dave Lopper', 'John Smith'], field.possible_values_options(project).map(&:first)
68 69
  end
69 70
  def test_possible_values_options_should_return_project_members_with_selected_role
70
    field = IssueCustomField.new(:field_format => 'user', :user_role => ["2"])
71
    project = Project.find(1)
71
    field = IssueCustomField.new(:field_format => 'user', :possible_principals => 'user_group', :user_role => ["2"])
72
    project = Project.find(5)
72
    assert_equal ['Dave Lopper'], field.possible_values_options(project).map(&:first)
73
    assert_equal ['User Misc', 'A Team'], field.possible_values_options(project).map(&:first)
73 74
  end
74 75
  def test_possible_values_options_should_return_project_members_and_me_if_logged_in
......
109 110
    assert_equal [2], field.value_from_keyword('jsmith', project)
110 111
    assert_equal [], field.value_from_keyword('Unknown User', project)
111 112
  end
113

  
114
  def test_possible_values_options_with_groups_only_should_include_groups_only
115
    # The current user is a member of the project but should not be included
116
    User.current = User.find(2)
117
    field = IssueCustomField.new(:field_format => 'user', :possible_principals => 'group')
118
    project = Project.find(5)
119

  
120
    assert_equal [['A Team', '10']], field.possible_values_options(project)
121
  end
122

  
123
  def test_group_value_should_be_invalid_when_groups_are_not_allowed
124
    field = IssueCustomField.create!(:name => 'Foo', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all)
125
    issue = Issue.new(:project_id => 5, :tracker_id => 1, :author_id => 2, :subject => 'Test',
126
                      :custom_field_values => {field.id => '10'})
127

  
128
    assert_equal false, issue.valid?
129
    assert_include "Foo #{::I18n.t('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
130
  end
131

  
132
  def test_existing_group_value_should_remain_valid_after_disallowing_groups
133
    field = IssueCustomField.create!(:name => 'Foo', :field_format => 'user', :possible_principals => 'user_group',
134
                                     :is_for_all => true, :trackers => Tracker.all)
135
    issue = Issue.generate!(:project_id => 5, :tracker_id => 1, :custom_field_values => {field.id => '10'})
136

  
137
    field.update!(:possible_principals => 'user')
138
    issue.reload
139
    assert_equal '10', issue.custom_field_value(field)
140
    assert issue.valid?
141
    assert_equal 'A Team', field.format.formatted_value(self, field, '10', issue).to_s
142

  
143
    # Group can still be selected for the issue, but not for other issues
144
    assert_include '10', issue.custom_field_values.detect {|v| v.custom_field == field}.then {|v| field.format.possible_custom_value_options(v)}.map(&:last)
145
    assert_not_include '10', field.possible_values_options(Issue.new(:project_id => 5)).map(&:last)
146
  end
147

  
148
  def test_edit_tag_should_not_use_optgroups_with_users_only_or_groups_only
149
    field = IssueCustomField.new(:field_format => 'user', :is_required => false)
150
    value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new(:project_id => 5))
151
    tag = field.format.edit_tag(self, 'abc', 'xyz', value)
152
    assert_select_in tag, 'select[id=abc][name=xyz]' do
153
      assert_select 'optgroup', 0
154
      assert_select 'select > option[value="2"]', :text => 'John Smith'
155
      assert_select 'option[value="10"]', 0
156
    end
157

  
158
    field = IssueCustomField.new(:field_format => 'user', :possible_principals => 'group', :is_required => false)
159
    value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new(:project_id => 5))
160
    tag = field.format.edit_tag(self, 'abc', 'xyz', value)
161
    assert_select_in tag, 'select[id=abc][name=xyz]' do
162
      assert_select 'optgroup', 0
163
      assert_select 'select > option[value="10"]', :text => 'A Team'
164
      assert_select 'option[value="2"]', 0
165
    end
166
  end
167

  
168
  def test_edit_tag_with_groups_should_group_users_and_groups_in_optgroups
169
    set_language_if_valid 'en'
170
    User.current = User.find(2)
171
    field = IssueCustomField.new(:field_format => 'user', :possible_principals => 'user_group', :is_required => false)
172
    value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new(:project_id => 5))
173

  
174
    tag = field.format.edit_tag(self, 'abc', 'xyz', value)
175
    assert_select_in tag, 'select[id=abc][name=xyz]' do
176
      assert_select 'option', :text => '<< me >>', :count => 1
177
      assert_select 'optgroup', 2
178
      assert_select 'optgroup:nth-of-type(1)[label="Users"]' do
179
        assert_select 'option[value="2"]', :text => 'John Smith'
180
        assert_select 'option[value="10"]', 0
181
      end
182
      assert_select 'optgroup:nth-of-type(2)[label="Groups"]' do
183
        assert_select 'option', 1
184
        assert_select 'option[value="10"]', :text => 'A Team'
185
      end
186
    end
187
  end
188

  
189
  def test_bulk_edit_tag_with_groups_should_group_users_and_groups_in_optgroups
190
    set_language_if_valid 'en'
191
    field = IssueCustomField.new(:field_format => 'user', :possible_principals => 'user_group', :is_required => false)
192
    issues = [Issue.new(:project_id => 5), Issue.new(:project_id => 5)]
193

  
194
    tag = field.format.bulk_edit_tag(self, 'abc', 'xyz', field, issues, '')
195
    assert_select_in tag, 'select[name=xyz]' do
196
      assert_select 'select > option[value=""]'
197
      assert_select 'select > option[value="__none__"]'
198
      assert_select 'optgroup[label="Users"] option[value="2"]', :text => 'John Smith'
199
      assert_select 'optgroup[label="Groups"] option[value="10"]', :text => 'A Team'
200
    end
201
  end
202

  
203
  def test_value_from_keyword_should_return_group_id_when_groups_are_allowed
204
    project = Project.find(5)
205

  
206
    field = IssueCustomField.new(:field_format => 'user', :possible_principals => 'user_group')
207
    assert_equal 10, field.value_from_keyword('A Team', project)
208

  
209
    field = IssueCustomField.new(:field_format => 'user')
210
    assert_nil field.value_from_keyword('A Team', project)
211
  end
112 212
end
test/unit/query_test.rb
1301 1301
    assert_equal issue1, result.first
1302 1302
  end
1303
  def test_user_custom_field_with_groups_should_be_filterable_on_users_and_groups
1304
    User.current = User.find(8)
1305
    cf = IssueCustomField.create!(:field_format => 'user', :possible_principals => 'user_group', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
1306
    issue1 = Issue.generate!(:project_id => 5, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '10'})
1307
    issue2 = Issue.generate!(:project_id => 5, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '8'})
1308
    issue3 = Issue.generate!(:project_id => 5, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'})
1309

  
1310
    query = IssueQuery.new(:name => '_', :project => Project.find(5))
1311
    filter = query.available_filters["cf_#{cf.id}"]
1312
    assert_not_nil filter
1313
    values = filter[:values].pluck(1)
1314
    assert_include 'me', values
1315
    assert_include '2', values
1316
    assert_include '10', values
1317

  
1318
    # "me" includes the groups of the current user
1319
    query.filters = {"cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
1320
    assert_equal [issue1, issue2].map(&:id).sort, query.issues.map(&:id).sort
1321
  end
1322

  
1303 1323
  def test_filter_on_chained_user_custom_field
1304 1324
    user = User.find(2)
1305 1325
    User.current = user
(3-3/3)