@@ -242,7 +242,9 class Query < ActiveRecord::Base | |||||
242 | when :date, :date_past |
|
242 | when :date, :date_past | |
243 | case operator_for(field) |
|
243 | case operator_for(field) | |
244 | when "=", ">=", "<=", "><" |
|
244 | when "=", ">=", "<=", "><" | |
245 |
add_filter_error(field, :invalid) if values_for(field).detect {|v| |
|
245 | add_filter_error(field, :invalid) if values_for(field).detect {|v| | |
|
246 | 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?) | |||
|
247 | } | |||
246 | when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-" |
|
248 | when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-" | |
247 | add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } |
|
249 | add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } | |
248 | end |
|
250 | end | |
@@ -624,7 +626,7 class Query < ActiveRecord::Base | |||||
624 | if value.any? |
|
626 | if value.any? | |
625 | case type_for(field) |
|
627 | case type_for(field) | |
626 | when :date, :date_past |
|
628 | when :date, :date_past | |
627 |
sql = date_clause(db_table, db_field, |
|
629 | sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first)) | |
628 | when :integer |
|
630 | when :integer | |
629 | if is_custom_filter |
|
631 | if is_custom_filter | |
630 | 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_i})" |
|
632 | 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_i})" | |
@@ -659,7 +661,7 class Query < ActiveRecord::Base | |||||
659 | sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter |
|
661 | sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter | |
660 | when ">=" |
|
662 | when ">=" | |
661 | if [:date, :date_past].include?(type_for(field)) |
|
663 | if [:date, :date_past].include?(type_for(field)) | |
662 |
sql = date_clause(db_table, db_field, |
|
664 | sql = date_clause(db_table, db_field, parse_date(value.first), nil) | |
663 | else |
|
665 | else | |
664 | if is_custom_filter |
|
666 | if is_custom_filter | |
665 | 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})" |
|
667 | 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 | |||||
669 | end |
|
671 | end | |
670 | when "<=" |
|
672 | when "<=" | |
671 | if [:date, :date_past].include?(type_for(field)) |
|
673 | if [:date, :date_past].include?(type_for(field)) | |
672 |
sql = date_clause(db_table, db_field, nil, |
|
674 | sql = date_clause(db_table, db_field, nil, parse_date(value.first)) | |
673 | else |
|
675 | else | |
674 | if is_custom_filter |
|
676 | if is_custom_filter | |
675 | 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})" |
|
677 | 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 | |||||
679 | end |
|
681 | end | |
680 | when "><" |
|
682 | when "><" | |
681 | if [:date, :date_past].include?(type_for(field)) |
|
683 | if [:date, :date_past].include?(type_for(field)) | |
682 |
sql = date_clause(db_table, db_field, |
|
684 | sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1])) | |
683 | else |
|
685 | else | |
684 | if is_custom_filter |
|
686 | if is_custom_filter | |
685 | 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})" |
|
687 | 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 | |||||
809 | def date_clause(table, field, from, to) |
|
811 | def date_clause(table, field, from, to) | |
810 | s = [] |
|
812 | s = [] | |
811 | if from |
|
813 | if from | |
812 | from_yesterday = from - 1 |
|
814 | if from.is_a?(Date) | |
813 |
from |
|
815 | from = Time.local(from.year, from.month, from.day).beginning_of_day | |
|
816 | end | |||
814 | if self.class.default_timezone == :utc |
|
817 | if self.class.default_timezone == :utc | |
815 |
from |
|
818 | from = from.utc | |
816 | end |
|
819 | end | |
817 | s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)]) |
|
820 | from = from - 1 | |
|
821 | s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from)]) | |||
818 | end |
|
822 | end | |
819 | if to |
|
823 | if to | |
820 | to_time = Time.local(to.year, to.month, to.day) |
|
824 | if to.is_a?(Date) | |
|
825 | to = Time.local(to.year, to.month, to.day).end_of_day | |||
|
826 | end | |||
821 | if self.class.default_timezone == :utc |
|
827 | if self.class.default_timezone == :utc | |
822 |
to |
|
828 | to = to.utc | |
823 | end |
|
829 | end | |
824 |
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to |
|
830 | s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to)]) | |
825 | end |
|
831 | end | |
826 | s.join(' AND ') |
|
832 | s.join(' AND ') | |
827 | end |
|
833 | end | |
@@ -831,6 +837,15 class Query < ActiveRecord::Base | |||||
831 | date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) |
|
837 | date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) | |
832 | end |
|
838 | end | |
833 |
|
839 | |||
|
840 | # Returns a Date or Time from the given filter value | |||
|
841 | def parse_date(arg) | |||
|
842 | if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/ | |||
|
843 | Time.parse(arg) rescue nil | |||
|
844 | else | |||
|
845 | Date.parse(arg) rescue nil | |||
|
846 | end | |||
|
847 | end | |||
|
848 | ||||
834 | # Additional joins required for the given sort options |
|
849 | # Additional joins required for the given sort options | |
835 | def joins_for_order_statement(order_options) |
|
850 | def joins_for_order_statement(order_options) | |
836 | joins = [] |
|
851 | joins = [] |
@@ -162,6 +162,29 class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base | |||||
162 | end |
|
162 | end | |
163 | end |
|
163 | end | |
164 |
|
164 | |||
|
165 | def test_index_should_allow_timestamp_filtering | |||
|
166 | Issue.delete_all | |||
|
167 | Issue.generate!(:subject => '1').update_column(:updated_on, Time.parse("2014-01-02T10:25:00Z")) | |||
|
168 | Issue.generate!(:subject => '2').update_column(:updated_on, Time.parse("2014-01-02T12:13:00Z")) | |||
|
169 | ||||
|
170 | get '/issues.xml', | |||
|
171 | {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '<='}, | |||
|
172 | :v => {:updated_on => ['2014-01-02T12:00:00Z']}} | |||
|
173 | assert_select 'issues>issue', :count => 1 | |||
|
174 | assert_select 'issues>issue>subject', :text => '1' | |||
|
175 | ||||
|
176 | get '/issues.xml', | |||
|
177 | {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '>='}, | |||
|
178 | :v => {:updated_on => ['2014-01-02T12:00:00Z']}} | |||
|
179 | assert_select 'issues>issue', :count => 1 | |||
|
180 | assert_select 'issues>issue>subject', :text => '2' | |||
|
181 | ||||
|
182 | get '/issues.xml', | |||
|
183 | {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '>='}, | |||
|
184 | :v => {:updated_on => ['2014-01-02T08:00:00Z']}} | |||
|
185 | assert_select 'issues>issue', :count => 2 | |||
|
186 | end | |||
|
187 | ||||
165 | context "/index.json" do |
|
188 | context "/index.json" do | |
166 | should_allow_api_authentication(:get, "/projects/private-child/issues.json") |
|
189 | should_allow_api_authentication(:get, "/projects/private-child/issues.json") | |
167 | end |
|
190 | end |
General Comments 0
You need to be logged in to leave comments.
Login now