Commit d570bc5c authored by Jean-Philippe Lang's avatar Jean-Philippe Lang

Custom fields for issues can now be used as filters on issue list.

To use a custom field as a filter, check "Used as a filter" on the custom field edit screen.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@447 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent 559b2069
...@@ -259,10 +259,10 @@ class ProjectsController < ApplicationController ...@@ -259,10 +259,10 @@ class ProjectsController < ApplicationController
end end
if @query.valid? if @query.valid?
@issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement) @issue_count = Issue.count(:include => [:status, :project, :custom_values], :conditions => @query.statement)
@issue_pages = Paginator.new self, @issue_count, @results_per_page, params['page'] @issue_pages = Paginator.new self, @issue_count, @results_per_page, params['page']
@issues = Issue.find :all, :order => sort_clause, @issues = Issue.find :all, :order => sort_clause,
:include => [ :assigned_to, :status, :tracker, :project, :priority ], :include => [ :assigned_to, :status, :tracker, :project, :priority, :custom_values ],
:conditions => @query.statement, :conditions => @query.statement,
:limit => @issue_pages.items_per_page, :limit => @issue_pages.items_per_page,
:offset => @issue_pages.current.offset :offset => @issue_pages.current.offset
......
# redMine - project management software # redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
...@@ -48,6 +48,7 @@ class Query < ActiveRecord::Base ...@@ -48,6 +48,7 @@ class Query < ActiveRecord::Base
:list_one_or_more => [ "*", "=" ], :list_one_or_more => [ "*", "=" ],
:date => [ "<t+", ">t+", "t+", "t", ">t-", "<t-", "t-" ], :date => [ "<t+", ">t+", "t+", "t", ">t-", "<t-", "t-" ],
:date_past => [ ">t-", "<t-", "t-", "t" ], :date_past => [ ">t-", "<t-", "t-", "t" ],
:string => [ "=", "~", "!", "!~" ],
:text => [ "~", "!~" ] } :text => [ "~", "!~" ] }
cattr_reader :operators_by_filter_type cattr_reader :operators_by_filter_type
...@@ -60,7 +61,7 @@ class Query < ActiveRecord::Base ...@@ -60,7 +61,7 @@ class Query < ActiveRecord::Base
def validate def validate
filters.each_key do |field| filters.each_key do |field|
errors.add field.gsub(/\_id$/, ""), :activerecord_error_blank unless errors.add label_for(field), :activerecord_error_blank unless
# filter requires one or more values # filter requires one or more values
(values_for(field) and !values_for(field).first.empty?) or (values_for(field) and !values_for(field).first.empty?) or
# filter doesn't require any value # filter doesn't require any value
...@@ -87,6 +88,21 @@ class Query < ActiveRecord::Base ...@@ -87,6 +88,21 @@ class Query < ActiveRecord::Base
unless @project.children.empty? unless @project.children.empty?
@available_filters["subproject_id"] = { :type => :list_one_or_more, :order => 13, :values => @project.children.collect{|s| [s.name, s.id.to_s] } } @available_filters["subproject_id"] = { :type => :list_one_or_more, :order => 13, :values => @project.children.collect{|s| [s.name, s.id.to_s] } }
end end
@project.all_custom_fields.select(&:is_filter?).each do |field|
case field.field_format
when "string", "int"
options = { :type => :string, :order => 20 }
when "text"
options = { :type => :text, :order => 20 }
when "list"
options = { :type => :list_optional, :values => field.possible_values, :order => 20}
when "date"
options = { :type => :date, :order => 20 }
when "bool"
options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
end
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
end
# remove category filter if no category defined # remove category filter if no category defined
@available_filters.delete "category_id" if @available_filters["category_id"][:values].empty? @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty?
end end
...@@ -126,6 +142,11 @@ class Query < ActiveRecord::Base ...@@ -126,6 +142,11 @@ class Query < ActiveRecord::Base
has_filter?(field) ? filters[field][:values] : nil has_filter?(field) ? filters[field][:values] : nil
end end
def label_for(field)
label = @available_filters[field][:name] if @available_filters.has_key?(field)
label ||= field.gsub(/\_id$/, "")
end
def statement def statement
sql = "1=1" sql = "1=1"
if has_filter?("subproject_id") if has_filter?("subproject_id")
...@@ -142,40 +163,56 @@ class Query < ActiveRecord::Base ...@@ -142,40 +163,56 @@ class Query < ActiveRecord::Base
filters.each_key do |field| filters.each_key do |field|
next if field == "subproject_id" next if field == "subproject_id"
v = values_for field v = values_for field
next unless v and !v.empty? next unless v and !v.empty?
sql = sql + " AND " unless sql.empty? sql = sql + " AND " unless sql.empty?
sql << "("
if field =~ /^cf_(\d+)$/
# custom field
db_table = CustomValue.table_name
db_field = "value"
sql << "#{db_table}.custom_field_id = #{$1} AND "
else
# regular field
db_table = Issue.table_name
db_field = field
end
case operator_for field case operator_for field
when "=" when "="
sql = sql + "#{Issue.table_name}.#{field} IN (" + v.each(&:to_i).join(",") + ")" sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!" when "!"
sql = sql + "#{Issue.table_name}.#{field} NOT IN (" + v.each(&:to_i).join(",") + ")" sql = sql + "#{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!*" when "!*"
sql = sql + "#{Issue.table_name}.#{field} IS NULL" sql = sql + "#{db_table}.#{db_field} IS NULL"
when "*" when "*"
sql = sql + "#{Issue.table_name}.#{field} IS NOT NULL" sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
when "o" when "o"
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c" when "c"
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
when ">t-" when ">t-"
sql = sql + "#{Issue.table_name}.#{field} >= '%s'" % connection.quoted_date(Date.today - v.first.to_i) sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date(Date.today - v.first.to_i)
when "<t-" when "<t-"
sql = sql + "#{Issue.table_name}.#{field} <= '" + (Date.today - v.first.to_i).strftime("%Y-%m-%d") + "'" sql = sql + "#{db_table}.#{db_field} BETWEEN '#{connection.quoted_date(Date.new(0))}' AND '" + (Date.today - v.first.to_i).strftime("%Y-%m-%d") + "'"
when "t-" when "t-"
sql = sql + "#{Issue.table_name}.#{field} = '" + (Date.today - v.first.to_i).strftime("%Y-%m-%d") + "'" sql = sql + "#{db_table}.#{db_field} = '" + (Date.today - v.first.to_i).strftime("%Y-%m-%d") + "'"
when ">t+" when ">t+"
sql = sql + "#{Issue.table_name}.#{field} >= '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'" sql = sql + "#{db_table}.#{db_field} >= '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'"
when "<t+" when "<t+"
sql = sql + "#{Issue.table_name}.#{field} <= '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'" sql = sql + "#{db_table}.#{db_field} BETWEEN '#{connection.quoted_date(Date.new(0))}' AND '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'"
when "t+" when "t+"
sql = sql + "#{Issue.table_name}.#{field} = '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'" sql = sql + "#{db_table}.#{db_field} = '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'"
when "t" when "t"
sql = sql + "#{Issue.table_name}.#{field} = '%s'" % connection.quoted_date(Date.today) sql = sql + "#{db_table}.#{db_field} = '%s'" % connection.quoted_date(Date.today)
when "~" when "~"
sql = sql + "#{Issue.table_name}.#{field} LIKE '%#{connection.quote_string(v.first)}%'" sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
when "!~" when "!~"
sql = sql + "#{Issue.table_name}.#{field} NOT LIKE '%#{connection.quote_string(v.first)}%'" sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
end end
sql << ")"
end if filters and valid? end if filters and valid?
sql sql
end end
......
...@@ -84,6 +84,7 @@ when "IssueCustomField" %> ...@@ -84,6 +84,7 @@ when "IssueCustomField" %>
&nbsp; &nbsp;
<p><%= f.check_box :is_required %></p> <p><%= f.check_box :is_required %></p>
<p><%= f.check_box :is_for_all %></p> <p><%= f.check_box :is_for_all %></p>
<p><%= f.check_box :is_filter %></p>
<% when "UserCustomField" %> <% when "UserCustomField" %>
<p><%= f.check_box :is_required %></p> <p><%= f.check_box :is_required %></p>
......
...@@ -66,7 +66,7 @@ function toggle_multi_select(field) { ...@@ -66,7 +66,7 @@ function toggle_multi_select(field) {
<tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>"> <tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>">
<td valign="top" style="width:200px;"> <td valign="top" style="width:200px;">
<%= check_box_tag 'fields[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %> <%= check_box_tag 'fields[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
<label for="cb_<%= field %>"><%= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label> <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
</td> </td>
<td valign="top" style="width:150px;"> <td valign="top" style="width:150px;">
<%= select_tag "operators[#{field}]", options_for_select(operators_for_select(options[:type]), query.operator_for(field)), :id => "operators_#{field}", :onchange => "toggle_operator('#{field}');", :class => "select-small", :style => "vertical-align: top;" %> <%= select_tag "operators[#{field}]", options_for_select(operators_for_select(options[:type]), query.operator_for(field)), :id => "operators_#{field}", :onchange => "toggle_operator('#{field}');", :class => "select-small", :style => "vertical-align: top;" %>
...@@ -81,7 +81,7 @@ function toggle_multi_select(field) { ...@@ -81,7 +81,7 @@ function toggle_multi_select(field) {
<%= link_to_function image_tag('expand.png'), "toggle_multi_select('#{field}');" %> <%= link_to_function image_tag('expand.png'), "toggle_multi_select('#{field}');" %>
<% when :date, :date_past %> <% when :date, :date_past %>
<%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %> <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %>
<% when :text %> <% when :string, :text %>
<%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 30, :class => "select-small" %> <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 30, :class => "select-small" %>
<% end %> <% end %>
</div> </div>
...@@ -93,7 +93,7 @@ function toggle_multi_select(field) { ...@@ -93,7 +93,7 @@ function toggle_multi_select(field) {
</td> </td>
<td align="right" valign="top"> <td align="right" valign="top">
<%= l(:label_filter_add) %>: <%= l(:label_filter_add) %>:
<%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [l(("field_"+field[0].to_s.gsub(/\_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact), :onchange => "add_filter();", :class => "select-small" %> <%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [ field[1][:name] || l(("field_"+field[0].to_s.gsub(/\_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact), :onchange => "add_filter();", :class => "select-small" %>
</td> </td>
</tr> </tr>
</table> </table>
......
class AddCustomFieldIsFilter < ActiveRecord::Migration
def self.up
add_column :custom_fields, :is_filter, :boolean, :null => false, :default => false
end
def self.down
remove_column :custom_fields, :is_filter
end
end
...@@ -147,6 +147,7 @@ field_hours: Stunden ...@@ -147,6 +147,7 @@ field_hours: Stunden
field_activity: Aktivität field_activity: Aktivität
field_spent_on: Datum field_spent_on: Datum
field_identifier: Identifier field_identifier: Identifier
field_is_filter: Used as a filter
setting_app_title: Applikation Titel setting_app_title: Applikation Titel
setting_app_subtitle: Applikation Untertitel setting_app_subtitle: Applikation Untertitel
......
...@@ -147,6 +147,7 @@ field_hours: Hours ...@@ -147,6 +147,7 @@ field_hours: Hours
field_activity: Activity field_activity: Activity
field_spent_on: Date field_spent_on: Date
field_identifier: Identifier field_identifier: Identifier
field_is_filter: Used as a filter
setting_app_title: Application title setting_app_title: Application title
setting_app_subtitle: Application subtitle setting_app_subtitle: Application subtitle
......
...@@ -147,6 +147,7 @@ field_hours: Hours ...@@ -147,6 +147,7 @@ field_hours: Hours
field_activity: Activity field_activity: Activity
field_spent_on: Fecha field_spent_on: Fecha
field_identifier: Identifier field_identifier: Identifier
field_is_filter: Used as a filter
setting_app_title: Título del aplicación setting_app_title: Título del aplicación
setting_app_subtitle: Subtítulo del aplicación setting_app_subtitle: Subtítulo del aplicación
......
...@@ -147,6 +147,7 @@ field_hours: Heures ...@@ -147,6 +147,7 @@ field_hours: Heures
field_activity: Activité field_activity: Activité
field_spent_on: Date field_spent_on: Date
field_identifier: Identifiant field_identifier: Identifiant
field_is_filter: Utilisé comme filtre
setting_app_title: Titre de l'application setting_app_title: Titre de l'application
setting_app_subtitle: Sous-titre de l'application setting_app_subtitle: Sous-titre de l'application
......
...@@ -147,6 +147,7 @@ field_hours: Hours ...@@ -147,6 +147,7 @@ field_hours: Hours
field_activity: Activity field_activity: Activity
field_spent_on: Data field_spent_on: Data
field_identifier: Identifier field_identifier: Identifier
field_is_filter: Used as a filter
setting_app_title: Titolo applicazione setting_app_title: Titolo applicazione
setting_app_subtitle: Sottotitolo applicazione setting_app_subtitle: Sottotitolo applicazione
......
...@@ -148,6 +148,7 @@ field_hours: 時間 ...@@ -148,6 +148,7 @@ field_hours: 時間
field_activity: 活動 field_activity: 活動
field_spent_on: 日付 field_spent_on: 日付
field_identifier: 識別子 field_identifier: 識別子
field_is_filter: Used as a filter
setting_app_title: アプリケーションのタイトル setting_app_title: アプリケーションのタイトル
setting_app_subtitle: アプリケーションのサブタイトル setting_app_subtitle: アプリケーションのサブタイトル
......
...@@ -150,6 +150,7 @@ field_hours: Hours ...@@ -150,6 +150,7 @@ field_hours: Hours
field_activity: 活动 field_activity: 活动
field_spent_on: 日期 field_spent_on: 日期
field_identifier: Identifier field_identifier: Identifier
field_is_filter: Used as a filter
setting_app_title: 应用程序标题 setting_app_title: 应用程序标题
setting_app_subtitle: 应用程序子标题 setting_app_subtitle: 应用程序子标题
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment