Ability to search all projects or the projects the user belongs to (#791).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1435 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
parent
1907c31138
commit
073818f8bc
|
@ -29,6 +29,16 @@ class SearchController < ApplicationController
|
||||||
@all_words = params[:all_words] || (params[:submit] ? false : true)
|
@all_words = params[:all_words] || (params[:submit] ? false : true)
|
||||||
@titles_only = !params[:titles_only].nil?
|
@titles_only = !params[:titles_only].nil?
|
||||||
|
|
||||||
|
projects_to_search =
|
||||||
|
case params[:projects]
|
||||||
|
when 'all'
|
||||||
|
nil
|
||||||
|
when 'my_projects'
|
||||||
|
User.current.memberships.collect(&:project)
|
||||||
|
else
|
||||||
|
@project
|
||||||
|
end
|
||||||
|
|
||||||
offset = nil
|
offset = nil
|
||||||
begin; offset = params[:offset].to_time if params[:offset]; rescue; end
|
begin; offset = params[:offset].to_time if params[:offset]; rescue; end
|
||||||
|
|
||||||
|
@ -38,17 +48,17 @@ class SearchController < ApplicationController
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if @project
|
@object_types = %w(issues news documents changesets wiki_pages messages projects)
|
||||||
|
if projects_to_search.is_a? Project
|
||||||
|
# don't search projects
|
||||||
|
@object_types.delete('projects')
|
||||||
# only show what the user is allowed to view
|
# only show what the user is allowed to view
|
||||||
@object_types = %w(issues news documents changesets wiki_pages messages)
|
@object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
|
||||||
@object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
|
|
||||||
|
|
||||||
@scope = @object_types.select {|t| params[t]}
|
|
||||||
@scope = @object_types if @scope.empty?
|
|
||||||
else
|
|
||||||
@object_types = @scope = %w(projects)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@scope = @object_types.select {|t| params[t]}
|
||||||
|
@scope = @object_types if @scope.empty?
|
||||||
|
|
||||||
# extract tokens from the question
|
# extract tokens from the question
|
||||||
# eg. hello "bye bye" => ["hello", "bye bye"]
|
# eg. hello "bye bye" => ["hello", "bye bye"]
|
||||||
@tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
|
@tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
|
||||||
|
@ -62,37 +72,27 @@ class SearchController < ApplicationController
|
||||||
like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
|
like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
|
||||||
@results = []
|
@results = []
|
||||||
limit = 10
|
limit = 10
|
||||||
if @project
|
@scope.each do |s|
|
||||||
@scope.each do |s|
|
@results += s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
|
||||||
@results += s.singularize.camelcase.constantize.search(like_tokens, @project,
|
:all_words => @all_words,
|
||||||
:all_words => @all_words,
|
:titles_only => @titles_only,
|
||||||
:titles_only => @titles_only,
|
:limit => (limit+1),
|
||||||
:limit => (limit+1),
|
:offset => offset,
|
||||||
:offset => offset,
|
:before => params[:previous].nil?)
|
||||||
:before => params[:previous].nil?)
|
end
|
||||||
end
|
@results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
|
||||||
@results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
|
if params[:previous].nil?
|
||||||
if params[:previous].nil?
|
@pagination_previous_date = @results[0].event_datetime if offset && @results[0]
|
||||||
@pagination_previous_date = @results[0].event_datetime if offset && @results[0]
|
if @results.size > limit
|
||||||
if @results.size > limit
|
@pagination_next_date = @results[limit-1].event_datetime
|
||||||
@pagination_next_date = @results[limit-1].event_datetime
|
@results = @results[0, limit]
|
||||||
@results = @results[0, limit]
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
|
|
||||||
if @results.size > limit
|
|
||||||
@pagination_previous_date = @results[-(limit)].event_datetime
|
|
||||||
@results = @results[-(limit), limit]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
operator = @all_words ? ' AND ' : ' OR '
|
@pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
|
||||||
@results += Project.find(:all,
|
if @results.size > limit
|
||||||
:limit => limit,
|
@pagination_previous_date = @results[-(limit)].event_datetime
|
||||||
:conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort]
|
@results = @results[-(limit), limit]
|
||||||
) if @scope.include? 'projects'
|
end
|
||||||
# if only one project is found, user is redirected to its overview
|
|
||||||
redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@question = ""
|
@question = ""
|
||||||
|
|
|
@ -35,4 +35,11 @@ module SearchHelper
|
||||||
end
|
end
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def project_select_tag
|
||||||
|
options = [[l(:label_project_all), 'all']]
|
||||||
|
options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
|
||||||
|
options << [@project.name, ''] unless @project.nil?
|
||||||
|
select_tag('projects', options_for_select(options, params[:projects].to_s)) if options.size > 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Changeset < ActiveRecord::Base
|
||||||
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
|
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
|
||||||
|
|
||||||
acts_as_searchable :columns => 'comments',
|
acts_as_searchable :columns => 'comments',
|
||||||
:include => :repository,
|
:include => {:repository => :project},
|
||||||
:project_key => "#{Repository.table_name}.project_id",
|
:project_key => "#{Repository.table_name}.project_id",
|
||||||
:date_column => 'committed_on'
|
:date_column => 'committed_on'
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Document < ActiveRecord::Base
|
||||||
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
|
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
|
||||||
has_many :attachments, :as => :container, :dependent => :destroy
|
has_many :attachments, :as => :container, :dependent => :destroy
|
||||||
|
|
||||||
acts_as_searchable :columns => ['title', 'description']
|
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
|
||||||
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
|
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
|
||||||
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
|
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
|
||||||
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
|
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
|
||||||
|
|
|
@ -36,7 +36,7 @@ class Issue < ActiveRecord::Base
|
||||||
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
|
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
|
||||||
|
|
||||||
acts_as_watchable
|
acts_as_watchable
|
||||||
acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
|
acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue}
|
||||||
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
|
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
|
||||||
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
|
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ class Journal < ActiveRecord::Base
|
||||||
attr_accessor :indice
|
attr_accessor :indice
|
||||||
|
|
||||||
acts_as_searchable :columns => 'notes',
|
acts_as_searchable :columns => 'notes',
|
||||||
:include => :issue,
|
:include => {:issue => :project},
|
||||||
:project_key => "#{Issue.table_name}.project_id",
|
:project_key => "#{Issue.table_name}.project_id",
|
||||||
:date_column => "#{Issue.table_name}.created_on"
|
:date_column => "#{Issue.table_name}.created_on"
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,9 @@ class Message < ActiveRecord::Base
|
||||||
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
|
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
|
||||||
|
|
||||||
acts_as_searchable :columns => ['subject', 'content'],
|
acts_as_searchable :columns => ['subject', 'content'],
|
||||||
:include => :board,
|
:include => {:board, :project},
|
||||||
:project_key => 'project_id',
|
:project_key => 'project_id',
|
||||||
:date_column => 'created_on'
|
:date_column => "#{table_name}.created_on"
|
||||||
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
|
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
|
||||||
:description => :content,
|
:description => :content,
|
||||||
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
|
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
|
||||||
|
|
|
@ -24,7 +24,7 @@ class News < ActiveRecord::Base
|
||||||
validates_length_of :title, :maximum => 60
|
validates_length_of :title, :maximum => 60
|
||||||
validates_length_of :summary, :maximum => 255
|
validates_length_of :summary, :maximum => 255
|
||||||
|
|
||||||
acts_as_searchable :columns => ['title', 'description']
|
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
|
||||||
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
|
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
|
||||||
|
|
||||||
# returns latest news for projects visible by user
|
# returns latest news for projects visible by user
|
||||||
|
|
|
@ -46,7 +46,7 @@ class Project < ActiveRecord::Base
|
||||||
|
|
||||||
acts_as_tree :order => "name", :counter_cache => true
|
acts_as_tree :order => "name", :counter_cache => true
|
||||||
|
|
||||||
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
|
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
|
||||||
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
|
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
|
||||||
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
|
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
|
||||||
|
|
||||||
|
@ -202,6 +202,10 @@ class Project < ActiveRecord::Base
|
||||||
@all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
|
@all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def project
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
def <=>(project)
|
def <=>(project)
|
||||||
name.downcase <=> project.name.downcase
|
name.downcase <=> project.name.downcase
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ class WikiPage < ActiveRecord::Base
|
||||||
:url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
|
:url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
|
||||||
|
|
||||||
acts_as_searchable :columns => ['title', 'text'],
|
acts_as_searchable :columns => ['title', 'text'],
|
||||||
:include => [:wiki, :content],
|
:include => [{:wiki => :project}, :content],
|
||||||
:project_key => "#{Wiki.table_name}.project_id"
|
:project_key => "#{Wiki.table_name}.project_id"
|
||||||
|
|
||||||
attr_accessor :redirect_existing_links
|
attr_accessor :redirect_existing_links
|
||||||
|
|
|
@ -4,23 +4,25 @@
|
||||||
<% form_tag({}, :method => :get) do %>
|
<% form_tag({}, :method => :get) do %>
|
||||||
<p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
|
<p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
|
||||||
<%= javascript_tag "Field.focus('search-input')" %>
|
<%= javascript_tag "Field.focus('search-input')" %>
|
||||||
|
<%= project_select_tag %>
|
||||||
<% @object_types.each do |t| %>
|
|
||||||
<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label>
|
|
||||||
<% end %>
|
|
||||||
<br />
|
|
||||||
<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
|
<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
|
||||||
<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
|
<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
|
||||||
</p>
|
</p>
|
||||||
<%= submit_tag l(:button_submit), :name => 'submit' %>
|
<p>
|
||||||
|
<% @object_types.each do |t| %>
|
||||||
|
<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if @results %>
|
<% if @results %>
|
||||||
<h3><%= l(:label_result_plural) %></h3>
|
<h3><%= l(:label_result_plural) %></h3>
|
||||||
<ul>
|
<ul id="search-results">
|
||||||
<% @results.each do |e| %>
|
<% @results.each do |e| %>
|
||||||
<li><p><%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %><br />
|
<li><p><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %><br />
|
||||||
<%= highlight_tokens(e.event_description, @tokens) %><br />
|
<%= highlight_tokens(e.event_description, @tokens) %><br />
|
||||||
<span class="author"><%= format_time(e.event_datetime) %></span></p></li>
|
<span class="author"><%= format_time(e.event_datetime) %></span></p></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -177,7 +177,7 @@ div#activity dd { margin-bottom: 1em; padding-left: 18px; }
|
||||||
div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
|
div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
|
||||||
div#activity dt .time { color: #777; font-size: 80%; }
|
div#activity dt .time { color: #777; font-size: 80%; }
|
||||||
div#activity dd .description { font-style: italic; }
|
div#activity dd .description { font-style: italic; }
|
||||||
div#activity span.project:after { content: " -"; }
|
div#activity span.project:after, #search-results span.project:after { content: " -"; }
|
||||||
div#activity dt.issue { background-image: url(../images/ticket.png); }
|
div#activity dt.issue { background-image: url(../images/ticket.png); }
|
||||||
div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); }
|
div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); }
|
||||||
div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); }
|
div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); }
|
||||||
|
|
|
@ -5,7 +5,10 @@ require 'search_controller'
|
||||||
class SearchController; def rescue_action(e) raise e end; end
|
class SearchController; def rescue_action(e) raise e end; end
|
||||||
|
|
||||||
class SearchControllerTest < Test::Unit::TestCase
|
class SearchControllerTest < Test::Unit::TestCase
|
||||||
fixtures :projects, :enabled_modules, :issues, :custom_fields, :custom_values
|
fixtures :projects, :enabled_modules, :roles, :users,
|
||||||
|
:issues, :trackers, :issue_statuses,
|
||||||
|
:custom_fields, :custom_values,
|
||||||
|
:repositories, :changesets
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
@controller = SearchController.new
|
@controller = SearchController.new
|
||||||
|
@ -25,6 +28,15 @@ class SearchControllerTest < Test::Unit::TestCase
|
||||||
assert assigns(:results).include?(Project.find(1))
|
assert assigns(:results).include?(Project.find(1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_search_all_projects
|
||||||
|
get :index, :q => 'recipe subproject commit', :submit => 'Search'
|
||||||
|
assert_response :success
|
||||||
|
assert_template 'index'
|
||||||
|
assert assigns(:results).include?(Issue.find(2))
|
||||||
|
assert assigns(:results).include?(Issue.find(5))
|
||||||
|
assert assigns(:results).include?(Changeset.find(101))
|
||||||
|
end
|
||||||
|
|
||||||
def test_search_without_searchable_custom_fields
|
def test_search_without_searchable_custom_fields
|
||||||
CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
|
CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,9 @@ module Redmine
|
||||||
raise 'No date column defined defined.'
|
raise 'No date column defined defined.'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Permission needed to search this model
|
||||||
|
searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission)
|
||||||
|
|
||||||
# Should we search custom fields on this model ?
|
# Should we search custom fields on this model ?
|
||||||
searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
|
searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
|
||||||
|
|
||||||
|
@ -62,8 +65,12 @@ module Redmine
|
||||||
end
|
end
|
||||||
|
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
def search(tokens, project, options={})
|
# Search the model for the given tokens
|
||||||
|
# projects argument can be either nil (will search all projects), a project or an array of projects
|
||||||
|
def search(tokens, projects, options={})
|
||||||
tokens = [] << tokens unless tokens.is_a?(Array)
|
tokens = [] << tokens unless tokens.is_a?(Array)
|
||||||
|
projects = [] << projects unless projects.nil? || projects.is_a?(Array)
|
||||||
|
|
||||||
find_options = {:include => searchable_options[:include]}
|
find_options = {:include => searchable_options[:include]}
|
||||||
find_options[:limit] = options[:limit] if options[:limit]
|
find_options[:limit] = options[:limit] if options[:limit]
|
||||||
find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC')
|
find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC')
|
||||||
|
@ -92,12 +99,17 @@ module Redmine
|
||||||
end
|
end
|
||||||
find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort]
|
find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort]
|
||||||
|
|
||||||
results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do
|
project_conditions = []
|
||||||
|
project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
|
||||||
|
Project.allowed_to_condition(User.current, searchable_options[:permission]))
|
||||||
|
project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
|
||||||
|
|
||||||
|
results = with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do
|
||||||
find(:all, find_options)
|
find(:all, find_options)
|
||||||
end
|
end
|
||||||
if searchable_options[:with] && !options[:titles_only]
|
if searchable_options[:with] && !options[:titles_only]
|
||||||
searchable_options[:with].each do |model, assoc|
|
searchable_options[:with].each do |model, assoc|
|
||||||
results += model.to_s.camelcase.constantize.search(tokens, project, options).collect {|r| r.send assoc}
|
results += model.to_s.camelcase.constantize.search(tokens, projects, options).collect {|r| r.send assoc}
|
||||||
end
|
end
|
||||||
results.uniq!
|
results.uniq!
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue