@@ -23,6 +23,7 class SearchController < ApplicationController | |||||
23 | @question.strip! |
|
23 | @question.strip! | |
24 | @all_words = params[:all_words] ? params[:all_words].present? : true |
|
24 | @all_words = params[:all_words] ? params[:all_words].present? : true | |
25 | @titles_only = params[:titles_only] ? params[:titles_only].present? : false |
|
25 | @titles_only = params[:titles_only] ? params[:titles_only].present? : false | |
|
26 | @search_attachments = params[:attachments].presence || '0' | |||
26 |
|
27 | |||
27 | # quick jump to an issue |
|
28 | # quick jump to an issue | |
28 | if (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i)) |
|
29 | if (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i)) | |
@@ -55,7 +56,8 class SearchController < ApplicationController | |||||
55 |
|
56 | |||
56 | fetcher = Redmine::Search::Fetcher.new( |
|
57 | fetcher = Redmine::Search::Fetcher.new( | |
57 | @question, User.current, @scope, projects_to_search, |
|
58 | @question, User.current, @scope, projects_to_search, | |
58 |
:all_words => @all_words, :titles_only => @titles_only, : |
|
59 | :all_words => @all_words, :titles_only => @titles_only, :attachments => @search_attachments, | |
|
60 | :cache => params[:page].present? | |||
59 | ) |
|
61 | ) | |
60 |
|
62 | |||
61 | if fetcher.tokens.present? |
|
63 | if fetcher.tokens.present? |
@@ -64,7 +64,7 class Project < ActiveRecord::Base | |||||
64 | :delete_permission => :manage_files |
|
64 | :delete_permission => :manage_files | |
65 |
|
65 | |||
66 | acts_as_customizable |
|
66 | acts_as_customizable | |
67 |
acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => |
|
67 | acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil | |
68 | acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, |
|
68 | acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, | |
69 | :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}}, |
|
69 | :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}}, | |
70 | :author => nil |
|
70 | :author => nil |
@@ -17,9 +17,21 | |||||
17 | <% end %> |
|
17 | <% end %> | |
18 | </p> |
|
18 | </p> | |
19 |
|
19 | |||
|
20 | <fieldset class="collapsible collapsed"> | |||
|
21 | <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend> | |||
|
22 | <div id="options-content" style="display:none;"> | |||
|
23 | <p> | |||
|
24 | <label><%= radio_button_tag 'attachments', '0', @search_attachments == '0' %> <%= l(:label_search_attachments_no) %></label> | |||
|
25 | <label><%= radio_button_tag 'attachments', '1', @search_attachments == '1' %> <%= l(:label_search_attachments_yes) %></label> | |||
|
26 | <label><%= radio_button_tag 'attachments', 'only', @search_attachments == 'only' %> <%= l(:label_search_attachments_only) %></label> | |||
|
27 | </p> | |||
|
28 | </div> | |||
|
29 | </fieldset> | |||
|
30 | <%= hidden_field_tag 'options', '', :id => 'show-options' %> | |||
|
31 | ||||
|
32 | </div> | |||
20 | <p><%= submit_tag l(:button_submit) %></p> |
|
33 | <p><%= submit_tag l(:button_submit) %></p> | |
21 | <% end %> |
|
34 | <% end %> | |
22 | </div> |
|
|||
23 |
|
35 | |||
24 | <% if @results %> |
|
36 | <% if @results %> | |
25 | <div id="search-results-counts"> |
|
37 | <div id="search-results-counts"> | |
@@ -53,4 +65,12 $("#search-types a").click(function(e){ | |||||
53 | $("#search-form").submit(); |
|
65 | $("#search-form").submit(); | |
54 | } |
|
66 | } | |
55 | }); |
|
67 | }); | |
|
68 | ||||
|
69 | $("#search-form").submit(function(){ | |||
|
70 | $("#show-options").val($("#options-content").is(":visible") ? '1' : '0'); | |||
|
71 | }); | |||
|
72 | ||||
|
73 | <% if params[:options] == '1' %> | |||
|
74 | toggleFieldset($("#options-content")); | |||
|
75 | <% end %> | |||
56 | <% end %> |
|
76 | <% end %> |
@@ -927,6 +927,9 en: | |||||
927 | label_edit_attachments: Edit attached files |
|
927 | label_edit_attachments: Edit attached files | |
928 | label_link_copied_issue: Link copied issue |
|
928 | label_link_copied_issue: Link copied issue | |
929 | label_ask: Ask |
|
929 | label_ask: Ask | |
|
930 | label_search_attachments_yes: Search attachment filenames and descriptions | |||
|
931 | label_search_attachments_no: Do not search attachments | |||
|
932 | label_search_attachments_only: Search attachments only | |||
930 |
|
933 | |||
931 | button_login: Login |
|
934 | button_login: Login | |
932 | button_submit: Submit |
|
935 | button_submit: Submit |
@@ -947,6 +947,9 fr: | |||||
947 | label_edit_attachments: Modifier les fichiers attachΓ©s |
|
947 | label_edit_attachments: Modifier les fichiers attachΓ©s | |
948 | label_link_copied_issue: Lier la demande copiΓ©e |
|
948 | label_link_copied_issue: Lier la demande copiΓ©e | |
949 | label_ask: Demander |
|
949 | label_ask: Demander | |
|
950 | label_search_attachments_yes: Rechercher les noms et descriptions de fichiers | |||
|
951 | label_search_attachments_no: Ne pas rechercher les fichiers | |||
|
952 | label_search_attachments_only: Rechercher les fichiers uniquement | |||
950 |
|
953 | |||
951 | button_login: Connexion |
|
954 | button_login: Connexion | |
952 | button_submit: Soumettre |
|
955 | button_submit: Soumettre |
@@ -50,6 +50,7 module Redmine | |||||
50 |
|
50 | |||
51 | # Should we search additional associations on this model ? |
|
51 | # Should we search additional associations on this model ? | |
52 | searchable_options[:search_custom_fields] = reflect_on_association(:custom_values).present? |
|
52 | searchable_options[:search_custom_fields] = reflect_on_association(:custom_values).present? | |
|
53 | searchable_options[:search_attachments] = reflect_on_association(:attachments).present? | |||
53 | searchable_options[:search_journals] = reflect_on_association(:journals).present? |
|
54 | searchable_options[:search_journals] = reflect_on_association(:journals).present? | |
54 |
|
55 | |||
55 | send :include, Redmine::Acts::Searchable::InstanceMethods |
|
56 | send :include, Redmine::Acts::Searchable::InstanceMethods | |
@@ -70,6 +71,7 module Redmine | |||||
70 | # Valid options: |
|
71 | # Valid options: | |
71 | # * :titles_only - searches tokens in the first searchable column only |
|
72 | # * :titles_only - searches tokens in the first searchable column only | |
72 | # * :all_words - searches results that match all token |
|
73 | # * :all_words - searches results that match all token | |
|
74 | # * : | |||
73 | # * :limit - maximum number of results to return |
|
75 | # * :limit - maximum number of results to return | |
74 | # |
|
76 | # | |
75 | # Example: |
|
77 | # Example: | |
@@ -82,12 +84,16 module Redmine | |||||
82 | columns = searchable_options[:columns] |
|
84 | columns = searchable_options[:columns] | |
83 | columns = columns[0..0] if options[:titles_only] |
|
85 | columns = columns[0..0] if options[:titles_only] | |
84 |
|
86 | |||
85 | token_clauses = columns.collect {|column| "(#{search_token_match_statement(column)})"} |
|
87 | r = [] | |
86 | sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') |
|
88 | queries = 0 | |
87 | tokens_conditions = [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort] |
|
|||
88 |
|
89 | |||
89 | r = fetch_ranks_and_ids(search_scope(user, projects).where(tokens_conditions), options[:limit]) |
|
90 | unless options[:attachments] == 'only' | |
90 | sort_and_limit_results = false |
|
91 | r = fetch_ranks_and_ids( | |
|
92 | search_scope(user, projects). | |||
|
93 | where(search_tokens_condition(columns, tokens, options[:all_words])), | |||
|
94 | options[:limit] | |||
|
95 | ) | |||
|
96 | queries += 1 | |||
91 |
|
97 | |||
92 | if !options[:titles_only] && searchable_options[:search_custom_fields] |
|
98 | if !options[:titles_only] && searchable_options[:search_custom_fields] | |
93 | searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a |
|
99 | searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a | |
@@ -102,37 +108,40 module Redmine | |||||
102 | end |
|
108 | end | |
103 | visibility = clauses.join(' OR ') |
|
109 | visibility = clauses.join(' OR ') | |
104 |
|
110 | |||
105 | sql = ([search_token_match_statement("#{CustomValue.table_name}.value")] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') |
|
|||
106 | tokens_conditions = [sql, * tokens.collect {|w| "%#{w}%"}.sort] |
|
|||
107 |
|
||||
108 | r |= fetch_ranks_and_ids( |
|
111 | r |= fetch_ranks_and_ids( | |
109 | search_scope(user, projects). |
|
112 | search_scope(user, projects). | |
110 | joins(:custom_values). |
|
113 | joins(:custom_values). | |
111 | where(visibility). |
|
114 | where(visibility). | |
112 | where(tokens_conditions), |
|
115 | where(search_tokens_condition(["#{CustomValue.table_name}.value"], tokens, options[:all_words])), | |
113 | options[:limit] |
|
116 | options[:limit] | |
114 | ) |
|
117 | ) | |
115 |
|
118 | queries += 1 | ||
116 | sort_and_limit_results = true |
|
|||
117 | end |
|
119 | end | |
118 | end |
|
120 | end | |
119 |
|
121 | |||
120 | if !options[:titles_only] && searchable_options[:search_journals] |
|
122 | if !options[:titles_only] && searchable_options[:search_journals] | |
121 | sql = ([search_token_match_statement("#{Journal.table_name}.notes")] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') |
|
|||
122 | tokens_conditions = [sql, * tokens.collect {|w| "%#{w}%"}.sort] |
|
|||
123 |
|
||||
124 | r |= fetch_ranks_and_ids( |
|
123 | r |= fetch_ranks_and_ids( | |
125 | search_scope(user, projects). |
|
124 | search_scope(user, projects). | |
126 | joins(:journals). |
|
125 | joins(:journals). | |
127 | where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false). |
|
126 | where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false). | |
128 | where(tokens_conditions), |
|
127 | where(search_tokens_condition(["#{Journal.table_name}.notes"], tokens, options[:all_words])), | |
129 | options[:limit] |
|
128 | options[:limit] | |
130 | ) |
|
129 | ) | |
|
130 | queries += 1 | |||
|
131 | end | |||
|
132 | end | |||
131 |
|
133 | |||
132 | sort_and_limit_results = true |
|
134 | if searchable_options[:search_attachments] && (options[:titles_only] ? options[:attachments] == 'only' : options[:attachments] != '0') | |
|
135 | r |= fetch_ranks_and_ids( | |||
|
136 | search_scope(user, projects). | |||
|
137 | joins(:attachments). | |||
|
138 | where(search_tokens_condition(["#{Attachment.table_name}.filename", "#{Attachment.table_name}.description"], tokens, options[:all_words])), | |||
|
139 | options[:limit] | |||
|
140 | ) | |||
|
141 | queries += 1 | |||
133 | end |
|
142 | end | |
134 |
|
143 | |||
135 |
if |
|
144 | if queries > 1 | |
136 | r = r.sort.reverse |
|
145 | r = r.sort.reverse | |
137 | if options[:limit] && r.size > options[:limit] |
|
146 | if options[:limit] && r.size > options[:limit] | |
138 | r = r[0, options[:limit]] |
|
147 | r = r[0, options[:limit]] | |
@@ -142,6 +151,13 module Redmine | |||||
142 | r |
|
151 | r | |
143 | end |
|
152 | end | |
144 |
|
153 | |||
|
154 | def search_tokens_condition(columns, tokens, all_words) | |||
|
155 | token_clauses = columns.map {|column| "(#{search_token_match_statement(column)})"} | |||
|
156 | sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(all_words ? ' AND ' : ' OR ') | |||
|
157 | [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort] | |||
|
158 | end | |||
|
159 | private :search_tokens_condition | |||
|
160 | ||||
145 | def search_token_match_statement(column, value='?') |
|
161 | def search_token_match_statement(column, value='?') | |
146 | case connection.adapter_name |
|
162 | case connection.adapter_name | |
147 | when /postgresql/i |
|
163 | when /postgresql/i |
@@ -473,6 +473,8 input#content_comments {width: 99%} | |||||
473 |
|
473 | |||
474 | p.pagination {margin-top:8px; font-size: 90%} |
|
474 | p.pagination {margin-top:8px; font-size: 90%} | |
475 |
|
475 | |||
|
476 | #search-form fieldset p {margin:0.2em 0;} | |||
|
477 | ||||
476 | /***** Tabular forms ******/ |
|
478 | /***** Tabular forms ******/ | |
477 | .tabular p{ |
|
479 | .tabular p{ | |
478 | margin: 0; |
|
480 | margin: 0; |
@@ -180,6 +180,35 class SearchControllerTest < ActionController::TestCase | |||||
180 | assert results.include?(Issue.find(7)) |
|
180 | assert results.include?(Issue.find(7)) | |
181 | end |
|
181 | end | |
182 |
|
182 | |||
|
183 | def test_search_without_attachments | |||
|
184 | issue = Issue.generate! :subject => 'search_attachments' | |||
|
185 | attachment = Attachment.generate! :container => Issue.find(1), :filename => 'search_attachments.patch' | |||
|
186 | ||||
|
187 | get :index, :id => 1, :q => 'search_attachments', :attachments => '0' | |||
|
188 | results = assigns(:results) | |||
|
189 | assert_equal 1, results.size | |||
|
190 | assert_equal issue, results.first | |||
|
191 | end | |||
|
192 | ||||
|
193 | def test_search_attachments_only | |||
|
194 | issue = Issue.generate! :subject => 'search_attachments' | |||
|
195 | attachment = Attachment.generate! :container => Issue.find(1), :filename => 'search_attachments.patch' | |||
|
196 | ||||
|
197 | get :index, :id => 1, :q => 'search_attachments', :attachments => 'only' | |||
|
198 | results = assigns(:results) | |||
|
199 | assert_equal 1, results.size | |||
|
200 | assert_equal attachment.container, results.first | |||
|
201 | end | |||
|
202 | ||||
|
203 | def test_search_with_attachments | |||
|
204 | Issue.generate! :subject => 'search_attachments' | |||
|
205 | Attachment.generate! :container => Issue.find(1), :filename => 'search_attachments.patch' | |||
|
206 | ||||
|
207 | get :index, :id => 1, :q => 'search_attachments', :attachments => '1' | |||
|
208 | results = assigns(:results) | |||
|
209 | assert_equal 2, results.size | |||
|
210 | end | |||
|
211 | ||||
183 | def test_search_all_words |
|
212 | def test_search_all_words | |
184 | # 'all words' is on by default |
|
213 | # 'all words' is on by default | |
185 | get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1' |
|
214 | get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1' |
General Comments 0
You need to be logged in to leave comments.
Login now