From: Jean-Philippe Lang Date: Fri, 9 Jan 2015 21:06:09 +0000 (+0000) Subject: Option to search attachment filenames and description (#4383). X-Git-Tag: 3.0.0~156 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=576a13e99d50a30cfcc9b78d4a0b3793990c0f40;p=redmine.git Option to search attachment filenames and description (#4383). git-svn-id: http://svn.redmine.org/redmine/trunk@13856 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- 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'