Adds cross-project time reports support (#994).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1778 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
parent
dbad26c87d
commit
696d21f8c8
|
@ -95,11 +95,15 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def deny_access
|
||||||
|
User.current.logged? ? render_403 : require_login
|
||||||
|
end
|
||||||
|
|
||||||
# Authorize the user for the requested action
|
# Authorize the user for the requested action
|
||||||
def authorize(ctrl = params[:controller], action = params[:action])
|
def authorize(ctrl = params[:controller], action = params[:action])
|
||||||
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
|
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
|
||||||
allowed ? true : (User.current.logged? ? render_403 : require_login)
|
allowed ? true : deny_access
|
||||||
end
|
end
|
||||||
|
|
||||||
# make sure that the user is a member of the project (or admin) if project is private
|
# make sure that the user is a member of the project (or admin) if project is private
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
class TimelogController < ApplicationController
|
class TimelogController < ApplicationController
|
||||||
menu_item :issues
|
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 }
|
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
|
# 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)",
|
@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,
|
:format => cf.field_format,
|
||||||
:label => cf.name}
|
:label => cf.name}
|
||||||
end
|
end if @project
|
||||||
|
|
||||||
# Add list and boolean time entry custom fields
|
# Add list and boolean time entry custom fields
|
||||||
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
|
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 << " FROM #{TimeEntry.table_name}"
|
||||||
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
|
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 << " 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 << " WHERE"
|
||||||
sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
|
sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
|
||||||
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 << " (%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"
|
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
|
||||||
|
|
||||||
@hours = ActiveRecord::Base.connection.select_all(sql)
|
@hours = ActiveRecord::Base.connection.select_all(sql)
|
||||||
|
@ -138,8 +141,13 @@ class TimelogController < ApplicationController
|
||||||
sort_update
|
sort_update
|
||||||
|
|
||||||
cond = ARCondition.new
|
cond = ARCondition.new
|
||||||
cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
|
if @project.nil?
|
||||||
["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
|
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
|
retrieve_date_range
|
||||||
cond << ['spent_on BETWEEN ? AND ?', @from, @to]
|
cond << ['spent_on BETWEEN ? AND ?', @from, @to]
|
||||||
|
@ -197,7 +205,7 @@ class TimelogController < ApplicationController
|
||||||
@time_entry.destroy
|
@time_entry.destroy
|
||||||
flash[:notice] = l(:notice_successful_delete)
|
flash[:notice] = l(:notice_successful_delete)
|
||||||
redirect_to :back
|
redirect_to :back
|
||||||
rescue RedirectBackError
|
rescue ::ActionController::RedirectBackError
|
||||||
redirect_to :action => 'details', :project_id => @time_entry.project
|
redirect_to :action => 'details', :project_id => @time_entry.project
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -219,6 +227,16 @@ private
|
||||||
render_404
|
render_404
|
||||||
end
|
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
|
# Retrieves the date range based on predefined ranges or specific from/to param dates
|
||||||
def retrieve_date_range
|
def retrieve_date_range
|
||||||
@free_period = false
|
@free_period = false
|
||||||
|
@ -261,7 +279,7 @@ private
|
||||||
end
|
end
|
||||||
|
|
||||||
@from, @to = @to, @from if @from && @to && @from > @to
|
@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
|
@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.project_condition(Setting.display_subprojects_issues?)) || Date.today)
|
@to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,14 @@
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
module TimelogHelper
|
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
|
def activity_collection_for_select_options
|
||||||
activities = Enumeration::get_values('ACTI')
|
activities = Enumeration::get_values('ACTI')
|
||||||
collection = []
|
collection = []
|
||||||
|
|
|
@ -243,7 +243,7 @@ class User < ActiveRecord::Base
|
||||||
elsif options[:global]
|
elsif options[:global]
|
||||||
# authorize if user has at least one role that has this permission
|
# authorize if user has at least one role that has this permission
|
||||||
roles = memberships.collect {|m| m.role}.uniq
|
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
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
|
@ -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' %>
|
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2><%= l(:label_spent_time) %></h2>
|
<%= render_timelog_breadcrumb %>
|
||||||
|
|
||||||
<% if @issue %>
|
<h2><%= l(:label_spent_time) %></h2>
|
||||||
<h3><%= link_to(@project.name, {:action => 'details', :project_id => @project}) %> / <%= link_to_issue(@issue) %></h3>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
|
<% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
|
||||||
<%= hidden_field_tag 'project_id', params[:project_id] %>
|
<%= hidden_field_tag 'project_id', params[:project_id] %>
|
||||||
|
|
|
@ -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' %>
|
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%= render_timelog_breadcrumb %>
|
||||||
|
|
||||||
<h2><%= l(:label_spent_time) %></h2>
|
<h2><%= l(:label_spent_time) %></h2>
|
||||||
|
|
||||||
<% form_remote_tag(:url => {}, :update => 'content') do %>
|
<% form_remote_tag(:url => {}, :update => 'content') do %>
|
||||||
|
|
|
@ -20,7 +20,7 @@ ActionController::Routing::Routes.draw do |map|
|
||||||
map.connect 'projects/:project_id/news/:action', :controller => 'news'
|
map.connect 'projects/:project_id/news/:action', :controller => 'news'
|
||||||
map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
|
map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
|
||||||
map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
|
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.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
|
||||||
|
|
||||||
map.with_options :controller => 'repositories' do |omap|
|
map.with_options :controller => 'repositories' do |omap|
|
||||||
|
|
|
@ -78,7 +78,7 @@ class TimelogControllerTest < Test::Unit::TestCase
|
||||||
assert_equal 2, entry.user_id
|
assert_equal 2, entry.user_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def test_destroy
|
||||||
@request.session[:user_id] = 2
|
@request.session[:user_id] = 2
|
||||||
post :destroy, :id => 1
|
post :destroy, :id => 1
|
||||||
assert_redirected_to 'projects/ecookbook/timelog/details'
|
assert_redirected_to 'projects/ecookbook/timelog/details'
|
||||||
|
@ -91,6 +91,29 @@ class TimelogControllerTest < Test::Unit::TestCase
|
||||||
assert_template 'report'
|
assert_template 'report'
|
||||||
end
|
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
|
def test_report_all_time
|
||||||
get :report, :project_id => 1, :criterias => ['project', 'issue']
|
get :report, :project_id => 1, :criterias => ['project', 'issue']
|
||||||
assert_response :success
|
assert_response :success
|
||||||
|
@ -148,7 +171,18 @@ class TimelogControllerTest < Test::Unit::TestCase
|
||||||
assert_not_nil assigns(:total_hours)
|
assert_not_nil assigns(:total_hours)
|
||||||
assert_equal "0.00", "%.2f" % assigns(:total_hours)
|
assert_equal "0.00", "%.2f" % assigns(:total_hours)
|
||||||
end
|
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
|
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"
|
get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
|
||||||
assert_response :success
|
assert_response :success
|
||||||
|
@ -159,6 +193,14 @@ class TimelogControllerTest < Test::Unit::TestCase
|
||||||
# Total row
|
# Total row
|
||||||
assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
|
assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
|
||||||
end
|
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
|
def test_details_at_project_level
|
||||||
get :details, :project_id => 1
|
get :details, :project_id => 1
|
||||||
|
@ -218,6 +260,14 @@ class TimelogControllerTest < Test::Unit::TestCase
|
||||||
assert assigns(:items).first.is_a?(TimeEntry)
|
assert assigns(:items).first.is_a?(TimeEntry)
|
||||||
end
|
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
|
def test_details_csv_export
|
||||||
get :details, :project_id => 1, :format => 'csv'
|
get :details, :project_id => 1, :format => 'csv'
|
||||||
assert_response :success
|
assert_response :success
|
||||||
|
|
Loading…
Reference in New Issue