##// END OF EJS Templates
Display all users roles on project overview (#3339)....
Jean-Philippe Lang -
r2635:da2854cf750b
parent child
Show More
@@ -1,319 +1,319
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 :require_admin, :only => [ :add, :copy, :archive, :unarchive, :destroy ]
29 before_filter :require_admin, :only => [ :add, :copy, :archive, :unarchive, :destroy ]
30 accept_key_auth :activity
30 accept_key_auth :activity
31
31
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 if controller.request.post?
33 if controller.request.post?
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 end
35 end
36 end
36 end
37
37
38 helper :sort
38 helper :sort
39 include SortHelper
39 include SortHelper
40 helper :custom_fields
40 helper :custom_fields
41 include CustomFieldsHelper
41 include CustomFieldsHelper
42 helper :issues
42 helper :issues
43 helper IssuesHelper
43 helper IssuesHelper
44 helper :queries
44 helper :queries
45 include QueriesHelper
45 include QueriesHelper
46 helper :repositories
46 helper :repositories
47 include RepositoriesHelper
47 include RepositoriesHelper
48 include ProjectsHelper
48 include ProjectsHelper
49
49
50 # Lists visible projects
50 # Lists visible projects
51 def index
51 def index
52 respond_to do |format|
52 respond_to do |format|
53 format.html {
53 format.html {
54 @projects = Project.visible.find(:all, :order => 'lft')
54 @projects = Project.visible.find(:all, :order => 'lft')
55 }
55 }
56 format.atom {
56 format.atom {
57 projects = Project.visible.find(:all, :order => 'created_on DESC',
57 projects = Project.visible.find(:all, :order => 'created_on DESC',
58 :limit => Setting.feeds_limit.to_i)
58 :limit => Setting.feeds_limit.to_i)
59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 }
60 }
61 end
61 end
62 end
62 end
63
63
64 # Add a new project
64 # Add a new project
65 def add
65 def add
66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 @trackers = Tracker.all
67 @trackers = Tracker.all
68 @project = Project.new(params[:project])
68 @project = Project.new(params[:project])
69 if request.get?
69 if request.get?
70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
71 @project.trackers = Tracker.all
71 @project.trackers = Tracker.all
72 @project.is_public = Setting.default_projects_public?
72 @project.is_public = Setting.default_projects_public?
73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
74 else
74 else
75 @project.enabled_module_names = params[:enabled_modules]
75 @project.enabled_module_names = params[:enabled_modules]
76 if @project.save
76 if @project.save
77 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
77 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
78 flash[:notice] = l(:notice_successful_create)
78 flash[:notice] = l(:notice_successful_create)
79 redirect_to :controller => 'admin', :action => 'projects'
79 redirect_to :controller => 'admin', :action => 'projects'
80 end
80 end
81 end
81 end
82 end
82 end
83
83
84 def copy
84 def copy
85 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
85 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
86 @trackers = Tracker.all
86 @trackers = Tracker.all
87 @root_projects = Project.find(:all,
87 @root_projects = Project.find(:all,
88 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
88 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
89 :order => 'name')
89 :order => 'name')
90 if request.get?
90 if request.get?
91 @project = Project.copy_from(params[:id])
91 @project = Project.copy_from(params[:id])
92 if @project
92 if @project
93 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
93 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
94 else
94 else
95 redirect_to :controller => 'admin', :action => 'projects'
95 redirect_to :controller => 'admin', :action => 'projects'
96 end
96 end
97 else
97 else
98 @project = Project.new(params[:project])
98 @project = Project.new(params[:project])
99 @project.enabled_module_names = params[:enabled_modules]
99 @project.enabled_module_names = params[:enabled_modules]
100 if @project.copy(params[:id])
100 if @project.copy(params[:id])
101 flash[:notice] = l(:notice_successful_create)
101 flash[:notice] = l(:notice_successful_create)
102 redirect_to :controller => 'admin', :action => 'projects'
102 redirect_to :controller => 'admin', :action => 'projects'
103 end
103 end
104 end
104 end
105 end
105 end
106
106
107
107
108 # Show @project
108 # Show @project
109 def show
109 def show
110 if params[:jump]
110 if params[:jump]
111 # try to redirect to the requested menu item
111 # try to redirect to the requested menu item
112 redirect_to_project_menu_item(@project, params[:jump]) && return
112 redirect_to_project_menu_item(@project, params[:jump]) && return
113 end
113 end
114
114
115 @members_by_role = @project.members.find(:all, :include => [:user, :roles], :order => 'position').group_by {|m| m.roles.first}
115 @users_by_role = @project.users_by_role
116 @subprojects = @project.children.visible
116 @subprojects = @project.children.visible
117 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
117 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
118 @trackers = @project.rolled_up_trackers
118 @trackers = @project.rolled_up_trackers
119
119
120 cond = @project.project_condition(Setting.display_subprojects_issues?)
120 cond = @project.project_condition(Setting.display_subprojects_issues?)
121
121
122 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
122 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
123 :include => [:project, :status, :tracker],
123 :include => [:project, :status, :tracker],
124 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
124 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
125 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
125 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
126 :include => [:project, :status, :tracker],
126 :include => [:project, :status, :tracker],
127 :conditions => cond)
127 :conditions => cond)
128
128
129 TimeEntry.visible_by(User.current) do
129 TimeEntry.visible_by(User.current) do
130 @total_hours = TimeEntry.sum(:hours,
130 @total_hours = TimeEntry.sum(:hours,
131 :include => :project,
131 :include => :project,
132 :conditions => cond).to_f
132 :conditions => cond).to_f
133 end
133 end
134 @key = User.current.rss_key
134 @key = User.current.rss_key
135 end
135 end
136
136
137 def settings
137 def settings
138 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
138 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
139 @issue_category ||= IssueCategory.new
139 @issue_category ||= IssueCategory.new
140 @member ||= @project.members.new
140 @member ||= @project.members.new
141 @trackers = Tracker.all
141 @trackers = Tracker.all
142 @repository ||= @project.repository
142 @repository ||= @project.repository
143 @wiki ||= @project.wiki
143 @wiki ||= @project.wiki
144 end
144 end
145
145
146 # Edit @project
146 # Edit @project
147 def edit
147 def edit
148 if request.post?
148 if request.post?
149 @project.attributes = params[:project]
149 @project.attributes = params[:project]
150 if @project.save
150 if @project.save
151 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
151 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
152 flash[:notice] = l(:notice_successful_update)
152 flash[:notice] = l(:notice_successful_update)
153 redirect_to :action => 'settings', :id => @project
153 redirect_to :action => 'settings', :id => @project
154 else
154 else
155 settings
155 settings
156 render :action => 'settings'
156 render :action => 'settings'
157 end
157 end
158 end
158 end
159 end
159 end
160
160
161 def modules
161 def modules
162 @project.enabled_module_names = params[:enabled_modules]
162 @project.enabled_module_names = params[:enabled_modules]
163 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
163 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
164 end
164 end
165
165
166 def archive
166 def archive
167 @project.archive if request.post? && @project.active?
167 @project.archive if request.post? && @project.active?
168 redirect_to :controller => 'admin', :action => 'projects'
168 redirect_to :controller => 'admin', :action => 'projects'
169 end
169 end
170
170
171 def unarchive
171 def unarchive
172 @project.unarchive if request.post? && !@project.active?
172 @project.unarchive if request.post? && !@project.active?
173 redirect_to :controller => 'admin', :action => 'projects'
173 redirect_to :controller => 'admin', :action => 'projects'
174 end
174 end
175
175
176 # Delete @project
176 # Delete @project
177 def destroy
177 def destroy
178 @project_to_destroy = @project
178 @project_to_destroy = @project
179 if request.post? and params[:confirm]
179 if request.post? and params[:confirm]
180 @project_to_destroy.destroy
180 @project_to_destroy.destroy
181 redirect_to :controller => 'admin', :action => 'projects'
181 redirect_to :controller => 'admin', :action => 'projects'
182 end
182 end
183 # hide project in layout
183 # hide project in layout
184 @project = nil
184 @project = nil
185 end
185 end
186
186
187 # Add a new issue category to @project
187 # Add a new issue category to @project
188 def add_issue_category
188 def add_issue_category
189 @category = @project.issue_categories.build(params[:category])
189 @category = @project.issue_categories.build(params[:category])
190 if request.post? and @category.save
190 if request.post? and @category.save
191 respond_to do |format|
191 respond_to do |format|
192 format.html do
192 format.html do
193 flash[:notice] = l(:notice_successful_create)
193 flash[:notice] = l(:notice_successful_create)
194 redirect_to :action => 'settings', :tab => 'categories', :id => @project
194 redirect_to :action => 'settings', :tab => 'categories', :id => @project
195 end
195 end
196 format.js do
196 format.js do
197 # IE doesn't support the replace_html rjs method for select box options
197 # IE doesn't support the replace_html rjs method for select box options
198 render(:update) {|page| page.replace "issue_category_id",
198 render(:update) {|page| page.replace "issue_category_id",
199 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]')
199 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]')
200 }
200 }
201 end
201 end
202 end
202 end
203 end
203 end
204 end
204 end
205
205
206 # Add a new version to @project
206 # Add a new version to @project
207 def add_version
207 def add_version
208 @version = @project.versions.build(params[:version])
208 @version = @project.versions.build(params[:version])
209 if request.post? and @version.save
209 if request.post? and @version.save
210 flash[:notice] = l(:notice_successful_create)
210 flash[:notice] = l(:notice_successful_create)
211 redirect_to :action => 'settings', :tab => 'versions', :id => @project
211 redirect_to :action => 'settings', :tab => 'versions', :id => @project
212 end
212 end
213 end
213 end
214
214
215 def add_file
215 def add_file
216 if request.post?
216 if request.post?
217 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
217 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
218 attachments = attach_files(container, params[:attachments])
218 attachments = attach_files(container, params[:attachments])
219 if !attachments.empty? && Setting.notified_events.include?('file_added')
219 if !attachments.empty? && Setting.notified_events.include?('file_added')
220 Mailer.deliver_attachments_added(attachments)
220 Mailer.deliver_attachments_added(attachments)
221 end
221 end
222 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
222 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
223 return
223 return
224 end
224 end
225 @versions = @project.versions.sort
225 @versions = @project.versions.sort
226 end
226 end
227
227
228 def list_files
228 def list_files
229 sort_init 'filename', 'asc'
229 sort_init 'filename', 'asc'
230 sort_update 'filename' => "#{Attachment.table_name}.filename",
230 sort_update 'filename' => "#{Attachment.table_name}.filename",
231 'created_on' => "#{Attachment.table_name}.created_on",
231 'created_on' => "#{Attachment.table_name}.created_on",
232 'size' => "#{Attachment.table_name}.filesize",
232 'size' => "#{Attachment.table_name}.filesize",
233 'downloads' => "#{Attachment.table_name}.downloads"
233 'downloads' => "#{Attachment.table_name}.downloads"
234
234
235 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
235 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
236 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
236 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
237 render :layout => !request.xhr?
237 render :layout => !request.xhr?
238 end
238 end
239
239
240 # Show changelog for @project
240 # Show changelog for @project
241 def changelog
241 def changelog
242 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
242 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
243 retrieve_selected_tracker_ids(@trackers)
243 retrieve_selected_tracker_ids(@trackers)
244 @versions = @project.versions.sort
244 @versions = @project.versions.sort
245 end
245 end
246
246
247 def roadmap
247 def roadmap
248 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
248 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
249 retrieve_selected_tracker_ids(@trackers)
249 retrieve_selected_tracker_ids(@trackers)
250 @versions = @project.versions.sort
250 @versions = @project.versions.sort
251 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
251 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
252 end
252 end
253
253
254 def activity
254 def activity
255 @days = Setting.activity_days_default.to_i
255 @days = Setting.activity_days_default.to_i
256
256
257 if params[:from]
257 if params[:from]
258 begin; @date_to = params[:from].to_date + 1; rescue; end
258 begin; @date_to = params[:from].to_date + 1; rescue; end
259 end
259 end
260
260
261 @date_to ||= Date.today + 1
261 @date_to ||= Date.today + 1
262 @date_from = @date_to - @days
262 @date_from = @date_to - @days
263 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
263 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
264 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
264 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
265
265
266 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
266 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
267 :with_subprojects => @with_subprojects,
267 :with_subprojects => @with_subprojects,
268 :author => @author)
268 :author => @author)
269 @activity.scope_select {|t| !params["show_#{t}"].nil?}
269 @activity.scope_select {|t| !params["show_#{t}"].nil?}
270 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
270 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
271
271
272 events = @activity.events(@date_from, @date_to)
272 events = @activity.events(@date_from, @date_to)
273
273
274 respond_to do |format|
274 respond_to do |format|
275 format.html {
275 format.html {
276 @events_by_day = events.group_by(&:event_date)
276 @events_by_day = events.group_by(&:event_date)
277 render :layout => false if request.xhr?
277 render :layout => false if request.xhr?
278 }
278 }
279 format.atom {
279 format.atom {
280 title = l(:label_activity)
280 title = l(:label_activity)
281 if @author
281 if @author
282 title = @author.name
282 title = @author.name
283 elsif @activity.scope.size == 1
283 elsif @activity.scope.size == 1
284 title = l("label_#{@activity.scope.first.singularize}_plural")
284 title = l("label_#{@activity.scope.first.singularize}_plural")
285 end
285 end
286 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
286 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
287 }
287 }
288 end
288 end
289
289
290 rescue ActiveRecord::RecordNotFound
290 rescue ActiveRecord::RecordNotFound
291 render_404
291 render_404
292 end
292 end
293
293
294 private
294 private
295 # Find project of id params[:id]
295 # Find project of id params[:id]
296 # if not found, redirect to project list
296 # if not found, redirect to project list
297 # Used as a before_filter
297 # Used as a before_filter
298 def find_project
298 def find_project
299 @project = Project.find(params[:id])
299 @project = Project.find(params[:id])
300 rescue ActiveRecord::RecordNotFound
300 rescue ActiveRecord::RecordNotFound
301 render_404
301 render_404
302 end
302 end
303
303
304 def find_optional_project
304 def find_optional_project
305 return true unless params[:id]
305 return true unless params[:id]
306 @project = Project.find(params[:id])
306 @project = Project.find(params[:id])
307 authorize
307 authorize
308 rescue ActiveRecord::RecordNotFound
308 rescue ActiveRecord::RecordNotFound
309 render_404
309 render_404
310 end
310 end
311
311
312 def retrieve_selected_tracker_ids(selectable_trackers)
312 def retrieve_selected_tracker_ids(selectable_trackers)
313 if ids = params[:tracker_ids]
313 if ids = params[:tracker_ids]
314 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
314 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
315 else
315 else
316 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
316 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
317 end
317 end
318 end
318 end
319 end
319 end
@@ -1,400 +1,411
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 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :users, :through => :members
24 has_many :users, :through => :members
25 has_many :enabled_modules, :dependent => :delete_all
25 has_many :enabled_modules, :dependent => :delete_all
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
28 has_many :issue_changes, :through => :issues, :source => :journals
28 has_many :issue_changes, :through => :issues, :source => :journals
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
30 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
32 has_many :documents, :dependent => :destroy
32 has_many :documents, :dependent => :destroy
33 has_many :news, :dependent => :delete_all, :include => :author
33 has_many :news, :dependent => :delete_all, :include => :author
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
36 has_one :repository, :dependent => :destroy
36 has_one :repository, :dependent => :destroy
37 has_many :changesets, :through => :repository
37 has_many :changesets, :through => :repository
38 has_one :wiki, :dependent => :destroy
38 has_one :wiki, :dependent => :destroy
39 # Custom field for the project issues
39 # Custom field for the project issues
40 has_and_belongs_to_many :issue_custom_fields,
40 has_and_belongs_to_many :issue_custom_fields,
41 :class_name => 'IssueCustomField',
41 :class_name => 'IssueCustomField',
42 :order => "#{CustomField.table_name}.position",
42 :order => "#{CustomField.table_name}.position",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 :association_foreign_key => 'custom_field_id'
44 :association_foreign_key => 'custom_field_id'
45
45
46 acts_as_nested_set :order => 'name', :dependent => :destroy
46 acts_as_nested_set :order => 'name', :dependent => :destroy
47 acts_as_attachable :view_permission => :view_files,
47 acts_as_attachable :view_permission => :view_files,
48 :delete_permission => :manage_files
48 :delete_permission => :manage_files
49
49
50 acts_as_customizable
50 acts_as_customizable
51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
54 :author => nil
54 :author => nil
55
55
56 attr_protected :status, :enabled_module_names
56 attr_protected :status, :enabled_module_names
57
57
58 validates_presence_of :name, :identifier
58 validates_presence_of :name, :identifier
59 validates_uniqueness_of :name, :identifier
59 validates_uniqueness_of :name, :identifier
60 validates_associated :repository, :wiki
60 validates_associated :repository, :wiki
61 validates_length_of :name, :maximum => 30
61 validates_length_of :name, :maximum => 30
62 validates_length_of :homepage, :maximum => 255
62 validates_length_of :homepage, :maximum => 255
63 validates_length_of :identifier, :in => 1..20
63 validates_length_of :identifier, :in => 1..20
64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
65
65
66 before_destroy :delete_all_members
66 before_destroy :delete_all_members
67
67
68 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] } }
68 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] } }
69 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
69 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
70 named_scope :public, { :conditions => { :is_public => true } }
70 named_scope :public, { :conditions => { :is_public => true } }
71 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
71 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
72
72
73 def identifier=(identifier)
73 def identifier=(identifier)
74 super unless identifier_frozen?
74 super unless identifier_frozen?
75 end
75 end
76
76
77 def identifier_frozen?
77 def identifier_frozen?
78 errors[:identifier].nil? && !(new_record? || identifier.blank?)
78 errors[:identifier].nil? && !(new_record? || identifier.blank?)
79 end
79 end
80
80
81 def issues_with_subprojects(include_subprojects=false)
81 def issues_with_subprojects(include_subprojects=false)
82 conditions = nil
82 conditions = nil
83 if include_subprojects
83 if include_subprojects
84 ids = [id] + descendants.collect(&:id)
84 ids = [id] + descendants.collect(&:id)
85 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
85 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
86 end
86 end
87 conditions ||= ["#{Project.table_name}.id = ?", id]
87 conditions ||= ["#{Project.table_name}.id = ?", id]
88 # Quick and dirty fix for Rails 2 compatibility
88 # Quick and dirty fix for Rails 2 compatibility
89 Issue.send(:with_scope, :find => { :conditions => conditions }) do
89 Issue.send(:with_scope, :find => { :conditions => conditions }) do
90 Version.send(:with_scope, :find => { :conditions => conditions }) do
90 Version.send(:with_scope, :find => { :conditions => conditions }) do
91 yield
91 yield
92 end
92 end
93 end
93 end
94 end
94 end
95
95
96 # returns latest created projects
96 # returns latest created projects
97 # non public projects will be returned only if user is a member of those
97 # non public projects will be returned only if user is a member of those
98 def self.latest(user=nil, count=5)
98 def self.latest(user=nil, count=5)
99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
100 end
100 end
101
101
102 # Returns a SQL :conditions string used to find all active projects for the specified user.
102 # Returns a SQL :conditions string used to find all active projects for the specified user.
103 #
103 #
104 # Examples:
104 # Examples:
105 # Projects.visible_by(admin) => "projects.status = 1"
105 # Projects.visible_by(admin) => "projects.status = 1"
106 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
106 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
107 def self.visible_by(user=nil)
107 def self.visible_by(user=nil)
108 user ||= User.current
108 user ||= User.current
109 if user && user.admin?
109 if user && user.admin?
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
111 elsif user && user.memberships.any?
111 elsif user && user.memberships.any?
112 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(',')}))"
112 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(',')}))"
113 else
113 else
114 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
114 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
115 end
115 end
116 end
116 end
117
117
118 def self.allowed_to_condition(user, permission, options={})
118 def self.allowed_to_condition(user, permission, options={})
119 statements = []
119 statements = []
120 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
120 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
121 if perm = Redmine::AccessControl.permission(permission)
121 if perm = Redmine::AccessControl.permission(permission)
122 unless perm.project_module.nil?
122 unless perm.project_module.nil?
123 # If the permission belongs to a project module, make sure the module is enabled
123 # If the permission belongs to a project module, make sure the module is enabled
124 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
124 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
125 end
125 end
126 end
126 end
127 if options[:project]
127 if options[:project]
128 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
128 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
129 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
129 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
130 base_statement = "(#{project_statement}) AND (#{base_statement})"
130 base_statement = "(#{project_statement}) AND (#{base_statement})"
131 end
131 end
132 if user.admin?
132 if user.admin?
133 # no restriction
133 # no restriction
134 else
134 else
135 statements << "1=0"
135 statements << "1=0"
136 if user.logged?
136 if user.logged?
137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
138 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
138 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
139 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
139 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
140 elsif Role.anonymous.allowed_to?(permission)
140 elsif Role.anonymous.allowed_to?(permission)
141 # anonymous user allowed on public project
141 # anonymous user allowed on public project
142 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
143 else
143 else
144 # anonymous user is not authorized
144 # anonymous user is not authorized
145 end
145 end
146 end
146 end
147 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
147 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
148 end
148 end
149
149
150 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
150 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
151 #
151 #
152 # Examples:
152 # Examples:
153 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
153 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
154 # project.project_condition(false) => "projects.id = 1"
154 # project.project_condition(false) => "projects.id = 1"
155 def project_condition(with_subprojects)
155 def project_condition(with_subprojects)
156 cond = "#{Project.table_name}.id = #{id}"
156 cond = "#{Project.table_name}.id = #{id}"
157 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
157 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
158 cond
158 cond
159 end
159 end
160
160
161 def self.find(*args)
161 def self.find(*args)
162 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
162 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
163 project = find_by_identifier(*args)
163 project = find_by_identifier(*args)
164 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
164 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
165 project
165 project
166 else
166 else
167 super
167 super
168 end
168 end
169 end
169 end
170
170
171 def to_param
171 def to_param
172 # id is used for projects with a numeric identifier (compatibility)
172 # id is used for projects with a numeric identifier (compatibility)
173 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
173 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
174 end
174 end
175
175
176 def active?
176 def active?
177 self.status == STATUS_ACTIVE
177 self.status == STATUS_ACTIVE
178 end
178 end
179
179
180 # Archives the project and its descendants recursively
180 # Archives the project and its descendants recursively
181 def archive
181 def archive
182 # Archive subprojects if any
182 # Archive subprojects if any
183 children.each do |subproject|
183 children.each do |subproject|
184 subproject.archive
184 subproject.archive
185 end
185 end
186 update_attribute :status, STATUS_ARCHIVED
186 update_attribute :status, STATUS_ARCHIVED
187 end
187 end
188
188
189 # Unarchives the project
189 # Unarchives the project
190 # All its ancestors must be active
190 # All its ancestors must be active
191 def unarchive
191 def unarchive
192 return false if ancestors.detect {|a| !a.active?}
192 return false if ancestors.detect {|a| !a.active?}
193 update_attribute :status, STATUS_ACTIVE
193 update_attribute :status, STATUS_ACTIVE
194 end
194 end
195
195
196 # Returns an array of projects the project can be moved to
196 # Returns an array of projects the project can be moved to
197 def possible_parents
197 def possible_parents
198 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
198 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
199 end
199 end
200
200
201 # Sets the parent of the project
201 # Sets the parent of the project
202 # Argument can be either a Project, a String, a Fixnum or nil
202 # Argument can be either a Project, a String, a Fixnum or nil
203 def set_parent!(p)
203 def set_parent!(p)
204 unless p.nil? || p.is_a?(Project)
204 unless p.nil? || p.is_a?(Project)
205 if p.to_s.blank?
205 if p.to_s.blank?
206 p = nil
206 p = nil
207 else
207 else
208 p = Project.find_by_id(p)
208 p = Project.find_by_id(p)
209 return false unless p
209 return false unless p
210 end
210 end
211 end
211 end
212 if p == parent && !p.nil?
212 if p == parent && !p.nil?
213 # Nothing to do
213 # Nothing to do
214 true
214 true
215 elsif p.nil? || (p.active? && move_possible?(p))
215 elsif p.nil? || (p.active? && move_possible?(p))
216 # Insert the project so that target's children or root projects stay alphabetically sorted
216 # Insert the project so that target's children or root projects stay alphabetically sorted
217 sibs = (p.nil? ? self.class.roots : p.children)
217 sibs = (p.nil? ? self.class.roots : p.children)
218 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
218 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
219 if to_be_inserted_before
219 if to_be_inserted_before
220 move_to_left_of(to_be_inserted_before)
220 move_to_left_of(to_be_inserted_before)
221 elsif p.nil?
221 elsif p.nil?
222 if sibs.empty?
222 if sibs.empty?
223 # move_to_root adds the project in first (ie. left) position
223 # move_to_root adds the project in first (ie. left) position
224 move_to_root
224 move_to_root
225 else
225 else
226 move_to_right_of(sibs.last) unless self == sibs.last
226 move_to_right_of(sibs.last) unless self == sibs.last
227 end
227 end
228 else
228 else
229 # move_to_child_of adds the project in last (ie.right) position
229 # move_to_child_of adds the project in last (ie.right) position
230 move_to_child_of(p)
230 move_to_child_of(p)
231 end
231 end
232 true
232 true
233 else
233 else
234 # Can not move to the given target
234 # Can not move to the given target
235 false
235 false
236 end
236 end
237 end
237 end
238
238
239 # Returns an array of the trackers used by the project and its active sub projects
239 # Returns an array of the trackers used by the project and its active sub projects
240 def rolled_up_trackers
240 def rolled_up_trackers
241 @rolled_up_trackers ||=
241 @rolled_up_trackers ||=
242 Tracker.find(:all, :include => :projects,
242 Tracker.find(:all, :include => :projects,
243 :select => "DISTINCT #{Tracker.table_name}.*",
243 :select => "DISTINCT #{Tracker.table_name}.*",
244 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
244 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
245 :order => "#{Tracker.table_name}.position")
245 :order => "#{Tracker.table_name}.position")
246 end
246 end
247
247
248 # Returns a hash of project users grouped by role
249 def users_by_role
250 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
251 m.roles.each do |r|
252 h[r] ||= []
253 h[r] << m.user
254 end
255 h
256 end
257 end
258
248 # Deletes all project's members
259 # Deletes all project's members
249 def delete_all_members
260 def delete_all_members
250 me, mr = Member.table_name, MemberRole.table_name
261 me, mr = Member.table_name, MemberRole.table_name
251 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
262 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
252 Member.delete_all(['project_id = ?', id])
263 Member.delete_all(['project_id = ?', id])
253 end
264 end
254
265
255 # Users issues can be assigned to
266 # Users issues can be assigned to
256 def assignable_users
267 def assignable_users
257 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
268 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
258 end
269 end
259
270
260 # Returns the mail adresses of users that should be always notified on project events
271 # Returns the mail adresses of users that should be always notified on project events
261 def recipients
272 def recipients
262 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
273 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
263 end
274 end
264
275
265 # Returns an array of all custom fields enabled for project issues
276 # Returns an array of all custom fields enabled for project issues
266 # (explictly associated custom fields and custom fields enabled for all projects)
277 # (explictly associated custom fields and custom fields enabled for all projects)
267 def all_issue_custom_fields
278 def all_issue_custom_fields
268 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
279 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
269 end
280 end
270
281
271 def project
282 def project
272 self
283 self
273 end
284 end
274
285
275 def <=>(project)
286 def <=>(project)
276 name.downcase <=> project.name.downcase
287 name.downcase <=> project.name.downcase
277 end
288 end
278
289
279 def to_s
290 def to_s
280 name
291 name
281 end
292 end
282
293
283 # Returns a short description of the projects (first lines)
294 # Returns a short description of the projects (first lines)
284 def short_description(length = 255)
295 def short_description(length = 255)
285 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
296 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
286 end
297 end
287
298
288 # Return true if this project is allowed to do the specified action.
299 # Return true if this project is allowed to do the specified action.
289 # action can be:
300 # action can be:
290 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
301 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
291 # * a permission Symbol (eg. :edit_project)
302 # * a permission Symbol (eg. :edit_project)
292 def allows_to?(action)
303 def allows_to?(action)
293 if action.is_a? Hash
304 if action.is_a? Hash
294 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
305 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
295 else
306 else
296 allowed_permissions.include? action
307 allowed_permissions.include? action
297 end
308 end
298 end
309 end
299
310
300 def module_enabled?(module_name)
311 def module_enabled?(module_name)
301 module_name = module_name.to_s
312 module_name = module_name.to_s
302 enabled_modules.detect {|m| m.name == module_name}
313 enabled_modules.detect {|m| m.name == module_name}
303 end
314 end
304
315
305 def enabled_module_names=(module_names)
316 def enabled_module_names=(module_names)
306 if module_names && module_names.is_a?(Array)
317 if module_names && module_names.is_a?(Array)
307 module_names = module_names.collect(&:to_s)
318 module_names = module_names.collect(&:to_s)
308 # remove disabled modules
319 # remove disabled modules
309 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
320 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
310 # add new modules
321 # add new modules
311 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
322 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
312 else
323 else
313 enabled_modules.clear
324 enabled_modules.clear
314 end
325 end
315 end
326 end
316
327
317 # Returns an auto-generated project identifier based on the last identifier used
328 # Returns an auto-generated project identifier based on the last identifier used
318 def self.next_identifier
329 def self.next_identifier
319 p = Project.find(:first, :order => 'created_on DESC')
330 p = Project.find(:first, :order => 'created_on DESC')
320 p.nil? ? nil : p.identifier.to_s.succ
331 p.nil? ? nil : p.identifier.to_s.succ
321 end
332 end
322
333
323 # Copies and saves the Project instance based on the +project+.
334 # Copies and saves the Project instance based on the +project+.
324 # Will duplicate the source project's:
335 # Will duplicate the source project's:
325 # * Issues
336 # * Issues
326 # * Members
337 # * Members
327 # * Queries
338 # * Queries
328 def copy(project)
339 def copy(project)
329 project = project.is_a?(Project) ? project : Project.find(project)
340 project = project.is_a?(Project) ? project : Project.find(project)
330
341
331 Project.transaction do
342 Project.transaction do
332 # Issues
343 # Issues
333 project.issues.each do |issue|
344 project.issues.each do |issue|
334 new_issue = Issue.new
345 new_issue = Issue.new
335 new_issue.copy_from(issue)
346 new_issue.copy_from(issue)
336 self.issues << new_issue
347 self.issues << new_issue
337 end
348 end
338
349
339 # Members
350 # Members
340 project.members.each do |member|
351 project.members.each do |member|
341 new_member = Member.new
352 new_member = Member.new
342 new_member.attributes = member.attributes.dup.except("project_id")
353 new_member.attributes = member.attributes.dup.except("project_id")
343 new_member.role_ids = member.role_ids.dup
354 new_member.role_ids = member.role_ids.dup
344 new_member.project = self
355 new_member.project = self
345 self.members << new_member
356 self.members << new_member
346 end
357 end
347
358
348 # Queries
359 # Queries
349 project.queries.each do |query|
360 project.queries.each do |query|
350 new_query = Query.new
361 new_query = Query.new
351 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
362 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
352 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
363 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
353 new_query.project = self
364 new_query.project = self
354 self.queries << new_query
365 self.queries << new_query
355 end
366 end
356
367
357 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
368 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
358 self.save
369 self.save
359 end
370 end
360 end
371 end
361
372
362
373
363 # Copies +project+ and returns the new instance. This will not save
374 # Copies +project+ and returns the new instance. This will not save
364 # the copy
375 # the copy
365 def self.copy_from(project)
376 def self.copy_from(project)
366 begin
377 begin
367 project = project.is_a?(Project) ? project : Project.find(project)
378 project = project.is_a?(Project) ? project : Project.find(project)
368 if project
379 if project
369 # clear unique attributes
380 # clear unique attributes
370 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
381 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
371 copy = Project.new(attributes)
382 copy = Project.new(attributes)
372 copy.enabled_modules = project.enabled_modules
383 copy.enabled_modules = project.enabled_modules
373 copy.trackers = project.trackers
384 copy.trackers = project.trackers
374 copy.custom_values = project.custom_values.collect {|v| v.clone}
385 copy.custom_values = project.custom_values.collect {|v| v.clone}
375 return copy
386 return copy
376 else
387 else
377 return nil
388 return nil
378 end
389 end
379 rescue ActiveRecord::RecordNotFound
390 rescue ActiveRecord::RecordNotFound
380 return nil
391 return nil
381 end
392 end
382 end
393 end
383
394
384 protected
395 protected
385 def validate
396 def validate
386 errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/)
397 errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/)
387 end
398 end
388
399
389 private
400 private
390 def allowed_permissions
401 def allowed_permissions
391 @allowed_permissions ||= begin
402 @allowed_permissions ||= begin
392 module_names = enabled_modules.collect {|m| m.name}
403 module_names = enabled_modules.collect {|m| m.name}
393 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
404 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
394 end
405 end
395 end
406 end
396
407
397 def allowed_actions
408 def allowed_actions
398 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
409 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
399 end
410 end
400 end
411 end
@@ -1,81 +1,79
1 <h2><%=l(:label_overview)%></h2>
1 <h2><%=l(:label_overview)%></h2>
2
2
3 <div class="splitcontentleft">
3 <div class="splitcontentleft">
4 <%= textilizable @project.description %>
4 <%= textilizable @project.description %>
5 <ul>
5 <ul>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
7 <% if @subprojects.any? %>
7 <% if @subprojects.any? %>
8 <li><%=l(:label_subproject_plural)%>:
8 <li><%=l(:label_subproject_plural)%>:
9 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
9 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
10 <% end %>
10 <% end %>
11 <% @project.custom_values.each do |custom_value| %>
11 <% @project.custom_values.each do |custom_value| %>
12 <% if !custom_value.value.empty? %>
12 <% if !custom_value.value.empty? %>
13 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
13 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
14 <% end %>
14 <% end %>
15 <% end %>
15 <% end %>
16 </ul>
16 </ul>
17
17
18 <% if User.current.allowed_to?(:view_issues, @project) %>
18 <% if User.current.allowed_to?(:view_issues, @project) %>
19 <div class="box">
19 <div class="box">
20 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
20 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
21 <ul>
21 <ul>
22 <% for tracker in @trackers %>
22 <% for tracker in @trackers %>
23 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
23 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
24 :set_filter => 1,
24 :set_filter => 1,
25 "tracker_id" => tracker.id %>:
25 "tracker_id" => tracker.id %>:
26 <%= l(:label_x_open_issues_abbr_on_total, :count => @open_issues_by_tracker[tracker].to_i,
26 <%= l(:label_x_open_issues_abbr_on_total, :count => @open_issues_by_tracker[tracker].to_i,
27 :total => @total_issues_by_tracker[tracker].to_i) %>
27 :total => @total_issues_by_tracker[tracker].to_i) %>
28 </li>
28 </li>
29 <% end %>
29 <% end %>
30 </ul>
30 </ul>
31 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
31 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
32 </div>
32 </div>
33 <% end %>
33 <% end %>
34 <%= call_hook(:view_projects_show_left, :project => @project) %>
34 <%= call_hook(:view_projects_show_left, :project => @project) %>
35 </div>
35 </div>
36
36
37 <div class="splitcontentright">
37 <div class="splitcontentright">
38 <% if @members_by_role.any? %>
38 <% if @users_by_role.any? %>
39 <div class="box">
39 <div class="box">
40 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
40 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
41 <p><% @members_by_role.keys.sort.each do |role| %>
41 <p><% @users_by_role.keys.sort.each do |role| %>
42 <%= role.name %>:
42 <%= role.name %>: <%= @users_by_role[role].sort.collect{|u| link_to_user u}.join(", ") %><br />
43 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
44 <br />
45 <% end %></p>
43 <% end %></p>
46 </div>
44 </div>
47 <% end %>
45 <% end %>
48
46
49 <% if @news.any? && authorize_for('news', 'index') %>
47 <% if @news.any? && authorize_for('news', 'index') %>
50 <div class="box">
48 <div class="box">
51 <h3><%=l(:label_news_latest)%></h3>
49 <h3><%=l(:label_news_latest)%></h3>
52 <%= render :partial => 'news/news', :collection => @news %>
50 <%= render :partial => 'news/news', :collection => @news %>
53 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
51 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
54 </div>
52 </div>
55 <% end %>
53 <% end %>
56 <%= call_hook(:view_projects_show_right, :project => @project) %>
54 <%= call_hook(:view_projects_show_right, :project => @project) %>
57 </div>
55 </div>
58
56
59 <% content_for :sidebar do %>
57 <% content_for :sidebar do %>
60 <% planning_links = []
58 <% planning_links = []
61 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
59 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
62 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
60 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
63 planning_links.compact!
61 planning_links.compact!
64 unless planning_links.empty? %>
62 unless planning_links.empty? %>
65 <h3><%= l(:label_planning) %></h3>
63 <h3><%= l(:label_planning) %></h3>
66 <p><%= planning_links.join(' | ') %></p>
64 <p><%= planning_links.join(' | ') %></p>
67 <% end %>
65 <% end %>
68
66
69 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
67 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
70 <h3><%= l(:label_spent_time) %></h3>
68 <h3><%= l(:label_spent_time) %></h3>
71 <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
69 <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
72 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
70 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
73 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
71 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
74 <% end %>
72 <% end %>
75 <% end %>
73 <% end %>
76
74
77 <% content_for :header_tags do %>
75 <% content_for :header_tags do %>
78 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
76 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
79 <% end %>
77 <% end %>
80
78
81 <% html_title(l(:label_overview)) -%>
79 <% html_title(l(:label_overview)) -%>
@@ -1,320 +1,328
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class ProjectTest < Test::Unit::TestCase
20 class ProjectTest < Test::Unit::TestCase
21 fixtures :projects, :enabled_modules,
21 fixtures :projects, :enabled_modules,
22 :issues, :issue_statuses, :journals, :journal_details,
22 :issues, :issue_statuses, :journals, :journal_details,
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 :queries
24 :queries
25
25
26 def setup
26 def setup
27 @ecookbook = Project.find(1)
27 @ecookbook = Project.find(1)
28 @ecookbook_sub1 = Project.find(3)
28 @ecookbook_sub1 = Project.find(3)
29 end
29 end
30
30
31 def test_truth
31 def test_truth
32 assert_kind_of Project, @ecookbook
32 assert_kind_of Project, @ecookbook
33 assert_equal "eCookbook", @ecookbook.name
33 assert_equal "eCookbook", @ecookbook.name
34 end
34 end
35
35
36 def test_update
36 def test_update
37 assert_equal "eCookbook", @ecookbook.name
37 assert_equal "eCookbook", @ecookbook.name
38 @ecookbook.name = "eCook"
38 @ecookbook.name = "eCook"
39 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
39 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
40 @ecookbook.reload
40 @ecookbook.reload
41 assert_equal "eCook", @ecookbook.name
41 assert_equal "eCook", @ecookbook.name
42 end
42 end
43
43
44 def test_validate
44 def test_validate
45 @ecookbook.name = ""
45 @ecookbook.name = ""
46 assert !@ecookbook.save
46 assert !@ecookbook.save
47 assert_equal 1, @ecookbook.errors.count
47 assert_equal 1, @ecookbook.errors.count
48 assert_equal I18n.translate('activerecord.errors.messages.blank'), @ecookbook.errors.on(:name)
48 assert_equal I18n.translate('activerecord.errors.messages.blank'), @ecookbook.errors.on(:name)
49 end
49 end
50
50
51 def test_archive
51 def test_archive
52 user = @ecookbook.members.first.user
52 user = @ecookbook.members.first.user
53 @ecookbook.archive
53 @ecookbook.archive
54 @ecookbook.reload
54 @ecookbook.reload
55
55
56 assert !@ecookbook.active?
56 assert !@ecookbook.active?
57 assert !user.projects.include?(@ecookbook)
57 assert !user.projects.include?(@ecookbook)
58 # Subproject are also archived
58 # Subproject are also archived
59 assert !@ecookbook.children.empty?
59 assert !@ecookbook.children.empty?
60 assert @ecookbook.descendants.active.empty?
60 assert @ecookbook.descendants.active.empty?
61 end
61 end
62
62
63 def test_unarchive
63 def test_unarchive
64 user = @ecookbook.members.first.user
64 user = @ecookbook.members.first.user
65 @ecookbook.archive
65 @ecookbook.archive
66 # A subproject of an archived project can not be unarchived
66 # A subproject of an archived project can not be unarchived
67 assert !@ecookbook_sub1.unarchive
67 assert !@ecookbook_sub1.unarchive
68
68
69 # Unarchive project
69 # Unarchive project
70 assert @ecookbook.unarchive
70 assert @ecookbook.unarchive
71 @ecookbook.reload
71 @ecookbook.reload
72 assert @ecookbook.active?
72 assert @ecookbook.active?
73 assert user.projects.include?(@ecookbook)
73 assert user.projects.include?(@ecookbook)
74 # Subproject can now be unarchived
74 # Subproject can now be unarchived
75 @ecookbook_sub1.reload
75 @ecookbook_sub1.reload
76 assert @ecookbook_sub1.unarchive
76 assert @ecookbook_sub1.unarchive
77 end
77 end
78
78
79 def test_destroy
79 def test_destroy
80 # 2 active members
80 # 2 active members
81 assert_equal 2, @ecookbook.members.size
81 assert_equal 2, @ecookbook.members.size
82 # and 1 is locked
82 # and 1 is locked
83 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
83 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
84 # some boards
84 # some boards
85 assert @ecookbook.boards.any?
85 assert @ecookbook.boards.any?
86
86
87 @ecookbook.destroy
87 @ecookbook.destroy
88 # make sure that the project non longer exists
88 # make sure that the project non longer exists
89 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
89 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
90 # make sure related data was removed
90 # make sure related data was removed
91 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
91 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
92 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
92 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
93 end
93 end
94
94
95 def test_move_an_orphan_project_to_a_root_project
95 def test_move_an_orphan_project_to_a_root_project
96 sub = Project.find(2)
96 sub = Project.find(2)
97 sub.set_parent! @ecookbook
97 sub.set_parent! @ecookbook
98 assert_equal @ecookbook.id, sub.parent.id
98 assert_equal @ecookbook.id, sub.parent.id
99 @ecookbook.reload
99 @ecookbook.reload
100 assert_equal 4, @ecookbook.children.size
100 assert_equal 4, @ecookbook.children.size
101 end
101 end
102
102
103 def test_move_an_orphan_project_to_a_subproject
103 def test_move_an_orphan_project_to_a_subproject
104 sub = Project.find(2)
104 sub = Project.find(2)
105 assert sub.set_parent!(@ecookbook_sub1)
105 assert sub.set_parent!(@ecookbook_sub1)
106 end
106 end
107
107
108 def test_move_a_root_project_to_a_project
108 def test_move_a_root_project_to_a_project
109 sub = @ecookbook
109 sub = @ecookbook
110 assert sub.set_parent!(Project.find(2))
110 assert sub.set_parent!(Project.find(2))
111 end
111 end
112
112
113 def test_should_not_move_a_project_to_its_children
113 def test_should_not_move_a_project_to_its_children
114 sub = @ecookbook
114 sub = @ecookbook
115 assert !(sub.set_parent!(Project.find(3)))
115 assert !(sub.set_parent!(Project.find(3)))
116 end
116 end
117
117
118 def test_set_parent_should_add_roots_in_alphabetical_order
118 def test_set_parent_should_add_roots_in_alphabetical_order
119 ProjectCustomField.delete_all
119 ProjectCustomField.delete_all
120 Project.delete_all
120 Project.delete_all
121 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
121 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
122 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
122 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
123 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
123 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
124 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
124 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
125
125
126 assert_equal 4, Project.count
126 assert_equal 4, Project.count
127 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
127 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
128 end
128 end
129
129
130 def test_set_parent_should_add_children_in_alphabetical_order
130 def test_set_parent_should_add_children_in_alphabetical_order
131 ProjectCustomField.delete_all
131 ProjectCustomField.delete_all
132 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
132 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
133 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
133 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
134 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
134 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
135 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
135 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
136 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
136 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
137
137
138 parent.reload
138 parent.reload
139 assert_equal 4, parent.children.size
139 assert_equal 4, parent.children.size
140 assert_equal parent.children.sort_by(&:name), parent.children
140 assert_equal parent.children.sort_by(&:name), parent.children
141 end
141 end
142
142
143 def test_rebuild_should_sort_children_alphabetically
143 def test_rebuild_should_sort_children_alphabetically
144 ProjectCustomField.delete_all
144 ProjectCustomField.delete_all
145 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
145 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
146 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
146 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
147 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
147 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
148 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
148 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
149 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
149 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
150
150
151 Project.update_all("lft = NULL, rgt = NULL")
151 Project.update_all("lft = NULL, rgt = NULL")
152 Project.rebuild!
152 Project.rebuild!
153
153
154 parent.reload
154 parent.reload
155 assert_equal 4, parent.children.size
155 assert_equal 4, parent.children.size
156 assert_equal parent.children.sort_by(&:name), parent.children
156 assert_equal parent.children.sort_by(&:name), parent.children
157 end
157 end
158
158
159 def test_parent
159 def test_parent
160 p = Project.find(6).parent
160 p = Project.find(6).parent
161 assert p.is_a?(Project)
161 assert p.is_a?(Project)
162 assert_equal 5, p.id
162 assert_equal 5, p.id
163 end
163 end
164
164
165 def test_ancestors
165 def test_ancestors
166 a = Project.find(6).ancestors
166 a = Project.find(6).ancestors
167 assert a.first.is_a?(Project)
167 assert a.first.is_a?(Project)
168 assert_equal [1, 5], a.collect(&:id)
168 assert_equal [1, 5], a.collect(&:id)
169 end
169 end
170
170
171 def test_root
171 def test_root
172 r = Project.find(6).root
172 r = Project.find(6).root
173 assert r.is_a?(Project)
173 assert r.is_a?(Project)
174 assert_equal 1, r.id
174 assert_equal 1, r.id
175 end
175 end
176
176
177 def test_children
177 def test_children
178 c = Project.find(1).children
178 c = Project.find(1).children
179 assert c.first.is_a?(Project)
179 assert c.first.is_a?(Project)
180 assert_equal [5, 3, 4], c.collect(&:id)
180 assert_equal [5, 3, 4], c.collect(&:id)
181 end
181 end
182
182
183 def test_descendants
183 def test_descendants
184 d = Project.find(1).descendants
184 d = Project.find(1).descendants
185 assert d.first.is_a?(Project)
185 assert d.first.is_a?(Project)
186 assert_equal [5, 6, 3, 4], d.collect(&:id)
186 assert_equal [5, 6, 3, 4], d.collect(&:id)
187 end
187 end
188
188
189 def test_users_by_role
190 users_by_role = Project.find(1).users_by_role
191 assert_kind_of Hash, users_by_role
192 role = Role.find(1)
193 assert_kind_of Array, users_by_role[role]
194 assert users_by_role[role].include?(User.find(2))
195 end
196
189 def test_rolled_up_trackers
197 def test_rolled_up_trackers
190 parent = Project.find(1)
198 parent = Project.find(1)
191 parent.trackers = Tracker.find([1,2])
199 parent.trackers = Tracker.find([1,2])
192 child = parent.children.find(3)
200 child = parent.children.find(3)
193
201
194 assert_equal [1, 2], parent.tracker_ids
202 assert_equal [1, 2], parent.tracker_ids
195 assert_equal [2, 3], child.trackers.collect(&:id)
203 assert_equal [2, 3], child.trackers.collect(&:id)
196
204
197 assert_kind_of Tracker, parent.rolled_up_trackers.first
205 assert_kind_of Tracker, parent.rolled_up_trackers.first
198 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
206 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
199
207
200 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
208 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
201 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
209 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
202 end
210 end
203
211
204 def test_rolled_up_trackers_should_ignore_archived_subprojects
212 def test_rolled_up_trackers_should_ignore_archived_subprojects
205 parent = Project.find(1)
213 parent = Project.find(1)
206 parent.trackers = Tracker.find([1,2])
214 parent.trackers = Tracker.find([1,2])
207 child = parent.children.find(3)
215 child = parent.children.find(3)
208 child.trackers = Tracker.find([1,3])
216 child.trackers = Tracker.find([1,3])
209 parent.children.each(&:archive)
217 parent.children.each(&:archive)
210
218
211 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
219 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
212 end
220 end
213
221
214 def test_next_identifier
222 def test_next_identifier
215 ProjectCustomField.delete_all
223 ProjectCustomField.delete_all
216 Project.create!(:name => 'last', :identifier => 'p2008040')
224 Project.create!(:name => 'last', :identifier => 'p2008040')
217 assert_equal 'p2008041', Project.next_identifier
225 assert_equal 'p2008041', Project.next_identifier
218 end
226 end
219
227
220 def test_next_identifier_first_project
228 def test_next_identifier_first_project
221 Project.delete_all
229 Project.delete_all
222 assert_nil Project.next_identifier
230 assert_nil Project.next_identifier
223 end
231 end
224
232
225
233
226 def test_enabled_module_names_should_not_recreate_enabled_modules
234 def test_enabled_module_names_should_not_recreate_enabled_modules
227 project = Project.find(1)
235 project = Project.find(1)
228 # Remove one module
236 # Remove one module
229 modules = project.enabled_modules.slice(0..-2)
237 modules = project.enabled_modules.slice(0..-2)
230 assert modules.any?
238 assert modules.any?
231 assert_difference 'EnabledModule.count', -1 do
239 assert_difference 'EnabledModule.count', -1 do
232 project.enabled_module_names = modules.collect(&:name)
240 project.enabled_module_names = modules.collect(&:name)
233 end
241 end
234 project.reload
242 project.reload
235 # Ids should be preserved
243 # Ids should be preserved
236 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
244 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
237 end
245 end
238
246
239 def test_copy_from_existing_project
247 def test_copy_from_existing_project
240 source_project = Project.find(1)
248 source_project = Project.find(1)
241 copied_project = Project.copy_from(1)
249 copied_project = Project.copy_from(1)
242
250
243 assert copied_project
251 assert copied_project
244 # Cleared attributes
252 # Cleared attributes
245 assert copied_project.id.blank?
253 assert copied_project.id.blank?
246 assert copied_project.name.blank?
254 assert copied_project.name.blank?
247 assert copied_project.identifier.blank?
255 assert copied_project.identifier.blank?
248
256
249 # Duplicated attributes
257 # Duplicated attributes
250 assert_equal source_project.description, copied_project.description
258 assert_equal source_project.description, copied_project.description
251 assert_equal source_project.enabled_modules, copied_project.enabled_modules
259 assert_equal source_project.enabled_modules, copied_project.enabled_modules
252 assert_equal source_project.trackers, copied_project.trackers
260 assert_equal source_project.trackers, copied_project.trackers
253
261
254 # Default attributes
262 # Default attributes
255 assert_equal 1, copied_project.status
263 assert_equal 1, copied_project.status
256 end
264 end
257
265
258 # Context: Project#copy
266 # Context: Project#copy
259 def test_copy_should_copy_issues
267 def test_copy_should_copy_issues
260 # Setup
268 # Setup
261 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
269 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
262 source_project = Project.find(2)
270 source_project = Project.find(2)
263 Project.destroy_all :identifier => "copy-test"
271 Project.destroy_all :identifier => "copy-test"
264 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
272 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
265 project.trackers = source_project.trackers
273 project.trackers = source_project.trackers
266 assert project.valid?
274 assert project.valid?
267
275
268 assert project.issues.empty?
276 assert project.issues.empty?
269 assert project.copy(source_project)
277 assert project.copy(source_project)
270
278
271 # Tests
279 # Tests
272 assert_equal source_project.issues.size, project.issues.size
280 assert_equal source_project.issues.size, project.issues.size
273 project.issues.each do |issue|
281 project.issues.each do |issue|
274 assert issue.valid?
282 assert issue.valid?
275 assert ! issue.assigned_to.blank?
283 assert ! issue.assigned_to.blank?
276 assert_equal project, issue.project
284 assert_equal project, issue.project
277 end
285 end
278 end
286 end
279
287
280 def test_copy_should_copy_members
288 def test_copy_should_copy_members
281 # Setup
289 # Setup
282 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
290 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
283 source_project = Project.find(2)
291 source_project = Project.find(2)
284 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
292 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
285 project.trackers = source_project.trackers
293 project.trackers = source_project.trackers
286 project.enabled_modules = source_project.enabled_modules
294 project.enabled_modules = source_project.enabled_modules
287 assert project.valid?
295 assert project.valid?
288
296
289 assert project.members.empty?
297 assert project.members.empty?
290 assert project.copy(source_project)
298 assert project.copy(source_project)
291
299
292 # Tests
300 # Tests
293 assert_equal source_project.members.size, project.members.size
301 assert_equal source_project.members.size, project.members.size
294 project.members.each do |member|
302 project.members.each do |member|
295 assert member
303 assert member
296 assert_equal project, member.project
304 assert_equal project, member.project
297 end
305 end
298 end
306 end
299
307
300 def test_copy_should_copy_project_level_queries
308 def test_copy_should_copy_project_level_queries
301 # Setup
309 # Setup
302 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
310 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
303 source_project = Project.find(2)
311 source_project = Project.find(2)
304 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
312 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
305 project.trackers = source_project.trackers
313 project.trackers = source_project.trackers
306 project.enabled_modules = source_project.enabled_modules
314 project.enabled_modules = source_project.enabled_modules
307 assert project.valid?
315 assert project.valid?
308
316
309 assert project.queries.empty?
317 assert project.queries.empty?
310 assert project.copy(source_project)
318 assert project.copy(source_project)
311
319
312 # Tests
320 # Tests
313 assert_equal source_project.queries.size, project.queries.size
321 assert_equal source_project.queries.size, project.queries.size
314 project.queries.each do |query|
322 project.queries.each do |query|
315 assert query
323 assert query
316 assert_equal project, query.project
324 assert_equal project, query.project
317 end
325 end
318 end
326 end
319
327
320 end
328 end
General Comments 0
You need to be logged in to leave comments. Login now