https://community.openproject.com/work_packages/24174 See also previous PRs under #5071, #5128pull/5320/head
parent
9aa4d54ea8
commit
8077afd57c
@ -0,0 +1,144 @@ |
||||
#-- encoding: UTF-8 |
||||
#-- copyright |
||||
# OpenProject is a project management system. |
||||
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See doc/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Queries::SqlForCalendarialField |
||||
private |
||||
|
||||
def sql_for_date_field(field, operator, values, db_table, db_field) |
||||
if operator == '=d' |
||||
date_range_clause(db_table, db_field, Date.parse(values.first), |
||||
Date.parse(values.first)) |
||||
elsif operator == '<>d' |
||||
if values.first != 'undefined' |
||||
from = Date.parse(values.first) |
||||
end |
||||
if values.size == 2 |
||||
to = Date.parse(values.last) |
||||
end |
||||
date_range_clause(db_table, db_field, from, to) |
||||
else |
||||
sql_for_calendarial_field(field, operator, values, db_table, db_field) |
||||
end |
||||
end |
||||
|
||||
def sql_for_datetime_field(field, operator, values, db_table, db_field) |
||||
if operator == '=d' |
||||
datetime = DateTime.parse(values.first) |
||||
datetime_range_clause(db_table, db_field, datetime.beginning_of_day, |
||||
datetime.end_of_day) |
||||
elsif operator == '<>d' |
||||
if values.first != 'undefined' |
||||
from = DateTime.parse(values.first).beginning_of_day |
||||
end |
||||
if values.size == 2 |
||||
to = DateTime.parse(values.last).end_of_day |
||||
end |
||||
datetime_range_clause(db_table, db_field, from, to) |
||||
else |
||||
sql_for_calendarial_field(field, operator, values, db_table, db_field) |
||||
end |
||||
end |
||||
|
||||
def sql_for_calendarial_field(field, operator, values, db_table, db_field) |
||||
case operator |
||||
when '>t-' |
||||
relative_date_range_clause(db_table, db_field, - values.first.to_i, 0) |
||||
when '<t-' |
||||
relative_date_range_clause(db_table, db_field, nil, - values.first.to_i) |
||||
when 't-' |
||||
relative_date_range_clause(db_table, db_field, - values.first.to_i, |
||||
- values.first.to_i) |
||||
when '>t+' |
||||
relative_date_range_clause(db_table, db_field, values.first.to_i, nil) |
||||
when '<t+' |
||||
relative_date_range_clause(db_table, db_field, 0, values.first.to_i) |
||||
when 't+' |
||||
relative_date_range_clause(db_table, db_field, values.first.to_i, values.first.to_i) |
||||
when 't' |
||||
relative_date_range_clause(db_table, db_field, 0, 0) |
||||
when 'w' |
||||
from = begin_of_week |
||||
"#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [ |
||||
connection.quoted_date(from), connection.quoted_date(from + 7.days) |
||||
] |
||||
end |
||||
end |
||||
|
||||
def begin_of_week |
||||
if l(:general_first_day_of_week) == '7' |
||||
# week starts on sunday |
||||
if Date.today.cwday == 7 |
||||
Time.now.at_beginning_of_day |
||||
else |
||||
Time.now.at_beginning_of_week - 1.day |
||||
end |
||||
else |
||||
# week starts on monday (Rails default) |
||||
Time.now.at_beginning_of_week |
||||
end |
||||
end |
||||
|
||||
# Returns a SQL clause for a date or datetime field for a relative range from |
||||
# the end of the day of yesterday + from until the end of today + to. |
||||
def relative_date_range_clause(table, field, from, to) |
||||
if from |
||||
from_date = Date.today + from |
||||
end |
||||
if to |
||||
to_date = Date.today + to |
||||
end |
||||
date_range_clause(table, field, from_date, to_date) |
||||
end |
||||
|
||||
# Returns a SQL clause for date or datetime field for an exact range starting |
||||
# at the beginning of the day of from until the end of the day of to |
||||
def date_range_clause(table, field, from, to) |
||||
s = [] |
||||
if from |
||||
s << "#{table}.#{field} > '%s'" % [ |
||||
connection.quoted_date(from.yesterday.to_time(:utc).end_of_day) |
||||
] |
||||
end |
||||
if to |
||||
s << "#{table}.#{field} <= '%s'" % [connection.quoted_date(to.to_time(:utc).end_of_day)] |
||||
end |
||||
s.join(' AND ') |
||||
end |
||||
|
||||
def datetime_range_clause(table, field, from, to) |
||||
s = [] |
||||
if from |
||||
s << ("#{table}.#{field} >= '%s'" % [connection.quoted_date(from)]) |
||||
end |
||||
if to |
||||
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to)]) |
||||
end |
||||
s.join(' AND ') |
||||
end |
||||
end |
@ -0,0 +1,62 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
function transformDateUtc(TimezoneService) { |
||||
return { |
||||
restrict: 'A', |
||||
scope: { |
||||
transformDateUtc: '@' |
||||
}, |
||||
require: '^ngModel', |
||||
link: function (scope, element, attrs, ngModelController) { |
||||
ngModelController.$parsers.push(function (data) { |
||||
if (!moment(data, 'YYYY-MM-DD', true).isValid()) { |
||||
return undefined; |
||||
} |
||||
var d = TimezoneService.parseLocalDate(data); |
||||
if (scope.transformDateUtc == 'end-of-day') { |
||||
d.endOf('day'); |
||||
} else { // begin-of-day
|
||||
d.startOf('day'); |
||||
} |
||||
return TimezoneService.formattedISODatetime(d); |
||||
}); |
||||
ngModelController.$formatters.push(function (data) { |
||||
if (!moment(data, 'YYYY-MM-DDTHH:mm:ssZ', true).isValid()) { |
||||
return undefined; |
||||
} |
||||
var d = TimezoneService.parseISODatetime(data); |
||||
return TimezoneService.formattedISODate(d); |
||||
}); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
angular |
||||
.module('openproject') |
||||
.directive('transformDateUtc', transformDateUtc); |
@ -0,0 +1,192 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
describe('transformDateUtc Directive', function() { |
||||
var compile, element, rootScope, scope, ConfigurationService, isTimezoneSetStub, timezoneStub; |
||||
|
||||
var prepare = function (timeOfDay) { |
||||
|
||||
angular.mock.module('openproject'); |
||||
|
||||
inject(function($rootScope, $compile, _ConfigurationService_) { |
||||
var html = |
||||
'<input type="text" ng-model="dateValue" transform-date-utc="' + timeOfDay + '"/>'; |
||||
|
||||
ConfigurationService = _ConfigurationService_; |
||||
isTimezoneSetStub = sinon.stub(ConfigurationService, 'isTimezoneSet'); |
||||
isTimezoneSetStub.returns(true); |
||||
timezoneStub = sinon.stub(ConfigurationService, 'timezone'); |
||||
|
||||
element = angular.element(html); |
||||
rootScope = $rootScope; |
||||
scope = $rootScope.$new(); |
||||
scope.dateValue = null; |
||||
|
||||
compile = function () { |
||||
$compile(element)(scope); |
||||
scope.$digest(); |
||||
}; |
||||
}); |
||||
}; |
||||
|
||||
var shouldBehaveLikeAParser = function (value, expected) { |
||||
it('these should parse as expected', function () { |
||||
compile(); |
||||
element.val(value); |
||||
element.trigger('input'); |
||||
expect(scope.dateValue).to.eql(expected); |
||||
}); |
||||
}; |
||||
|
||||
var shouldBehaveLikeAFormatter = function (value, expected) { |
||||
it('should format the value as expected', function () { |
||||
scope.dateValue = value; |
||||
compile(); |
||||
expect(element.val()).to.eql(expected); |
||||
}); |
||||
} |
||||
|
||||
var shouldBehaveCorrectlyWhenGivenInvalidDateValues = function () { |
||||
describe('when given invalid date values', function () { |
||||
shouldBehaveLikeAParser('', undefined); |
||||
shouldBehaveLikeAParser('invalid', undefined); |
||||
shouldBehaveLikeAParser('2016-12', undefined); |
||||
shouldBehaveLikeAFormatter(undefined, ''); |
||||
shouldBehaveLikeAFormatter('invalid', ''); |
||||
shouldBehaveLikeAFormatter('2016-12', ''); |
||||
}); |
||||
}; |
||||
|
||||
context('should behave like begin-of-day with no time-of-day value given', function () { |
||||
beforeEach(function () { |
||||
prepare(); |
||||
timezoneStub.returns('UTC'); |
||||
}); |
||||
|
||||
describe('when given valid date values', function() { |
||||
var value = '2016-12-01'; |
||||
var expectedValue = value + 'T00:00:00+00:00'; |
||||
shouldBehaveLikeAParser(value, expectedValue); |
||||
shouldBehaveLikeAFormatter(expectedValue, value); |
||||
}); |
||||
|
||||
shouldBehaveCorrectlyWhenGivenInvalidDateValues(); |
||||
}); |
||||
|
||||
context('with begin-of-day', function () { |
||||
beforeEach(function () { |
||||
prepare('begin-of-day'); |
||||
timezoneStub.returns('UTC'); |
||||
}); |
||||
|
||||
describe('when given valid date values', function() { |
||||
var value = '2016-12-01'; |
||||
var expectedValue = value + 'T00:00:00+00:00'; |
||||
shouldBehaveLikeAParser(value, expectedValue); |
||||
shouldBehaveLikeAFormatter(expectedValue, value); |
||||
}); |
||||
|
||||
shouldBehaveCorrectlyWhenGivenInvalidDateValues(); |
||||
}); |
||||
|
||||
context('with end-of-day', function () { |
||||
beforeEach(function () { |
||||
prepare('end-of-day'); |
||||
timezoneStub.returns('UTC'); |
||||
}); |
||||
|
||||
describe('when given valid date values', function() { |
||||
var value = '2016-12-01'; |
||||
var expectedValue = value + 'T23:59:59+00:00'; |
||||
shouldBehaveLikeAParser(value, expectedValue); |
||||
shouldBehaveLikeAFormatter(expectedValue, value); |
||||
}); |
||||
|
||||
shouldBehaveCorrectlyWhenGivenInvalidDateValues(); |
||||
}); |
||||
|
||||
context('when receiving datetime values from a different timezone', function () { |
||||
context('with begin-of-day', function () { |
||||
beforeEach(function () { |
||||
prepare('begin-of-day'); |
||||
// moment-timezone: GMT+1 is actually GMT-1
|
||||
timezoneStub.returns('Etc/GMT+1'); |
||||
}); |
||||
|
||||
describe('it should be transformed to the local timezone', function () { |
||||
var value = '2016-12-03T00:00:00+00:00'; |
||||
var expectedValue = '2016-12-02'; |
||||
shouldBehaveLikeAFormatter(value, expectedValue); |
||||
}); |
||||
}); |
||||
|
||||
context('with end-of-day', function () { |
||||
beforeEach(function () { |
||||
prepare('end-of-day'); |
||||
// moment-timezone: GMT-1 is actually GMT+1
|
||||
timezoneStub.returns('Etc/GMT-1'); |
||||
}); |
||||
|
||||
describe('it should be transformed to the local timezone', function () { |
||||
var value = '2016-12-01T23:59:59+00:00'; |
||||
var expectedValue = '2016-12-02'; |
||||
shouldBehaveLikeAFormatter(value, expectedValue); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
context('when operating in a different timezone than UTC', function () { |
||||
context('with begin-of-day', function () { |
||||
beforeEach(function () { |
||||
prepare('begin-of-day'); |
||||
// moment-timezone: GMT-1 is actually GMT+1
|
||||
timezoneStub.returns('Etc/GMT-1'); |
||||
}); |
||||
|
||||
describe('it should have the expected timezone offset', function () { |
||||
var value = '2016-12-01'; |
||||
var expectedValue = value + 'T00:00:00+01:00'; |
||||
shouldBehaveLikeAParser(value, expectedValue); |
||||
}); |
||||
}); |
||||
|
||||
context('with end-of-day', function () { |
||||
beforeEach(function () { |
||||
prepare('end-of-day'); |
||||
// moment-timezone: GMT+1 is actually GMT-1
|
||||
timezoneStub.returns('Etc/GMT+1'); |
||||
}); |
||||
|
||||
describe('it should have the expected timezone offset', function () { |
||||
var value = '2016-12-01'; |
||||
var expectedValue = '2016-11-30T23:59:59-01:00'; |
||||
shouldBehaveLikeAParser(value, expectedValue); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue