]> source.dussan.org Git - redmine.git/commitdiff
Adds cross-project time reports support (#994).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 31 Aug 2008 16:34:54 +0000 (16:34 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 31 Aug 2008 16:34:54 +0000 (16:34 +0000)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1778 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/controllers/application.rb
app/controllers/timelog_controller.rb
app/helpers/timelog_helper.rb
app/models/user.rb
app/views/timelog/details.rhtml
app/views/timelog/report.rhtml
config/routes.rb
test/functional/timelog_controller_test.rb

index 7a56e61f016b08d39b6828c449eaaa400c6a0faf..d21d0bd8c9cebd3c687814f46452c9db3adc4bcd 100644 (file)
@@ -95,11 +95,15 @@ class ApplicationController < ActionController::Base
     end
     true
   end
+  
+  def deny_access
+    User.current.logged? ? render_403 : require_login
+  end
 
   # Authorize the user for the requested action
   def authorize(ctrl = params[:controller], action = params[:action])
     allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
-    allowed ? true : (User.current.logged? ? render_403 : require_login)
+    allowed ? true : deny_access
   end
   
   # make sure that the user is a member of the project (or admin) if project is private
index f331cdbe4e850a9911b84610aae1dcbb34fd1505..897a50fe54145cfe5e4a0268116e4c6d7f83a0c8 100644 (file)
@@ -17,7 +17,8 @@
 
 class TimelogController < ApplicationController
   menu_item :issues
-  before_filter :find_project, :authorize
+  before_filter :find_project, :authorize, :only => [:edit, :destroy]
+  before_filter :find_optional_project, :only => [:report, :details]
 
   verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
   
@@ -53,11 +54,12 @@ class TimelogController < ApplicationController
                            }
     
     # Add list and boolean custom fields as available criterias
-    @project.all_issue_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
+    custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
+    custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
       @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)",
                                              :format => cf.field_format,
                                              :label => cf.name}
-    end
+    end if @project
     
     # Add list and boolean time entry custom fields
     TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@@ -83,9 +85,10 @@ class TimelogController < ApplicationController
       sql << " FROM #{TimeEntry.table_name}"
       sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
       sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
-      sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
-      sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
-      sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
+      sql << " WHERE"
+      sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
+      sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
+      sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
       sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
       
       @hours = ActiveRecord::Base.connection.select_all(sql)
@@ -138,8 +141,13 @@ class TimelogController < ApplicationController
     sort_update
     
     cond = ARCondition.new
-    cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
-                           ["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
+    if @project.nil?
+      cond << Project.allowed_to_condition(User.current, :view_time_entries)
+    elsif @issue.nil?
+      cond << @project.project_condition(Setting.display_subprojects_issues?)
+    else
+      cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
+    end
     
     retrieve_date_range
     cond << ['spent_on BETWEEN ? AND ?', @from, @to]
@@ -197,7 +205,7 @@ class TimelogController < ApplicationController
     @time_entry.destroy
     flash[:notice] = l(:notice_successful_delete)
     redirect_to :back
-  rescue RedirectBackError
+  rescue ::ActionController::RedirectBackError
     redirect_to :action => 'details', :project_id => @time_entry.project
   end
 
@@ -219,6 +227,16 @@ private
     render_404
   end
   
+  def find_optional_project
+    if !params[:issue_id].blank?
+      @issue = Issue.find(params[:issue_id])
+      @project = @issue.project
+    elsif !params[:project_id].blank?
+      @project = Project.find(params[:project_id])
+    end
+    deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
+  end
+  
   # Retrieves the date range based on predefined ranges or specific from/to param dates
   def retrieve_date_range
     @free_period = false
@@ -261,7 +279,7 @@ private
     end
     
     @from, @to = @to, @from if @from && @to && @from > @to
-    @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1
-    @to   ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
+    @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
+    @to   ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
   end
 end
index 2c1225a7c7e83ea13e968b6e3510a0ebec96b25c..f55a8ffe7fa68393ceb9d99f879496825e243d42 100644 (file)
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 module TimelogHelper
+  def render_timelog_breadcrumb
+    links = []
+    links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
+    links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
+    links << link_to_issue(@issue) if @issue
+    breadcrumb links
+  end
+  
   def activity_collection_for_select_options
     activities = Enumeration::get_values('ACTI')
     collection = []
index 05a75e1adcef5e287c56725cc84bebb9da584ac5..4f82f61b20b07f7e333b739d0ca5b84a048b45cc 100644 (file)
@@ -243,7 +243,7 @@ class User < ActiveRecord::Base
     elsif options[:global]
       # authorize if user has at least one role that has this permission
       roles = memberships.collect {|m| m.role}.uniq
-      roles.detect {|r| r.allowed_to?(action)}
+      roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
     else
       false
     end
index f111cbfc00b722335e1a7724dfaba5b9eb9cae71..db62fae6650c5f151d78cf3c8bfba1d473ba61d6 100644 (file)
@@ -2,11 +2,9 @@
 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>\r
 </div>\r
 \r
-<h2><%= l(:label_spent_time) %></h2>\r
+<%= render_timelog_breadcrumb %>\r
 \r
-<% if @issue %>\r
-<h3><%= link_to(@project.name, {:action => 'details', :project_id => @project}) %> / <%= link_to_issue(@issue) %></h3>\r
-<% end %>\r
+<h2><%= l(:label_spent_time) %></h2>\r
 \r
 <% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>\r
 <%= hidden_field_tag 'project_id', params[:project_id] %>\r
index 97251bc118310d1928e96d9458cd3bd50570ebcf..eea0d0fc76fd870c028393e54c56ad8a16c840e0 100644 (file)
@@ -2,6 +2,8 @@
 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
 </div>
 
+<%= render_timelog_breadcrumb %>
+
 <h2><%= l(:label_spent_time) %></h2>
 
 <% form_remote_tag(:url => {}, :update => 'content') do %>
index c34052f86b9a951bd6f77962f4ce38570d86095c..913a8020f977c6181faceb7c9ebdc204caf2c419 100644 (file)
@@ -20,7 +20,7 @@ ActionController::Routing::Routes.draw do |map|
   map.connect 'projects/:project_id/news/:action', :controller => 'news'
   map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
   map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
-  map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog'
+  map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog', :project_id => /.+/
   map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
 
   map.with_options :controller => 'repositories' do |omap|
index 7b4622daab83637eb04f48fd1d1c081b85aea519..28f2a28e26886de3bb5439594e0d5af5ff97b603 100644 (file)
@@ -78,7 +78,7 @@ class TimelogControllerTest < Test::Unit::TestCase
     assert_equal 2, entry.user_id
   end
   
-  def destroy
+  def test_destroy
     @request.session[:user_id] = 2
     post :destroy, :id => 1
     assert_redirected_to 'projects/ecookbook/timelog/details'
@@ -91,6 +91,29 @@ class TimelogControllerTest < Test::Unit::TestCase
     assert_template 'report'
   end
 
+  def test_report_all_projects
+    get :report
+    assert_response :success
+    assert_template 'report'
+  end
+  
+  def test_report_all_projects_denied
+    r = Role.anonymous
+    r.permissions.delete(:view_time_entries)
+    r.permissions_will_change!
+    r.save
+    get :report
+    assert_redirected_to '/account/login'
+  end
+  
+  def test_report_all_projects_one_criteria
+    get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
+    assert_response :success
+    assert_template 'report'
+    assert_not_nil assigns(:total_hours)
+    assert_equal "8.65", "%.2f" % assigns(:total_hours)
+  end
+
   def test_report_all_time
     get :report, :project_id => 1, :criterias => ['project', 'issue']
     assert_response :success
@@ -148,7 +171,18 @@ class TimelogControllerTest < Test::Unit::TestCase
     assert_not_nil assigns(:total_hours)
     assert_equal "0.00", "%.2f" % assigns(:total_hours)
   end
+  
+  def test_report_all_projects_csv_export
+    get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
+    assert_response :success
+    assert_equal 'text/csv', @response.content_type
+    lines = @response.body.chomp.split("\n")
+    # Headers
+    assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
+    # Total row
+    assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
+  end
+  
   def test_report_csv_export
     get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
     assert_response :success
@@ -159,6 +193,14 @@ class TimelogControllerTest < Test::Unit::TestCase
     # Total row
     assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
   end
+  
+  def test_details_all_projects
+    get :details
+    assert_response :success
+    assert_template 'details'
+    assert_not_nil assigns(:total_hours)
+    assert_equal "162.90", "%.2f" % assigns(:total_hours)
+  end
 
   def test_details_at_project_level
     get :details, :project_id => 1
@@ -218,6 +260,14 @@ class TimelogControllerTest < Test::Unit::TestCase
     assert assigns(:items).first.is_a?(TimeEntry)
   end
   
+  def test_details_all_projects_csv_export
+    get :details, :format => 'csv'
+    assert_response :success
+    assert_equal 'text/csv', @response.content_type
+    assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
+    assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
+  end
+  
   def test_details_csv_export
     get :details, :project_id => 1, :format => 'csv'
     assert_response :success