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
This commit is contained in:
parent
38b185f1dc
commit
58610ec52a
|
@ -43,6 +43,8 @@ class CustomField < ActiveRecord::Base
|
||||||
def before_validation
|
def before_validation
|
||||||
# remove empty values
|
# remove empty values
|
||||||
self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact
|
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
|
end
|
||||||
|
|
||||||
def validate
|
def validate
|
||||||
|
|
|
@ -7,21 +7,32 @@ function toggle_custom_field_format() {
|
||||||
p_length = $("custom_field_min_length");
|
p_length = $("custom_field_min_length");
|
||||||
p_regexp = $("custom_field_regexp");
|
p_regexp = $("custom_field_regexp");
|
||||||
p_values = $("custom_field_possible_values");
|
p_values = $("custom_field_possible_values");
|
||||||
|
p_searchable = $("custom_field_searchable");
|
||||||
switch (format.value) {
|
switch (format.value) {
|
||||||
case "list":
|
case "list":
|
||||||
Element.hide(p_length.parentNode);
|
Element.hide(p_length.parentNode);
|
||||||
Element.hide(p_regexp.parentNode);
|
Element.hide(p_regexp.parentNode);
|
||||||
|
Element.show(p_searchable.parentNode);
|
||||||
Element.show(p_values);
|
Element.show(p_values);
|
||||||
break;
|
break;
|
||||||
case "date":
|
case "date":
|
||||||
case "bool":
|
case "bool":
|
||||||
Element.hide(p_length.parentNode);
|
Element.hide(p_length.parentNode);
|
||||||
Element.hide(p_regexp.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);
|
Element.hide(p_values);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Element.show(p_length.parentNode);
|
Element.show(p_length.parentNode);
|
||||||
Element.show(p_regexp.parentNode);
|
Element.show(p_regexp.parentNode);
|
||||||
|
Element.show(p_searchable.parentNode);
|
||||||
Element.hide(p_values);
|
Element.hide(p_values);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +58,6 @@ function deleteValueField(e) {
|
||||||
//]]>
|
//]]>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!--[form:custom_field]-->
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<p><%= f.text_field :name, :required => true %></p>
|
<p><%= f.text_field :name, :required => true %></p>
|
||||||
<p><%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %></p>
|
<p><%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %></p>
|
||||||
|
@ -59,11 +69,8 @@ function deleteValueField(e) {
|
||||||
<% (@custom_field.possible_values.to_a + [""]).each do |value| %>
|
<% (@custom_field.possible_values.to_a + [""]).each do |value| %>
|
||||||
<span><%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %><br /></span>
|
<span><%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %><br /></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<%= javascript_tag "toggle_custom_field_format();" %>
|
|
||||||
<!--[eoform:custom_field]-->
|
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<% case @custom_field.type.to_s
|
<% case @custom_field.type.to_s
|
||||||
|
@ -78,6 +85,7 @@ when "IssueCustomField" %>
|
||||||
<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>
|
<p><%= f.check_box :is_filter %></p>
|
||||||
|
<p><%= f.check_box :searchable %></p>
|
||||||
|
|
||||||
<% when "UserCustomField" %>
|
<% when "UserCustomField" %>
|
||||||
<p><%= f.check_box :is_required %></p>
|
<p><%= f.check_box :is_required %></p>
|
||||||
|
@ -87,3 +95,4 @@ when "IssueCustomField" %>
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
<%= javascript_tag "toggle_custom_field_format();" %>
|
||||||
|
|
|
@ -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
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Muss mindestens %d Zeichen lang sein.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -170,6 +170,7 @@ field_redirect_existing_links: Redirect existing links
|
||||||
field_estimated_hours: Estimated time
|
field_estimated_hours: Estimated time
|
||||||
field_column_names: Columns
|
field_column_names: Columns
|
||||||
field_time_zone: Time zone
|
field_time_zone: Time zone
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
||||||
setting_app_title: Application title
|
setting_app_title: Application title
|
||||||
setting_app_subtitle: Application subtitle
|
setting_app_subtitle: Application subtitle
|
||||||
|
|
|
@ -552,3 +552,4 @@ setting_time_format: Formato de hora
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -170,6 +170,7 @@ field_redirect_existing_links: Rediriger les liens existants
|
||||||
field_estimated_hours: Temps estimé
|
field_estimated_hours: Temps estimé
|
||||||
field_column_names: Colonnes
|
field_column_names: Colonnes
|
||||||
field_time_zone: Fuseau horaire
|
field_time_zone: Fuseau horaire
|
||||||
|
field_searchable: Utilisé pour les recherches
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -550,3 +550,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -550,3 +550,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Musi być nie krótsze niż %d znaków.
|
||||||
setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc)
|
setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc)
|
||||||
button_annotate: Adnotuj
|
button_annotate: Adnotuj
|
||||||
label_issues_by: Zagadnienia wprowadzone przez %s
|
label_issues_by: Zagadnienia wprowadzone przez %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -550,3 +550,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -550,3 +550,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -549,3 +549,4 @@ default_activity_development: 開發
|
||||||
enumeration_issue_priorities: 項目重要性
|
enumeration_issue_priorities: 項目重要性
|
||||||
enumeration_doc_categories: 文件分類
|
enumeration_doc_categories: 文件分類
|
||||||
enumeration_activities: 活動 (time tracking)
|
enumeration_activities: 活動 (time tracking)
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -552,3 +552,4 @@ text_caracters_minimum: Must be at least %d characters long.
|
||||||
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
setting_bcc_recipients: Blind carbon copy recipients (bcc)
|
||||||
button_annotate: Annotate
|
button_annotate: Annotate
|
||||||
label_issues_by: Issues by %s
|
label_issues_by: Issues by %s
|
||||||
|
field_searchable: Searchable
|
||||||
|
|
|
@ -11,16 +11,17 @@ custom_fields_001:
|
||||||
is_required: false
|
is_required: false
|
||||||
field_format: list
|
field_format: list
|
||||||
custom_fields_002:
|
custom_fields_002:
|
||||||
name: Build
|
name: Searchable field
|
||||||
min_length: 1
|
min_length: 1
|
||||||
regexp: ""
|
regexp: ""
|
||||||
is_for_all: true
|
is_for_all: true
|
||||||
type: IssueCustomField
|
type: IssueCustomField
|
||||||
max_length: 10
|
max_length: 100
|
||||||
possible_values: ""
|
possible_values: ""
|
||||||
id: 2
|
id: 2
|
||||||
is_required: false
|
is_required: false
|
||||||
field_format: string
|
field_format: string
|
||||||
|
searchable: true
|
||||||
custom_fields_003:
|
custom_fields_003:
|
||||||
name: Development status
|
name: Development status
|
||||||
min_length: 0
|
min_length: 0
|
||||||
|
|
|
@ -46,4 +46,11 @@ custom_values_008:
|
||||||
custom_field_id: 1
|
custom_field_id: 1
|
||||||
customized_id: 3
|
customized_id: 3
|
||||||
id: 11
|
id: 11
|
||||||
value: "MySQL"
|
value: "MySQL"
|
||||||
|
custom_values_009:
|
||||||
|
customized_type: Issue
|
||||||
|
custom_field_id: 2
|
||||||
|
customized_id: 3
|
||||||
|
id: 12
|
||||||
|
value: "this is a stringforcustomfield search"
|
||||||
|
|
|
@ -5,7 +5,7 @@ require 'search_controller'
|
||||||
class SearchController; def rescue_action(e) raise e end; end
|
class SearchController; def rescue_action(e) raise e end; end
|
||||||
|
|
||||||
class SearchControllerTest < Test::Unit::TestCase
|
class SearchControllerTest < Test::Unit::TestCase
|
||||||
fixtures :projects, :issues
|
fixtures :projects, :issues, :custom_fields, :custom_values
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
@controller = SearchController.new
|
@controller = SearchController.new
|
||||||
|
@ -25,7 +25,9 @@ class SearchControllerTest < Test::Unit::TestCase
|
||||||
assert assigns(:results).include?(Project.find(1))
|
assert assigns(:results).include?(Project.find(1))
|
||||||
end
|
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
|
get :index, :id => 1
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_template 'index'
|
assert_template 'index'
|
||||||
|
@ -36,6 +38,15 @@ class SearchControllerTest < Test::Unit::TestCase
|
||||||
assert_template 'index'
|
assert_template 'index'
|
||||||
end
|
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
|
def test_quick_jump_to_issue
|
||||||
# issue of a public project
|
# issue of a public project
|
||||||
get :index, :q => "3"
|
get :index, :q => "3"
|
||||||
|
|
|
@ -49,6 +49,9 @@ module Redmine
|
||||||
raise 'No date column defined defined.'
|
raise 'No date column defined defined.'
|
||||||
end
|
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
|
send :include, Redmine::Acts::Searchable::InstanceMethods
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -67,11 +70,27 @@ module Redmine
|
||||||
columns = searchable_options[:columns]
|
columns = searchable_options[:columns]
|
||||||
columns.slice!(1..-1) if options[:titles_only]
|
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]
|
if options[:offset]
|
||||||
sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
|
sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
|
||||||
end
|
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
|
results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do
|
||||||
find(:all, find_options)
|
find(:all, find_options)
|
||||||
|
|
Loading…
Reference in New Issue