git-svn-id: http://svn.redmine.org/redmine/trunk@16366 e93f8b46-1217-0410-a6f0-8f06a7374b81tags/3.4.0
@@ -245,6 +245,7 @@ class Issue < ActiveRecord::Base | |||
@spent_hours = nil | |||
@total_spent_hours = nil | |||
@total_estimated_hours = nil | |||
@last_updated_by = nil | |||
base_reload(*args) | |||
end | |||
@@ -1069,6 +1070,14 @@ class Issue < ActiveRecord::Base | |||
@relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort) | |||
end | |||
def last_updated_by | |||
if @last_updated_by | |||
@last_updated_by.presence | |||
else | |||
journals.reorder(:id => :desc).first.try(:user) | |||
end | |||
end | |||
# Preloads relations for a collection of issues | |||
def self.load_relations(issues) | |||
if issues.any? | |||
@@ -1132,6 +1141,23 @@ class Issue < ActiveRecord::Base | |||
where(:ancestors => {:id => issues.map(&:id)}) | |||
end | |||
# Preloads users who updated last a collection of issues | |||
def self.load_visible_last_updated_by(issues, user=User.current) | |||
if issues.any? | |||
issue_ids = issues.map(&:id) | |||
journals = Journal.joins(issue: :project).preload(:user). | |||
where(:journalized_type => 'Issue', :journalized_id => issue_ids). | |||
where("#{Journal.table_name}.id = (SELECT MAX(j.id) FROM #{Journal.table_name} j" + | |||
" WHERE j.journalized_type='Issue' AND j.journalized_id=#{Journal.table_name}.journalized_id" + | |||
" AND #{Journal.visible_notes_condition(user, :skip_pre_condition => true)})").to_a | |||
issues.each do |issue| | |||
journal = journals.detect {|j| j.journalized_id == issue.id} | |||
issue.instance_variable_set("@last_updated_by", journal.try(:user) || '') | |||
end | |||
end | |||
end | |||
# Finds an issue relation given its id. | |||
def find_relation(relation_id) | |||
IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id) |
@@ -43,6 +43,7 @@ class IssueQuery < Query | |||
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), | |||
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), | |||
QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'), | |||
QueryColumn.new(:last_updated_by, :sortable => lambda {User.fields_for_order_statement("last_journal_user")}), | |||
QueryColumn.new(:relations, :caption => :label_related_issues), | |||
QueryColumn.new(:description, :inline => false) | |||
] | |||
@@ -298,6 +299,9 @@ class IssueQuery < Query | |||
if has_column?(:total_spent_hours) | |||
Issue.load_visible_total_spent_hours(issues) | |||
end | |||
if has_column?(:last_updated_by) | |||
Issue.load_visible_last_updated_by(issues) | |||
end | |||
if has_column?(:relations) | |||
Issue.load_visible_relations(issues) | |||
end | |||
@@ -572,6 +576,11 @@ class IssueQuery < Query | |||
if order_options.include?('users') | |||
joins << "LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{queried_table_name}.assigned_to_id" | |||
end | |||
if order_options.include?('last_journal_user') | |||
joins << "LEFT OUTER JOIN #{Journal.table_name} ON #{Journal.table_name}.id = (SELECT MAX(#{Journal.table_name}.id) FROM #{Journal.table_name}" + | |||
" WHERE #{Journal.table_name}.journalized_type='Issue' AND #{Journal.table_name}.journalized_id=#{Issue.table_name}.id AND #{Journal.visible_notes_condition(User.current, :skip_pre_condition => true)})" + | |||
" LEFT OUTER JOIN #{User.table_name} last_journal_user ON last_journal_user.id = #{Journal.table_name}.user_id"; | |||
end | |||
if order_options.include?('versions') | |||
joins << "LEFT OUTER JOIN #{Version.table_name} ON #{Version.table_name}.id = #{queried_table_name}.fixed_version_id" | |||
end |
@@ -254,7 +254,7 @@ tr.project.idnt-8 td.name {padding-left: 11em;} | |||
tr.project.idnt-9 td.name {padding-left: 12.5em;} | |||
tr.issue { text-align: center; white-space: nowrap; } | |||
tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.list, tr.issue td.relations, tr.issue td.parent { white-space: normal; } | |||
tr.issue td.subject, tr.issue td.category, td.assigned_to, td.last_updated_by, tr.issue td.string, tr.issue td.text, tr.issue td.list, tr.issue td.relations, tr.issue td.parent { white-space: normal; } | |||
tr.issue td.relations { text-align: left; } | |||
tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;} | |||
tr.issue td.relations span {white-space: nowrap;} |
@@ -741,6 +741,18 @@ class IssuesControllerTest < Redmine::ControllerTest | |||
assert_response :success | |||
end | |||
def test_index_sort_by_last_updated_by | |||
get :index, :sort => 'last_updated_by' | |||
assert_response :success | |||
assert_select 'table.issues.sort-by-last-updated-by.sort-asc' | |||
end | |||
def test_index_sort_by_last_updated_by_desc | |||
get :index, :sort => 'last_updated_by:desc' | |||
assert_response :success | |||
assert_select 'table.issues.sort-by-last-updated-by.sort-desc' | |||
end | |||
def test_index_sort_by_spent_hours | |||
get :index, :sort => 'spent_hours:desc' | |||
assert_response :success | |||
@@ -970,6 +982,13 @@ class IssuesControllerTest < Redmine::ControllerTest | |||
assert_select 'td.parent a[title=?]', parent.subject | |||
end | |||
def test_index_with_last_updated_by_column | |||
get :index, :c => %w(subject last_updated_by), :issue_id => '1,2,3', :sort => 'id', :set_filter => '1' | |||
assert_select 'td.last_updated_by' | |||
assert_equal ["John Smith", "John Smith", ""], css_select('td.last_updated_by').map(&:text) | |||
end | |||
def test_index_with_estimated_hours_total | |||
Issue.delete_all | |||
Issue.generate!(:estimated_hours => 5.5) |
@@ -29,7 +29,7 @@ class QueryTest < ActiveSupport::TestCase | |||
:queries, | |||
:projects_trackers, | |||
:custom_fields_trackers, | |||
:workflows, | |||
:workflows, :journals, | |||
:attachments | |||
INTEGER_KLASS = RUBY_VERSION >= "2.4" ? Integer : Fixnum | |||
@@ -762,7 +762,7 @@ class QueryTest < ActiveSupport::TestCase | |||
query = IssueQuery.new(:name => '_') | |||
filter_name = "updated_by" | |||
assert_include filter_name, query.available_filters.keys | |||
query.filters = {filter_name => {:operator => '=', :values => ['me']}} | |||
assert_equal [2], find_issues_with_query(query).map(&:id).sort | |||
end | |||
@@ -1322,6 +1322,19 @@ class QueryTest < ActiveSupport::TestCase | |||
assert_not_nil issues.first.instance_variable_get("@spent_hours") | |||
end | |||
def test_query_should_preload_last_updated_by | |||
with_current_user User.find(2) do | |||
q = IssueQuery.new(:name => '_', :column_names => [:subject, :last_updated_by]) | |||
q.filters = {"issue_id" => {:operator => '=', :values => ['1,2,3']}} | |||
assert q.has_column?(:last_updated_by) | |||
issues = q.issues.sort_by(&:id) | |||
assert issues.all? {|issue| !issue.instance_variable_get("@last_updated_by").nil?} | |||
assert_equal ["User", "User", "NilClass"], issues.map { |i| i.last_updated_by.class.name} | |||
assert_equal ["John Smith", "John Smith", ""], issues.map { |i| i.last_updated_by.to_s } | |||
end | |||
end | |||
def test_groupable_columns_should_include_custom_fields | |||
q = IssueQuery.new | |||
column = q.groupable_columns.detect {|c| c.name == :cf_1} | |||
@@ -1384,6 +1397,16 @@ class QueryTest < ActiveSupport::TestCase | |||
end | |||
end | |||
def test_sortable_columns_should_sort_last_updated_by_according_to_user_format_setting | |||
with_settings :user_format => 'lastname_comma_firstname' do | |||
q = IssueQuery.new | |||
q.sort_criteria = [['last_updated_by', 'desc']] | |||
assert q.sortable_columns.has_key?('last_updated_by') | |||
assert_equal %w(last_journal_user.lastname last_journal_user.firstname last_journal_user.id), q.sortable_columns['last_updated_by'] | |||
end | |||
end | |||
def test_sortable_columns_should_include_custom_field | |||
q = IssueQuery.new | |||
assert q.sortable_columns['cf_1'] |