##// 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,57 +84,64 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(
91
92 search_scope(user, projects).
92 if !options[:titles_only] && searchable_options[:search_custom_fields]
93 where(search_tokens_condition(columns, tokens, options[:all_words])),
93 searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a
94 options[:limit]
95 )
96 queries += 1
94
97
95 if searchable_custom_fields.any?
98 if !options[:titles_only] && searchable_options[:search_custom_fields]
96 fields_by_visibility = searchable_custom_fields.group_by {|field|
99 searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a
97 field.visibility_by_project_condition(searchable_options[:project_key], user, "#{CustomValue.table_name}.custom_field_id")
100
98 }
101 if searchable_custom_fields.any?
99 clauses = []
102 fields_by_visibility = searchable_custom_fields.group_by {|field|
100 fields_by_visibility.each do |visibility, fields|
103 field.visibility_by_project_condition(searchable_options[:project_key], user, "#{CustomValue.table_name}.custom_field_id")
101 clauses << "(#{CustomValue.table_name}.custom_field_id IN (#{fields.map(&:id).join(',')}) AND (#{visibility}))"
104 }
102 end
105 clauses = []
103 visibility = clauses.join(' OR ')
106 fields_by_visibility.each do |visibility, fields|
107 clauses << "(#{CustomValue.table_name}.custom_field_id IN (#{fields.map(&:id).join(',')}) AND (#{visibility}))"
108 end
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 ')
111 r |= fetch_ranks_and_ids(
106 tokens_conditions = [sql, * tokens.collect {|w| "%#{w}%"}.sort]
112 search_scope(user, projects).
113 joins(:custom_values).
114 where(visibility).
115 where(search_tokens_condition(["#{CustomValue.table_name}.value"], tokens, options[:all_words])),
116 options[:limit]
117 )
118 queries += 1
119 end
120 end
107
121
122 if !options[:titles_only] && searchable_options[:search_journals]
108 r |= fetch_ranks_and_ids(
123 r |= fetch_ranks_and_ids(
109 search_scope(user, projects).
124 search_scope(user, projects).
110 joins(:custom_values).
125 joins(:journals).
111 where(visibility).
126 where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false).
112 where(tokens_conditions),
127 where(search_tokens_condition(["#{Journal.table_name}.notes"], tokens, options[:all_words])),
113 options[:limit]
128 options[:limit]
114 )
129 )
115
130 queries += 1
116 sort_and_limit_results = true
117 end
131 end
118 end
132 end
119
133
120 if !options[:titles_only] && searchable_options[:search_journals]
134 if searchable_options[:search_attachments] && (options[:titles_only] ? options[:attachments] == 'only' : options[:attachments] != '0')
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(
135 r |= fetch_ranks_and_ids(
125 search_scope(user, projects).
136 search_scope(user, projects).
126 joins(:journals).
137 joins(:attachments).
127 where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false).
138 where(search_tokens_condition(["#{Attachment.table_name}.filename", "#{Attachment.table_name}.description"], tokens, options[:all_words])),
128 where(tokens_conditions),
129 options[:limit]
139 options[:limit]
130 )
140 )
131
141 queries += 1
132 sort_and_limit_results = true
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