mirror of
https://github.com/meineerde/redmine.git
synced 2026-02-01 03:57:15 +00:00
Display totals for each group on grouped queries (#1561).
git-svn-id: http://svn.redmine.org/redmine/trunk@14665 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
parent
3a3fe668c7
commit
498a429a41
@ -34,6 +34,10 @@ module IssuesHelper
|
||||
|
||||
def grouped_issue_list(issues, query, issue_count_by_group, &block)
|
||||
previous_group, first = false, true
|
||||
totals_by_group = query.totalable_columns.inject({}) do |h, column|
|
||||
h[column] = query.total_by_group_for(column)
|
||||
h
|
||||
end
|
||||
issue_list(issues) do |issue, level|
|
||||
group_name = group_count = nil
|
||||
if query.grouped? && ((group = query.group_by_column.value(issue)) != previous_group || first)
|
||||
@ -44,8 +48,9 @@ module IssuesHelper
|
||||
end
|
||||
group_name ||= ""
|
||||
group_count = issue_count_by_group[group]
|
||||
group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
|
||||
end
|
||||
yield issue, level, group_name, group_count
|
||||
yield issue, level, group_name, group_count, group_totals
|
||||
previous_group, first = group, false
|
||||
end
|
||||
end
|
||||
|
||||
@ -108,13 +108,17 @@ module QueriesHelper
|
||||
def render_query_totals(query)
|
||||
return unless query.totalable_columns.present?
|
||||
totals = query.totalable_columns.map do |column|
|
||||
label = content_tag('span', "#{column.caption}:")
|
||||
value = content_tag('span', " #{query.total_for(column)}", :class => 'value')
|
||||
content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
|
||||
total_tag(column, query.total_for(column))
|
||||
end
|
||||
content_tag('p', totals.join(" ").html_safe, :class => "query-totals")
|
||||
end
|
||||
|
||||
def total_tag(column, value)
|
||||
label = content_tag('span', "#{column.caption}:")
|
||||
value = content_tag('span', format_object(value), :class => 'value')
|
||||
content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
|
||||
end
|
||||
|
||||
def column_header(column)
|
||||
column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
|
||||
:default_order => column.default_order) :
|
||||
|
||||
@ -303,7 +303,6 @@ class IssueQuery < Query
|
||||
def base_scope
|
||||
Issue.visible.joins(:status, :project).where(statement)
|
||||
end
|
||||
private :base_scope
|
||||
|
||||
# Returns the issue count
|
||||
def issue_count
|
||||
@ -312,55 +311,21 @@ class IssueQuery < Query
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
# Returns the issue count by group or nil if query is not grouped
|
||||
def issue_count_by_group
|
||||
grouped_query do |scope|
|
||||
scope.count
|
||||
end
|
||||
end
|
||||
|
||||
# Returns sum of all the issue's estimated_hours
|
||||
def total_for_estimated_hours
|
||||
base_scope.sum(:estimated_hours).to_f.round(2)
|
||||
def total_for_estimated_hours(scope)
|
||||
scope.sum(:estimated_hours)
|
||||
end
|
||||
|
||||
# Returns sum of all the issue's time entries hours
|
||||
def total_for_spent_hours
|
||||
base_scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f.round(2)
|
||||
end
|
||||
|
||||
def total_for_custom_field(custom_field)
|
||||
base_scope.joins(:custom_values).
|
||||
where(:custom_values => {:custom_field_id => custom_field.id}).
|
||||
where.not(:custom_values => {:value => ''}).
|
||||
sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
|
||||
end
|
||||
private :total_for_custom_field
|
||||
|
||||
def total_for_float_custom_field(custom_field)
|
||||
total_for_custom_field(custom_field).to_f
|
||||
end
|
||||
|
||||
def total_for_int_custom_field(custom_field)
|
||||
total_for_custom_field(custom_field).to_i
|
||||
end
|
||||
|
||||
# Returns the issue count by group or nil if query is not grouped
|
||||
def issue_count_by_group
|
||||
r = nil
|
||||
if grouped?
|
||||
begin
|
||||
# Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
|
||||
r = Issue.visible.
|
||||
joins(:status, :project).
|
||||
where(statement).
|
||||
joins(joins_for_order_statement(group_by_statement)).
|
||||
group(group_by_statement).
|
||||
count
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
r = {nil => issue_count}
|
||||
end
|
||||
c = group_by_column
|
||||
if c.is_a?(QueryCustomFieldColumn)
|
||||
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
|
||||
end
|
||||
end
|
||||
r
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
def total_for_spent_hours(scope)
|
||||
scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours")
|
||||
end
|
||||
|
||||
# Returns the issues
|
||||
|
||||
@ -632,21 +632,99 @@ class Query < ActiveRecord::Base
|
||||
|
||||
# Returns the sum of values for the given column
|
||||
def total_for(column)
|
||||
total_with_scope(column, base_scope)
|
||||
end
|
||||
|
||||
# Returns a hash of the sum of the given column for each group,
|
||||
# or nil if the query is not grouped
|
||||
def total_by_group_for(column)
|
||||
grouped_query do |scope|
|
||||
total_with_scope(column, scope)
|
||||
end
|
||||
end
|
||||
|
||||
def totals
|
||||
totals = totalable_columns.map {|column| [column, total_for(column)]}
|
||||
yield totals if block_given?
|
||||
totals
|
||||
end
|
||||
|
||||
def totals_by_group
|
||||
totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
|
||||
yield totals if block_given?
|
||||
totals
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def grouped_query(&block)
|
||||
r = nil
|
||||
if grouped?
|
||||
begin
|
||||
# Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
|
||||
r = yield base_group_scope
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
r = {nil => yield(base_scope)}
|
||||
end
|
||||
c = group_by_column
|
||||
if c.is_a?(QueryCustomFieldColumn)
|
||||
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
|
||||
end
|
||||
end
|
||||
r
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
def total_with_scope(column, scope)
|
||||
unless column.is_a?(QueryColumn)
|
||||
column = column.to_sym
|
||||
column = available_totalable_columns.detect {|c| c.name == column}
|
||||
end
|
||||
if column.is_a?(QueryCustomFieldColumn)
|
||||
custom_field = column.custom_field
|
||||
send "total_for_#{custom_field.field_format}_custom_field", custom_field
|
||||
send "total_for_#{custom_field.field_format}_custom_field", custom_field, scope
|
||||
else
|
||||
send "total_for_#{column.name}"
|
||||
send "total_for_#{column.name}", scope
|
||||
end
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
def base_scope
|
||||
raise "unimplemented"
|
||||
end
|
||||
|
||||
def base_group_scope
|
||||
base_scope.
|
||||
joins(joins_for_order_statement(group_by_statement)).
|
||||
group(group_by_statement)
|
||||
end
|
||||
|
||||
def total_for_float_custom_field(custom_field, scope)
|
||||
total_for_custom_field(custom_field, scope) {|t| t.to_f.round(2)}
|
||||
end
|
||||
|
||||
def total_for_int_custom_field(custom_field, scope)
|
||||
total_for_custom_field(custom_field, scope) {|t| t.to_i}
|
||||
end
|
||||
|
||||
def total_for_custom_field(custom_field, scope)
|
||||
total = scope.joins(:custom_values).
|
||||
where(:custom_values => {:custom_field_id => custom_field.id}).
|
||||
where.not(:custom_values => {:value => ''}).
|
||||
sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
|
||||
|
||||
if block_given?
|
||||
if total.is_a?(Hash)
|
||||
total.keys.each {|k| total[k] = yield total[k]}
|
||||
else
|
||||
total = yield total
|
||||
end
|
||||
end
|
||||
|
||||
total
|
||||
end
|
||||
|
||||
def sql_for_custom_field(field, operator, value, custom_field_id)
|
||||
db_table = CustomValue.table_name
|
||||
|
||||
@ -15,13 +15,13 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% grouped_issue_list(issues, @query, @issue_count_by_group) do |issue, level, group_name, group_count| -%>
|
||||
<% grouped_issue_list(issues, @query, @issue_count_by_group) do |issue, level, group_name, group_count, group_totals| -%>
|
||||
<% if group_name %>
|
||||
<% reset_cycle %>
|
||||
<tr class="group open">
|
||||
<td colspan="<%= query.inline_columns.size + 2 %>">
|
||||
<span class="expander" onclick="toggleRowGroup(this);"> </span>
|
||||
<%= group_name %> <span class="count"><%= group_count %></span>
|
||||
<span class="name"><%= group_name %></span> <span class="count"><%= group_count %></span> <span class="totals"><%= group_totals %></span>
|
||||
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
|
||||
"toggleAllRowGroups(this)", :class => 'toggle-all') %>
|
||||
</td>
|
||||
|
||||
@ -231,9 +231,12 @@ table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px;
|
||||
table.plugins span.description { display: block; font-size: 0.9em; }
|
||||
table.plugins span.url { display: block; font-size: 0.9em; }
|
||||
|
||||
table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; text-align:left; }
|
||||
table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
|
||||
tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
|
||||
tr.group td { padding: 0.8em 0 0.5em 0.3em; border-bottom: 1px solid #ccc; text-align:left; }
|
||||
tr.group span.name {font-weight:bold;}
|
||||
tr.group span.count {font-weight:bold; position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
|
||||
tr.group span.totals {color: #aaa; font-size: 80%;}
|
||||
tr.group span.totals .value {font-weight:bold; color:#777;}
|
||||
tr.group a.toggle-all { color: #aaa; font-size: 80%; display:none; float:right; margin-right:4px;}
|
||||
tr.group:hover a.toggle-all { display:inline;}
|
||||
a.toggle-all:hover {text-decoration:none;}
|
||||
|
||||
|
||||
@ -953,10 +953,32 @@ class IssuesControllerTest < ActionController::TestCase
|
||||
get :index, :t => %w(estimated_hours)
|
||||
assert_response :success
|
||||
assert_select '.query-totals'
|
||||
assert_select '.total-for-estimated-hours span.value', :text => '6.6'
|
||||
assert_select '.total-for-estimated-hours span.value', :text => '6.60'
|
||||
assert_select 'input[type=checkbox][name=?][value=estimated_hours][checked=checked]', 't[]'
|
||||
end
|
||||
|
||||
def test_index_with_grouped_query_and_estimated_hours_total
|
||||
Issue.delete_all
|
||||
Issue.generate!(:estimated_hours => 5.5, :category_id => 1)
|
||||
Issue.generate!(:estimated_hours => 2.3, :category_id => 1)
|
||||
Issue.generate!(:estimated_hours => 1.1, :category_id => 2)
|
||||
Issue.generate!(:estimated_hours => 4.6)
|
||||
|
||||
get :index, :t => %w(estimated_hours), :group_by => 'category'
|
||||
assert_response :success
|
||||
assert_select '.query-totals'
|
||||
assert_select '.query-totals .total-for-estimated-hours span.value', :text => '13.50'
|
||||
assert_select 'tr.group', :text => /Printing/ do
|
||||
assert_select '.total-for-estimated-hours span.value', :text => '7.80'
|
||||
end
|
||||
assert_select 'tr.group', :text => /Recipes/ do
|
||||
assert_select '.total-for-estimated-hours span.value', :text => '1.10'
|
||||
end
|
||||
assert_select 'tr.group', :text => /blank/ do
|
||||
assert_select '.total-for-estimated-hours span.value', :text => '4.60'
|
||||
end
|
||||
end
|
||||
|
||||
def test_index_with_int_custom_field_total
|
||||
field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
|
||||
CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
|
||||
|
||||
@ -1183,6 +1183,19 @@ class QueryTest < ActiveSupport::TestCase
|
||||
assert_equal 6.6, q.total_for(:estimated_hours)
|
||||
end
|
||||
|
||||
def test_total_by_group_for_estimated_hours
|
||||
Issue.delete_all
|
||||
Issue.generate!(:estimated_hours => 5.5, :assigned_to_id => 2)
|
||||
Issue.generate!(:estimated_hours => 1.1, :assigned_to_id => 3)
|
||||
Issue.generate!(:estimated_hours => 3.5)
|
||||
|
||||
q = IssueQuery.new(:group_by => 'assigned_to')
|
||||
assert_equal(
|
||||
{nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
|
||||
q.total_by_group_for(:estimated_hours)
|
||||
)
|
||||
end
|
||||
|
||||
def test_total_for_spent_hours
|
||||
TimeEntry.delete_all
|
||||
TimeEntry.generate!(:hours => 5.5)
|
||||
@ -1192,6 +1205,20 @@ class QueryTest < ActiveSupport::TestCase
|
||||
assert_equal 6.6, q.total_for(:spent_hours)
|
||||
end
|
||||
|
||||
def test_total_by_group_for_spent_hours
|
||||
TimeEntry.delete_all
|
||||
TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
|
||||
TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
|
||||
Issue.where(:id => 1).update_all(:assigned_to_id => 2)
|
||||
Issue.where(:id => 2).update_all(:assigned_to_id => 3)
|
||||
|
||||
q = IssueQuery.new(:group_by => 'assigned_to')
|
||||
assert_equal(
|
||||
{User.find(2) => 5.5, User.find(3) => 1.1},
|
||||
q.total_by_group_for(:spent_hours)
|
||||
)
|
||||
end
|
||||
|
||||
def test_total_for_int_custom_field
|
||||
field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
|
||||
CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
|
||||
@ -1202,6 +1229,20 @@ class QueryTest < ActiveSupport::TestCase
|
||||
assert_equal 9, q.total_for("cf_#{field.id}")
|
||||
end
|
||||
|
||||
def test_total_by_group_for_int_custom_field
|
||||
field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
|
||||
CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
|
||||
CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
|
||||
Issue.where(:id => 1).update_all(:assigned_to_id => 2)
|
||||
Issue.where(:id => 2).update_all(:assigned_to_id => 3)
|
||||
|
||||
q = IssueQuery.new(:group_by => 'assigned_to')
|
||||
assert_equal(
|
||||
{User.find(2) => 2, User.find(3) => 7},
|
||||
q.total_by_group_for("cf_#{field.id}")
|
||||
)
|
||||
end
|
||||
|
||||
def test_total_for_float_custom_field
|
||||
field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
|
||||
CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2.3')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user