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 |