From 576a13e99d50a30cfcc9b78d4a0b3793990c0f40 Mon Sep 17 00:00:00 2001
From: Jean-Philippe Lang
Date: Fri, 9 Jan 2015 21:06:09 +0000
Subject: [PATCH] Option to search attachment filenames and description
(#4383).
git-svn-id: http://svn.redmine.org/redmine/trunk@13856 e93f8b46-1217-0410-a6f0-8f06a7374b81
---
app/controllers/search_controller.rb | 4 +-
app/models/project.rb | 2 +-
app/views/search/index.html.erb | 22 ++++-
config/locales/en.yml | 3 +
config/locales/fr.yml | 3 +
.../lib/acts_as_searchable.rb | 84 +++++++++++--------
public/stylesheets/application.css | 2 +
test/functional/search_controller_test.rb | 29 +++++++
8 files changed, 112 insertions(+), 37 deletions(-)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 3f80b18b0..e140878a9 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -23,6 +23,7 @@ class SearchController < ApplicationController
@question.strip!
@all_words = params[:all_words] ? params[:all_words].present? : true
@titles_only = params[:titles_only] ? params[:titles_only].present? : false
+ @search_attachments = params[:attachments].presence || '0'
# quick jump to an issue
if (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i))
@@ -55,7 +56,8 @@ class SearchController < ApplicationController
fetcher = Redmine::Search::Fetcher.new(
@question, User.current, @scope, projects_to_search,
- :all_words => @all_words, :titles_only => @titles_only, :cache => params[:page].present?
+ :all_words => @all_words, :titles_only => @titles_only, :attachments => @search_attachments,
+ :cache => params[:page].present?
)
if fetcher.tokens.present?
diff --git a/app/models/project.rb b/app/models/project.rb
index fa035b022..53613b097 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -64,7 +64,7 @@ class Project < ActiveRecord::Base
:delete_permission => :manage_files
acts_as_customizable
- acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
+ acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
:author => nil
diff --git a/app/views/search/index.html.erb b/app/views/search/index.html.erb
index 6bbe8d482..aaf08d369 100644
--- a/app/views/search/index.html.erb
+++ b/app/views/search/index.html.erb
@@ -17,9 +17,21 @@
<% end %>
+
+<%= hidden_field_tag 'options', '', :id => 'show-options' %>
+
+
<%= submit_tag l(:button_submit) %>
<% end %>
-
<% if @results %>
@@ -53,4 +65,12 @@ $("#search-types a").click(function(e){
$("#search-form").submit();
}
});
+
+$("#search-form").submit(function(){
+ $("#show-options").val($("#options-content").is(":visible") ? '1' : '0');
+});
+
+<% if params[:options] == '1' %>
+toggleFieldset($("#options-content"));
+<% end %>
<% end %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 919d77861..c7d13cc0f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -927,6 +927,9 @@ en:
label_edit_attachments: Edit attached files
label_link_copied_issue: Link copied issue
label_ask: Ask
+ label_search_attachments_yes: Search attachment filenames and descriptions
+ label_search_attachments_no: Do not search attachments
+ label_search_attachments_only: Search attachments only
button_login: Login
button_submit: Submit
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index a167fa0e9..26857d9c8 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -947,6 +947,9 @@ fr:
label_edit_attachments: Modifier les fichiers attachés
label_link_copied_issue: Lier la demande copiée
label_ask: Demander
+ label_search_attachments_yes: Rechercher les noms et descriptions de fichiers
+ label_search_attachments_no: Ne pas rechercher les fichiers
+ label_search_attachments_only: Rechercher les fichiers uniquement
button_login: Connexion
button_submit: Soumettre
diff --git a/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb
index 25d6e225f..2ffe79e0a 100644
--- a/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb
+++ b/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb
@@ -50,6 +50,7 @@ module Redmine
# Should we search additional associations on this model ?
searchable_options[:search_custom_fields] = reflect_on_association(:custom_values).present?
+ searchable_options[:search_attachments] = reflect_on_association(:attachments).present?
searchable_options[:search_journals] = reflect_on_association(:journals).present?
send :include, Redmine::Acts::Searchable::InstanceMethods
@@ -70,6 +71,7 @@ module Redmine
# Valid options:
# * :titles_only - searches tokens in the first searchable column only
# * :all_words - searches results that match all token
+ # * :
# * :limit - maximum number of results to return
#
# Example:
@@ -82,57 +84,64 @@ module Redmine
columns = searchable_options[:columns]
columns = columns[0..0] if options[:titles_only]
- token_clauses = columns.collect {|column| "(#{search_token_match_statement(column)})"}
- sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
- tokens_conditions = [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort]
+ r = []
+ queries = 0
- r = fetch_ranks_and_ids(search_scope(user, projects).where(tokens_conditions), options[:limit])
- sort_and_limit_results = false
-
- if !options[:titles_only] && searchable_options[:search_custom_fields]
- searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a
+ unless options[:attachments] == 'only'
+ r = fetch_ranks_and_ids(
+ search_scope(user, projects).
+ where(search_tokens_condition(columns, tokens, options[:all_words])),
+ options[:limit]
+ )
+ queries += 1
- if searchable_custom_fields.any?
- fields_by_visibility = searchable_custom_fields.group_by {|field|
- field.visibility_by_project_condition(searchable_options[:project_key], user, "#{CustomValue.table_name}.custom_field_id")
- }
- clauses = []
- fields_by_visibility.each do |visibility, fields|
- clauses << "(#{CustomValue.table_name}.custom_field_id IN (#{fields.map(&:id).join(',')}) AND (#{visibility}))"
- end
- visibility = clauses.join(' OR ')
+ if !options[:titles_only] && searchable_options[:search_custom_fields]
+ searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a
+
+ if searchable_custom_fields.any?
+ fields_by_visibility = searchable_custom_fields.group_by {|field|
+ field.visibility_by_project_condition(searchable_options[:project_key], user, "#{CustomValue.table_name}.custom_field_id")
+ }
+ clauses = []
+ fields_by_visibility.each do |visibility, fields|
+ clauses << "(#{CustomValue.table_name}.custom_field_id IN (#{fields.map(&:id).join(',')}) AND (#{visibility}))"
+ end
+ visibility = clauses.join(' OR ')
- sql = ([search_token_match_statement("#{CustomValue.table_name}.value")] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
- tokens_conditions = [sql, * tokens.collect {|w| "%#{w}%"}.sort]
+ r |= fetch_ranks_and_ids(
+ search_scope(user, projects).
+ joins(:custom_values).
+ where(visibility).
+ where(search_tokens_condition(["#{CustomValue.table_name}.value"], tokens, options[:all_words])),
+ options[:limit]
+ )
+ queries += 1
+ end
+ end
+ if !options[:titles_only] && searchable_options[:search_journals]
r |= fetch_ranks_and_ids(
search_scope(user, projects).
- joins(:custom_values).
- where(visibility).
- where(tokens_conditions),
+ joins(:journals).
+ where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false).
+ where(search_tokens_condition(["#{Journal.table_name}.notes"], tokens, options[:all_words])),
options[:limit]
)
-
- sort_and_limit_results = true
+ queries += 1
end
end
- if !options[:titles_only] && searchable_options[:search_journals]
- sql = ([search_token_match_statement("#{Journal.table_name}.notes")] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
- tokens_conditions = [sql, * tokens.collect {|w| "%#{w}%"}.sort]
-
+ if searchable_options[:search_attachments] && (options[:titles_only] ? options[:attachments] == 'only' : options[:attachments] != '0')
r |= fetch_ranks_and_ids(
search_scope(user, projects).
- joins(:journals).
- where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false).
- where(tokens_conditions),
+ joins(:attachments).
+ where(search_tokens_condition(["#{Attachment.table_name}.filename", "#{Attachment.table_name}.description"], tokens, options[:all_words])),
options[:limit]
)
-
- sort_and_limit_results = true
+ queries += 1
end
- if sort_and_limit_results
+ if queries > 1
r = r.sort.reverse
if options[:limit] && r.size > options[:limit]
r = r[0, options[:limit]]
@@ -142,6 +151,13 @@ module Redmine
r
end
+ def search_tokens_condition(columns, tokens, all_words)
+ token_clauses = columns.map {|column| "(#{search_token_match_statement(column)})"}
+ sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(all_words ? ' AND ' : ' OR ')
+ [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort]
+ end
+ private :search_tokens_condition
+
def search_token_match_statement(column, value='?')
case connection.adapter_name
when /postgresql/i
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css
index 2e7e5c899..4b9a4ff49 100644
--- a/public/stylesheets/application.css
+++ b/public/stylesheets/application.css
@@ -473,6 +473,8 @@ input#content_comments {width: 99%}
p.pagination {margin-top:8px; font-size: 90%}
+#search-form fieldset p {margin:0.2em 0;}
+
/***** Tabular forms ******/
.tabular p{
margin: 0;
diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb
index 957ff1e57..d57cc0757 100644
--- a/test/functional/search_controller_test.rb
+++ b/test/functional/search_controller_test.rb
@@ -180,6 +180,35 @@ class SearchControllerTest < ActionController::TestCase
assert results.include?(Issue.find(7))
end
+ def test_search_without_attachments
+ issue = Issue.generate! :subject => 'search_attachments'
+ attachment = Attachment.generate! :container => Issue.find(1), :filename => 'search_attachments.patch'
+
+ get :index, :id => 1, :q => 'search_attachments', :attachments => '0'
+ results = assigns(:results)
+ assert_equal 1, results.size
+ assert_equal issue, results.first
+ end
+
+ def test_search_attachments_only
+ issue = Issue.generate! :subject => 'search_attachments'
+ attachment = Attachment.generate! :container => Issue.find(1), :filename => 'search_attachments.patch'
+
+ get :index, :id => 1, :q => 'search_attachments', :attachments => 'only'
+ results = assigns(:results)
+ assert_equal 1, results.size
+ assert_equal attachment.container, results.first
+ end
+
+ def test_search_with_attachments
+ Issue.generate! :subject => 'search_attachments'
+ Attachment.generate! :container => Issue.find(1), :filename => 'search_attachments.patch'
+
+ get :index, :id => 1, :q => 'search_attachments', :attachments => '1'
+ results = assigns(:results)
+ assert_equal 2, results.size
+ end
+
def test_search_all_words
# 'all words' is on by default
get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1'
--
2.39.5