Feature #4437 » task_4437-add-timestamp-in-custom-fileds_rev0.patch
| app/helpers/application_helper.rb | ||
|---|---|---|
| 1615 | 1615 | ) | 
| 1616 | 1616 | end | 
| 1617 | 1617 | |
| 1618 | def datetimepicker_for(field_id) | |
| 1619 | javascript_tag( | |
| 1620 |       "$(function() { $('##{field_id}').attr('type','datetime-local')" + | |
| 1621 |       ".attr('pattern','[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}')" + | |
| 1622 |       ".attr('placeholder','yyyy-mm-ddThh:mm'); });" | |
| 1623 | ) | |
| 1624 | end | |
| 1625 | ||
| 1618 | 1626 | def include_calendar_headers_tags | 
| 1619 | 1627 | unless @calendar_headers_tags_included | 
| 1620 | 1628 | tags = ''.html_safe | 
| app/views/custom_fields/formats/_timestamp.html.erb | ||
|---|---|---|
| 1 | <p><%= f.datetime_local_field(:default_value, :value => Redmine::FieldFormat::TimestampFormat.time_local(@custom_field.default_value), :size => 12) %> | |
| 2 | (<%= Redmine::FieldFormat::TimestampFormat.timezone(@custom_field.default_value) %>)</p> | |
| 3 | <%= datetimepicker_for('custom_field_default_value') %> | |
| 4 | <p><%= f.text_field :url_pattern, :size => 50, :label => :label_link_values_to %></p> | |
| config/locales/en.yml | ||
|---|---|---|
| 686 | 686 | label_min_max_length: Min - Max length | 
| 687 | 687 | label_list: List | 
| 688 | 688 | label_date: Date | 
| 689 | label_timestamp: DateTime (timezone aware) | |
| 689 | 690 | label_integer: Integer | 
| 690 | 691 | label_float: Float | 
| 691 | 692 | label_boolean: Boolean | 
| lib/redmine/field_format.rb | ||
|---|---|---|
| 583 | 583 | end | 
| 584 | 584 | end | 
| 585 | 585 | |
| 586 | class TimestampFormat < Unbounded | |
| 587 | add 'timestamp' | |
| 588 | self.is_filter_supported = false | |
| 589 | self.searchable_supported = false | |
| 590 | self.form_partial = 'custom_fields/formats/timestamp' | |
| 591 |  | |
| 592 | def set_custom_field_value(custom_field, custom_field_value, value) | |
| 593 | # modify already stored default_value at custom_fields_controller.rb#update (Line 65) | |
| 594 | if !custom_field_value.customized.present? and custom_field.default_value === value | |
| 595 | custom_field.default_value = custom_field.default_value.in_time_zone(User.current.time_zone).utc.iso8601 rescue custom_field.default_value | |
| 596 | end | |
| 597 | # value is datetime_local in user's time_zone but no timezone | |
| 598 | # returns iso8601 formatted string trailing Z | |
| 599 | value.in_time_zone(User.current.time_zone).utc.iso8601 rescue value | |
| 600 | end | |
| 601 |  | |
| 602 | def validate_single_value(custom_field, value, customized=nil) | |
| 603 |         if /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.match?(value) | |
| 604 | [] | |
| 605 | else | |
| 606 |           [::I18n.t('activerecord.errors.messages.not_a_date')] | |
| 607 | end | |
| 608 | end | |
| 609 |  | |
| 610 | def cast_single_value(custom_field, value, customized=nil) | |
| 611 | # value is a iso8601 formatted string trailing Z | |
| 612 | # returns timewithzone in user's timezone | |
| 613 | value.in_time_zone(User.current.time_zone) rescue nil | |
| 614 | end | |
| 615 |  | |
| 616 | def self.time_local(value) | |
| 617 | # value is a iso8601 formatted string trailing Z | |
| 618 | # .to_time transforms value to Time in utc with tz=utc | |
| 619 | # .in_time_zone transforms Time to TimeWithZone in user's tz or Time if tz=nil | |
| 620 | # .iso8601 transforms to string like yyyy-MM-ddThh:mm:ss+tz | |
| 621 | # .slice trims the timezone, as datetime_local | |
| 622 | value.to_time.in_time_zone(User.current.time_zone).iso8601.slice(0,16) rescue "" | |
| 623 | end | |
| 624 |  | |
| 625 | def self.timezone(value) | |
| 626 | (value&.to_time || Time.now()).in_time_zone(User.current.time_zone).zone | |
| 627 | end | |
| 628 | ||
| 629 |       def edit_tag(view, tag_id, tag_name, custom_value, options={}) | |
| 630 | view.datetime_local_field_tag(tag_name, TimestampFormat.time_local(custom_value.value), options.merge(:id => tag_id, :size => 12)) + | |
| 631 |         view.datetimepicker_for(tag_id)  + " (#{TimestampFormat.timezone(custom_value.value)})" | |
| 632 | end | |
| 633 |  | |
| 634 |       def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={}) | |
| 635 | view.datetime_local_field_tag(tag_name, TimestampFormat.time_local(value), options.merge(:id => tag_id, :size => 12)) + | |
| 636 |         view.datetimepicker_for(tag_id)  + " (#{TimestampFormat.timezone(value)})" + | |
| 637 | bulk_clear_tag(view, tag_id, tag_name, custom_field, value) | |
| 638 | end | |
| 639 | end | |
| 640 | ||
| 586 | 641 | class List < Base | 
| 587 | 642 | self.multiple_supported = true | 
| 588 | 643 | field_attributes :edit_tag_style | 
| test/fixtures/custom_fields.yml | ||
|---|---|---|
| 146 | 146 | - Other value | 
| 147 | 147 | field_format: list | 
| 148 | 148 | position: 1 | 
| 149 | custom_fields_012: | |
| 150 | id: 12 | |
| 151 | name: Epoch | |
| 152 | type: IssueCustomField | |
| 153 | is_for_all: true | |
| 154 | possible_values: "" | |
| 155 | field_format: timestamp | |
| 156 | position: 1 | |
| 157 | ||
| test/fixtures/custom_fields_trackers.yml | ||
|---|---|---|
| 29 | 29 | custom_fields_trackers_010: | 
| 30 | 30 | custom_field_id: 9 | 
| 31 | 31 | tracker_id: 1 | 
| 32 | custom_fields_trackers_011: | |
| 33 | custom_field_id: 12 | |
| 34 | tracker_id: 1 | |
| test/fixtures/custom_values.yml | ||
|---|---|---|
| 101 | 101 | customized_id: 1 | 
| 102 | 102 | id: 17 | 
| 103 | 103 | value: '2009-12-01' | 
| 104 | custom_values_018: | |
| 105 | customized_type: Issue | |
| 106 | custom_field_id: 12 | |
| 107 | customized_id: 3 | |
| 108 | id: 18 | |
| 109 | value: '2011-03-11T05:46:00Z' | |
| test/integration/custom_field_timestamp_test.rb | ||
|---|---|---|
| 1 | # frozen_string_literal: true | |
| 2 | ||
| 3 | # Redmine - project management software | |
| 4 | # Copyright (C) 2006-2021 Jean-Philippe Lang | |
| 5 | # | |
| 6 | # This program is free software; you can redistribute it and/or | |
| 7 | # modify it under the terms of the GNU General Public License | |
| 8 | # as published by the Free Software Foundation; either version 2 | |
| 9 | # of the License, or (at your option) any later version. | |
| 10 | # | |
| 11 | # This program is distributed in the hope that it will be useful, | |
| 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 14 | # GNU General Public License for more details. | |
| 15 | # | |
| 16 | # You should have received a copy of the GNU General Public License | |
| 17 | # along with this program; if not, write to the Free Software | |
| 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
| 19 | ||
| 20 | require File.expand_path('../../test_helper', __FILE__) | |
| 21 | ||
| 22 | class CustomFieldsTimestampTest < Redmine::IntegrationTest | |
| 23 | fixtures :projects, | |
| 24 | :users, :email_addresses, | |
| 25 | :user_preferences, | |
| 26 | :roles, | |
| 27 | :members, | |
| 28 | :member_roles, | |
| 29 | :trackers, | |
| 30 | :projects_trackers, | |
| 31 | :enabled_modules, | |
| 32 | :issue_statuses, | |
| 33 | :issues, | |
| 34 | :enumerations, | |
| 35 | :custom_fields, | |
| 36 | :custom_values, | |
| 37 | :custom_fields_trackers, | |
| 38 | :attachments | |
| 39 | ||
| 40 | def setup | |
| 41 | @field = IssueCustomField.find(12) | |
| 42 | @issue = Issue.find(3) | |
| 43 |     log_user('jsmith', 'jsmith') | |
| 44 | @user = User.find(session[:user_id]) | |
| 45 | end | |
| 46 | ||
| 47 | def test_get_issue_with_timestamp_custom_field | |
| 48 | assert_nil ENV['TZ'] | |
| 49 | assert_equal 'UTC', RedmineApp::Application.config.time_zone | |
| 50 | assert_equal :local, RedmineApp::Application.config.active_record.default_timezone | |
| 51 | assert_equal 'en', Setting.default_language | |
| 52 | ||
| 53 | # get issues/14 in tz=nil | |
| 54 | assert_nil @user.preference.time_zone | |
| 55 |     get "/issues/#{@issue.id}" | |
| 56 | assert_response :success | |
| 57 |     assert_select ".cf_#{@field.id} .value", :text => '03/11/2011 05:46 AM' | |
| 58 |     assert_select 'input[name=?][value=?]', "issue[custom_field_values][#{@field.id}]", '2011-03-11T05:46' | |
| 59 | ||
| 60 | # get issues/14 in tz='UTC' | |
| 61 | @user.preference.time_zone = 'UTC' | |
| 62 | @user.preference.save | |
| 63 |     get "/issues/#{@issue.id}" | |
| 64 | assert_response :success | |
| 65 |     assert_select ".cf_#{@field.id} .value", :text => '03/11/2011 05:46 AM' | |
| 66 |     assert_select 'input[name=?][value=?]', "issue[custom_field_values][#{@field.id}]", '2011-03-11T05:46' | |
| 67 |  | |
| 68 | # get issues/14 in lang="ja", tz='Tokyo' (+0900) | |
| 69 | @user.preference.time_zone = 'Tokyo' | |
| 70 | @user.preference.save | |
| 71 | @user.language = "ja" | |
| 72 | @user.save | |
| 73 |     get "/issues/#{@issue.id}" | |
| 74 | assert_response :success | |
| 75 |     assert_select ".cf_#{@field.id} .value", :text => '2011/03/11 14:46' | |
| 76 |     assert_select 'input[name=?][value=?]', "issue[custom_field_values][#{@field.id}]", '2011-03-11T14:46' | |
| 77 | end | |
| 78 | ||
| 79 | def test_bulk_edit | |
| 80 | get( | |
| 81 | "/issues/bulk_edit", | |
| 82 |       :params => { | |
| 83 | :ids => [1, @issue.id], | |
| 84 | } | |
| 85 | ) | |
| 86 | assert_response :success | |
| 87 | end | |
| 88 | ||
| 89 | def test_put_issue_with_timestamp_custom_field | |
| 90 | # update issues/14 in lang="ja", tz='Tokyo' (+0900) | |
| 91 | @user.preference.time_zone = 'Tokyo' | |
| 92 | @user.preference.save | |
| 93 | @user.language = "ja" | |
| 94 | @user.save | |
| 95 |     put "/issues/#{@issue.id}", | |
| 96 |     :params => { | |
| 97 |       :issue => { | |
| 98 |         :custom_field_values => {@field.id.to_s => "1985-08-12T18:56"}  # in tz=Asia/Tokyo +0900 | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | assert_response :found | |
| 103 | assert_equal "1985-08-12T09:56:00Z", CustomValue.find_by(:customized_id=>@issue.id, :custom_field_id => @field.id).value | |
| 104 | ||
| 105 | # update issues/14 in tz=nil | |
| 106 | @user.preference.time_zone = nil | |
| 107 | @user.preference.save | |
| 108 |     put "/issues/#{@issue.id}", | |
| 109 |     :params => { | |
| 110 |       :issue => { | |
| 111 |         :custom_field_values => {@field.id.to_s => "1912-04-14T23:40"}  # in UTC | |
| 112 | } | |
| 113 | } | |
| 114 | assert_equal "1912-04-14T23:40:00Z", CustomValue.find_by(:customized_id=>@issue.id, :custom_field_id => @field.id).value | |
| 115 | end | |
| 116 | ||
| 117 | def test_put_issue_with_timestamp_custom_field_mail | |
| 118 |  | |
| 119 | ActionMailer::Base.deliveries.clear | |
| 120 | Setting.plain_text_mail = '0' | |
| 121 |  | |
| 122 | @user.preference.time_zone = 'Tokyo' | |
| 123 | @user.preference.save | |
| 124 | @user.language = "ja" | |
| 125 | @user.save | |
| 126 |  | |
| 127 | watcher = User.find_by(:login => 'dlopper') | |
| 128 | watcher.preference ||= UserPreference.new(:user_id => watcher.id) | |
| 129 | watcher.preference.time_zone = 'Newfoundland' | |
| 130 | watcher.preference.save | |
| 131 | watcher.language = "fr" | |
| 132 | watcher.save | |
| 133 |  | |
| 134 |     put "/issues/#{@issue.id}", | |
| 135 |     :params => { | |
| 136 |       :issue => { | |
| 137 |         :custom_field_values => {@field.id.to_s => "2005-04-25T09:18"}  # in tz=Asia/Tokyo +0900 | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | ActionMailer::Base.deliveries.each do |mail| | |
| 142 | recipient = mail.header["To"].value.first | |
| 143 | case | |
| 144 |       when recipient.starts_with?("jsmith@") | |
| 145 | assert_mail_body_match 'Epoch を 2011/03/11 14:46 から 2005/04/25 09:18 に変更', mail | |
| 146 |       when recipient.starts_with?("dlopper@") | |
| 147 | assert_mail_body_match 'Epoch changé de 11/03/2011 02:16 à 24/04/2005 21:48', mail | |
| 148 | else | |
| 149 | flunk | |
| 150 | end | |
| 151 | end | |
| 152 | end | |
| 153 | ||
| 154 | test "timestamp may always utc.iso8601 via api" do | |
| 155 | @user.preference.time_zone = 'Tokyo' | |
| 156 | @user.preference.save | |
| 157 | @user.language = "ja" | |
| 158 | @user.save | |
| 159 | with_settings :rest_api_enabled => '1' do | |
| 160 | get '/issues/3.xml', :headers => credentials(@user.login) | |
| 161 | assert_response :success | |
| 162 | assert_equal 'application/xml', response.media_type | |
| 163 | assert_select "custom_field[id=12] value", '2011-03-11T05:46:00Z', 'timestamp may always utc.iso8601 via api' | |
| 164 | end | |
| 165 | end | |
| 166 | ||
| 167 | test 'the value of custom_field_default_value should be presented in timezone of the user' do | |
| 168 |     user = User.find_by_login('admin') | |
| 169 |     post("/login",:params => {:username => user.login,:password => user.login}) | |
| 170 |  | |
| 171 | assert_nil @field.default_value | |
| 172 |     get "/custom_fields/#{@field.id}/edit" | |
| 173 | assert_response :success | |
| 174 | assert_select '#custom_field_default_value', :value => '' | |
| 175 |  | |
| 176 | @field.default_value = '1986-01-28T16:39:00Z' | |
| 177 | @field.save | |
| 178 |     get "/custom_fields/#{@field.id}/edit" | |
| 179 | assert_response :success | |
| 180 | assert_select '#custom_field_default_value[value=?]', '1986-01-28T16:39' | |
| 181 | assert_select 'p:has(#custom_field_default_value)', /(UTC)/ | |
| 182 |  | |
| 183 | user.preference.time_zone = "Eastern Time (US & Canada)" | |
| 184 | user.preference.save | |
| 185 |     get "/custom_fields/#{@field.id}/edit" | |
| 186 | assert_response :success | |
| 187 | assert_select '#custom_field_default_value[value=?]', '1986-01-28T11:39' | |
| 188 | assert_select 'p:has(#custom_field_default_value)', /(EST)/ | |
| 189 |  | |
| 190 | user.preference.time_zone = "Central Time (US & Canada)" | |
| 191 | user.preference.save | |
| 192 |     put "/custom_fields/#{@field.id}", | |
| 193 |     :params => { | |
| 194 |       :custom_field => {:default_value => "2003-02-01T08:59"}  # in tz=CST -0600 | |
| 195 | } | |
| 196 | assert_response :found | |
| 197 | assert_equal "2003-02-01T14:59:00Z", CustomField.find(@field.id).default_value | |
| 198 | end | |
| 199 | ||
| 200 | end | |
| test/unit/custom_field_test.rb | ||
|---|---|---|
| 195 | 195 |     assert !f.valid_field_value?('abc') | 
| 196 | 196 | end | 
| 197 | 197 | |
| 198 | def test_timestamp_field_validation | |
| 199 | f = CustomField.new(:field_format => 'timestamp') | |
| 200 | ||
| 201 | assert f.valid_field_value?(nil) | |
| 202 |     assert f.valid_field_value?('') | |
| 203 |     assert !f.valid_field_value?(' ') | |
| 204 |     assert f.valid_field_value?('1975-07-14T00:00') | |
| 205 |     assert !f.valid_field_value?('1975-07-33T00:00') | |
| 206 |     assert !f.valid_field_value?('1975-07-14T25:00') | |
| 207 |     assert f.valid_field_value?('1975-07-14T00:59') | |
| 208 |     assert !f.valid_field_value?('1975-07-14T00:60') | |
| 209 |     assert !f.valid_field_value?('abc') | |
| 210 | end | |
| 211 | ||
| 198 | 212 | def test_list_field_validation | 
| 199 | 213 | f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2']) | 
| 200 | 214 | |