##// END OF EJS Templates
Extracted time report logic from the controller....
Jean-Philippe Lang -
r7906:caf898d7d13f
parent child
Show More
@@ -0,0 +1,164
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module Helpers
20 class TimeReport
21 attr_reader :criteria, :columns, :from, :to, :hours, :total_hours, :periods
22
23 def initialize(project, issue, criteria, columns, from, to)
24 @project = project
25 @issue = issue
26
27 @criteria = criteria || []
28 @criteria = @criteria.select{|criteria| available_criteria.has_key? criteria}
29 @criteria.uniq!
30 @criteria = @criteria[0,3]
31
32 @columns = (columns && %w(year month week day).include?(columns)) ? columns : 'month'
33 @from = from
34 @to = to
35
36 run
37 end
38
39 def available_criteria
40 @available_criteria || load_available_criteria
41 end
42
43 private
44
45 def run
46 unless @criteria.empty?
47 sql_select = @criteria.collect{|criteria| @available_criteria[criteria][:sql] + " AS " + criteria}.join(', ')
48 sql_group_by = @criteria.collect{|criteria| @available_criteria[criteria][:sql]}.join(', ')
49 sql_condition = ''
50
51 if @project.nil?
52 sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
53 elsif @issue.nil?
54 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
55 else
56 sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
57 end
58
59 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
60 sql << " FROM #{TimeEntry.table_name}"
61 sql << time_report_joins
62 sql << " WHERE"
63 sql << " (%s) AND" % sql_condition
64 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
65 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
66
67 @hours = ActiveRecord::Base.connection.select_all(sql)
68
69 @hours.each do |row|
70 case @columns
71 when 'year'
72 row['year'] = row['tyear']
73 when 'month'
74 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
75 when 'week'
76 row['week'] = "#{row['tyear']}-#{row['tweek']}"
77 when 'day'
78 row['day'] = "#{row['spent_on']}"
79 end
80 end
81
82 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
83
84 @periods = []
85 # Date#at_beginning_of_ not supported in Rails 1.2.x
86 date_from = @from.to_time
87 # 100 columns max
88 while date_from <= @to.to_time && @periods.length < 100
89 case @columns
90 when 'year'
91 @periods << "#{date_from.year}"
92 date_from = (date_from + 1.year).at_beginning_of_year
93 when 'month'
94 @periods << "#{date_from.year}-#{date_from.month}"
95 date_from = (date_from + 1.month).at_beginning_of_month
96 when 'week'
97 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
98 date_from = (date_from + 7.day).at_beginning_of_week
99 when 'day'
100 @periods << "#{date_from.to_date}"
101 date_from = date_from + 1.day
102 end
103 end
104 end
105 end
106
107 def load_available_criteria
108 @available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
109 :klass => Project,
110 :label => :label_project},
111 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
112 :klass => Version,
113 :label => :label_version},
114 'category' => {:sql => "#{Issue.table_name}.category_id",
115 :klass => IssueCategory,
116 :label => :field_category},
117 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
118 :klass => User,
119 :label => :label_member},
120 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
121 :klass => Tracker,
122 :label => :label_tracker},
123 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
124 :klass => TimeEntryActivity,
125 :label => :label_activity},
126 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
127 :klass => Issue,
128 :label => :label_issue}
129 }
130
131 # Add list and boolean custom fields as available criteria
132 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
133 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
134 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
135 :format => cf.field_format,
136 :label => cf.name}
137 end if @project
138
139 # Add list and boolean time entry custom fields
140 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
141 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
142 :format => cf.field_format,
143 :label => cf.name}
144 end
145
146 # Add list and boolean time entry activity custom fields
147 TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
148 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
149 :format => cf.field_format,
150 :label => cf.name}
151 end
152
153 @available_criteria
154 end
155
156 def time_report_joins
157 sql = ''
158 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
159 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
160 sql
161 end
162 end
163 end
164 end
@@ -1,209 +1,82
1 class TimeEntryReportsController < ApplicationController
1 class TimeEntryReportsController < ApplicationController
2 menu_item :issues
2 menu_item :issues
3 before_filter :find_optional_project
3 before_filter :find_optional_project
4 before_filter :load_available_criterias
5
4
6 helper :sort
5 helper :sort
7 include SortHelper
6 include SortHelper
8 helper :issues
7 helper :issues
9 helper :timelog
8 helper :timelog
10 include TimelogHelper
9 include TimelogHelper
11 helper :custom_fields
10 helper :custom_fields
12 include CustomFieldsHelper
11 include CustomFieldsHelper
13
12
14 def report
13 def report
15 @criterias = params[:criterias] || []
16 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
17 @criterias.uniq!
18 @criterias = @criterias[0,3]
19
20 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
21
22 retrieve_date_range
14 retrieve_date_range
23
15 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
24 unless @criterias.empty?
25 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
26 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
27 sql_condition = ''
28
29 if @project.nil?
30 sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
31 elsif @issue.nil?
32 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
33 else
34 sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
35 end
36
37 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
38 sql << " FROM #{TimeEntry.table_name}"
39 sql << time_report_joins
40 sql << " WHERE"
41 sql << " (%s) AND" % sql_condition
42 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
43 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
44
45 @hours = ActiveRecord::Base.connection.select_all(sql)
46
47 @hours.each do |row|
48 case @columns
49 when 'year'
50 row['year'] = row['tyear']
51 when 'month'
52 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
53 when 'week'
54 row['week'] = "#{row['tyear']}-#{row['tweek']}"
55 when 'day'
56 row['day'] = "#{row['spent_on']}"
57 end
58 end
59
60 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
61
62 @periods = []
63 # Date#at_beginning_of_ not supported in Rails 1.2.x
64 date_from = @from.to_time
65 # 100 columns max
66 while date_from <= @to.to_time && @periods.length < 100
67 case @columns
68 when 'year'
69 @periods << "#{date_from.year}"
70 date_from = (date_from + 1.year).at_beginning_of_year
71 when 'month'
72 @periods << "#{date_from.year}-#{date_from.month}"
73 date_from = (date_from + 1.month).at_beginning_of_month
74 when 'week'
75 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
76 date_from = (date_from + 7.day).at_beginning_of_week
77 when 'day'
78 @periods << "#{date_from.to_date}"
79 date_from = date_from + 1.day
80 end
81 end
82 end
83
16
84 respond_to do |format|
17 respond_to do |format|
85 format.html { render :layout => !request.xhr? }
18 format.html { render :layout => !request.xhr? }
86 format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
19 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
87 end
20 end
88 end
21 end
89
22
90 private
23 private
91
24
92 # TODO: duplicated in TimelogController
25 # TODO: duplicated in TimelogController
93 def find_optional_project
26 def find_optional_project
94 if !params[:issue_id].blank?
27 if !params[:issue_id].blank?
95 @issue = Issue.find(params[:issue_id])
28 @issue = Issue.find(params[:issue_id])
96 @project = @issue.project
29 @project = @issue.project
97 elsif !params[:project_id].blank?
30 elsif !params[:project_id].blank?
98 @project = Project.find(params[:project_id])
31 @project = Project.find(params[:project_id])
99 end
32 end
100 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
33 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
101 end
34 end
102
35
103 # Retrieves the date range based on predefined ranges or specific from/to param dates
36 # Retrieves the date range based on predefined ranges or specific from/to param dates
104 # TODO: duplicated in TimelogController
37 # TODO: duplicated in TimelogController
105 def retrieve_date_range
38 def retrieve_date_range
106 @free_period = false
39 @free_period = false
107 @from, @to = nil, nil
40 @from, @to = nil, nil
108
41
109 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
42 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
110 case params[:period].to_s
43 case params[:period].to_s
111 when 'today'
44 when 'today'
112 @from = @to = Date.today
45 @from = @to = Date.today
113 when 'yesterday'
46 when 'yesterday'
114 @from = @to = Date.today - 1
47 @from = @to = Date.today - 1
115 when 'current_week'
48 when 'current_week'
116 @from = Date.today - (Date.today.cwday - 1)%7
49 @from = Date.today - (Date.today.cwday - 1)%7
117 @to = @from + 6
50 @to = @from + 6
118 when 'last_week'
51 when 'last_week'
119 @from = Date.today - 7 - (Date.today.cwday - 1)%7
52 @from = Date.today - 7 - (Date.today.cwday - 1)%7
120 @to = @from + 6
53 @to = @from + 6
121 when '7_days'
54 when '7_days'
122 @from = Date.today - 7
55 @from = Date.today - 7
123 @to = Date.today
56 @to = Date.today
124 when 'current_month'
57 when 'current_month'
125 @from = Date.civil(Date.today.year, Date.today.month, 1)
58 @from = Date.civil(Date.today.year, Date.today.month, 1)
126 @to = (@from >> 1) - 1
59 @to = (@from >> 1) - 1
127 when 'last_month'
60 when 'last_month'
128 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
61 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
129 @to = (@from >> 1) - 1
62 @to = (@from >> 1) - 1
130 when '30_days'
63 when '30_days'
131 @from = Date.today - 30
64 @from = Date.today - 30
132 @to = Date.today
65 @to = Date.today
133 when 'current_year'
66 when 'current_year'
134 @from = Date.civil(Date.today.year, 1, 1)
67 @from = Date.civil(Date.today.year, 1, 1)
135 @to = Date.civil(Date.today.year, 12, 31)
68 @to = Date.civil(Date.today.year, 12, 31)
136 end
69 end
137 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
70 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
138 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
71 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
139 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
72 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
140 @free_period = true
73 @free_period = true
141 else
74 else
142 # default
75 # default
143 end
76 end
144
77
145 @from, @to = @to, @from if @from && @to && @from > @to
78 @from, @to = @to, @from if @from && @to && @from > @to
146 @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
79 @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
147 @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
80 @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
148 end
81 end
149
150 def load_available_criterias
151 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
152 :klass => Project,
153 :label => :label_project},
154 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
155 :klass => Version,
156 :label => :label_version},
157 'category' => {:sql => "#{Issue.table_name}.category_id",
158 :klass => IssueCategory,
159 :label => :field_category},
160 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
161 :klass => User,
162 :label => :label_member},
163 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
164 :klass => Tracker,
165 :label => :label_tracker},
166 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
167 :klass => TimeEntryActivity,
168 :label => :label_activity},
169 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
170 :klass => Issue,
171 :label => :label_issue}
172 }
173
174 # Add list and boolean custom fields as available criterias
175 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
176 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
177 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
178 :format => cf.field_format,
179 :label => cf.name}
180 end if @project
181
182 # Add list and boolean time entry custom fields
183 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
184 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
185 :format => cf.field_format,
186 :label => cf.name}
187 end
188
189 # Add list and boolean time entry activity custom fields
190 TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
191 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
192 :format => cf.field_format,
193 :label => cf.name}
194 end
195
196 call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project })
197 @available_criterias
198 end
199
200 def time_report_joins
201 sql = ''
202 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
203 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
204 # TODO: rename hook
205 call_hook(:controller_timelog_time_report_joins, {:sql => sql} )
206 sql
207 end
208
209 end
82 end
@@ -1,194 +1,194
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module TimelogHelper
18 module TimelogHelper
19 include ApplicationHelper
19 include ApplicationHelper
20
20
21 def render_timelog_breadcrumb
21 def render_timelog_breadcrumb
22 links = []
22 links = []
23 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
23 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
24 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
24 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
25 if @issue
25 if @issue
26 if @issue.visible?
26 if @issue.visible?
27 links << link_to_issue(@issue, :subject => false)
27 links << link_to_issue(@issue, :subject => false)
28 else
28 else
29 links << "##{@issue.id}"
29 links << "##{@issue.id}"
30 end
30 end
31 end
31 end
32 breadcrumb links
32 breadcrumb links
33 end
33 end
34
34
35 # Returns a collection of activities for a select field. time_entry
35 # Returns a collection of activities for a select field. time_entry
36 # is optional and will be used to check if the selected TimeEntryActivity
36 # is optional and will be used to check if the selected TimeEntryActivity
37 # is active.
37 # is active.
38 def activity_collection_for_select_options(time_entry=nil, project=nil)
38 def activity_collection_for_select_options(time_entry=nil, project=nil)
39 project ||= @project
39 project ||= @project
40 if project.nil?
40 if project.nil?
41 activities = TimeEntryActivity.shared.active
41 activities = TimeEntryActivity.shared.active
42 else
42 else
43 activities = project.activities
43 activities = project.activities
44 end
44 end
45
45
46 collection = []
46 collection = []
47 if time_entry && time_entry.activity && !time_entry.activity.active?
47 if time_entry && time_entry.activity && !time_entry.activity.active?
48 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
48 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
49 else
49 else
50 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
50 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
51 end
51 end
52 activities.each { |a| collection << [a.name, a.id] }
52 activities.each { |a| collection << [a.name, a.id] }
53 collection
53 collection
54 end
54 end
55
55
56 def select_hours(data, criteria, value)
56 def select_hours(data, criteria, value)
57 if value.to_s.empty?
57 if value.to_s.empty?
58 data.select {|row| row[criteria].blank? }
58 data.select {|row| row[criteria].blank? }
59 else
59 else
60 data.select {|row| row[criteria].to_s == value.to_s}
60 data.select {|row| row[criteria].to_s == value.to_s}
61 end
61 end
62 end
62 end
63
63
64 def sum_hours(data)
64 def sum_hours(data)
65 sum = 0
65 sum = 0
66 data.each do |row|
66 data.each do |row|
67 sum += row['hours'].to_f
67 sum += row['hours'].to_f
68 end
68 end
69 sum
69 sum
70 end
70 end
71
71
72 def options_for_period_select(value)
72 def options_for_period_select(value)
73 options_for_select([[l(:label_all_time), 'all'],
73 options_for_select([[l(:label_all_time), 'all'],
74 [l(:label_today), 'today'],
74 [l(:label_today), 'today'],
75 [l(:label_yesterday), 'yesterday'],
75 [l(:label_yesterday), 'yesterday'],
76 [l(:label_this_week), 'current_week'],
76 [l(:label_this_week), 'current_week'],
77 [l(:label_last_week), 'last_week'],
77 [l(:label_last_week), 'last_week'],
78 [l(:label_last_n_days, 7), '7_days'],
78 [l(:label_last_n_days, 7), '7_days'],
79 [l(:label_this_month), 'current_month'],
79 [l(:label_this_month), 'current_month'],
80 [l(:label_last_month), 'last_month'],
80 [l(:label_last_month), 'last_month'],
81 [l(:label_last_n_days, 30), '30_days'],
81 [l(:label_last_n_days, 30), '30_days'],
82 [l(:label_this_year), 'current_year']],
82 [l(:label_this_year), 'current_year']],
83 value)
83 value)
84 end
84 end
85
85
86 def entries_to_csv(entries)
86 def entries_to_csv(entries)
87 decimal_separator = l(:general_csv_decimal_separator)
87 decimal_separator = l(:general_csv_decimal_separator)
88 custom_fields = TimeEntryCustomField.find(:all)
88 custom_fields = TimeEntryCustomField.find(:all)
89 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
89 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
90 # csv header fields
90 # csv header fields
91 headers = [l(:field_spent_on),
91 headers = [l(:field_spent_on),
92 l(:field_user),
92 l(:field_user),
93 l(:field_activity),
93 l(:field_activity),
94 l(:field_project),
94 l(:field_project),
95 l(:field_issue),
95 l(:field_issue),
96 l(:field_tracker),
96 l(:field_tracker),
97 l(:field_subject),
97 l(:field_subject),
98 l(:field_hours),
98 l(:field_hours),
99 l(:field_comments)
99 l(:field_comments)
100 ]
100 ]
101 # Export custom fields
101 # Export custom fields
102 headers += custom_fields.collect(&:name)
102 headers += custom_fields.collect(&:name)
103
103
104 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
104 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
105 c.to_s,
105 c.to_s,
106 l(:general_csv_encoding) ) }
106 l(:general_csv_encoding) ) }
107 # csv lines
107 # csv lines
108 entries.each do |entry|
108 entries.each do |entry|
109 fields = [format_date(entry.spent_on),
109 fields = [format_date(entry.spent_on),
110 entry.user,
110 entry.user,
111 entry.activity,
111 entry.activity,
112 entry.project,
112 entry.project,
113 (entry.issue ? entry.issue.id : nil),
113 (entry.issue ? entry.issue.id : nil),
114 (entry.issue ? entry.issue.tracker : nil),
114 (entry.issue ? entry.issue.tracker : nil),
115 (entry.issue ? entry.issue.subject : nil),
115 (entry.issue ? entry.issue.subject : nil),
116 entry.hours.to_s.gsub('.', decimal_separator),
116 entry.hours.to_s.gsub('.', decimal_separator),
117 entry.comments
117 entry.comments
118 ]
118 ]
119 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
119 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
120
120
121 csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(
121 csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(
122 c.to_s,
122 c.to_s,
123 l(:general_csv_encoding) ) }
123 l(:general_csv_encoding) ) }
124 end
124 end
125 end
125 end
126 export
126 export
127 end
127 end
128
128
129 def format_criteria_value(criteria, value)
129 def format_criteria_value(criteria_options, value)
130 if value.blank?
130 if value.blank?
131 l(:label_none)
131 l(:label_none)
132 elsif k = @available_criterias[criteria][:klass]
132 elsif k = criteria_options[:klass]
133 obj = k.find_by_id(value.to_i)
133 obj = k.find_by_id(value.to_i)
134 if obj.is_a?(Issue)
134 if obj.is_a?(Issue)
135 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
135 obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
136 else
136 else
137 obj
137 obj
138 end
138 end
139 else
139 else
140 format_value(value, @available_criterias[criteria][:format])
140 format_value(value, criteria_options[:format])
141 end
141 end
142 end
142 end
143
143
144 def report_to_csv(criterias, periods, hours)
144 def report_to_csv(report)
145 decimal_separator = l(:general_csv_decimal_separator)
145 decimal_separator = l(:general_csv_decimal_separator)
146 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
146 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
147 # Column headers
147 # Column headers
148 headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
148 headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
149 headers += periods
149 headers += report.periods
150 headers << l(:label_total)
150 headers << l(:label_total)
151 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
151 csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
152 c.to_s,
152 c.to_s,
153 l(:general_csv_encoding) ) }
153 l(:general_csv_encoding) ) }
154 # Content
154 # Content
155 report_criteria_to_csv(csv, criterias, periods, hours)
155 report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
156 # Total row
156 # Total row
157 str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding))
157 str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding))
158 row = [ str_total ] + [''] * (criterias.size - 1)
158 row = [ str_total ] + [''] * (report.criteria.size - 1)
159 total = 0
159 total = 0
160 periods.each do |period|
160 report.periods.each do |period|
161 sum = sum_hours(select_hours(hours, @columns, period.to_s))
161 sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
162 total += sum
162 total += sum
163 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
163 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
164 end
164 end
165 row << ("%.2f" % total).gsub('.',decimal_separator)
165 row << ("%.2f" % total).gsub('.',decimal_separator)
166 csv << row
166 csv << row
167 end
167 end
168 export
168 export
169 end
169 end
170
170
171 def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
171 def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
172 decimal_separator = l(:general_csv_decimal_separator)
172 decimal_separator = l(:general_csv_decimal_separator)
173 hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
173 hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
174 hours_for_value = select_hours(hours, criterias[level], value)
174 hours_for_value = select_hours(hours, criteria[level], value)
175 next if hours_for_value.empty?
175 next if hours_for_value.empty?
176 row = [''] * level
176 row = [''] * level
177 row << Redmine::CodesetUtil.from_utf8(
177 row << Redmine::CodesetUtil.from_utf8(
178 format_criteria_value(criterias[level], value).to_s,
178 format_criteria_value(available_criteria[criteria[level]], value).to_s,
179 l(:general_csv_encoding) )
179 l(:general_csv_encoding) )
180 row += [''] * (criterias.length - level - 1)
180 row += [''] * (criteria.length - level - 1)
181 total = 0
181 total = 0
182 periods.each do |period|
182 periods.each do |period|
183 sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
183 sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
184 total += sum
184 total += sum
185 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
185 row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
186 end
186 end
187 row << ("%.2f" % total).gsub('.',decimal_separator)
187 row << ("%.2f" % total).gsub('.',decimal_separator)
188 csv << row
188 csv << row
189 if criterias.length > level + 1
189 if criteria.length > level + 1
190 report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
190 report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
191 end
191 end
192 end
192 end
193 end
193 end
194 end
194 end
@@ -1,19 +1,19
1 <% @hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %>
1 <% @report.hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %>
2 <% hours_for_value = select_hours(hours, criterias[level], value) -%>
2 <% hours_for_value = select_hours(hours, criterias[level], value) -%>
3 <% next if hours_for_value.empty? -%>
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 criterias.length > level+1 %>">
5 <%= '<td></td>' * level %>
5 <%= '<td></td>' * level %>
6 <td><%= h(format_criteria_value(criterias[level], value)) %></td>
6 <td><%= h(format_criteria_value(@report.available_criteria[criterias[level]], value)) %></td>
7 <%= '<td></td>' * (criterias.length - level - 1) -%>
7 <%= '<td></td>' * (criterias.length - level - 1) -%>
8 <% total = 0 -%>
8 <% total = 0 -%>
9 <% @periods.each do |period| -%>
9 <% @report.periods.each do |period| -%>
10 <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)); total += sum -%>
10 <% sum = sum_hours(select_hours(hours_for_value, @report.columns, period.to_s)); total += sum -%>
11 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
11 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
12 <% end -%>
12 <% end -%>
13 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
13 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
14 </tr>
14 </tr>
15 <% if criterias.length > level+1 -%>
15 <% if criterias.length > level+1 -%>
16 <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %>
16 <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %>
17 <% end -%>
17 <% end -%>
18
18
19 <% end %>
19 <% end %>
@@ -1,72 +1,72
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 </div>
3 </div>
4
4
5 <%= render_timelog_breadcrumb %>
5 <%= render_timelog_breadcrumb %>
6
6
7 <h2><%= l(:label_spent_time) %></h2>
7 <h2><%= l(:label_spent_time) %></h2>
8
8
9 <% form_tag({:controller => 'time_entry_reports', :action => 'report', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
9 <% form_tag({:controller => 'time_entry_reports', :action => 'report', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
10 <% @criterias.each do |criteria| %>
10 <% @report.criteria.each do |criterion| %>
11 <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
11 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
12 <% end %>
12 <% end %>
13 <%= render :partial => 'timelog/date_range' %>
13 <%= render :partial => 'timelog/date_range' %>
14
14
15 <p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
15 <p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
16 [l(:label_month), 'month'],
16 [l(:label_month), 'month'],
17 [l(:label_week), 'week'],
17 [l(:label_week), 'week'],
18 [l(:label_day_plural).titleize, 'day']], @columns),
18 [l(:label_day_plural).titleize, 'day']], @report.columns),
19 :onchange => "this.form.onsubmit();" %>
19 :onchange => "this.form.onsubmit();" %>
20
20
21 <label for='criterias'><%= l(:button_add) %></label>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l_or_humanize(@available_criterias[k][:label]), k]}),
21 <label for='criterias'><%= l(:button_add) %></label>: <%= select_tag('criteria[]', options_for_select([[]] + (@report.available_criteria.keys - @report.criteria).collect{|k| [l_or_humanize(@report.available_criteria[k][:label]), k]}),
22 :onchange => "this.form.submit();",
22 :onchange => "this.form.submit();",
23 :style => 'width: 200px',
23 :style => 'width: 200px',
24 :id => nil,
24 :id => nil,
25 :disabled => (@criterias.length >= 3), :id => "criterias") %>
25 :disabled => (@report.criteria.length >= 3), :id => "criterias") %>
26 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns}, :class => 'icon icon-reload' %></p>
26 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @report.columns}, :class => 'icon icon-reload' %></p>
27 <% end %>
27 <% end %>
28
28
29 <% unless @criterias.empty? %>
29 <% unless @report.criteria.empty? %>
30 <div class="total-hours">
30 <div class="total-hours">
31 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
31 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@report.total_hours)) %></p>
32 </div>
32 </div>
33
33
34 <% unless @hours.empty? %>
34 <% unless @report.hours.empty? %>
35 <div class="autoscroll">
35 <div class="autoscroll">
36 <table class="list" id="time-report">
36 <table class="list" id="time-report">
37 <thead>
37 <thead>
38 <tr>
38 <tr>
39 <% @criterias.each do |criteria| %>
39 <% @report.criteria.each do |criteria| %>
40 <th><%= l_or_humanize(@available_criterias[criteria][:label]) %></th>
40 <th><%= l_or_humanize(@report.available_criteria[criteria][:label]) %></th>
41 <% end %>
41 <% end %>
42 <% columns_width = (40 / (@periods.length+1)).to_i %>
42 <% columns_width = (40 / (@report.periods.length+1)).to_i %>
43 <% @periods.each do |period| %>
43 <% @report.periods.each do |period| %>
44 <th class="period" width="<%= columns_width %>%"><%= period %></th>
44 <th class="period" width="<%= columns_width %>%"><%= period %></th>
45 <% end %>
45 <% end %>
46 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
46 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
47 </tr>
47 </tr>
48 </thead>
48 </thead>
49 <tbody>
49 <tbody>
50 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
50 <%= render :partial => 'report_criteria', :locals => {:criterias => @report.criteria, :hours => @report.hours, :level => 0} %>
51 <tr class="total">
51 <tr class="total">
52 <td><%= l(:label_total) %></td>
52 <td><%= l(:label_total) %></td>
53 <%= '<td></td>' * (@criterias.size - 1) %>
53 <%= '<td></td>' * (@report.criteria.size - 1) %>
54 <% total = 0 -%>
54 <% total = 0 -%>
55 <% @periods.each do |period| -%>
55 <% @report.periods.each do |period| -%>
56 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%>
56 <% sum = sum_hours(select_hours(@report.hours, @report.columns, period.to_s)); total += sum -%>
57 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
57 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
58 <% end -%>
58 <% end -%>
59 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
59 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
60 </tr>
60 </tr>
61 </tbody>
61 </tbody>
62 </table>
62 </table>
63 </div>
63 </div>
64
64
65 <% other_formats_links do |f| %>
65 <% other_formats_links do |f| %>
66 <%= f.link_to 'CSV', :url => params %>
66 <%= f.link_to 'CSV', :url => params %>
67 <% end %>
67 <% end %>
68 <% end %>
68 <% end %>
69 <% end %>
69 <% end %>
70
70
71 <% html_title l(:label_spent_time), l(:label_report) %>
71 <% html_title l(:label_spent_time), l(:label_report) %>
72
72
@@ -1,298 +1,297
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 require File.expand_path('../../test_helper', __FILE__)
2 require File.expand_path('../../test_helper', __FILE__)
3
3
4 class TimeEntryReportsControllerTest < ActionController::TestCase
4 class TimeEntryReportsControllerTest < ActionController::TestCase
5 fixtures :projects, :enabled_modules, :roles, :members, :member_roles,
5 fixtures :projects, :enabled_modules, :roles, :members, :member_roles,
6 :issues, :time_entries, :users, :trackers, :enumerations,
6 :issues, :time_entries, :users, :trackers, :enumerations,
7 :issue_statuses, :custom_fields, :custom_values
7 :issue_statuses, :custom_fields, :custom_values
8
8
9 include Redmine::I18n
9 include Redmine::I18n
10
10
11 def setup
11 def setup
12 Setting.default_language = "en"
12 Setting.default_language = "en"
13 end
13 end
14
14
15 def test_report_at_project_level
15 def test_report_at_project_level
16 get :report, :project_id => 'ecookbook'
16 get :report, :project_id => 'ecookbook'
17 assert_response :success
17 assert_response :success
18 assert_template 'report'
18 assert_template 'report'
19 assert_tag :form,
19 assert_tag :form,
20 :attributes => {:action => "/projects/ecookbook/time_entries/report", :id => 'query_form'}
20 :attributes => {:action => "/projects/ecookbook/time_entries/report", :id => 'query_form'}
21 end
21 end
22
22
23 def test_report_all_projects
23 def test_report_all_projects
24 get :report
24 get :report
25 assert_response :success
25 assert_response :success
26 assert_template 'report'
26 assert_template 'report'
27 assert_tag :form,
27 assert_tag :form,
28 :attributes => {:action => "/time_entries/report", :id => 'query_form'}
28 :attributes => {:action => "/time_entries/report", :id => 'query_form'}
29 end
29 end
30
30
31 def test_report_all_projects_denied
31 def test_report_all_projects_denied
32 r = Role.anonymous
32 r = Role.anonymous
33 r.permissions.delete(:view_time_entries)
33 r.permissions.delete(:view_time_entries)
34 r.permissions_will_change!
34 r.permissions_will_change!
35 r.save
35 r.save
36 get :report
36 get :report
37 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
37 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
38 end
38 end
39
39
40 def test_report_all_projects_one_criteria
40 def test_report_all_projects_one_criteria
41 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
41 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
42 assert_response :success
42 assert_response :success
43 assert_template 'report'
43 assert_template 'report'
44 assert_not_nil assigns(:total_hours)
44 assert_not_nil assigns(:report)
45 assert_equal "8.65", "%.2f" % assigns(:total_hours)
45 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
46 end
46 end
47
47
48 def test_report_all_time
48 def test_report_all_time
49 get :report, :project_id => 1, :criterias => ['project', 'issue']
49 get :report, :project_id => 1, :criteria => ['project', 'issue']
50 assert_response :success
50 assert_response :success
51 assert_template 'report'
51 assert_template 'report'
52 assert_not_nil assigns(:total_hours)
52 assert_not_nil assigns(:report)
53 assert_equal "162.90", "%.2f" % assigns(:total_hours)
53 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
54 end
54 end
55
55
56 def test_report_all_time_by_day
56 def test_report_all_time_by_day
57 get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
57 get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day'
58 assert_response :success
58 assert_response :success
59 assert_template 'report'
59 assert_template 'report'
60 assert_not_nil assigns(:total_hours)
60 assert_not_nil assigns(:report)
61 assert_equal "162.90", "%.2f" % assigns(:total_hours)
61 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
62 assert_tag :tag => 'th', :content => '2007-03-12'
62 assert_tag :tag => 'th', :content => '2007-03-12'
63 end
63 end
64
64
65 def test_report_one_criteria
65 def test_report_one_criteria
66 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
66 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project']
67 assert_response :success
67 assert_response :success
68 assert_template 'report'
68 assert_template 'report'
69 assert_not_nil assigns(:total_hours)
69 assert_not_nil assigns(:report)
70 assert_equal "8.65", "%.2f" % assigns(:total_hours)
70 assert_equal "8.65", "%.2f" % assigns(:report).total_hours
71 end
71 end
72
72
73 def test_report_two_criterias
73 def test_report_two_criteria
74 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
74 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"]
75 assert_response :success
75 assert_response :success
76 assert_template 'report'
76 assert_template 'report'
77 assert_not_nil assigns(:total_hours)
77 assert_not_nil assigns(:report)
78 assert_equal "162.90", "%.2f" % assigns(:total_hours)
78 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
79 end
79 end
80
80
81 def test_report_one_day
81 def test_report_one_day
82 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criterias => ["member", "activity"]
82 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["member", "activity"]
83 assert_response :success
83 assert_response :success
84 assert_template 'report'
84 assert_template 'report'
85 assert_not_nil assigns(:total_hours)
85 assert_not_nil assigns(:report)
86 assert_equal "4.25", "%.2f" % assigns(:total_hours)
86 assert_equal "4.25", "%.2f" % assigns(:report).total_hours
87 end
87 end
88
88
89 def test_report_at_issue_level
89 def test_report_at_issue_level
90 get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
90 get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"]
91 assert_response :success
91 assert_response :success
92 assert_template 'report'
92 assert_template 'report'
93 assert_not_nil assigns(:total_hours)
93 assert_not_nil assigns(:report)
94 assert_equal "154.25", "%.2f" % assigns(:total_hours)
94 assert_equal "154.25", "%.2f" % assigns(:report).total_hours
95 assert_tag :form,
95 assert_tag :form,
96 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries/report", :id => 'query_form'}
96 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries/report", :id => 'query_form'}
97 end
97 end
98
98
99 def test_report_custom_field_criteria
99 def test_report_custom_field_criteria
100 get :report, :project_id => 1, :criterias => ['project', 'cf_1', 'cf_7']
100 get :report, :project_id => 1, :criteria => ['project', 'cf_1', 'cf_7']
101 assert_response :success
101 assert_response :success
102 assert_template 'report'
102 assert_template 'report'
103 assert_not_nil assigns(:total_hours)
103 assert_not_nil assigns(:report)
104 assert_not_nil assigns(:criterias)
104 assert_equal 3, assigns(:report).criteria.size
105 assert_equal 3, assigns(:criterias).size
105 assert_equal "162.90", "%.2f" % assigns(:report).total_hours
106 assert_equal "162.90", "%.2f" % assigns(:total_hours)
107 # Custom field column
106 # Custom field column
108 assert_tag :tag => 'th', :content => 'Database'
107 assert_tag :tag => 'th', :content => 'Database'
109 # Custom field row
108 # Custom field row
110 assert_tag :tag => 'td', :content => 'MySQL',
109 assert_tag :tag => 'td', :content => 'MySQL',
111 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
110 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
112 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
111 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
113 :content => '1' }}
112 :content => '1' }}
114 # Second custom field column
113 # Second custom field column
115 assert_tag :tag => 'th', :content => 'Billable'
114 assert_tag :tag => 'th', :content => 'Billable'
116 end
115 end
117
116
118 def test_report_one_criteria_no_result
117 def test_report_one_criteria_no_result
119 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
118 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project']
120 assert_response :success
119 assert_response :success
121 assert_template 'report'
120 assert_template 'report'
122 assert_not_nil assigns(:total_hours)
121 assert_not_nil assigns(:report)
123 assert_equal "0.00", "%.2f" % assigns(:total_hours)
122 assert_equal "0.00", "%.2f" % assigns(:report).total_hours
124 end
123 end
125
124
126 def test_report_all_projects_csv_export
125 def test_report_all_projects_csv_export
127 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30",
126 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30",
128 :criterias => ["project", "member", "activity"], :format => "csv"
127 :criteria => ["project", "member", "activity"], :format => "csv"
129 assert_response :success
128 assert_response :success
130 assert_equal 'text/csv', @response.content_type
129 assert_equal 'text/csv', @response.content_type
131 lines = @response.body.chomp.split("\n")
130 lines = @response.body.chomp.split("\n")
132 # Headers
131 # Headers
133 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total',
132 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total',
134 lines.first
133 lines.first
135 # Total row
134 # Total row
136 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
135 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
137 end
136 end
138
137
139 def test_report_csv_export
138 def test_report_csv_export
140 get :report, :project_id => 1, :columns => 'month',
139 get :report, :project_id => 1, :columns => 'month',
141 :from => "2007-01-01", :to => "2007-06-30",
140 :from => "2007-01-01", :to => "2007-06-30",
142 :criterias => ["project", "member", "activity"], :format => "csv"
141 :criteria => ["project", "member", "activity"], :format => "csv"
143 assert_response :success
142 assert_response :success
144 assert_equal 'text/csv', @response.content_type
143 assert_equal 'text/csv', @response.content_type
145 lines = @response.body.chomp.split("\n")
144 lines = @response.body.chomp.split("\n")
146 # Headers
145 # Headers
147 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total',
146 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total',
148 lines.first
147 lines.first
149 # Total row
148 # Total row
150 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
149 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
151 end
150 end
152
151
153 def test_csv_big_5
152 def test_csv_big_5
154 Setting.default_language = "zh-TW"
153 Setting.default_language = "zh-TW"
155 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
154 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
156 str_big5 = "\xa4@\xa4\xeb"
155 str_big5 = "\xa4@\xa4\xeb"
157 if str_utf8.respond_to?(:force_encoding)
156 if str_utf8.respond_to?(:force_encoding)
158 str_utf8.force_encoding('UTF-8')
157 str_utf8.force_encoding('UTF-8')
159 str_big5.force_encoding('Big5')
158 str_big5.force_encoding('Big5')
160 end
159 end
161 user = User.find_by_id(3)
160 user = User.find_by_id(3)
162 user.firstname = str_utf8
161 user.firstname = str_utf8
163 user.lastname = "test-lastname"
162 user.lastname = "test-lastname"
164 assert user.save
163 assert user.save
165 comments = "test_csv_big_5"
164 comments = "test_csv_big_5"
166 te1 = TimeEntry.create(:spent_on => '2011-11-11',
165 te1 = TimeEntry.create(:spent_on => '2011-11-11',
167 :hours => 7.3,
166 :hours => 7.3,
168 :project => Project.find(1),
167 :project => Project.find(1),
169 :user => user,
168 :user => user,
170 :activity => TimeEntryActivity.find_by_name('Design'),
169 :activity => TimeEntryActivity.find_by_name('Design'),
171 :comments => comments)
170 :comments => comments)
172
171
173 te2 = TimeEntry.find_by_comments(comments)
172 te2 = TimeEntry.find_by_comments(comments)
174 assert_not_nil te2
173 assert_not_nil te2
175 assert_equal 7.3, te2.hours
174 assert_equal 7.3, te2.hours
176 assert_equal 3, te2.user_id
175 assert_equal 3, te2.user_id
177
176
178 get :report, :project_id => 1, :columns => 'day',
177 get :report, :project_id => 1, :columns => 'day',
179 :from => "2011-11-11", :to => "2011-11-11",
178 :from => "2011-11-11", :to => "2011-11-11",
180 :criterias => ["member"], :format => "csv"
179 :criteria => ["member"], :format => "csv"
181 assert_response :success
180 assert_response :success
182 assert_equal 'text/csv', @response.content_type
181 assert_equal 'text/csv', @response.content_type
183 lines = @response.body.chomp.split("\n")
182 lines = @response.body.chomp.split("\n")
184 # Headers
183 # Headers
185 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
184 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
186 s2 = "\xc1`\xadp"
185 s2 = "\xc1`\xadp"
187 if s1.respond_to?(:force_encoding)
186 if s1.respond_to?(:force_encoding)
188 s1.force_encoding('Big5')
187 s1.force_encoding('Big5')
189 s2.force_encoding('Big5')
188 s2.force_encoding('Big5')
190 end
189 end
191 assert_equal s1, lines.first
190 assert_equal s1, lines.first
192 # Total row
191 # Total row
193 assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1]
192 assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1]
194 assert_equal "#{s2},7.30,7.30", lines[2]
193 assert_equal "#{s2},7.30,7.30", lines[2]
195
194
196 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
195 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
197 if str_tw.respond_to?(:force_encoding)
196 if str_tw.respond_to?(:force_encoding)
198 str_tw.force_encoding('UTF-8')
197 str_tw.force_encoding('UTF-8')
199 end
198 end
200 assert_equal str_tw, l(:general_lang_name)
199 assert_equal str_tw, l(:general_lang_name)
201 assert_equal 'Big5', l(:general_csv_encoding)
200 assert_equal 'Big5', l(:general_csv_encoding)
202 assert_equal ',', l(:general_csv_separator)
201 assert_equal ',', l(:general_csv_separator)
203 assert_equal '.', l(:general_csv_decimal_separator)
202 assert_equal '.', l(:general_csv_decimal_separator)
204 end
203 end
205
204
206 def test_csv_cannot_convert_should_be_replaced_big_5
205 def test_csv_cannot_convert_should_be_replaced_big_5
207 Setting.default_language = "zh-TW"
206 Setting.default_language = "zh-TW"
208 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
207 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
209 if str_utf8.respond_to?(:force_encoding)
208 if str_utf8.respond_to?(:force_encoding)
210 str_utf8.force_encoding('UTF-8')
209 str_utf8.force_encoding('UTF-8')
211 end
210 end
212 user = User.find_by_id(3)
211 user = User.find_by_id(3)
213 user.firstname = str_utf8
212 user.firstname = str_utf8
214 user.lastname = "test-lastname"
213 user.lastname = "test-lastname"
215 assert user.save
214 assert user.save
216 comments = "test_replaced"
215 comments = "test_replaced"
217 te1 = TimeEntry.create(:spent_on => '2011-11-11',
216 te1 = TimeEntry.create(:spent_on => '2011-11-11',
218 :hours => 7.3,
217 :hours => 7.3,
219 :project => Project.find(1),
218 :project => Project.find(1),
220 :user => user,
219 :user => user,
221 :activity => TimeEntryActivity.find_by_name('Design'),
220 :activity => TimeEntryActivity.find_by_name('Design'),
222 :comments => comments)
221 :comments => comments)
223
222
224 te2 = TimeEntry.find_by_comments(comments)
223 te2 = TimeEntry.find_by_comments(comments)
225 assert_not_nil te2
224 assert_not_nil te2
226 assert_equal 7.3, te2.hours
225 assert_equal 7.3, te2.hours
227 assert_equal 3, te2.user_id
226 assert_equal 3, te2.user_id
228
227
229 get :report, :project_id => 1, :columns => 'day',
228 get :report, :project_id => 1, :columns => 'day',
230 :from => "2011-11-11", :to => "2011-11-11",
229 :from => "2011-11-11", :to => "2011-11-11",
231 :criterias => ["member"], :format => "csv"
230 :criteria => ["member"], :format => "csv"
232 assert_response :success
231 assert_response :success
233 assert_equal 'text/csv', @response.content_type
232 assert_equal 'text/csv', @response.content_type
234 lines = @response.body.chomp.split("\n")
233 lines = @response.body.chomp.split("\n")
235 # Headers
234 # Headers
236 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
235 s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp"
237 if s1.respond_to?(:force_encoding)
236 if s1.respond_to?(:force_encoding)
238 s1.force_encoding('Big5')
237 s1.force_encoding('Big5')
239 end
238 end
240 assert_equal s1, lines.first
239 assert_equal s1, lines.first
241 # Total row
240 # Total row
242 s2 = ""
241 s2 = ""
243 if s2.respond_to?(:force_encoding)
242 if s2.respond_to?(:force_encoding)
244 s2 = "\xa5H?"
243 s2 = "\xa5H?"
245 s2.force_encoding('Big5')
244 s2.force_encoding('Big5')
246 elsif RUBY_PLATFORM == 'java'
245 elsif RUBY_PLATFORM == 'java'
247 s2 = "??"
246 s2 = "??"
248 else
247 else
249 s2 = "\xa5H???"
248 s2 = "\xa5H???"
250 end
249 end
251 assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1]
250 assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1]
252 end
251 end
253
252
254 def test_csv_fr
253 def test_csv_fr
255 with_settings :default_language => "fr" do
254 with_settings :default_language => "fr" do
256 str1 = "test_csv_fr"
255 str1 = "test_csv_fr"
257 user = User.find_by_id(3)
256 user = User.find_by_id(3)
258 te1 = TimeEntry.create(:spent_on => '2011-11-11',
257 te1 = TimeEntry.create(:spent_on => '2011-11-11',
259 :hours => 7.3,
258 :hours => 7.3,
260 :project => Project.find(1),
259 :project => Project.find(1),
261 :user => user,
260 :user => user,
262 :activity => TimeEntryActivity.find_by_name('Design'),
261 :activity => TimeEntryActivity.find_by_name('Design'),
263 :comments => str1)
262 :comments => str1)
264
263
265 te2 = TimeEntry.find_by_comments(str1)
264 te2 = TimeEntry.find_by_comments(str1)
266 assert_not_nil te2
265 assert_not_nil te2
267 assert_equal 7.3, te2.hours
266 assert_equal 7.3, te2.hours
268 assert_equal 3, te2.user_id
267 assert_equal 3, te2.user_id
269
268
270 get :report, :project_id => 1, :columns => 'day',
269 get :report, :project_id => 1, :columns => 'day',
271 :from => "2011-11-11", :to => "2011-11-11",
270 :from => "2011-11-11", :to => "2011-11-11",
272 :criterias => ["member"], :format => "csv"
271 :criteria => ["member"], :format => "csv"
273 assert_response :success
272 assert_response :success
274 assert_equal 'text/csv', @response.content_type
273 assert_equal 'text/csv', @response.content_type
275 lines = @response.body.chomp.split("\n")
274 lines = @response.body.chomp.split("\n")
276 # Headers
275 # Headers
277 s1 = "Membre;2011-11-11;Total"
276 s1 = "Membre;2011-11-11;Total"
278 s2 = "Total"
277 s2 = "Total"
279 if s1.respond_to?(:force_encoding)
278 if s1.respond_to?(:force_encoding)
280 s1.force_encoding('ISO-8859-1')
279 s1.force_encoding('ISO-8859-1')
281 s2.force_encoding('ISO-8859-1')
280 s2.force_encoding('ISO-8859-1')
282 end
281 end
283 assert_equal s1, lines.first
282 assert_equal s1, lines.first
284 # Total row
283 # Total row
285 assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1]
284 assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1]
286 assert_equal "#{s2};7,30;7,30", lines[2]
285 assert_equal "#{s2};7,30;7,30", lines[2]
287
286
288 str_fr = "Fran\xc3\xa7ais"
287 str_fr = "Fran\xc3\xa7ais"
289 if str_fr.respond_to?(:force_encoding)
288 if str_fr.respond_to?(:force_encoding)
290 str_fr.force_encoding('UTF-8')
289 str_fr.force_encoding('UTF-8')
291 end
290 end
292 assert_equal str_fr, l(:general_lang_name)
291 assert_equal str_fr, l(:general_lang_name)
293 assert_equal 'ISO-8859-1', l(:general_csv_encoding)
292 assert_equal 'ISO-8859-1', l(:general_csv_encoding)
294 assert_equal ';', l(:general_csv_separator)
293 assert_equal ';', l(:general_csv_separator)
295 assert_equal ',', l(:general_csv_decimal_separator)
294 assert_equal ',', l(:general_csv_decimal_separator)
296 end
295 end
297 end
296 end
298 end
297 end
General Comments 0
You need to be logged in to leave comments. Login now