##// END OF EJS Templates
Adds cross-project time reports support (#994)....
Jean-Philippe Lang -
r1777:696d21f8c8c8
parent child
Show More
@@ -95,11 +95,15 class ApplicationController < ActionController::Base
95 end
95 end
96 true
96 true
97 end
97 end
98
99 def deny_access
100 User.current.logged? ? render_403 : require_login
101 end
98
102
99 # Authorize the user for the requested action
103 # Authorize the user for the requested action
100 def authorize(ctrl = params[:controller], action = params[:action])
104 def authorize(ctrl = params[:controller], action = params[:action])
101 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
105 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
102 allowed ? true : (User.current.logged? ? render_403 : require_login)
106 allowed ? true : deny_access
103 end
107 end
104
108
105 # make sure that the user is a member of the project (or admin) if project is private
109 # make sure that the user is a member of the project (or admin) if project is private
@@ -17,7 +17,8
17
17
18 class TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_project, :authorize
20 before_filter :find_project, :authorize, :only => [:edit, :destroy]
21 before_filter :find_optional_project, :only => [:report, :details]
21
22
22 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
23
24
@@ -53,11 +54,12 class TimelogController < ApplicationController
53 }
54 }
54
55
55 # Add list and boolean custom fields as available criterias
56 # Add list and boolean custom fields as available criterias
56 @project.all_issue_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
57 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
58 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
57 @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)",
59 @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)",
58 :format => cf.field_format,
60 :format => cf.field_format,
59 :label => cf.name}
61 :label => cf.name}
60 end
62 end if @project
61
63
62 # Add list and boolean time entry custom fields
64 # Add list and boolean time entry custom fields
63 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
65 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@@ -83,9 +85,10 class TimelogController < ApplicationController
83 sql << " FROM #{TimeEntry.table_name}"
85 sql << " FROM #{TimeEntry.table_name}"
84 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
86 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
85 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
87 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
86 sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
88 sql << " WHERE"
87 sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
89 sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
88 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
90 sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
91 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
89 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
92 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
90
93
91 @hours = ActiveRecord::Base.connection.select_all(sql)
94 @hours = ActiveRecord::Base.connection.select_all(sql)
@@ -138,8 +141,13 class TimelogController < ApplicationController
138 sort_update
141 sort_update
139
142
140 cond = ARCondition.new
143 cond = ARCondition.new
141 cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
144 if @project.nil?
142 ["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
145 cond << Project.allowed_to_condition(User.current, :view_time_entries)
146 elsif @issue.nil?
147 cond << @project.project_condition(Setting.display_subprojects_issues?)
148 else
149 cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
150 end
143
151
144 retrieve_date_range
152 retrieve_date_range
145 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
153 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
@@ -197,7 +205,7 class TimelogController < ApplicationController
197 @time_entry.destroy
205 @time_entry.destroy
198 flash[:notice] = l(:notice_successful_delete)
206 flash[:notice] = l(:notice_successful_delete)
199 redirect_to :back
207 redirect_to :back
200 rescue RedirectBackError
208 rescue ::ActionController::RedirectBackError
201 redirect_to :action => 'details', :project_id => @time_entry.project
209 redirect_to :action => 'details', :project_id => @time_entry.project
202 end
210 end
203
211
@@ -219,6 +227,16 private
219 render_404
227 render_404
220 end
228 end
221
229
230 def find_optional_project
231 if !params[:issue_id].blank?
232 @issue = Issue.find(params[:issue_id])
233 @project = @issue.project
234 elsif !params[:project_id].blank?
235 @project = Project.find(params[:project_id])
236 end
237 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
238 end
239
222 # Retrieves the date range based on predefined ranges or specific from/to param dates
240 # Retrieves the date range based on predefined ranges or specific from/to param dates
223 def retrieve_date_range
241 def retrieve_date_range
224 @free_period = false
242 @free_period = false
@@ -261,7 +279,7 private
261 end
279 end
262
280
263 @from, @to = @to, @from if @from && @to && @from > @to
281 @from, @to = @to, @from if @from && @to && @from > @to
264 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1
282 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
265 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
283 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
266 end
284 end
267 end
285 end
@@ -16,6 +16,14
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 def render_timelog_breadcrumb
20 links = []
21 links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
22 links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
23 links << link_to_issue(@issue) if @issue
24 breadcrumb links
25 end
26
19 def activity_collection_for_select_options
27 def activity_collection_for_select_options
20 activities = Enumeration::get_values('ACTI')
28 activities = Enumeration::get_values('ACTI')
21 collection = []
29 collection = []
@@ -243,7 +243,7 class User < ActiveRecord::Base
243 elsif options[:global]
243 elsif options[:global]
244 # authorize if user has at least one role that has this permission
244 # authorize if user has at least one role that has this permission
245 roles = memberships.collect {|m| m.role}.uniq
245 roles = memberships.collect {|m| m.role}.uniq
246 roles.detect {|r| r.allowed_to?(action)}
246 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
247 else
247 else
248 false
248 false
249 end
249 end
@@ -2,11 +2,9
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
3 </div>
3 </div>
4
4
5 <h2><%= l(:label_spent_time) %></h2>
5 <%= render_timelog_breadcrumb %>
6
6
7 <% if @issue %>
7 <h2><%= l(:label_spent_time) %></h2>
8 <h3><%= link_to(@project.name, {:action => 'details', :project_id => @project}) %> / <%= link_to_issue(@issue) %></h3>
9 <% end %>
10
8
11 <% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
9 <% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
12 <%= hidden_field_tag 'project_id', params[:project_id] %>
10 <%= hidden_field_tag 'project_id', params[:project_id] %>
@@ -2,6 +2,8
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
3 </div>
3 </div>
4
4
5 <%= render_timelog_breadcrumb %>
6
5 <h2><%= l(:label_spent_time) %></h2>
7 <h2><%= l(:label_spent_time) %></h2>
6
8
7 <% form_remote_tag(:url => {}, :update => 'content') do %>
9 <% form_remote_tag(:url => {}, :update => 'content') do %>
@@ -20,7 +20,7 ActionController::Routing::Routes.draw do |map|
20 map.connect 'projects/:project_id/news/:action', :controller => 'news'
20 map.connect 'projects/:project_id/news/:action', :controller => 'news'
21 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
21 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
22 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
22 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
23 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog'
23 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog', :project_id => /.+/
24 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
24 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
25
25
26 map.with_options :controller => 'repositories' do |omap|
26 map.with_options :controller => 'repositories' do |omap|
@@ -78,7 +78,7 class TimelogControllerTest < Test::Unit::TestCase
78 assert_equal 2, entry.user_id
78 assert_equal 2, entry.user_id
79 end
79 end
80
80
81 def destroy
81 def test_destroy
82 @request.session[:user_id] = 2
82 @request.session[:user_id] = 2
83 post :destroy, :id => 1
83 post :destroy, :id => 1
84 assert_redirected_to 'projects/ecookbook/timelog/details'
84 assert_redirected_to 'projects/ecookbook/timelog/details'
@@ -91,6 +91,29 class TimelogControllerTest < Test::Unit::TestCase
91 assert_template 'report'
91 assert_template 'report'
92 end
92 end
93
93
94 def test_report_all_projects
95 get :report
96 assert_response :success
97 assert_template 'report'
98 end
99
100 def test_report_all_projects_denied
101 r = Role.anonymous
102 r.permissions.delete(:view_time_entries)
103 r.permissions_will_change!
104 r.save
105 get :report
106 assert_redirected_to '/account/login'
107 end
108
109 def test_report_all_projects_one_criteria
110 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
111 assert_response :success
112 assert_template 'report'
113 assert_not_nil assigns(:total_hours)
114 assert_equal "8.65", "%.2f" % assigns(:total_hours)
115 end
116
94 def test_report_all_time
117 def test_report_all_time
95 get :report, :project_id => 1, :criterias => ['project', 'issue']
118 get :report, :project_id => 1, :criterias => ['project', 'issue']
96 assert_response :success
119 assert_response :success
@@ -148,7 +171,18 class TimelogControllerTest < Test::Unit::TestCase
148 assert_not_nil assigns(:total_hours)
171 assert_not_nil assigns(:total_hours)
149 assert_equal "0.00", "%.2f" % assigns(:total_hours)
172 assert_equal "0.00", "%.2f" % assigns(:total_hours)
150 end
173 end
151
174
175 def test_report_all_projects_csv_export
176 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
177 assert_response :success
178 assert_equal 'text/csv', @response.content_type
179 lines = @response.body.chomp.split("\n")
180 # Headers
181 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
182 # Total row
183 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
184 end
185
152 def test_report_csv_export
186 def test_report_csv_export
153 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
187 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
154 assert_response :success
188 assert_response :success
@@ -159,6 +193,14 class TimelogControllerTest < Test::Unit::TestCase
159 # Total row
193 # Total row
160 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
194 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
161 end
195 end
196
197 def test_details_all_projects
198 get :details
199 assert_response :success
200 assert_template 'details'
201 assert_not_nil assigns(:total_hours)
202 assert_equal "162.90", "%.2f" % assigns(:total_hours)
203 end
162
204
163 def test_details_at_project_level
205 def test_details_at_project_level
164 get :details, :project_id => 1
206 get :details, :project_id => 1
@@ -218,6 +260,14 class TimelogControllerTest < Test::Unit::TestCase
218 assert assigns(:items).first.is_a?(TimeEntry)
260 assert assigns(:items).first.is_a?(TimeEntry)
219 end
261 end
220
262
263 def test_details_all_projects_csv_export
264 get :details, :format => 'csv'
265 assert_response :success
266 assert_equal 'text/csv', @response.content_type
267 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
268 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
269 end
270
221 def test_details_csv_export
271 def test_details_csv_export
222 get :details, :project_id => 1, :format => 'csv'
272 get :details, :project_id => 1, :format => 'csv'
223 assert_response :success
273 assert_response :success
General Comments 0
You need to be logged in to leave comments. Login now