##// END OF EJS Templates
Fixed: Pre-filled time tracking date ignores timezone (#4160)....
Jean-Philippe Lang -
r2898:6245f49934d2
parent child
Show More
@@ -1,308 +1,308
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, :only => [:edit, :destroy]
20 before_filter :find_project, :authorize, :only => [:edit, :destroy]
21 before_filter :find_optional_project, :only => [:report, :details]
21 before_filter :find_optional_project, :only => [:report, :details]
22
22
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
24
24
25 helper :sort
25 helper :sort
26 include SortHelper
26 include SortHelper
27 helper :issues
27 helper :issues
28 include TimelogHelper
28 include TimelogHelper
29 helper :custom_fields
29 helper :custom_fields
30 include CustomFieldsHelper
30 include CustomFieldsHelper
31
31
32 def report
32 def report
33 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
33 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
34 :klass => Project,
34 :klass => Project,
35 :label => :label_project},
35 :label => :label_project},
36 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
36 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
37 :klass => Version,
37 :klass => Version,
38 :label => :label_version},
38 :label => :label_version},
39 'category' => {:sql => "#{Issue.table_name}.category_id",
39 'category' => {:sql => "#{Issue.table_name}.category_id",
40 :klass => IssueCategory,
40 :klass => IssueCategory,
41 :label => :field_category},
41 :label => :field_category},
42 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
42 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
43 :klass => User,
43 :klass => User,
44 :label => :label_member},
44 :label => :label_member},
45 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
45 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
46 :klass => Tracker,
46 :klass => Tracker,
47 :label => :label_tracker},
47 :label => :label_tracker},
48 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
48 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
49 :klass => TimeEntryActivity,
49 :klass => TimeEntryActivity,
50 :label => :label_activity},
50 :label => :label_activity},
51 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
51 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
52 :klass => Issue,
52 :klass => Issue,
53 :label => :label_issue}
53 :label => :label_issue}
54 }
54 }
55
55
56 # Add list and boolean custom fields as available criterias
56 # Add list and boolean custom fields as available criterias
57 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
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|
58 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
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)",
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)",
60 :format => cf.field_format,
60 :format => cf.field_format,
61 :label => cf.name}
61 :label => cf.name}
62 end if @project
62 end if @project
63
63
64 # Add list and boolean time entry custom fields
64 # Add list and boolean time entry custom fields
65 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|
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)",
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)",
67 :format => cf.field_format,
67 :format => cf.field_format,
68 :label => cf.name}
68 :label => cf.name}
69 end
69 end
70
70
71 # Add list and boolean time entry activity custom fields
71 # Add list and boolean time entry activity custom fields
72 TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
72 TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
73 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
73 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
74 :format => cf.field_format,
74 :format => cf.field_format,
75 :label => cf.name}
75 :label => cf.name}
76 end
76 end
77
77
78 @criterias = params[:criterias] || []
78 @criterias = params[:criterias] || []
79 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
79 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
80 @criterias.uniq!
80 @criterias.uniq!
81 @criterias = @criterias[0,3]
81 @criterias = @criterias[0,3]
82
82
83 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
83 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
84
84
85 retrieve_date_range
85 retrieve_date_range
86
86
87 unless @criterias.empty?
87 unless @criterias.empty?
88 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
88 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
89 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
89 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
90 sql_condition = ''
90 sql_condition = ''
91
91
92 if @project.nil?
92 if @project.nil?
93 sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
93 sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
94 elsif @issue.nil?
94 elsif @issue.nil?
95 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
95 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
96 else
96 else
97 sql_condition = "#{TimeEntry.table_name}.issue_id = #{@issue.id}"
97 sql_condition = "#{TimeEntry.table_name}.issue_id = #{@issue.id}"
98 end
98 end
99
99
100 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
100 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
101 sql << " FROM #{TimeEntry.table_name}"
101 sql << " FROM #{TimeEntry.table_name}"
102 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
102 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
103 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
103 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
104 sql << " WHERE"
104 sql << " WHERE"
105 sql << " (%s) AND" % sql_condition
105 sql << " (%s) AND" % sql_condition
106 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
106 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
107 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
107 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
108
108
109 @hours = ActiveRecord::Base.connection.select_all(sql)
109 @hours = ActiveRecord::Base.connection.select_all(sql)
110
110
111 @hours.each do |row|
111 @hours.each do |row|
112 case @columns
112 case @columns
113 when 'year'
113 when 'year'
114 row['year'] = row['tyear']
114 row['year'] = row['tyear']
115 when 'month'
115 when 'month'
116 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
116 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
117 when 'week'
117 when 'week'
118 row['week'] = "#{row['tyear']}-#{row['tweek']}"
118 row['week'] = "#{row['tyear']}-#{row['tweek']}"
119 when 'day'
119 when 'day'
120 row['day'] = "#{row['spent_on']}"
120 row['day'] = "#{row['spent_on']}"
121 end
121 end
122 end
122 end
123
123
124 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
124 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
125
125
126 @periods = []
126 @periods = []
127 # Date#at_beginning_of_ not supported in Rails 1.2.x
127 # Date#at_beginning_of_ not supported in Rails 1.2.x
128 date_from = @from.to_time
128 date_from = @from.to_time
129 # 100 columns max
129 # 100 columns max
130 while date_from <= @to.to_time && @periods.length < 100
130 while date_from <= @to.to_time && @periods.length < 100
131 case @columns
131 case @columns
132 when 'year'
132 when 'year'
133 @periods << "#{date_from.year}"
133 @periods << "#{date_from.year}"
134 date_from = (date_from + 1.year).at_beginning_of_year
134 date_from = (date_from + 1.year).at_beginning_of_year
135 when 'month'
135 when 'month'
136 @periods << "#{date_from.year}-#{date_from.month}"
136 @periods << "#{date_from.year}-#{date_from.month}"
137 date_from = (date_from + 1.month).at_beginning_of_month
137 date_from = (date_from + 1.month).at_beginning_of_month
138 when 'week'
138 when 'week'
139 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
139 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
140 date_from = (date_from + 7.day).at_beginning_of_week
140 date_from = (date_from + 7.day).at_beginning_of_week
141 when 'day'
141 when 'day'
142 @periods << "#{date_from.to_date}"
142 @periods << "#{date_from.to_date}"
143 date_from = date_from + 1.day
143 date_from = date_from + 1.day
144 end
144 end
145 end
145 end
146 end
146 end
147
147
148 respond_to do |format|
148 respond_to do |format|
149 format.html { render :layout => !request.xhr? }
149 format.html { render :layout => !request.xhr? }
150 format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
150 format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
151 end
151 end
152 end
152 end
153
153
154 def details
154 def details
155 sort_init 'spent_on', 'desc'
155 sort_init 'spent_on', 'desc'
156 sort_update 'spent_on' => 'spent_on',
156 sort_update 'spent_on' => 'spent_on',
157 'user' => 'user_id',
157 'user' => 'user_id',
158 'activity' => 'activity_id',
158 'activity' => 'activity_id',
159 'project' => "#{Project.table_name}.name",
159 'project' => "#{Project.table_name}.name",
160 'issue' => 'issue_id',
160 'issue' => 'issue_id',
161 'hours' => 'hours'
161 'hours' => 'hours'
162
162
163 cond = ARCondition.new
163 cond = ARCondition.new
164 if @project.nil?
164 if @project.nil?
165 cond << Project.allowed_to_condition(User.current, :view_time_entries)
165 cond << Project.allowed_to_condition(User.current, :view_time_entries)
166 elsif @issue.nil?
166 elsif @issue.nil?
167 cond << @project.project_condition(Setting.display_subprojects_issues?)
167 cond << @project.project_condition(Setting.display_subprojects_issues?)
168 else
168 else
169 cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
169 cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
170 end
170 end
171
171
172 retrieve_date_range
172 retrieve_date_range
173 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
173 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
174
174
175 TimeEntry.visible_by(User.current) do
175 TimeEntry.visible_by(User.current) do
176 respond_to do |format|
176 respond_to do |format|
177 format.html {
177 format.html {
178 # Paginate results
178 # Paginate results
179 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
179 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
180 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
180 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
181 @entries = TimeEntry.find(:all,
181 @entries = TimeEntry.find(:all,
182 :include => [:project, :activity, :user, {:issue => :tracker}],
182 :include => [:project, :activity, :user, {:issue => :tracker}],
183 :conditions => cond.conditions,
183 :conditions => cond.conditions,
184 :order => sort_clause,
184 :order => sort_clause,
185 :limit => @entry_pages.items_per_page,
185 :limit => @entry_pages.items_per_page,
186 :offset => @entry_pages.current.offset)
186 :offset => @entry_pages.current.offset)
187 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
187 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
188
188
189 render :layout => !request.xhr?
189 render :layout => !request.xhr?
190 }
190 }
191 format.atom {
191 format.atom {
192 entries = TimeEntry.find(:all,
192 entries = TimeEntry.find(:all,
193 :include => [:project, :activity, :user, {:issue => :tracker}],
193 :include => [:project, :activity, :user, {:issue => :tracker}],
194 :conditions => cond.conditions,
194 :conditions => cond.conditions,
195 :order => "#{TimeEntry.table_name}.created_on DESC",
195 :order => "#{TimeEntry.table_name}.created_on DESC",
196 :limit => Setting.feeds_limit.to_i)
196 :limit => Setting.feeds_limit.to_i)
197 render_feed(entries, :title => l(:label_spent_time))
197 render_feed(entries, :title => l(:label_spent_time))
198 }
198 }
199 format.csv {
199 format.csv {
200 # Export all entries
200 # Export all entries
201 @entries = TimeEntry.find(:all,
201 @entries = TimeEntry.find(:all,
202 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
202 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
203 :conditions => cond.conditions,
203 :conditions => cond.conditions,
204 :order => sort_clause)
204 :order => sort_clause)
205 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
205 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
206 }
206 }
207 end
207 end
208 end
208 end
209 end
209 end
210
210
211 def edit
211 def edit
212 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
212 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
213 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
213 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
214 @time_entry.attributes = params[:time_entry]
214 @time_entry.attributes = params[:time_entry]
215
215
216 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
216 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
217
217
218 if request.post? and @time_entry.save
218 if request.post? and @time_entry.save
219 flash[:notice] = l(:notice_successful_update)
219 flash[:notice] = l(:notice_successful_update)
220 redirect_back_or_default :action => 'details', :project_id => @time_entry.project
220 redirect_back_or_default :action => 'details', :project_id => @time_entry.project
221 return
221 return
222 end
222 end
223 end
223 end
224
224
225 def destroy
225 def destroy
226 render_404 and return unless @time_entry
226 render_404 and return unless @time_entry
227 render_403 and return unless @time_entry.editable_by?(User.current)
227 render_403 and return unless @time_entry.editable_by?(User.current)
228 @time_entry.destroy
228 @time_entry.destroy
229 flash[:notice] = l(:notice_successful_delete)
229 flash[:notice] = l(:notice_successful_delete)
230 redirect_to :back
230 redirect_to :back
231 rescue ::ActionController::RedirectBackError
231 rescue ::ActionController::RedirectBackError
232 redirect_to :action => 'details', :project_id => @time_entry.project
232 redirect_to :action => 'details', :project_id => @time_entry.project
233 end
233 end
234
234
235 private
235 private
236 def find_project
236 def find_project
237 if params[:id]
237 if params[:id]
238 @time_entry = TimeEntry.find(params[:id])
238 @time_entry = TimeEntry.find(params[:id])
239 @project = @time_entry.project
239 @project = @time_entry.project
240 elsif params[:issue_id]
240 elsif params[:issue_id]
241 @issue = Issue.find(params[:issue_id])
241 @issue = Issue.find(params[:issue_id])
242 @project = @issue.project
242 @project = @issue.project
243 elsif params[:project_id]
243 elsif params[:project_id]
244 @project = Project.find(params[:project_id])
244 @project = Project.find(params[:project_id])
245 else
245 else
246 render_404
246 render_404
247 return false
247 return false
248 end
248 end
249 rescue ActiveRecord::RecordNotFound
249 rescue ActiveRecord::RecordNotFound
250 render_404
250 render_404
251 end
251 end
252
252
253 def find_optional_project
253 def find_optional_project
254 if !params[:issue_id].blank?
254 if !params[:issue_id].blank?
255 @issue = Issue.find(params[:issue_id])
255 @issue = Issue.find(params[:issue_id])
256 @project = @issue.project
256 @project = @issue.project
257 elsif !params[:project_id].blank?
257 elsif !params[:project_id].blank?
258 @project = Project.find(params[:project_id])
258 @project = Project.find(params[:project_id])
259 end
259 end
260 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
260 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
261 end
261 end
262
262
263 # Retrieves the date range based on predefined ranges or specific from/to param dates
263 # Retrieves the date range based on predefined ranges or specific from/to param dates
264 def retrieve_date_range
264 def retrieve_date_range
265 @free_period = false
265 @free_period = false
266 @from, @to = nil, nil
266 @from, @to = nil, nil
267
267
268 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
268 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
269 case params[:period].to_s
269 case params[:period].to_s
270 when 'today'
270 when 'today'
271 @from = @to = Date.today
271 @from = @to = Date.today
272 when 'yesterday'
272 when 'yesterday'
273 @from = @to = Date.today - 1
273 @from = @to = Date.today - 1
274 when 'current_week'
274 when 'current_week'
275 @from = Date.today - (Date.today.cwday - 1)%7
275 @from = Date.today - (Date.today.cwday - 1)%7
276 @to = @from + 6
276 @to = @from + 6
277 when 'last_week'
277 when 'last_week'
278 @from = Date.today - 7 - (Date.today.cwday - 1)%7
278 @from = Date.today - 7 - (Date.today.cwday - 1)%7
279 @to = @from + 6
279 @to = @from + 6
280 when '7_days'
280 when '7_days'
281 @from = Date.today - 7
281 @from = Date.today - 7
282 @to = Date.today
282 @to = Date.today
283 when 'current_month'
283 when 'current_month'
284 @from = Date.civil(Date.today.year, Date.today.month, 1)
284 @from = Date.civil(Date.today.year, Date.today.month, 1)
285 @to = (@from >> 1) - 1
285 @to = (@from >> 1) - 1
286 when 'last_month'
286 when 'last_month'
287 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
287 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
288 @to = (@from >> 1) - 1
288 @to = (@from >> 1) - 1
289 when '30_days'
289 when '30_days'
290 @from = Date.today - 30
290 @from = Date.today - 30
291 @to = Date.today
291 @to = Date.today
292 when 'current_year'
292 when 'current_year'
293 @from = Date.civil(Date.today.year, 1, 1)
293 @from = Date.civil(Date.today.year, 1, 1)
294 @to = Date.civil(Date.today.year, 12, 31)
294 @to = Date.civil(Date.today.year, 12, 31)
295 end
295 end
296 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
296 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
297 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
297 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
298 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
298 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
299 @free_period = true
299 @free_period = true
300 else
300 else
301 # default
301 # default
302 end
302 end
303
303
304 @from, @to = @to, @from if @from && @to && @from > @to
304 @from, @to = @to, @from if @from && @to && @from > @to
305 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
305 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
306 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
306 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
307 end
307 end
308 end
308 end
@@ -1,344 +1,353
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 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 < Principal
20 class User < Principal
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_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
36 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
37 :after_remove => Proc.new {|user, group| group.user_removed(user)}
37 :after_remove => Proc.new {|user, group| group.user_removed(user)}
38 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
38 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
39 has_many :changesets, :dependent => :nullify
39 has_many :changesets, :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 # Active non-anonymous users scope
44 # Active non-anonymous users scope
45 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
45 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
46
46
47 acts_as_customizable
47 acts_as_customizable
48
48
49 attr_accessor :password, :password_confirmation
49 attr_accessor :password, :password_confirmation
50 attr_accessor :last_before_login_on
50 attr_accessor :last_before_login_on
51 # Prevents unauthorized assignments
51 # Prevents unauthorized assignments
52 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
52 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
53
53
54 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
54 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
55 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
55 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
56 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
56 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
57 # Login must contain lettres, numbers, underscores only
57 # Login must contain lettres, numbers, underscores only
58 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
58 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
59 validates_length_of :login, :maximum => 30
59 validates_length_of :login, :maximum => 30
60 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
60 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
61 validates_length_of :firstname, :lastname, :maximum => 30
61 validates_length_of :firstname, :lastname, :maximum => 30
62 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
62 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
63 validates_length_of :mail, :maximum => 60, :allow_nil => true
63 validates_length_of :mail, :maximum => 60, :allow_nil => true
64 validates_confirmation_of :password, :allow_nil => true
64 validates_confirmation_of :password, :allow_nil => true
65
65
66 def before_create
66 def before_create
67 self.mail_notification = false
67 self.mail_notification = false
68 true
68 true
69 end
69 end
70
70
71 def before_save
71 def before_save
72 # update hashed_password if password was set
72 # update hashed_password if password was set
73 self.hashed_password = User.hash_password(self.password) if self.password
73 self.hashed_password = User.hash_password(self.password) if self.password
74 end
74 end
75
75
76 def reload(*args)
76 def reload(*args)
77 @name = nil
77 @name = nil
78 super
78 super
79 end
79 end
80
80
81 def identity_url=(url)
81 def identity_url=(url)
82 if url.blank?
82 if url.blank?
83 write_attribute(:identity_url, '')
83 write_attribute(:identity_url, '')
84 else
84 else
85 begin
85 begin
86 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
86 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
87 rescue OpenIdAuthentication::InvalidOpenId
87 rescue OpenIdAuthentication::InvalidOpenId
88 # Invlaid url, don't save
88 # Invlaid url, don't save
89 end
89 end
90 end
90 end
91 self.read_attribute(:identity_url)
91 self.read_attribute(:identity_url)
92 end
92 end
93
93
94 # Returns the user that matches provided login and password, or nil
94 # Returns the user that matches provided login and password, or nil
95 def self.try_to_login(login, password)
95 def self.try_to_login(login, password)
96 # Make sure no one can sign in with an empty password
96 # Make sure no one can sign in with an empty password
97 return nil if password.to_s.empty?
97 return nil if password.to_s.empty?
98 user = find(:first, :conditions => ["login=?", login])
98 user = find(:first, :conditions => ["login=?", login])
99 if user
99 if user
100 # user is already in local database
100 # user is already in local database
101 return nil if !user.active?
101 return nil if !user.active?
102 if user.auth_source
102 if user.auth_source
103 # user has an external authentication method
103 # user has an external authentication method
104 return nil unless user.auth_source.authenticate(login, password)
104 return nil unless user.auth_source.authenticate(login, password)
105 else
105 else
106 # authentication with local password
106 # authentication with local password
107 return nil unless User.hash_password(password) == user.hashed_password
107 return nil unless User.hash_password(password) == user.hashed_password
108 end
108 end
109 else
109 else
110 # user is not yet registered, try to authenticate with available sources
110 # user is not yet registered, try to authenticate with available sources
111 attrs = AuthSource.authenticate(login, password)
111 attrs = AuthSource.authenticate(login, password)
112 if attrs
112 if attrs
113 user = new(*attrs)
113 user = new(*attrs)
114 user.login = login
114 user.login = login
115 user.language = Setting.default_language
115 user.language = Setting.default_language
116 if user.save
116 if user.save
117 user.reload
117 user.reload
118 logger.info("User '#{user.login}' created from the LDAP") if logger
118 logger.info("User '#{user.login}' created from the LDAP") if logger
119 end
119 end
120 end
120 end
121 end
121 end
122 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
122 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
123 user
123 user
124 rescue => text
124 rescue => text
125 raise text
125 raise text
126 end
126 end
127
127
128 # Returns the user who matches the given autologin +key+ or nil
128 # Returns the user who matches the given autologin +key+ or nil
129 def self.try_to_autologin(key)
129 def self.try_to_autologin(key)
130 tokens = Token.find_all_by_action_and_value('autologin', key)
130 tokens = Token.find_all_by_action_and_value('autologin', key)
131 # Make sure there's only 1 token that matches the key
131 # Make sure there's only 1 token that matches the key
132 if tokens.size == 1
132 if tokens.size == 1
133 token = tokens.first
133 token = tokens.first
134 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
134 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
135 token.user.update_attribute(:last_login_on, Time.now)
135 token.user.update_attribute(:last_login_on, Time.now)
136 token.user
136 token.user
137 end
137 end
138 end
138 end
139 end
139 end
140
140
141 # Return user's full name for display
141 # Return user's full name for display
142 def name(formatter = nil)
142 def name(formatter = nil)
143 if formatter
143 if formatter
144 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
144 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
145 else
145 else
146 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
146 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
147 end
147 end
148 end
148 end
149
149
150 def active?
150 def active?
151 self.status == STATUS_ACTIVE
151 self.status == STATUS_ACTIVE
152 end
152 end
153
153
154 def registered?
154 def registered?
155 self.status == STATUS_REGISTERED
155 self.status == STATUS_REGISTERED
156 end
156 end
157
157
158 def locked?
158 def locked?
159 self.status == STATUS_LOCKED
159 self.status == STATUS_LOCKED
160 end
160 end
161
161
162 def check_password?(clear_password)
162 def check_password?(clear_password)
163 User.hash_password(clear_password) == self.hashed_password
163 User.hash_password(clear_password) == self.hashed_password
164 end
164 end
165
165
166 # Generate and set a random password. Useful for automated user creation
166 # Generate and set a random password. Useful for automated user creation
167 # Based on Token#generate_token_value
167 # Based on Token#generate_token_value
168 #
168 #
169 def random_password
169 def random_password
170 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
170 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
171 password = ''
171 password = ''
172 40.times { |i| password << chars[rand(chars.size-1)] }
172 40.times { |i| password << chars[rand(chars.size-1)] }
173 self.password = password
173 self.password = password
174 self.password_confirmation = password
174 self.password_confirmation = password
175 self
175 self
176 end
176 end
177
177
178 def pref
178 def pref
179 self.preference ||= UserPreference.new(:user => self)
179 self.preference ||= UserPreference.new(:user => self)
180 end
180 end
181
181
182 def time_zone
182 def time_zone
183 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
183 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
184 end
184 end
185
185
186 def wants_comments_in_reverse_order?
186 def wants_comments_in_reverse_order?
187 self.pref[:comments_sorting] == 'desc'
187 self.pref[:comments_sorting] == 'desc'
188 end
188 end
189
189
190 # Return user's RSS key (a 40 chars long string), used to access feeds
190 # Return user's RSS key (a 40 chars long string), used to access feeds
191 def rss_key
191 def rss_key
192 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
192 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
193 token.value
193 token.value
194 end
194 end
195
195
196 # Return an array of project ids for which the user has explicitly turned mail notifications on
196 # Return an array of project ids for which the user has explicitly turned mail notifications on
197 def notified_projects_ids
197 def notified_projects_ids
198 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
198 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
199 end
199 end
200
200
201 def notified_project_ids=(ids)
201 def notified_project_ids=(ids)
202 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
202 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
203 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
203 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
204 @notified_projects_ids = nil
204 @notified_projects_ids = nil
205 notified_projects_ids
205 notified_projects_ids
206 end
206 end
207
207
208 def self.find_by_rss_key(key)
208 def self.find_by_rss_key(key)
209 token = Token.find_by_value(key)
209 token = Token.find_by_value(key)
210 token && token.user.active? ? token.user : nil
210 token && token.user.active? ? token.user : nil
211 end
211 end
212
212
213 # Makes find_by_mail case-insensitive
213 # Makes find_by_mail case-insensitive
214 def self.find_by_mail(mail)
214 def self.find_by_mail(mail)
215 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
215 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
216 end
216 end
217
217
218 # Sort users by their display names
218 # Sort users by their display names
219 def <=>(user)
219 def <=>(user)
220 self.to_s.downcase <=> user.to_s.downcase
220 self.to_s.downcase <=> user.to_s.downcase
221 end
221 end
222
222
223 def to_s
223 def to_s
224 name
224 name
225 end
225 end
226
226
227 # Returns the current day according to user's time zone
228 def today
229 if time_zone.nil?
230 Date.today
231 else
232 Time.now.in_time_zone(time_zone).to_date
233 end
234 end
235
227 def logged?
236 def logged?
228 true
237 true
229 end
238 end
230
239
231 def anonymous?
240 def anonymous?
232 !logged?
241 !logged?
233 end
242 end
234
243
235 # Return user's roles for project
244 # Return user's roles for project
236 def roles_for_project(project)
245 def roles_for_project(project)
237 roles = []
246 roles = []
238 # No role on archived projects
247 # No role on archived projects
239 return roles unless project && project.active?
248 return roles unless project && project.active?
240 if logged?
249 if logged?
241 # Find project membership
250 # Find project membership
242 membership = memberships.detect {|m| m.project_id == project.id}
251 membership = memberships.detect {|m| m.project_id == project.id}
243 if membership
252 if membership
244 roles = membership.roles
253 roles = membership.roles
245 else
254 else
246 @role_non_member ||= Role.non_member
255 @role_non_member ||= Role.non_member
247 roles << @role_non_member
256 roles << @role_non_member
248 end
257 end
249 else
258 else
250 @role_anonymous ||= Role.anonymous
259 @role_anonymous ||= Role.anonymous
251 roles << @role_anonymous
260 roles << @role_anonymous
252 end
261 end
253 roles
262 roles
254 end
263 end
255
264
256 # Return true if the user is a member of project
265 # Return true if the user is a member of project
257 def member_of?(project)
266 def member_of?(project)
258 !roles_for_project(project).detect {|role| role.member?}.nil?
267 !roles_for_project(project).detect {|role| role.member?}.nil?
259 end
268 end
260
269
261 # Return true if the user is allowed to do the specified action on project
270 # Return true if the user is allowed to do the specified action on project
262 # action can be:
271 # action can be:
263 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
272 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
264 # * a permission Symbol (eg. :edit_project)
273 # * a permission Symbol (eg. :edit_project)
265 def allowed_to?(action, project, options={})
274 def allowed_to?(action, project, options={})
266 if project
275 if project
267 # No action allowed on archived projects
276 # No action allowed on archived projects
268 return false unless project.active?
277 return false unless project.active?
269 # No action allowed on disabled modules
278 # No action allowed on disabled modules
270 return false unless project.allows_to?(action)
279 return false unless project.allows_to?(action)
271 # Admin users are authorized for anything else
280 # Admin users are authorized for anything else
272 return true if admin?
281 return true if admin?
273
282
274 roles = roles_for_project(project)
283 roles = roles_for_project(project)
275 return false unless roles
284 return false unless roles
276 roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
285 roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
277
286
278 elsif options[:global]
287 elsif options[:global]
279 # Admin users are always authorized
288 # Admin users are always authorized
280 return true if admin?
289 return true if admin?
281
290
282 # authorize if user has at least one role that has this permission
291 # authorize if user has at least one role that has this permission
283 roles = memberships.collect {|m| m.roles}.flatten.uniq
292 roles = memberships.collect {|m| m.roles}.flatten.uniq
284 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
293 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
285 else
294 else
286 false
295 false
287 end
296 end
288 end
297 end
289
298
290 def self.current=(user)
299 def self.current=(user)
291 @current_user = user
300 @current_user = user
292 end
301 end
293
302
294 def self.current
303 def self.current
295 @current_user ||= User.anonymous
304 @current_user ||= User.anonymous
296 end
305 end
297
306
298 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
307 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
299 # one anonymous user per database.
308 # one anonymous user per database.
300 def self.anonymous
309 def self.anonymous
301 anonymous_user = AnonymousUser.find(:first)
310 anonymous_user = AnonymousUser.find(:first)
302 if anonymous_user.nil?
311 if anonymous_user.nil?
303 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
312 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
304 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
313 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
305 end
314 end
306 anonymous_user
315 anonymous_user
307 end
316 end
308
317
309 protected
318 protected
310
319
311 def validate
320 def validate
312 # Password length validation based on setting
321 # Password length validation based on setting
313 if !password.nil? && password.size < Setting.password_min_length.to_i
322 if !password.nil? && password.size < Setting.password_min_length.to_i
314 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
323 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
315 end
324 end
316 end
325 end
317
326
318 private
327 private
319
328
320 # Return password digest
329 # Return password digest
321 def self.hash_password(clear_password)
330 def self.hash_password(clear_password)
322 Digest::SHA1.hexdigest(clear_password || "")
331 Digest::SHA1.hexdigest(clear_password || "")
323 end
332 end
324 end
333 end
325
334
326 class AnonymousUser < User
335 class AnonymousUser < User
327
336
328 def validate_on_create
337 def validate_on_create
329 # There should be only one AnonymousUser in the database
338 # There should be only one AnonymousUser in the database
330 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
339 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
331 end
340 end
332
341
333 def available_custom_fields
342 def available_custom_fields
334 []
343 []
335 end
344 end
336
345
337 # Overrides a few properties
346 # Overrides a few properties
338 def logged?; false end
347 def logged?; false end
339 def admin; false end
348 def admin; false end
340 def name; 'Anonymous' end
349 def name; 'Anonymous' end
341 def mail; nil end
350 def mail; nil end
342 def time_zone; nil end
351 def time_zone; nil end
343 def rss_key; nil end
352 def rss_key; nil end
344 end
353 end
General Comments 0
You need to be logged in to leave comments. Login now