git-svn-id: http://svn.redmine.org/redmine/trunk@15649 e93f8b46-1217-0410-a6f0-8f06a7374b81tags/3.4.0
@@ -53,7 +53,6 @@ class TimelogController < ApplicationController | |||
@entry_count = scope.count | |||
@entry_pages = Paginator.new @entry_count, per_page_option, params['page'] | |||
@entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a | |||
@total_hours = scope.sum(:hours).to_f | |||
render :layout => !request.xhr? | |||
} |
@@ -33,28 +33,13 @@ module IssuesHelper | |||
end | |||
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) | |||
if first || group != previous_group | |||
if group.blank? && group != false | |||
group_name = "(#{l(:label_blank_value)})" | |||
else | |||
group_name = format_object(group) | |||
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 | |||
ancestors = [] | |||
grouped_query_results(issues, query, issue_count_by_group) do |issue, group_name, group_count, group_totals| | |||
while (ancestors.any? && !issue.is_descendant_of?(ancestors.last)) | |||
ancestors.pop | |||
end | |||
yield issue, level, group_name, group_count, group_totals | |||
previous_group, first = group, false | |||
yield issue, ancestors.size, group_name, group_count, group_totals | |||
ancestors << issue unless issue.leaf? | |||
end | |||
end | |||
@@ -78,6 +78,11 @@ module QueriesHelper | |||
query_filters_hidden_tags(query) + query_columns_hidden_tags(query) | |||
end | |||
def group_by_column_select_tag(query) | |||
options = [[]] + query.groupable_columns.collect {|c| [c.caption, c.name.to_s]} | |||
select_tag('group_by', options_for_select(options, @query.group_by)) | |||
end | |||
def available_block_columns_tags(query) | |||
tags = ''.html_safe | |||
query.available_block_columns.each do |column| | |||
@@ -108,6 +113,32 @@ module QueriesHelper | |||
render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name} | |||
end | |||
def grouped_query_results(items, query, item_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 | |||
items.each do |item| | |||
group_name = group_count = nil | |||
if query.grouped? | |||
group = query.group_by_column.value(item) | |||
if first || group != previous_group | |||
if group.blank? && group != false | |||
group_name = "(#{l(:label_blank_value)})" | |||
else | |||
group_name = format_object(group) | |||
end | |||
group_name ||= "" | |||
group_count = item_count_by_group ? item_count_by_group[group] : nil | |||
group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe | |||
end | |||
end | |||
yield item, group_name, group_count, group_totals | |||
previous_group, first = group, false | |||
end | |||
end | |||
def render_query_totals(query) | |||
return unless query.totalable_columns.present? | |||
totals = query.totalable_columns.map do |column| |
@@ -249,6 +249,10 @@ class IssueQuery < Query | |||
end | |||
end | |||
def default_totalable_names | |||
Setting.issue_list_default_totals.map(&:to_sym) | |||
end | |||
def base_scope | |||
Issue.visible.joins(:status, :project).where(statement) | |||
end |
@@ -76,11 +76,11 @@ end | |||
class QueryCustomFieldColumn < QueryColumn | |||
def initialize(custom_field) | |||
def initialize(custom_field, options={}) | |||
self.name = "cf_#{custom_field.id}".to_sym | |||
self.sortable = custom_field.order_statement || false | |||
self.groupable = custom_field.group_statement || false | |||
self.totalable = custom_field.totalable? | |||
self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable? | |||
@inline = true | |||
@cf = custom_field | |||
end | |||
@@ -120,8 +120,8 @@ end | |||
class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn | |||
def initialize(association, custom_field) | |||
super(custom_field) | |||
def initialize(association, custom_field, options={}) | |||
super(custom_field, options) | |||
self.name = "#{association}.cf_#{custom_field.id}".to_sym | |||
# TODO: support sorting/grouping by association custom field | |||
self.sortable = false | |||
@@ -546,6 +546,10 @@ class Query < ActiveRecord::Base | |||
[] | |||
end | |||
def default_totalable_names | |||
[] | |||
end | |||
def column_names=(names) | |||
if names | |||
names = names.select {|n| n.is_a?(Symbol) || !n.blank? } | |||
@@ -583,7 +587,7 @@ class Query < ActiveRecord::Base | |||
end | |||
def totalable_names | |||
options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || [] | |||
options[:totalable_names] || default_totalable_names || [] | |||
end | |||
def sort_criteria=(arg) |
@@ -28,7 +28,7 @@ class TimeEntryQuery < Query | |||
QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true), | |||
QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"), | |||
QueryColumn.new(:comments), | |||
QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours"), | |||
QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true), | |||
] | |||
def initialize(attributes=nil, *args) | |||
@@ -105,7 +105,7 @@ class TimeEntryQuery < Query | |||
@available_columns += TimeEntryCustomField.visible. | |||
map {|cf| QueryCustomFieldColumn.new(cf) } | |||
@available_columns += IssueCustomField.visible. | |||
map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) } | |||
map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) } | |||
@available_columns | |||
end | |||
@@ -113,6 +113,14 @@ class TimeEntryQuery < Query | |||
@default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours] | |||
end | |||
def default_totalable_names | |||
[:hours] | |||
end | |||
def base_scope | |||
TimeEntry.visible.where(statement) | |||
end | |||
def results_scope(options={}) | |||
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?) | |||
@@ -123,6 +131,11 @@ class TimeEntryQuery < Query | |||
includes(:activity). | |||
references(:activity) | |||
end | |||
# Returns sum of all the spent hours | |||
def total_for_hours(scope) | |||
map_total(scope.sum(:hours)) {|t| t.to_f.round(2)} | |||
end | |||
def sql_for_issue_id_field(field, operator, value) | |||
case operator |
@@ -7,65 +7,10 @@ | |||
<h2><%= @query.new_record? ? l(:label_issue_plural) : @query.name %></h2> | |||
<% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %> | |||
<%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project }, | |||
:method => :get, :id => 'query_form') do %> | |||
<div id="query_form_with_buttons" class="hide-when-print"> | |||
<%= hidden_field_tag 'set_filter', '1' %> | |||
<div id="query_form_content"> | |||
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>"> | |||
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> | |||
<div style="<%= @query.new_record? ? "" : "display: none;" %>"> | |||
<%= render :partial => 'queries/filters', :locals => {:query => @query} %> | |||
</div> | |||
</fieldset> | |||
<fieldset id="options" class="collapsible collapsed"> | |||
<legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend> | |||
<div style="display: none;"> | |||
<table> | |||
<tr> | |||
<td class="field"><%= l(:field_column_names) %></td> | |||
<td><%= render_query_columns_selection(@query) %></td> | |||
</tr> | |||
<tr> | |||
<td class="field"><label for='group_by'><%= l(:field_group_by) %></label></td> | |||
<td><%= select_tag('group_by', | |||
options_for_select( | |||
[[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, | |||
@query.group_by) | |||
) %></td> | |||
</tr> | |||
<tr> | |||
<td class="field"><%= l(:button_show) %></td> | |||
<td><%= available_block_columns_tags(@query) %></td> | |||
</tr> | |||
<tr> | |||
<td><%= l(:label_total_plural) %></td> | |||
<td><%= available_totalable_columns_tags(@query) %></td> | |||
</tr> | |||
</table> | |||
</div> | |||
</fieldset> | |||
</div> | |||
<p class="buttons"> | |||
<%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %> | |||
<%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> | |||
<% if @query.new_record? %> | |||
<% if User.current.allowed_to?(:save_queries, @project, :global => true) %> | |||
<%= link_to_function l(:button_save), | |||
"$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }').submit()", | |||
:class => 'icon icon-save' %> | |||
<% end %> | |||
<% else %> | |||
<% if @query.editable_by?(User.current) %> | |||
<%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %> | |||
<%= delete_link query_path(@query) %> | |||
<% end %> | |||
<% end %> | |||
</p> | |||
</div> | |||
<%= form_tag(_project_issues_path(@project), :method => :get, :id => 'query_form') do %> | |||
<%= render :partial => 'queries/query_form' %> | |||
<% end %> | |||
<%= error_messages_for 'query' %> | |||
<% if @query.valid? %> | |||
<% if @issues.empty? %> | |||
<p class="nodata"><%= l(:label_no_data) %></p> |
@@ -0,0 +1,62 @@ | |||
<%= hidden_field_tag 'set_filter', '1' %> | |||
<%= hidden_field_tag 'type', @query.type, :disabled => true, :id => 'query_type' %> | |||
<div id="query_form_with_buttons" class="hide-when-print"> | |||
<div id="query_form_content"> | |||
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>"> | |||
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> | |||
<div style="<%= @query.new_record? ? "" : "display: none;" %>"> | |||
<%= render :partial => 'queries/filters', :locals => {:query => @query} %> | |||
</div> | |||
</fieldset> | |||
<fieldset id="options" class="collapsible collapsed"> | |||
<legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend> | |||
<div style="display: none;"> | |||
<table> | |||
<tr> | |||
<td class="field"><%= l(:field_column_names) %></td> | |||
<td><%= render_query_columns_selection(@query) %></td> | |||
</tr> | |||
<% if @query.groupable_columns.any? %> | |||
<tr> | |||
<td class="field"><label for='group_by'><%= l(:field_group_by) %></label></td> | |||
<td><%= group_by_column_select_tag(@query) %></td> | |||
</tr> | |||
<% end %> | |||
<% if @query.available_block_columns.any? %> | |||
<tr> | |||
<td class="field"><%= l(:button_show) %></td> | |||
<td><%= available_block_columns_tags(@query) %></td> | |||
</tr> | |||
<% end %> | |||
<% if @query.available_totalable_columns.any? %> | |||
<tr> | |||
<td><%= l(:label_total_plural) %></td> | |||
<td><%= available_totalable_columns_tags(@query) %></td> | |||
</tr> | |||
<% end %> | |||
</table> | |||
</div> | |||
</fieldset> | |||
</div> | |||
<p class="buttons"> | |||
<%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %> | |||
<%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> | |||
<% if @query.new_record? %> | |||
<% if User.current.allowed_to?(:save_queries, @project, :global => true) %> | |||
<%= link_to_function l(:button_save), | |||
"$('#query_type').prop('disabled',false);$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }').submit()", | |||
:class => 'icon icon-save' %> | |||
<% end %> | |||
<% else %> | |||
<% if @query.editable_by?(User.current) %> | |||
<%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %> | |||
<%= delete_link query_path(@query) %> | |||
<% end %> | |||
<% end %> | |||
</p> | |||
</div> | |||
<%= error_messages_for @query %> |
@@ -1,44 +1,4 @@ | |||
<div id="query_form_with_buttons" class="hide-when-print"> | |||
<%= hidden_field_tag 'set_filter', '1' %> | |||
<div id="query_form_content"> | |||
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>"> | |||
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> | |||
<div style="<%= @query.new_record? ? "" : "display: none;" %>"> | |||
<%= render :partial => 'queries/filters', :locals => {:query => @query} %> | |||
</div> | |||
</fieldset> | |||
<fieldset id="options" class="collapsible collapsed"> | |||
<legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend> | |||
<div style="display: none;"> | |||
<table> | |||
<tr> | |||
<td class="field"><%= l(:field_column_names) %></td> | |||
<td><%= render_query_columns_selection(@query) %></td> | |||
</tr> | |||
</table> | |||
</div> | |||
</fieldset> | |||
</div> | |||
<p class="buttons"> | |||
<%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %> | |||
<%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %> | |||
<% if @query.new_record? %> | |||
<% if User.current.allowed_to?(:save_queries, @project, :global => true) %> | |||
<%= link_to_function l(:button_save), | |||
"$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }').submit()", | |||
:class => 'icon icon-save' %> | |||
<% end %> | |||
<% else %> | |||
<% if @query.editable_by?(User.current) %> | |||
<%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %> | |||
<%= delete_link query_path(@query) %> | |||
<% end %> | |||
<% end %> | |||
</p> | |||
<%= hidden_field_tag 'type', 'TimeEntryQuery' %> | |||
</div> | |||
<%= render :partial => 'queries/query_form' %> | |||
<div class="tabs hide-when-print"> | |||
<% query_params = request.query_parameters %> |
@@ -15,7 +15,22 @@ | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<% entries.each do |entry| -%> | |||
<% grouped_query_results(entries, @query, @entry_count_by_group) do |entry, 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> | |||
<span class="name"><%= group_name %></span> | |||
<% if group_count %> | |||
<span class="count"><%= group_count %></span> | |||
<% end %> | |||
<span class="totals"><%= group_totals %></span> | |||
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", | |||
"toggleAllRowGroups(this)", :class => 'toggle-all') %> | |||
</td> | |||
</tr> | |||
<% end %> | |||
<tr 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 %> | |||
@@ -32,6 +47,13 @@ | |||
<% end -%> | |||
</td> | |||
</tr> | |||
<% @query.block_columns.each do |column| | |||
if (text = column_content(column, issue)) && text.present? -%> | |||
<tr class="<%= current_cycle %>"> | |||
<td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>"><%= text %></td> | |||
</tr> | |||
<% end -%> | |||
<% end -%> | |||
<% end -%> | |||
</tbody> | |||
</table> |
@@ -10,11 +10,8 @@ | |||
<%= render :partial => 'date_range' %> | |||
<% end %> | |||
<div class="total-hours"> | |||
<p><%= l(:label_total_time) %>: <%= html_hours(l_hours(@total_hours)) %></p> | |||
</div> | |||
<% unless @entries.empty? %> | |||
<%= render_query_totals(@query) %> | |||
<%= render :partial => 'list', :locals => { :entries => @entries }%> | |||
<span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span> | |||
@@ -27,10 +27,6 @@ | |||
<% end %> | |||
<% unless @report.criteria.empty? %> | |||
<div class="total-hours"> | |||
<p><%= l(:label_total_time) %>: <%= html_hours(l_hours(@report.total_hours)) %></p> | |||
</div> | |||
<% unless @report.hours.empty? %> | |||
<div class="autoscroll"> | |||
<table class="list" id="time-report"> |
@@ -314,9 +314,10 @@ table.query-columns td.buttons { | |||
text-align: center; | |||
} | |||
table.query-columns td.buttons input[type=button] {width:35px;} | |||
.query-totals {text-align:right; margin-top:-2.3em;} | |||
.query-totals {text-align:right;} | |||
.query-totals>span {margin-left:0.6em;} | |||
.query-totals .value {font-weight:bold;} | |||
body.controller-issues .query-totals {margin-top:-2.3em;} | |||
td.center {text-align:center;} | |||
@@ -582,9 +582,8 @@ class TimelogControllerTest < ActionController::TestCase | |||
def test_index_all_projects | |||
get :index | |||
assert_response :success | |||
assert_template 'index' | |||
assert_not_nil assigns(:total_hours) | |||
assert_equal "162.90", "%.2f" % assigns(:total_hours) | |||
assert_select '.total-for-hours', :text => 'Hours: 162.90' | |||
assert_select 'form#query_form[action=?]', '/time_entries' | |||
end | |||
@@ -612,8 +611,8 @@ class TimelogControllerTest < ActionController::TestCase | |||
assert_equal 4, assigns(:entries).size | |||
# project and subproject | |||
assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort | |||
assert_not_nil assigns(:total_hours) | |||
assert_equal "162.90", "%.2f" % assigns(:total_hours) | |||
assert_select '.total-for-hours', :text => 'Hours: 162.90' | |||
assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries' | |||
end | |||
@@ -646,7 +645,7 @@ class TimelogControllerTest < ActionController::TestCase | |||
@request.session[:user_id] = 2 | |||
get :index, :project_id => 'ecookbook', :issue_id => issue.id.to_s, :set_filter => 1 | |||
assert_select '.total-hours', :text => 'Total time: 7.00 hours' | |||
assert_select '.total-for-hours', :text => 'Hours: 7.00' | |||
end | |||
def test_index_at_project_level_with_issue_fixed_version_id_short_filter | |||
@@ -657,7 +656,7 @@ class TimelogControllerTest < ActionController::TestCase | |||
@request.session[:user_id] = 2 | |||
get :index, :project_id => 'ecookbook', :"issue.fixed_version_id" => version.id.to_s, :set_filter => 1 | |||
assert_select '.total-hours', :text => 'Total time: 5.00 hours' | |||
assert_select '.total-for-hours', :text => 'Hours: 5.00' | |||
end | |||
def test_index_at_project_level_with_date_range | |||
@@ -669,8 +668,8 @@ class TimelogControllerTest < ActionController::TestCase | |||
assert_template 'index' | |||
assert_not_nil assigns(:entries) | |||
assert_equal 3, assigns(:entries).size | |||
assert_not_nil assigns(:total_hours) | |||
assert_equal "12.90", "%.2f" % assigns(:total_hours) | |||
assert_select '.total-for-hours', :text => 'Hours: 12.90' | |||
assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries' | |||
end | |||
@@ -680,8 +679,8 @@ class TimelogControllerTest < ActionController::TestCase | |||
assert_template 'index' | |||
assert_not_nil assigns(:entries) | |||
assert_equal 3, assigns(:entries).size | |||
assert_not_nil assigns(:total_hours) | |||
assert_equal "12.90", "%.2f" % assigns(:total_hours) | |||
assert_select '.total-for-hours', :text => 'Hours: 12.90' | |||
assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries' | |||
end | |||
@@ -691,9 +690,7 @@ class TimelogControllerTest < ActionController::TestCase | |||
:op => {'spent_on' => '>t-'}, | |||
:v => {'spent_on' => ['7']} | |||
assert_response :success | |||
assert_template 'index' | |||
assert_not_nil assigns(:entries) | |||
assert_not_nil assigns(:total_hours) | |||
assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries' | |||
end | |||