Project

General

Profile

Feature #44129 » 0001-Add-relative-date-option-for-date-format-custom-fiel.patch

Go MAEDA, 2026-05-30 11:53

View differences:

app/javascript/controllers/custom_field_default_value_controller.js
1
import { Controller } from "@hotwired/stimulus"
2

  
3
export default class extends Controller {
4
  static targets = [
5
    "mode",
6
    "fixedDate",
7
    "fixedDateInput",
8
    "dateOffset",
9
    "dateOffsetInput"
10
  ]
11

  
12
  connect() {
13
    this.update()
14
  }
15

  
16
  update() {
17
    const mode = this.modeTarget.value
18

  
19
    this.fixedDateTarget.hidden = mode !== "fixed_date"
20
    this.fixedDateInputTarget.disabled = mode !== "fixed_date"
21
    this.dateOffsetTarget.hidden = mode !== "date_offset"
22
    this.dateOffsetInputTarget.disabled = mode !== "date_offset"
23
  }
24
}
app/models/custom_field.rb
89 89
    'position',
90 90
    'searchable',
91 91
    'default_value',
92
    'default_value_mode',
92 93
    'editable',
93 94
    'visible',
94 95
    'multiple',
......
129 130
    @format ||= Redmine::FieldFormat.find(field_format)
130 131
  end
131 132

  
133
  def default_value
134
    if field_format == 'date' && default_value_mode == 'date_offset' && self[:default_value].present?
135
      (User.current.today + Integer(self[:default_value], 10)).to_s
136
    else
137
      self[:default_value]
138
    end
139
  rescue ArgumentError
140
    self[:default_value]
141
  end
142

  
132 143
  def field_format=(arg)
133 144
    # cannot change format of a saved custom field
134 145
    if new_record?
......
158 169
      end
159 170
    end
160 171

  
161
    if default_value.present? && errors[:regexp].blank?
162
      validate_field_value(default_value).each do |message|
172
    if field_format == 'date' && default_value_mode == 'date_offset'
173
      validate_date_default_value_offset
174
    elsif self[:default_value].present? && errors[:regexp].blank?
175
      validate_field_value(self[:default_value]).each do |message|
163 176
        errors.add :default_value, message
164 177
      end
165 178
    end
166 179
  end
167 180

  
181
  def validate_date_default_value_offset
182
    value = self[:default_value].to_s.strip
183
    return if value.blank?
184

  
185
    Integer(value, 10)
186
  rescue ArgumentError
187
    errors.add :default_value, :not_a_number
188
  end
189

  
168 190
  def possible_custom_value_options(custom_value)
169 191
    format.possible_custom_value_options(custom_value)
170 192
  end
app/views/custom_fields/formats/_date.html.erb
1
<p><%= f.date_field(:default_value, :value => @custom_field.default_value, :size => 10) %></p>
2
<%= calendar_for('custom_field_default_value') %>
1
<% default_value_mode = @custom_field.default_value_mode.presence || 'fixed_date' %>
2
<% fixed_date_default_value = default_value_mode == 'fixed_date' ? @custom_field[:default_value] : nil %>
3
<% date_offset_default_value = default_value_mode == 'date_offset' ? @custom_field[:default_value] : nil %>
4
<div data-controller="custom-field-default-value">
5
  <p>
6
    <%= f.select(:default_value_mode,
7
                 [
8
                   [l(:label_absolute), 'fixed_date'],
9
                   [l(:label_relative), 'date_offset']
10
                 ],
11
                 {:selected => default_value_mode},
12
                 :data => {
13
                   :custom_field_default_value_target => 'mode',
14
                   :action => 'custom-field-default-value#update'
15
                 }) %>
16
  </p>
17
  <p class="indent" data-custom-field-default-value-target="fixedDate" <%= 'hidden' if default_value_mode != 'fixed_date' %>>
18
    <%= f.date_field(:default_value,
19
                     :value => fixed_date_default_value,
20
                     :size => 10,
21
                     :disabled => default_value_mode != 'fixed_date',
22
                     :data => {:custom_field_default_value_target => 'fixedDateInput'}) %>
23
    <%= calendar_for('custom_field_default_value') %>
24
  </p>
25
  <p class="indent" data-custom-field-default-value-target="dateOffset" <%= 'hidden' if default_value_mode != 'date_offset' %>>
26
    <%= f.text_field(:default_value,
27
                     :id => 'custom_field_default_value_offset',
28
                     :value => date_offset_default_value,
29
                     :size => 6,
30
                     :disabled => default_value_mode != 'date_offset',
31
                     :data => {:custom_field_default_value_target => 'dateOffsetInput'}) %>
32
    <%= l(:label_days_relative_to_today) %>
33
  </p>
34
</div>
3 35
<p><%= f.text_field :url_pattern, :size => 50, :label => :label_link_values_to %></p>
app/views/custom_fields/index.api.rsb
13 13
      api.is_filter         field.is_filter?
14 14
      api.searchable        field.searchable
15 15
      api.multiple          field.multiple?
16
      api.default_value     field.default_value
16
      api.default_value     field[:default_value]
17
      api.default_value_mode(field.default_value_mode.presence || 'fixed_date') if field.field_format == 'date'
17 18
      api.visible           field.visible?
18 19
      api.editable          field.editable?
19 20

  
config/locales/en.yml
372 372
  field_time_zone: Time zone
373 373
  field_searchable: Searchable
374 374
  field_default_value: Default value
375
  field_default_value_mode: Default value mode
375 376
  field_comments_sorting: Display comments
376 377
  field_parent_title: Parent page
377 378
  field_editable: Editable
......
784 785
  label_all: all
785 786
  label_any: any
786 787
  label_none: none
788
  label_absolute: Absolute
789
  label_relative: Relative
787 790
  label_nobody: nobody
788 791
  label_next: Next
789 792
  label_previous: Previous
config/locales/ja.yml
326 326
  field_time_zone: タイムゾーン
327 327
  field_searchable: 検索対象
328 328
  field_default_value: デフォルト値
329
  field_default_value_mode: デフォルト値の指定方法
329 330
  field_comments_sorting: コメントの表示順
330 331
  field_parent_title: 親ページ
331 332
  field_editable: 編集可能
......
596 597
  label_new_statuses_allowed: 遷移できるステータス
597 598
  label_all: すべて
598 599
  label_none: なし
600
  label_absolute: 絶対値
601
  label_relative: 相対値
599 602
  label_nobody: 無記名
600 603
  label_next: 次
601 604
  label_previous: 前
lib/redmine/field_format.rb
555 555
    class DateFormat < Unbounded
556 556
      add 'date'
557 557
      self.form_partial = 'custom_fields/formats/date'
558
      field_attributes :default_value_mode
558 559

  
559 560
      def cast_single_value(custom_field, value, customized=nil)
560 561
        value.to_date rescue nil
......
583 584
        {:type => :date}
584 585
      end
585 586

  
587
      def before_custom_field_save(custom_field)
588
        super
589

  
590
        custom_field.default_value_mode =
591
          custom_field.default_value_mode == 'date_offset' ? 'date_offset' : 'fixed_date'
592
        custom_field.default_value = custom_field[:default_value].to_s.strip if custom_field.default_value_mode == 'date_offset'
593
      end
594

  
586 595
      def group_statement(custom_field)
587 596
        order_statement(custom_field)
588 597
      end
test/functional/issues_controller_test.rb
3802 3802
  end
3803 3803

  
3804 3804
  def test_get_new_with_date_custom_field
3805
    field = IssueCustomField.create!(:name => 'Date', :field_format => 'date',
3806
                                     :tracker_ids => [1], :is_for_all => true)
3807
    @request.session[:user_id] = 2
3808
    get(
3809
      :new,
3810
      :params => {
3811
        :project_id => 1,
3812
        :tracker_id => 1
3813
      }
3814
    )
3815
    assert_response :success
3816
    assert_select 'input[name=?]', "issue[custom_field_values][#{field.id}]"
3805
    travel_to Time.zone.parse('2026-05-24T23:00:00Z') do
3806
      # 2026-05-24 23:00 UTC is 2026-05-25 08:00 in Tokyo
3807
      User.find(2).pref.update!(:time_zone => 'Tokyo')
3808
      fixed_date_field =
3809
        IssueCustomField.create!(
3810
          :name => 'Fixed date default value',
3811
          :field_format => 'date',
3812
          :default_value_mode => 'fixed_date',
3813
          :default_value => '2026-03-21',
3814
          :tracker_ids => [1],
3815
          :is_for_all => true
3816
        )
3817
      date_offset_field =
3818
        IssueCustomField.create!(
3819
          :name => 'Date offset default value',
3820
          :field_format => 'date',
3821
          :default_value_mode => 'date_offset',
3822
          :default_value => '5',
3823
          :tracker_ids => [1],
3824
          :is_for_all => true
3825
        )
3826
      @request.session[:user_id] = 2
3827
      get(
3828
        :new,
3829
        :params => {
3830
          :project_id => 1,
3831
          :tracker_id => 1
3832
        }
3833
      )
3834
      assert_response :success
3835
      assert_select 'input[name=?][value=?]', "issue[custom_field_values][#{fixed_date_field.id}]", '2026-03-21'
3836
      assert_select 'input[name=?][value=?]',
3837
                    "issue[custom_field_values][#{date_offset_field.id}]",
3838
                    '2026-05-30' # 5 days after in user's time zone
3839
    end
3817 3840
  end
3818 3841

  
3819 3842
  def test_get_new_with_text_custom_field
test/integration/api_test/custom_fields_test.rb
56 56
      assert_select "value:contains(?) + label:contains(?)", bar.id.to_s, 'Bar'
57 57
    end
58 58
  end
59

  
60
  test "GET /custom_fields.xml should include date offset default value mode" do
61
    field =
62
      IssueCustomField.generate!(
63
        :field_format => 'date',
64
        :default_value_mode => 'date_offset',
65
        :default_value => '-3'
66
      )
67

  
68
    get '/custom_fields.xml', :headers => credentials('admin')
69
    assert_response :success
70

  
71
    assert_select 'custom_field' do |elements|
72
      element = elements.detect {|e| e.at('id')&.text == field.id.to_s}
73
      assert_not_nil element
74
      assert_equal 'date_offset', element.at('default_value_mode').text
75
      assert_equal '-3', element.at('default_value').text
76
    end
77
  end
59 78
end
test/integration/api_test/issues_test.rb
681 681
    assert_equal "", issue.custom_field_value(field)
682 682
  end
683 683

  
684
  test "POST /issues.json with omitted date custom field should set date offset default" do
685
    User.current = User.find_by_login('jsmith')
686
    field =
687
      IssueCustomField.generate!(
688
        :field_format => 'date',
689
        :default_value_mode => 'date_offset',
690
        :default_value => '5',
691
        :trackers => Tracker.all,
692
        :is_for_all => true
693
      )
694
    issue = new_record(Issue) do
695
      post(
696
        '/issues.json',
697
        :params => {
698
          :issue => {
699
            :project_id => 1,
700
            :tracker_id => 1,
701
            :subject => 'API date default',
702
            :custom_field_values => {}
703
          }
704
        },
705
        :headers => credentials('jsmith'))
706
    end
707

  
708
    assert_equal (User.current.today + 5).to_s, issue.custom_field_value(field)
709
  end
710

  
711
  test "POST /issues.json with date custom field set to blank should not set date offset default" do
712
    field =
713
      IssueCustomField.generate!(
714
        :field_format => 'date',
715
        :default_value_mode => 'date_offset',
716
        :default_value => '5',
717
        :trackers => Tracker.all,
718
        :is_for_all => true
719
      )
720
    issue = new_record(Issue) do
721
      post(
722
        '/issues.json',
723
        :params => {
724
          :issue => {
725
            :project_id => 1,
726
            :tracker_id => 1,
727
            :subject => 'API blank date default',
728
            :custom_field_values => {field.id.to_s => ''}
729
          }
730
        },
731
        :headers => credentials('jsmith'))
732
    end
733

  
734
    assert_equal "", issue.custom_field_value(field)
735
  end
736

  
684 737
  test "POST /issues.json with failure should return errors" do
685 738
    assert_no_difference('Issue.count') do
686 739
      post(
test/unit/custom_field_test.rb
72 72
    assert field.valid?
73 73
  end
74 74

  
75
  def test_date_default_value_should_return_date_offset_from_today
76
    user = User.generate!
77
    user.stubs(:today).returns(Date.parse('2026-03-21'))
78
    User.current = user
79

  
80
    field = IssueCustomField.new(:name => 'Date', :field_format => 'date', :default_value_mode => 'date_offset')
81

  
82
    field.default_value = '0'
83
    assert_equal '2026-03-21', field.default_value
84

  
85
    field.default_value = '5'
86
    assert_equal '2026-03-26', field.default_value
87

  
88
    field.default_value = '-3'
89
    assert_equal '2026-03-18', field.default_value
90
  end
91

  
92
  def test_date_default_value_should_be_validated_when_date_offset_mode_is_selected
93
    field = IssueCustomField.new(:name => 'Date', :field_format => 'date', :default_value_mode => 'date_offset')
94

  
95
    field.default_value = 'invalid'
96
    assert field.invalid?
97

  
98
    field.default_value = '1.5'
99
    assert field.invalid?
100

  
101
    field.default_value = '+5'
102
    assert field.valid?
103

  
104
    field.default_value = '-3'
105
    assert field.valid?
106

  
107
    field.default_value = ''
108
    assert field.valid?
109
  end
110

  
111
  def test_date_default_value_should_be_validated_when_fixed_date_mode_is_selected
112
    field = IssueCustomField.new(:name => 'Date', :field_format => 'date', :default_value_mode => 'fixed_date')
113

  
114
    field.default_value = 'invalid'
115
    assert field.invalid?
116

  
117
    field.default_value = '2026-03-21'
118
    assert field.valid?
119
  end
120

  
75 121
  def test_field_format_should_be_validated
76 122
    field = CustomField.new(:name => 'Test', :field_format => 'foo')
77 123
    assert field.invalid?
(3-3/3)