##// END OF EJS Templates
Option to search attachment filenames and description (#4383)....
Jean-Philippe Lang -
r13474:576a13e99d50
parent child
Show More
@@ -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, :cache => params[:page].present?
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 => 'id', :permission => nil
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 sort_and_limit_results
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