@@ -29,6 +29,16 class SearchController < ApplicationController | |||||
29 | @all_words = params[:all_words] || (params[:submit] ? false : true) |
|
29 | @all_words = params[:all_words] || (params[:submit] ? false : true) | |
30 | @titles_only = !params[:titles_only].nil? |
|
30 | @titles_only = !params[:titles_only].nil? | |
31 |
|
31 | |||
|
32 | projects_to_search = | |||
|
33 | case params[:projects] | |||
|
34 | when 'all' | |||
|
35 | nil | |||
|
36 | when 'my_projects' | |||
|
37 | User.current.memberships.collect(&:project) | |||
|
38 | else | |||
|
39 | @project | |||
|
40 | end | |||
|
41 | ||||
32 | offset = nil |
|
42 | offset = nil | |
33 | begin; offset = params[:offset].to_time if params[:offset]; rescue; end |
|
43 | begin; offset = params[:offset].to_time if params[:offset]; rescue; end | |
34 |
|
44 | |||
@@ -38,16 +48,16 class SearchController < ApplicationController | |||||
38 | return |
|
48 | return | |
39 | end |
|
49 | end | |
40 |
|
50 | |||
41 | if @project |
|
51 | @object_types = %w(issues news documents changesets wiki_pages messages projects) | |
|
52 | if projects_to_search.is_a? Project | |||
|
53 | # don't search projects | |||
|
54 | @object_types.delete('projects') | |||
42 | # only show what the user is allowed to view |
|
55 | # only show what the user is allowed to view | |
43 | @object_types = %w(issues news documents changesets wiki_pages messages) |
|
56 | @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)} | |
44 | @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)} |
|
57 | end | |
45 |
|
58 | |||
46 |
|
|
59 | @scope = @object_types.select {|t| params[t]} | |
47 |
|
|
60 | @scope = @object_types if @scope.empty? | |
48 | else |
|
|||
49 | @object_types = @scope = %w(projects) |
|
|||
50 | end |
|
|||
51 |
|
61 | |||
52 | # extract tokens from the question |
|
62 | # extract tokens from the question | |
53 | # eg. hello "bye bye" => ["hello", "bye bye"] |
|
63 | # eg. hello "bye bye" => ["hello", "bye bye"] | |
@@ -62,9 +72,8 class SearchController < ApplicationController | |||||
62 | like_tokens = @tokens.collect {|w| "%#{w.downcase}%"} |
|
72 | like_tokens = @tokens.collect {|w| "%#{w.downcase}%"} | |
63 | @results = [] |
|
73 | @results = [] | |
64 | limit = 10 |
|
74 | limit = 10 | |
65 | if @project |
|
|||
66 |
|
|
75 | @scope.each do |s| | |
67 |
|
|
76 | @results += s.singularize.camelcase.constantize.search(like_tokens, projects_to_search, | |
68 |
|
|
77 | :all_words => @all_words, | |
69 |
|
|
78 | :titles_only => @titles_only, | |
70 |
|
|
79 | :limit => (limit+1), | |
@@ -86,15 +95,6 class SearchController < ApplicationController | |||||
86 |
|
|
95 | end | |
87 |
|
|
96 | end | |
88 |
|
|
97 | else | |
89 | operator = @all_words ? ' AND ' : ' OR ' |
|
|||
90 | @results += Project.find(:all, |
|
|||
91 | :limit => limit, |
|
|||
92 | :conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] |
|
|||
93 | ) if @scope.include? 'projects' |
|
|||
94 | # if only one project is found, user is redirected to its overview |
|
|||
95 | redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1 |
|
|||
96 | end |
|
|||
97 | else |
|
|||
98 | @question = "" |
|
98 | @question = "" | |
99 | end |
|
99 | end | |
100 | render :layout => false if request.xhr? |
|
100 | render :layout => false if request.xhr? |
@@ -35,4 +35,11 module SearchHelper | |||||
35 | end |
|
35 | end | |
36 | result |
|
36 | result | |
37 | end |
|
37 | end | |
|
38 | ||||
|
39 | def project_select_tag | |||
|
40 | options = [[l(:label_project_all), 'all']] | |||
|
41 | options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? | |||
|
42 | options << [@project.name, ''] unless @project.nil? | |||
|
43 | select_tag('projects', options_for_select(options, params[:projects].to_s)) if options.size > 1 | |||
|
44 | end | |||
38 | end |
|
45 | end |
@@ -27,7 +27,7 class Changeset < ActiveRecord::Base | |||||
27 | :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}} |
|
27 | :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}} | |
28 |
|
28 | |||
29 | acts_as_searchable :columns => 'comments', |
|
29 | acts_as_searchable :columns => 'comments', | |
30 | :include => :repository, |
|
30 | :include => {:repository => :project}, | |
31 | :project_key => "#{Repository.table_name}.project_id", |
|
31 | :project_key => "#{Repository.table_name}.project_id", | |
32 | :date_column => 'committed_on' |
|
32 | :date_column => 'committed_on' | |
33 |
|
33 |
@@ -20,7 +20,7 class Document < ActiveRecord::Base | |||||
20 | belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id" |
|
20 | belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id" | |
21 | has_many :attachments, :as => :container, :dependent => :destroy |
|
21 | has_many :attachments, :as => :container, :dependent => :destroy | |
22 |
|
22 | |||
23 |
acts_as_searchable :columns => ['title', |
|
23 | acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project | |
24 | acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, |
|
24 | acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, | |
25 | :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil }, |
|
25 | :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil }, | |
26 | :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} |
|
26 | :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} |
@@ -36,7 +36,7 class Issue < ActiveRecord::Base | |||||
36 | has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all |
|
36 | has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all | |
37 |
|
37 | |||
38 | acts_as_watchable |
|
38 | acts_as_watchable | |
39 |
acts_as_searchable :columns => ['subject', |
|
39 | acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue} | |
40 | acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, |
|
40 | acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, | |
41 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} |
|
41 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} | |
42 |
|
42 |
@@ -26,7 +26,7 class Journal < ActiveRecord::Base | |||||
26 | attr_accessor :indice |
|
26 | attr_accessor :indice | |
27 |
|
27 | |||
28 | acts_as_searchable :columns => 'notes', |
|
28 | acts_as_searchable :columns => 'notes', | |
29 | :include => :issue, |
|
29 | :include => {:issue => :project}, | |
30 | :project_key => "#{Issue.table_name}.project_id", |
|
30 | :project_key => "#{Issue.table_name}.project_id", | |
31 | :date_column => "#{Issue.table_name}.created_on" |
|
31 | :date_column => "#{Issue.table_name}.created_on" | |
32 |
|
32 |
@@ -23,9 +23,9 class Message < ActiveRecord::Base | |||||
23 | belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' |
|
23 | belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' | |
24 |
|
24 | |||
25 | acts_as_searchable :columns => ['subject', 'content'], |
|
25 | acts_as_searchable :columns => ['subject', 'content'], | |
26 | :include => :board, |
|
26 | :include => {:board, :project}, | |
27 | :project_key => 'project_id', |
|
27 | :project_key => 'project_id', | |
28 |
:date_column => |
|
28 | :date_column => "#{table_name}.created_on" | |
29 | acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, |
|
29 | acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, | |
30 | :description => :content, |
|
30 | :description => :content, | |
31 | :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, |
|
31 | :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, |
@@ -24,7 +24,7 class News < ActiveRecord::Base | |||||
24 | validates_length_of :title, :maximum => 60 |
|
24 | validates_length_of :title, :maximum => 60 | |
25 | validates_length_of :summary, :maximum => 255 |
|
25 | validates_length_of :summary, :maximum => 255 | |
26 |
|
26 | |||
27 |
acts_as_searchable :columns => ['title', |
|
27 | acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project | |
28 | acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} |
|
28 | acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} | |
29 |
|
29 | |||
30 | # returns latest news for projects visible by user |
|
30 | # returns latest news for projects visible by user |
@@ -46,7 +46,7 class Project < ActiveRecord::Base | |||||
46 |
|
46 | |||
47 | acts_as_tree :order => "name", :counter_cache => true |
|
47 | acts_as_tree :order => "name", :counter_cache => true | |
48 |
|
48 | |||
49 | acts_as_searchable :columns => ['name', 'description'], :project_key => 'id' |
|
49 | acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil | |
50 | acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, |
|
50 | acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, | |
51 | :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}} |
|
51 | :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}} | |
52 |
|
52 | |||
@@ -202,6 +202,10 class Project < ActiveRecord::Base | |||||
202 | @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq |
|
202 | @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq | |
203 | end |
|
203 | end | |
204 |
|
204 | |||
|
205 | def project | |||
|
206 | self | |||
|
207 | end | |||
|
208 | ||||
205 | def <=>(project) |
|
209 | def <=>(project) | |
206 | name.downcase <=> project.name.downcase |
|
210 | name.downcase <=> project.name.downcase | |
207 | end |
|
211 | end |
@@ -29,7 +29,7 class WikiPage < ActiveRecord::Base | |||||
29 | :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}} |
|
29 | :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}} | |
30 |
|
30 | |||
31 | acts_as_searchable :columns => ['title', 'text'], |
|
31 | acts_as_searchable :columns => ['title', 'text'], | |
32 | :include => [:wiki, :content], |
|
32 | :include => [{:wiki => :project}, :content], | |
33 | :project_key => "#{Wiki.table_name}.project_id" |
|
33 | :project_key => "#{Wiki.table_name}.project_id" | |
34 |
|
34 | |||
35 | attr_accessor :redirect_existing_links |
|
35 | attr_accessor :redirect_existing_links |
@@ -4,23 +4,25 | |||||
4 | <% form_tag({}, :method => :get) do %> |
|
4 | <% form_tag({}, :method => :get) do %> | |
5 | <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %> |
|
5 | <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %> | |
6 | <%= javascript_tag "Field.focus('search-input')" %> |
|
6 | <%= javascript_tag "Field.focus('search-input')" %> | |
7 |
|
7 | <%= project_select_tag %> | ||
|
8 | <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label> | |||
|
9 | <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label> | |||
|
10 | </p> | |||
|
11 | <p> | |||
8 | <% @object_types.each do |t| %> |
|
12 | <% @object_types.each do |t| %> | |
9 | <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label> |
|
13 | <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label> | |
10 | <% end %> |
|
14 | <% end %> | |
11 | <br /> |
|
|||
12 | <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label> |
|
|||
13 | <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label> |
|
|||
14 | </p> |
|
15 | </p> | |
15 | <%= submit_tag l(:button_submit), :name => 'submit' %> |
|
16 | ||
|
17 | <p><%= submit_tag l(:button_submit), :name => 'submit' %></p> | |||
16 | <% end %> |
|
18 | <% end %> | |
17 | </div> |
|
19 | </div> | |
18 |
|
20 | |||
19 | <% if @results %> |
|
21 | <% if @results %> | |
20 | <h3><%= l(:label_result_plural) %></h3> |
|
22 | <h3><%= l(:label_result_plural) %></h3> | |
21 | <ul> |
|
23 | <ul id="search-results"> | |
22 | <% @results.each do |e| %> |
|
24 | <% @results.each do |e| %> | |
23 | <li><p><%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %><br /> |
|
25 | <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 /> | |
24 | <%= highlight_tokens(e.event_description, @tokens) %><br /> |
|
26 | <%= highlight_tokens(e.event_description, @tokens) %><br /> | |
25 | <span class="author"><%= format_time(e.event_datetime) %></span></p></li> |
|
27 | <span class="author"><%= format_time(e.event_datetime) %></span></p></li> | |
26 | <% end %> |
|
28 | <% end %> |
@@ -177,7 +177,7 div#activity dd { margin-bottom: 1em; padding-left: 18px; } | |||||
177 | div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } |
|
177 | div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } | |
178 | div#activity dt .time { color: #777; font-size: 80%; } |
|
178 | div#activity dt .time { color: #777; font-size: 80%; } | |
179 | div#activity dd .description { font-style: italic; } |
|
179 | div#activity dd .description { font-style: italic; } | |
180 | div#activity span.project:after { content: " -"; } |
|
180 | div#activity span.project:after, #search-results span.project:after { content: " -"; } | |
181 | div#activity dt.issue { background-image: url(../images/ticket.png); } |
|
181 | div#activity dt.issue { background-image: url(../images/ticket.png); } | |
182 | div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); } |
|
182 | div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); } | |
183 | div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); } |
|
183 | div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); } |
@@ -5,7 +5,10 require 'search_controller' | |||||
5 | class SearchController; def rescue_action(e) raise e end; end |
|
5 | class SearchController; def rescue_action(e) raise e end; end | |
6 |
|
6 | |||
7 | class SearchControllerTest < Test::Unit::TestCase |
|
7 | class SearchControllerTest < Test::Unit::TestCase | |
8 |
fixtures :projects, :enabled_modules, : |
|
8 | fixtures :projects, :enabled_modules, :roles, :users, | |
|
9 | :issues, :trackers, :issue_statuses, | |||
|
10 | :custom_fields, :custom_values, | |||
|
11 | :repositories, :changesets | |||
9 |
|
12 | |||
10 | def setup |
|
13 | def setup | |
11 | @controller = SearchController.new |
|
14 | @controller = SearchController.new | |
@@ -25,6 +28,15 class SearchControllerTest < Test::Unit::TestCase | |||||
25 | assert assigns(:results).include?(Project.find(1)) |
|
28 | assert assigns(:results).include?(Project.find(1)) | |
26 | end |
|
29 | end | |
27 |
|
30 | |||
|
31 | def test_search_all_projects | |||
|
32 | get :index, :q => 'recipe subproject commit', :submit => 'Search' | |||
|
33 | assert_response :success | |||
|
34 | assert_template 'index' | |||
|
35 | assert assigns(:results).include?(Issue.find(2)) | |||
|
36 | assert assigns(:results).include?(Issue.find(5)) | |||
|
37 | assert assigns(:results).include?(Changeset.find(101)) | |||
|
38 | end | |||
|
39 | ||||
28 | def test_search_without_searchable_custom_fields |
|
40 | def test_search_without_searchable_custom_fields | |
29 | CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}" |
|
41 | CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}" | |
30 |
|
42 |
@@ -49,6 +49,9 module Redmine | |||||
49 | raise 'No date column defined defined.' |
|
49 | raise 'No date column defined defined.' | |
50 | end |
|
50 | end | |
51 |
|
51 | |||
|
52 | # Permission needed to search this model | |||
|
53 | searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission) | |||
|
54 | ||||
52 | # Should we search custom fields on this model ? |
|
55 | # Should we search custom fields on this model ? | |
53 | searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? |
|
56 | searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? | |
54 |
|
57 | |||
@@ -62,8 +65,12 module Redmine | |||||
62 | end |
|
65 | end | |
63 |
|
66 | |||
64 | module ClassMethods |
|
67 | module ClassMethods | |
65 | def search(tokens, project, options={}) |
|
68 | # Search the model for the given tokens | |
|
69 | # projects argument can be either nil (will search all projects), a project or an array of projects | |||
|
70 | def search(tokens, projects, options={}) | |||
66 | tokens = [] << tokens unless tokens.is_a?(Array) |
|
71 | tokens = [] << tokens unless tokens.is_a?(Array) | |
|
72 | projects = [] << projects unless projects.nil? || projects.is_a?(Array) | |||
|
73 | ||||
67 | find_options = {:include => searchable_options[:include]} |
|
74 | find_options = {:include => searchable_options[:include]} | |
68 | find_options[:limit] = options[:limit] if options[:limit] |
|
75 | find_options[:limit] = options[:limit] if options[:limit] | |
69 | find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC') |
|
76 | find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC') | |
@@ -92,12 +99,17 module Redmine | |||||
92 | end |
|
99 | end | |
93 | find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort] |
|
100 | find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort] | |
94 |
|
101 | |||
95 | results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do |
|
102 | project_conditions = [] | |
|
103 | project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) : | |||
|
104 | Project.allowed_to_condition(User.current, searchable_options[:permission])) | |||
|
105 | project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil? | |||
|
106 | ||||
|
107 | results = with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do | |||
96 | find(:all, find_options) |
|
108 | find(:all, find_options) | |
97 | end |
|
109 | end | |
98 | if searchable_options[:with] && !options[:titles_only] |
|
110 | if searchable_options[:with] && !options[:titles_only] | |
99 | searchable_options[:with].each do |model, assoc| |
|
111 | searchable_options[:with].each do |model, assoc| | |
100 | results += model.to_s.camelcase.constantize.search(tokens, project, options).collect {|r| r.send assoc} |
|
112 | results += model.to_s.camelcase.constantize.search(tokens, projects, options).collect {|r| r.send assoc} | |
101 | end |
|
113 | end | |
102 | results.uniq! |
|
114 | results.uniq! | |
103 | end |
|
115 | end |
General Comments 0
You need to be logged in to leave comments.
Login now