##// END OF EJS Templates
When a specific TimeEntryActivity are change, associated TimeEntries will be...
Eric Davis -
r2836:37d401ac58c3
parent child
Show More
@@ -1,344 +1,345
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 ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :activity, :only => :activity
20 menu_item :activity, :only => :activity
21 menu_item :roadmap, :only => :roadmap
21 menu_item :roadmap, :only => :roadmap
22 menu_item :files, :only => [:list_files, :add_file]
22 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :settings, :only => :settings
23 menu_item :settings, :only => :settings
24 menu_item :issues, :only => [:changelog]
24 menu_item :issues, :only => [:changelog]
25
25
26 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
26 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
27 before_filter :find_optional_project, :only => :activity
27 before_filter :find_optional_project, :only => :activity
28 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
28 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
29 before_filter :authorize_global, :only => :add
29 before_filter :authorize_global, :only => :add
30 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
30 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
31 accept_key_auth :activity
31 accept_key_auth :activity
32
32
33 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
34 if controller.request.post?
34 if controller.request.post?
35 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
36 end
36 end
37 end
37 end
38
38
39 helper :sort
39 helper :sort
40 include SortHelper
40 include SortHelper
41 helper :custom_fields
41 helper :custom_fields
42 include CustomFieldsHelper
42 include CustomFieldsHelper
43 helper :issues
43 helper :issues
44 helper IssuesHelper
44 helper IssuesHelper
45 helper :queries
45 helper :queries
46 include QueriesHelper
46 include QueriesHelper
47 helper :repositories
47 helper :repositories
48 include RepositoriesHelper
48 include RepositoriesHelper
49 include ProjectsHelper
49 include ProjectsHelper
50
50
51 # Lists visible projects
51 # Lists visible projects
52 def index
52 def index
53 respond_to do |format|
53 respond_to do |format|
54 format.html {
54 format.html {
55 @projects = Project.visible.find(:all, :order => 'lft')
55 @projects = Project.visible.find(:all, :order => 'lft')
56 }
56 }
57 format.atom {
57 format.atom {
58 projects = Project.visible.find(:all, :order => 'created_on DESC',
58 projects = Project.visible.find(:all, :order => 'created_on DESC',
59 :limit => Setting.feeds_limit.to_i)
59 :limit => Setting.feeds_limit.to_i)
60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
61 }
61 }
62 end
62 end
63 end
63 end
64
64
65 # Add a new project
65 # Add a new project
66 def add
66 def add
67 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
68 @trackers = Tracker.all
68 @trackers = Tracker.all
69 @project = Project.new(params[:project])
69 @project = Project.new(params[:project])
70 if request.get?
70 if request.get?
71 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
71 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
72 @project.trackers = Tracker.all
72 @project.trackers = Tracker.all
73 @project.is_public = Setting.default_projects_public?
73 @project.is_public = Setting.default_projects_public?
74 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
74 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
75 else
75 else
76 @project.enabled_module_names = params[:enabled_modules]
76 @project.enabled_module_names = params[:enabled_modules]
77 if @project.save
77 if @project.save
78 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
78 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
79 # Add current user as a project member if he is not admin
79 # Add current user as a project member if he is not admin
80 unless User.current.admin?
80 unless User.current.admin?
81 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
81 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
82 m = Member.new(:user => User.current, :roles => [r])
82 m = Member.new(:user => User.current, :roles => [r])
83 @project.members << m
83 @project.members << m
84 end
84 end
85 flash[:notice] = l(:notice_successful_create)
85 flash[:notice] = l(:notice_successful_create)
86 redirect_to :controller => 'projects', :action => 'settings', :id => @project
86 redirect_to :controller => 'projects', :action => 'settings', :id => @project
87 end
87 end
88 end
88 end
89 end
89 end
90
90
91 def copy
91 def copy
92 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
92 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
93 @trackers = Tracker.all
93 @trackers = Tracker.all
94 @root_projects = Project.find(:all,
94 @root_projects = Project.find(:all,
95 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
95 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
96 :order => 'name')
96 :order => 'name')
97 if request.get?
97 if request.get?
98 @project = Project.copy_from(params[:id])
98 @project = Project.copy_from(params[:id])
99 if @project
99 if @project
100 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
100 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
101 else
101 else
102 redirect_to :controller => 'admin', :action => 'projects'
102 redirect_to :controller => 'admin', :action => 'projects'
103 end
103 end
104 else
104 else
105 @project = Project.new(params[:project])
105 @project = Project.new(params[:project])
106 @project.enabled_module_names = params[:enabled_modules]
106 @project.enabled_module_names = params[:enabled_modules]
107 if @project.copy(params[:id])
107 if @project.copy(params[:id])
108 flash[:notice] = l(:notice_successful_create)
108 flash[:notice] = l(:notice_successful_create)
109 redirect_to :controller => 'admin', :action => 'projects'
109 redirect_to :controller => 'admin', :action => 'projects'
110 end
110 end
111 end
111 end
112 end
112 end
113
113
114
114
115 # Show @project
115 # Show @project
116 def show
116 def show
117 if params[:jump]
117 if params[:jump]
118 # try to redirect to the requested menu item
118 # try to redirect to the requested menu item
119 redirect_to_project_menu_item(@project, params[:jump]) && return
119 redirect_to_project_menu_item(@project, params[:jump]) && return
120 end
120 end
121
121
122 @users_by_role = @project.users_by_role
122 @users_by_role = @project.users_by_role
123 @subprojects = @project.children.visible
123 @subprojects = @project.children.visible
124 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
124 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
125 @trackers = @project.rolled_up_trackers
125 @trackers = @project.rolled_up_trackers
126
126
127 cond = @project.project_condition(Setting.display_subprojects_issues?)
127 cond = @project.project_condition(Setting.display_subprojects_issues?)
128
128
129 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
129 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
130 :include => [:project, :status, :tracker],
130 :include => [:project, :status, :tracker],
131 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
131 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
132 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
132 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
133 :include => [:project, :status, :tracker],
133 :include => [:project, :status, :tracker],
134 :conditions => cond)
134 :conditions => cond)
135
135
136 TimeEntry.visible_by(User.current) do
136 TimeEntry.visible_by(User.current) do
137 @total_hours = TimeEntry.sum(:hours,
137 @total_hours = TimeEntry.sum(:hours,
138 :include => :project,
138 :include => :project,
139 :conditions => cond).to_f
139 :conditions => cond).to_f
140 end
140 end
141 @key = User.current.rss_key
141 @key = User.current.rss_key
142 end
142 end
143
143
144 def settings
144 def settings
145 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
145 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
146 @issue_category ||= IssueCategory.new
146 @issue_category ||= IssueCategory.new
147 @member ||= @project.members.new
147 @member ||= @project.members.new
148 @trackers = Tracker.all
148 @trackers = Tracker.all
149 @repository ||= @project.repository
149 @repository ||= @project.repository
150 @wiki ||= @project.wiki
150 @wiki ||= @project.wiki
151 end
151 end
152
152
153 # Edit @project
153 # Edit @project
154 def edit
154 def edit
155 if request.post?
155 if request.post?
156 @project.attributes = params[:project]
156 @project.attributes = params[:project]
157 if @project.save
157 if @project.save
158 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
158 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
159 flash[:notice] = l(:notice_successful_update)
159 flash[:notice] = l(:notice_successful_update)
160 redirect_to :action => 'settings', :id => @project
160 redirect_to :action => 'settings', :id => @project
161 else
161 else
162 settings
162 settings
163 render :action => 'settings'
163 render :action => 'settings'
164 end
164 end
165 end
165 end
166 end
166 end
167
167
168 def modules
168 def modules
169 @project.enabled_module_names = params[:enabled_modules]
169 @project.enabled_module_names = params[:enabled_modules]
170 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
170 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
171 end
171 end
172
172
173 def archive
173 def archive
174 @project.archive if request.post? && @project.active?
174 @project.archive if request.post? && @project.active?
175 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
175 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
176 end
176 end
177
177
178 def unarchive
178 def unarchive
179 @project.unarchive if request.post? && !@project.active?
179 @project.unarchive if request.post? && !@project.active?
180 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
180 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
181 end
181 end
182
182
183 # Delete @project
183 # Delete @project
184 def destroy
184 def destroy
185 @project_to_destroy = @project
185 @project_to_destroy = @project
186 if request.post? and params[:confirm]
186 if request.post? and params[:confirm]
187 @project_to_destroy.destroy
187 @project_to_destroy.destroy
188 redirect_to :controller => 'admin', :action => 'projects'
188 redirect_to :controller => 'admin', :action => 'projects'
189 end
189 end
190 # hide project in layout
190 # hide project in layout
191 @project = nil
191 @project = nil
192 end
192 end
193
193
194 # Add a new issue category to @project
194 # Add a new issue category to @project
195 def add_issue_category
195 def add_issue_category
196 @category = @project.issue_categories.build(params[:category])
196 @category = @project.issue_categories.build(params[:category])
197 if request.post? and @category.save
197 if request.post? and @category.save
198 respond_to do |format|
198 respond_to do |format|
199 format.html do
199 format.html do
200 flash[:notice] = l(:notice_successful_create)
200 flash[:notice] = l(:notice_successful_create)
201 redirect_to :action => 'settings', :tab => 'categories', :id => @project
201 redirect_to :action => 'settings', :tab => 'categories', :id => @project
202 end
202 end
203 format.js do
203 format.js do
204 # IE doesn't support the replace_html rjs method for select box options
204 # IE doesn't support the replace_html rjs method for select box options
205 render(:update) {|page| page.replace "issue_category_id",
205 render(:update) {|page| page.replace "issue_category_id",
206 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
206 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
207 }
207 }
208 end
208 end
209 end
209 end
210 end
210 end
211 end
211 end
212
212
213 # Add a new version to @project
213 # Add a new version to @project
214 def add_version
214 def add_version
215 @version = @project.versions.build(params[:version])
215 @version = @project.versions.build(params[:version])
216 if request.post? and @version.save
216 if request.post? and @version.save
217 flash[:notice] = l(:notice_successful_create)
217 flash[:notice] = l(:notice_successful_create)
218 redirect_to :action => 'settings', :tab => 'versions', :id => @project
218 redirect_to :action => 'settings', :tab => 'versions', :id => @project
219 end
219 end
220 end
220 end
221
221
222 def add_file
222 def add_file
223 if request.post?
223 if request.post?
224 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
224 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
225 attachments = attach_files(container, params[:attachments])
225 attachments = attach_files(container, params[:attachments])
226 if !attachments.empty? && Setting.notified_events.include?('file_added')
226 if !attachments.empty? && Setting.notified_events.include?('file_added')
227 Mailer.deliver_attachments_added(attachments)
227 Mailer.deliver_attachments_added(attachments)
228 end
228 end
229 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
229 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
230 return
230 return
231 end
231 end
232 @versions = @project.versions.sort
232 @versions = @project.versions.sort
233 end
233 end
234
234
235 def save_activities
235 def save_activities
236 if request.post? && params[:enumerations]
236 if request.post? && params[:enumerations]
237 params[:enumerations].each do |id, activity|
237 Project.transaction do
238 @project.update_or_build_time_entry_activity(id, activity)
238 params[:enumerations].each do |id, activity|
239 @project.update_or_create_time_entry_activity(id, activity)
240 end
239 end
241 end
240 @project.save
241 end
242 end
242
243
243 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
244 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
244 end
245 end
245
246
246 def reset_activities
247 def reset_activities
247 @project.time_entry_activities.each do |time_entry_activity|
248 @project.time_entry_activities.each do |time_entry_activity|
248 time_entry_activity.destroy
249 time_entry_activity.destroy(time_entry_activity.parent)
249 end
250 end
250 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
251 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
251 end
252 end
252
253
253 def list_files
254 def list_files
254 sort_init 'filename', 'asc'
255 sort_init 'filename', 'asc'
255 sort_update 'filename' => "#{Attachment.table_name}.filename",
256 sort_update 'filename' => "#{Attachment.table_name}.filename",
256 'created_on' => "#{Attachment.table_name}.created_on",
257 'created_on' => "#{Attachment.table_name}.created_on",
257 'size' => "#{Attachment.table_name}.filesize",
258 'size' => "#{Attachment.table_name}.filesize",
258 'downloads' => "#{Attachment.table_name}.downloads"
259 'downloads' => "#{Attachment.table_name}.downloads"
259
260
260 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
261 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
261 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
262 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
262 render :layout => !request.xhr?
263 render :layout => !request.xhr?
263 end
264 end
264
265
265 # Show changelog for @project
266 # Show changelog for @project
266 def changelog
267 def changelog
267 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
268 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
268 retrieve_selected_tracker_ids(@trackers)
269 retrieve_selected_tracker_ids(@trackers)
269 @versions = @project.versions.sort
270 @versions = @project.versions.sort
270 end
271 end
271
272
272 def roadmap
273 def roadmap
273 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
274 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
274 retrieve_selected_tracker_ids(@trackers)
275 retrieve_selected_tracker_ids(@trackers)
275 @versions = @project.versions.sort
276 @versions = @project.versions.sort
276 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
277 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
277 end
278 end
278
279
279 def activity
280 def activity
280 @days = Setting.activity_days_default.to_i
281 @days = Setting.activity_days_default.to_i
281
282
282 if params[:from]
283 if params[:from]
283 begin; @date_to = params[:from].to_date + 1; rescue; end
284 begin; @date_to = params[:from].to_date + 1; rescue; end
284 end
285 end
285
286
286 @date_to ||= Date.today + 1
287 @date_to ||= Date.today + 1
287 @date_from = @date_to - @days
288 @date_from = @date_to - @days
288 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
289 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
289 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
290 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
290
291
291 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
292 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
292 :with_subprojects => @with_subprojects,
293 :with_subprojects => @with_subprojects,
293 :author => @author)
294 :author => @author)
294 @activity.scope_select {|t| !params["show_#{t}"].nil?}
295 @activity.scope_select {|t| !params["show_#{t}"].nil?}
295 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
296 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
296
297
297 events = @activity.events(@date_from, @date_to)
298 events = @activity.events(@date_from, @date_to)
298
299
299 respond_to do |format|
300 respond_to do |format|
300 format.html {
301 format.html {
301 @events_by_day = events.group_by(&:event_date)
302 @events_by_day = events.group_by(&:event_date)
302 render :layout => false if request.xhr?
303 render :layout => false if request.xhr?
303 }
304 }
304 format.atom {
305 format.atom {
305 title = l(:label_activity)
306 title = l(:label_activity)
306 if @author
307 if @author
307 title = @author.name
308 title = @author.name
308 elsif @activity.scope.size == 1
309 elsif @activity.scope.size == 1
309 title = l("label_#{@activity.scope.first.singularize}_plural")
310 title = l("label_#{@activity.scope.first.singularize}_plural")
310 end
311 end
311 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
312 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
312 }
313 }
313 end
314 end
314
315
315 rescue ActiveRecord::RecordNotFound
316 rescue ActiveRecord::RecordNotFound
316 render_404
317 render_404
317 end
318 end
318
319
319 private
320 private
320 # Find project of id params[:id]
321 # Find project of id params[:id]
321 # if not found, redirect to project list
322 # if not found, redirect to project list
322 # Used as a before_filter
323 # Used as a before_filter
323 def find_project
324 def find_project
324 @project = Project.find(params[:id])
325 @project = Project.find(params[:id])
325 rescue ActiveRecord::RecordNotFound
326 rescue ActiveRecord::RecordNotFound
326 render_404
327 render_404
327 end
328 end
328
329
329 def find_optional_project
330 def find_optional_project
330 return true unless params[:id]
331 return true unless params[:id]
331 @project = Project.find(params[:id])
332 @project = Project.find(params[:id])
332 authorize
333 authorize
333 rescue ActiveRecord::RecordNotFound
334 rescue ActiveRecord::RecordNotFound
334 render_404
335 render_404
335 end
336 end
336
337
337 def retrieve_selected_tracker_ids(selectable_trackers)
338 def retrieve_selected_tracker_ids(selectable_trackers)
338 if ids = params[:tracker_ids]
339 if ids = params[:tracker_ids]
339 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
340 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
340 else
341 else
341 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
342 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
342 end
343 end
343 end
344 end
344 end
345 end
@@ -1,527 +1,538
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 # Specific overidden Activities
23 # Specific overidden Activities
24 has_many :time_entry_activities do
24 has_many :time_entry_activities do
25 def active
25 def active
26 find(:all, :conditions => {:active => true})
26 find(:all, :conditions => {:active => true})
27 end
27 end
28 end
28 end
29 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
29 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :member_principals, :class_name => 'Member',
30 has_many :member_principals, :class_name => 'Member',
31 :include => :principal,
31 :include => :principal,
32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
33 has_many :users, :through => :members
33 has_many :users, :through => :members
34 has_many :principals, :through => :member_principals, :source => :principal
34 has_many :principals, :through => :member_principals, :source => :principal
35
35
36 has_many :enabled_modules, :dependent => :delete_all
36 has_many :enabled_modules, :dependent => :delete_all
37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
39 has_many :issue_changes, :through => :issues, :source => :journals
39 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :time_entries, :dependent => :delete_all
41 has_many :time_entries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all
43 has_many :documents, :dependent => :destroy
43 has_many :documents, :dependent => :destroy
44 has_many :news, :dependent => :delete_all, :include => :author
44 has_many :news, :dependent => :delete_all, :include => :author
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :boards, :dependent => :destroy, :order => "position ASC"
46 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_one :repository, :dependent => :destroy
47 has_one :repository, :dependent => :destroy
48 has_many :changesets, :through => :repository
48 has_many :changesets, :through => :repository
49 has_one :wiki, :dependent => :destroy
49 has_one :wiki, :dependent => :destroy
50 # Custom field for the project issues
50 # Custom field for the project issues
51 has_and_belongs_to_many :issue_custom_fields,
51 has_and_belongs_to_many :issue_custom_fields,
52 :class_name => 'IssueCustomField',
52 :class_name => 'IssueCustomField',
53 :order => "#{CustomField.table_name}.position",
53 :order => "#{CustomField.table_name}.position",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
55 :association_foreign_key => 'custom_field_id'
55 :association_foreign_key => 'custom_field_id'
56
56
57 acts_as_nested_set :order => 'name', :dependent => :destroy
57 acts_as_nested_set :order => 'name', :dependent => :destroy
58 acts_as_attachable :view_permission => :view_files,
58 acts_as_attachable :view_permission => :view_files,
59 :delete_permission => :manage_files
59 :delete_permission => :manage_files
60
60
61 acts_as_customizable
61 acts_as_customizable
62 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
62 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
65 :author => nil
65 :author => nil
66
66
67 attr_protected :status, :enabled_module_names
67 attr_protected :status, :enabled_module_names
68
68
69 validates_presence_of :name, :identifier
69 validates_presence_of :name, :identifier
70 validates_uniqueness_of :name, :identifier
70 validates_uniqueness_of :name, :identifier
71 validates_associated :repository, :wiki
71 validates_associated :repository, :wiki
72 validates_length_of :name, :maximum => 30
72 validates_length_of :name, :maximum => 30
73 validates_length_of :homepage, :maximum => 255
73 validates_length_of :homepage, :maximum => 255
74 validates_length_of :identifier, :in => 1..20
74 validates_length_of :identifier, :in => 1..20
75 # donwcase letters, digits, dashes but not digits only
75 # donwcase letters, digits, dashes but not digits only
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
77 # reserved words
77 # reserved words
78 validates_exclusion_of :identifier, :in => %w( new )
78 validates_exclusion_of :identifier, :in => %w( new )
79
79
80 before_destroy :delete_all_members
80 before_destroy :delete_all_members
81
81
82 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] } }
82 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] } }
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
84 named_scope :all_public, { :conditions => { :is_public => true } }
84 named_scope :all_public, { :conditions => { :is_public => true } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
86
86
87 def identifier=(identifier)
87 def identifier=(identifier)
88 super unless identifier_frozen?
88 super unless identifier_frozen?
89 end
89 end
90
90
91 def identifier_frozen?
91 def identifier_frozen?
92 errors[:identifier].nil? && !(new_record? || identifier.blank?)
92 errors[:identifier].nil? && !(new_record? || identifier.blank?)
93 end
93 end
94
94
95 def issues_with_subprojects(include_subprojects=false)
95 def issues_with_subprojects(include_subprojects=false)
96 conditions = nil
96 conditions = nil
97 if include_subprojects
97 if include_subprojects
98 ids = [id] + descendants.collect(&:id)
98 ids = [id] + descendants.collect(&:id)
99 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
99 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
100 end
100 end
101 conditions ||= ["#{Project.table_name}.id = ?", id]
101 conditions ||= ["#{Project.table_name}.id = ?", id]
102 # Quick and dirty fix for Rails 2 compatibility
102 # Quick and dirty fix for Rails 2 compatibility
103 Issue.send(:with_scope, :find => { :conditions => conditions }) do
103 Issue.send(:with_scope, :find => { :conditions => conditions }) do
104 Version.send(:with_scope, :find => { :conditions => conditions }) do
104 Version.send(:with_scope, :find => { :conditions => conditions }) do
105 yield
105 yield
106 end
106 end
107 end
107 end
108 end
108 end
109
109
110 # returns latest created projects
110 # returns latest created projects
111 # non public projects will be returned only if user is a member of those
111 # non public projects will be returned only if user is a member of those
112 def self.latest(user=nil, count=5)
112 def self.latest(user=nil, count=5)
113 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
113 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
114 end
114 end
115
115
116 # Returns a SQL :conditions string used to find all active projects for the specified user.
116 # Returns a SQL :conditions string used to find all active projects for the specified user.
117 #
117 #
118 # Examples:
118 # Examples:
119 # Projects.visible_by(admin) => "projects.status = 1"
119 # Projects.visible_by(admin) => "projects.status = 1"
120 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
120 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
121 def self.visible_by(user=nil)
121 def self.visible_by(user=nil)
122 user ||= User.current
122 user ||= User.current
123 if user && user.admin?
123 if user && user.admin?
124 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
124 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
125 elsif user && user.memberships.any?
125 elsif user && user.memberships.any?
126 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
126 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
127 else
127 else
128 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
128 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
129 end
129 end
130 end
130 end
131
131
132 def self.allowed_to_condition(user, permission, options={})
132 def self.allowed_to_condition(user, permission, options={})
133 statements = []
133 statements = []
134 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
134 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
135 if perm = Redmine::AccessControl.permission(permission)
135 if perm = Redmine::AccessControl.permission(permission)
136 unless perm.project_module.nil?
136 unless perm.project_module.nil?
137 # If the permission belongs to a project module, make sure the module is enabled
137 # If the permission belongs to a project module, make sure the module is enabled
138 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
138 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
139 end
139 end
140 end
140 end
141 if options[:project]
141 if options[:project]
142 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
142 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
143 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
143 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
144 base_statement = "(#{project_statement}) AND (#{base_statement})"
144 base_statement = "(#{project_statement}) AND (#{base_statement})"
145 end
145 end
146 if user.admin?
146 if user.admin?
147 # no restriction
147 # no restriction
148 else
148 else
149 statements << "1=0"
149 statements << "1=0"
150 if user.logged?
150 if user.logged?
151 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
151 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
152 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
152 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
153 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
153 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
154 elsif Role.anonymous.allowed_to?(permission)
154 elsif Role.anonymous.allowed_to?(permission)
155 # anonymous user allowed on public project
155 # anonymous user allowed on public project
156 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
156 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 else
157 else
158 # anonymous user is not authorized
158 # anonymous user is not authorized
159 end
159 end
160 end
160 end
161 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
161 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
162 end
162 end
163
163
164 # Returns the Systemwide and project specific activities
164 # Returns the Systemwide and project specific activities
165 def activities(include_inactive=false)
165 def activities(include_inactive=false)
166 if include_inactive
166 if include_inactive
167 return all_activities
167 return all_activities
168 else
168 else
169 return active_activities
169 return active_activities
170 end
170 end
171 end
171 end
172
172
173 # Will build a new Project specific Activity or update an existing one
173 # Will create a new Project specific Activity or update an existing one
174 def update_or_build_time_entry_activity(id, activity_hash)
174 #
175 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
176 # does not successfully save.
177 def update_or_create_time_entry_activity(id, activity_hash)
175 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
178 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
176 self.build_time_entry_activity_if_needed(activity_hash)
179 self.create_time_entry_activity_if_needed(activity_hash)
177 else
180 else
178 activity = project.time_entry_activities.find_by_id(id.to_i)
181 activity = project.time_entry_activities.find_by_id(id.to_i)
179 activity.update_attributes(activity_hash) if activity
182 activity.update_attributes(activity_hash) if activity
180 end
183 end
181 end
184 end
182
185
183 # Builds new activity
186 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
184 def build_time_entry_activity_if_needed(activity)
187 #
185 # Only new override activities are built
188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
189 # does not successfully save.
190 def create_time_entry_activity_if_needed(activity)
186 if activity['parent_id']
191 if activity['parent_id']
187
192
188 parent_activity = TimeEntryActivity.find(activity['parent_id'])
193 parent_activity = TimeEntryActivity.find(activity['parent_id'])
189 activity['name'] = parent_activity.name
194 activity['name'] = parent_activity.name
190 activity['position'] = parent_activity.position
195 activity['position'] = parent_activity.position
191
196
192 if Enumeration.overridding_change?(activity, parent_activity)
197 if Enumeration.overridding_change?(activity, parent_activity)
193 self.time_entry_activities.build(activity)
198 project_activity = self.time_entry_activities.create(activity)
199
200 if project_activity.new_record?
201 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
202 else
203 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
204 end
194 end
205 end
195 end
206 end
196 end
207 end
197
208
198 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
209 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
199 #
210 #
200 # Examples:
211 # Examples:
201 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
212 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
202 # project.project_condition(false) => "projects.id = 1"
213 # project.project_condition(false) => "projects.id = 1"
203 def project_condition(with_subprojects)
214 def project_condition(with_subprojects)
204 cond = "#{Project.table_name}.id = #{id}"
215 cond = "#{Project.table_name}.id = #{id}"
205 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
216 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
206 cond
217 cond
207 end
218 end
208
219
209 def self.find(*args)
220 def self.find(*args)
210 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
221 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
211 project = find_by_identifier(*args)
222 project = find_by_identifier(*args)
212 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
223 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
213 project
224 project
214 else
225 else
215 super
226 super
216 end
227 end
217 end
228 end
218
229
219 def to_param
230 def to_param
220 # id is used for projects with a numeric identifier (compatibility)
231 # id is used for projects with a numeric identifier (compatibility)
221 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
232 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
222 end
233 end
223
234
224 def active?
235 def active?
225 self.status == STATUS_ACTIVE
236 self.status == STATUS_ACTIVE
226 end
237 end
227
238
228 # Archives the project and its descendants recursively
239 # Archives the project and its descendants recursively
229 def archive
240 def archive
230 # Archive subprojects if any
241 # Archive subprojects if any
231 children.each do |subproject|
242 children.each do |subproject|
232 subproject.archive
243 subproject.archive
233 end
244 end
234 update_attribute :status, STATUS_ARCHIVED
245 update_attribute :status, STATUS_ARCHIVED
235 end
246 end
236
247
237 # Unarchives the project
248 # Unarchives the project
238 # All its ancestors must be active
249 # All its ancestors must be active
239 def unarchive
250 def unarchive
240 return false if ancestors.detect {|a| !a.active?}
251 return false if ancestors.detect {|a| !a.active?}
241 update_attribute :status, STATUS_ACTIVE
252 update_attribute :status, STATUS_ACTIVE
242 end
253 end
243
254
244 # Returns an array of projects the project can be moved to
255 # Returns an array of projects the project can be moved to
245 def possible_parents
256 def possible_parents
246 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
257 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
247 end
258 end
248
259
249 # Sets the parent of the project
260 # Sets the parent of the project
250 # Argument can be either a Project, a String, a Fixnum or nil
261 # Argument can be either a Project, a String, a Fixnum or nil
251 def set_parent!(p)
262 def set_parent!(p)
252 unless p.nil? || p.is_a?(Project)
263 unless p.nil? || p.is_a?(Project)
253 if p.to_s.blank?
264 if p.to_s.blank?
254 p = nil
265 p = nil
255 else
266 else
256 p = Project.find_by_id(p)
267 p = Project.find_by_id(p)
257 return false unless p
268 return false unless p
258 end
269 end
259 end
270 end
260 if p == parent && !p.nil?
271 if p == parent && !p.nil?
261 # Nothing to do
272 # Nothing to do
262 true
273 true
263 elsif p.nil? || (p.active? && move_possible?(p))
274 elsif p.nil? || (p.active? && move_possible?(p))
264 # Insert the project so that target's children or root projects stay alphabetically sorted
275 # Insert the project so that target's children or root projects stay alphabetically sorted
265 sibs = (p.nil? ? self.class.roots : p.children)
276 sibs = (p.nil? ? self.class.roots : p.children)
266 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
277 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
267 if to_be_inserted_before
278 if to_be_inserted_before
268 move_to_left_of(to_be_inserted_before)
279 move_to_left_of(to_be_inserted_before)
269 elsif p.nil?
280 elsif p.nil?
270 if sibs.empty?
281 if sibs.empty?
271 # move_to_root adds the project in first (ie. left) position
282 # move_to_root adds the project in first (ie. left) position
272 move_to_root
283 move_to_root
273 else
284 else
274 move_to_right_of(sibs.last) unless self == sibs.last
285 move_to_right_of(sibs.last) unless self == sibs.last
275 end
286 end
276 else
287 else
277 # move_to_child_of adds the project in last (ie.right) position
288 # move_to_child_of adds the project in last (ie.right) position
278 move_to_child_of(p)
289 move_to_child_of(p)
279 end
290 end
280 true
291 true
281 else
292 else
282 # Can not move to the given target
293 # Can not move to the given target
283 false
294 false
284 end
295 end
285 end
296 end
286
297
287 # Returns an array of the trackers used by the project and its active sub projects
298 # Returns an array of the trackers used by the project and its active sub projects
288 def rolled_up_trackers
299 def rolled_up_trackers
289 @rolled_up_trackers ||=
300 @rolled_up_trackers ||=
290 Tracker.find(:all, :include => :projects,
301 Tracker.find(:all, :include => :projects,
291 :select => "DISTINCT #{Tracker.table_name}.*",
302 :select => "DISTINCT #{Tracker.table_name}.*",
292 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
303 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
293 :order => "#{Tracker.table_name}.position")
304 :order => "#{Tracker.table_name}.position")
294 end
305 end
295
306
296 # Returns a hash of project users grouped by role
307 # Returns a hash of project users grouped by role
297 def users_by_role
308 def users_by_role
298 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
309 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
299 m.roles.each do |r|
310 m.roles.each do |r|
300 h[r] ||= []
311 h[r] ||= []
301 h[r] << m.user
312 h[r] << m.user
302 end
313 end
303 h
314 h
304 end
315 end
305 end
316 end
306
317
307 # Deletes all project's members
318 # Deletes all project's members
308 def delete_all_members
319 def delete_all_members
309 me, mr = Member.table_name, MemberRole.table_name
320 me, mr = Member.table_name, MemberRole.table_name
310 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
321 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
311 Member.delete_all(['project_id = ?', id])
322 Member.delete_all(['project_id = ?', id])
312 end
323 end
313
324
314 # Users issues can be assigned to
325 # Users issues can be assigned to
315 def assignable_users
326 def assignable_users
316 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
327 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
317 end
328 end
318
329
319 # Returns the mail adresses of users that should be always notified on project events
330 # Returns the mail adresses of users that should be always notified on project events
320 def recipients
331 def recipients
321 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
332 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
322 end
333 end
323
334
324 # Returns an array of all custom fields enabled for project issues
335 # Returns an array of all custom fields enabled for project issues
325 # (explictly associated custom fields and custom fields enabled for all projects)
336 # (explictly associated custom fields and custom fields enabled for all projects)
326 def all_issue_custom_fields
337 def all_issue_custom_fields
327 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
338 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
328 end
339 end
329
340
330 def project
341 def project
331 self
342 self
332 end
343 end
333
344
334 def <=>(project)
345 def <=>(project)
335 name.downcase <=> project.name.downcase
346 name.downcase <=> project.name.downcase
336 end
347 end
337
348
338 def to_s
349 def to_s
339 name
350 name
340 end
351 end
341
352
342 # Returns a short description of the projects (first lines)
353 # Returns a short description of the projects (first lines)
343 def short_description(length = 255)
354 def short_description(length = 255)
344 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
355 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
345 end
356 end
346
357
347 # Return true if this project is allowed to do the specified action.
358 # Return true if this project is allowed to do the specified action.
348 # action can be:
359 # action can be:
349 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
360 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
350 # * a permission Symbol (eg. :edit_project)
361 # * a permission Symbol (eg. :edit_project)
351 def allows_to?(action)
362 def allows_to?(action)
352 if action.is_a? Hash
363 if action.is_a? Hash
353 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
364 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
354 else
365 else
355 allowed_permissions.include? action
366 allowed_permissions.include? action
356 end
367 end
357 end
368 end
358
369
359 def module_enabled?(module_name)
370 def module_enabled?(module_name)
360 module_name = module_name.to_s
371 module_name = module_name.to_s
361 enabled_modules.detect {|m| m.name == module_name}
372 enabled_modules.detect {|m| m.name == module_name}
362 end
373 end
363
374
364 def enabled_module_names=(module_names)
375 def enabled_module_names=(module_names)
365 if module_names && module_names.is_a?(Array)
376 if module_names && module_names.is_a?(Array)
366 module_names = module_names.collect(&:to_s)
377 module_names = module_names.collect(&:to_s)
367 # remove disabled modules
378 # remove disabled modules
368 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
379 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
369 # add new modules
380 # add new modules
370 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
381 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
371 else
382 else
372 enabled_modules.clear
383 enabled_modules.clear
373 end
384 end
374 end
385 end
375
386
376 # Returns an auto-generated project identifier based on the last identifier used
387 # Returns an auto-generated project identifier based on the last identifier used
377 def self.next_identifier
388 def self.next_identifier
378 p = Project.find(:first, :order => 'created_on DESC')
389 p = Project.find(:first, :order => 'created_on DESC')
379 p.nil? ? nil : p.identifier.to_s.succ
390 p.nil? ? nil : p.identifier.to_s.succ
380 end
391 end
381
392
382 # Copies and saves the Project instance based on the +project+.
393 # Copies and saves the Project instance based on the +project+.
383 # Will duplicate the source project's:
394 # Will duplicate the source project's:
384 # * Issues
395 # * Issues
385 # * Members
396 # * Members
386 # * Queries
397 # * Queries
387 def copy(project)
398 def copy(project)
388 project = project.is_a?(Project) ? project : Project.find(project)
399 project = project.is_a?(Project) ? project : Project.find(project)
389
400
390 Project.transaction do
401 Project.transaction do
391 # Wikis
402 # Wikis
392 self.wiki = Wiki.new(project.wiki.attributes.dup.except("project_id"))
403 self.wiki = Wiki.new(project.wiki.attributes.dup.except("project_id"))
393 project.wiki.pages.each do |page|
404 project.wiki.pages.each do |page|
394 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("page_id"))
405 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("page_id"))
395 new_wiki_page = WikiPage.new(page.attributes.dup.except("wiki_id"))
406 new_wiki_page = WikiPage.new(page.attributes.dup.except("wiki_id"))
396 new_wiki_page.content = new_wiki_content
407 new_wiki_page.content = new_wiki_content
397
408
398 self.wiki.pages << new_wiki_page
409 self.wiki.pages << new_wiki_page
399 end
410 end
400
411
401 # Versions
412 # Versions
402 project.versions.each do |version|
413 project.versions.each do |version|
403 new_version = Version.new
414 new_version = Version.new
404 new_version.attributes = version.attributes.dup.except("project_id")
415 new_version.attributes = version.attributes.dup.except("project_id")
405 self.versions << new_version
416 self.versions << new_version
406 end
417 end
407
418
408 project.issue_categories.each do |issue_category|
419 project.issue_categories.each do |issue_category|
409 new_issue_category = IssueCategory.new
420 new_issue_category = IssueCategory.new
410 new_issue_category.attributes = issue_category.attributes.dup.except("project_id")
421 new_issue_category.attributes = issue_category.attributes.dup.except("project_id")
411 self.issue_categories << new_issue_category
422 self.issue_categories << new_issue_category
412 end
423 end
413
424
414 # Issues
425 # Issues
415 project.issues.each do |issue|
426 project.issues.each do |issue|
416 new_issue = Issue.new
427 new_issue = Issue.new
417 new_issue.copy_from(issue)
428 new_issue.copy_from(issue)
418 # Reassign fixed_versions by name, since names are unique per
429 # Reassign fixed_versions by name, since names are unique per
419 # project and the versions for self are not yet saved
430 # project and the versions for self are not yet saved
420 if issue.fixed_version
431 if issue.fixed_version
421 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
432 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
422 end
433 end
423 # Reassign the category by name, since names are unique per
434 # Reassign the category by name, since names are unique per
424 # project and the categories for self are not yet saved
435 # project and the categories for self are not yet saved
425 if issue.category
436 if issue.category
426 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
437 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
427 end
438 end
428
439
429 self.issues << new_issue
440 self.issues << new_issue
430 end
441 end
431
442
432 # Members
443 # Members
433 project.members.each do |member|
444 project.members.each do |member|
434 new_member = Member.new
445 new_member = Member.new
435 new_member.attributes = member.attributes.dup.except("project_id")
446 new_member.attributes = member.attributes.dup.except("project_id")
436 new_member.role_ids = member.role_ids.dup
447 new_member.role_ids = member.role_ids.dup
437 new_member.project = self
448 new_member.project = self
438 self.members << new_member
449 self.members << new_member
439 end
450 end
440
451
441 # Queries
452 # Queries
442 project.queries.each do |query|
453 project.queries.each do |query|
443 new_query = Query.new
454 new_query = Query.new
444 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
455 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
445 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
456 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
446 new_query.project = self
457 new_query.project = self
447 self.queries << new_query
458 self.queries << new_query
448 end
459 end
449
460
450 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
461 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
451 self.save
462 self.save
452 end
463 end
453 end
464 end
454
465
455
466
456 # Copies +project+ and returns the new instance. This will not save
467 # Copies +project+ and returns the new instance. This will not save
457 # the copy
468 # the copy
458 def self.copy_from(project)
469 def self.copy_from(project)
459 begin
470 begin
460 project = project.is_a?(Project) ? project : Project.find(project)
471 project = project.is_a?(Project) ? project : Project.find(project)
461 if project
472 if project
462 # clear unique attributes
473 # clear unique attributes
463 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
474 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
464 copy = Project.new(attributes)
475 copy = Project.new(attributes)
465 copy.enabled_modules = project.enabled_modules
476 copy.enabled_modules = project.enabled_modules
466 copy.trackers = project.trackers
477 copy.trackers = project.trackers
467 copy.custom_values = project.custom_values.collect {|v| v.clone}
478 copy.custom_values = project.custom_values.collect {|v| v.clone}
468 copy.issue_custom_fields = project.issue_custom_fields
479 copy.issue_custom_fields = project.issue_custom_fields
469 return copy
480 return copy
470 else
481 else
471 return nil
482 return nil
472 end
483 end
473 rescue ActiveRecord::RecordNotFound
484 rescue ActiveRecord::RecordNotFound
474 return nil
485 return nil
475 end
486 end
476 end
487 end
477
488
478 private
489 private
479 def allowed_permissions
490 def allowed_permissions
480 @allowed_permissions ||= begin
491 @allowed_permissions ||= begin
481 module_names = enabled_modules.collect {|m| m.name}
492 module_names = enabled_modules.collect {|m| m.name}
482 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
493 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
483 end
494 end
484 end
495 end
485
496
486 def allowed_actions
497 def allowed_actions
487 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
498 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
488 end
499 end
489
500
490 # Returns all the active Systemwide and project specific activities
501 # Returns all the active Systemwide and project specific activities
491 def active_activities
502 def active_activities
492 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
503 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
493
504
494 if overridden_activity_ids.empty?
505 if overridden_activity_ids.empty?
495 return TimeEntryActivity.active
506 return TimeEntryActivity.active
496 else
507 else
497 return system_activities_and_project_overrides
508 return system_activities_and_project_overrides
498 end
509 end
499 end
510 end
500
511
501 # Returns all the Systemwide and project specific activities
512 # Returns all the Systemwide and project specific activities
502 # (inactive and active)
513 # (inactive and active)
503 def all_activities
514 def all_activities
504 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
515 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
505
516
506 if overridden_activity_ids.empty?
517 if overridden_activity_ids.empty?
507 return TimeEntryActivity.all
518 return TimeEntryActivity.all
508 else
519 else
509 return system_activities_and_project_overrides(true)
520 return system_activities_and_project_overrides(true)
510 end
521 end
511 end
522 end
512
523
513 # Returns the systemwide active activities merged with the project specific overrides
524 # Returns the systemwide active activities merged with the project specific overrides
514 def system_activities_and_project_overrides(include_inactive=false)
525 def system_activities_and_project_overrides(include_inactive=false)
515 if include_inactive
526 if include_inactive
516 return TimeEntryActivity.all.
527 return TimeEntryActivity.all.
517 find(:all,
528 find(:all,
518 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
529 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
519 self.time_entry_activities
530 self.time_entry_activities
520 else
531 else
521 return TimeEntryActivity.active.
532 return TimeEntryActivity.active.
522 find(:all,
533 find(:all,
523 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
534 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
524 self.time_entry_activities.active
535 self.time_entry_activities.active
525 end
536 end
526 end
537 end
527 end
538 end
@@ -1,58 +1,58
1 ---
1 ---
2 time_entries_001:
2 time_entries_001:
3 created_on: 2007-03-23 12:54:18 +01:00
3 created_on: 2007-03-23 12:54:18 +01:00
4 tweek: 12
4 tweek: 12
5 tmonth: 3
5 tmonth: 3
6 project_id: 1
6 project_id: 1
7 comments: My hours
7 comments: My hours
8 updated_on: 2007-03-23 12:54:18 +01:00
8 updated_on: 2007-03-23 12:54:18 +01:00
9 activity_id: 9
9 activity_id: 9
10 spent_on: 2007-03-23
10 spent_on: 2007-03-23
11 issue_id: 1
11 issue_id: 1
12 id: 1
12 id: 1
13 hours: 4.25
13 hours: 4.25
14 user_id: 2
14 user_id: 2
15 tyear: 2007
15 tyear: 2007
16 time_entries_002:
16 time_entries_002:
17 created_on: 2007-03-23 14:11:04 +01:00
17 created_on: 2007-03-23 14:11:04 +01:00
18 tweek: 11
18 tweek: 11
19 tmonth: 3
19 tmonth: 3
20 project_id: 1
20 project_id: 1
21 comments: ""
21 comments: ""
22 updated_on: 2007-03-23 14:11:04 +01:00
22 updated_on: 2007-03-23 14:11:04 +01:00
23 activity_id: 9
23 activity_id: 9
24 spent_on: 2007-03-12
24 spent_on: 2007-03-12
25 issue_id: 1
25 issue_id: 1
26 id: 2
26 id: 2
27 hours: 150.0
27 hours: 150.0
28 user_id: 1
28 user_id: 1
29 tyear: 2007
29 tyear: 2007
30 time_entries_003:
30 time_entries_003:
31 created_on: 2007-04-21 12:20:48 +02:00
31 created_on: 2007-04-21 12:20:48 +02:00
32 tweek: 16
32 tweek: 16
33 tmonth: 4
33 tmonth: 4
34 project_id: 1
34 project_id: 1
35 comments: ""
35 comments: ""
36 updated_on: 2007-04-21 12:20:48 +02:00
36 updated_on: 2007-04-21 12:20:48 +02:00
37 activity_id: 9
37 activity_id: 9
38 spent_on: 2007-04-21
38 spent_on: 2007-04-21
39 issue_id: 3
39 issue_id: 3
40 id: 3
40 id: 3
41 hours: 1.0
41 hours: 1.0
42 user_id: 1
42 user_id: 1
43 tyear: 2007
43 tyear: 2007
44 time_entries_004:
44 time_entries_004:
45 created_on: 2007-04-22 12:20:48 +02:00
45 created_on: 2007-04-22 12:20:48 +02:00
46 tweek: 16
46 tweek: 16
47 tmonth: 4
47 tmonth: 4
48 project_id: 3
48 project_id: 3
49 comments: Time spent on a subproject
49 comments: Time spent on a subproject
50 updated_on: 2007-04-22 12:20:48 +02:00
50 updated_on: 2007-04-22 12:20:48 +02:00
51 activity_id: 10
51 activity_id: 10
52 spent_on: 2007-04-22
52 spent_on: 2007-04-22
53 issue_id:
53 issue_id:
54 id: 4
54 id: 4
55 hours: 7.65
55 hours: 7.65
56 user_id: 1
56 user_id: 1
57 tyear: 2007
57 tyear: 2007
58 No newline at end of file
58
@@ -1,704 +1,765
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < ActionController::TestCase
24 class ProjectsControllerTest < ActionController::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments, :custom_fields, :custom_values
27 :attachments, :custom_fields, :custom_values, :time_entries
28
28
29 def setup
29 def setup
30 @controller = ProjectsController.new
30 @controller = ProjectsController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 @request.session[:user_id] = nil
33 @request.session[:user_id] = nil
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 end
35 end
36
36
37 def test_index_routing
37 def test_index_routing
38 assert_routing(
38 assert_routing(
39 {:method => :get, :path => '/projects'},
39 {:method => :get, :path => '/projects'},
40 :controller => 'projects', :action => 'index'
40 :controller => 'projects', :action => 'index'
41 )
41 )
42 end
42 end
43
43
44 def test_index
44 def test_index
45 get :index
45 get :index
46 assert_response :success
46 assert_response :success
47 assert_template 'index'
47 assert_template 'index'
48 assert_not_nil assigns(:projects)
48 assert_not_nil assigns(:projects)
49
49
50 assert_tag :ul, :child => {:tag => 'li',
50 assert_tag :ul, :child => {:tag => 'li',
51 :descendant => {:tag => 'a', :content => 'eCookbook'},
51 :descendant => {:tag => 'a', :content => 'eCookbook'},
52 :child => { :tag => 'ul',
52 :child => { :tag => 'ul',
53 :descendant => { :tag => 'a',
53 :descendant => { :tag => 'a',
54 :content => 'Child of private child'
54 :content => 'Child of private child'
55 }
55 }
56 }
56 }
57 }
57 }
58
58
59 assert_no_tag :a, :content => /Private child of eCookbook/
59 assert_no_tag :a, :content => /Private child of eCookbook/
60 end
60 end
61
61
62 def test_index_atom_routing
62 def test_index_atom_routing
63 assert_routing(
63 assert_routing(
64 {:method => :get, :path => '/projects.atom'},
64 {:method => :get, :path => '/projects.atom'},
65 :controller => 'projects', :action => 'index', :format => 'atom'
65 :controller => 'projects', :action => 'index', :format => 'atom'
66 )
66 )
67 end
67 end
68
68
69 def test_index_atom
69 def test_index_atom
70 get :index, :format => 'atom'
70 get :index, :format => 'atom'
71 assert_response :success
71 assert_response :success
72 assert_template 'common/feed.atom.rxml'
72 assert_template 'common/feed.atom.rxml'
73 assert_select 'feed>title', :text => 'Redmine: Latest projects'
73 assert_select 'feed>title', :text => 'Redmine: Latest projects'
74 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
74 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
75 end
75 end
76
76
77 def test_add_routing
77 def test_add_routing
78 assert_routing(
78 assert_routing(
79 {:method => :get, :path => '/projects/new'},
79 {:method => :get, :path => '/projects/new'},
80 :controller => 'projects', :action => 'add'
80 :controller => 'projects', :action => 'add'
81 )
81 )
82 assert_recognizes(
82 assert_recognizes(
83 {:controller => 'projects', :action => 'add'},
83 {:controller => 'projects', :action => 'add'},
84 {:method => :post, :path => '/projects/new'}
84 {:method => :post, :path => '/projects/new'}
85 )
85 )
86 assert_recognizes(
86 assert_recognizes(
87 {:controller => 'projects', :action => 'add'},
87 {:controller => 'projects', :action => 'add'},
88 {:method => :post, :path => '/projects'}
88 {:method => :post, :path => '/projects'}
89 )
89 )
90 end
90 end
91
91
92 def test_get_add
92 def test_get_add
93 @request.session[:user_id] = 1
93 @request.session[:user_id] = 1
94 get :add
94 get :add
95 assert_response :success
95 assert_response :success
96 assert_template 'add'
96 assert_template 'add'
97 end
97 end
98
98
99 def test_get_add_by_non_admin
99 def test_get_add_by_non_admin
100 @request.session[:user_id] = 2
100 @request.session[:user_id] = 2
101 get :add
101 get :add
102 assert_response :success
102 assert_response :success
103 assert_template 'add'
103 assert_template 'add'
104 end
104 end
105
105
106 def test_post_add
106 def test_post_add
107 @request.session[:user_id] = 1
107 @request.session[:user_id] = 1
108 post :add, :project => { :name => "blog",
108 post :add, :project => { :name => "blog",
109 :description => "weblog",
109 :description => "weblog",
110 :identifier => "blog",
110 :identifier => "blog",
111 :is_public => 1,
111 :is_public => 1,
112 :custom_field_values => { '3' => 'Beta' }
112 :custom_field_values => { '3' => 'Beta' }
113 }
113 }
114 assert_redirected_to '/projects/blog/settings'
114 assert_redirected_to '/projects/blog/settings'
115
115
116 project = Project.find_by_name('blog')
116 project = Project.find_by_name('blog')
117 assert_kind_of Project, project
117 assert_kind_of Project, project
118 assert_equal 'weblog', project.description
118 assert_equal 'weblog', project.description
119 assert_equal true, project.is_public?
119 assert_equal true, project.is_public?
120 end
120 end
121
121
122 def test_post_add_by_non_admin
122 def test_post_add_by_non_admin
123 @request.session[:user_id] = 2
123 @request.session[:user_id] = 2
124 post :add, :project => { :name => "blog",
124 post :add, :project => { :name => "blog",
125 :description => "weblog",
125 :description => "weblog",
126 :identifier => "blog",
126 :identifier => "blog",
127 :is_public => 1,
127 :is_public => 1,
128 :custom_field_values => { '3' => 'Beta' }
128 :custom_field_values => { '3' => 'Beta' }
129 }
129 }
130 assert_redirected_to '/projects/blog/settings'
130 assert_redirected_to '/projects/blog/settings'
131
131
132 project = Project.find_by_name('blog')
132 project = Project.find_by_name('blog')
133 assert_kind_of Project, project
133 assert_kind_of Project, project
134 assert_equal 'weblog', project.description
134 assert_equal 'weblog', project.description
135 assert_equal true, project.is_public?
135 assert_equal true, project.is_public?
136
136
137 # User should be added as a project member
137 # User should be added as a project member
138 assert User.find(2).member_of?(project)
138 assert User.find(2).member_of?(project)
139 assert_equal 1, project.members.size
139 assert_equal 1, project.members.size
140 end
140 end
141
141
142 def test_show_routing
142 def test_show_routing
143 assert_routing(
143 assert_routing(
144 {:method => :get, :path => '/projects/test'},
144 {:method => :get, :path => '/projects/test'},
145 :controller => 'projects', :action => 'show', :id => 'test'
145 :controller => 'projects', :action => 'show', :id => 'test'
146 )
146 )
147 end
147 end
148
148
149 def test_show_by_id
149 def test_show_by_id
150 get :show, :id => 1
150 get :show, :id => 1
151 assert_response :success
151 assert_response :success
152 assert_template 'show'
152 assert_template 'show'
153 assert_not_nil assigns(:project)
153 assert_not_nil assigns(:project)
154 end
154 end
155
155
156 def test_show_by_identifier
156 def test_show_by_identifier
157 get :show, :id => 'ecookbook'
157 get :show, :id => 'ecookbook'
158 assert_response :success
158 assert_response :success
159 assert_template 'show'
159 assert_template 'show'
160 assert_not_nil assigns(:project)
160 assert_not_nil assigns(:project)
161 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
161 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
162 end
162 end
163
163
164 def test_show_should_not_fail_when_custom_values_are_nil
164 def test_show_should_not_fail_when_custom_values_are_nil
165 project = Project.find_by_identifier('ecookbook')
165 project = Project.find_by_identifier('ecookbook')
166 project.custom_values.first.update_attribute(:value, nil)
166 project.custom_values.first.update_attribute(:value, nil)
167 get :show, :id => 'ecookbook'
167 get :show, :id => 'ecookbook'
168 assert_response :success
168 assert_response :success
169 assert_template 'show'
169 assert_template 'show'
170 assert_not_nil assigns(:project)
170 assert_not_nil assigns(:project)
171 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
171 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
172 end
172 end
173
173
174 def test_private_subprojects_hidden
174 def test_private_subprojects_hidden
175 get :show, :id => 'ecookbook'
175 get :show, :id => 'ecookbook'
176 assert_response :success
176 assert_response :success
177 assert_template 'show'
177 assert_template 'show'
178 assert_no_tag :tag => 'a', :content => /Private child/
178 assert_no_tag :tag => 'a', :content => /Private child/
179 end
179 end
180
180
181 def test_private_subprojects_visible
181 def test_private_subprojects_visible
182 @request.session[:user_id] = 2 # manager who is a member of the private subproject
182 @request.session[:user_id] = 2 # manager who is a member of the private subproject
183 get :show, :id => 'ecookbook'
183 get :show, :id => 'ecookbook'
184 assert_response :success
184 assert_response :success
185 assert_template 'show'
185 assert_template 'show'
186 assert_tag :tag => 'a', :content => /Private child/
186 assert_tag :tag => 'a', :content => /Private child/
187 end
187 end
188
188
189 def test_settings_routing
189 def test_settings_routing
190 assert_routing(
190 assert_routing(
191 {:method => :get, :path => '/projects/4223/settings'},
191 {:method => :get, :path => '/projects/4223/settings'},
192 :controller => 'projects', :action => 'settings', :id => '4223'
192 :controller => 'projects', :action => 'settings', :id => '4223'
193 )
193 )
194 assert_routing(
194 assert_routing(
195 {:method => :get, :path => '/projects/4223/settings/members'},
195 {:method => :get, :path => '/projects/4223/settings/members'},
196 :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
196 :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
197 )
197 )
198 end
198 end
199
199
200 def test_settings
200 def test_settings
201 @request.session[:user_id] = 2 # manager
201 @request.session[:user_id] = 2 # manager
202 get :settings, :id => 1
202 get :settings, :id => 1
203 assert_response :success
203 assert_response :success
204 assert_template 'settings'
204 assert_template 'settings'
205 end
205 end
206
206
207 def test_edit
207 def test_edit
208 @request.session[:user_id] = 2 # manager
208 @request.session[:user_id] = 2 # manager
209 post :edit, :id => 1, :project => {:name => 'Test changed name',
209 post :edit, :id => 1, :project => {:name => 'Test changed name',
210 :issue_custom_field_ids => ['']}
210 :issue_custom_field_ids => ['']}
211 assert_redirected_to 'projects/ecookbook/settings'
211 assert_redirected_to 'projects/ecookbook/settings'
212 project = Project.find(1)
212 project = Project.find(1)
213 assert_equal 'Test changed name', project.name
213 assert_equal 'Test changed name', project.name
214 end
214 end
215
215
216 def test_add_version_routing
216 def test_add_version_routing
217 assert_routing(
217 assert_routing(
218 {:method => :get, :path => 'projects/64/versions/new'},
218 {:method => :get, :path => 'projects/64/versions/new'},
219 :controller => 'projects', :action => 'add_version', :id => '64'
219 :controller => 'projects', :action => 'add_version', :id => '64'
220 )
220 )
221 assert_routing(
221 assert_routing(
222 #TODO: use PUT
222 #TODO: use PUT
223 {:method => :post, :path => 'projects/64/versions/new'},
223 {:method => :post, :path => 'projects/64/versions/new'},
224 :controller => 'projects', :action => 'add_version', :id => '64'
224 :controller => 'projects', :action => 'add_version', :id => '64'
225 )
225 )
226 end
226 end
227
227
228 def test_add_issue_category_routing
228 def test_add_issue_category_routing
229 assert_routing(
229 assert_routing(
230 {:method => :get, :path => 'projects/test/categories/new'},
230 {:method => :get, :path => 'projects/test/categories/new'},
231 :controller => 'projects', :action => 'add_issue_category', :id => 'test'
231 :controller => 'projects', :action => 'add_issue_category', :id => 'test'
232 )
232 )
233 assert_routing(
233 assert_routing(
234 #TODO: use PUT and update form
234 #TODO: use PUT and update form
235 {:method => :post, :path => 'projects/64/categories/new'},
235 {:method => :post, :path => 'projects/64/categories/new'},
236 :controller => 'projects', :action => 'add_issue_category', :id => '64'
236 :controller => 'projects', :action => 'add_issue_category', :id => '64'
237 )
237 )
238 end
238 end
239
239
240 def test_destroy_routing
240 def test_destroy_routing
241 assert_routing(
241 assert_routing(
242 {:method => :get, :path => '/projects/567/destroy'},
242 {:method => :get, :path => '/projects/567/destroy'},
243 :controller => 'projects', :action => 'destroy', :id => '567'
243 :controller => 'projects', :action => 'destroy', :id => '567'
244 )
244 )
245 assert_routing(
245 assert_routing(
246 #TODO: use DELETE and update form
246 #TODO: use DELETE and update form
247 {:method => :post, :path => 'projects/64/destroy'},
247 {:method => :post, :path => 'projects/64/destroy'},
248 :controller => 'projects', :action => 'destroy', :id => '64'
248 :controller => 'projects', :action => 'destroy', :id => '64'
249 )
249 )
250 end
250 end
251
251
252 def test_get_destroy
252 def test_get_destroy
253 @request.session[:user_id] = 1 # admin
253 @request.session[:user_id] = 1 # admin
254 get :destroy, :id => 1
254 get :destroy, :id => 1
255 assert_response :success
255 assert_response :success
256 assert_template 'destroy'
256 assert_template 'destroy'
257 assert_not_nil Project.find_by_id(1)
257 assert_not_nil Project.find_by_id(1)
258 end
258 end
259
259
260 def test_post_destroy
260 def test_post_destroy
261 @request.session[:user_id] = 1 # admin
261 @request.session[:user_id] = 1 # admin
262 post :destroy, :id => 1, :confirm => 1
262 post :destroy, :id => 1, :confirm => 1
263 assert_redirected_to 'admin/projects'
263 assert_redirected_to 'admin/projects'
264 assert_nil Project.find_by_id(1)
264 assert_nil Project.find_by_id(1)
265 end
265 end
266
266
267 def test_add_file
267 def test_add_file
268 set_tmp_attachments_directory
268 set_tmp_attachments_directory
269 @request.session[:user_id] = 2
269 @request.session[:user_id] = 2
270 Setting.notified_events = ['file_added']
270 Setting.notified_events = ['file_added']
271 ActionMailer::Base.deliveries.clear
271 ActionMailer::Base.deliveries.clear
272
272
273 assert_difference 'Attachment.count' do
273 assert_difference 'Attachment.count' do
274 post :add_file, :id => 1, :version_id => '',
274 post :add_file, :id => 1, :version_id => '',
275 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
275 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
276 end
276 end
277 assert_redirected_to 'projects/ecookbook/files'
277 assert_redirected_to 'projects/ecookbook/files'
278 a = Attachment.find(:first, :order => 'created_on DESC')
278 a = Attachment.find(:first, :order => 'created_on DESC')
279 assert_equal 'testfile.txt', a.filename
279 assert_equal 'testfile.txt', a.filename
280 assert_equal Project.find(1), a.container
280 assert_equal Project.find(1), a.container
281
281
282 mail = ActionMailer::Base.deliveries.last
282 mail = ActionMailer::Base.deliveries.last
283 assert_kind_of TMail::Mail, mail
283 assert_kind_of TMail::Mail, mail
284 assert_equal "[eCookbook] New file", mail.subject
284 assert_equal "[eCookbook] New file", mail.subject
285 assert mail.body.include?('testfile.txt')
285 assert mail.body.include?('testfile.txt')
286 end
286 end
287
287
288 def test_add_file_routing
288 def test_add_file_routing
289 assert_routing(
289 assert_routing(
290 {:method => :get, :path => '/projects/33/files/new'},
290 {:method => :get, :path => '/projects/33/files/new'},
291 :controller => 'projects', :action => 'add_file', :id => '33'
291 :controller => 'projects', :action => 'add_file', :id => '33'
292 )
292 )
293 assert_routing(
293 assert_routing(
294 {:method => :post, :path => '/projects/33/files/new'},
294 {:method => :post, :path => '/projects/33/files/new'},
295 :controller => 'projects', :action => 'add_file', :id => '33'
295 :controller => 'projects', :action => 'add_file', :id => '33'
296 )
296 )
297 end
297 end
298
298
299 def test_add_version_file
299 def test_add_version_file
300 set_tmp_attachments_directory
300 set_tmp_attachments_directory
301 @request.session[:user_id] = 2
301 @request.session[:user_id] = 2
302 Setting.notified_events = ['file_added']
302 Setting.notified_events = ['file_added']
303
303
304 assert_difference 'Attachment.count' do
304 assert_difference 'Attachment.count' do
305 post :add_file, :id => 1, :version_id => '2',
305 post :add_file, :id => 1, :version_id => '2',
306 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
306 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
307 end
307 end
308 assert_redirected_to 'projects/ecookbook/files'
308 assert_redirected_to 'projects/ecookbook/files'
309 a = Attachment.find(:first, :order => 'created_on DESC')
309 a = Attachment.find(:first, :order => 'created_on DESC')
310 assert_equal 'testfile.txt', a.filename
310 assert_equal 'testfile.txt', a.filename
311 assert_equal Version.find(2), a.container
311 assert_equal Version.find(2), a.container
312 end
312 end
313
313
314 def test_list_files
314 def test_list_files
315 get :list_files, :id => 1
315 get :list_files, :id => 1
316 assert_response :success
316 assert_response :success
317 assert_template 'list_files'
317 assert_template 'list_files'
318 assert_not_nil assigns(:containers)
318 assert_not_nil assigns(:containers)
319
319
320 # file attached to the project
320 # file attached to the project
321 assert_tag :a, :content => 'project_file.zip',
321 assert_tag :a, :content => 'project_file.zip',
322 :attributes => { :href => '/attachments/download/8/project_file.zip' }
322 :attributes => { :href => '/attachments/download/8/project_file.zip' }
323
323
324 # file attached to a project's version
324 # file attached to a project's version
325 assert_tag :a, :content => 'version_file.zip',
325 assert_tag :a, :content => 'version_file.zip',
326 :attributes => { :href => '/attachments/download/9/version_file.zip' }
326 :attributes => { :href => '/attachments/download/9/version_file.zip' }
327 end
327 end
328
328
329 def test_list_files_routing
329 def test_list_files_routing
330 assert_routing(
330 assert_routing(
331 {:method => :get, :path => '/projects/33/files'},
331 {:method => :get, :path => '/projects/33/files'},
332 :controller => 'projects', :action => 'list_files', :id => '33'
332 :controller => 'projects', :action => 'list_files', :id => '33'
333 )
333 )
334 end
334 end
335
335
336 def test_changelog_routing
336 def test_changelog_routing
337 assert_routing(
337 assert_routing(
338 {:method => :get, :path => '/projects/44/changelog'},
338 {:method => :get, :path => '/projects/44/changelog'},
339 :controller => 'projects', :action => 'changelog', :id => '44'
339 :controller => 'projects', :action => 'changelog', :id => '44'
340 )
340 )
341 end
341 end
342
342
343 def test_changelog
343 def test_changelog
344 get :changelog, :id => 1
344 get :changelog, :id => 1
345 assert_response :success
345 assert_response :success
346 assert_template 'changelog'
346 assert_template 'changelog'
347 assert_not_nil assigns(:versions)
347 assert_not_nil assigns(:versions)
348 end
348 end
349
349
350 def test_roadmap_routing
350 def test_roadmap_routing
351 assert_routing(
351 assert_routing(
352 {:method => :get, :path => 'projects/33/roadmap'},
352 {:method => :get, :path => 'projects/33/roadmap'},
353 :controller => 'projects', :action => 'roadmap', :id => '33'
353 :controller => 'projects', :action => 'roadmap', :id => '33'
354 )
354 )
355 end
355 end
356
356
357 def test_roadmap
357 def test_roadmap
358 get :roadmap, :id => 1
358 get :roadmap, :id => 1
359 assert_response :success
359 assert_response :success
360 assert_template 'roadmap'
360 assert_template 'roadmap'
361 assert_not_nil assigns(:versions)
361 assert_not_nil assigns(:versions)
362 # Version with no date set appears
362 # Version with no date set appears
363 assert assigns(:versions).include?(Version.find(3))
363 assert assigns(:versions).include?(Version.find(3))
364 # Completed version doesn't appear
364 # Completed version doesn't appear
365 assert !assigns(:versions).include?(Version.find(1))
365 assert !assigns(:versions).include?(Version.find(1))
366 end
366 end
367
367
368 def test_roadmap_with_completed_versions
368 def test_roadmap_with_completed_versions
369 get :roadmap, :id => 1, :completed => 1
369 get :roadmap, :id => 1, :completed => 1
370 assert_response :success
370 assert_response :success
371 assert_template 'roadmap'
371 assert_template 'roadmap'
372 assert_not_nil assigns(:versions)
372 assert_not_nil assigns(:versions)
373 # Version with no date set appears
373 # Version with no date set appears
374 assert assigns(:versions).include?(Version.find(3))
374 assert assigns(:versions).include?(Version.find(3))
375 # Completed version appears
375 # Completed version appears
376 assert assigns(:versions).include?(Version.find(1))
376 assert assigns(:versions).include?(Version.find(1))
377 end
377 end
378
378
379 def test_project_activity_routing
379 def test_project_activity_routing
380 assert_routing(
380 assert_routing(
381 {:method => :get, :path => '/projects/1/activity'},
381 {:method => :get, :path => '/projects/1/activity'},
382 :controller => 'projects', :action => 'activity', :id => '1'
382 :controller => 'projects', :action => 'activity', :id => '1'
383 )
383 )
384 end
384 end
385
385
386 def test_project_activity_atom_routing
386 def test_project_activity_atom_routing
387 assert_routing(
387 assert_routing(
388 {:method => :get, :path => '/projects/1/activity.atom'},
388 {:method => :get, :path => '/projects/1/activity.atom'},
389 :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
389 :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
390 )
390 )
391 end
391 end
392
392
393 def test_project_activity
393 def test_project_activity
394 get :activity, :id => 1, :with_subprojects => 0
394 get :activity, :id => 1, :with_subprojects => 0
395 assert_response :success
395 assert_response :success
396 assert_template 'activity'
396 assert_template 'activity'
397 assert_not_nil assigns(:events_by_day)
397 assert_not_nil assigns(:events_by_day)
398
398
399 assert_tag :tag => "h3",
399 assert_tag :tag => "h3",
400 :content => /#{2.days.ago.to_date.day}/,
400 :content => /#{2.days.ago.to_date.day}/,
401 :sibling => { :tag => "dl",
401 :sibling => { :tag => "dl",
402 :child => { :tag => "dt",
402 :child => { :tag => "dt",
403 :attributes => { :class => /issue-edit/ },
403 :attributes => { :class => /issue-edit/ },
404 :child => { :tag => "a",
404 :child => { :tag => "a",
405 :content => /(#{IssueStatus.find(2).name})/,
405 :content => /(#{IssueStatus.find(2).name})/,
406 }
406 }
407 }
407 }
408 }
408 }
409 end
409 end
410
410
411 def test_previous_project_activity
411 def test_previous_project_activity
412 get :activity, :id => 1, :from => 3.days.ago.to_date
412 get :activity, :id => 1, :from => 3.days.ago.to_date
413 assert_response :success
413 assert_response :success
414 assert_template 'activity'
414 assert_template 'activity'
415 assert_not_nil assigns(:events_by_day)
415 assert_not_nil assigns(:events_by_day)
416
416
417 assert_tag :tag => "h3",
417 assert_tag :tag => "h3",
418 :content => /#{3.day.ago.to_date.day}/,
418 :content => /#{3.day.ago.to_date.day}/,
419 :sibling => { :tag => "dl",
419 :sibling => { :tag => "dl",
420 :child => { :tag => "dt",
420 :child => { :tag => "dt",
421 :attributes => { :class => /issue/ },
421 :attributes => { :class => /issue/ },
422 :child => { :tag => "a",
422 :child => { :tag => "a",
423 :content => /#{Issue.find(1).subject}/,
423 :content => /#{Issue.find(1).subject}/,
424 }
424 }
425 }
425 }
426 }
426 }
427 end
427 end
428
428
429 def test_global_activity_routing
429 def test_global_activity_routing
430 assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity', :id => nil)
430 assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity', :id => nil)
431 end
431 end
432
432
433 def test_global_activity
433 def test_global_activity
434 get :activity
434 get :activity
435 assert_response :success
435 assert_response :success
436 assert_template 'activity'
436 assert_template 'activity'
437 assert_not_nil assigns(:events_by_day)
437 assert_not_nil assigns(:events_by_day)
438
438
439 assert_tag :tag => "h3",
439 assert_tag :tag => "h3",
440 :content => /#{5.day.ago.to_date.day}/,
440 :content => /#{5.day.ago.to_date.day}/,
441 :sibling => { :tag => "dl",
441 :sibling => { :tag => "dl",
442 :child => { :tag => "dt",
442 :child => { :tag => "dt",
443 :attributes => { :class => /issue/ },
443 :attributes => { :class => /issue/ },
444 :child => { :tag => "a",
444 :child => { :tag => "a",
445 :content => /#{Issue.find(5).subject}/,
445 :content => /#{Issue.find(5).subject}/,
446 }
446 }
447 }
447 }
448 }
448 }
449 end
449 end
450
450
451 def test_user_activity
451 def test_user_activity
452 get :activity, :user_id => 2
452 get :activity, :user_id => 2
453 assert_response :success
453 assert_response :success
454 assert_template 'activity'
454 assert_template 'activity'
455 assert_not_nil assigns(:events_by_day)
455 assert_not_nil assigns(:events_by_day)
456
456
457 assert_tag :tag => "h3",
457 assert_tag :tag => "h3",
458 :content => /#{3.day.ago.to_date.day}/,
458 :content => /#{3.day.ago.to_date.day}/,
459 :sibling => { :tag => "dl",
459 :sibling => { :tag => "dl",
460 :child => { :tag => "dt",
460 :child => { :tag => "dt",
461 :attributes => { :class => /issue/ },
461 :attributes => { :class => /issue/ },
462 :child => { :tag => "a",
462 :child => { :tag => "a",
463 :content => /#{Issue.find(1).subject}/,
463 :content => /#{Issue.find(1).subject}/,
464 }
464 }
465 }
465 }
466 }
466 }
467 end
467 end
468
468
469 def test_global_activity_atom_routing
469 def test_global_activity_atom_routing
470 assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :id => nil, :format => 'atom')
470 assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :id => nil, :format => 'atom')
471 end
471 end
472
472
473 def test_activity_atom_feed
473 def test_activity_atom_feed
474 get :activity, :format => 'atom'
474 get :activity, :format => 'atom'
475 assert_response :success
475 assert_response :success
476 assert_template 'common/feed.atom.rxml'
476 assert_template 'common/feed.atom.rxml'
477 end
477 end
478
478
479 def test_archive_routing
479 def test_archive_routing
480 assert_routing(
480 assert_routing(
481 #TODO: use PUT to project path and modify form
481 #TODO: use PUT to project path and modify form
482 {:method => :post, :path => 'projects/64/archive'},
482 {:method => :post, :path => 'projects/64/archive'},
483 :controller => 'projects', :action => 'archive', :id => '64'
483 :controller => 'projects', :action => 'archive', :id => '64'
484 )
484 )
485 end
485 end
486
486
487 def test_archive
487 def test_archive
488 @request.session[:user_id] = 1 # admin
488 @request.session[:user_id] = 1 # admin
489 post :archive, :id => 1
489 post :archive, :id => 1
490 assert_redirected_to 'admin/projects'
490 assert_redirected_to 'admin/projects'
491 assert !Project.find(1).active?
491 assert !Project.find(1).active?
492 end
492 end
493
493
494 def test_unarchive_routing
494 def test_unarchive_routing
495 assert_routing(
495 assert_routing(
496 #TODO: use PUT to project path and modify form
496 #TODO: use PUT to project path and modify form
497 {:method => :post, :path => '/projects/567/unarchive'},
497 {:method => :post, :path => '/projects/567/unarchive'},
498 :controller => 'projects', :action => 'unarchive', :id => '567'
498 :controller => 'projects', :action => 'unarchive', :id => '567'
499 )
499 )
500 end
500 end
501
501
502 def test_unarchive
502 def test_unarchive
503 @request.session[:user_id] = 1 # admin
503 @request.session[:user_id] = 1 # admin
504 Project.find(1).archive
504 Project.find(1).archive
505 post :unarchive, :id => 1
505 post :unarchive, :id => 1
506 assert_redirected_to 'admin/projects'
506 assert_redirected_to 'admin/projects'
507 assert Project.find(1).active?
507 assert Project.find(1).active?
508 end
508 end
509
509
510 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
510 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
511 CustomField.delete_all
511 CustomField.delete_all
512 parent = nil
512 parent = nil
513 6.times do |i|
513 6.times do |i|
514 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
514 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
515 p.set_parent!(parent)
515 p.set_parent!(parent)
516 get :show, :id => p
516 get :show, :id => p
517 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
517 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
518 :children => { :count => [i, 3].min,
518 :children => { :count => [i, 3].min,
519 :only => { :tag => 'a' } }
519 :only => { :tag => 'a' } }
520
520
521 parent = p
521 parent = p
522 end
522 end
523 end
523 end
524
524
525 def test_copy_with_project
525 def test_copy_with_project
526 @request.session[:user_id] = 1 # admin
526 @request.session[:user_id] = 1 # admin
527 get :copy, :id => 1
527 get :copy, :id => 1
528 assert_response :success
528 assert_response :success
529 assert_template 'copy'
529 assert_template 'copy'
530 assert assigns(:project)
530 assert assigns(:project)
531 assert_equal Project.find(1).description, assigns(:project).description
531 assert_equal Project.find(1).description, assigns(:project).description
532 assert_nil assigns(:project).id
532 assert_nil assigns(:project).id
533 end
533 end
534
534
535 def test_copy_without_project
535 def test_copy_without_project
536 @request.session[:user_id] = 1 # admin
536 @request.session[:user_id] = 1 # admin
537 get :copy
537 get :copy
538 assert_response :redirect
538 assert_response :redirect
539 assert_redirected_to :controller => 'admin', :action => 'projects'
539 assert_redirected_to :controller => 'admin', :action => 'projects'
540 end
540 end
541
541
542 def test_jump_should_redirect_to_active_tab
542 def test_jump_should_redirect_to_active_tab
543 get :show, :id => 1, :jump => 'issues'
543 get :show, :id => 1, :jump => 'issues'
544 assert_redirected_to 'projects/ecookbook/issues'
544 assert_redirected_to 'projects/ecookbook/issues'
545 end
545 end
546
546
547 def test_jump_should_not_redirect_to_inactive_tab
547 def test_jump_should_not_redirect_to_inactive_tab
548 get :show, :id => 3, :jump => 'documents'
548 get :show, :id => 3, :jump => 'documents'
549 assert_response :success
549 assert_response :success
550 assert_template 'show'
550 assert_template 'show'
551 end
551 end
552
552
553 def test_jump_should_not_redirect_to_unknown_tab
553 def test_jump_should_not_redirect_to_unknown_tab
554 get :show, :id => 3, :jump => 'foobar'
554 get :show, :id => 3, :jump => 'foobar'
555 assert_response :success
555 assert_response :success
556 assert_template 'show'
556 assert_template 'show'
557 end
557 end
558
558
559 def test_reset_activities_routing
559 def test_reset_activities_routing
560 assert_routing({:method => :delete, :path => 'projects/64/reset_activities'},
560 assert_routing({:method => :delete, :path => 'projects/64/reset_activities'},
561 :controller => 'projects', :action => 'reset_activities', :id => '64')
561 :controller => 'projects', :action => 'reset_activities', :id => '64')
562 end
562 end
563
563
564 def test_reset_activities
564 def test_reset_activities
565 @request.session[:user_id] = 2 # manager
565 @request.session[:user_id] = 2 # manager
566 project_activity = TimeEntryActivity.new({
566 project_activity = TimeEntryActivity.new({
567 :name => 'Project Specific',
567 :name => 'Project Specific',
568 :parent => TimeEntryActivity.find(:first),
568 :parent => TimeEntryActivity.find(:first),
569 :project => Project.find(1),
569 :project => Project.find(1),
570 :active => true
570 :active => true
571 })
571 })
572 assert project_activity.save
572 assert project_activity.save
573 project_activity_two = TimeEntryActivity.new({
573 project_activity_two = TimeEntryActivity.new({
574 :name => 'Project Specific Two',
574 :name => 'Project Specific Two',
575 :parent => TimeEntryActivity.find(:last),
575 :parent => TimeEntryActivity.find(:last),
576 :project => Project.find(1),
576 :project => Project.find(1),
577 :active => true
577 :active => true
578 })
578 })
579 assert project_activity_two.save
579 assert project_activity_two.save
580
580
581 delete :reset_activities, :id => 1
581 delete :reset_activities, :id => 1
582 assert_response :redirect
582 assert_response :redirect
583 assert_redirected_to 'projects/ecookbook/settings/activities'
583 assert_redirected_to 'projects/ecookbook/settings/activities'
584
584
585 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
585 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
586 assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
586 assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
587 end
587 end
588
588
589 def test_reset_activities_should_reassign_time_entries_back_to_the_system_activity
590 @request.session[:user_id] = 2 # manager
591 project_activity = TimeEntryActivity.new({
592 :name => 'Project Specific Design',
593 :parent => TimeEntryActivity.find(9),
594 :project => Project.find(1),
595 :active => true
596 })
597 assert project_activity.save
598 assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9])
599 assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size
600
601 delete :reset_activities, :id => 1
602 assert_response :redirect
603 assert_redirected_to 'projects/ecookbook/settings/activities'
604
605 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
606 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity"
607 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity"
608 end
609
589 def test_save_activities_routing
610 def test_save_activities_routing
590 assert_routing({:method => :post, :path => 'projects/64/activities/save'},
611 assert_routing({:method => :post, :path => 'projects/64/activities/save'},
591 :controller => 'projects', :action => 'save_activities', :id => '64')
612 :controller => 'projects', :action => 'save_activities', :id => '64')
592 end
613 end
593
614
594 def test_save_activities_to_override_system_activities
615 def test_save_activities_to_override_system_activities
595 @request.session[:user_id] = 2 # manager
616 @request.session[:user_id] = 2 # manager
596 billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
617 billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
597
618
598 post :save_activities, :id => 1, :enumerations => {
619 post :save_activities, :id => 1, :enumerations => {
599 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
620 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
600 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
621 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
601 "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
622 "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
602 "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
623 "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
603 }
624 }
604
625
605 assert_response :redirect
626 assert_response :redirect
606 assert_redirected_to 'projects/ecookbook/settings/activities'
627 assert_redirected_to 'projects/ecookbook/settings/activities'
607
628
608 # Created project specific activities...
629 # Created project specific activities...
609 project = Project.find('ecookbook')
630 project = Project.find('ecookbook')
610
631
611 # ... Design
632 # ... Design
612 design = project.time_entry_activities.find_by_name("Design")
633 design = project.time_entry_activities.find_by_name("Design")
613 assert design, "Project activity not found"
634 assert design, "Project activity not found"
614
635
615 assert_equal 9, design.parent_id # Relate to the system activity
636 assert_equal 9, design.parent_id # Relate to the system activity
616 assert_not_equal design.parent.id, design.id # Different records
637 assert_not_equal design.parent.id, design.id # Different records
617 assert_equal design.parent.name, design.name # Same name
638 assert_equal design.parent.name, design.name # Same name
618 assert !design.active?
639 assert !design.active?
619
640
620 # ... Development
641 # ... Development
621 development = project.time_entry_activities.find_by_name("Development")
642 development = project.time_entry_activities.find_by_name("Development")
622 assert development, "Project activity not found"
643 assert development, "Project activity not found"
623
644
624 assert_equal 10, development.parent_id # Relate to the system activity
645 assert_equal 10, development.parent_id # Relate to the system activity
625 assert_not_equal development.parent.id, development.id # Different records
646 assert_not_equal development.parent.id, development.id # Different records
626 assert_equal development.parent.name, development.name # Same name
647 assert_equal development.parent.name, development.name # Same name
627 assert development.active?
648 assert development.active?
628 assert_equal "0", development.custom_value_for(billable_field).value
649 assert_equal "0", development.custom_value_for(billable_field).value
629
650
630 # ... Inactive Activity
651 # ... Inactive Activity
631 previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
652 previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
632 assert previously_inactive, "Project activity not found"
653 assert previously_inactive, "Project activity not found"
633
654
634 assert_equal 14, previously_inactive.parent_id # Relate to the system activity
655 assert_equal 14, previously_inactive.parent_id # Relate to the system activity
635 assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
656 assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
636 assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
657 assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
637 assert previously_inactive.active?
658 assert previously_inactive.active?
638 assert_equal "1", previously_inactive.custom_value_for(billable_field).value
659 assert_equal "1", previously_inactive.custom_value_for(billable_field).value
639
660
640 # ... QA
661 # ... QA
641 assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
662 assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
642 end
663 end
643
664
644 def test_save_activities_will_update_project_specific_activities
665 def test_save_activities_will_update_project_specific_activities
645 @request.session[:user_id] = 2 # manager
666 @request.session[:user_id] = 2 # manager
646
667
647 project_activity = TimeEntryActivity.new({
668 project_activity = TimeEntryActivity.new({
648 :name => 'Project Specific',
669 :name => 'Project Specific',
649 :parent => TimeEntryActivity.find(:first),
670 :parent => TimeEntryActivity.find(:first),
650 :project => Project.find(1),
671 :project => Project.find(1),
651 :active => true
672 :active => true
652 })
673 })
653 assert project_activity.save
674 assert project_activity.save
654 project_activity_two = TimeEntryActivity.new({
675 project_activity_two = TimeEntryActivity.new({
655 :name => 'Project Specific Two',
676 :name => 'Project Specific Two',
656 :parent => TimeEntryActivity.find(:last),
677 :parent => TimeEntryActivity.find(:last),
657 :project => Project.find(1),
678 :project => Project.find(1),
658 :active => true
679 :active => true
659 })
680 })
660 assert project_activity_two.save
681 assert project_activity_two.save
661
682
662
683
663 post :save_activities, :id => 1, :enumerations => {
684 post :save_activities, :id => 1, :enumerations => {
664 project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
685 project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
665 project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
686 project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
666 }
687 }
667
688
668 assert_response :redirect
689 assert_response :redirect
669 assert_redirected_to 'projects/ecookbook/settings/activities'
690 assert_redirected_to 'projects/ecookbook/settings/activities'
670
691
671 # Created project specific activities...
692 # Created project specific activities...
672 project = Project.find('ecookbook')
693 project = Project.find('ecookbook')
673 assert_equal 2, project.time_entry_activities.count
694 assert_equal 2, project.time_entry_activities.count
674
695
675 activity_one = project.time_entry_activities.find_by_name(project_activity.name)
696 activity_one = project.time_entry_activities.find_by_name(project_activity.name)
676 assert activity_one, "Project activity not found"
697 assert activity_one, "Project activity not found"
677 assert_equal project_activity.id, activity_one.id
698 assert_equal project_activity.id, activity_one.id
678 assert !activity_one.active?
699 assert !activity_one.active?
679
700
680 activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
701 activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
681 assert activity_two, "Project activity not found"
702 assert activity_two, "Project activity not found"
682 assert_equal project_activity_two.id, activity_two.id
703 assert_equal project_activity_two.id, activity_two.id
683 assert !activity_two.active?
704 assert !activity_two.active?
684 end
705 end
685
706
707 def test_save_activities_when_creating_new_activities_will_convert_existing_data
708 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
709
710 @request.session[:user_id] = 2 # manager
711 post :save_activities, :id => 1, :enumerations => {
712 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate
713 }
714 assert_response :redirect
715
716 # No more TimeEntries using the system activity
717 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities"
718 # All TimeEntries using project activity
719 project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1)
720 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity"
721 end
722
723 def test_save_activities_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
724 # TODO: Need to cause an exception on create but these tests
725 # aren't setup for mocking. Just create a record now so the
726 # second one is a dupicate
727 parent = TimeEntryActivity.find(9)
728 TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true})
729 TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
730
731 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
732 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size
733
734 @request.session[:user_id] = 2 # manager
735 post :save_activities, :id => 1, :enumerations => {
736 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design
737 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value
738 }
739 assert_response :redirect
740
741 # TimeEntries shouldn't have been reassigned on the failed record
742 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities"
743 # TimeEntries shouldn't have been reassigned on the saved record either
744 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities"
745 end
746
686 # A hook that is manually registered later
747 # A hook that is manually registered later
687 class ProjectBasedTemplate < Redmine::Hook::ViewListener
748 class ProjectBasedTemplate < Redmine::Hook::ViewListener
688 def view_layouts_base_html_head(context)
749 def view_layouts_base_html_head(context)
689 # Adds a project stylesheet
750 # Adds a project stylesheet
690 stylesheet_link_tag(context[:project].identifier) if context[:project]
751 stylesheet_link_tag(context[:project].identifier) if context[:project]
691 end
752 end
692 end
753 end
693 # Don't use this hook now
754 # Don't use this hook now
694 Redmine::Hook.clear_listeners
755 Redmine::Hook.clear_listeners
695
756
696 def test_hook_response
757 def test_hook_response
697 Redmine::Hook.add_listener(ProjectBasedTemplate)
758 Redmine::Hook.add_listener(ProjectBasedTemplate)
698 get :show, :id => 1
759 get :show, :id => 1
699 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
760 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
700 :parent => {:tag => 'head'}
761 :parent => {:tag => 'head'}
701
762
702 Redmine::Hook.clear_listeners
763 Redmine::Hook.clear_listeners
703 end
764 end
704 end
765 end
General Comments 0
You need to be logged in to leave comments. Login now