]> source.dussan.org Git - redmine.git/commitdiff
Option to search attachment filenames and description (#4383).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Fri, 9 Jan 2015 21:06:09 +0000 (21:06 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Fri, 9 Jan 2015 21:06:09 +0000 (21:06 +0000)
git-svn-id: http://svn.redmine.org/redmine/trunk@13856 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/controllers/search_controller.rb
app/models/project.rb
app/views/search/index.html.erb
config/locales/en.yml
config/locales/fr.yml
lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb
public/stylesheets/application.css
test/functional/search_controller_test.rb

index 3f80b18b07370b01bb897e125845e314433e0070..e140878a9082dd891f9efcac7ad1b25d9d48a371 100644 (file)
@@ -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?
index fa035b02296a050ae519b642467fc71d5d69a401..53613b09776f4fea907fcd71905c7e85aa0a3bf5 100644 (file)
@@ -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
index 6bbe8d48253f8d2a0e31b4ef5095cd371eed361c..aaf08d3698f5c82877081db004802309dedaeb0a 100644 (file)
 <% end %>
 </p>
 
+<fieldset class="collapsible collapsed">
+  <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
+  <div id="options-content" style="display:none;">
+    <p>
+      <label><%= radio_button_tag 'attachments', '0', @search_attachments == '0' %> <%= l(:label_search_attachments_no) %></label>
+      <label><%= radio_button_tag 'attachments', '1', @search_attachments == '1' %> <%= l(:label_search_attachments_yes) %></label>
+      <label><%= radio_button_tag 'attachments', 'only', @search_attachments == 'only' %> <%= l(:label_search_attachments_only) %></label>
+    </p>
+  </div>
+</fieldset>
+<%= hidden_field_tag 'options', '', :id => 'show-options' %>
+
+</div>
 <p><%= submit_tag l(:button_submit) %></p>
 <% end %>
-</div>
 
 <% if @results %>
     <div id="search-results-counts">
@@ -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 %>
index 919d77861afe068fc31d839e52374a690ab2dd7c..c7d13cc0f3679469fb49d93f84456d95dc33e8b8 100644 (file)
@@ -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
index a167fa0e9c48abcd216dcb288e6cc6c37d07b397..26857d9c842e63e15442f5eb1f37b804b7d28a6e 100644 (file)
@@ -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
index 25d6e225f10af341b71ed721a90739a0bf58f1ed..2ffe79e0af66c017a373248615366e7edd77f0c5 100644 (file)
@@ -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
index 2e7e5c8994adf18cb745bd7489456232caa1d042..4b9a4ff4979cb157c342a5282111b23392b0b115 100644 (file)
@@ -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;
index 957ff1e574a744d1f0a218c3c52093790e412cf7..d57cc0757b79161bb625363b42a30198c249aa29 100644 (file)
@@ -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'