summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/controllers/timelog_controller.rb1
-rw-r--r--app/models/query.rb20
-rw-r--r--app/models/time_entry_query.rb42
-rw-r--r--app/views/timelog/_list.html.erb2
-rw-r--r--lib/redmine/helpers/time_report.rb2
-rw-r--r--test/functional/timelog_controller_test.rb86
6 files changed, 147 insertions, 6 deletions
diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb
index 2c72f49c6..93051fd66 100644
--- a/app/controllers/timelog_controller.rb
+++ b/app/controllers/timelog_controller.rb
@@ -45,7 +45,6 @@ class TimelogController < ApplicationController
sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
sort_update(@query.sortable_columns)
scope = time_entry_scope(:order => sort_clause).
- includes(:project, :user, :issue).
preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
respond_to do |format|
diff --git a/app/models/query.rb b/app/models/query.rb
index 9a1dc0d5a..fdfe8ec7e 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -74,6 +74,26 @@ class QueryColumn
end
end
+class QueryAssociationColumn < QueryColumn
+
+ def initialize(association, attribute, options={})
+ @association = association
+ @attribute = attribute
+ name_with_assoc = "#{association}.#{attribute}".to_sym
+ super(name_with_assoc, options)
+ end
+
+ def value_object(object)
+ if assoc = object.send(@association)
+ assoc.send @attribute
+ end
+ end
+
+ def css_classes
+ @css_classes ||= "#{@association}-#{@attribute}"
+ end
+end
+
class QueryCustomFieldColumn < QueryColumn
def initialize(custom_field, options={})
diff --git a/app/models/time_entry_query.rb b/app/models/time_entry_query.rb
index eefee0eb4..5d0bdee3b 100644
--- a/app/models/time_entry_query.rb
+++ b/app/models/time_entry_query.rb
@@ -27,6 +27,8 @@ class TimeEntryQuery < Query
QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
+ QueryAssociationColumn.new(:issue, :tracker, :caption => :field_tracker, :sortable => "#{Tracker.table_name}.position"),
+ QueryAssociationColumn.new(:issue, :status, :caption => :field_status, :sortable => "#{IssueStatus.table_name}.position"),
QueryColumn.new(:comments),
QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true),
]
@@ -71,6 +73,14 @@ class TimeEntryQuery < Query
end
add_available_filter("issue_id", :type => :tree, :label => :label_issue)
+ add_available_filter("issue.tracker_id",
+ :type => :list,
+ :name => l("label_attribute_of_issue", :name => l(:field_tracker)),
+ :values => Tracker.sorted.map {|t| [t.name, t.id.to_s]})
+ add_available_filter("issue.status_id",
+ :type => :list,
+ :name => l("label_attribute_of_issue", :name => l(:field_status)),
+ :values => IssueStatus.sorted.map {|s| [s.name, s.id.to_s]})
add_available_filter("issue.fixed_version_id",
:type => :list,
:name => l("label_attribute_of_issue", :name => l(:field_fixed_version)),
@@ -118,14 +128,16 @@ class TimeEntryQuery < Query
end
def base_scope
- TimeEntry.visible.where(statement)
+ TimeEntry.visible.
+ joins(:project, :user).
+ joins("LEFT OUTER JOIN issues ON issues.id = time_entries.issue_id").
+ where(statement)
end
def results_scope(options={})
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
- TimeEntry.visible.
- where(statement).
+ base_scope.
order(order_option).
joins(joins_for_order_statement(order_option.join(','))).
includes(:activity).
@@ -185,6 +197,14 @@ class TimeEntryQuery < Query
end
end
+ def sql_for_issue_tracker_id_field(field, operator, value)
+ sql_for_field("tracker_id", operator, value, Issue.table_name, "tracker_id")
+ end
+
+ def sql_for_issue_status_id_field(field, operator, value)
+ sql_for_field("status_id", operator, value, Issue.table_name, "status_id")
+ end
+
# Accepts :from/:to params as shortcut filters
def build_from_params(params)
super
@@ -197,4 +217,20 @@ class TimeEntryQuery < Query
end
self
end
+
+ def joins_for_order_statement(order_options)
+ joins = [super]
+
+ if order_options
+ if order_options.include?('issue_statuses')
+ joins << "LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
+ end
+ if order_options.include?('trackers')
+ joins << "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
+ end
+ end
+
+ joins.compact!
+ joins.any? ? joins.join(' ') : nil
+ end
end
diff --git a/app/views/timelog/_list.html.erb b/app/views/timelog/_list.html.erb
index 3a854ccef..be02adb5c 100644
--- a/app/views/timelog/_list.html.erb
+++ b/app/views/timelog/_list.html.erb
@@ -31,7 +31,7 @@
</td>
</tr>
<% end %>
- <tr class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
+ <tr id="time-entry-<%= entry.id %>" class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
<td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
<%= raw @query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, entry)}</td>"}.join %>
<td class="buttons">
diff --git a/lib/redmine/helpers/time_report.rb b/lib/redmine/helpers/time_report.rb
index 8991592af..e06499ea8 100644
--- a/lib/redmine/helpers/time_report.rb
+++ b/lib/redmine/helpers/time_report.rb
@@ -45,7 +45,7 @@ module Redmine
unless @criteria.empty?
time_columns = %w(tyear tmonth tweek spent_on)
@hours = []
- @scope.includes(:issue, :activity).
+ @scope.includes(:activity).
group(@criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns).
joins(@criteria.collect{|criteria| @available_criteria[criteria][:joins]}.compact).
sum(:hours).each do |hash, hours|
diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb
index 15a70404a..49ac0e136 100644
--- a/test/functional/timelog_controller_test.rb
+++ b/test/functional/timelog_controller_test.rb
@@ -797,6 +797,92 @@ class TimelogControllerTest < Redmine::ControllerTest
assert_equal [t3, t1, t2].map(&:id).map(&:to_s), css_select('input[name="ids[]"]').map {|e| e.attr('value')}
end
+ def test_index_with_issue_status_filter
+ Issue.where(:status_id => 4).update_all(:status_id => 2)
+ issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :status_id => 4)
+ entry = TimeEntry.generate!(:issue => issue, :hours => 4.5)
+
+ get :index, :params => {
+ :f => ['issue.status_id'],
+ :op => {'issue.status_id' => '='},
+ :v => {'issue.status_id' => ['4']}
+ }
+ assert_response :success
+ assert_equal [entry].map(&:id).map(&:to_s), css_select('input[name="ids[]"]').map {|e| e.attr('value')}
+ end
+
+ def test_index_with_issue_status_column
+ issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :status_id => 4)
+ entry = TimeEntry.generate!(:issue => issue)
+
+ get :index, :params => {
+ :c => %w(project spent_on issue comments hours issue.status)
+ }
+ assert_response :success
+ assert_select 'td.issue-status', :text => issue.status.name
+ end
+
+ def test_index_with_issue_status_sort
+ TimeEntry.delete_all
+ TimeEntry.generate!(:issue => Issue.generate!(:status_id => 1))
+ TimeEntry.generate!(:issue => Issue.generate!(:status_id => 5))
+ TimeEntry.generate!(:issue => Issue.generate!(:status_id => 3))
+ TimeEntry.generate!(:project_id => 1)
+
+ get :index, :params => {
+ :c => ["hours", 'issue.status'],
+ :sort => 'issue.status'
+ }
+ assert_response :success
+
+ # Make sure that values are properly sorted
+ values = css_select("td.issue-status").map(&:text).reject(&:blank?)
+ assert_equal IssueStatus.where(:id => [1, 5, 3]).sorted.pluck(:name), values
+ end
+
+ def test_index_with_issue_tracker_filter
+ Issue.where(:tracker_id => 2).update_all(:tracker_id => 1)
+ issue = Issue.generate!(:project_id => 1, :tracker_id => 2)
+ entry = TimeEntry.generate!(:issue => issue, :hours => 4.5)
+
+ get :index, :params => {
+ :f => ['issue.tracker_id'],
+ :op => {'issue.tracker_id' => '='},
+ :v => {'issue.tracker_id' => ['2']}
+ }
+ assert_response :success
+ assert_equal [entry].map(&:id).map(&:to_s), css_select('input[name="ids[]"]').map {|e| e.attr('value')}
+ end
+
+ def test_index_with_issue_tracker_column
+ issue = Issue.generate!(:project_id => 1, :tracker_id => 2)
+ entry = TimeEntry.generate!(:issue => issue)
+
+ get :index, :params => {
+ :c => %w(project spent_on issue comments hours issue.tracker)
+ }
+ assert_response :success
+ assert_select 'td.issue-tracker', :text => issue.tracker.name
+ end
+
+ def test_index_with_issue_tracker_sort
+ TimeEntry.delete_all
+ TimeEntry.generate!(:issue => Issue.generate!(:tracker_id => 1))
+ TimeEntry.generate!(:issue => Issue.generate!(:tracker_id => 3))
+ TimeEntry.generate!(:issue => Issue.generate!(:tracker_id => 2))
+ TimeEntry.generate!(:project_id => 1)
+
+ get :index, :params => {
+ :c => ["hours", 'issue.tracker'],
+ :sort => 'issue.tracker'
+ }
+ assert_response :success
+
+ # Make sure that values are properly sorted
+ values = css_select("td.issue-tracker").map(&:text).reject(&:blank?)
+ assert_equal Tracker.where(:id => [1, 2, 3]).sorted.pluck(:name), values
+ end
+
def test_index_with_filter_on_issue_custom_field
issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)