diff options
-rw-r--r-- | app/controllers/timelog_controller.rb | 1 | ||||
-rw-r--r-- | app/models/query.rb | 20 | ||||
-rw-r--r-- | app/models/time_entry_query.rb | 42 | ||||
-rw-r--r-- | app/views/timelog/_list.html.erb | 2 | ||||
-rw-r--r-- | lib/redmine/helpers/time_report.rb | 2 | ||||
-rw-r--r-- | test/functional/timelog_controller_test.rb | 86 |
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) |