##// END OF EJS Templates
Enable global time logging at /time_entries/new (#10020)....
Jean-Philippe Lang -
r8571:41eab6615b31
parent child
Show More
@@ -1,330 +1,337
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimelogController < ApplicationController
19 19 menu_item :issues
20 before_filter :find_project, :only => [:new, :create]
20
21 before_filter :find_project, :only => [:create]
21 22 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :authorize, :except => [:index, :report]
24 before_filter :find_optional_project, :only => [:index, :report]
24 before_filter :authorize, :except => [:new, :index, :report]
25
26 before_filter :find_optional_project, :only => [:new, :index, :report]
27 before_filter :authorize_global, :only => [:new, :index, :report]
28
25 29 accept_rss_auth :index
26 30 accept_api_auth :index, :show, :create, :update, :destroy
27 31
28 32 helper :sort
29 33 include SortHelper
30 34 helper :issues
31 35 include TimelogHelper
32 36 helper :custom_fields
33 37 include CustomFieldsHelper
34 38
35 39 def index
36 40 sort_init 'spent_on', 'desc'
37 41 sort_update 'spent_on' => 'spent_on',
38 42 'user' => 'user_id',
39 43 'activity' => 'activity_id',
40 44 'project' => "#{Project.table_name}.name",
41 45 'issue' => 'issue_id',
42 46 'hours' => 'hours'
43 47
44 48 retrieve_date_range
45 49
46 50 scope = TimeEntry.visible.spent_between(@from, @to)
47 51 if @issue
48 52 scope = scope.on_issue(@issue)
49 53 elsif @project
50 54 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
51 55 end
52 56
53 57 respond_to do |format|
54 58 format.html {
55 59 # Paginate results
56 60 @entry_count = scope.count
57 61 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
58 62 @entries = scope.all(
59 63 :include => [:project, :activity, :user, {:issue => :tracker}],
60 64 :order => sort_clause,
61 65 :limit => @entry_pages.items_per_page,
62 66 :offset => @entry_pages.current.offset
63 67 )
64 68 @total_hours = scope.sum(:hours).to_f
65 69
66 70 render :layout => !request.xhr?
67 71 }
68 72 format.api {
69 73 @entry_count = scope.count
70 74 @offset, @limit = api_offset_and_limit
71 75 @entries = scope.all(
72 76 :include => [:project, :activity, :user, {:issue => :tracker}],
73 77 :order => sort_clause,
74 78 :limit => @limit,
75 79 :offset => @offset
76 80 )
77 81 }
78 82 format.atom {
79 83 entries = scope.all(
80 84 :include => [:project, :activity, :user, {:issue => :tracker}],
81 85 :order => "#{TimeEntry.table_name}.created_on DESC",
82 86 :limit => Setting.feeds_limit.to_i
83 87 )
84 88 render_feed(entries, :title => l(:label_spent_time))
85 89 }
86 90 format.csv {
87 91 # Export all entries
88 92 @entries = scope.all(
89 93 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
90 94 :order => sort_clause
91 95 )
92 96 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
93 97 }
94 98 end
95 99 end
96 100
97 101 def report
98 102 retrieve_date_range
99 103 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
100 104
101 105 respond_to do |format|
102 106 format.html { render :layout => !request.xhr? }
103 107 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
104 108 end
105 109 end
106 110
107 111 def show
108 112 respond_to do |format|
109 113 # TODO: Implement html response
110 114 format.html { render :nothing => true, :status => 406 }
111 115 format.api
112 116 end
113 117 end
114 118
115 119 def new
116 120 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
117 121 @time_entry.attributes = params[:time_entry]
118 122 end
119 123
120 124 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
121 125 def create
122 126 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
123 127 @time_entry.attributes = params[:time_entry]
124 128
125 129 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
126 130
127 131 if @time_entry.save
128 132 respond_to do |format|
129 133 format.html {
130 134 flash[:notice] = l(:notice_successful_create)
131 135 if params[:continue]
136 if params[:project_id]
132 137 redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue
133 138 else
139 redirect_to :action => 'new'
140 end
141 else
134 142 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
135 143 end
136 144 }
137 145 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
138 146 end
139 147 else
140 148 respond_to do |format|
141 format.html { render :action => 'edit' }
149 format.html { render :action => 'new' }
142 150 format.api { render_validation_errors(@time_entry) }
143 151 end
144 152 end
145 153 end
146 154
147 155 def edit
148 156 @time_entry.attributes = params[:time_entry]
149 157 end
150 158
151 159 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
152 160 def update
153 161 @time_entry.attributes = params[:time_entry]
154 162
155 163 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
156 164
157 165 if @time_entry.save
158 166 respond_to do |format|
159 167 format.html {
160 168 flash[:notice] = l(:notice_successful_update)
161 169 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
162 170 }
163 171 format.api { head :ok }
164 172 end
165 173 else
166 174 respond_to do |format|
167 175 format.html { render :action => 'edit' }
168 176 format.api { render_validation_errors(@time_entry) }
169 177 end
170 178 end
171 179 end
172 180
173 181 def bulk_edit
174 182 @available_activities = TimeEntryActivity.shared.active
175 183 @custom_fields = TimeEntry.first.available_custom_fields
176 184 end
177 185
178 186 def bulk_update
179 187 attributes = parse_params_for_bulk_time_entry_attributes(params)
180 188
181 189 unsaved_time_entry_ids = []
182 190 @time_entries.each do |time_entry|
183 191 time_entry.reload
184 192 time_entry.attributes = attributes
185 193 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
186 194 unless time_entry.save
187 195 # Keep unsaved time_entry ids to display them in flash error
188 196 unsaved_time_entry_ids << time_entry.id
189 197 end
190 198 end
191 199 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
192 200 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
193 201 end
194 202
195 203 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
196 204 def destroy
197 205 @time_entries.each do |t|
198 206 begin
199 207 unless t.destroy && t.destroyed?
200 208 respond_to do |format|
201 209 format.html {
202 210 flash[:error] = l(:notice_unable_delete_time_entry)
203 211 redirect_to :back
204 212 }
205 213 format.api { render_validation_errors(t) }
206 214 end
207 215 return
208 216 end
209 217 rescue ::ActionController::RedirectBackError
210 218 redirect_to :action => 'index', :project_id => @projects.first
211 219 return
212 220 end
213 221 end
214 222
215 223 respond_to do |format|
216 224 format.html {
217 225 flash[:notice] = l(:notice_successful_delete)
218 226 redirect_back_or_default(:action => 'index', :project_id => @projects.first)
219 227 }
220 228 format.api { head :ok }
221 229 end
222 230 end
223 231
224 232 private
225 233 def find_time_entry
226 234 @time_entry = TimeEntry.find(params[:id])
227 235 unless @time_entry.editable_by?(User.current)
228 236 render_403
229 237 return false
230 238 end
231 239 @project = @time_entry.project
232 240 rescue ActiveRecord::RecordNotFound
233 241 render_404
234 242 end
235 243
236 244 def find_time_entries
237 245 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
238 246 raise ActiveRecord::RecordNotFound if @time_entries.empty?
239 247 @projects = @time_entries.collect(&:project).compact.uniq
240 248 @project = @projects.first if @projects.size == 1
241 249 rescue ActiveRecord::RecordNotFound
242 250 render_404
243 251 end
244 252
245 253 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
246 254 if unsaved_time_entry_ids.empty?
247 255 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
248 256 else
249 257 flash[:error] = l(:notice_failed_to_save_time_entries,
250 258 :count => unsaved_time_entry_ids.size,
251 259 :total => time_entries.size,
252 260 :ids => '#' + unsaved_time_entry_ids.join(', #'))
253 261 end
254 262 end
255 263
256 264 def find_project
257 265 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
258 266 @issue = Issue.find(issue_id)
259 267 @project = @issue.project
260 268 elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
261 269 @project = Project.find(project_id)
262 270 else
263 271 render_404
264 272 return false
265 273 end
266 274 rescue ActiveRecord::RecordNotFound
267 275 render_404
268 276 end
269 277
270 278 def find_optional_project
271 279 if !params[:issue_id].blank?
272 280 @issue = Issue.find(params[:issue_id])
273 281 @project = @issue.project
274 282 elsif !params[:project_id].blank?
275 283 @project = Project.find(params[:project_id])
276 284 end
277 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
278 285 end
279 286
280 287 # Retrieves the date range based on predefined ranges or specific from/to param dates
281 288 def retrieve_date_range
282 289 @free_period = false
283 290 @from, @to = nil, nil
284 291
285 292 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
286 293 case params[:period].to_s
287 294 when 'today'
288 295 @from = @to = Date.today
289 296 when 'yesterday'
290 297 @from = @to = Date.today - 1
291 298 when 'current_week'
292 299 @from = Date.today - (Date.today.cwday - 1)%7
293 300 @to = @from + 6
294 301 when 'last_week'
295 302 @from = Date.today - 7 - (Date.today.cwday - 1)%7
296 303 @to = @from + 6
297 304 when '7_days'
298 305 @from = Date.today - 7
299 306 @to = Date.today
300 307 when 'current_month'
301 308 @from = Date.civil(Date.today.year, Date.today.month, 1)
302 309 @to = (@from >> 1) - 1
303 310 when 'last_month'
304 311 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
305 312 @to = (@from >> 1) - 1
306 313 when '30_days'
307 314 @from = Date.today - 30
308 315 @to = Date.today
309 316 when 'current_year'
310 317 @from = Date.civil(Date.today.year, 1, 1)
311 318 @to = Date.civil(Date.today.year, 12, 31)
312 319 end
313 320 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
314 321 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
315 322 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
316 323 @free_period = true
317 324 else
318 325 # default
319 326 end
320 327
321 328 @from, @to = @to, @from if @from && @to && @from > @to
322 329 end
323 330
324 331 def parse_params_for_bulk_time_entry_attributes(params)
325 332 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
326 333 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
327 334 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
328 335 attributes
329 336 end
330 337 end
@@ -1,889 +1,900
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 # Project statuses
22 22 STATUS_ACTIVE = 1
23 23 STATUS_ARCHIVED = 9
24 24
25 25 # Maximum length for project identifiers
26 26 IDENTIFIER_MAX_LENGTH = 100
27 27
28 28 # Specific overidden Activities
29 29 has_many :time_entry_activities
30 30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 31 has_many :memberships, :class_name => 'Member'
32 32 has_many :member_principals, :class_name => 'Member',
33 33 :include => :principal,
34 34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 35 has_many :users, :through => :members
36 36 has_many :principals, :through => :member_principals, :source => :principal
37 37
38 38 has_many :enabled_modules, :dependent => :delete_all
39 39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 41 has_many :issue_changes, :through => :issues, :source => :journals
42 42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 43 has_many :time_entries, :dependent => :delete_all
44 44 has_many :queries, :dependent => :delete_all
45 45 has_many :documents, :dependent => :destroy
46 46 has_many :news, :dependent => :destroy, :include => :author
47 47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 49 has_one :repository, :conditions => ["is_default = ?", true]
50 50 has_many :repositories, :dependent => :destroy
51 51 has_many :changesets, :through => :repository
52 52 has_one :wiki, :dependent => :destroy
53 53 # Custom field for the project issues
54 54 has_and_belongs_to_many :issue_custom_fields,
55 55 :class_name => 'IssueCustomField',
56 56 :order => "#{CustomField.table_name}.position",
57 57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 58 :association_foreign_key => 'custom_field_id'
59 59
60 60 acts_as_nested_set :order => 'name', :dependent => :destroy
61 61 acts_as_attachable :view_permission => :view_files,
62 62 :delete_permission => :manage_files
63 63
64 64 acts_as_customizable
65 65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 68 :author => nil
69 69
70 70 attr_protected :status
71 71
72 72 validates_presence_of :name, :identifier
73 73 validates_uniqueness_of :identifier
74 74 validates_associated :repository, :wiki
75 75 validates_length_of :name, :maximum => 255
76 76 validates_length_of :homepage, :maximum => 255
77 77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 78 # donwcase letters, digits, dashes but not digits only
79 79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
80 80 # reserved words
81 81 validates_exclusion_of :identifier, :in => %w( new )
82 82
83 83 before_destroy :delete_all_members
84 84
85 85 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
86 86 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
87 87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
88 88 named_scope :all_public, { :conditions => { :is_public => true } }
89 89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
90 named_scope :allowed_to, lambda {|*args|
91 user = User.current
92 permission = nil
93 if args.first.is_a?(Symbol)
94 permission = args.shift
95 else
96 user = args.shift
97 permission = args.shift
98 end
99 { :conditions => Project.allowed_to_condition(user, permission, *args) }
100 }
90 101 named_scope :like, lambda {|arg|
91 102 if arg.blank?
92 103 {}
93 104 else
94 105 pattern = "%#{arg.to_s.strip.downcase}%"
95 106 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
96 107 end
97 108 }
98 109
99 110 def initialize(attributes=nil, *args)
100 111 super
101 112
102 113 initialized = (attributes || {}).stringify_keys
103 114 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
104 115 self.identifier = Project.next_identifier
105 116 end
106 117 if !initialized.key?('is_public')
107 118 self.is_public = Setting.default_projects_public?
108 119 end
109 120 if !initialized.key?('enabled_module_names')
110 121 self.enabled_module_names = Setting.default_projects_modules
111 122 end
112 123 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
113 124 self.trackers = Tracker.all
114 125 end
115 126 end
116 127
117 128 def identifier=(identifier)
118 129 super unless identifier_frozen?
119 130 end
120 131
121 132 def identifier_frozen?
122 133 errors[:identifier].nil? && !(new_record? || identifier.blank?)
123 134 end
124 135
125 136 # returns latest created projects
126 137 # non public projects will be returned only if user is a member of those
127 138 def self.latest(user=nil, count=5)
128 139 visible(user).find(:all, :limit => count, :order => "created_on DESC")
129 140 end
130 141
131 142 # Returns true if the project is visible to +user+ or to the current user.
132 143 def visible?(user=User.current)
133 144 user.allowed_to?(:view_project, self)
134 145 end
135 146
136 147 # Returns a SQL conditions string used to find all projects visible by the specified user.
137 148 #
138 149 # Examples:
139 150 # Project.visible_condition(admin) => "projects.status = 1"
140 151 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
141 152 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
142 153 def self.visible_condition(user, options={})
143 154 allowed_to_condition(user, :view_project, options)
144 155 end
145 156
146 157 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
147 158 #
148 159 # Valid options:
149 160 # * :project => limit the condition to project
150 161 # * :with_subprojects => limit the condition to project and its subprojects
151 162 # * :member => limit the condition to the user projects
152 163 def self.allowed_to_condition(user, permission, options={})
153 164 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
154 165 if perm = Redmine::AccessControl.permission(permission)
155 166 unless perm.project_module.nil?
156 167 # If the permission belongs to a project module, make sure the module is enabled
157 168 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
158 169 end
159 170 end
160 171 if options[:project]
161 172 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
162 173 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
163 174 base_statement = "(#{project_statement}) AND (#{base_statement})"
164 175 end
165 176
166 177 if user.admin?
167 178 base_statement
168 179 else
169 180 statement_by_role = {}
170 181 unless options[:member]
171 182 role = user.logged? ? Role.non_member : Role.anonymous
172 183 if role.allowed_to?(permission)
173 184 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
174 185 end
175 186 end
176 187 if user.logged?
177 188 user.projects_by_role.each do |role, projects|
178 189 if role.allowed_to?(permission)
179 190 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
180 191 end
181 192 end
182 193 end
183 194 if statement_by_role.empty?
184 195 "1=0"
185 196 else
186 197 if block_given?
187 198 statement_by_role.each do |role, statement|
188 199 if s = yield(role, user)
189 200 statement_by_role[role] = "(#{statement} AND (#{s}))"
190 201 end
191 202 end
192 203 end
193 204 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
194 205 end
195 206 end
196 207 end
197 208
198 209 # Returns the Systemwide and project specific activities
199 210 def activities(include_inactive=false)
200 211 if include_inactive
201 212 return all_activities
202 213 else
203 214 return active_activities
204 215 end
205 216 end
206 217
207 218 # Will create a new Project specific Activity or update an existing one
208 219 #
209 220 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
210 221 # does not successfully save.
211 222 def update_or_create_time_entry_activity(id, activity_hash)
212 223 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
213 224 self.create_time_entry_activity_if_needed(activity_hash)
214 225 else
215 226 activity = project.time_entry_activities.find_by_id(id.to_i)
216 227 activity.update_attributes(activity_hash) if activity
217 228 end
218 229 end
219 230
220 231 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
221 232 #
222 233 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
223 234 # does not successfully save.
224 235 def create_time_entry_activity_if_needed(activity)
225 236 if activity['parent_id']
226 237
227 238 parent_activity = TimeEntryActivity.find(activity['parent_id'])
228 239 activity['name'] = parent_activity.name
229 240 activity['position'] = parent_activity.position
230 241
231 242 if Enumeration.overridding_change?(activity, parent_activity)
232 243 project_activity = self.time_entry_activities.create(activity)
233 244
234 245 if project_activity.new_record?
235 246 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
236 247 else
237 248 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
238 249 end
239 250 end
240 251 end
241 252 end
242 253
243 254 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
244 255 #
245 256 # Examples:
246 257 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
247 258 # project.project_condition(false) => "projects.id = 1"
248 259 def project_condition(with_subprojects)
249 260 cond = "#{Project.table_name}.id = #{id}"
250 261 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
251 262 cond
252 263 end
253 264
254 265 def self.find(*args)
255 266 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
256 267 project = find_by_identifier(*args)
257 268 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
258 269 project
259 270 else
260 271 super
261 272 end
262 273 end
263 274
264 275 def to_param
265 276 # id is used for projects with a numeric identifier (compatibility)
266 277 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
267 278 end
268 279
269 280 def active?
270 281 self.status == STATUS_ACTIVE
271 282 end
272 283
273 284 def archived?
274 285 self.status == STATUS_ARCHIVED
275 286 end
276 287
277 288 # Archives the project and its descendants
278 289 def archive
279 290 # Check that there is no issue of a non descendant project that is assigned
280 291 # to one of the project or descendant versions
281 292 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
282 293 if v_ids.any? && Issue.find(:first, :include => :project,
283 294 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
284 295 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
285 296 return false
286 297 end
287 298 Project.transaction do
288 299 archive!
289 300 end
290 301 true
291 302 end
292 303
293 304 # Unarchives the project
294 305 # All its ancestors must be active
295 306 def unarchive
296 307 return false if ancestors.detect {|a| !a.active?}
297 308 update_attribute :status, STATUS_ACTIVE
298 309 end
299 310
300 311 # Returns an array of projects the project can be moved to
301 312 # by the current user
302 313 def allowed_parents
303 314 return @allowed_parents if @allowed_parents
304 315 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
305 316 @allowed_parents = @allowed_parents - self_and_descendants
306 317 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
307 318 @allowed_parents << nil
308 319 end
309 320 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
310 321 @allowed_parents << parent
311 322 end
312 323 @allowed_parents
313 324 end
314 325
315 326 # Sets the parent of the project with authorization check
316 327 def set_allowed_parent!(p)
317 328 unless p.nil? || p.is_a?(Project)
318 329 if p.to_s.blank?
319 330 p = nil
320 331 else
321 332 p = Project.find_by_id(p)
322 333 return false unless p
323 334 end
324 335 end
325 336 if p.nil?
326 337 if !new_record? && allowed_parents.empty?
327 338 return false
328 339 end
329 340 elsif !allowed_parents.include?(p)
330 341 return false
331 342 end
332 343 set_parent!(p)
333 344 end
334 345
335 346 # Sets the parent of the project
336 347 # Argument can be either a Project, a String, a Fixnum or nil
337 348 def set_parent!(p)
338 349 unless p.nil? || p.is_a?(Project)
339 350 if p.to_s.blank?
340 351 p = nil
341 352 else
342 353 p = Project.find_by_id(p)
343 354 return false unless p
344 355 end
345 356 end
346 357 if p == parent && !p.nil?
347 358 # Nothing to do
348 359 true
349 360 elsif p.nil? || (p.active? && move_possible?(p))
350 361 # Insert the project so that target's children or root projects stay alphabetically sorted
351 362 sibs = (p.nil? ? self.class.roots : p.children)
352 363 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
353 364 if to_be_inserted_before
354 365 move_to_left_of(to_be_inserted_before)
355 366 elsif p.nil?
356 367 if sibs.empty?
357 368 # move_to_root adds the project in first (ie. left) position
358 369 move_to_root
359 370 else
360 371 move_to_right_of(sibs.last) unless self == sibs.last
361 372 end
362 373 else
363 374 # move_to_child_of adds the project in last (ie.right) position
364 375 move_to_child_of(p)
365 376 end
366 377 Issue.update_versions_from_hierarchy_change(self)
367 378 true
368 379 else
369 380 # Can not move to the given target
370 381 false
371 382 end
372 383 end
373 384
374 385 # Returns an array of the trackers used by the project and its active sub projects
375 386 def rolled_up_trackers
376 387 @rolled_up_trackers ||=
377 388 Tracker.find(:all, :joins => :projects,
378 389 :select => "DISTINCT #{Tracker.table_name}.*",
379 390 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
380 391 :order => "#{Tracker.table_name}.position")
381 392 end
382 393
383 394 # Closes open and locked project versions that are completed
384 395 def close_completed_versions
385 396 Version.transaction do
386 397 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
387 398 if version.completed?
388 399 version.update_attribute(:status, 'closed')
389 400 end
390 401 end
391 402 end
392 403 end
393 404
394 405 # Returns a scope of the Versions on subprojects
395 406 def rolled_up_versions
396 407 @rolled_up_versions ||=
397 408 Version.scoped(:include => :project,
398 409 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
399 410 end
400 411
401 412 # Returns a scope of the Versions used by the project
402 413 def shared_versions
403 414 @shared_versions ||= begin
404 415 r = root? ? self : root
405 416 Version.scoped(:include => :project,
406 417 :conditions => "#{Project.table_name}.id = #{id}" +
407 418 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
408 419 " #{Version.table_name}.sharing = 'system'" +
409 420 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
410 421 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
411 422 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
412 423 "))")
413 424 end
414 425 end
415 426
416 427 # Returns a hash of project users grouped by role
417 428 def users_by_role
418 429 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
419 430 m.roles.each do |r|
420 431 h[r] ||= []
421 432 h[r] << m.user
422 433 end
423 434 h
424 435 end
425 436 end
426 437
427 438 # Deletes all project's members
428 439 def delete_all_members
429 440 me, mr = Member.table_name, MemberRole.table_name
430 441 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
431 442 Member.delete_all(['project_id = ?', id])
432 443 end
433 444
434 445 # Users/groups issues can be assigned to
435 446 def assignable_users
436 447 assignable = Setting.issue_group_assignment? ? member_principals : members
437 448 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
438 449 end
439 450
440 451 # Returns the mail adresses of users that should be always notified on project events
441 452 def recipients
442 453 notified_users.collect {|user| user.mail}
443 454 end
444 455
445 456 # Returns the users that should be notified on project events
446 457 def notified_users
447 458 # TODO: User part should be extracted to User#notify_about?
448 459 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
449 460 end
450 461
451 462 # Returns an array of all custom fields enabled for project issues
452 463 # (explictly associated custom fields and custom fields enabled for all projects)
453 464 def all_issue_custom_fields
454 465 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
455 466 end
456 467
457 468 # Returns an array of all custom fields enabled for project time entries
458 469 # (explictly associated custom fields and custom fields enabled for all projects)
459 470 def all_time_entry_custom_fields
460 471 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
461 472 end
462 473
463 474 def project
464 475 self
465 476 end
466 477
467 478 def <=>(project)
468 479 name.downcase <=> project.name.downcase
469 480 end
470 481
471 482 def to_s
472 483 name
473 484 end
474 485
475 486 # Returns a short description of the projects (first lines)
476 487 def short_description(length = 255)
477 488 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
478 489 end
479 490
480 491 def css_classes
481 492 s = 'project'
482 493 s << ' root' if root?
483 494 s << ' child' if child?
484 495 s << (leaf? ? ' leaf' : ' parent')
485 496 s
486 497 end
487 498
488 499 # The earliest start date of a project, based on it's issues and versions
489 500 def start_date
490 501 [
491 502 issues.minimum('start_date'),
492 503 shared_versions.collect(&:effective_date),
493 504 shared_versions.collect(&:start_date)
494 505 ].flatten.compact.min
495 506 end
496 507
497 508 # The latest due date of an issue or version
498 509 def due_date
499 510 [
500 511 issues.maximum('due_date'),
501 512 shared_versions.collect(&:effective_date),
502 513 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
503 514 ].flatten.compact.max
504 515 end
505 516
506 517 def overdue?
507 518 active? && !due_date.nil? && (due_date < Date.today)
508 519 end
509 520
510 521 # Returns the percent completed for this project, based on the
511 522 # progress on it's versions.
512 523 def completed_percent(options={:include_subprojects => false})
513 524 if options.delete(:include_subprojects)
514 525 total = self_and_descendants.collect(&:completed_percent).sum
515 526
516 527 total / self_and_descendants.count
517 528 else
518 529 if versions.count > 0
519 530 total = versions.collect(&:completed_pourcent).sum
520 531
521 532 total / versions.count
522 533 else
523 534 100
524 535 end
525 536 end
526 537 end
527 538
528 539 # Return true if this project is allowed to do the specified action.
529 540 # action can be:
530 541 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
531 542 # * a permission Symbol (eg. :edit_project)
532 543 def allows_to?(action)
533 544 if action.is_a? Hash
534 545 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
535 546 else
536 547 allowed_permissions.include? action
537 548 end
538 549 end
539 550
540 551 def module_enabled?(module_name)
541 552 module_name = module_name.to_s
542 553 enabled_modules.detect {|m| m.name == module_name}
543 554 end
544 555
545 556 def enabled_module_names=(module_names)
546 557 if module_names && module_names.is_a?(Array)
547 558 module_names = module_names.collect(&:to_s).reject(&:blank?)
548 559 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
549 560 else
550 561 enabled_modules.clear
551 562 end
552 563 end
553 564
554 565 # Returns an array of the enabled modules names
555 566 def enabled_module_names
556 567 enabled_modules.collect(&:name)
557 568 end
558 569
559 570 # Enable a specific module
560 571 #
561 572 # Examples:
562 573 # project.enable_module!(:issue_tracking)
563 574 # project.enable_module!("issue_tracking")
564 575 def enable_module!(name)
565 576 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
566 577 end
567 578
568 579 # Disable a module if it exists
569 580 #
570 581 # Examples:
571 582 # project.disable_module!(:issue_tracking)
572 583 # project.disable_module!("issue_tracking")
573 584 # project.disable_module!(project.enabled_modules.first)
574 585 def disable_module!(target)
575 586 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
576 587 target.destroy unless target.blank?
577 588 end
578 589
579 590 safe_attributes 'name',
580 591 'description',
581 592 'homepage',
582 593 'is_public',
583 594 'identifier',
584 595 'custom_field_values',
585 596 'custom_fields',
586 597 'tracker_ids',
587 598 'issue_custom_field_ids'
588 599
589 600 safe_attributes 'enabled_module_names',
590 601 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
591 602
592 603 # Returns an array of projects that are in this project's hierarchy
593 604 #
594 605 # Example: parents, children, siblings
595 606 def hierarchy
596 607 parents = project.self_and_ancestors || []
597 608 descendants = project.descendants || []
598 609 project_hierarchy = parents | descendants # Set union
599 610 end
600 611
601 612 # Returns an auto-generated project identifier based on the last identifier used
602 613 def self.next_identifier
603 614 p = Project.find(:first, :order => 'created_on DESC')
604 615 p.nil? ? nil : p.identifier.to_s.succ
605 616 end
606 617
607 618 # Copies and saves the Project instance based on the +project+.
608 619 # Duplicates the source project's:
609 620 # * Wiki
610 621 # * Versions
611 622 # * Categories
612 623 # * Issues
613 624 # * Members
614 625 # * Queries
615 626 #
616 627 # Accepts an +options+ argument to specify what to copy
617 628 #
618 629 # Examples:
619 630 # project.copy(1) # => copies everything
620 631 # project.copy(1, :only => 'members') # => copies members only
621 632 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
622 633 def copy(project, options={})
623 634 project = project.is_a?(Project) ? project : Project.find(project)
624 635
625 636 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
626 637 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
627 638
628 639 Project.transaction do
629 640 if save
630 641 reload
631 642 to_be_copied.each do |name|
632 643 send "copy_#{name}", project
633 644 end
634 645 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
635 646 save
636 647 end
637 648 end
638 649 end
639 650
640 651
641 652 # Copies +project+ and returns the new instance. This will not save
642 653 # the copy
643 654 def self.copy_from(project)
644 655 begin
645 656 project = project.is_a?(Project) ? project : Project.find(project)
646 657 if project
647 658 # clear unique attributes
648 659 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
649 660 copy = Project.new(attributes)
650 661 copy.enabled_modules = project.enabled_modules
651 662 copy.trackers = project.trackers
652 663 copy.custom_values = project.custom_values.collect {|v| v.clone}
653 664 copy.issue_custom_fields = project.issue_custom_fields
654 665 return copy
655 666 else
656 667 return nil
657 668 end
658 669 rescue ActiveRecord::RecordNotFound
659 670 return nil
660 671 end
661 672 end
662 673
663 674 # Yields the given block for each project with its level in the tree
664 675 def self.project_tree(projects, &block)
665 676 ancestors = []
666 677 projects.sort_by(&:lft).each do |project|
667 678 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
668 679 ancestors.pop
669 680 end
670 681 yield project, ancestors.size
671 682 ancestors << project
672 683 end
673 684 end
674 685
675 686 private
676 687
677 688 # Copies wiki from +project+
678 689 def copy_wiki(project)
679 690 # Check that the source project has a wiki first
680 691 unless project.wiki.nil?
681 692 self.wiki ||= Wiki.new
682 693 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
683 694 wiki_pages_map = {}
684 695 project.wiki.pages.each do |page|
685 696 # Skip pages without content
686 697 next if page.content.nil?
687 698 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
688 699 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
689 700 new_wiki_page.content = new_wiki_content
690 701 wiki.pages << new_wiki_page
691 702 wiki_pages_map[page.id] = new_wiki_page
692 703 end
693 704 wiki.save
694 705 # Reproduce page hierarchy
695 706 project.wiki.pages.each do |page|
696 707 if page.parent_id && wiki_pages_map[page.id]
697 708 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
698 709 wiki_pages_map[page.id].save
699 710 end
700 711 end
701 712 end
702 713 end
703 714
704 715 # Copies versions from +project+
705 716 def copy_versions(project)
706 717 project.versions.each do |version|
707 718 new_version = Version.new
708 719 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
709 720 self.versions << new_version
710 721 end
711 722 end
712 723
713 724 # Copies issue categories from +project+
714 725 def copy_issue_categories(project)
715 726 project.issue_categories.each do |issue_category|
716 727 new_issue_category = IssueCategory.new
717 728 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
718 729 self.issue_categories << new_issue_category
719 730 end
720 731 end
721 732
722 733 # Copies issues from +project+
723 734 # Note: issues assigned to a closed version won't be copied due to validation rules
724 735 def copy_issues(project)
725 736 # Stores the source issue id as a key and the copied issues as the
726 737 # value. Used to map the two togeather for issue relations.
727 738 issues_map = {}
728 739
729 740 # Get issues sorted by root_id, lft so that parent issues
730 741 # get copied before their children
731 742 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
732 743 new_issue = Issue.new
733 744 new_issue.copy_from(issue)
734 745 new_issue.project = self
735 746 # Reassign fixed_versions by name, since names are unique per
736 747 # project and the versions for self are not yet saved
737 748 if issue.fixed_version
738 749 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
739 750 end
740 751 # Reassign the category by name, since names are unique per
741 752 # project and the categories for self are not yet saved
742 753 if issue.category
743 754 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
744 755 end
745 756 # Parent issue
746 757 if issue.parent_id
747 758 if copied_parent = issues_map[issue.parent_id]
748 759 new_issue.parent_issue_id = copied_parent.id
749 760 end
750 761 end
751 762
752 763 self.issues << new_issue
753 764 if new_issue.new_record?
754 765 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
755 766 else
756 767 issues_map[issue.id] = new_issue unless new_issue.new_record?
757 768 end
758 769 end
759 770
760 771 # Relations after in case issues related each other
761 772 project.issues.each do |issue|
762 773 new_issue = issues_map[issue.id]
763 774 unless new_issue
764 775 # Issue was not copied
765 776 next
766 777 end
767 778
768 779 # Relations
769 780 issue.relations_from.each do |source_relation|
770 781 new_issue_relation = IssueRelation.new
771 782 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
772 783 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
773 784 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
774 785 new_issue_relation.issue_to = source_relation.issue_to
775 786 end
776 787 new_issue.relations_from << new_issue_relation
777 788 end
778 789
779 790 issue.relations_to.each do |source_relation|
780 791 new_issue_relation = IssueRelation.new
781 792 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
782 793 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
783 794 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
784 795 new_issue_relation.issue_from = source_relation.issue_from
785 796 end
786 797 new_issue.relations_to << new_issue_relation
787 798 end
788 799 end
789 800 end
790 801
791 802 # Copies members from +project+
792 803 def copy_members(project)
793 804 # Copy users first, then groups to handle members with inherited and given roles
794 805 members_to_copy = []
795 806 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
796 807 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
797 808
798 809 members_to_copy.each do |member|
799 810 new_member = Member.new
800 811 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
801 812 # only copy non inherited roles
802 813 # inherited roles will be added when copying the group membership
803 814 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
804 815 next if role_ids.empty?
805 816 new_member.role_ids = role_ids
806 817 new_member.project = self
807 818 self.members << new_member
808 819 end
809 820 end
810 821
811 822 # Copies queries from +project+
812 823 def copy_queries(project)
813 824 project.queries.each do |query|
814 825 new_query = ::Query.new
815 826 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
816 827 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
817 828 new_query.project = self
818 829 new_query.user_id = query.user_id
819 830 self.queries << new_query
820 831 end
821 832 end
822 833
823 834 # Copies boards from +project+
824 835 def copy_boards(project)
825 836 project.boards.each do |board|
826 837 new_board = Board.new
827 838 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
828 839 new_board.project = self
829 840 self.boards << new_board
830 841 end
831 842 end
832 843
833 844 def allowed_permissions
834 845 @allowed_permissions ||= begin
835 846 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
836 847 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
837 848 end
838 849 end
839 850
840 851 def allowed_actions
841 852 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
842 853 end
843 854
844 855 # Returns all the active Systemwide and project specific activities
845 856 def active_activities
846 857 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
847 858
848 859 if overridden_activity_ids.empty?
849 860 return TimeEntryActivity.shared.active
850 861 else
851 862 return system_activities_and_project_overrides
852 863 end
853 864 end
854 865
855 866 # Returns all the Systemwide and project specific activities
856 867 # (inactive and active)
857 868 def all_activities
858 869 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
859 870
860 871 if overridden_activity_ids.empty?
861 872 return TimeEntryActivity.shared
862 873 else
863 874 return system_activities_and_project_overrides(true)
864 875 end
865 876 end
866 877
867 878 # Returns the systemwide active activities merged with the project specific overrides
868 879 def system_activities_and_project_overrides(include_inactive=false)
869 880 if include_inactive
870 881 return TimeEntryActivity.shared.
871 882 find(:all,
872 883 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
873 884 self.time_entry_activities
874 885 else
875 886 return TimeEntryActivity.shared.active.
876 887 find(:all,
877 888 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
878 889 self.time_entry_activities.active
879 890 end
880 891 end
881 892
882 893 # Archives subprojects recursively
883 894 def archive!
884 895 children.each do |subproject|
885 896 subproject.send :archive!
886 897 end
887 898 update_attribute :status, STATUS_ARCHIVED
888 899 end
889 900 end
@@ -1,14 +1,21
1 1 <%= error_messages_for 'time_entry' %>
2 2 <%= back_url_hidden_field_tag %>
3 3
4 4 <div class="box tabular">
5 <% if @time_entry.new_record? %>
6 <% if params[:project_id] %>
7 <%= f.hidden_field :project_id %>
8 <% else %>
9 <p><%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).all, :selected => @time_entry.project), :required => true %></p>
10 <% end %>
11 <% end %>
5 12 <p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p>
6 13 <p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
7 14 <p><%= f.text_field :hours, :size => 6, :required => true %></p>
8 15 <p><%= f.text_field :comments, :size => 100 %></p>
9 16 <p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
10 17 <% @time_entry.custom_field_values.each do |value| %>
11 18 <p><%= custom_field_tag_with_label :time_entry, value %></p>
12 19 <% end %>
13 20 <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
14 21 </div>
@@ -1,31 +1,33
1 1 <div class="contextual">
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
2 <%= link_to l(:button_log_time),
3 {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue},
4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
3 5 </div>
4 6
5 7 <%= render_timelog_breadcrumb %>
6 8
7 9 <h2><%= l(:label_spent_time) %></h2>
8 10
9 11 <% form_tag({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
10 12 <%= render :partial => 'date_range' %>
11 13 <% end %>
12 14
13 15 <div class="total-hours">
14 16 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
15 17 </div>
16 18
17 19 <% unless @entries.empty? %>
18 20 <%= render :partial => 'list', :locals => { :entries => @entries }%>
19 21 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
20 22
21 23 <% other_formats_links do |f| %>
22 24 <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %>
23 25 <%= f.link_to 'CSV', :url => params %>
24 26 <% end %>
25 27 <% end %>
26 28
27 29 <% html_title l(:label_spent_time), l(:label_details) %>
28 30
29 31 <% content_for :header_tags do %>
30 32 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
31 33 <% end %>
@@ -1,7 +1,7
1 1 <h2><%= l(:label_spent_time) %></h2>
2 2
3 <% labelled_form_for @time_entry, :url => project_time_entries_path(@time_entry.project) do |f| %>
3 <% labelled_form_for @time_entry, :url => time_entries_path do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <%= submit_tag l(:button_create) %>
6 6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
7 7 <% end %>
@@ -1,610 +1,704
1 1 # -*- coding: utf-8 -*-
2 2 # Redmine - project management software
3 3 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 4 #
5 5 # This program is free software; you can redistribute it and/or
6 6 # modify it under the terms of the GNU General Public License
7 7 # as published by the Free Software Foundation; either version 2
8 8 # of the License, or (at your option) any later version.
9 9 #
10 10 # This program is distributed in the hope that it will be useful,
11 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 13 # GNU General Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License
16 16 # along with this program; if not, write to the Free Software
17 17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 18
19 19 require File.expand_path('../../test_helper', __FILE__)
20 20 require 'timelog_controller'
21 21
22 22 # Re-raise errors caught by the controller.
23 23 class TimelogController; def rescue_action(e) raise e end; end
24 24
25 25 class TimelogControllerTest < ActionController::TestCase
26 26 fixtures :projects, :enabled_modules, :roles, :members,
27 27 :member_roles, :issues, :time_entries, :users,
28 28 :trackers, :enumerations, :issue_statuses,
29 29 :custom_fields, :custom_values
30 30
31 31 include Redmine::I18n
32 32
33 33 def setup
34 34 @controller = TimelogController.new
35 35 @request = ActionController::TestRequest.new
36 36 @response = ActionController::TestResponse.new
37 37 end
38 38
39 39 def test_get_new
40 40 @request.session[:user_id] = 3
41 41 get :new, :project_id => 1
42 42 assert_response :success
43 43 assert_template 'new'
44 44 # Default activity selected
45 45 assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
46 46 :content => 'Development'
47 47 end
48 48
49 49 def test_get_new_should_only_show_active_time_entry_activities
50 50 @request.session[:user_id] = 3
51 51 get :new, :project_id => 1
52 52 assert_response :success
53 53 assert_template 'new'
54 assert_no_tag :tag => 'option', :content => 'Inactive Activity'
54 assert_no_tag 'select', :attributes => {:name => 'time_entry[project_id]'}
55 assert_no_tag 'option', :content => 'Inactive Activity'
56 end
57
58 def test_new_without_project
59 @request.session[:user_id] = 3
60 get :new
61 assert_response :success
62 assert_template 'new'
63 assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'}
64 end
65
66 def test_new_without_project_should_deny_without_permission
67 Role.all.each {|role| role.remove_permission! :log_time}
68 @request.session[:user_id] = 3
69
70 get :new
71 assert_response 403
55 72 end
56 73
57 74 def test_get_edit_existing_time
58 75 @request.session[:user_id] = 2
59 76 get :edit, :id => 2, :project_id => nil
60 77 assert_response :success
61 78 assert_template 'edit'
62 79 # Default activity selected
63 80 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' }
64 81 end
65 82
66 83 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
67 84 te = TimeEntry.find(1)
68 85 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
69 86 te.save!
70 87
71 88 @request.session[:user_id] = 1
72 89 get :edit, :project_id => 1, :id => 1
73 90 assert_response :success
74 91 assert_template 'edit'
75 92 # Blank option since nothing is pre-selected
76 93 assert_tag :tag => 'option', :content => '--- Please select ---'
77 94 end
78 95
79 96 def test_post_create
80 97 # TODO: should POST to issues’ time log instead of project. change form
81 98 # and routing
82 99 @request.session[:user_id] = 3
83 100 post :create, :project_id => 1,
84 101 :time_entry => {:comments => 'Some work on TimelogControllerTest',
85 102 # Not the default activity
86 103 :activity_id => '11',
87 104 :spent_on => '2008-03-14',
88 105 :issue_id => '1',
89 106 :hours => '7.3'}
90 107 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
91 108
92 109 i = Issue.find(1)
93 110 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
94 111 assert_not_nil t
95 112 assert_equal 11, t.activity_id
96 113 assert_equal 7.3, t.hours
97 114 assert_equal 3, t.user_id
98 115 assert_equal i, t.issue
99 116 assert_equal i.project, t.project
100 117 end
101 118
102 119 def test_post_create_with_blank_issue
103 120 # TODO: should POST to issues’ time log instead of project. change form
104 121 # and routing
105 122 @request.session[:user_id] = 3
106 123 post :create, :project_id => 1,
107 124 :time_entry => {:comments => 'Some work on TimelogControllerTest',
108 125 # Not the default activity
109 126 :activity_id => '11',
110 127 :issue_id => '',
111 128 :spent_on => '2008-03-14',
112 129 :hours => '7.3'}
113 130 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
114 131
115 132 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
116 133 assert_not_nil t
117 134 assert_equal 11, t.activity_id
118 135 assert_equal 7.3, t.hours
119 136 assert_equal 3, t.user_id
120 137 end
121 138
122 139 def test_create_and_continue
123 140 @request.session[:user_id] = 2
124 141 post :create, :project_id => 1,
125 142 :time_entry => {:activity_id => '11',
126 143 :issue_id => '',
127 144 :spent_on => '2008-03-14',
128 145 :hours => '7.3'},
129 146 :continue => '1'
130 147 assert_redirected_to '/projects/ecookbook/time_entries/new'
131 148 end
132 149
133 150 def test_create_and_continue_with_issue_id
134 151 @request.session[:user_id] = 2
135 152 post :create, :project_id => 1,
136 153 :time_entry => {:activity_id => '11',
137 154 :issue_id => '1',
138 155 :spent_on => '2008-03-14',
139 156 :hours => '7.3'},
140 157 :continue => '1'
141 158 assert_redirected_to '/projects/ecookbook/issues/1/time_entries/new'
142 159 end
143 160
161 def test_create_and_continue_without_project
162 @request.session[:user_id] = 2
163 post :create, :time_entry => {:project_id => '1',
164 :activity_id => '11',
165 :issue_id => '',
166 :spent_on => '2008-03-14',
167 :hours => '7.3'},
168 :continue => '1'
169
170 assert_redirected_to '/time_entries/new'
171 end
172
144 173 def test_create_without_log_time_permission_should_be_denied
145 174 @request.session[:user_id] = 2
146 175 Role.find_by_name('Manager').remove_permission! :log_time
147 176 post :create, :project_id => 1,
148 177 :time_entry => {:activity_id => '11',
149 178 :issue_id => '',
150 179 :spent_on => '2008-03-14',
151 180 :hours => '7.3'}
152 181
153 182 assert_response 403
154 183 end
155 184
185 def test_create_with_failure
186 @request.session[:user_id] = 2
187 post :create, :project_id => 1,
188 :time_entry => {:activity_id => '',
189 :issue_id => '',
190 :spent_on => '2008-03-14',
191 :hours => '7.3'}
192
193 assert_response :success
194 assert_template 'new'
195 end
196
197 def test_create_without_project
198 @request.session[:user_id] = 2
199 assert_difference 'TimeEntry.count' do
200 post :create, :time_entry => {:project_id => '1',
201 :activity_id => '11',
202 :issue_id => '',
203 :spent_on => '2008-03-14',
204 :hours => '7.3'}
205 end
206
207 assert_redirected_to '/projects/ecookbook/time_entries'
208 time_entry = TimeEntry.first(:order => 'id DESC')
209 assert_equal 1, time_entry.project_id
210 end
211
212 def test_create_without_project_should_deny_without_permission
213 @request.session[:user_id] = 2
214 Project.find(3).disable_module!(:time_tracking)
215
216 assert_no_difference 'TimeEntry.count' do
217 post :create, :time_entry => {:project_id => '3',
218 :activity_id => '11',
219 :issue_id => '',
220 :spent_on => '2008-03-14',
221 :hours => '7.3'}
222 end
223
224 assert_response 403
225 end
226
227 def test_create_without_project_with_failure
228 @request.session[:user_id] = 2
229 assert_no_difference 'TimeEntry.count' do
230 post :create, :time_entry => {:project_id => '1',
231 :activity_id => '11',
232 :issue_id => '',
233 :spent_on => '2008-03-14',
234 :hours => ''}
235 end
236
237 assert_response :success
238 assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'},
239 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
240 end
241
156 242 def test_update
157 243 entry = TimeEntry.find(1)
158 244 assert_equal 1, entry.issue_id
159 245 assert_equal 2, entry.user_id
160 246
161 247 @request.session[:user_id] = 1
162 248 put :update, :id => 1,
163 249 :time_entry => {:issue_id => '2',
164 250 :hours => '8'}
165 251 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
166 252 entry.reload
167 253
168 254 assert_equal 8, entry.hours
169 255 assert_equal 2, entry.issue_id
170 256 assert_equal 2, entry.user_id
171 257 end
172 258
173 259 def test_get_bulk_edit
174 260 @request.session[:user_id] = 2
175 261 get :bulk_edit, :ids => [1, 2]
176 262 assert_response :success
177 263 assert_template 'bulk_edit'
178 264
179 265 # System wide custom field
180 266 assert_tag :select, :attributes => {:name => 'time_entry[custom_field_values][10]'}
181 267 end
182 268
183 269 def test_get_bulk_edit_on_different_projects
184 270 @request.session[:user_id] = 2
185 271 get :bulk_edit, :ids => [1, 2, 6]
186 272 assert_response :success
187 273 assert_template 'bulk_edit'
188 274 end
189 275
190 276 def test_bulk_update
191 277 @request.session[:user_id] = 2
192 278 # update time entry activity
193 279 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
194 280
195 281 assert_response 302
196 282 # check that the issues were updated
197 283 assert_equal [9, 9], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.activity_id}
198 284 end
199 285
200 286 def test_bulk_update_with_failure
201 287 @request.session[:user_id] = 2
202 288 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
203 289
204 290 assert_response 302
205 291 assert_match /Failed to save 2 time entrie/, flash[:error]
206 292 end
207 293
208 294 def test_bulk_update_on_different_projects
209 295 @request.session[:user_id] = 2
210 296 # makes user a manager on the other project
211 297 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
212 298
213 299 # update time entry activity
214 300 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
215 301
216 302 assert_response 302
217 303 # check that the issues were updated
218 304 assert_equal [9, 9, 9], TimeEntry.find_all_by_id([1, 2, 4]).collect {|i| i.activity_id}
219 305 end
220 306
221 307 def test_bulk_update_on_different_projects_without_rights
222 308 @request.session[:user_id] = 3
223 309 user = User.find(3)
224 310 action = { :controller => "timelog", :action => "bulk_update" }
225 311 assert user.allowed_to?(action, TimeEntry.find(1).project)
226 312 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
227 313 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
228 314 assert_response 403
229 315 end
230 316
231 317 def test_bulk_update_custom_field
232 318 @request.session[:user_id] = 2
233 319 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
234 320
235 321 assert_response 302
236 322 assert_equal ["0", "0"], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.custom_value_for(10).value}
237 323 end
238 324
239 325 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
240 326 @request.session[:user_id] = 2
241 327 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
242 328
243 329 assert_response :redirect
244 330 assert_redirected_to '/time_entries'
245 331 end
246 332
247 333 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
248 334 @request.session[:user_id] = 2
249 335 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
250 336
251 337 assert_response :redirect
252 338 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
253 339 end
254 340
255 341 def test_post_bulk_update_without_edit_permission_should_be_denied
256 342 @request.session[:user_id] = 2
257 343 Role.find_by_name('Manager').remove_permission! :edit_time_entries
258 344 post :bulk_update, :ids => [1,2]
259 345
260 346 assert_response 403
261 347 end
262 348
263 349 def test_destroy
264 350 @request.session[:user_id] = 2
265 351 delete :destroy, :id => 1
266 352 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
267 353 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
268 354 assert_nil TimeEntry.find_by_id(1)
269 355 end
270 356
271 357 def test_destroy_should_fail
272 358 # simulate that this fails (e.g. due to a plugin), see #5700
273 359 TimeEntry.any_instance.expects(:destroy).returns(false)
274 360
275 361 @request.session[:user_id] = 2
276 362 delete :destroy, :id => 1
277 363 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
278 364 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
279 365 assert_not_nil TimeEntry.find_by_id(1)
280 366 end
281 367
282 368 def test_index_all_projects
283 369 get :index
284 370 assert_response :success
285 371 assert_template 'index'
286 372 assert_not_nil assigns(:total_hours)
287 373 assert_equal "162.90", "%.2f" % assigns(:total_hours)
288 374 assert_tag :form,
289 375 :attributes => {:action => "/time_entries", :id => 'query_form'}
290 376 end
291 377
378 def test_index_all_projects_should_show_log_time_link
379 @request.session[:user_id] = 2
380 get :index
381 assert_response :success
382 assert_template 'index'
383 assert_tag 'a', :attributes => {:href => '/time_entries/new'}, :content => /Log time/
384 end
385
292 386 def test_index_at_project_level
293 387 get :index, :project_id => 'ecookbook'
294 388 assert_response :success
295 389 assert_template 'index'
296 390 assert_not_nil assigns(:entries)
297 391 assert_equal 4, assigns(:entries).size
298 392 # project and subproject
299 393 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
300 394 assert_not_nil assigns(:total_hours)
301 395 assert_equal "162.90", "%.2f" % assigns(:total_hours)
302 396 # display all time by default
303 397 assert_nil assigns(:from)
304 398 assert_nil assigns(:to)
305 399 assert_tag :form,
306 400 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
307 401 end
308 402
309 403 def test_index_at_project_level_with_date_range
310 404 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
311 405 assert_response :success
312 406 assert_template 'index'
313 407 assert_not_nil assigns(:entries)
314 408 assert_equal 3, assigns(:entries).size
315 409 assert_not_nil assigns(:total_hours)
316 410 assert_equal "12.90", "%.2f" % assigns(:total_hours)
317 411 assert_equal '2007-03-20'.to_date, assigns(:from)
318 412 assert_equal '2007-04-30'.to_date, assigns(:to)
319 413 assert_tag :form,
320 414 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
321 415 end
322 416
323 417 def test_index_at_project_level_with_period
324 418 get :index, :project_id => 'ecookbook', :period => '7_days'
325 419 assert_response :success
326 420 assert_template 'index'
327 421 assert_not_nil assigns(:entries)
328 422 assert_not_nil assigns(:total_hours)
329 423 assert_equal Date.today - 7, assigns(:from)
330 424 assert_equal Date.today, assigns(:to)
331 425 assert_tag :form,
332 426 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
333 427 end
334 428
335 429 def test_index_one_day
336 430 get :index, :project_id => 'ecookbook', :from => "2007-03-23", :to => "2007-03-23"
337 431 assert_response :success
338 432 assert_template 'index'
339 433 assert_not_nil assigns(:total_hours)
340 434 assert_equal "4.25", "%.2f" % assigns(:total_hours)
341 435 assert_tag :form,
342 436 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
343 437 end
344 438
345 439 def test_index_today
346 440 Date.stubs(:today).returns('2011-12-15'.to_date)
347 441 get :index, :period => 'today'
348 442 assert_equal '2011-12-15'.to_date, assigns(:from)
349 443 assert_equal '2011-12-15'.to_date, assigns(:to)
350 444 end
351 445
352 446 def test_index_yesterday
353 447 Date.stubs(:today).returns('2011-12-15'.to_date)
354 448 get :index, :period => 'yesterday'
355 449 assert_equal '2011-12-14'.to_date, assigns(:from)
356 450 assert_equal '2011-12-14'.to_date, assigns(:to)
357 451 end
358 452
359 453 def test_index_current_week
360 454 Date.stubs(:today).returns('2011-12-15'.to_date)
361 455 get :index, :period => 'current_week'
362 456 assert_equal '2011-12-12'.to_date, assigns(:from)
363 457 assert_equal '2011-12-18'.to_date, assigns(:to)
364 458 end
365 459
366 460 def test_index_last_week
367 461 Date.stubs(:today).returns('2011-12-15'.to_date)
368 462 get :index, :period => 'current_week'
369 463 assert_equal '2011-12-05'.to_date, assigns(:from)
370 464 assert_equal '2011-12-11'.to_date, assigns(:to)
371 465 end
372 466
373 467 def test_index_last_week
374 468 Date.stubs(:today).returns('2011-12-15'.to_date)
375 469 get :index, :period => 'last_week'
376 470 assert_equal '2011-12-05'.to_date, assigns(:from)
377 471 assert_equal '2011-12-11'.to_date, assigns(:to)
378 472 end
379 473
380 474 def test_index_7_days
381 475 Date.stubs(:today).returns('2011-12-15'.to_date)
382 476 get :index, :period => '7_days'
383 477 assert_equal '2011-12-08'.to_date, assigns(:from)
384 478 assert_equal '2011-12-15'.to_date, assigns(:to)
385 479 end
386 480
387 481 def test_index_current_month
388 482 Date.stubs(:today).returns('2011-12-15'.to_date)
389 483 get :index, :period => 'current_month'
390 484 assert_equal '2011-12-01'.to_date, assigns(:from)
391 485 assert_equal '2011-12-31'.to_date, assigns(:to)
392 486 end
393 487
394 488 def test_index_last_month
395 489 Date.stubs(:today).returns('2011-12-15'.to_date)
396 490 get :index, :period => 'last_month'
397 491 assert_equal '2011-11-01'.to_date, assigns(:from)
398 492 assert_equal '2011-11-30'.to_date, assigns(:to)
399 493 end
400 494
401 495 def test_index_30_days
402 496 Date.stubs(:today).returns('2011-12-15'.to_date)
403 497 get :index, :period => '30_days'
404 498 assert_equal '2011-11-15'.to_date, assigns(:from)
405 499 assert_equal '2011-12-15'.to_date, assigns(:to)
406 500 end
407 501
408 502 def test_index_current_year
409 503 Date.stubs(:today).returns('2011-12-15'.to_date)
410 504 get :index, :period => 'current_year'
411 505 assert_equal '2011-01-01'.to_date, assigns(:from)
412 506 assert_equal '2011-12-31'.to_date, assigns(:to)
413 507 end
414 508
415 509 def test_index_at_issue_level
416 510 get :index, :issue_id => 1
417 511 assert_response :success
418 512 assert_template 'index'
419 513 assert_not_nil assigns(:entries)
420 514 assert_equal 2, assigns(:entries).size
421 515 assert_not_nil assigns(:total_hours)
422 516 assert_equal 154.25, assigns(:total_hours)
423 517 # display all time
424 518 assert_nil assigns(:from)
425 519 assert_nil assigns(:to)
426 520 # TODO: remove /projects/:project_id/issues/:issue_id/time_entries routes
427 521 # to use /issues/:issue_id/time_entries
428 522 assert_tag :form,
429 523 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'}
430 524 end
431 525
432 526 def test_index_atom_feed
433 527 get :index, :project_id => 1, :format => 'atom'
434 528 assert_response :success
435 529 assert_equal 'application/atom+xml', @response.content_type
436 530 assert_not_nil assigns(:items)
437 531 assert assigns(:items).first.is_a?(TimeEntry)
438 532 end
439 533
440 534 def test_index_all_projects_csv_export
441 535 Setting.date_format = '%m/%d/%Y'
442 536 get :index, :format => 'csv'
443 537 assert_response :success
444 538 assert_equal 'text/csv', @response.content_type
445 539 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
446 540 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
447 541 end
448 542
449 543 def test_index_csv_export
450 544 Setting.date_format = '%m/%d/%Y'
451 545 get :index, :project_id => 1, :format => 'csv'
452 546 assert_response :success
453 547 assert_equal 'text/csv', @response.content_type
454 548 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
455 549 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
456 550 end
457 551
458 552 def test_csv_big_5
459 553 user = User.find_by_id(3)
460 554 user.language = "zh-TW"
461 555 assert user.save
462 556 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
463 557 str_big5 = "\xa4@\xa4\xeb"
464 558 if str_utf8.respond_to?(:force_encoding)
465 559 str_utf8.force_encoding('UTF-8')
466 560 str_big5.force_encoding('Big5')
467 561 end
468 562 @request.session[:user_id] = 3
469 563 post :create, :project_id => 1,
470 564 :time_entry => {:comments => str_utf8,
471 565 # Not the default activity
472 566 :activity_id => '11',
473 567 :issue_id => '',
474 568 :spent_on => '2011-11-10',
475 569 :hours => '7.3'}
476 570 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
477 571
478 572 t = TimeEntry.find_by_comments(str_utf8)
479 573 assert_not_nil t
480 574 assert_equal 11, t.activity_id
481 575 assert_equal 7.3, t.hours
482 576 assert_equal 3, t.user_id
483 577
484 578 get :index, :project_id => 1, :format => 'csv',
485 579 :from => '2011-11-10', :to => '2011-11-10'
486 580 assert_response :success
487 581 assert_equal 'text/csv', @response.content_type
488 582 ar = @response.body.chomp.split("\n")
489 583 s1 = "\xa4\xe9\xb4\xc1"
490 584 if str_utf8.respond_to?(:force_encoding)
491 585 s1.force_encoding('Big5')
492 586 end
493 587 assert ar[0].include?(s1)
494 588 assert ar[1].include?(str_big5)
495 589 end
496 590
497 591 def test_csv_cannot_convert_should_be_replaced_big_5
498 592 user = User.find_by_id(3)
499 593 user.language = "zh-TW"
500 594 assert user.save
501 595 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
502 596 if str_utf8.respond_to?(:force_encoding)
503 597 str_utf8.force_encoding('UTF-8')
504 598 end
505 599 @request.session[:user_id] = 3
506 600 post :create, :project_id => 1,
507 601 :time_entry => {:comments => str_utf8,
508 602 # Not the default activity
509 603 :activity_id => '11',
510 604 :issue_id => '',
511 605 :spent_on => '2011-11-10',
512 606 :hours => '7.3'}
513 607 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
514 608
515 609 t = TimeEntry.find_by_comments(str_utf8)
516 610 assert_not_nil t
517 611 assert_equal 11, t.activity_id
518 612 assert_equal 7.3, t.hours
519 613 assert_equal 3, t.user_id
520 614
521 615 get :index, :project_id => 1, :format => 'csv',
522 616 :from => '2011-11-10', :to => '2011-11-10'
523 617 assert_response :success
524 618 assert_equal 'text/csv', @response.content_type
525 619 ar = @response.body.chomp.split("\n")
526 620 s1 = "\xa4\xe9\xb4\xc1"
527 621 if str_utf8.respond_to?(:force_encoding)
528 622 s1.force_encoding('Big5')
529 623 end
530 624 assert ar[0].include?(s1)
531 625 s2 = ar[1].split(",")[8]
532 626 if s2.respond_to?(:force_encoding)
533 627 s3 = "\xa5H?"
534 628 s3.force_encoding('Big5')
535 629 assert_equal s3, s2
536 630 elsif RUBY_PLATFORM == 'java'
537 631 assert_equal "??", s2
538 632 else
539 633 assert_equal "\xa5H???", s2
540 634 end
541 635 end
542 636
543 637 def test_csv_tw
544 638 with_settings :default_language => "zh-TW" do
545 639 str1 = "test_csv_tw"
546 640 user = User.find_by_id(3)
547 641 te1 = TimeEntry.create(:spent_on => '2011-11-10',
548 642 :hours => 999.9,
549 643 :project => Project.find(1),
550 644 :user => user,
551 645 :activity => TimeEntryActivity.find_by_name('Design'),
552 646 :comments => str1)
553 647 te2 = TimeEntry.find_by_comments(str1)
554 648 assert_not_nil te2
555 649 assert_equal 999.9, te2.hours
556 650 assert_equal 3, te2.user_id
557 651
558 652 get :index, :project_id => 1, :format => 'csv',
559 653 :from => '2011-11-10', :to => '2011-11-10'
560 654 assert_response :success
561 655 assert_equal 'text/csv', @response.content_type
562 656
563 657 ar = @response.body.chomp.split("\n")
564 658 s2 = ar[1].split(",")[7]
565 659 assert_equal '999.9', s2
566 660
567 661 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
568 662 if str_tw.respond_to?(:force_encoding)
569 663 str_tw.force_encoding('UTF-8')
570 664 end
571 665 assert_equal str_tw, l(:general_lang_name)
572 666 assert_equal ',', l(:general_csv_separator)
573 667 assert_equal '.', l(:general_csv_decimal_separator)
574 668 end
575 669 end
576 670
577 671 def test_csv_fr
578 672 with_settings :default_language => "fr" do
579 673 str1 = "test_csv_fr"
580 674 user = User.find_by_id(3)
581 675 te1 = TimeEntry.create(:spent_on => '2011-11-10',
582 676 :hours => 999.9,
583 677 :project => Project.find(1),
584 678 :user => user,
585 679 :activity => TimeEntryActivity.find_by_name('Design'),
586 680 :comments => str1)
587 681 te2 = TimeEntry.find_by_comments(str1)
588 682 assert_not_nil te2
589 683 assert_equal 999.9, te2.hours
590 684 assert_equal 3, te2.user_id
591 685
592 686 get :index, :project_id => 1, :format => 'csv',
593 687 :from => '2011-11-10', :to => '2011-11-10'
594 688 assert_response :success
595 689 assert_equal 'text/csv', @response.content_type
596 690
597 691 ar = @response.body.chomp.split("\n")
598 692 s2 = ar[1].split(";")[7]
599 693 assert_equal '999,9', s2
600 694
601 695 str_fr = "Fran\xc3\xa7ais"
602 696 if str_fr.respond_to?(:force_encoding)
603 697 str_fr.force_encoding('UTF-8')
604 698 end
605 699 assert_equal str_fr, l(:general_lang_name)
606 700 assert_equal ';', l(:general_csv_separator)
607 701 assert_equal ',', l(:general_csv_decimal_separator)
608 702 end
609 703 end
610 704 end
General Comments 0
You need to be logged in to leave comments. Login now