Index: test/unit/activity_test.rb =================================================================== --- test/unit/activity_test.rb (revision 5009) +++ test/unit/activity_test.rb (working copy) @@ -24,7 +24,28 @@ def setup @project = Project.find(1) end - + + def test_activity_contains_issue_status_update_events + events = find_events(User.anonymous, :project => @project) + assert_not_nil events + + events.sort! { |x,y| x.event_datetime <=> y.event_datetime } + + issue_creation_events = events.find_all { |event| event == Issue.find(8) } + + assert_equal 1, issue_creation_events.size + assert_equal IssueStatus.find_by_name('New'), issue_creation_events.first.initial_status + + issue_status_update_events = events.find_all { |event| event.is_a?(Journal) && event.issue == Issue.find(8) } + + assert_equal 2, issue_status_update_events.size + assert_equal IssueStatus.find_by_name('New'), issue_status_update_events.first.prev_status + assert_equal IssueStatus.find_by_name('Resolved'), issue_status_update_events.first.new_status + + assert_equal IssueStatus.find_by_name('Resolved'), issue_status_update_events.last.prev_status + assert_equal IssueStatus.find_by_name('Closed'), issue_status_update_events.last.new_status + end + def test_activity_without_subprojects events = find_events(User.anonymous, :project => @project) assert_not_nil events Index: vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb =================================================================== --- vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb (revision 5009) +++ vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb (working copy) @@ -24,16 +24,25 @@ module ClassMethods # Options: + # * :title - the title # * :columns - a column or an array of columns to search # * :project_key - project foreign key (default to project_id) # * :date_column - name of the datetime column (default to created_on) - # * :sort_order - name of the column used to sort results (default to :date_column or created_on) + # * :order_column - name of the column used to sort results (default to :date_column or created_on) # * :permission - permission required to search the model (default to :view_"objects") def acts_as_searchable(options = {}) return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods) - + + default_options = { :datetime => :created_on, + :title => :title, + :description => :description, + :author => :author, + :url => {:controller => 'welcome'}, + :type => self.name.underscore.dasherize } + cattr_accessor :searchable_options - self.searchable_options = options + options.assert_valid_keys(:title, :description, :datetime, :url, :type, :columns, :project_key, :date_column, :order_column, :permission, :include) + self.searchable_options = default_options.merge(options) if searchable_options[:columns].nil? raise 'No searchable column defined.' @@ -44,13 +53,13 @@ searchable_options[:project_key] ||= "#{table_name}.project_id" searchable_options[:date_column] ||= "#{table_name}.created_on" searchable_options[:order_column] ||= searchable_options[:date_column] - + # 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 ? searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? - + send :include, Redmine::Acts::Searchable::InstanceMethods end end @@ -60,6 +69,35 @@ base.extend ClassMethods end + %w(datetime title description author type).each do |attr| + src = <<-END_SRC + def searchable_#{attr} + option = searchable_options[:#{attr}] + if option.is_a?(Proc) + option.call(self) + elsif option.is_a?(Symbol) + send(option) + else + option + end + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + + def searchable_url(options = {}) + option = searchable_options[:url] + if option.is_a?(Proc) + option.call(self).merge(options) + elsif option.is_a?(Hash) + option.merge(options) + elsif option.is_a?(Symbol) + send(option).merge(options) + else + option + end + end + module ClassMethods # Searches the model for the given tokens # projects argument can be either nil (will search all projects), a project or an array of projects @@ -94,19 +132,19 @@ token_clauses << custom_field_sql end end - + sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') - + find_options[:conditions] = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort] - + 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 = [] results_count = 0 - + with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do with_scope(:find => find_options) do results_count = count(:all) Index: app/models/project.rb =================================================================== --- app/models/project.rb (revision 5009) +++ app/models/project.rb (working copy) @@ -61,7 +61,8 @@ :delete_permission => :manage_files acts_as_customizable - acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil + acts_as_searchable :title => lambda { |p| p.event_title } , :url => lambda { |p| p.event_url }, + :columns => ['name', 'identifier', 'description'], :project_key => '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: app/models/issue.rb =================================================================== --- app/models/issue.rb (revision 5009) +++ app/models/issue.rb (working copy) @@ -17,7 +17,7 @@ class Issue < ActiveRecord::Base include Redmine::SafeAttributes - + belongs_to :project belongs_to :tracker belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' @@ -38,11 +38,15 @@ acts_as_attachable :after_remove => :attachment_removed acts_as_customizable acts_as_watchable - acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], + + acts_as_searchable :title => lambda { |i| "#{i.tracker.name} ##{i.id} (#{i.status()}): #{i.subject}" }, + :url => lambda { |i| i.event_url }, :type => lambda { |i| i.event_type }, + :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], :include => [:project, :journals], # sort by id so that limited eager loading doesn't break with postgresql :order_column => "#{table_name}.id" - acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, + + acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.initial_status()}): #{o.subject}"}, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } @@ -80,17 +84,22 @@ } } - named_scope :with_query, lambda {|query| - { - :conditions => Query.merge_conditions(query.statement) - } - } - before_create :default_assign before_save :close_duplicates, :update_done_ratio_from_issue_status after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal after_destroy :update_parent_attributes - + + # Retrieves issue's original status from journal if modified since issue creation + def initial_status() + + first_status_modification_journal = self.journals.first( { + :joins => :details, + :conditions => { JournalDetail.table_name => { :property => 'attr', :prop_key => 'status_id' } }, + :order => :created_on } ) + + first_status_modification_journal ? first_status_modification_journal.prev_status : self.status + end + # Returns true if usr or current user is allowed to view the issue def visible?(usr=nil) (usr || User.current).allowed_to?(:view_issues, self.project) Index: app/models/document.rb =================================================================== --- app/models/document.rb (revision 5009) +++ app/models/document.rb (working copy) @@ -20,7 +20,8 @@ belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id" acts_as_attachable :delete_permission => :manage_documents - acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project + acts_as_searchable :title => lambda { |d| d.event_title }, :url => lambda { |d| d.event_url }, + :columns => ['title', "#{table_name}.description"], :include => :project 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 }, :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} Index: app/models/journal.rb =================================================================== --- app/models/journal.rb (revision 5009) +++ app/models/journal.rb (working copy) @@ -48,7 +48,12 @@ c = details.detect {|detail| detail.prop_key == 'status_id'} (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil end - + + def prev_status + c = details.detect {|detail| detail.prop_key == 'status_id'} + (c && c.old_value) ? IssueStatus.find_by_id(c.old_value.to_i) : nil + end + def new_value_for(prop) c = details.detect {|detail| detail.prop_key == prop} c ? c.value : nil Index: app/models/changeset.rb =================================================================== --- app/models/changeset.rb (revision 5009) +++ app/models/changeset.rb (working copy) @@ -23,15 +23,17 @@ has_many :changes, :dependent => :delete_all has_and_belongs_to_many :issues + acts_as_searchable :title => lambda { |c| c.event_title }, :url => lambda { |c| c.event_url }, + :description => :long_comments, :datetime => :committed_on, + :columns => 'comments', + :include => {:repository => :project}, + :project_key => "#{Repository.table_name}.project_id", + :date_column => 'committed_on' + acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, :description => :long_comments, :datetime => :committed_on, :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}} - - acts_as_searchable :columns => 'comments', - :include => {:repository => :project}, - :project_key => "#{Repository.table_name}.project_id", - :date_column => 'committed_on' acts_as_activity_provider :timestamp => "#{table_name}.committed_on", :author_key => :user_id, Index: app/models/news.rb =================================================================== --- app/models/news.rb (revision 5009) +++ app/models/news.rb (working copy) @@ -24,7 +24,8 @@ validates_length_of :title, :maximum => 60 validates_length_of :summary, :maximum => 255 - acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project + acts_as_searchable :url => lambda { |n| n.event_url }, + :columns => ['title', 'summary', "#{table_name}.description"], :include => :project acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} acts_as_activity_provider :find_options => {:include => [:project, :author]}, :author_key => :author_id Index: app/models/message.rb =================================================================== --- app/models/message.rb (revision 5009) +++ app/models/message.rb (working copy) @@ -22,7 +22,10 @@ acts_as_attachable belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' - acts_as_searchable :columns => ['subject', 'content'], + acts_as_searchable :title => lambda { |m| m.event_title }, :url => lambda { |m| m.event_url }, + :type => lambda { |m| m.event_type }, + :description => :content, + :columns => ['subject', 'content'], :include => {:board => :project}, :project_key => 'project_id', :date_column => "#{table_name}.created_on" Index: app/models/wiki_page.rb =================================================================== --- app/models/wiki_page.rb (revision 5009) +++ app/models/wiki_page.rb (working copy) @@ -25,15 +25,17 @@ acts_as_tree :dependent => :nullify, :order => 'title' acts_as_watchable - acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"}, - :description => :text, - :datetime => :created_on, - :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}} - acts_as_searchable :columns => ['title', 'text'], + acts_as_searchable :title => lambda { |wp| wp.event_title }, :url => lambda { |wp| wp.event_url }, + :description => :text, + :columns => ['title', 'text'], :include => [{:wiki => :project}, :content], :project_key => "#{Wiki.table_name}.project_id" + acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"}, + :description => :text, + :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}} + attr_accessor :redirect_existing_links validates_presence_of :title Index: app/views/search/index.rhtml =================================================================== --- app/views/search/index.rhtml (revision 5009) +++ app/views/search/index.rhtml (working copy) @@ -25,10 +25,10 @@

<%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)

- <% @results.each do |e| %> -
<%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, :length => 255), @tokens), e.event_url %>
-
<%= highlight_tokens(e.event_description, @tokens) %> - <%= format_time(e.event_datetime) %>
+ <% @results.each do |r| %> +
<%= content_tag('span', h(r.project), :class => 'project') unless @project == r.project %> <%= link_to highlight_tokens(truncate(r.searchable_title, :length => 255), @tokens), r.searchable_url %>
+
<%= highlight_tokens(r.searchable_description, @tokens) %> + <%= format_time(r.searchable_datetime) %>
<% end %>
<% end %> Index: app/controllers/search_controller.rb =================================================================== --- app/controllers/search_controller.rb (revision 5009) +++ app/controllers/search_controller.rb (working copy) @@ -83,17 +83,17 @@ @results += r @results_by_type[s] += c end - @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime} + @results = @results.sort {|a,b| b.searchable_datetime <=> a.searchable_datetime} if params[:previous].nil? - @pagination_previous_date = @results[0].event_datetime if offset && @results[0] + @pagination_previous_date = @results[0].searchable_datetime if offset && @results[0] if @results.size > limit - @pagination_next_date = @results[limit-1].event_datetime + @pagination_next_date = @results[limit-1].searchable_datetime @results = @results[0, limit] end else - @pagination_next_date = @results[-1].event_datetime if offset && @results[-1] + @pagination_next_date = @results[-1].searchable_datetime if offset && @results[-1] if @results.size > limit - @pagination_previous_date = @results[-(limit)].event_datetime + @pagination_previous_date = @results[-(limit)].searchable_datetime @results = @results[-(limit), limit] end end Index: test/fixtures/journal_details.yml =================================================================== --- test/fixtures/journal_details.yml (revision 5009) +++ test/fixtures/journal_details.yml (working copy) @@ -27,3 +27,17 @@ value: "This word was and an other was added" prop_key: description journal_id: 3 +journal_details_005: + old_value: "1" + property: attr + id: 5 + value: "3" + prop_key: status_id + journal_id: 5 +journal_details_006: + old_value: "3" + property: attr + id: 6 + value: "5" + prop_key: status_id + journal_id: 6 Index: test/fixtures/issues.yml =================================================================== --- test/fixtures/issues.yml (revision 5009) +++ test/fixtures/issues.yml (working copy) @@ -132,7 +132,7 @@ lft: 1 rgt: 2 issues_008: - created_on: <%= 10.days.ago.to_date.to_s(:db) %> + created_on: <%= 12.days.ago.to_date.to_s(:db) %> project_id: 1 updated_on: <%= 10.days.ago.to_date.to_s(:db) %> priority_id: 5 Index: test/fixtures/journals.yml =================================================================== --- test/fixtures/journals.yml (revision 5009) +++ test/fixtures/journals.yml (working copy) @@ -27,3 +27,17 @@ journalized_type: Issue user_id: 1 journalized_id: 6 +journals_005: + created_on: <%= 11.days.ago.to_date.to_s(:db) %> + notes: "Resolving issue 8." + id: 5 + journalized_type: Issue + user_id: 1 + journalized_id: 8 +journals_006: + created_on: <%= 10.days.ago.to_date.to_s(:db) %> + notes: "Closing issue 8." + id: 6 + journalized_type: Issue + user_id: 1 + journalized_id: 8