From 58610ec52af2249c4c5eebf35e11cd827a7966ab Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 18:54:55 +0000 Subject: [PATCH] Search engine: issue custom fields can now be searched. Each issue custom field (excepting numeric, date and boolean fields) can be marked as "Searchable" (default to false). git-svn-id: http://redmine.rubyforge.org/svn/trunk@994 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/custom_field.rb | 2 ++ app/views/custom_fields/_form.rhtml | 17 ++++++++++---- .../086_add_custom_fields_searchable.rb | 9 ++++++++ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + test/fixtures/custom_fields.yml | 5 ++-- test/fixtures/custom_values.yml | 9 +++++++- test/functional/search_controller_test.rb | 15 ++++++++++-- .../lib/acts_as_searchable.rb | 23 +++++++++++++++++-- 27 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 db/migrate/086_add_custom_fields_searchable.rb diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index c3b5f2a9f..e1fd8666d 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -43,6 +43,8 @@ class CustomField < ActiveRecord::Base def before_validation # remove empty values self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact + # make sure these fields are not searchable + self.searchable = false if %w(int float date bool).include?(field_format) end def validate diff --git a/app/views/custom_fields/_form.rhtml b/app/views/custom_fields/_form.rhtml index 013be9b70..915daab32 100644 --- a/app/views/custom_fields/_form.rhtml +++ b/app/views/custom_fields/_form.rhtml @@ -7,21 +7,32 @@ function toggle_custom_field_format() { p_length = $("custom_field_min_length"); p_regexp = $("custom_field_regexp"); p_values = $("custom_field_possible_values"); + p_searchable = $("custom_field_searchable"); switch (format.value) { case "list": Element.hide(p_length.parentNode); Element.hide(p_regexp.parentNode); + Element.show(p_searchable.parentNode); Element.show(p_values); break; case "date": case "bool": Element.hide(p_length.parentNode); Element.hide(p_regexp.parentNode); + Element.hide(p_searchable.parentNode); + Element.hide(p_values); + break; + case "float": + case "int": + Element.show(p_length.parentNode); + Element.show(p_regexp.parentNode); + Element.hide(p_searchable.parentNode); Element.hide(p_values); break; default: Element.show(p_length.parentNode); Element.show(p_regexp.parentNode); + Element.show(p_searchable.parentNode); Element.hide(p_values); break; } @@ -47,7 +58,6 @@ function deleteValueField(e) { //]]> -

<%= f.text_field :name, :required => true %>

<%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %>

@@ -59,11 +69,8 @@ function deleteValueField(e) { <% (@custom_field.possible_values.to_a + [""]).each do |value| %> <%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %>
<% end %> -

-<%= javascript_tag "toggle_custom_field_format();" %> -
<% case @custom_field.type.to_s @@ -78,6 +85,7 @@ when "IssueCustomField" %>

<%= f.check_box :is_required %>

<%= f.check_box :is_for_all %>

<%= f.check_box :is_filter %>

+

<%= f.check_box :searchable %>

<% when "UserCustomField" %>

<%= f.check_box :is_required %>

@@ -87,3 +95,4 @@ when "IssueCustomField" %> <% end %>
+<%= javascript_tag "toggle_custom_field_format();" %> diff --git a/db/migrate/086_add_custom_fields_searchable.rb b/db/migrate/086_add_custom_fields_searchable.rb new file mode 100644 index 000000000..53158d14e --- /dev/null +++ b/db/migrate/086_add_custom_fields_searchable.rb @@ -0,0 +1,9 @@ +class AddCustomFieldsSearchable < ActiveRecord::Migration + def self.up + add_column :custom_fields, :searchable, :boolean, :default => false + end + + def self.down + remove_column :custom_fields, :searchable + end +end diff --git a/lang/bg.yml b/lang/bg.yml index 590952681..d9bcd6c7c 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/cs.yml b/lang/cs.yml index 816f9b92e..fe29defe6 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/de.yml b/lang/de.yml index 046ed9994..c9cdc0c12 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Muss mindestens %d Zeichen lang sein. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/en.yml b/lang/en.yml index 104e7fe6e..116935f73 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -170,6 +170,7 @@ field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time field_column_names: Columns field_time_zone: Time zone +field_searchable: Searchable setting_app_title: Application title setting_app_subtitle: Application subtitle diff --git a/lang/es.yml b/lang/es.yml index d806d066e..50c652f2c 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -552,3 +552,4 @@ setting_time_format: Formato de hora setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/fr.yml b/lang/fr.yml index 36ccc463f..d6ad5da56 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -170,6 +170,7 @@ field_redirect_existing_links: Rediriger les liens existants field_estimated_hours: Temps estimé field_column_names: Colonnes field_time_zone: Fuseau horaire +field_searchable: Utilisé pour les recherches setting_app_title: Titre de l'application setting_app_subtitle: Sous-titre de l'application diff --git a/lang/he.yml b/lang/he.yml index 6be1a4c77..0ed57e527 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/it.yml b/lang/it.yml index 8a3e954f4..d266f797e 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/ja.yml b/lang/ja.yml index 1ecfb1ae9..bfe120cfe 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -550,3 +550,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/ko.yml b/lang/ko.yml index ef081e622..76debf345 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/nl.yml b/lang/nl.yml index 24a343eb3..a8b5cc64a 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -550,3 +550,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/pl.yml b/lang/pl.yml index 222c7ea9d..5fa50ec51 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Musi być nie krótsze niż %d znaków. setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc) button_annotate: Adnotuj label_issues_by: Zagadnienia wprowadzone przez %s +field_searchable: Searchable diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 8c903edd4..ed31915dd 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/pt.yml b/lang/pt.yml index 10de07b5f..72ba4d2b0 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/ro.yml b/lang/ro.yml index f7d3acd56..6f1eb7810 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/ru.yml b/lang/ru.yml index cad357c0c..c7697ae1a 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/sr.yml b/lang/sr.yml index f9008d890..b5e9ecfe3 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -550,3 +550,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/sv.yml b/lang/sv.yml index a4f55a17a..d789a2a37 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -550,3 +550,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index af0fd3ed4..4d730c1a9 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -549,3 +549,4 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目重要性 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (time tracking) +field_searchable: Searchable diff --git a/lang/zh.yml b/lang/zh.yml index de4ae1ece..6876daa17 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -552,3 +552,4 @@ text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml index ce7509fe9..e73e6de96 100644 --- a/test/fixtures/custom_fields.yml +++ b/test/fixtures/custom_fields.yml @@ -11,16 +11,17 @@ custom_fields_001: is_required: false field_format: list custom_fields_002: - name: Build + name: Searchable field min_length: 1 regexp: "" is_for_all: true type: IssueCustomField - max_length: 10 + max_length: 100 possible_values: "" id: 2 is_required: false field_format: string + searchable: true custom_fields_003: name: Development status min_length: 0 diff --git a/test/fixtures/custom_values.yml b/test/fixtures/custom_values.yml index b71227971..572142889 100644 --- a/test/fixtures/custom_values.yml +++ b/test/fixtures/custom_values.yml @@ -46,4 +46,11 @@ custom_values_008: custom_field_id: 1 customized_id: 3 id: 11 - value: "MySQL" \ No newline at end of file + value: "MySQL" +custom_values_009: + customized_type: Issue + custom_field_id: 2 + customized_id: 3 + id: 12 + value: "this is a stringforcustomfield search" + \ No newline at end of file diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb index 5e3673a4e..330cd0de0 100644 --- a/test/functional/search_controller_test.rb +++ b/test/functional/search_controller_test.rb @@ -5,7 +5,7 @@ require 'search_controller' class SearchController; def rescue_action(e) raise e end; end class SearchControllerTest < Test::Unit::TestCase - fixtures :projects, :issues + fixtures :projects, :issues, :custom_fields, :custom_values def setup @controller = SearchController.new @@ -25,7 +25,9 @@ class SearchControllerTest < Test::Unit::TestCase assert assigns(:results).include?(Project.find(1)) end - def test_search_in_project + def test_search_without_searchable_custom_fields + CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}" + get :index, :id => 1 assert_response :success assert_template 'index' @@ -36,6 +38,15 @@ class SearchControllerTest < Test::Unit::TestCase assert_template 'index' end + def test_search_with_searchable_custom_fields + get :index, :id => 1, :q => "stringforcustomfield" + assert_response :success + results = assigns(:results) + assert_not_nil results + assert_equal 1, results.size + assert results.include?(Issue.find(3)) + end + def test_quick_jump_to_issue # issue of a public project get :index, :q => "3" diff --git a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb index e2a323abb..1dd88978c 100644 --- a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb +++ b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb @@ -49,6 +49,9 @@ module Redmine raise 'No date column defined defined.' end + # Should we search custom fields on this model ? + searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? + send :include, Redmine::Acts::Searchable::InstanceMethods end end @@ -67,11 +70,27 @@ module Redmine columns = searchable_options[:columns] columns.slice!(1..-1) if options[:titles_only] - sql = ([ '(' + columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}.join(' OR ') + ')' ] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"} + + if !options[:titles_only] && searchable_options[:search_custom_fields] + searchable_custom_field_ids = CustomField.find(:all, + :select => 'id', + :conditions => { :type => "#{self.name}CustomField", + :searchable => true }).collect(&:id) + if searchable_custom_field_ids.any? + custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" + + " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" + + " AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))" + token_clauses << custom_field_sql + end + end + + sql = ([token_clauses.join(' OR ')] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + if options[:offset] sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')" end - find_options[:conditions] = [sql, * (tokens * columns.size).sort] + find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort] results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do find(:all, find_options)