Project

General

Profile

Feature #4437 » task_4437-add-timestamp-in-custom-fileds_rev0.patch

Akihiro MATOBA, 2021-11-23 07:31

View differences:

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

  
(1-1/2)