Patch #1028 ยป cleanup_timelog.diff
| app/controllers/timelog_controller.rb (working copy) | ||
|---|---|---|
| 30 | 30 |
include CustomFieldsHelper |
| 31 | 31 |
|
| 32 | 32 |
def report |
| 33 |
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
|
|
| 33 |
@available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
|
|
| 34 | 34 |
:klass => Project, |
| 35 | 35 |
:label => :label_project}, |
| 36 | 36 |
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
|
| ... | ... | |
| 53 | 53 |
:label => :label_issue} |
| 54 | 54 |
} |
| 55 | 55 |
|
| 56 |
# Add list and boolean custom fields as available criterias
|
|
| 56 |
# Add list and boolean custom fields as available criteria |
|
| 57 | 57 |
@project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
|
| 58 |
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)",
|
|
| 58 |
@available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)",
|
|
| 59 | 59 |
:format => cf.field_format, |
| 60 | 60 |
:label => cf.name} |
| 61 | 61 |
end |
| 62 | 62 |
|
| 63 |
@criterias = params[:criterias] || []
|
|
| 64 |
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
|
|
| 65 |
@criterias.uniq!
|
|
| 66 |
@criterias = @criterias[0,3]
|
|
| 63 |
@criteria = params[:criteria] || []
|
|
| 64 |
@criteria = @criteria.select{|criterion| @available_criteria.has_key? criterion}
|
|
| 65 |
@criteria.uniq! |
|
| 66 |
@criteria = @criteria[0,3]
|
|
| 67 | 67 |
|
| 68 | 68 |
@columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month' |
| 69 | 69 |
|
| 70 | 70 |
retrieve_date_range |
| 71 | 71 |
|
| 72 |
unless @criterias.empty?
|
|
| 73 |
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
|
|
| 74 |
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
|
|
| 72 |
unless @criteria.empty? |
|
| 73 |
sql_select = @criteria.collect{|criterion| @available_criteria[criterion][:sql] + " AS " + criterion}.join(', ')
|
|
| 74 |
sql_group_by = @criteria.collect{|criterion| @available_criteria[criterion][:sql]}.join(', ')
|
|
| 75 | 75 |
|
| 76 | 76 |
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
|
| 77 | 77 |
sql << " FROM #{TimeEntry.table_name}"
|
| ... | ... | |
| 123 | 123 |
|
| 124 | 124 |
respond_to do |format| |
| 125 | 125 |
format.html { render :layout => !request.xhr? }
|
| 126 |
format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
|
|
| 126 |
format.csv { send_data(report_to_csv(@criteria, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
|
|
| 127 | 127 |
end |
| 128 | 128 |
end |
| 129 | 129 |
|
| app/helpers/timelog_helper.rb (working copy) | ||
|---|---|---|
| 16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 17 | 17 | |
| 18 | 18 |
module TimelogHelper |
| 19 |
def select_hours(data, criteria, value)
|
|
| 20 |
data.select {|row| row[criteria] == value}
|
|
| 19 |
def select_hours(data, criterion, value)
|
|
| 20 |
data.select {|row| row[criterion] == value}
|
|
| 21 | 21 |
end |
| 22 | 22 |
|
| 23 | 23 |
def sum_hours(data) |
| ... | ... | |
| 77 | 77 |
export |
| 78 | 78 |
end |
| 79 | 79 |
|
| 80 |
def format_criteria_value(criteria, value)
|
|
| 81 |
value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
|
|
| 80 |
def format_criterion_value(criterion, value)
|
|
| 81 |
value.blank? ? l(:label_none) : ((k = @available_criteria[criterion][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criteria[criterion][:format]))
|
|
| 82 | 82 |
end |
| 83 | 83 |
|
| 84 |
def report_to_csv(criterias, periods, hours)
|
|
| 84 |
def report_to_csv(criteria, periods, hours) |
|
| 85 | 85 |
export = StringIO.new |
| 86 | 86 |
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| |
| 87 | 87 |
# Column headers |
| 88 |
headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
|
|
| 88 |
headers = criteria.collect {|criterion| l(@available_criteria[criterion][:label]) }
|
|
| 89 | 89 |
headers += periods |
| 90 | 90 |
headers << l(:label_total) |
| 91 | 91 |
csv << headers.collect {|c| to_utf8(c) }
|
| 92 | 92 |
# Content |
| 93 |
report_criteria_to_csv(csv, criterias, periods, hours)
|
|
| 93 |
report_criteria_to_csv(csv, criteria, periods, hours) |
|
| 94 | 94 |
# Total row |
| 95 |
row = [ l(:label_total) ] + [''] * (criterias.size - 1)
|
|
| 95 |
row = [ l(:label_total) ] + [''] * (criteria.size - 1) |
|
| 96 | 96 |
total = 0 |
| 97 | 97 |
periods.each do |period| |
| 98 | 98 |
sum = sum_hours(select_hours(hours, @columns, period.to_s)) |
| ... | ... | |
| 106 | 106 |
export |
| 107 | 107 |
end |
| 108 | 108 |
|
| 109 |
def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
|
|
| 110 |
hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
|
|
| 111 |
hours_for_value = select_hours(hours, criterias[level], value)
|
|
| 109 |
def report_criteria_to_csv(csv, criteria, periods, hours, level=0) |
|
| 110 |
hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
|
|
| 111 |
hours_for_value = select_hours(hours, criteria[level], value) |
|
| 112 | 112 |
next if hours_for_value.empty? |
| 113 | 113 |
row = [''] * level |
| 114 |
row << to_utf8(format_criteria_value(criterias[level], value))
|
|
| 115 |
row += [''] * (criterias.length - level - 1)
|
|
| 114 |
row << to_utf8(format_criterion_value(criteria[level], value))
|
|
| 115 |
row += [''] * (criteria.length - level - 1) |
|
| 116 | 116 |
total = 0 |
| 117 | 117 |
periods.each do |period| |
| 118 | 118 |
sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)) |
| ... | ... | |
| 122 | 122 |
row << "%.2f" %total |
| 123 | 123 |
csv << row |
| 124 | 124 |
|
| 125 |
if criterias.length > level + 1
|
|
| 126 |
report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
|
|
| 125 |
if criteria.length > level + 1 |
|
| 126 |
report_criteria_to_csv(csv, criteria, periods, hours_for_value, level + 1) |
|
| 127 | 127 |
end |
| 128 | 128 |
end |
| 129 | 129 |
end |
| app/views/timelog/report.rhtml (working copy) | ||
|---|---|---|
| 5 | 5 |
<h2><%= l(:label_spent_time) %></h2> |
| 6 | 6 | |
| 7 | 7 |
<% form_remote_tag(:url => {}, :update => 'content') do %>
|
| 8 |
<% @criterias.each do |criteria| %>
|
|
| 9 |
<%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
|
|
| 8 |
<% @criteria.each do |criterion| %>
|
|
| 9 |
<%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
|
|
| 10 | 10 |
<% end %> |
| 11 | 11 |
<%= hidden_field_tag 'project_id', params[:project_id] %> |
| 12 | 12 |
<%= render :partial => 'date_range' %> |
| ... | ... | |
| 17 | 17 |
[l(:label_day_plural).titleize, 'day']], @columns), |
| 18 | 18 |
:onchange => "this.form.onsubmit();" %> |
| 19 | 19 | |
| 20 |
<%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
|
|
| 20 |
<%= l(:button_add) %>: <%= select_tag('criteria[]', options_for_select([[]] + (@available_criteria.keys - @criteria).collect{|k| [l(@available_criteria[k][:label]), k]}),
|
|
| 21 | 21 |
:onchange => "this.form.onsubmit();", |
| 22 | 22 |
:style => 'width: 200px', |
| 23 | 23 |
:id => nil, |
| 24 |
:disabled => (@criterias.length >= 3)) %>
|
|
| 24 |
:disabled => (@criteria.length >= 3)) %> |
|
| 25 | 25 |
<%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns},
|
| 26 | 26 |
:update => 'content' |
| 27 | 27 |
}, :class => 'icon icon-reload' %></p> |
| 28 | 28 |
<% end %> |
| 29 | 29 | |
| 30 |
<% unless @criterias.empty? %>
|
|
| 30 |
<% unless @criteria.empty? %> |
|
| 31 | 31 |
<div class="total-hours"> |
| 32 | 32 |
<p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p> |
| 33 | 33 |
</div> |
| ... | ... | |
| 36 | 36 |
<table class="list" id="time-report"> |
| 37 | 37 |
<thead> |
| 38 | 38 |
<tr> |
| 39 |
<% @criterias.each do |criteria| %>
|
|
| 40 |
<th><%= l(@available_criterias[criteria][:label]) %></th>
|
|
| 39 |
<% @criteria.each do |criterion| %>
|
|
| 40 |
<th><%= l(@available_criteria[criterion][:label]) %></th>
|
|
| 41 | 41 |
<% end %> |
| 42 | 42 |
<% columns_width = (40 / (@periods.length+1)).to_i %> |
| 43 | 43 |
<% @periods.each do |period| %> |
| ... | ... | |
| 47 | 47 |
</tr> |
| 48 | 48 |
</thead> |
| 49 | 49 |
<tbody> |
| 50 |
<%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
|
|
| 50 |
<%= render :partial => 'report_criteria', :locals => {:criteria => @criteria, :hours => @hours, :level => 0} %>
|
|
| 51 | 51 |
<tr class="total"> |
| 52 | 52 |
<td><%= l(:label_total) %></td> |
| 53 |
<%= '<td></td>' * (@criterias.size - 1) %>
|
|
| 53 |
<%= '<td></td>' * (@criteria.size - 1) %> |
|
| 54 | 54 |
<% total = 0 -%> |
| 55 | 55 |
<% @periods.each do |period| -%> |
| 56 | 56 |
<% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%> |
| app/views/timelog/_report_criteria.rhtml (working copy) | ||
|---|---|---|
| 1 |
<% @hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %>
|
|
| 2 |
<% hours_for_value = select_hours(hours, criterias[level], value) -%>
|
|
| 1 |
<% @hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value| %>
|
|
| 2 |
<% hours_for_value = select_hours(hours, criteria[level], value) -%> |
|
| 3 | 3 |
<% next if hours_for_value.empty? -%> |
| 4 |
<tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criterias.length > level+1 %>">
|
|
| 4 |
<tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criteria.length > level+1 %>">
|
|
| 5 | 5 |
<%= '<td></td>' * level %> |
| 6 |
<td><%= format_criteria_value(criterias[level], value) %></td>
|
|
| 7 |
<%= '<td></td>' * (criterias.length - level - 1) -%>
|
|
| 6 |
<td><%= format_criterion_value(criteria[level], value) %></td>
|
|
| 7 |
<%= '<td></td>' * (criteria.length - level - 1) -%> |
|
| 8 | 8 |
<% total = 0 -%> |
| 9 | 9 |
<% @periods.each do |period| -%> |
| 10 | 10 |
<% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)); total += sum -%> |
| ... | ... | |
| 12 | 12 |
<% end -%> |
| 13 | 13 |
<td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
|
| 14 | 14 |
</tr> |
| 15 |
<% if criterias.length > level+1 -%>
|
|
| 16 |
<%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %>
|
|
| 15 |
<% if criteria.length > level+1 -%> |
|
| 16 |
<%= render(:partial => 'report_criteria', :locals => {:criteria => criteria, :hours => hours_for_value, :level => (level + 1)}) %>
|
|
| 17 | 17 |
<% end -%> |
| 18 | 18 | |
| 19 | 19 |
<% end %> |
| test/functional/timelog_controller_test.rb (working copy) | ||
|---|---|---|
| 80 | 80 |
end |
| 81 | 81 | |
| 82 | 82 |
def test_report_all_time |
| 83 |
get :report, :project_id => 1, :criterias => ['project', 'issue']
|
|
| 83 |
get :report, :project_id => 1, :criteria => ['project', 'issue'] |
|
| 84 | 84 |
assert_response :success |
| 85 | 85 |
assert_template 'report' |
| 86 | 86 |
assert_not_nil assigns(:total_hours) |
| ... | ... | |
| 88 | 88 |
end |
| 89 | 89 | |
| 90 | 90 |
def test_report_all_time_by_day |
| 91 |
get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
|
|
| 91 |
get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day' |
|
| 92 | 92 |
assert_response :success |
| 93 | 93 |
assert_template 'report' |
| 94 | 94 |
assert_not_nil assigns(:total_hours) |
| ... | ... | |
| 96 | 96 |
assert_tag :tag => 'th', :content => '2007-03-12' |
| 97 | 97 |
end |
| 98 | 98 |
|
| 99 |
def test_report_one_criteria
|
|
| 100 |
get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
|
|
| 99 |
def test_report_one_criterion
|
|
| 100 |
get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project'] |
|
| 101 | 101 |
assert_response :success |
| 102 | 102 |
assert_template 'report' |
| 103 | 103 |
assert_not_nil assigns(:total_hours) |
| 104 | 104 |
assert_equal "8.65", "%.2f" % assigns(:total_hours) |
| 105 | 105 |
end |
| 106 | 106 |
|
| 107 |
def test_report_two_criterias
|
|
| 108 |
get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
|
|
| 107 |
def test_report_two_criteria |
|
| 108 |
get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"] |
|
| 109 | 109 |
assert_response :success |
| 110 | 110 |
assert_template 'report' |
| 111 | 111 |
assert_not_nil assigns(:total_hours) |
| 112 | 112 |
assert_equal "162.90", "%.2f" % assigns(:total_hours) |
| 113 | 113 |
end |
| 114 | 114 |
|
| 115 |
def test_report_custom_field_criteria
|
|
| 116 |
get :report, :project_id => 1, :criterias => ['project', 'cf_1']
|
|
| 115 |
def test_report_custom_field_criterion
|
|
| 116 |
get :report, :project_id => 1, :criteria => ['project', 'cf_1'] |
|
| 117 | 117 |
assert_response :success |
| 118 | 118 |
assert_template 'report' |
| 119 | 119 |
assert_not_nil assigns(:total_hours) |
| 120 |
assert_not_nil assigns(:criterias)
|
|
| 121 |
assert_equal 2, assigns(:criterias).size
|
|
| 120 |
assert_not_nil assigns(:criteria) |
|
| 121 |
assert_equal 2, assigns(:criteria).size |
|
| 122 | 122 |
assert_equal "162.90", "%.2f" % assigns(:total_hours) |
| 123 | 123 |
# Custom field column |
| 124 | 124 |
assert_tag :tag => 'th', :content => 'Database' |
| ... | ... | |
| 129 | 129 |
:content => '1' }} |
| 130 | 130 |
end |
| 131 | 131 |
|
| 132 |
def test_report_one_criteria_no_result
|
|
| 133 |
get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
|
|
| 132 |
def test_report_one_criterion_no_result
|
|
| 133 |
get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project'] |
|
| 134 | 134 |
assert_response :success |
| 135 | 135 |
assert_template 'report' |
| 136 | 136 |
assert_not_nil assigns(:total_hours) |
| ... | ... | |
| 138 | 138 |
end |
| 139 | 139 |
|
| 140 | 140 |
def test_report_csv_export |
| 141 |
get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
|
|
| 141 |
get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criteria => ["project", "member", "activity"], :format => "csv" |
|
| 142 | 142 |
assert_response :success |
| 143 | 143 |
assert_equal 'text/csv', @response.content_type |
| 144 | 144 |
lines = @response.body.chomp.split("\n")
|