[#47862] Support timezones in the Query#timestamps relative date keywords.

https://community.openproject.org/work_packages/47862
This commit is contained in:
Dombi Attila
2023-04-27 17:45:49 +03:00
committed by Oliver Günther
parent 09ed751fbf
commit c193387b5c
22 changed files with 223 additions and 88 deletions
+7 -3
View File
@@ -33,7 +33,11 @@ class Timestamp
class TimestampParser
DATE_KEYWORD_REGEX =
/^(?:oneDayAgo|lastWorkingDay|oneWeekAgo|oneMonthAgo)@(?:([0-1]?[0-9]|2[0-3]):[0-5]?[0-9])$/
%r{
^(?:oneDayAgo|lastWorkingDay|oneWeekAgo|oneMonthAgo) # match the relative date keyword
@(?:([0-1]?[0-9]|2[0-3]):[0-5]?[0-9]) # match the hour part
[+-](?:([0-1]?[0-9]|2[0-3]):[0-5]?[0-9])$ # match the timezone offset
}x
def initialize(string)
@original_string = string
@@ -163,8 +167,8 @@ class Timestamp
date = case relative_date_keyword
when 'oneDayAgo' then 1.day.ago
when 'lastWorkingDay' then Day.last_working.date || 1.day.ago
when 'oneWeekAgo' then 1.week.ago
when 'oneMonthAgo' then 1.month.ago
when 'oneWeekAgo' then 1.week.ago
when 'oneMonthAgo' then 1.month.ago
end
Time.zone.parse(time_part, date)
@@ -78,8 +78,10 @@ get:
description: >
Indicates the timestamps to filter by when showing changed attributes
on work packages. Values can be either ISO8601 dates, ISO8601 durations and the
following relative date keywords: "oneDayAgo@HH:MM", "lastWorkingDay@HH:MM", "oneWeekAgo@HH:MM",
"oneMonthAgo@HH:MM". The "HH:MM" part represents the zero paded hours and minutes.
following relative date keywords: "oneDayAgo@HH:MM+HH:MM", "lastWorkingDay@HH:MM+HH:MM",
"oneWeekAgo@HH:MM+HH:MM", "oneMonthAgo@HH:MM+HH:MM".
The first "HH:MM" part represents the zero paded hours and minutes.
The last "+HH:MM" part represents the timezone offset from UTC associated with the time.
example: '2023-01-01,P-1Y,PT0S,lastWorkingDay@12:00'
in: query
required: false
+6 -2
View File
@@ -70,8 +70,12 @@ get:
description: >
Indicates the timestamps to filter by when showing changed attributes
on work packages. Values can be either ISO8601 dates, ISO8601 durations and the
following relative date keywords: "oneDayAgo@HH:MM", "lastWorkingDay@HH:MM", "oneWeekAgo@HH:MM",
"oneMonthAgo@HH:MM". The "HH:MM" part represents the zero paded hours and minutes.
following relative date keywords: "oneDayAgo@HH:MM+HH:MM", "lastWorkingDay@HH:MM+HH:MM",
"oneWeekAgo@HH:MM+HH:MM", "oneMonthAgo@HH:MM+HH:MM".
The first "HH:MM" part represents the zero paded hours and minutes.
The last "+HH:MM" part represents the timezone offset from UTC associated with the time,
the offset can be positive or negative e.g."oneDayAgo@01:00+01:00", "oneDayAgo@01:00-01:00".
example: '2023-01-01,P-1Y,PT0S,lastWorkingDay@12:00'
in: query
required: false
+6 -2
View File
@@ -138,8 +138,12 @@ get:
description: >
Indicates the timestamps to filter by when showing changed attributes
on work packages. Values can be either ISO8601 dates, ISO8601 durations and the
following relative date keywords: "oneDayAgo@HH:MM", "lastWorkingDay@HH:MM", "oneWeekAgo@HH:MM",
"oneMonthAgo@HH:MM". The "HH:MM" part represents the zero paded hours and minutes.
following relative date keywords: "oneDayAgo@HH:MM+HH:MM", "lastWorkingDay@HH:MM+HH:MM",
"oneWeekAgo@HH:MM+HH:MM", "oneMonthAgo@HH:MM+HH:MM".
The first "HH:MM" part represents the zero paded hours and minutes.
The last "+HH:MM" part represents the timezone offset from UTC associated with the time,
the offset can be positive or negative e.g."oneDayAgo@01:00+01:00", "oneDayAgo@01:00-01:00".
example: '2023-01-01,P-1Y,PT0S,lastWorkingDay@12:00'
in: query
required: false
+6 -2
View File
@@ -74,8 +74,12 @@ get:
- description: |-
In order to perform a [baseline comparison](/docs/api/baseline-comparisons) of the work-package attributes, you may
provide one or several timestamps in ISO-8601 format as comma-separated list. The timestamps may be absolute or relative,
such as ISO8601 dates, ISO8601 durations and the following relative date keywords: "oneDayAgo@HH:MM", "lastWorkingDay@HH:MM",
"oneWeekAgo@HH:MM", "oneMonthAgo@HH:MM". The "HH:MM" part represents the zero paded hours and minutes.
such as ISO8601 dates, ISO8601 durations and the following relative date keywords: "oneDayAgo@HH:MM+HH:MM",
"lastWorkingDay@HH:MM+HH:MM", "oneWeekAgo@HH:MM+HH:MM", "oneMonthAgo@HH:MM+HH:MM".
The first "HH:MM" part represents the zero paded hours and minutes.
The last "+HH:MM" part represents the timezone offset from UTC associated with the time,
the offset can be positive or negative e.g."oneDayAgo@01:00+01:00", "oneDayAgo@01:00-01:00".
Usually, the first timestamp is the baseline date, the last timestamp is the current date.
in: query
name: timestamps
+6 -2
View File
@@ -127,8 +127,12 @@ get:
- description: |-
In order to perform a [baseline comparison](/docs/api/baseline-comparisons), you may provide one or several timestamps
in ISO-8601 format as comma-separated list. The timestamps may be absolute or relative,
such as ISO8601 dates, ISO8601 durations and the following relative date keywords: "oneDayAgo@HH:MM", "lastWorkingDay@HH:MM",
"oneWeekAgo@HH:MM", "oneMonthAgo@HH:MM". The "HH:MM" part represents the zero paded hours and minutes.
such as ISO8601 dates, ISO8601 durations and the following relative date keywords: "oneDayAgo@HH:MM+HH:MM",
"lastWorkingDay@HH:MM+HH:MM", "oneWeekAgo@HH:MM+HH:MM", "oneMonthAgo@HH:MM+HH:MM".
The first "HH:MM" part represents the zero paded hours and minutes.
The last "+HH:MM" part represents the timezone offset from UTC associated with the time,
the offset can be positive or negative e.g."oneDayAgo@01:00+01:00", "oneDayAgo@01:00-01:00".
Usually, the first timestamp is the baseline date, the last timestamp is the current date.
example: '2022-01-01T00:00:00Z,PT0S'
in: query
+2 -1
View File
@@ -22,7 +22,8 @@ description: |-
```
Each timestamp should be given as an [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601) string, either an absolute date and time with timezone, e.g. `"2022-01-01T00:00:00Z"`, or a relative timestamp utilizing the [ISO-8601-Duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) format, e.g. `"P-1Y"`, which is composed of an initial `"P"` for "Period", and a duration. `"P-1Y"` is interpreted as the relative timestamp "1 year ago".
Furthermore, a set of predefined relative date keywords can also be passed for the timestamps: `"oneDayAgo@HH:MM", "lastWorkingDay@HH:MM", "oneWeekAgo@HH:MM", "oneMonthAgo@HH:MM"`. The `"HH:MM"` part represents the zero paded hours and minutes, e.g. `"oneMonthAgo@21:20"`.
Furthermore, a set of predefined relative date keywords can also be passed for the timestamps: `"oneDayAgo@HH:MM+HH:MM", "lastWorkingDay@HH:MM+HH:MM", "oneWeekAgo@HH:MM+HH:MM", "oneMonthAgo@HH:MM+HH:MM"`. The `"HH:MM"` part represents the zero paded hours and minutes, e.g. `"oneMonthAgo@21:00+00:00"`. The last "+HH:MM" part represents the timezone offset from UTC associated with the time,
e.g. `"oneMonthAgo@21:00+02:00"` means a +2 hour timezone offset from UTC. The offset can be positive or negative e.g."oneDayAgo@01:00+01:00", "oneDayAgo@01:00-01:00".
Several timestamps should be passed as comma-separated list of these ISO-8601 strings to the `timestamps` parameter, e.g. `"2022-01-01T00:00:00Z,PT0S"`.
+4 -2
View File
@@ -237,8 +237,10 @@ description: |-
* `Timestamp` - refers to an ISO 8601 combined date and time, e.g. "2014-05-21T13:37:00Z" or to an ISO 8601 duration, e.g. "P1DT18H".
The following shorthand values are also being parsed as duration: "1d", "1w", "1m", "1y", "1y-2m", "-2y".
It can also refer the following relative date keywords: "oneDayAgo@HH:MM", "lastWorkingDay@HH:MM", "oneWeekAgo@HH:MM", "oneMonthAgo@HH:MM".
The "HH:MM" part represents the zero paded hours and minutes of the relative date e.g. "oneDayAgo@00:00", "lastWorkingDay@21:00".
It can also refer the following relative date keywords: `"oneDayAgo@HH:MM+HH:MM", "lastWorkingDay@HH:MM+HH:MM", "oneWeekAgo@HH:MM+HH:MM", "oneMonthAgo@HH:MM+HH:MM"`.
The `"HH:MM"` part represents the zero paded hours and minutes, e.g. `"oneMonthAgo@21:00+00:00"`. The last "+HH:MM" part represents the timezone offset from UTC
associated with the time, e.g. `"oneMonthAgo@21:00+02:00"` means a +2 hour timezone offset from UTC.
The offset can be positive or negative e.g."oneDayAgo@01:00+01:00", "oneDayAgo@01:00-01:00".
## Colors
@@ -111,8 +111,8 @@ export class TimezoneService {
return date.diff(today, 'days');
}
public formattedTime(datetimeString:string):string {
return this.parseDatetime(datetimeString).format(this.getTimeFormat());
public formattedTime(datetimeString:string, format?:string):string {
return this.parseDatetime(datetimeString).format(format || this.getTimeFormat());
}
public formattedDatetime(datetimeString:string):string {
@@ -52,9 +52,7 @@
</spot-tooltip>
<span slot="description"
*ngIf="timeZoneSelected"
[textContent]="text.time_description()"></span>
<span slot="description" [textContent]="text.time_description()"></span>
</spot-form-field>
<div class="spot-action-bar"
@@ -76,12 +76,10 @@ export class OpBaselineComponent extends UntilDestroyedMixin implements OnInit {
public selectedFilter = '-';
public selectedTimezoneFormattedTime = '';
public selectedTimezoneFormattedTime = `${this.selectedTime}+00:00`;
public filterSelected = false;
public timeZoneSelected = false;
public daysNumber = 0;
public tooltipPosition = SpotDropAlignmentOption.TopRight;
@@ -141,17 +139,18 @@ export class OpBaselineComponent extends UntilDestroyedMixin implements OnInit {
public ngOnInit():void {
if (this.wpTableBaseline.isActive()) {
const value = this.wpTableBaseline.current[0];
const [date, time] = value.split('@');
const [date, timeWithZone] = value.split('@');
const time = timeWithZone.split(/[+-]/)[0];
this.filterChange(date);
this.selectedTime = time || '00:00';
this.selectedTimezoneFormattedTime = timeWithZone || '00:00+00:00';
this.filterSelected = true;
}
}
public clearSelection():void {
this.filterSelected = false;
this.timeZoneSelected = false;
this.selectedTime = '0:00';
this.selectedDate = '';
this.selectedFilter = '-';
@@ -167,7 +166,7 @@ export class OpBaselineComponent extends UntilDestroyedMixin implements OnInit {
if (this.selectedFilter === '-') {
this.wpTableBaseline.disable();
} else {
const filterString = `${this.selectedFilter}@${this.selectedTime}`;
const filterString = `${this.selectedFilter}@${this.selectedTimezoneFormattedTime}`;
this.wpTableBaseline.update([filterString, DEFAULT_TIMESTAMP]);
}
@@ -236,12 +235,8 @@ export class OpBaselineComponent extends UntilDestroyedMixin implements OnInit {
public timeChange(value:string):void {
this.selectedTime = value;
const timeZone = this.configuration.isTimezoneSet();
if (timeZone) {
this.timeZoneSelected = true;
const dateTime= `${this.selectedDate} ${value}`;
this.selectedTimezoneFormattedTime = this.timezoneService.formattedTime(dateTime);
}
const dateTime= `${this.selectedDate} ${value}`;
this.selectedTimezoneFormattedTime = this.timezoneService.formattedTime(dateTime, 'HH:mmZ');
}
public filterChange(value:string):void {
@@ -65,7 +65,7 @@ describe 'baseline query saving', js: true do
wp_table.expect_and_dismiss_toaster(message: 'Successful creation.')
query = retry_block { Query.find_by! name: 'Baseline query' }
expect(query.timestamps).to eq ['oneDayAgo@00:00', 'PT0S']
expect(query.timestamps).to eq ['oneDayAgo@00:00+00:00', 'PT0S']
wp_table.visit_query query
+139 -24
View File
@@ -178,19 +178,67 @@ describe Timestamp do
describe "when providing relative date keywords" do
describe "oneDayAgo@12:00" do
subject { described_class.parse("oneDayAgo@12:00") }
subject { described_class.parse("oneDayAgo@12:00+00:00") }
it "returns a Timestamp representing the yesterday at 12:00 pm" do
it "returns a Timestamp representing the yesterday at 12:00 pm +00:00" do
expect(subject).to be_a described_class
expect(subject).to be_valid
expect(subject).to be_relative
expect(subject).to be_relative_date_keyword
expect(subject.to_time).to eq 1.day.ago.change(hour: 12)
expect(subject.to_time).to eq 1.day.ago.utc.change(hour: 12)
end
context "with a timezone difference of +02:00" do
subject { described_class.parse("oneDayAgo@12:00+02:00") }
it "returns a Timestamp representing the yesterday at 10:00 pm UTC" do
expect(subject).to be_a described_class
expect(subject).to be_valid
expect(subject).to be_relative
expect(subject).to be_relative_date_keyword
expect(subject.to_time).to eq 1.day.ago.utc.change(hour: 10)
end
end
context "with a timezone difference of -02:00" do
subject { described_class.parse("oneDayAgo@12:00-02:00") }
it "returns a Timestamp representing the yesterday at 14:00 pm UTC" do
expect(subject).to be_a described_class
expect(subject).to be_valid
expect(subject).to be_relative
expect(subject).to be_relative_date_keyword
expect(subject.to_time).to eq 1.day.ago.utc.change(hour: 14)
end
end
context "with a timezone rolling over to next day" do
subject { described_class.parse("oneDayAgo@23:00-02:00") }
it "returns a Timestamp representing today at 1:00 am UTC" do
expect(subject).to be_a described_class
expect(subject).to be_valid
expect(subject).to be_relative
expect(subject).to be_relative_date_keyword
expect(subject.to_time).to eq Time.now.utc.change(hour: 1)
end
end
context "with a timezone rolling back to previous day" do
subject { described_class.parse("oneDayAgo@00:00+02:00") }
it "returns a Timestamp representing the 2 days ago at 22:00 pm UTC" do
expect(subject).to be_a described_class
expect(subject).to be_valid
expect(subject).to be_relative
expect(subject).to be_relative_date_keyword
expect(subject.to_time).to eq 2.days.ago.utc.change(hour: 22)
end
end
end
describe "lastWorkingDay@12:00" do
subject { described_class.parse("lastWorkingDay@12:00") }
describe "lastWorkingDay@12:00+00:00" do
subject { described_class.parse("lastWorkingDay@12:00+00:00") }
before do
week_with_all_days_working
@@ -202,31 +250,31 @@ describe Timestamp do
expect(subject).to be_valid
expect(subject).to be_relative
expect(subject).to be_relative_date_keyword
expect(subject.to_time).to eq 2.days.ago.change(hour: 12)
expect(subject.to_time).to eq 2.days.ago.utc.change(hour: 12)
end
end
describe "oneWeekAgo@12:00" do
subject { described_class.parse("oneWeekAgo@12:00") }
describe "oneWeekAgo@12:00+00:00" do
subject { described_class.parse("oneWeekAgo@12:00+00:00") }
it "returns a Timestamp representing the last week at 12:00 pm" do
expect(subject).to be_a described_class
expect(subject).to be_valid
expect(subject).to be_relative
expect(subject).to be_relative_date_keyword
expect(subject.to_time).to eq 1.week.ago.change(hour: 12)
expect(subject.to_time).to eq 1.week.ago.utc.change(hour: 12)
end
end
describe "oneMonthAgo@12:00" do
subject { described_class.parse("oneMonthAgo@12:00") }
describe "oneMonthAgo@00:00+00:00" do
subject { described_class.parse("oneMonthAgo@00:00+00:00") }
it "returns a Timestamp representing the last month at 12:00 pm" do
it "returns a Timestamp representing the last month at 00:00 am" do
expect(subject).to be_a described_class
expect(subject).to be_valid
expect(subject).to be_relative
expect(subject).to be_relative_date_keyword
expect(subject.to_time).to eq 1.month.ago.change(hour: 12)
expect(subject.to_time).to eq 1.month.ago.utc.change(hour: 0)
end
end
end
@@ -248,18 +296,85 @@ describe Timestamp do
end
describe "when providing something invalid with relative date keywords" do
subject { described_class.parse("oneDayAgo@") }
context "with missing the hours part" do
subject { described_class.parse("oneDayAgo@") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
end
describe "when providing something invalid with relative date keywords#2" do
subject { described_class.parse("oneDayAgo@11:22:asd") }
context "with having an invalid hours part" do
subject { described_class.parse("oneDayAgo@11:22:asd") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
context "with having an invalid timezone part" do
subject { described_class.parse("oneDayAgo@11:22+00:0a") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
context "with having more than 23 in the hours part" do
subject { described_class.parse("oneDayAgo@24:22+00:00") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
context "with having more than 59 in the minutes part" do
subject { described_class.parse("oneDayAgo@23:60+00:00") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
context "with having more than 23 in the time zone offset hours part" do
subject { described_class.parse("oneDayAgo@00:00+24:00") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
context "with having more than 59 in the time zone offset minutes part" do
subject { described_class.parse("oneDayAgo@00:00+00:60") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
context "with having a negative hours part" do
subject { described_class.parse("oneDayAgo@-23:00+00:00") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
context "with having a negative minutes part" do
subject { described_class.parse("oneDayAgo@00:-50+00:00") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
# Negtive time zone offsets are allowed
context "with having a negative time zone offset minutes part" do
subject { described_class.parse("oneDayAgo@00:00+00:-50") }
it "raises an error" do
expect { subject }.to raise_error ArgumentError
end
end
end
end
@@ -345,7 +460,7 @@ describe Timestamp do
end
describe "for a timestamp as a date keyword representing a point in time relative to now" do
let(:timestamp) { described_class.new("oneWeekAgo@12:00") }
let(:timestamp) { described_class.new("oneWeekAgo@12:00+00:00") }
it "returns true" do
expect(subject).to be true
@@ -374,10 +489,10 @@ describe Timestamp do
end
describe "for a timestamp as a date keyword representing a point in time relative to now" do
let(:timestamp) { described_class.new('oneDayAgo@12:00') }
let(:timestamp) { described_class.new('oneDayAgo@12:00+02:00') }
it "returns an relative date keyword" do
expect(subject).to eq 'oneDayAgo@12:00'
expect(subject).to eq 'oneDayAgo@12:00+02:00'
end
end
end
@@ -326,7 +326,7 @@ describe "POST /api/v3/queries/form", with_flag: { show_changes: true } do
describe 'with all parameters given' do
let(:status) { create(:status) }
let(:timestamps) { [1.week.ago.iso8601, 'lastWorkingDay@12:00', "P0D"] }
let(:timestamps) { [1.week.ago.iso8601, 'lastWorkingDay@12:00+00:00', "P0D"] }
let(:parameters) do
{
@@ -32,7 +32,7 @@ describe "POST /api/v3/queries", with_flag: { show_changes: true } do
shared_let(:user) { create(:admin) }
shared_let(:status) { create(:status) }
shared_let(:project) { create(:project) }
shared_let(:timestamps) { [1.week.ago.iso8601, 'lastWorkingDay@12:00', "P0D"] }
shared_let(:timestamps) { [1.week.ago.iso8601, 'lastWorkingDay@12:00+00:00', "P0D"] }
let(:default_params) do
{
@@ -35,7 +35,7 @@ describe "POST /api/v3/queries/form", with_flag: { show_changes: true } do
let(:user) { create(:admin) }
let(:role) { create(:existing_role, permissions:) }
let(:permissions) { %i(view_work_packages manage_public_queries) }
let(:timestamps) { [1.week.ago.iso8601, 'lastWorkingDay@12:00', "P0D"] }
let(:timestamps) { [1.week.ago.iso8601, 'lastWorkingDay@12:00+00:00', "P0D"] }
let!(:project) { create(:project_with_types, members: { user => role }) }
@@ -32,7 +32,7 @@ describe "PATCH /api/v3/queries/:id" do
shared_let(:user) { create(:admin) }
shared_let(:status) { create(:status) }
shared_let(:project) { create(:project) }
shared_let(:timestamps) { [1.week.ago.iso8601, 'lastWorkingDay@12:00', "P0D"] }
shared_let(:timestamps) { [1.week.ago.iso8601, 'lastWorkingDay@12:00+00:00', "P0D"] }
let!(:query) do
create(
@@ -244,8 +244,9 @@ describe 'API v3 Work package resource',
last_response
end
let(:path) { "#{api_v3_paths.work_packages}?timestamps=#{timestamps.join(',')}" }
let(:timestamps) { [Timestamp.parse('2015-01-01T00:00:00Z'), Timestamp.now] }
let(:timestamps_param) { CGI.escape(timestamps.join(',')) }
let(:path) { "#{api_v3_paths.work_packages}?timestamps=#{timestamps_param}" }
let(:baseline_time) { timestamps.first.to_time }
let(:created_at) { baseline_time - 1.day }
@@ -355,7 +356,7 @@ describe 'API v3 Work package resource',
end
describe "when filtering such that the filters do not match at all timestamps" do
let(:path) { "#{api_v3_paths.path_for(:work_packages, filters:)}&timestamps=#{timestamps.join(',')}" }
let(:path) { "#{api_v3_paths.path_for(:work_packages, filters:)}&timestamps=#{timestamps_param}" }
let(:filters) do
[
{
@@ -774,7 +775,7 @@ describe 'API v3 Work package resource',
end
context 'when the timestamps are relative date keywords' do
let(:timestamps) { [Timestamp.parse('oneWeekAgo@11:00'), Timestamp.parse('lastWorkingDay@12:00')] }
let(:timestamps) { [Timestamp.parse('oneWeekAgo@11:00+00:00'), Timestamp.parse('lastWorkingDay@12:00+00:00')] }
it 'has an embedded link to the baseline work package' do
expect(subject.body)
@@ -801,13 +802,13 @@ describe 'API v3 Work package resource',
it 'has the relative timestamps within the _meta timestamps' do
expect(subject.body)
.to be_json_eql('oneWeekAgo@11:00'.to_json)
.to be_json_eql('oneWeekAgo@11:00+00:00'.to_json)
.at_path('_embedded/elements/0/_embedded/attributesByTimestamp/0/_meta/timestamp')
expect(subject.body)
.to be_json_eql('lastWorkingDay@12:00'.to_json)
.to be_json_eql('lastWorkingDay@12:00+00:00'.to_json)
.at_path('_embedded/elements/0/_embedded/attributesByTimestamp/1/_meta/timestamp')
expect(subject.body)
.to be_json_eql('lastWorkingDay@12:00'.to_json)
.to be_json_eql('lastWorkingDay@12:00+00:00'.to_json)
.at_path('_embedded/elements/0/_meta/timestamp')
end
@@ -820,7 +821,7 @@ describe 'API v3 Work package resource',
describe "timestamp" do
it 'has the baseline timestamp, which is the first timestmap' do
expect(subject.body)
.to be_json_eql('oneWeekAgo@11:00'.to_json)
.to be_json_eql('oneWeekAgo@11:00+00:00'.to_json)
.at_path('_embedded/elements/0/_embedded/attributesByTimestamp/0/_meta/timestamp')
end
end
@@ -832,7 +833,7 @@ describe 'API v3 Work package resource',
describe "timestamp" do
it 'has the current timestamp, which is the second timestamp' do
expect(subject.body)
.to be_json_eql('lastWorkingDay@12:00'.to_json)
.to be_json_eql('lastWorkingDay@12:00+00:00'.to_json)
.at_path('_embedded/elements/0/_embedded/attributesByTimestamp/1/_meta/timestamp')
end
end
@@ -842,7 +843,7 @@ describe 'API v3 Work package resource',
end
describe "when the work package has not changed at all between the baseline and today" do
let(:timestamps) { [Timestamp.parse('lastWorkingDay@12:00'), Timestamp.now] }
let(:timestamps) { [Timestamp.parse('lastWorkingDay@12:00+00:00'), Timestamp.now] }
describe "_meta" do
describe "timestamp" do
@@ -868,7 +869,7 @@ describe 'API v3 Work package resource',
it 'has the baseline timestamp, which is the first timestamp, ' \
'in the same format as given in the request parameter' do
expect(subject.body)
.to be_json_eql('lastWorkingDay@12:00'.to_json)
.to be_json_eql('lastWorkingDay@12:00+00:00'.to_json)
.at_path('_embedded/elements/0/_embedded/attributesByTimestamp/0/_meta/timestamp')
end
end
@@ -889,7 +890,7 @@ describe 'API v3 Work package resource',
# before the baseline date, because the baseline date is relative to the
# current date. This means that the filter will become outdated and we cannot
# use a cached result in this case.
let(:path) { "#{api_v3_paths.path_for(:work_packages, filters:)}&timestamps=#{timestamps.join(',')}" }
let(:path) { "#{api_v3_paths.path_for(:work_packages, filters:)}&timestamps=#{timestamps_param}" }
let(:filters) do
[
{
@@ -930,7 +931,7 @@ describe 'API v3 Work package resource',
end
context "with relative date keyword timestamps" do
let(:timestamps) { [Timestamp.parse('oneWeekAgo@12:00'), Timestamp.now] }
let(:timestamps) { [Timestamp.parse('oneWeekAgo@12:00+00:00'), Timestamp.now] }
let(:created_at) { '2015-01-01' }
describe "when the filter becomes outdated" do
@@ -940,7 +941,7 @@ describe 'API v3 Work package resource',
# current date. This means that the filter will become outdated and we cannot
# use a cached result in this case.
let(:path) { "#{api_v3_paths.path_for(:work_packages, filters:)}&timestamps=#{timestamps.join(',')}" }
let(:path) { "#{api_v3_paths.path_for(:work_packages, filters:)}&timestamps=#{timestamps_param}" }
let(:filters) do
[
{
@@ -954,10 +955,10 @@ describe 'API v3 Work package resource',
let(:search_term) { 'original' }
it 'has the relative timestamps within the _meta timestamps' do
expect(timestamps.first.to_s).to eq('oneWeekAgo@12:00')
expect(timestamps.first.to_s).to eq('oneWeekAgo@12:00+00:00')
expect(timestamps.first).to be_relative
expect(subject.body)
.to be_json_eql('oneWeekAgo@12:00'.to_json)
.to be_json_eql('oneWeekAgo@12:00+00:00'.to_json)
.at_path('_embedded/elements/0/_embedded/attributesByTimestamp/0/_meta/timestamp')
expect(subject.body)
.to be_json_eql('PT0S'.to_json)
@@ -208,7 +208,8 @@ describe 'API v3 Work package resource',
end
describe 'GET /api/v3/work_packages/:id?timestamps=' do
let(:get_path) { "#{api_v3_paths.work_package(work_package.id)}?timestamps=#{timestamps.map(&:to_s).join(',')}" }
let(:timestamps_param) { CGI.escape(timestamps.map(&:to_s).join(',')) }
let(:get_path) { "#{api_v3_paths.work_package(work_package.id)}?timestamps=#{timestamps_param}" }
describe 'response body' do
subject do
@@ -420,7 +421,7 @@ describe 'API v3 Work package resource',
end
context 'when the timestamps are relative date keywords' do
let(:timestamps) { [Timestamp.new('oneWeekAgo@12:00'), Timestamp.now] }
let(:timestamps) { [Timestamp.new('oneWeekAgo@12:00+00:00'), Timestamp.now] }
it 'has an embedded link to the baseline work package' do
expect(subject)
@@ -441,7 +442,7 @@ describe 'API v3 Work package resource',
describe 'timestamp' do
it 'has the relative timestamps' do
expect(subject)
.to be_json_eql('oneWeekAgo@12:00'.to_json)
.to be_json_eql('oneWeekAgo@12:00+00:00'.to_json)
.at_path('_embedded/attributesByTimestamp/0/_meta/timestamp')
expect(subject)
.to be_json_eql('PT0S'.to_json)
@@ -455,7 +456,7 @@ describe 'API v3 Work package resource',
before { login_as current_user }
context "with relative timestamps" do
let(:timestamps) { [Timestamp.parse("oneDayAgo@00:00"), Timestamp.now] }
let(:timestamps) { [Timestamp.parse("oneDayAgo@00:00+00:00"), Timestamp.now] }
let(:created_at) { '2015-01-01' }
describe "attributesByTimestamp" do
@@ -490,7 +491,7 @@ describe 'API v3 Work package resource',
describe "_meta" do
describe "exists" do
let(:timestamps) { [Timestamp.parse("oneDayAgo@00:00")] }
let(:timestamps) { [Timestamp.parse("oneDayAgo@00:00+00:00")] }
let(:created_at) { 25.hours.ago }
it "is not cached" do
@@ -342,13 +342,13 @@ describe API::V3::ParseQueryParamsService,
end
it_behaves_like 'transforms' do
let(:params) { { timestamps: "oneMonthAgo@11:00, now" } }
let(:expected) { { timestamps: [Timestamp.parse("oneMonthAgo@11:00"), Timestamp.parse("P-0Y")] } }
let(:params) { { timestamps: "oneMonthAgo@11:00+00:00, now" } }
let(:expected) { { timestamps: [Timestamp.parse("oneMonthAgo@11:00+00:00"), Timestamp.parse("P-0Y")] } }
end
it_behaves_like 'transforms' do
let(:params) { { timestamps: "oneMonthAgo@11:00, oneWeekAgo@12:00" } }
let(:expected) { { timestamps: [Timestamp.parse("oneMonthAgo@11:00"), Timestamp.parse("oneWeekAgo@12:00")] } }
let(:params) { { timestamps: "oneMonthAgo@11:00+00:00, oneWeekAgo@12:00+10:00" } }
let(:expected) { { timestamps: [Timestamp.parse("oneMonthAgo@11:00+00:00"), Timestamp.parse("oneWeekAgo@12:00+10:00")] } }
end
describe "for invalid parameters" do
@@ -236,7 +236,7 @@ describe API::V3::WorkPackageCollectionFromQueryService,
end
context 'when timestamps are given', with_flag: { show_changes: true } do
let(:timestamps) { [Timestamp.parse("P-1Y"), Timestamp.parse("oneWeekAgo@12:00"), Timestamp.now] }
let(:timestamps) { [Timestamp.parse("P-1Y"), Timestamp.parse("oneWeekAgo@12:00+00:00"), Timestamp.now] }
let(:query) { build_stubbed(:query, timestamps:) }
it 'has the query timestamps' do
@@ -195,7 +195,7 @@ describe UpdateQueryFromParamsService,
let(:timestamps) do
[
Timestamp.parse("2022-10-29T23:01:23Z"),
Timestamp.parse("oneWeekAgo@12:00"),
Timestamp.parse("oneWeekAgo@12:00+00:00"),
Timestamp.parse("PT0S")
]
end