@@ -0,0 +1,9 | |||||
|
1 | class AddCustomFieldsSearchable < ActiveRecord::Migration | |||
|
2 | def self.up | |||
|
3 | add_column :custom_fields, :searchable, :boolean, :default => false | |||
|
4 | end | |||
|
5 | ||||
|
6 | def self.down | |||
|
7 | remove_column :custom_fields, :searchable | |||
|
8 | end | |||
|
9 | end |
@@ -43,6 +43,8 class CustomField < ActiveRecord::Base | |||||
43 | def before_validation |
|
43 | def before_validation | |
44 | # remove empty values |
|
44 | # remove empty values | |
45 | self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact |
|
45 | self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact | |
|
46 | # make sure these fields are not searchable | |||
|
47 | self.searchable = false if %w(int float date bool).include?(field_format) | |||
46 | end |
|
48 | end | |
47 |
|
49 | |||
48 | def validate |
|
50 | def validate |
@@ -7,21 +7,32 function toggle_custom_field_format() { | |||||
7 | p_length = $("custom_field_min_length"); |
|
7 | p_length = $("custom_field_min_length"); | |
8 | p_regexp = $("custom_field_regexp"); |
|
8 | p_regexp = $("custom_field_regexp"); | |
9 | p_values = $("custom_field_possible_values"); |
|
9 | p_values = $("custom_field_possible_values"); | |
|
10 | p_searchable = $("custom_field_searchable"); | |||
10 | switch (format.value) { |
|
11 | switch (format.value) { | |
11 | case "list": |
|
12 | case "list": | |
12 | Element.hide(p_length.parentNode); |
|
13 | Element.hide(p_length.parentNode); | |
13 | Element.hide(p_regexp.parentNode); |
|
14 | Element.hide(p_regexp.parentNode); | |
|
15 | Element.show(p_searchable.parentNode); | |||
14 | Element.show(p_values); |
|
16 | Element.show(p_values); | |
15 | break; |
|
17 | break; | |
16 | case "date": |
|
18 | case "date": | |
17 | case "bool": |
|
19 | case "bool": | |
18 | Element.hide(p_length.parentNode); |
|
20 | Element.hide(p_length.parentNode); | |
19 | Element.hide(p_regexp.parentNode); |
|
21 | Element.hide(p_regexp.parentNode); | |
|
22 | Element.hide(p_searchable.parentNode); | |||
|
23 | Element.hide(p_values); | |||
|
24 | break; | |||
|
25 | case "float": | |||
|
26 | case "int": | |||
|
27 | Element.show(p_length.parentNode); | |||
|
28 | Element.show(p_regexp.parentNode); | |||
|
29 | Element.hide(p_searchable.parentNode); | |||
20 | Element.hide(p_values); |
|
30 | Element.hide(p_values); | |
21 | break; |
|
31 | break; | |
22 | default: |
|
32 | default: | |
23 | Element.show(p_length.parentNode); |
|
33 | Element.show(p_length.parentNode); | |
24 | Element.show(p_regexp.parentNode); |
|
34 | Element.show(p_regexp.parentNode); | |
|
35 | Element.show(p_searchable.parentNode); | |||
25 | Element.hide(p_values); |
|
36 | Element.hide(p_values); | |
26 | break; |
|
37 | break; | |
27 | } |
|
38 | } | |
@@ -47,7 +58,6 function deleteValueField(e) { | |||||
47 | //]]> |
|
58 | //]]> | |
48 | </script> |
|
59 | </script> | |
49 |
|
60 | |||
50 | <!--[form:custom_field]--> |
|
|||
51 | <div class="box"> |
|
61 | <div class="box"> | |
52 | <p><%= f.text_field :name, :required => true %></p> |
|
62 | <p><%= f.text_field :name, :required => true %></p> | |
53 | <p><%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %></p> |
|
63 | <p><%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %></p> | |
@@ -59,11 +69,8 function deleteValueField(e) { | |||||
59 | <% (@custom_field.possible_values.to_a + [""]).each do |value| %> |
|
69 | <% (@custom_field.possible_values.to_a + [""]).each do |value| %> | |
60 | <span><%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %><br /></span> |
|
70 | <span><%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %><br /></span> | |
61 | <% end %> |
|
71 | <% end %> | |
62 |
|
||||
63 | </p> |
|
72 | </p> | |
64 | </div> |
|
73 | </div> | |
65 | <%= javascript_tag "toggle_custom_field_format();" %> |
|
|||
66 | <!--[eoform:custom_field]--> |
|
|||
67 |
|
74 | |||
68 | <div class="box"> |
|
75 | <div class="box"> | |
69 | <% case @custom_field.type.to_s |
|
76 | <% case @custom_field.type.to_s | |
@@ -78,6 +85,7 when "IssueCustomField" %> | |||||
78 | <p><%= f.check_box :is_required %></p> |
|
85 | <p><%= f.check_box :is_required %></p> | |
79 | <p><%= f.check_box :is_for_all %></p> |
|
86 | <p><%= f.check_box :is_for_all %></p> | |
80 | <p><%= f.check_box :is_filter %></p> |
|
87 | <p><%= f.check_box :is_filter %></p> | |
|
88 | <p><%= f.check_box :searchable %></p> | |||
81 |
|
89 | |||
82 | <% when "UserCustomField" %> |
|
90 | <% when "UserCustomField" %> | |
83 | <p><%= f.check_box :is_required %></p> |
|
91 | <p><%= f.check_box :is_required %></p> | |
@@ -87,3 +95,4 when "IssueCustomField" %> | |||||
87 |
|
95 | |||
88 | <% end %> |
|
96 | <% end %> | |
89 | </div> |
|
97 | </div> | |
|
98 | <%= javascript_tag "toggle_custom_field_format();" %> |
@@ -549,3 +549,4 text_caracters_minimum: Must be at least %d characters long. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -549,3 +549,4 text_caracters_minimum: Must be at least %d characters long. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -549,3 +549,4 text_caracters_minimum: Muss mindestens %d Zeichen lang sein. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -170,6 +170,7 field_redirect_existing_links: Redirect existing links | |||||
170 | field_estimated_hours: Estimated time |
|
170 | field_estimated_hours: Estimated time | |
171 | field_column_names: Columns |
|
171 | field_column_names: Columns | |
172 | field_time_zone: Time zone |
|
172 | field_time_zone: Time zone | |
|
173 | field_searchable: Searchable | |||
173 |
|
174 | |||
174 | setting_app_title: Application title |
|
175 | setting_app_title: Application title | |
175 | setting_app_subtitle: Application subtitle |
|
176 | setting_app_subtitle: Application subtitle |
@@ -552,3 +552,4 setting_time_format: Formato de hora | |||||
552 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
552 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
553 | button_annotate: Annotate |
|
553 | button_annotate: Annotate | |
554 | label_issues_by: Issues by %s |
|
554 | label_issues_by: Issues by %s | |
|
555 | field_searchable: Searchable |
@@ -170,6 +170,7 field_redirect_existing_links: Rediriger les liens existants | |||||
170 | field_estimated_hours: Temps estimé |
|
170 | field_estimated_hours: Temps estimé | |
171 | field_column_names: Colonnes |
|
171 | field_column_names: Colonnes | |
172 | field_time_zone: Fuseau horaire |
|
172 | field_time_zone: Fuseau horaire | |
|
173 | field_searchable: Utilisé pour les recherches | |||
173 |
|
174 | |||
174 | setting_app_title: Titre de l'application |
|
175 | setting_app_title: Titre de l'application | |
175 | setting_app_subtitle: Sous-titre de l'application |
|
176 | setting_app_subtitle: Sous-titre de l'application |
@@ -549,3 +549,4 text_caracters_minimum: Must be at least %d characters long. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -549,3 +549,4 text_caracters_minimum: Must be at least %d characters long. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -550,3 +550,4 text_caracters_minimum: Must be at least %d characters long. | |||||
550 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
550 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
551 | button_annotate: Annotate |
|
551 | button_annotate: Annotate | |
552 | label_issues_by: Issues by %s |
|
552 | label_issues_by: Issues by %s | |
|
553 | field_searchable: Searchable |
@@ -549,3 +549,4 text_caracters_minimum: Must be at least %d characters long. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -550,3 +550,4 text_caracters_minimum: Must be at least %d characters long. | |||||
550 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
550 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
551 | button_annotate: Annotate |
|
551 | button_annotate: Annotate | |
552 | label_issues_by: Issues by %s |
|
552 | label_issues_by: Issues by %s | |
|
553 | field_searchable: Searchable |
@@ -549,3 +549,4 text_caracters_minimum: Musi być nie krótsze niż %d znaków. | |||||
549 | setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc) |
|
549 | setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc) | |
550 | button_annotate: Adnotuj |
|
550 | button_annotate: Adnotuj | |
551 | label_issues_by: Zagadnienia wprowadzone przez %s |
|
551 | label_issues_by: Zagadnienia wprowadzone przez %s | |
|
552 | field_searchable: Searchable |
@@ -549,3 +549,4 text_caracters_minimum: Must be at least %d characters long. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -549,3 +549,4 text_caracters_minimum: Must be at least %d characters long. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -549,3 +549,4 text_caracters_minimum: Must be at least %d characters long. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -549,3 +549,4 text_caracters_minimum: Must be at least %d characters long. | |||||
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
549 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
550 | button_annotate: Annotate |
|
550 | button_annotate: Annotate | |
551 | label_issues_by: Issues by %s |
|
551 | label_issues_by: Issues by %s | |
|
552 | field_searchable: Searchable |
@@ -550,3 +550,4 text_caracters_minimum: Must be at least %d characters long. | |||||
550 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
550 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
551 | button_annotate: Annotate |
|
551 | button_annotate: Annotate | |
552 | label_issues_by: Issues by %s |
|
552 | label_issues_by: Issues by %s | |
|
553 | field_searchable: Searchable |
@@ -550,3 +550,4 text_caracters_minimum: Must be at least %d characters long. | |||||
550 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
550 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
551 | button_annotate: Annotate |
|
551 | button_annotate: Annotate | |
552 | label_issues_by: Issues by %s |
|
552 | label_issues_by: Issues by %s | |
|
553 | field_searchable: Searchable |
@@ -549,3 +549,4 default_activity_development: 開發 | |||||
549 | enumeration_issue_priorities: 項目重要性 |
|
549 | enumeration_issue_priorities: 項目重要性 | |
550 | enumeration_doc_categories: 文件分類 |
|
550 | enumeration_doc_categories: 文件分類 | |
551 | enumeration_activities: 活動 (time tracking) |
|
551 | enumeration_activities: 活動 (time tracking) | |
|
552 | field_searchable: Searchable |
@@ -552,3 +552,4 text_caracters_minimum: Must be at least %d characters long. | |||||
552 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
552 | setting_bcc_recipients: Blind carbon copy recipients (bcc) | |
553 | button_annotate: Annotate |
|
553 | button_annotate: Annotate | |
554 | label_issues_by: Issues by %s |
|
554 | label_issues_by: Issues by %s | |
|
555 | field_searchable: Searchable |
@@ -11,16 +11,17 custom_fields_001: | |||||
11 | is_required: false |
|
11 | is_required: false | |
12 | field_format: list |
|
12 | field_format: list | |
13 | custom_fields_002: |
|
13 | custom_fields_002: | |
14 | name: Build |
|
14 | name: Searchable field | |
15 | min_length: 1 |
|
15 | min_length: 1 | |
16 | regexp: "" |
|
16 | regexp: "" | |
17 | is_for_all: true |
|
17 | is_for_all: true | |
18 | type: IssueCustomField |
|
18 | type: IssueCustomField | |
19 | max_length: 10 |
|
19 | max_length: 100 | |
20 | possible_values: "" |
|
20 | possible_values: "" | |
21 | id: 2 |
|
21 | id: 2 | |
22 | is_required: false |
|
22 | is_required: false | |
23 | field_format: string |
|
23 | field_format: string | |
|
24 | searchable: true | |||
24 | custom_fields_003: |
|
25 | custom_fields_003: | |
25 | name: Development status |
|
26 | name: Development status | |
26 | min_length: 0 |
|
27 | min_length: 0 |
@@ -47,3 +47,10 custom_values_008: | |||||
47 | customized_id: 3 |
|
47 | customized_id: 3 | |
48 | id: 11 |
|
48 | id: 11 | |
49 | value: "MySQL" |
|
49 | value: "MySQL" | |
|
50 | custom_values_009: | |||
|
51 | customized_type: Issue | |||
|
52 | custom_field_id: 2 | |||
|
53 | customized_id: 3 | |||
|
54 | id: 12 | |||
|
55 | value: "this is a stringforcustomfield search" | |||
|
56 | No newline at end of file |
@@ -5,7 +5,7 require 'search_controller' | |||||
5 | class SearchController; def rescue_action(e) raise e end; end |
|
5 | class SearchController; def rescue_action(e) raise e end; end | |
6 |
|
6 | |||
7 | class SearchControllerTest < Test::Unit::TestCase |
|
7 | class SearchControllerTest < Test::Unit::TestCase | |
8 | fixtures :projects, :issues |
|
8 | fixtures :projects, :issues, :custom_fields, :custom_values | |
9 |
|
9 | |||
10 | def setup |
|
10 | def setup | |
11 | @controller = SearchController.new |
|
11 | @controller = SearchController.new | |
@@ -25,7 +25,9 class SearchControllerTest < Test::Unit::TestCase | |||||
25 | assert assigns(:results).include?(Project.find(1)) |
|
25 | assert assigns(:results).include?(Project.find(1)) | |
26 | end |
|
26 | end | |
27 |
|
27 | |||
28 | def test_search_in_project |
|
28 | def test_search_without_searchable_custom_fields | |
|
29 | CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}" | |||
|
30 | ||||
29 | get :index, :id => 1 |
|
31 | get :index, :id => 1 | |
30 | assert_response :success |
|
32 | assert_response :success | |
31 | assert_template 'index' |
|
33 | assert_template 'index' | |
@@ -36,6 +38,15 class SearchControllerTest < Test::Unit::TestCase | |||||
36 | assert_template 'index' |
|
38 | assert_template 'index' | |
37 | end |
|
39 | end | |
38 |
|
40 | |||
|
41 | def test_search_with_searchable_custom_fields | |||
|
42 | get :index, :id => 1, :q => "stringforcustomfield" | |||
|
43 | assert_response :success | |||
|
44 | results = assigns(:results) | |||
|
45 | assert_not_nil results | |||
|
46 | assert_equal 1, results.size | |||
|
47 | assert results.include?(Issue.find(3)) | |||
|
48 | end | |||
|
49 | ||||
39 | def test_quick_jump_to_issue |
|
50 | def test_quick_jump_to_issue | |
40 | # issue of a public project |
|
51 | # issue of a public project | |
41 | get :index, :q => "3" |
|
52 | get :index, :q => "3" |
@@ -49,6 +49,9 module Redmine | |||||
49 | raise 'No date column defined defined.' |
|
49 | raise 'No date column defined defined.' | |
50 | end |
|
50 | end | |
51 |
|
51 | |||
|
52 | # Should we search custom fields on this model ? | |||
|
53 | searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? | |||
|
54 | ||||
52 | send :include, Redmine::Acts::Searchable::InstanceMethods |
|
55 | send :include, Redmine::Acts::Searchable::InstanceMethods | |
53 | end |
|
56 | end | |
54 | end |
|
57 | end | |
@@ -67,11 +70,27 module Redmine | |||||
67 | columns = searchable_options[:columns] |
|
70 | columns = searchable_options[:columns] | |
68 | columns.slice!(1..-1) if options[:titles_only] |
|
71 | columns.slice!(1..-1) if options[:titles_only] | |
69 |
|
72 | |||
70 | sql = ([ '(' + columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}.join(' OR ') + ')' ] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') |
|
73 | token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"} | |
|
74 | ||||
|
75 | if !options[:titles_only] && searchable_options[:search_custom_fields] | |||
|
76 | searchable_custom_field_ids = CustomField.find(:all, | |||
|
77 | :select => 'id', | |||
|
78 | :conditions => { :type => "#{self.name}CustomField", | |||
|
79 | :searchable => true }).collect(&:id) | |||
|
80 | if searchable_custom_field_ids.any? | |||
|
81 | custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" + | |||
|
82 | " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" + | |||
|
83 | " AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))" | |||
|
84 | token_clauses << custom_field_sql | |||
|
85 | end | |||
|
86 | end | |||
|
87 | ||||
|
88 | sql = ([token_clauses.join(' OR ')] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') | |||
|
89 | ||||
71 | if options[:offset] |
|
90 | if options[:offset] | |
72 | sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')" |
|
91 | sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')" | |
73 | end |
|
92 | end | |
74 |
find_options[:conditions] = [sql, * (tokens * |
|
93 | find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort] | |
75 |
|
94 | |||
76 | results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do |
|
95 | results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do | |
77 | find(:all, find_options) |
|
96 | find(:all, find_options) |
General Comments 0
You need to be logged in to leave comments.
Login now