Feature #44129 » 0001-Add-relative-date-option-for-date-format-custom-fiel.patch
| 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? |
- « Previous
- 1
- 2
- 3
- Next »