Defect #7293 » activity_new_issue_event_status.patch
| test/unit/activity_test.rb (working copy) | ||
|---|---|---|
| 24 | 24 |
def setup |
| 25 | 25 |
@project = Project.find(1) |
| 26 | 26 |
end |
| 27 |
|
|
| 27 |
|
|
| 28 |
def test_activity_contains_issue_status_update_events |
|
| 29 |
events = find_events(User.anonymous, :project => @project) |
|
| 30 |
assert_not_nil events |
|
| 31 |
|
|
| 32 |
events.sort! { |x,y| x.event_datetime <=> y.event_datetime }
|
|
| 33 |
|
|
| 34 |
issue_creation_events = events.find_all { |event| event == Issue.find(8) }
|
|
| 35 |
|
|
| 36 |
assert_equal 1, issue_creation_events.size |
|
| 37 |
assert_equal IssueStatus.find_by_name('New'), issue_creation_events.first.initial_status
|
|
| 38 |
|
|
| 39 |
issue_status_update_events = events.find_all { |event| event.is_a?(Journal) && event.issue == Issue.find(8) }
|
|
| 40 |
|
|
| 41 |
assert_equal 2, issue_status_update_events.size |
|
| 42 |
assert_equal IssueStatus.find_by_name('New'), issue_status_update_events.first.prev_status
|
|
| 43 |
assert_equal IssueStatus.find_by_name('Resolved'), issue_status_update_events.first.new_status
|
|
| 44 |
|
|
| 45 |
assert_equal IssueStatus.find_by_name('Resolved'), issue_status_update_events.last.prev_status
|
|
| 46 |
assert_equal IssueStatus.find_by_name('Closed'), issue_status_update_events.last.new_status
|
|
| 47 |
end |
|
| 48 |
|
|
| 28 | 49 |
def test_activity_without_subprojects |
| 29 | 50 |
events = find_events(User.anonymous, :project => @project) |
| 30 | 51 |
assert_not_nil events |
| vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb (working copy) | ||
|---|---|---|
| 24 | 24 |
|
| 25 | 25 |
module ClassMethods |
| 26 | 26 |
# Options: |
| 27 |
# * :title - the title |
|
| 27 | 28 |
# * :columns - a column or an array of columns to search |
| 28 | 29 |
# * :project_key - project foreign key (default to project_id) |
| 29 | 30 |
# * :date_column - name of the datetime column (default to created_on) |
| 30 |
# * :sort_order - name of the column used to sort results (default to :date_column or created_on)
|
|
| 31 |
# * :order_column - name of the column used to sort results (default to :date_column or created_on)
|
|
| 31 | 32 |
# * :permission - permission required to search the model (default to :view_"objects") |
| 32 | 33 |
def acts_as_searchable(options = {})
|
| 33 | 34 |
return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods) |
| 34 |
|
|
| 35 |
|
|
| 36 |
default_options = { :datetime => :created_on,
|
|
| 37 |
:title => :title, |
|
| 38 |
:description => :description, |
|
| 39 |
:author => :author, |
|
| 40 |
:url => {:controller => 'welcome'},
|
|
| 41 |
:type => self.name.underscore.dasherize } |
|
| 42 |
|
|
| 35 | 43 |
cattr_accessor :searchable_options |
| 36 |
self.searchable_options = options |
|
| 44 |
options.assert_valid_keys(:title, :description, :datetime, :url, :type, :columns, :project_key, :date_column, :order_column, :permission, :include) |
|
| 45 |
self.searchable_options = default_options.merge(options) |
|
| 37 | 46 |
|
| 38 | 47 |
if searchable_options[:columns].nil? |
| 39 | 48 |
raise 'No searchable column defined.' |
| ... | ... | |
| 44 | 53 |
searchable_options[:project_key] ||= "#{table_name}.project_id"
|
| 45 | 54 |
searchable_options[:date_column] ||= "#{table_name}.created_on"
|
| 46 | 55 |
searchable_options[:order_column] ||= searchable_options[:date_column] |
| 47 |
|
|
| 56 |
|
|
| 48 | 57 |
# Permission needed to search this model |
| 49 | 58 |
searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission)
|
| 50 |
|
|
| 59 |
|
|
| 51 | 60 |
# Should we search custom fields on this model ? |
| 52 | 61 |
searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? |
| 53 |
|
|
| 62 |
|
|
| 54 | 63 |
send :include, Redmine::Acts::Searchable::InstanceMethods |
| 55 | 64 |
end |
| 56 | 65 |
end |
| ... | ... | |
| 60 | 69 |
base.extend ClassMethods |
| 61 | 70 |
end |
| 62 | 71 |
|
| 72 |
%w(datetime title description author type).each do |attr| |
|
| 73 |
src = <<-END_SRC |
|
| 74 |
def searchable_#{attr}
|
|
| 75 |
option = searchable_options[:#{attr}]
|
|
| 76 |
if option.is_a?(Proc) |
|
| 77 |
option.call(self) |
|
| 78 |
elsif option.is_a?(Symbol) |
|
| 79 |
send(option) |
|
| 80 |
else |
|
| 81 |
option |
|
| 82 |
end |
|
| 83 |
end |
|
| 84 |
END_SRC |
|
| 85 |
class_eval src, __FILE__, __LINE__ |
|
| 86 |
end |
|
| 87 |
|
|
| 88 |
def searchable_url(options = {})
|
|
| 89 |
option = searchable_options[:url] |
|
| 90 |
if option.is_a?(Proc) |
|
| 91 |
option.call(self).merge(options) |
|
| 92 |
elsif option.is_a?(Hash) |
|
| 93 |
option.merge(options) |
|
| 94 |
elsif option.is_a?(Symbol) |
|
| 95 |
send(option).merge(options) |
|
| 96 |
else |
|
| 97 |
option |
|
| 98 |
end |
|
| 99 |
end |
|
| 100 |
|
|
| 63 | 101 |
module ClassMethods |
| 64 | 102 |
# Searches the model for the given tokens |
| 65 | 103 |
# projects argument can be either nil (will search all projects), a project or an array of projects |
| ... | ... | |
| 94 | 132 |
token_clauses << custom_field_sql |
| 95 | 133 |
end |
| 96 | 134 |
end |
| 97 |
|
|
| 135 |
|
|
| 98 | 136 |
sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
|
| 99 |
|
|
| 137 |
|
|
| 100 | 138 |
find_options[:conditions] = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
|
| 101 |
|
|
| 139 |
|
|
| 102 | 140 |
project_conditions = [] |
| 103 | 141 |
project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) : |
| 104 | 142 |
Project.allowed_to_condition(User.current, searchable_options[:permission])) |
| 105 | 143 |
project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
|
| 106 |
|
|
| 144 |
|
|
| 107 | 145 |
results = [] |
| 108 | 146 |
results_count = 0 |
| 109 |
|
|
| 147 |
|
|
| 110 | 148 |
with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do
|
| 111 | 149 |
with_scope(:find => find_options) do |
| 112 | 150 |
results_count = count(:all) |
| app/models/project.rb (working copy) | ||
|---|---|---|
| 61 | 61 |
:delete_permission => :manage_files |
| 62 | 62 |
|
| 63 | 63 |
acts_as_customizable |
| 64 |
acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil |
|
| 64 |
acts_as_searchable :title => lambda { |p| p.event_title } , :url => lambda { |p| p.event_url },
|
|
| 65 |
:columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil |
|
| 65 | 66 |
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
|
| 66 | 67 |
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
|
| 67 | 68 |
:author => nil |
| app/models/issue.rb (working copy) | ||
|---|---|---|
| 17 | 17 |
|
| 18 | 18 |
class Issue < ActiveRecord::Base |
| 19 | 19 |
include Redmine::SafeAttributes |
| 20 |
|
|
| 20 |
|
|
| 21 | 21 |
belongs_to :project |
| 22 | 22 |
belongs_to :tracker |
| 23 | 23 |
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' |
| ... | ... | |
| 38 | 38 |
acts_as_attachable :after_remove => :attachment_removed |
| 39 | 39 |
acts_as_customizable |
| 40 | 40 |
acts_as_watchable |
| 41 |
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
|
|
| 41 |
|
|
| 42 |
acts_as_searchable :title => lambda { |i| "#{i.tracker.name} ##{i.id} (#{i.status()}): #{i.subject}" },
|
|
| 43 |
:url => lambda { |i| i.event_url }, :type => lambda { |i| i.event_type },
|
|
| 44 |
:columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
|
|
| 42 | 45 |
:include => [:project, :journals], |
| 43 | 46 |
# sort by id so that limited eager loading doesn't break with postgresql |
| 44 | 47 |
:order_column => "#{table_name}.id"
|
| 45 |
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
|
|
| 48 |
|
|
| 49 |
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.initial_status()}): #{o.subject}"},
|
|
| 46 | 50 |
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
|
| 47 | 51 |
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
|
| 48 | 52 |
|
| ... | ... | |
| 80 | 84 |
} |
| 81 | 85 |
} |
| 82 | 86 |
|
| 83 |
named_scope :with_query, lambda {|query|
|
|
| 84 |
{
|
|
| 85 |
:conditions => Query.merge_conditions(query.statement) |
|
| 86 |
} |
|
| 87 |
} |
|
| 88 |
|
|
| 89 | 87 |
before_create :default_assign |
| 90 | 88 |
before_save :close_duplicates, :update_done_ratio_from_issue_status |
| 91 | 89 |
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal |
| 92 | 90 |
after_destroy :update_parent_attributes |
| 93 |
|
|
| 91 |
|
|
| 92 |
# Retrieves issue's original status from journal if modified since issue creation |
|
| 93 |
def initial_status() |
|
| 94 |
|
|
| 95 |
first_status_modification_journal = self.journals.first( {
|
|
| 96 |
:joins => :details, |
|
| 97 |
:conditions => { JournalDetail.table_name => { :property => 'attr', :prop_key => 'status_id' } },
|
|
| 98 |
:order => :created_on } ) |
|
| 99 |
|
|
| 100 |
first_status_modification_journal ? first_status_modification_journal.prev_status : self.status |
|
| 101 |
end |
|
| 102 |
|
|
| 94 | 103 |
# Returns true if usr or current user is allowed to view the issue |
| 95 | 104 |
def visible?(usr=nil) |
| 96 | 105 |
(usr || User.current).allowed_to?(:view_issues, self.project) |
| app/models/document.rb (working copy) | ||
|---|---|---|
| 20 | 20 |
belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id" |
| 21 | 21 |
acts_as_attachable :delete_permission => :manage_documents |
| 22 | 22 |
|
| 23 |
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
|
|
| 23 |
acts_as_searchable :title => lambda { |d| d.event_title }, :url => lambda { |d| d.event_url },
|
|
| 24 |
:columns => ['title', "#{table_name}.description"], :include => :project
|
|
| 24 | 25 |
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
|
| 25 | 26 |
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
|
| 26 | 27 |
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
|
| app/models/journal.rb (working copy) | ||
|---|---|---|
| 48 | 48 |
c = details.detect {|detail| detail.prop_key == 'status_id'}
|
| 49 | 49 |
(c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil |
| 50 | 50 |
end |
| 51 |
|
|
| 51 |
|
|
| 52 |
def prev_status |
|
| 53 |
c = details.detect {|detail| detail.prop_key == 'status_id'}
|
|
| 54 |
(c && c.old_value) ? IssueStatus.find_by_id(c.old_value.to_i) : nil |
|
| 55 |
end |
|
| 56 |
|
|
| 52 | 57 |
def new_value_for(prop) |
| 53 | 58 |
c = details.detect {|detail| detail.prop_key == prop}
|
| 54 | 59 |
c ? c.value : nil |
| app/models/changeset.rb (working copy) | ||
|---|---|---|
| 23 | 23 |
has_many :changes, :dependent => :delete_all |
| 24 | 24 |
has_and_belongs_to_many :issues |
| 25 | 25 |
|
| 26 |
acts_as_searchable :title => lambda { |c| c.event_title }, :url => lambda { |c| c.event_url },
|
|
| 27 |
:description => :long_comments, :datetime => :committed_on, |
|
| 28 |
:columns => 'comments', |
|
| 29 |
:include => {:repository => :project},
|
|
| 30 |
:project_key => "#{Repository.table_name}.project_id",
|
|
| 31 |
:date_column => 'committed_on' |
|
| 32 |
|
|
| 26 | 33 |
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
|
| 27 | 34 |
:description => :long_comments, |
| 28 | 35 |
:datetime => :committed_on, |
| 29 | 36 |
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
|
| 30 |
|
|
| 31 |
acts_as_searchable :columns => 'comments', |
|
| 32 |
:include => {:repository => :project},
|
|
| 33 |
:project_key => "#{Repository.table_name}.project_id",
|
|
| 34 |
:date_column => 'committed_on' |
|
| 35 | 37 |
|
| 36 | 38 |
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
|
| 37 | 39 |
:author_key => :user_id, |
| app/models/news.rb (working copy) | ||
|---|---|---|
| 24 | 24 |
validates_length_of :title, :maximum => 60 |
| 25 | 25 |
validates_length_of :summary, :maximum => 255 |
| 26 | 26 |
|
| 27 |
acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project
|
|
| 27 |
acts_as_searchable :url => lambda { |n| n.event_url },
|
|
| 28 |
:columns => ['title', 'summary', "#{table_name}.description"], :include => :project
|
|
| 28 | 29 |
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
|
| 29 | 30 |
acts_as_activity_provider :find_options => {:include => [:project, :author]},
|
| 30 | 31 |
:author_key => :author_id |
| app/models/message.rb (working copy) | ||
|---|---|---|
| 22 | 22 |
acts_as_attachable |
| 23 | 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 :title => lambda { |m| m.event_title }, :url => lambda { |m| m.event_url },
|
|
| 26 |
:type => lambda { |m| m.event_type },
|
|
| 27 |
:description => :content, |
|
| 28 |
:columns => ['subject', 'content'], |
|
| 26 | 29 |
:include => {:board => :project},
|
| 27 | 30 |
:project_key => 'project_id', |
| 28 | 31 |
:date_column => "#{table_name}.created_on"
|
| app/models/wiki_page.rb (working copy) | ||
|---|---|---|
| 25 | 25 |
acts_as_tree :dependent => :nullify, :order => 'title' |
| 26 | 26 |
|
| 27 | 27 |
acts_as_watchable |
| 28 |
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
|
|
| 29 |
:description => :text, |
|
| 30 |
:datetime => :created_on, |
|
| 31 |
:url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
|
|
| 32 | 28 |
|
| 33 |
acts_as_searchable :columns => ['title', 'text'], |
|
| 29 |
acts_as_searchable :title => lambda { |wp| wp.event_title }, :url => lambda { |wp| wp.event_url },
|
|
| 30 |
:description => :text, |
|
| 31 |
:columns => ['title', 'text'], |
|
| 34 | 32 |
:include => [{:wiki => :project}, :content],
|
| 35 | 33 |
:project_key => "#{Wiki.table_name}.project_id"
|
| 36 | 34 |
|
| 35 |
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
|
|
| 36 |
:description => :text, |
|
| 37 |
:url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
|
|
| 38 |
|
|
| 37 | 39 |
attr_accessor :redirect_existing_links |
| 38 | 40 |
|
| 39 | 41 |
validates_presence_of :title |
| app/views/search/index.rhtml (working copy) | ||
|---|---|---|
| 25 | 25 |
|
| 26 | 26 |
<h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3> |
| 27 | 27 |
<dl id="search-results"> |
| 28 |
<% @results.each do |e| %>
|
|
| 29 |
<dt class="<%= e.event_type %>"><%= 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 %></dt>
|
|
| 30 |
<dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
|
|
| 31 |
<span class="author"><%= format_time(e.event_datetime) %></span></dd>
|
|
| 28 |
<% @results.each do |r| %>
|
|
| 29 |
<dt class="<%= r.searchable_type %>"><%= 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 %></dt>
|
|
| 30 |
<dd><span class="description"><%= highlight_tokens(r.searchable_description, @tokens) %></span>
|
|
| 31 |
<span class="datetime"><%= format_time(r.searchable_datetime) %></span></dd>
|
|
| 32 | 32 |
<% end %> |
| 33 | 33 |
</dl> |
| 34 | 34 |
<% end %> |
| app/controllers/search_controller.rb (working copy) | ||
|---|---|---|
| 83 | 83 |
@results += r |
| 84 | 84 |
@results_by_type[s] += c |
| 85 | 85 |
end |
| 86 |
@results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
|
|
| 86 |
@results = @results.sort {|a,b| b.searchable_datetime <=> a.searchable_datetime}
|
|
| 87 | 87 |
if params[:previous].nil? |
| 88 |
@pagination_previous_date = @results[0].event_datetime if offset && @results[0]
|
|
| 88 |
@pagination_previous_date = @results[0].searchable_datetime if offset && @results[0]
|
|
| 89 | 89 |
if @results.size > limit |
| 90 |
@pagination_next_date = @results[limit-1].event_datetime
|
|
| 90 |
@pagination_next_date = @results[limit-1].searchable_datetime
|
|
| 91 | 91 |
@results = @results[0, limit] |
| 92 | 92 |
end |
| 93 | 93 |
else |
| 94 |
@pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
|
|
| 94 |
@pagination_next_date = @results[-1].searchable_datetime if offset && @results[-1]
|
|
| 95 | 95 |
if @results.size > limit |
| 96 |
@pagination_previous_date = @results[-(limit)].event_datetime
|
|
| 96 |
@pagination_previous_date = @results[-(limit)].searchable_datetime
|
|
| 97 | 97 |
@results = @results[-(limit), limit] |
| 98 | 98 |
end |
| 99 | 99 |
end |
| test/fixtures/journal_details.yml (working copy) | ||
|---|---|---|
| 27 | 27 |
value: "This word was and an other was added" |
| 28 | 28 |
prop_key: description |
| 29 | 29 |
journal_id: 3 |
| 30 |
journal_details_005: |
|
| 31 |
old_value: "1" |
|
| 32 |
property: attr |
|
| 33 |
id: 5 |
|
| 34 |
value: "3" |
|
| 35 |
prop_key: status_id |
|
| 36 |
journal_id: 5 |
|
| 37 |
journal_details_006: |
|
| 38 |
old_value: "3" |
|
| 39 |
property: attr |
|
| 40 |
id: 6 |
|
| 41 |
value: "5" |
|
| 42 |
prop_key: status_id |
|
| 43 |
journal_id: 6 |
|
| test/fixtures/issues.yml (working copy) | ||
|---|---|---|
| 132 | 132 |
lft: 1 |
| 133 | 133 |
rgt: 2 |
| 134 | 134 |
issues_008: |
| 135 |
created_on: <%= 10.days.ago.to_date.to_s(:db) %>
|
|
| 135 |
created_on: <%= 12.days.ago.to_date.to_s(:db) %>
|
|
| 136 | 136 |
project_id: 1 |
| 137 | 137 |
updated_on: <%= 10.days.ago.to_date.to_s(:db) %> |
| 138 | 138 |
priority_id: 5 |
| test/fixtures/journals.yml (working copy) | ||
|---|---|---|
| 27 | 27 |
journalized_type: Issue |
| 28 | 28 |
user_id: 1 |
| 29 | 29 |
journalized_id: 6 |
| 30 |
journals_005: |
|
| 31 |
created_on: <%= 11.days.ago.to_date.to_s(:db) %> |
|
| 32 |
notes: "Resolving issue 8." |
|
| 33 |
id: 5 |
|
| 34 |
journalized_type: Issue |
|
| 35 |
user_id: 1 |
|
| 36 |
journalized_id: 8 |
|
| 37 |
journals_006: |
|
| 38 |
created_on: <%= 10.days.ago.to_date.to_s(:db) %> |
|
| 39 |
notes: "Closing issue 8." |
|
| 40 |
id: 6 |
|
| 41 |
journalized_type: Issue |
|
| 42 |
user_id: 1 |
|
| 43 |
journalized_id: 8 |
|