From a4d3da988a3e9f7a7d246ef7e80d11c92270f13b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 3 Jan 2014 15:54:49 +0000 Subject: [PATCH] Allow filtering with timestamp (#8842). git-svn-id: http://svn.redmine.org/redmine/trunk@12477 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 43 ++++++++++++++++-------- test/integration/api_test/issues_test.rb | 23 +++++++++++++ 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/app/models/query.rb b/app/models/query.rb index 9adc43f13..3c3bae76d 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -242,7 +242,9 @@ class Query < ActiveRecord::Base when :date, :date_past case operator_for(field) when "=", ">=", "<=", "><" - add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) } + add_filter_error(field, :invalid) if values_for(field).detect {|v| + v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?) + } when ">t-", "t+", " ''" if is_custom_filter when ">=" if [:date, :date_past].include?(type_for(field)) - sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil) + sql = date_clause(db_table, db_field, parse_date(value.first), nil) else if is_custom_filter sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})" @@ -669,7 +671,7 @@ class Query < ActiveRecord::Base end when "<=" if [:date, :date_past].include?(type_for(field)) - sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil)) + sql = date_clause(db_table, db_field, nil, parse_date(value.first)) else if is_custom_filter sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})" @@ -679,7 +681,7 @@ class Query < ActiveRecord::Base end when "><" if [:date, :date_past].include?(type_for(field)) - sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil)) + sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1])) else if is_custom_filter sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})" @@ -809,19 +811,23 @@ class Query < ActiveRecord::Base def date_clause(table, field, from, to) s = [] if from - from_yesterday = from - 1 - from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day) - if self.class.default_timezone == :utc - from_yesterday_time = from_yesterday_time.utc + if from.is_a?(Date) + from = Time.local(from.year, from.month, from.day).beginning_of_day end - s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)]) + if self.class.default_timezone == :utc + from = from.utc + end + from = from - 1 + s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from)]) end if to - to_time = Time.local(to.year, to.month, to.day) - if self.class.default_timezone == :utc - to_time = to_time.utc + if to.is_a?(Date) + to = Time.local(to.year, to.month, to.day).end_of_day end - s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)]) + if self.class.default_timezone == :utc + to = to.utc + end + s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to)]) end s.join(' AND ') end @@ -831,6 +837,15 @@ class Query < ActiveRecord::Base date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) end + # Returns a Date or Time from the given filter value + def parse_date(arg) + if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/ + Time.parse(arg) rescue nil + else + Date.parse(arg) rescue nil + end + end + # Additional joins required for the given sort options def joins_for_order_statement(order_options) joins = [] diff --git a/test/integration/api_test/issues_test.rb b/test/integration/api_test/issues_test.rb index d8f1dad48..a8839a391 100644 --- a/test/integration/api_test/issues_test.rb +++ b/test/integration/api_test/issues_test.rb @@ -162,6 +162,29 @@ class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base end end + def test_index_should_allow_timestamp_filtering + Issue.delete_all + Issue.generate!(:subject => '1').update_column(:updated_on, Time.parse("2014-01-02T10:25:00Z")) + Issue.generate!(:subject => '2').update_column(:updated_on, Time.parse("2014-01-02T12:13:00Z")) + + get '/issues.xml', + {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '<='}, + :v => {:updated_on => ['2014-01-02T12:00:00Z']}} + assert_select 'issues>issue', :count => 1 + assert_select 'issues>issue>subject', :text => '1' + + get '/issues.xml', + {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '>='}, + :v => {:updated_on => ['2014-01-02T12:00:00Z']}} + assert_select 'issues>issue', :count => 1 + assert_select 'issues>issue>subject', :text => '2' + + get '/issues.xml', + {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '>='}, + :v => {:updated_on => ['2014-01-02T08:00:00Z']}} + assert_select 'issues>issue', :count => 2 + end + context "/index.json" do should_allow_api_authentication(:get, "/projects/private-child/issues.json") end