]> source.dussan.org Git - redmine.git/commitdiff
Adds date based filters (#4729) and date range filter (#6954).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 10 Jul 2011 17:29:29 +0000 (17:29 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 10 Jul 2011 17:29:29 +0000 (17:29 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@6226 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/models/query.rb
app/views/queries/_filters.rhtml
test/unit/query_test.rb

index fa8a449c5736b3b91b2556e6ce506c7e8b200516..7746af88eb4e0383d6c25fdf9defaed193528faa 100644 (file)
@@ -119,8 +119,8 @@ class Query < ActiveRecord::Base
                                  :list_status => [ "o", "=", "!", "c", "*" ],
                                  :list_optional => [ "=", "!", "!*", "*" ],
                                  :list_subprojects => [ "*", "!*", "=" ],
-                                 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
-                                 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
+                                 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
+                                 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w" ],
                                  :string => [ "=", "~", "!", "!~" ],
                                  :text => [  "~", "!~" ],
                                  :integer => [ "=", ">=", "<=", "><", "!*", "*" ] }
@@ -268,7 +268,7 @@ class Query < ActiveRecord::Base
 
   def add_filter(field, operator, values)
     # values must be an array
-    return unless values and values.is_a? Array # and !values.first.empty?
+    return unless values.nil? || values.is_a?(Array)
     # check if field is defined as an available filter
     if available_filters.has_key? field
       filter_options = available_filters[field]
@@ -277,7 +277,7 @@ class Query < ActiveRecord::Base
       #  allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
       #  filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
       #end
-      filters[field] = {:operator => operator, :values => values }
+      filters[field] = {:operator => operator, :values => (values || ['']) }
     end
   end
 
@@ -289,9 +289,9 @@ class Query < ActiveRecord::Base
 
   # Add multiple filters using +add_filter+
   def add_filters(fields, operators, values)
-    if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
+    if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
       fields.each do |field|
-        add_filter(field, operators[field], values[field])
+        add_filter(field, operators[field], values && values[field])
       end
     end
   end
@@ -299,6 +299,10 @@ class Query < ActiveRecord::Base
   def has_filter?(field)
     filters and filters[field]
   end
+  
+  def type_for(field)
+    available_filters[field][:type] if available_filters.has_key?(field)
+  end
 
   def operator_for(field)
     has_filter?(field) ? filters[field][:operator] : nil
@@ -601,11 +605,15 @@ class Query < ActiveRecord::Base
     sql = ''
     case operator
     when "="
-      if value.any?
-        sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
+      if [:date, :date_past].include?(type_for(field))
+        sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
       else
-        # IN an empty set
-        sql = "1=0"
+        if value.any?
+          sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
+        else
+          # IN an empty set
+          sql = "1=0"
+        end
       end
     when "!"
       if value.any?
@@ -621,46 +629,58 @@ class Query < ActiveRecord::Base
       sql = "#{db_table}.#{db_field} IS NOT NULL"
       sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
     when ">="
-      if is_custom_filter
-        sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_i}"
+      if [:date, :date_past].include?(type_for(field))
+        sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
       else
-        sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
+        if is_custom_filter
+          sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_i}"
+        else
+          sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
+        end
       end
     when "<="
-      if is_custom_filter
-        sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_i}"
+      if [:date, :date_past].include?(type_for(field))
+        sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
       else
-        sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
+        if is_custom_filter
+          sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_i}"
+        else
+          sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
+        end
       end
     when "><"
-      if is_custom_filter
-        sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
+      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))
       else
-        sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
+        if is_custom_filter
+          sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
+        else
+          sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
+        end
       end
     when "o"
       sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
     when "c"
       sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
     when ">t-"
-      sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
+      sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
     when "<t-"
-      sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
+      sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
     when "t-"
-      sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
+      sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
     when ">t+"
-      sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
+      sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
     when "<t+"
-      sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
+      sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
     when "t+"
-      sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
+      sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
     when "t"
-      sql = date_range_clause(db_table, db_field, 0, 0)
+      sql = relative_date_clause(db_table, db_field, 0, 0)
     when "w"
       first_day_of_week = l(:general_first_day_of_week).to_i
       day_of_week = Date.today.cwday
       days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
-      sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
+      sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
     when "~"
       sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
     when "!~"
@@ -696,16 +716,21 @@ class Query < ActiveRecord::Base
       @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
     end
   end
-
+  
   # Returns a SQL clause for a date or datetime field.
-  def date_range_clause(table, field, from, to)
+  def date_clause(table, field, from, to)
     s = []
     if from
-      s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
+      s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((from - 1).to_time.end_of_day)])
     end
     if to
-      s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
+      s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to.to_time.end_of_day)])
     end
     s.join(' AND ')
   end
+  
+  # Returns a SQL clause for a date or datetime field using relative dates.
+  def relative_date_clause(table, field, days_from, days_to)
+    date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
+  end
 end
index e9c7502e1e4fcc06eb452ef443c143d12a990088..8942c8b607b9bcc9403f90792a4fc5fa4667acd2 100644 (file)
@@ -1,37 +1,53 @@
 <script type="text/javascript">
 //<![CDATA[
 function add_filter() {
-    select = $('add_filter_select');
-    field = select.value
-    Element.show('tr_' +  field);
-    check_box = $('cb_' + field);
-    check_box.checked = true;
-    toggle_filter(field);
-    select.selectedIndex = 0;
-    
-    for (i=0; i<select.options.length; i++) {
-        if (select.options[i].value == field) {
-            select.options[i].disabled = true;
-        }    
-    }
+  select = $('add_filter_select');
+  field = select.value
+  Element.show('tr_' +  field);
+  check_box = $('cb_' + field);
+  check_box.checked = true;
+  toggle_filter(field);
+  select.selectedIndex = 0;
+
+  for (i=0; i<select.options.length; i++) {
+    if (select.options[i].value == field) {
+      select.options[i].disabled = true;
+    }    
+  }
 }
 
 function toggle_filter(field) {
-    check_box = $('cb_' + field);
-    
-    if (check_box.checked) {
-        Element.show("operators_" + field);
-                               Form.Element.enable("operators_" + field);
-                               $$(".values_" + field).each(function(el){ Form.Element.enable(el)});
-        toggle_operator(field);
-    } else {
-        Element.hide("operators_" + field);
-        Element.hide("div_values_" + field);
-        Form.Element.disable("operators_" + field);
-        $$(".values_" + field).each(function(el){ Form.Element.disable(el)});
+  check_box = $('cb_' + field);
+  if (check_box.checked) {
+    Element.show("operators_" + field);
+    Form.Element.enable("operators_" + field);
+    toggle_operator(field);
+  } else {
+    Element.hide("operators_" + field);
+    Form.Element.disable("operators_" + field);
+    enableValues(field, []);
   }
 }
 
+function enableValues(field, indexes) {
+       var f = $$(".values_" + field);
+       for(var i=0;i<f.length;i++) {
+               if (indexes.include(i)) {
+                       Form.Element.enable(f[i]);
+                       f[i].up('span').show();
+               } else {
+                       f[i].value = '';
+      Form.Element.disable(f[i]);
+      f[i].up('span').hide();
+               }
+       }
+       if (indexes.length > 0) {
+               Element.show("div_values_" + field);
+       } else {
+               Element.hide("div_values_" + field);
+       }
+}
+
 function toggle_operator(field) {
   operator = $("operators_" + field);
   switch (operator.value) {
@@ -41,30 +57,32 @@ function toggle_operator(field) {
     case "w":
     case "o":
     case "c":
-      Element.hide("div_values_" + field);
-      var v = $$(".values_" + field);
-      if (v.length > 1) {v[1].hide(); Form.Element.disable(v[1])}
+      enableValues(field, []);
       break;
     case "><":
-      Element.show("div_values_" + field);
-      var v = $$(".values_" + field);
-      if (v.length > 1) {v[1].show(); Form.Element.enable(v[1])}
+      enableValues(field, [0,1]);
+      break;
+    case "<t+":
+    case ">t+":
+    case "t+":
+    case ">t-":
+    case "<t-":
+    case "t-":
+      enableValues(field, [2]);
       break;
     default:
-      Element.show("div_values_" + field);
-                       var v = $$(".values_" + field);
-                       if (v.length > 1) {v[1].hide(); Form.Element.disable(v[1])}
+      enableValues(field, [0]);
       break;
   }
 }
 
-function toggle_multi_select(field) {
-    select = $('values_' + field);
-    if (select.multiple == true) {
-        select.multiple = false;
-    } else {
-        select.multiple = true;
-    }
+function toggle_multi_select(el) {
+  var select = $(el);
+  if (select.multiple == true) {
+    select.multiple = false;
+  } else {
+    select.multiple = true;
+  }
 }
 
 function submit_query_form(id) {
@@ -102,15 +120,19 @@ Event.observe(document,"dom:loaded", apply_filters_observer);
     <div id="div_values_<%= field %>" style="display:none;">
     <% case options[:type]
     when :list, :list_optional, :list_status, :list_subprojects %>
-        <%= select_tag "v[#{field}][]", options_for_select(options[:values], query.values_for(field)), :class => "values_#{field}", :multiple => (query.values_for(field) && query.values_for(field).length > 1) %>
-        <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('#{field}');", :style => "vertical-align: bottom;" %>
+        <span class="span_values_<%= field %>">
+               <%= select_tag "v[#{field}][]", options_for_select(options[:values], query.values_for(field)), :class => "values_#{field}", :id => "values_#{field}_1", :multiple => (query.values_for(field) && query.values_for(field).length > 1) %>
+          <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('values_#{field}_1');", :style => "vertical-align: bottom;" %>
+        </span>
     <% when :date, :date_past %>
-        <%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :size => 3 %> <%= l(:label_day_plural) %>
+        <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 10, :class => "values_#{field}", :id => "values_#{field}_1" %> <%= calendar_for "values_#{field}_1" %></span>
+        <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :size => 10, :class => "values_#{field}", :id => "values_#{field}_2" %> <%= calendar_for "values_#{field}_2" %></span>
+        <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 3, :class => "values_#{field}" %> <%= l(:label_day_plural) %></span>
     <% when :string, :text %>
-        <%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :size => 30 %>
+        <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}", :size => 30 %></span>
     <% when :integer %>
-        <%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :size => 3 %>
-        <%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :class => "values_#{field}", :size => 3 %>
+        <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}_1", :size => 3 %></span>
+        <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :class => "values_#{field}", :id => "values_#{field}_2", :size => 3 %></span>
     <% end %>
     </div>
     <script type="text/javascript">toggle_filter('<%= field %>');</script>
index 633edebb1b182905b665c2298ae55c012d377d30..31dfa488e5000dec041c637140e4125a4a69e1bd 100644 (file)
@@ -146,6 +146,34 @@ class QueryTest < ActiveSupport::TestCase
     find_issues_with_query(query)
   end
 
+  def test_operator_date_equals
+    query = Query.new(:name => '_')
+    query.add_filter('due_date', '=', ['2011-07-10'])
+    assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
+    find_issues_with_query(query)
+  end
+
+  def test_operator_date_lesser_than
+    query = Query.new(:name => '_')
+    query.add_filter('due_date', '<=', ['2011-07-10'])
+    assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
+    find_issues_with_query(query)
+  end
+
+  def test_operator_date_greater_than
+    query = Query.new(:name => '_')
+    query.add_filter('due_date', '>=', ['2011-07-10'])
+    assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
+    find_issues_with_query(query)
+  end
+
+  def test_operator_date_between
+    query = Query.new(:name => '_')
+    query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
+    assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
+    find_issues_with_query(query)
+  end
+
   def test_operator_in_more_than
     Issue.find(7).update_attribute(:due_date, (Date.today + 15))
     query = Query.new(:project => Project.find(1), :name => '_')