##// END OF EJS Templates
Adds cross-project time reports support (#994)....
Jean-Philippe Lang -
r1777:696d21f8c8c8
parent child
Show More
@@ -1,221 +1,225
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 require 'uri'
18 require 'uri'
19
19
20 class ApplicationController < ActionController::Base
20 class ApplicationController < ActionController::Base
21 layout 'base'
21 layout 'base'
22
22
23 before_filter :user_setup, :check_if_login_required, :set_localization
23 before_filter :user_setup, :check_if_login_required, :set_localization
24 filter_parameter_logging :password
24 filter_parameter_logging :password
25
25
26 include Redmine::MenuManager::MenuController
26 include Redmine::MenuManager::MenuController
27 helper Redmine::MenuManager::MenuHelper
27 helper Redmine::MenuManager::MenuHelper
28
28
29 REDMINE_SUPPORTED_SCM.each do |scm|
29 REDMINE_SUPPORTED_SCM.each do |scm|
30 require_dependency "repository/#{scm.underscore}"
30 require_dependency "repository/#{scm.underscore}"
31 end
31 end
32
32
33 def current_role
33 def current_role
34 @current_role ||= User.current.role_for_project(@project)
34 @current_role ||= User.current.role_for_project(@project)
35 end
35 end
36
36
37 def user_setup
37 def user_setup
38 # Check the settings cache for each request
38 # Check the settings cache for each request
39 Setting.check_cache
39 Setting.check_cache
40 # Find the current user
40 # Find the current user
41 User.current = find_current_user
41 User.current = find_current_user
42 end
42 end
43
43
44 # Returns the current user or nil if no user is logged in
44 # Returns the current user or nil if no user is logged in
45 def find_current_user
45 def find_current_user
46 if session[:user_id]
46 if session[:user_id]
47 # existing session
47 # existing session
48 (User.find_active(session[:user_id]) rescue nil)
48 (User.find_active(session[:user_id]) rescue nil)
49 elsif cookies[:autologin] && Setting.autologin?
49 elsif cookies[:autologin] && Setting.autologin?
50 # auto-login feature
50 # auto-login feature
51 User.find_by_autologin_key(cookies[:autologin])
51 User.find_by_autologin_key(cookies[:autologin])
52 elsif params[:key] && accept_key_auth_actions.include?(params[:action])
52 elsif params[:key] && accept_key_auth_actions.include?(params[:action])
53 # RSS key authentication
53 # RSS key authentication
54 User.find_by_rss_key(params[:key])
54 User.find_by_rss_key(params[:key])
55 end
55 end
56 end
56 end
57
57
58 # check if login is globally required to access the application
58 # check if login is globally required to access the application
59 def check_if_login_required
59 def check_if_login_required
60 # no check needed if user is already logged in
60 # no check needed if user is already logged in
61 return true if User.current.logged?
61 return true if User.current.logged?
62 require_login if Setting.login_required?
62 require_login if Setting.login_required?
63 end
63 end
64
64
65 def set_localization
65 def set_localization
66 User.current.language = nil unless User.current.logged?
66 User.current.language = nil unless User.current.logged?
67 lang = begin
67 lang = begin
68 if !User.current.language.blank? && GLoc.valid_language?(User.current.language)
68 if !User.current.language.blank? && GLoc.valid_language?(User.current.language)
69 User.current.language
69 User.current.language
70 elsif request.env['HTTP_ACCEPT_LANGUAGE']
70 elsif request.env['HTTP_ACCEPT_LANGUAGE']
71 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
71 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
72 if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first))
72 if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first))
73 User.current.language = accept_lang
73 User.current.language = accept_lang
74 end
74 end
75 end
75 end
76 rescue
76 rescue
77 nil
77 nil
78 end || Setting.default_language
78 end || Setting.default_language
79 set_language_if_valid(lang)
79 set_language_if_valid(lang)
80 end
80 end
81
81
82 def require_login
82 def require_login
83 if !User.current.logged?
83 if !User.current.logged?
84 redirect_to :controller => "account", :action => "login", :back_url => request.request_uri
84 redirect_to :controller => "account", :action => "login", :back_url => request.request_uri
85 return false
85 return false
86 end
86 end
87 true
87 true
88 end
88 end
89
89
90 def require_admin
90 def require_admin
91 return unless require_login
91 return unless require_login
92 if !User.current.admin?
92 if !User.current.admin?
93 render_403
93 render_403
94 return false
94 return false
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
106 # used as a before_filter for actions that do not require any particular permission on the project
110 # used as a before_filter for actions that do not require any particular permission on the project
107 def check_project_privacy
111 def check_project_privacy
108 if @project && @project.active?
112 if @project && @project.active?
109 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
113 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
110 true
114 true
111 else
115 else
112 User.current.logged? ? render_403 : require_login
116 User.current.logged? ? render_403 : require_login
113 end
117 end
114 else
118 else
115 @project = nil
119 @project = nil
116 render_404
120 render_404
117 false
121 false
118 end
122 end
119 end
123 end
120
124
121 def redirect_back_or_default(default)
125 def redirect_back_or_default(default)
122 back_url = params[:back_url]
126 back_url = params[:back_url]
123 if !back_url.blank?
127 if !back_url.blank?
124 uri = URI.parse(back_url)
128 uri = URI.parse(back_url)
125 # do not redirect user to another host
129 # do not redirect user to another host
126 if uri.relative? || (uri.host == request.host)
130 if uri.relative? || (uri.host == request.host)
127 redirect_to(back_url) and return
131 redirect_to(back_url) and return
128 end
132 end
129 end
133 end
130 redirect_to default
134 redirect_to default
131 end
135 end
132
136
133 def render_403
137 def render_403
134 @project = nil
138 @project = nil
135 render :template => "common/403", :layout => !request.xhr?, :status => 403
139 render :template => "common/403", :layout => !request.xhr?, :status => 403
136 return false
140 return false
137 end
141 end
138
142
139 def render_404
143 def render_404
140 render :template => "common/404", :layout => !request.xhr?, :status => 404
144 render :template => "common/404", :layout => !request.xhr?, :status => 404
141 return false
145 return false
142 end
146 end
143
147
144 def render_error(msg)
148 def render_error(msg)
145 flash.now[:error] = msg
149 flash.now[:error] = msg
146 render :nothing => true, :layout => !request.xhr?, :status => 500
150 render :nothing => true, :layout => !request.xhr?, :status => 500
147 end
151 end
148
152
149 def render_feed(items, options={})
153 def render_feed(items, options={})
150 @items = items || []
154 @items = items || []
151 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
155 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
152 @items = @items.slice(0, Setting.feeds_limit.to_i)
156 @items = @items.slice(0, Setting.feeds_limit.to_i)
153 @title = options[:title] || Setting.app_title
157 @title = options[:title] || Setting.app_title
154 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
158 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
155 end
159 end
156
160
157 def self.accept_key_auth(*actions)
161 def self.accept_key_auth(*actions)
158 actions = actions.flatten.map(&:to_s)
162 actions = actions.flatten.map(&:to_s)
159 write_inheritable_attribute('accept_key_auth_actions', actions)
163 write_inheritable_attribute('accept_key_auth_actions', actions)
160 end
164 end
161
165
162 def accept_key_auth_actions
166 def accept_key_auth_actions
163 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
167 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
164 end
168 end
165
169
166 # TODO: move to model
170 # TODO: move to model
167 def attach_files(obj, attachments)
171 def attach_files(obj, attachments)
168 attached = []
172 attached = []
169 if attachments && attachments.is_a?(Hash)
173 if attachments && attachments.is_a?(Hash)
170 attachments.each_value do |attachment|
174 attachments.each_value do |attachment|
171 file = attachment['file']
175 file = attachment['file']
172 next unless file && file.size > 0
176 next unless file && file.size > 0
173 a = Attachment.create(:container => obj,
177 a = Attachment.create(:container => obj,
174 :file => file,
178 :file => file,
175 :description => attachment['description'].to_s.strip,
179 :description => attachment['description'].to_s.strip,
176 :author => User.current)
180 :author => User.current)
177 attached << a unless a.new_record?
181 attached << a unless a.new_record?
178 end
182 end
179 end
183 end
180 attached
184 attached
181 end
185 end
182
186
183 # Returns the number of objects that should be displayed
187 # Returns the number of objects that should be displayed
184 # on the paginated list
188 # on the paginated list
185 def per_page_option
189 def per_page_option
186 per_page = nil
190 per_page = nil
187 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
191 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
188 per_page = params[:per_page].to_s.to_i
192 per_page = params[:per_page].to_s.to_i
189 session[:per_page] = per_page
193 session[:per_page] = per_page
190 elsif session[:per_page]
194 elsif session[:per_page]
191 per_page = session[:per_page]
195 per_page = session[:per_page]
192 else
196 else
193 per_page = Setting.per_page_options_array.first || 25
197 per_page = Setting.per_page_options_array.first || 25
194 end
198 end
195 per_page
199 per_page
196 end
200 end
197
201
198 # qvalues http header parser
202 # qvalues http header parser
199 # code taken from webrick
203 # code taken from webrick
200 def parse_qvalues(value)
204 def parse_qvalues(value)
201 tmp = []
205 tmp = []
202 if value
206 if value
203 parts = value.split(/,\s*/)
207 parts = value.split(/,\s*/)
204 parts.each {|part|
208 parts.each {|part|
205 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
209 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
206 val = m[1]
210 val = m[1]
207 q = (m[2] or 1).to_f
211 q = (m[2] or 1).to_f
208 tmp.push([val, q])
212 tmp.push([val, q])
209 end
213 end
210 }
214 }
211 tmp = tmp.sort_by{|val, q| -q}
215 tmp = tmp.sort_by{|val, q| -q}
212 tmp.collect!{|val, q| val}
216 tmp.collect!{|val, q| val}
213 end
217 end
214 return tmp
218 return tmp
215 end
219 end
216
220
217 # Returns a string that can be used as filename value in Content-Disposition header
221 # Returns a string that can be used as filename value in Content-Disposition header
218 def filename_for_content_disposition(name)
222 def filename_for_content_disposition(name)
219 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
223 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
220 end
224 end
221 end
225 end
@@ -1,267 +1,285
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 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
24 helper :sort
25 helper :sort
25 include SortHelper
26 include SortHelper
26 helper :issues
27 helper :issues
27 include TimelogHelper
28 include TimelogHelper
28 helper :custom_fields
29 helper :custom_fields
29 include CustomFieldsHelper
30 include CustomFieldsHelper
30
31
31 def report
32 def report
32 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
33 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
33 :klass => Project,
34 :klass => Project,
34 :label => :label_project},
35 :label => :label_project},
35 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
36 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
36 :klass => Version,
37 :klass => Version,
37 :label => :label_version},
38 :label => :label_version},
38 'category' => {:sql => "#{Issue.table_name}.category_id",
39 'category' => {:sql => "#{Issue.table_name}.category_id",
39 :klass => IssueCategory,
40 :klass => IssueCategory,
40 :label => :field_category},
41 :label => :field_category},
41 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
42 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
42 :klass => User,
43 :klass => User,
43 :label => :label_member},
44 :label => :label_member},
44 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
45 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
45 :klass => Tracker,
46 :klass => Tracker,
46 :label => :label_tracker},
47 :label => :label_tracker},
47 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
48 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
48 :klass => Enumeration,
49 :klass => Enumeration,
49 :label => :label_activity},
50 :label => :label_activity},
50 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
51 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
51 :klass => Issue,
52 :klass => Issue,
52 :label => :label_issue}
53 :label => :label_issue}
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|
64 @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)",
66 @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)",
65 :format => cf.field_format,
67 :format => cf.field_format,
66 :label => cf.name}
68 :label => cf.name}
67 end
69 end
68
70
69 @criterias = params[:criterias] || []
71 @criterias = params[:criterias] || []
70 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
72 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
71 @criterias.uniq!
73 @criterias.uniq!
72 @criterias = @criterias[0,3]
74 @criterias = @criterias[0,3]
73
75
74 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
76 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
75
77
76 retrieve_date_range
78 retrieve_date_range
77
79
78 unless @criterias.empty?
80 unless @criterias.empty?
79 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
81 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
80 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
82 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
81
83
82 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
84 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
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)
92
95
93 @hours.each do |row|
96 @hours.each do |row|
94 case @columns
97 case @columns
95 when 'year'
98 when 'year'
96 row['year'] = row['tyear']
99 row['year'] = row['tyear']
97 when 'month'
100 when 'month'
98 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
101 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
99 when 'week'
102 when 'week'
100 row['week'] = "#{row['tyear']}-#{row['tweek']}"
103 row['week'] = "#{row['tyear']}-#{row['tweek']}"
101 when 'day'
104 when 'day'
102 row['day'] = "#{row['spent_on']}"
105 row['day'] = "#{row['spent_on']}"
103 end
106 end
104 end
107 end
105
108
106 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
109 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
107
110
108 @periods = []
111 @periods = []
109 # Date#at_beginning_of_ not supported in Rails 1.2.x
112 # Date#at_beginning_of_ not supported in Rails 1.2.x
110 date_from = @from.to_time
113 date_from = @from.to_time
111 # 100 columns max
114 # 100 columns max
112 while date_from <= @to.to_time && @periods.length < 100
115 while date_from <= @to.to_time && @periods.length < 100
113 case @columns
116 case @columns
114 when 'year'
117 when 'year'
115 @periods << "#{date_from.year}"
118 @periods << "#{date_from.year}"
116 date_from = (date_from + 1.year).at_beginning_of_year
119 date_from = (date_from + 1.year).at_beginning_of_year
117 when 'month'
120 when 'month'
118 @periods << "#{date_from.year}-#{date_from.month}"
121 @periods << "#{date_from.year}-#{date_from.month}"
119 date_from = (date_from + 1.month).at_beginning_of_month
122 date_from = (date_from + 1.month).at_beginning_of_month
120 when 'week'
123 when 'week'
121 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
124 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
122 date_from = (date_from + 7.day).at_beginning_of_week
125 date_from = (date_from + 7.day).at_beginning_of_week
123 when 'day'
126 when 'day'
124 @periods << "#{date_from.to_date}"
127 @periods << "#{date_from.to_date}"
125 date_from = date_from + 1.day
128 date_from = date_from + 1.day
126 end
129 end
127 end
130 end
128 end
131 end
129
132
130 respond_to do |format|
133 respond_to do |format|
131 format.html { render :layout => !request.xhr? }
134 format.html { render :layout => !request.xhr? }
132 format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
135 format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
133 end
136 end
134 end
137 end
135
138
136 def details
139 def details
137 sort_init 'spent_on', 'desc'
140 sort_init 'spent_on', 'desc'
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]
146
154
147 TimeEntry.visible_by(User.current) do
155 TimeEntry.visible_by(User.current) do
148 respond_to do |format|
156 respond_to do |format|
149 format.html {
157 format.html {
150 # Paginate results
158 # Paginate results
151 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
159 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
152 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
160 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
153 @entries = TimeEntry.find(:all,
161 @entries = TimeEntry.find(:all,
154 :include => [:project, :activity, :user, {:issue => :tracker}],
162 :include => [:project, :activity, :user, {:issue => :tracker}],
155 :conditions => cond.conditions,
163 :conditions => cond.conditions,
156 :order => sort_clause,
164 :order => sort_clause,
157 :limit => @entry_pages.items_per_page,
165 :limit => @entry_pages.items_per_page,
158 :offset => @entry_pages.current.offset)
166 :offset => @entry_pages.current.offset)
159 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
167 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
160
168
161 render :layout => !request.xhr?
169 render :layout => !request.xhr?
162 }
170 }
163 format.atom {
171 format.atom {
164 entries = TimeEntry.find(:all,
172 entries = TimeEntry.find(:all,
165 :include => [:project, :activity, :user, {:issue => :tracker}],
173 :include => [:project, :activity, :user, {:issue => :tracker}],
166 :conditions => cond.conditions,
174 :conditions => cond.conditions,
167 :order => "#{TimeEntry.table_name}.created_on DESC",
175 :order => "#{TimeEntry.table_name}.created_on DESC",
168 :limit => Setting.feeds_limit.to_i)
176 :limit => Setting.feeds_limit.to_i)
169 render_feed(entries, :title => l(:label_spent_time))
177 render_feed(entries, :title => l(:label_spent_time))
170 }
178 }
171 format.csv {
179 format.csv {
172 # Export all entries
180 # Export all entries
173 @entries = TimeEntry.find(:all,
181 @entries = TimeEntry.find(:all,
174 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
182 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
175 :conditions => cond.conditions,
183 :conditions => cond.conditions,
176 :order => sort_clause)
184 :order => sort_clause)
177 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
185 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
178 }
186 }
179 end
187 end
180 end
188 end
181 end
189 end
182
190
183 def edit
191 def edit
184 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
192 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
185 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
193 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
186 @time_entry.attributes = params[:time_entry]
194 @time_entry.attributes = params[:time_entry]
187 if request.post? and @time_entry.save
195 if request.post? and @time_entry.save
188 flash[:notice] = l(:notice_successful_update)
196 flash[:notice] = l(:notice_successful_update)
189 redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url])
197 redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url])
190 return
198 return
191 end
199 end
192 end
200 end
193
201
194 def destroy
202 def destroy
195 render_404 and return unless @time_entry
203 render_404 and return unless @time_entry
196 render_403 and return unless @time_entry.editable_by?(User.current)
204 render_403 and return unless @time_entry.editable_by?(User.current)
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
204 private
212 private
205 def find_project
213 def find_project
206 if params[:id]
214 if params[:id]
207 @time_entry = TimeEntry.find(params[:id])
215 @time_entry = TimeEntry.find(params[:id])
208 @project = @time_entry.project
216 @project = @time_entry.project
209 elsif params[:issue_id]
217 elsif params[:issue_id]
210 @issue = Issue.find(params[:issue_id])
218 @issue = Issue.find(params[:issue_id])
211 @project = @issue.project
219 @project = @issue.project
212 elsif params[:project_id]
220 elsif params[:project_id]
213 @project = Project.find(params[:project_id])
221 @project = Project.find(params[:project_id])
214 else
222 else
215 render_404
223 render_404
216 return false
224 return false
217 end
225 end
218 rescue ActiveRecord::RecordNotFound
226 rescue ActiveRecord::RecordNotFound
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
225 @from, @to = nil, nil
243 @from, @to = nil, nil
226
244
227 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
245 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
228 case params[:period].to_s
246 case params[:period].to_s
229 when 'today'
247 when 'today'
230 @from = @to = Date.today
248 @from = @to = Date.today
231 when 'yesterday'
249 when 'yesterday'
232 @from = @to = Date.today - 1
250 @from = @to = Date.today - 1
233 when 'current_week'
251 when 'current_week'
234 @from = Date.today - (Date.today.cwday - 1)%7
252 @from = Date.today - (Date.today.cwday - 1)%7
235 @to = @from + 6
253 @to = @from + 6
236 when 'last_week'
254 when 'last_week'
237 @from = Date.today - 7 - (Date.today.cwday - 1)%7
255 @from = Date.today - 7 - (Date.today.cwday - 1)%7
238 @to = @from + 6
256 @to = @from + 6
239 when '7_days'
257 when '7_days'
240 @from = Date.today - 7
258 @from = Date.today - 7
241 @to = Date.today
259 @to = Date.today
242 when 'current_month'
260 when 'current_month'
243 @from = Date.civil(Date.today.year, Date.today.month, 1)
261 @from = Date.civil(Date.today.year, Date.today.month, 1)
244 @to = (@from >> 1) - 1
262 @to = (@from >> 1) - 1
245 when 'last_month'
263 when 'last_month'
246 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
264 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
247 @to = (@from >> 1) - 1
265 @to = (@from >> 1) - 1
248 when '30_days'
266 when '30_days'
249 @from = Date.today - 30
267 @from = Date.today - 30
250 @to = Date.today
268 @to = Date.today
251 when 'current_year'
269 when 'current_year'
252 @from = Date.civil(Date.today.year, 1, 1)
270 @from = Date.civil(Date.today.year, 1, 1)
253 @to = Date.civil(Date.today.year, 12, 31)
271 @to = Date.civil(Date.today.year, 12, 31)
254 end
272 end
255 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
273 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
256 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
274 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
257 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
275 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
258 @free_period = true
276 @free_period = true
259 else
277 else
260 # default
278 # default
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
@@ -1,150 +1,158
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 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 = []
22 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
30 collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
23 activities.each { |a| collection << [a.name, a.id] }
31 activities.each { |a| collection << [a.name, a.id] }
24 collection
32 collection
25 end
33 end
26
34
27 def select_hours(data, criteria, value)
35 def select_hours(data, criteria, value)
28 data.select {|row| row[criteria] == value}
36 data.select {|row| row[criteria] == value}
29 end
37 end
30
38
31 def sum_hours(data)
39 def sum_hours(data)
32 sum = 0
40 sum = 0
33 data.each do |row|
41 data.each do |row|
34 sum += row['hours'].to_f
42 sum += row['hours'].to_f
35 end
43 end
36 sum
44 sum
37 end
45 end
38
46
39 def options_for_period_select(value)
47 def options_for_period_select(value)
40 options_for_select([[l(:label_all_time), 'all'],
48 options_for_select([[l(:label_all_time), 'all'],
41 [l(:label_today), 'today'],
49 [l(:label_today), 'today'],
42 [l(:label_yesterday), 'yesterday'],
50 [l(:label_yesterday), 'yesterday'],
43 [l(:label_this_week), 'current_week'],
51 [l(:label_this_week), 'current_week'],
44 [l(:label_last_week), 'last_week'],
52 [l(:label_last_week), 'last_week'],
45 [l(:label_last_n_days, 7), '7_days'],
53 [l(:label_last_n_days, 7), '7_days'],
46 [l(:label_this_month), 'current_month'],
54 [l(:label_this_month), 'current_month'],
47 [l(:label_last_month), 'last_month'],
55 [l(:label_last_month), 'last_month'],
48 [l(:label_last_n_days, 30), '30_days'],
56 [l(:label_last_n_days, 30), '30_days'],
49 [l(:label_this_year), 'current_year']],
57 [l(:label_this_year), 'current_year']],
50 value)
58 value)
51 end
59 end
52
60
53 def entries_to_csv(entries)
61 def entries_to_csv(entries)
54 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
62 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
55 decimal_separator = l(:general_csv_decimal_separator)
63 decimal_separator = l(:general_csv_decimal_separator)
56 custom_fields = TimeEntryCustomField.find(:all)
64 custom_fields = TimeEntryCustomField.find(:all)
57 export = StringIO.new
65 export = StringIO.new
58 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
66 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
59 # csv header fields
67 # csv header fields
60 headers = [l(:field_spent_on),
68 headers = [l(:field_spent_on),
61 l(:field_user),
69 l(:field_user),
62 l(:field_activity),
70 l(:field_activity),
63 l(:field_project),
71 l(:field_project),
64 l(:field_issue),
72 l(:field_issue),
65 l(:field_tracker),
73 l(:field_tracker),
66 l(:field_subject),
74 l(:field_subject),
67 l(:field_hours),
75 l(:field_hours),
68 l(:field_comments)
76 l(:field_comments)
69 ]
77 ]
70 # Export custom fields
78 # Export custom fields
71 headers += custom_fields.collect(&:name)
79 headers += custom_fields.collect(&:name)
72
80
73 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
81 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
74 # csv lines
82 # csv lines
75 entries.each do |entry|
83 entries.each do |entry|
76 fields = [l_date(entry.spent_on),
84 fields = [l_date(entry.spent_on),
77 entry.user,
85 entry.user,
78 entry.activity,
86 entry.activity,
79 entry.project,
87 entry.project,
80 (entry.issue ? entry.issue.id : nil),
88 (entry.issue ? entry.issue.id : nil),
81 (entry.issue ? entry.issue.tracker : nil),
89 (entry.issue ? entry.issue.tracker : nil),
82 (entry.issue ? entry.issue.subject : nil),
90 (entry.issue ? entry.issue.subject : nil),
83 entry.hours.to_s.gsub('.', decimal_separator),
91 entry.hours.to_s.gsub('.', decimal_separator),
84 entry.comments
92 entry.comments
85 ]
93 ]
86 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
94 fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
87
95
88 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
96 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
89 end
97 end
90 end
98 end
91 export.rewind
99 export.rewind
92 export
100 export
93 end
101 end
94
102
95 def format_criteria_value(criteria, value)
103 def format_criteria_value(criteria, value)
96 value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
104 value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
97 end
105 end
98
106
99 def report_to_csv(criterias, periods, hours)
107 def report_to_csv(criterias, periods, hours)
100 export = StringIO.new
108 export = StringIO.new
101 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
109 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
102 # Column headers
110 # Column headers
103 headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
111 headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
104 headers += periods
112 headers += periods
105 headers << l(:label_total)
113 headers << l(:label_total)
106 csv << headers.collect {|c| to_utf8(c) }
114 csv << headers.collect {|c| to_utf8(c) }
107 # Content
115 # Content
108 report_criteria_to_csv(csv, criterias, periods, hours)
116 report_criteria_to_csv(csv, criterias, periods, hours)
109 # Total row
117 # Total row
110 row = [ l(:label_total) ] + [''] * (criterias.size - 1)
118 row = [ l(:label_total) ] + [''] * (criterias.size - 1)
111 total = 0
119 total = 0
112 periods.each do |period|
120 periods.each do |period|
113 sum = sum_hours(select_hours(hours, @columns, period.to_s))
121 sum = sum_hours(select_hours(hours, @columns, period.to_s))
114 total += sum
122 total += sum
115 row << (sum > 0 ? "%.2f" % sum : '')
123 row << (sum > 0 ? "%.2f" % sum : '')
116 end
124 end
117 row << "%.2f" %total
125 row << "%.2f" %total
118 csv << row
126 csv << row
119 end
127 end
120 export.rewind
128 export.rewind
121 export
129 export
122 end
130 end
123
131
124 def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
132 def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
125 hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
133 hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
126 hours_for_value = select_hours(hours, criterias[level], value)
134 hours_for_value = select_hours(hours, criterias[level], value)
127 next if hours_for_value.empty?
135 next if hours_for_value.empty?
128 row = [''] * level
136 row = [''] * level
129 row << to_utf8(format_criteria_value(criterias[level], value))
137 row << to_utf8(format_criteria_value(criterias[level], value))
130 row += [''] * (criterias.length - level - 1)
138 row += [''] * (criterias.length - level - 1)
131 total = 0
139 total = 0
132 periods.each do |period|
140 periods.each do |period|
133 sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
141 sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
134 total += sum
142 total += sum
135 row << (sum > 0 ? "%.2f" % sum : '')
143 row << (sum > 0 ? "%.2f" % sum : '')
136 end
144 end
137 row << "%.2f" %total
145 row << "%.2f" %total
138 csv << row
146 csv << row
139
147
140 if criterias.length > level + 1
148 if criterias.length > level + 1
141 report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
149 report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
142 end
150 end
143 end
151 end
144 end
152 end
145
153
146 def to_utf8(s)
154 def to_utf8(s)
147 @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
155 @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
148 begin; @ic.iconv(s.to_s); rescue; s.to_s; end
156 begin; @ic.iconv(s.to_s); rescue; s.to_s; end
149 end
157 end
150 end
158 end
@@ -1,294 +1,294
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < ActiveRecord::Base
20 class User < ActiveRecord::Base
21
21
22 # Account statuses
22 # Account statuses
23 STATUS_ANONYMOUS = 0
23 STATUS_ANONYMOUS = 0
24 STATUS_ACTIVE = 1
24 STATUS_ACTIVE = 1
25 STATUS_REGISTERED = 2
25 STATUS_REGISTERED = 2
26 STATUS_LOCKED = 3
26 STATUS_LOCKED = 3
27
27
28 USER_FORMATS = {
28 USER_FORMATS = {
29 :firstname_lastname => '#{firstname} #{lastname}',
29 :firstname_lastname => '#{firstname} #{lastname}',
30 :firstname => '#{firstname}',
30 :firstname => '#{firstname}',
31 :lastname_firstname => '#{lastname} #{firstname}',
31 :lastname_firstname => '#{lastname} #{firstname}',
32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 :username => '#{login}'
33 :username => '#{login}'
34 }
34 }
35
35
36 has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
36 has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
37 has_many :members, :dependent => :delete_all
37 has_many :members, :dependent => :delete_all
38 has_many :projects, :through => :memberships
38 has_many :projects, :through => :memberships
39 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
39 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
40 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
40 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
41 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
41 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
42 belongs_to :auth_source
42 belongs_to :auth_source
43
43
44 acts_as_customizable
44 acts_as_customizable
45
45
46 attr_accessor :password, :password_confirmation
46 attr_accessor :password, :password_confirmation
47 attr_accessor :last_before_login_on
47 attr_accessor :last_before_login_on
48 # Prevents unauthorized assignments
48 # Prevents unauthorized assignments
49 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
49 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
50
50
51 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
51 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
52 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
52 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
53 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
53 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
54 # Login must contain lettres, numbers, underscores only
54 # Login must contain lettres, numbers, underscores only
55 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
55 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
56 validates_length_of :login, :maximum => 30
56 validates_length_of :login, :maximum => 30
57 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
57 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
58 validates_length_of :firstname, :lastname, :maximum => 30
58 validates_length_of :firstname, :lastname, :maximum => 30
59 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
59 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
60 validates_length_of :mail, :maximum => 60, :allow_nil => true
60 validates_length_of :mail, :maximum => 60, :allow_nil => true
61 validates_length_of :password, :minimum => 4, :allow_nil => true
61 validates_length_of :password, :minimum => 4, :allow_nil => true
62 validates_confirmation_of :password, :allow_nil => true
62 validates_confirmation_of :password, :allow_nil => true
63
63
64 def before_create
64 def before_create
65 self.mail_notification = false
65 self.mail_notification = false
66 true
66 true
67 end
67 end
68
68
69 def before_save
69 def before_save
70 # update hashed_password if password was set
70 # update hashed_password if password was set
71 self.hashed_password = User.hash_password(self.password) if self.password
71 self.hashed_password = User.hash_password(self.password) if self.password
72 end
72 end
73
73
74 def self.active
74 def self.active
75 with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
75 with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
76 yield
76 yield
77 end
77 end
78 end
78 end
79
79
80 def self.find_active(*args)
80 def self.find_active(*args)
81 active do
81 active do
82 find(*args)
82 find(*args)
83 end
83 end
84 end
84 end
85
85
86 # Returns the user that matches provided login and password, or nil
86 # Returns the user that matches provided login and password, or nil
87 def self.try_to_login(login, password)
87 def self.try_to_login(login, password)
88 # Make sure no one can sign in with an empty password
88 # Make sure no one can sign in with an empty password
89 return nil if password.to_s.empty?
89 return nil if password.to_s.empty?
90 user = find(:first, :conditions => ["login=?", login])
90 user = find(:first, :conditions => ["login=?", login])
91 if user
91 if user
92 # user is already in local database
92 # user is already in local database
93 return nil if !user.active?
93 return nil if !user.active?
94 if user.auth_source
94 if user.auth_source
95 # user has an external authentication method
95 # user has an external authentication method
96 return nil unless user.auth_source.authenticate(login, password)
96 return nil unless user.auth_source.authenticate(login, password)
97 else
97 else
98 # authentication with local password
98 # authentication with local password
99 return nil unless User.hash_password(password) == user.hashed_password
99 return nil unless User.hash_password(password) == user.hashed_password
100 end
100 end
101 else
101 else
102 # user is not yet registered, try to authenticate with available sources
102 # user is not yet registered, try to authenticate with available sources
103 attrs = AuthSource.authenticate(login, password)
103 attrs = AuthSource.authenticate(login, password)
104 if attrs
104 if attrs
105 user = new(*attrs)
105 user = new(*attrs)
106 user.login = login
106 user.login = login
107 user.language = Setting.default_language
107 user.language = Setting.default_language
108 if user.save
108 if user.save
109 user.reload
109 user.reload
110 logger.info("User '#{user.login}' created from the LDAP") if logger
110 logger.info("User '#{user.login}' created from the LDAP") if logger
111 end
111 end
112 end
112 end
113 end
113 end
114 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
114 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
115 user
115 user
116 rescue => text
116 rescue => text
117 raise text
117 raise text
118 end
118 end
119
119
120 # Return user's full name for display
120 # Return user's full name for display
121 def name(formatter = nil)
121 def name(formatter = nil)
122 f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
122 f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
123 eval '"' + f + '"'
123 eval '"' + f + '"'
124 end
124 end
125
125
126 def active?
126 def active?
127 self.status == STATUS_ACTIVE
127 self.status == STATUS_ACTIVE
128 end
128 end
129
129
130 def registered?
130 def registered?
131 self.status == STATUS_REGISTERED
131 self.status == STATUS_REGISTERED
132 end
132 end
133
133
134 def locked?
134 def locked?
135 self.status == STATUS_LOCKED
135 self.status == STATUS_LOCKED
136 end
136 end
137
137
138 def check_password?(clear_password)
138 def check_password?(clear_password)
139 User.hash_password(clear_password) == self.hashed_password
139 User.hash_password(clear_password) == self.hashed_password
140 end
140 end
141
141
142 def pref
142 def pref
143 self.preference ||= UserPreference.new(:user => self)
143 self.preference ||= UserPreference.new(:user => self)
144 end
144 end
145
145
146 def time_zone
146 def time_zone
147 self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
147 self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
148 end
148 end
149
149
150 def wants_comments_in_reverse_order?
150 def wants_comments_in_reverse_order?
151 self.pref[:comments_sorting] == 'desc'
151 self.pref[:comments_sorting] == 'desc'
152 end
152 end
153
153
154 # Return user's RSS key (a 40 chars long string), used to access feeds
154 # Return user's RSS key (a 40 chars long string), used to access feeds
155 def rss_key
155 def rss_key
156 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
156 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
157 token.value
157 token.value
158 end
158 end
159
159
160 # Return an array of project ids for which the user has explicitly turned mail notifications on
160 # Return an array of project ids for which the user has explicitly turned mail notifications on
161 def notified_projects_ids
161 def notified_projects_ids
162 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
162 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
163 end
163 end
164
164
165 def notified_project_ids=(ids)
165 def notified_project_ids=(ids)
166 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
166 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
167 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
167 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
168 @notified_projects_ids = nil
168 @notified_projects_ids = nil
169 notified_projects_ids
169 notified_projects_ids
170 end
170 end
171
171
172 def self.find_by_rss_key(key)
172 def self.find_by_rss_key(key)
173 token = Token.find_by_value(key)
173 token = Token.find_by_value(key)
174 token && token.user.active? ? token.user : nil
174 token && token.user.active? ? token.user : nil
175 end
175 end
176
176
177 def self.find_by_autologin_key(key)
177 def self.find_by_autologin_key(key)
178 token = Token.find_by_action_and_value('autologin', key)
178 token = Token.find_by_action_and_value('autologin', key)
179 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
179 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
180 end
180 end
181
181
182 def <=>(user)
182 def <=>(user)
183 if user.nil?
183 if user.nil?
184 -1
184 -1
185 elsif lastname.to_s.downcase == user.lastname.to_s.downcase
185 elsif lastname.to_s.downcase == user.lastname.to_s.downcase
186 firstname.to_s.downcase <=> user.firstname.to_s.downcase
186 firstname.to_s.downcase <=> user.firstname.to_s.downcase
187 else
187 else
188 lastname.to_s.downcase <=> user.lastname.to_s.downcase
188 lastname.to_s.downcase <=> user.lastname.to_s.downcase
189 end
189 end
190 end
190 end
191
191
192 def to_s
192 def to_s
193 name
193 name
194 end
194 end
195
195
196 def logged?
196 def logged?
197 true
197 true
198 end
198 end
199
199
200 def anonymous?
200 def anonymous?
201 !logged?
201 !logged?
202 end
202 end
203
203
204 # Return user's role for project
204 # Return user's role for project
205 def role_for_project(project)
205 def role_for_project(project)
206 # No role on archived projects
206 # No role on archived projects
207 return nil unless project && project.active?
207 return nil unless project && project.active?
208 if logged?
208 if logged?
209 # Find project membership
209 # Find project membership
210 membership = memberships.detect {|m| m.project_id == project.id}
210 membership = memberships.detect {|m| m.project_id == project.id}
211 if membership
211 if membership
212 membership.role
212 membership.role
213 else
213 else
214 @role_non_member ||= Role.non_member
214 @role_non_member ||= Role.non_member
215 end
215 end
216 else
216 else
217 @role_anonymous ||= Role.anonymous
217 @role_anonymous ||= Role.anonymous
218 end
218 end
219 end
219 end
220
220
221 # Return true if the user is a member of project
221 # Return true if the user is a member of project
222 def member_of?(project)
222 def member_of?(project)
223 role_for_project(project).member?
223 role_for_project(project).member?
224 end
224 end
225
225
226 # Return true if the user is allowed to do the specified action on project
226 # Return true if the user is allowed to do the specified action on project
227 # action can be:
227 # action can be:
228 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
228 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
229 # * a permission Symbol (eg. :edit_project)
229 # * a permission Symbol (eg. :edit_project)
230 def allowed_to?(action, project, options={})
230 def allowed_to?(action, project, options={})
231 if project
231 if project
232 # No action allowed on archived projects
232 # No action allowed on archived projects
233 return false unless project.active?
233 return false unless project.active?
234 # No action allowed on disabled modules
234 # No action allowed on disabled modules
235 return false unless project.allows_to?(action)
235 return false unless project.allows_to?(action)
236 # Admin users are authorized for anything else
236 # Admin users are authorized for anything else
237 return true if admin?
237 return true if admin?
238
238
239 role = role_for_project(project)
239 role = role_for_project(project)
240 return false unless role
240 return false unless role
241 role.allowed_to?(action) && (project.is_public? || role.member?)
241 role.allowed_to?(action) && (project.is_public? || role.member?)
242
242
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
250 end
250 end
251
251
252 def self.current=(user)
252 def self.current=(user)
253 @current_user = user
253 @current_user = user
254 end
254 end
255
255
256 def self.current
256 def self.current
257 @current_user ||= User.anonymous
257 @current_user ||= User.anonymous
258 end
258 end
259
259
260 def self.anonymous
260 def self.anonymous
261 anonymous_user = AnonymousUser.find(:first)
261 anonymous_user = AnonymousUser.find(:first)
262 if anonymous_user.nil?
262 if anonymous_user.nil?
263 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
263 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
264 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
264 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
265 end
265 end
266 anonymous_user
266 anonymous_user
267 end
267 end
268
268
269 private
269 private
270 # Return password digest
270 # Return password digest
271 def self.hash_password(clear_password)
271 def self.hash_password(clear_password)
272 Digest::SHA1.hexdigest(clear_password || "")
272 Digest::SHA1.hexdigest(clear_password || "")
273 end
273 end
274 end
274 end
275
275
276 class AnonymousUser < User
276 class AnonymousUser < User
277
277
278 def validate_on_create
278 def validate_on_create
279 # There should be only one AnonymousUser in the database
279 # There should be only one AnonymousUser in the database
280 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
280 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
281 end
281 end
282
282
283 def available_custom_fields
283 def available_custom_fields
284 []
284 []
285 end
285 end
286
286
287 # Overrides a few properties
287 # Overrides a few properties
288 def logged?; false end
288 def logged?; false end
289 def admin; false end
289 def admin; false end
290 def name; 'Anonymous' end
290 def name; 'Anonymous' end
291 def mail; nil end
291 def mail; nil end
292 def time_zone; nil end
292 def time_zone; nil end
293 def rss_key; nil end
293 def rss_key; nil end
294 end
294 end
@@ -1,36 +1,34
1 <div class="contextual">
1 <div class="contextual">
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] %>
13 <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
11 <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
14 <%= render :partial => 'date_range' %>
12 <%= render :partial => 'date_range' %>
15 <% end %>
13 <% end %>
16
14
17 <div class="total-hours">
15 <div class="total-hours">
18 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
16 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
19 </div>
17 </div>
20
18
21 <% unless @entries.empty? %>
19 <% unless @entries.empty? %>
22 <%= render :partial => 'list', :locals => { :entries => @entries }%>
20 <%= render :partial => 'list', :locals => { :entries => @entries }%>
23 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
21 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
24
22
25 <p class="other-formats">
23 <p class="other-formats">
26 <%= l(:label_export_to) %>
24 <%= l(:label_export_to) %>
27 <span><%= link_to 'Atom', {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
25 <span><%= link_to 'Atom', {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
28 <span><%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %></span>
26 <span><%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %></span>
29 </p>
27 </p>
30 <% end %>
28 <% end %>
31
29
32 <% html_title l(:label_spent_time), l(:label_details) %>
30 <% html_title l(:label_spent_time), l(:label_details) %>
33
31
34 <% content_for :header_tags do %>
32 <% content_for :header_tags do %>
35 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
33 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
36 <% end %>
34 <% end %>
@@ -1,72 +1,74
1 <div class="contextual">
1 <div class="contextual">
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 %>
8 <% @criterias.each do |criteria| %>
10 <% @criterias.each do |criteria| %>
9 <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
11 <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
10 <% end %>
12 <% end %>
11 <%= hidden_field_tag 'project_id', params[:project_id] %>
13 <%= hidden_field_tag 'project_id', params[:project_id] %>
12 <%= render :partial => 'date_range' %>
14 <%= render :partial => 'date_range' %>
13
15
14 <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
16 <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
15 [l(:label_month), 'month'],
17 [l(:label_month), 'month'],
16 [l(:label_week), 'week'],
18 [l(:label_week), 'week'],
17 [l(:label_day_plural).titleize, 'day']], @columns),
19 [l(:label_day_plural).titleize, 'day']], @columns),
18 :onchange => "this.form.onsubmit();" %>
20 :onchange => "this.form.onsubmit();" %>
19
21
20 <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
22 <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
21 :onchange => "this.form.onsubmit();",
23 :onchange => "this.form.onsubmit();",
22 :style => 'width: 200px',
24 :style => 'width: 200px',
23 :id => nil,
25 :id => nil,
24 :disabled => (@criterias.length >= 3)) %>
26 :disabled => (@criterias.length >= 3)) %>
25 <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns},
27 <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns},
26 :update => 'content'
28 :update => 'content'
27 }, :class => 'icon icon-reload' %></p>
29 }, :class => 'icon icon-reload' %></p>
28 <% end %>
30 <% end %>
29
31
30 <% unless @criterias.empty? %>
32 <% unless @criterias.empty? %>
31 <div class="total-hours">
33 <div class="total-hours">
32 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
34 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
33 </div>
35 </div>
34
36
35 <% unless @hours.empty? %>
37 <% unless @hours.empty? %>
36 <table class="list" id="time-report">
38 <table class="list" id="time-report">
37 <thead>
39 <thead>
38 <tr>
40 <tr>
39 <% @criterias.each do |criteria| %>
41 <% @criterias.each do |criteria| %>
40 <th><%= l(@available_criterias[criteria][:label]) %></th>
42 <th><%= l(@available_criterias[criteria][:label]) %></th>
41 <% end %>
43 <% end %>
42 <% columns_width = (40 / (@periods.length+1)).to_i %>
44 <% columns_width = (40 / (@periods.length+1)).to_i %>
43 <% @periods.each do |period| %>
45 <% @periods.each do |period| %>
44 <th class="period" width="<%= columns_width %>%"><%= period %></th>
46 <th class="period" width="<%= columns_width %>%"><%= period %></th>
45 <% end %>
47 <% end %>
46 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
48 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
47 </tr>
49 </tr>
48 </thead>
50 </thead>
49 <tbody>
51 <tbody>
50 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
52 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
51 <tr class="total">
53 <tr class="total">
52 <td><%= l(:label_total) %></td>
54 <td><%= l(:label_total) %></td>
53 <%= '<td></td>' * (@criterias.size - 1) %>
55 <%= '<td></td>' * (@criterias.size - 1) %>
54 <% total = 0 -%>
56 <% total = 0 -%>
55 <% @periods.each do |period| -%>
57 <% @periods.each do |period| -%>
56 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%>
58 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%>
57 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
59 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
58 <% end -%>
60 <% end -%>
59 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
61 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
60 </tr>
62 </tr>
61 </tbody>
63 </tbody>
62 </table>
64 </table>
63
65
64 <p class="other-formats">
66 <p class="other-formats">
65 <%= l(:label_export_to) %>
67 <%= l(:label_export_to) %>
66 <span><%= link_to 'CSV', params.merge({:format => 'csv'}), :class => 'csv' %></span>
68 <span><%= link_to 'CSV', params.merge({:format => 'csv'}), :class => 'csv' %></span>
67 </p>
69 </p>
68 <% end %>
70 <% end %>
69 <% end %>
71 <% end %>
70
72
71 <% html_title l(:label_spent_time), l(:label_report) %>
73 <% html_title l(:label_spent_time), l(:label_report) %>
72
74
@@ -1,46 +1,46
1 ActionController::Routing::Routes.draw do |map|
1 ActionController::Routing::Routes.draw do |map|
2 # Add your own custom routes here.
2 # Add your own custom routes here.
3 # The priority is based upon order of creation: first created -> highest priority.
3 # The priority is based upon order of creation: first created -> highest priority.
4
4
5 # Here's a sample route:
5 # Here's a sample route:
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 # Keep in mind you can assign values other than :controller and :action
7 # Keep in mind you can assign values other than :controller and :action
8
8
9 map.home '', :controller => 'welcome'
9 map.home '', :controller => 'welcome'
10 map.signin 'login', :controller => 'account', :action => 'login'
10 map.signin 'login', :controller => 'account', :action => 'login'
11 map.signout 'logout', :controller => 'account', :action => 'logout'
11 map.signout 'logout', :controller => 'account', :action => 'logout'
12
12
13 map.connect 'wiki/:id/:page/:action', :controller => 'wiki', :page => nil
13 map.connect 'wiki/:id/:page/:action', :controller => 'wiki', :page => nil
14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
15 map.connect 'help/:ctrl/:page', :controller => 'help'
15 map.connect 'help/:ctrl/:page', :controller => 'help'
16 #map.connect ':controller/:action/:id/:sort_key/:sort_order'
16 #map.connect ':controller/:action/:id/:sort_key/:sort_order'
17
17
18 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
18 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
19 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
19 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
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|
27 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
27 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
28 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
28 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
29 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
29 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
30 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
30 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
31 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
31 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
32 omap.repositories_revision 'repositories/revision/:id/:rev', :action => 'revision'
32 omap.repositories_revision 'repositories/revision/:id/:rev', :action => 'revision'
33 end
33 end
34
34
35 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
35 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
36 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
36 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
37 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
37 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
38
38
39 # Allow downloading Web Service WSDL as a file with an extension
39 # Allow downloading Web Service WSDL as a file with an extension
40 # instead of a file named 'wsdl'
40 # instead of a file named 'wsdl'
41 map.connect ':controller/service.wsdl', :action => 'wsdl'
41 map.connect ':controller/service.wsdl', :action => 'wsdl'
42
42
43
43
44 # Install the default route as the lowest priority.
44 # Install the default route as the lowest priority.
45 map.connect ':controller/:action/:id'
45 map.connect ':controller/:action/:id'
46 end
46 end
@@ -1,228 +1,278
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'timelog_controller'
19 require 'timelog_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class TimelogController; def rescue_action(e) raise e end; end
22 class TimelogController; def rescue_action(e) raise e end; end
23
23
24 class TimelogControllerTest < Test::Unit::TestCase
24 class TimelogControllerTest < Test::Unit::TestCase
25 fixtures :projects, :enabled_modules, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values
25 fixtures :projects, :enabled_modules, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values
26
26
27 def setup
27 def setup
28 @controller = TimelogController.new
28 @controller = TimelogController.new
29 @request = ActionController::TestRequest.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
30 @response = ActionController::TestResponse.new
31 end
31 end
32
32
33 def test_get_edit
33 def test_get_edit
34 @request.session[:user_id] = 3
34 @request.session[:user_id] = 3
35 get :edit, :project_id => 1
35 get :edit, :project_id => 1
36 assert_response :success
36 assert_response :success
37 assert_template 'edit'
37 assert_template 'edit'
38 # Default activity selected
38 # Default activity selected
39 assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
39 assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
40 :content => 'Development'
40 :content => 'Development'
41 end
41 end
42
42
43 def test_post_edit
43 def test_post_edit
44 @request.session[:user_id] = 3
44 @request.session[:user_id] = 3
45 post :edit, :project_id => 1,
45 post :edit, :project_id => 1,
46 :time_entry => {:comments => 'Some work on TimelogControllerTest',
46 :time_entry => {:comments => 'Some work on TimelogControllerTest',
47 # Not the default activity
47 # Not the default activity
48 :activity_id => '11',
48 :activity_id => '11',
49 :spent_on => '2008-03-14',
49 :spent_on => '2008-03-14',
50 :issue_id => '1',
50 :issue_id => '1',
51 :hours => '7.3'}
51 :hours => '7.3'}
52 assert_redirected_to 'projects/ecookbook/timelog/details'
52 assert_redirected_to 'projects/ecookbook/timelog/details'
53
53
54 i = Issue.find(1)
54 i = Issue.find(1)
55 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
55 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
56 assert_not_nil t
56 assert_not_nil t
57 assert_equal 11, t.activity_id
57 assert_equal 11, t.activity_id
58 assert_equal 7.3, t.hours
58 assert_equal 7.3, t.hours
59 assert_equal 3, t.user_id
59 assert_equal 3, t.user_id
60 assert_equal i, t.issue
60 assert_equal i, t.issue
61 assert_equal i.project, t.project
61 assert_equal i.project, t.project
62 end
62 end
63
63
64 def test_update
64 def test_update
65 entry = TimeEntry.find(1)
65 entry = TimeEntry.find(1)
66 assert_equal 1, entry.issue_id
66 assert_equal 1, entry.issue_id
67 assert_equal 2, entry.user_id
67 assert_equal 2, entry.user_id
68
68
69 @request.session[:user_id] = 1
69 @request.session[:user_id] = 1
70 post :edit, :id => 1,
70 post :edit, :id => 1,
71 :time_entry => {:issue_id => '2',
71 :time_entry => {:issue_id => '2',
72 :hours => '8'}
72 :hours => '8'}
73 assert_redirected_to 'projects/ecookbook/timelog/details'
73 assert_redirected_to 'projects/ecookbook/timelog/details'
74 entry.reload
74 entry.reload
75
75
76 assert_equal 8, entry.hours
76 assert_equal 8, entry.hours
77 assert_equal 2, entry.issue_id
77 assert_equal 2, entry.issue_id
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'
85 assert_nil TimeEntry.find_by_id(1)
85 assert_nil TimeEntry.find_by_id(1)
86 end
86 end
87
87
88 def test_report_no_criteria
88 def test_report_no_criteria
89 get :report, :project_id => 1
89 get :report, :project_id => 1
90 assert_response :success
90 assert_response :success
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
97 assert_template 'report'
120 assert_template 'report'
98 assert_not_nil assigns(:total_hours)
121 assert_not_nil assigns(:total_hours)
99 assert_equal "162.90", "%.2f" % assigns(:total_hours)
122 assert_equal "162.90", "%.2f" % assigns(:total_hours)
100 end
123 end
101
124
102 def test_report_all_time_by_day
125 def test_report_all_time_by_day
103 get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
126 get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
104 assert_response :success
127 assert_response :success
105 assert_template 'report'
128 assert_template 'report'
106 assert_not_nil assigns(:total_hours)
129 assert_not_nil assigns(:total_hours)
107 assert_equal "162.90", "%.2f" % assigns(:total_hours)
130 assert_equal "162.90", "%.2f" % assigns(:total_hours)
108 assert_tag :tag => 'th', :content => '2007-03-12'
131 assert_tag :tag => 'th', :content => '2007-03-12'
109 end
132 end
110
133
111 def test_report_one_criteria
134 def test_report_one_criteria
112 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
135 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
113 assert_response :success
136 assert_response :success
114 assert_template 'report'
137 assert_template 'report'
115 assert_not_nil assigns(:total_hours)
138 assert_not_nil assigns(:total_hours)
116 assert_equal "8.65", "%.2f" % assigns(:total_hours)
139 assert_equal "8.65", "%.2f" % assigns(:total_hours)
117 end
140 end
118
141
119 def test_report_two_criterias
142 def test_report_two_criterias
120 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
143 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
121 assert_response :success
144 assert_response :success
122 assert_template 'report'
145 assert_template 'report'
123 assert_not_nil assigns(:total_hours)
146 assert_not_nil assigns(:total_hours)
124 assert_equal "162.90", "%.2f" % assigns(:total_hours)
147 assert_equal "162.90", "%.2f" % assigns(:total_hours)
125 end
148 end
126
149
127 def test_report_custom_field_criteria
150 def test_report_custom_field_criteria
128 get :report, :project_id => 1, :criterias => ['project', 'cf_1']
151 get :report, :project_id => 1, :criterias => ['project', 'cf_1']
129 assert_response :success
152 assert_response :success
130 assert_template 'report'
153 assert_template 'report'
131 assert_not_nil assigns(:total_hours)
154 assert_not_nil assigns(:total_hours)
132 assert_not_nil assigns(:criterias)
155 assert_not_nil assigns(:criterias)
133 assert_equal 2, assigns(:criterias).size
156 assert_equal 2, assigns(:criterias).size
134 assert_equal "162.90", "%.2f" % assigns(:total_hours)
157 assert_equal "162.90", "%.2f" % assigns(:total_hours)
135 # Custom field column
158 # Custom field column
136 assert_tag :tag => 'th', :content => 'Database'
159 assert_tag :tag => 'th', :content => 'Database'
137 # Custom field row
160 # Custom field row
138 assert_tag :tag => 'td', :content => 'MySQL',
161 assert_tag :tag => 'td', :content => 'MySQL',
139 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
162 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
140 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
163 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
141 :content => '1' }}
164 :content => '1' }}
142 end
165 end
143
166
144 def test_report_one_criteria_no_result
167 def test_report_one_criteria_no_result
145 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
168 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
146 assert_response :success
169 assert_response :success
147 assert_template 'report'
170 assert_template 'report'
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
155 assert_equal 'text/csv', @response.content_type
189 assert_equal 'text/csv', @response.content_type
156 lines = @response.body.chomp.split("\n")
190 lines = @response.body.chomp.split("\n")
157 # Headers
191 # Headers
158 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
192 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
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
165 assert_response :success
207 assert_response :success
166 assert_template 'details'
208 assert_template 'details'
167 assert_not_nil assigns(:entries)
209 assert_not_nil assigns(:entries)
168 assert_equal 4, assigns(:entries).size
210 assert_equal 4, assigns(:entries).size
169 # project and subproject
211 # project and subproject
170 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
212 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
171 assert_not_nil assigns(:total_hours)
213 assert_not_nil assigns(:total_hours)
172 assert_equal "162.90", "%.2f" % assigns(:total_hours)
214 assert_equal "162.90", "%.2f" % assigns(:total_hours)
173 # display all time by default
215 # display all time by default
174 assert_equal '2007-03-11'.to_date, assigns(:from)
216 assert_equal '2007-03-11'.to_date, assigns(:from)
175 assert_equal '2007-04-22'.to_date, assigns(:to)
217 assert_equal '2007-04-22'.to_date, assigns(:to)
176 end
218 end
177
219
178 def test_details_at_project_level_with_date_range
220 def test_details_at_project_level_with_date_range
179 get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
221 get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
180 assert_response :success
222 assert_response :success
181 assert_template 'details'
223 assert_template 'details'
182 assert_not_nil assigns(:entries)
224 assert_not_nil assigns(:entries)
183 assert_equal 3, assigns(:entries).size
225 assert_equal 3, assigns(:entries).size
184 assert_not_nil assigns(:total_hours)
226 assert_not_nil assigns(:total_hours)
185 assert_equal "12.90", "%.2f" % assigns(:total_hours)
227 assert_equal "12.90", "%.2f" % assigns(:total_hours)
186 assert_equal '2007-03-20'.to_date, assigns(:from)
228 assert_equal '2007-03-20'.to_date, assigns(:from)
187 assert_equal '2007-04-30'.to_date, assigns(:to)
229 assert_equal '2007-04-30'.to_date, assigns(:to)
188 end
230 end
189
231
190 def test_details_at_project_level_with_period
232 def test_details_at_project_level_with_period
191 get :details, :project_id => 1, :period => '7_days'
233 get :details, :project_id => 1, :period => '7_days'
192 assert_response :success
234 assert_response :success
193 assert_template 'details'
235 assert_template 'details'
194 assert_not_nil assigns(:entries)
236 assert_not_nil assigns(:entries)
195 assert_not_nil assigns(:total_hours)
237 assert_not_nil assigns(:total_hours)
196 assert_equal Date.today - 7, assigns(:from)
238 assert_equal Date.today - 7, assigns(:from)
197 assert_equal Date.today, assigns(:to)
239 assert_equal Date.today, assigns(:to)
198 end
240 end
199
241
200 def test_details_at_issue_level
242 def test_details_at_issue_level
201 get :details, :issue_id => 1
243 get :details, :issue_id => 1
202 assert_response :success
244 assert_response :success
203 assert_template 'details'
245 assert_template 'details'
204 assert_not_nil assigns(:entries)
246 assert_not_nil assigns(:entries)
205 assert_equal 2, assigns(:entries).size
247 assert_equal 2, assigns(:entries).size
206 assert_not_nil assigns(:total_hours)
248 assert_not_nil assigns(:total_hours)
207 assert_equal 154.25, assigns(:total_hours)
249 assert_equal 154.25, assigns(:total_hours)
208 # display all time by default
250 # display all time by default
209 assert_equal '2007-03-11'.to_date, assigns(:from)
251 assert_equal '2007-03-11'.to_date, assigns(:from)
210 assert_equal '2007-04-22'.to_date, assigns(:to)
252 assert_equal '2007-04-22'.to_date, assigns(:to)
211 end
253 end
212
254
213 def test_details_atom_feed
255 def test_details_atom_feed
214 get :details, :project_id => 1, :format => 'atom'
256 get :details, :project_id => 1, :format => 'atom'
215 assert_response :success
257 assert_response :success
216 assert_equal 'application/atom+xml', @response.content_type
258 assert_equal 'application/atom+xml', @response.content_type
217 assert_not_nil assigns(:items)
259 assert_not_nil assigns(:items)
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
224 assert_equal 'text/csv', @response.content_type
274 assert_equal 'text/csv', @response.content_type
225 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
275 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
226 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
276 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
227 end
277 end
228 end
278 end
General Comments 0
You need to be logged in to leave comments. Login now