##// END OF EJS Templates
Moves enabled_module_names param to project attribute so that it can be set through the Project API....
Jean-Philippe Lang -
r4525:9fb770ba503b
parent child
Show More
@@ -1,270 +1,269
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :roadmap, :only => :roadmap
20 menu_item :roadmap, :only => :roadmap
21 menu_item :settings, :only => :settings
21 menu_item :settings, :only => :settings
22
22
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 before_filter :authorize_global, :only => [:new, :create]
25 before_filter :authorize_global, :only => [:new, :create]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 accept_key_auth :index, :show, :create, :update, :destroy
27 accept_key_auth :index, :show, :create, :update, :destroy
28
28
29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 if controller.request.post?
30 if controller.request.post?
31 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
31 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
32 end
32 end
33 end
33 end
34
34
35 # TODO: convert to PUT only
35 # TODO: convert to PUT only
36 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
36 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
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 :queries
43 helper :queries
44 include QueriesHelper
44 include QueriesHelper
45 helper :repositories
45 helper :repositories
46 include RepositoriesHelper
46 include RepositoriesHelper
47 include ProjectsHelper
47 include ProjectsHelper
48
48
49 # Lists visible projects
49 # Lists visible projects
50 def index
50 def index
51 respond_to do |format|
51 respond_to do |format|
52 format.html {
52 format.html {
53 @projects = Project.visible.find(:all, :order => 'lft')
53 @projects = Project.visible.find(:all, :order => 'lft')
54 }
54 }
55 format.api {
55 format.api {
56 @offset, @limit = api_offset_and_limit
56 @offset, @limit = api_offset_and_limit
57 @project_count = Project.visible.count
57 @project_count = Project.visible.count
58 @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
58 @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
59 }
59 }
60 format.atom {
60 format.atom {
61 projects = Project.visible.find(:all, :order => 'created_on DESC',
61 projects = Project.visible.find(:all, :order => 'created_on DESC',
62 :limit => Setting.feeds_limit.to_i)
62 :limit => Setting.feeds_limit.to_i)
63 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
63 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
64 }
64 }
65 end
65 end
66 end
66 end
67
67
68 def new
68 def new
69 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
69 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
70 @trackers = Tracker.all
70 @trackers = Tracker.all
71 @project = Project.new(params[:project])
71 @project = Project.new(params[:project])
72 end
72 end
73
73
74 def create
74 def create
75 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
75 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
76 @trackers = Tracker.all
76 @trackers = Tracker.all
77 @project = Project.new
77 @project = Project.new
78 @project.safe_attributes = params[:project]
78 @project.safe_attributes = params[:project]
79
79
80 @project.enabled_module_names = params[:enabled_modules] if params[:enabled_modules]
81 if validate_parent_id && @project.save
80 if validate_parent_id && @project.save
82 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
81 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
83 # Add current user as a project member if he is not admin
82 # Add current user as a project member if he is not admin
84 unless User.current.admin?
83 unless User.current.admin?
85 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
84 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
86 m = Member.new(:user => User.current, :roles => [r])
85 m = Member.new(:user => User.current, :roles => [r])
87 @project.members << m
86 @project.members << m
88 end
87 end
89 respond_to do |format|
88 respond_to do |format|
90 format.html {
89 format.html {
91 flash[:notice] = l(:notice_successful_create)
90 flash[:notice] = l(:notice_successful_create)
92 redirect_to :controller => 'projects', :action => 'settings', :id => @project
91 redirect_to :controller => 'projects', :action => 'settings', :id => @project
93 }
92 }
94 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
93 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
95 end
94 end
96 else
95 else
97 respond_to do |format|
96 respond_to do |format|
98 format.html { render :action => 'new' }
97 format.html { render :action => 'new' }
99 format.api { render_validation_errors(@project) }
98 format.api { render_validation_errors(@project) }
100 end
99 end
101 end
100 end
102
101
103 end
102 end
104
103
105 def copy
104 def copy
106 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
105 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
107 @trackers = Tracker.all
106 @trackers = Tracker.all
108 @root_projects = Project.find(:all,
107 @root_projects = Project.find(:all,
109 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
108 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
110 :order => 'name')
109 :order => 'name')
111 @source_project = Project.find(params[:id])
110 @source_project = Project.find(params[:id])
112 if request.get?
111 if request.get?
113 @project = Project.copy_from(@source_project)
112 @project = Project.copy_from(@source_project)
114 if @project
113 if @project
115 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
114 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
116 else
115 else
117 redirect_to :controller => 'admin', :action => 'projects'
116 redirect_to :controller => 'admin', :action => 'projects'
118 end
117 end
119 else
118 else
120 Mailer.with_deliveries(params[:notifications] == '1') do
119 Mailer.with_deliveries(params[:notifications] == '1') do
121 @project = Project.new
120 @project = Project.new
122 @project.safe_attributes = params[:project]
121 @project.safe_attributes = params[:project]
123 @project.enabled_module_names = params[:enabled_modules]
122 @project.enabled_module_names = params[:enabled_modules]
124 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
123 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
125 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
124 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
126 flash[:notice] = l(:notice_successful_create)
125 flash[:notice] = l(:notice_successful_create)
127 redirect_to :controller => 'projects', :action => 'settings', :id => @project
126 redirect_to :controller => 'projects', :action => 'settings', :id => @project
128 elsif !@project.new_record?
127 elsif !@project.new_record?
129 # Project was created
128 # Project was created
130 # But some objects were not copied due to validation failures
129 # But some objects were not copied due to validation failures
131 # (eg. issues from disabled trackers)
130 # (eg. issues from disabled trackers)
132 # TODO: inform about that
131 # TODO: inform about that
133 redirect_to :controller => 'projects', :action => 'settings', :id => @project
132 redirect_to :controller => 'projects', :action => 'settings', :id => @project
134 end
133 end
135 end
134 end
136 end
135 end
137 rescue ActiveRecord::RecordNotFound
136 rescue ActiveRecord::RecordNotFound
138 redirect_to :controller => 'admin', :action => 'projects'
137 redirect_to :controller => 'admin', :action => 'projects'
139 end
138 end
140
139
141 # Show @project
140 # Show @project
142 def show
141 def show
143 if params[:jump]
142 if params[:jump]
144 # try to redirect to the requested menu item
143 # try to redirect to the requested menu item
145 redirect_to_project_menu_item(@project, params[:jump]) && return
144 redirect_to_project_menu_item(@project, params[:jump]) && return
146 end
145 end
147
146
148 @users_by_role = @project.users_by_role
147 @users_by_role = @project.users_by_role
149 @subprojects = @project.children.visible
148 @subprojects = @project.children.visible
150 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
149 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
151 @trackers = @project.rolled_up_trackers
150 @trackers = @project.rolled_up_trackers
152
151
153 cond = @project.project_condition(Setting.display_subprojects_issues?)
152 cond = @project.project_condition(Setting.display_subprojects_issues?)
154
153
155 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
154 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
156 :include => [:project, :status, :tracker],
155 :include => [:project, :status, :tracker],
157 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
156 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
158 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
157 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
159 :include => [:project, :status, :tracker],
158 :include => [:project, :status, :tracker],
160 :conditions => cond)
159 :conditions => cond)
161
160
162 TimeEntry.visible_by(User.current) do
161 TimeEntry.visible_by(User.current) do
163 @total_hours = TimeEntry.sum(:hours,
162 @total_hours = TimeEntry.sum(:hours,
164 :include => :project,
163 :include => :project,
165 :conditions => cond).to_f
164 :conditions => cond).to_f
166 end
165 end
167 @key = User.current.rss_key
166 @key = User.current.rss_key
168
167
169 respond_to do |format|
168 respond_to do |format|
170 format.html
169 format.html
171 format.api
170 format.api
172 end
171 end
173 end
172 end
174
173
175 def settings
174 def settings
176 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
175 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
177 @issue_category ||= IssueCategory.new
176 @issue_category ||= IssueCategory.new
178 @member ||= @project.members.new
177 @member ||= @project.members.new
179 @trackers = Tracker.all
178 @trackers = Tracker.all
180 @repository ||= @project.repository
179 @repository ||= @project.repository
181 @wiki ||= @project.wiki
180 @wiki ||= @project.wiki
182 end
181 end
183
182
184 def edit
183 def edit
185 end
184 end
186
185
187 def update
186 def update
188 @project.safe_attributes = params[:project]
187 @project.safe_attributes = params[:project]
189 if validate_parent_id && @project.save
188 if validate_parent_id && @project.save
190 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
189 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
191 respond_to do |format|
190 respond_to do |format|
192 format.html {
191 format.html {
193 flash[:notice] = l(:notice_successful_update)
192 flash[:notice] = l(:notice_successful_update)
194 redirect_to :action => 'settings', :id => @project
193 redirect_to :action => 'settings', :id => @project
195 }
194 }
196 format.api { head :ok }
195 format.api { head :ok }
197 end
196 end
198 else
197 else
199 respond_to do |format|
198 respond_to do |format|
200 format.html {
199 format.html {
201 settings
200 settings
202 render :action => 'settings'
201 render :action => 'settings'
203 }
202 }
204 format.api { render_validation_errors(@project) }
203 format.api { render_validation_errors(@project) }
205 end
204 end
206 end
205 end
207 end
206 end
208
207
209 def modules
208 def modules
210 @project.enabled_module_names = params[:enabled_modules]
209 @project.enabled_module_names = params[:enabled_modules]
211 flash[:notice] = l(:notice_successful_update)
210 flash[:notice] = l(:notice_successful_update)
212 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
211 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
213 end
212 end
214
213
215 def archive
214 def archive
216 if request.post?
215 if request.post?
217 unless @project.archive
216 unless @project.archive
218 flash[:error] = l(:error_can_not_archive_project)
217 flash[:error] = l(:error_can_not_archive_project)
219 end
218 end
220 end
219 end
221 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
220 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
222 end
221 end
223
222
224 def unarchive
223 def unarchive
225 @project.unarchive if request.post? && !@project.active?
224 @project.unarchive if request.post? && !@project.active?
226 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
225 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
227 end
226 end
228
227
229 # Delete @project
228 # Delete @project
230 def destroy
229 def destroy
231 @project_to_destroy = @project
230 @project_to_destroy = @project
232 if request.get?
231 if request.get?
233 # display confirmation view
232 # display confirmation view
234 else
233 else
235 if api_request? || params[:confirm]
234 if api_request? || params[:confirm]
236 @project_to_destroy.destroy
235 @project_to_destroy.destroy
237 respond_to do |format|
236 respond_to do |format|
238 format.html { redirect_to :controller => 'admin', :action => 'projects' }
237 format.html { redirect_to :controller => 'admin', :action => 'projects' }
239 format.api { head :ok }
238 format.api { head :ok }
240 end
239 end
241 end
240 end
242 end
241 end
243 # hide project in layout
242 # hide project in layout
244 @project = nil
243 @project = nil
245 end
244 end
246
245
247 private
246 private
248 def find_optional_project
247 def find_optional_project
249 return true unless params[:id]
248 return true unless params[:id]
250 @project = Project.find(params[:id])
249 @project = Project.find(params[:id])
251 authorize
250 authorize
252 rescue ActiveRecord::RecordNotFound
251 rescue ActiveRecord::RecordNotFound
253 render_404
252 render_404
254 end
253 end
255
254
256 # Validates parent_id param according to user's permissions
255 # Validates parent_id param according to user's permissions
257 # TODO: move it to Project model in a validation that depends on User.current
256 # TODO: move it to Project model in a validation that depends on User.current
258 def validate_parent_id
257 def validate_parent_id
259 return true if User.current.admin?
258 return true if User.current.admin?
260 parent_id = params[:project] && params[:project][:parent_id]
259 parent_id = params[:project] && params[:project][:parent_id]
261 if parent_id || @project.new_record?
260 if parent_id || @project.new_record?
262 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
261 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
263 unless @project.allowed_parents.include?(parent)
262 unless @project.allowed_parents.include?(parent)
264 @project.errors.add :parent_id, :invalid
263 @project.errors.add :parent_id, :invalid
265 return false
264 return false
266 end
265 end
267 end
266 end
268 true
267 true
269 end
268 end
270 end
269 end
@@ -1,838 +1,841
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 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :delete_all, :include => :author
46 has_many :news, :dependent => :delete_all, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :dependent => :destroy
49 has_one :repository, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_nested_set :order => 'name'
59 acts_as_nested_set :order => 'name'
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status, :enabled_module_names
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
79 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
81
81
82 before_destroy :delete_all_members, :destroy_children
82 before_destroy :delete_all_members, :destroy_children
83
83
84 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] } }
84 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] } }
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :all_public, { :conditions => { :is_public => true } }
86 named_scope :all_public, { :conditions => { :is_public => true } }
87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
88
88
89 def initialize(attributes = nil)
89 def initialize(attributes = nil)
90 super
90 super
91
91
92 initialized = (attributes || {}).stringify_keys
92 initialized = (attributes || {}).stringify_keys
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
94 self.identifier = Project.next_identifier
94 self.identifier = Project.next_identifier
95 end
95 end
96 if !initialized.key?('is_public')
96 if !initialized.key?('is_public')
97 self.is_public = Setting.default_projects_public?
97 self.is_public = Setting.default_projects_public?
98 end
98 end
99 if !initialized.key?('enabled_module_names')
99 if !initialized.key?('enabled_module_names')
100 self.enabled_module_names = Setting.default_projects_modules
100 self.enabled_module_names = Setting.default_projects_modules
101 end
101 end
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
103 self.trackers = Tracker.all
103 self.trackers = Tracker.all
104 end
104 end
105 end
105 end
106
106
107 def identifier=(identifier)
107 def identifier=(identifier)
108 super unless identifier_frozen?
108 super unless identifier_frozen?
109 end
109 end
110
110
111 def identifier_frozen?
111 def identifier_frozen?
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
113 end
113 end
114
114
115 # returns latest created projects
115 # returns latest created projects
116 # non public projects will be returned only if user is a member of those
116 # non public projects will be returned only if user is a member of those
117 def self.latest(user=nil, count=5)
117 def self.latest(user=nil, count=5)
118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
119 end
119 end
120
120
121 # Returns a SQL :conditions string used to find all active projects for the specified user.
121 # Returns a SQL :conditions string used to find all active projects for the specified user.
122 #
122 #
123 # Examples:
123 # Examples:
124 # Projects.visible_by(admin) => "projects.status = 1"
124 # Projects.visible_by(admin) => "projects.status = 1"
125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
126 def self.visible_by(user=nil)
126 def self.visible_by(user=nil)
127 user ||= User.current
127 user ||= User.current
128 if user && user.admin?
128 if user && user.admin?
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
130 elsif user && user.memberships.any?
130 elsif user && user.memberships.any?
131 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(',')}))"
131 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(',')}))"
132 else
132 else
133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
134 end
134 end
135 end
135 end
136
136
137 def self.allowed_to_condition(user, permission, options={})
137 def self.allowed_to_condition(user, permission, options={})
138 statements = []
138 statements = []
139 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
139 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
140 if perm = Redmine::AccessControl.permission(permission)
140 if perm = Redmine::AccessControl.permission(permission)
141 unless perm.project_module.nil?
141 unless perm.project_module.nil?
142 # If the permission belongs to a project module, make sure the module is enabled
142 # If the permission belongs to a project module, make sure the module is enabled
143 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
143 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
144 end
144 end
145 end
145 end
146 if options[:project]
146 if options[:project]
147 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
147 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
148 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
148 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
149 base_statement = "(#{project_statement}) AND (#{base_statement})"
149 base_statement = "(#{project_statement}) AND (#{base_statement})"
150 end
150 end
151 if user.admin?
151 if user.admin?
152 # no restriction
152 # no restriction
153 else
153 else
154 statements << "1=0"
154 statements << "1=0"
155 if user.logged?
155 if user.logged?
156 if Role.non_member.allowed_to?(permission) && !options[:member]
156 if Role.non_member.allowed_to?(permission) && !options[:member]
157 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
158 end
158 end
159 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
159 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
160 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
160 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
161 else
161 else
162 if Role.anonymous.allowed_to?(permission) && !options[:member]
162 if Role.anonymous.allowed_to?(permission) && !options[:member]
163 # anonymous user allowed on public project
163 # anonymous user allowed on public project
164 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
164 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
165 end
165 end
166 end
166 end
167 end
167 end
168 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
168 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
169 end
169 end
170
170
171 # Returns the Systemwide and project specific activities
171 # Returns the Systemwide and project specific activities
172 def activities(include_inactive=false)
172 def activities(include_inactive=false)
173 if include_inactive
173 if include_inactive
174 return all_activities
174 return all_activities
175 else
175 else
176 return active_activities
176 return active_activities
177 end
177 end
178 end
178 end
179
179
180 # Will create a new Project specific Activity or update an existing one
180 # Will create a new Project specific Activity or update an existing one
181 #
181 #
182 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
182 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
183 # does not successfully save.
183 # does not successfully save.
184 def update_or_create_time_entry_activity(id, activity_hash)
184 def update_or_create_time_entry_activity(id, activity_hash)
185 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
185 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
186 self.create_time_entry_activity_if_needed(activity_hash)
186 self.create_time_entry_activity_if_needed(activity_hash)
187 else
187 else
188 activity = project.time_entry_activities.find_by_id(id.to_i)
188 activity = project.time_entry_activities.find_by_id(id.to_i)
189 activity.update_attributes(activity_hash) if activity
189 activity.update_attributes(activity_hash) if activity
190 end
190 end
191 end
191 end
192
192
193 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
193 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
194 #
194 #
195 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
195 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
196 # does not successfully save.
196 # does not successfully save.
197 def create_time_entry_activity_if_needed(activity)
197 def create_time_entry_activity_if_needed(activity)
198 if activity['parent_id']
198 if activity['parent_id']
199
199
200 parent_activity = TimeEntryActivity.find(activity['parent_id'])
200 parent_activity = TimeEntryActivity.find(activity['parent_id'])
201 activity['name'] = parent_activity.name
201 activity['name'] = parent_activity.name
202 activity['position'] = parent_activity.position
202 activity['position'] = parent_activity.position
203
203
204 if Enumeration.overridding_change?(activity, parent_activity)
204 if Enumeration.overridding_change?(activity, parent_activity)
205 project_activity = self.time_entry_activities.create(activity)
205 project_activity = self.time_entry_activities.create(activity)
206
206
207 if project_activity.new_record?
207 if project_activity.new_record?
208 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
208 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
209 else
209 else
210 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
210 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
211 end
211 end
212 end
212 end
213 end
213 end
214 end
214 end
215
215
216 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
216 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
217 #
217 #
218 # Examples:
218 # Examples:
219 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
219 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
220 # project.project_condition(false) => "projects.id = 1"
220 # project.project_condition(false) => "projects.id = 1"
221 def project_condition(with_subprojects)
221 def project_condition(with_subprojects)
222 cond = "#{Project.table_name}.id = #{id}"
222 cond = "#{Project.table_name}.id = #{id}"
223 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
223 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
224 cond
224 cond
225 end
225 end
226
226
227 def self.find(*args)
227 def self.find(*args)
228 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
228 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
229 project = find_by_identifier(*args)
229 project = find_by_identifier(*args)
230 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
230 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
231 project
231 project
232 else
232 else
233 super
233 super
234 end
234 end
235 end
235 end
236
236
237 def to_param
237 def to_param
238 # id is used for projects with a numeric identifier (compatibility)
238 # id is used for projects with a numeric identifier (compatibility)
239 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
239 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
240 end
240 end
241
241
242 def active?
242 def active?
243 self.status == STATUS_ACTIVE
243 self.status == STATUS_ACTIVE
244 end
244 end
245
245
246 def archived?
246 def archived?
247 self.status == STATUS_ARCHIVED
247 self.status == STATUS_ARCHIVED
248 end
248 end
249
249
250 # Archives the project and its descendants
250 # Archives the project and its descendants
251 def archive
251 def archive
252 # Check that there is no issue of a non descendant project that is assigned
252 # Check that there is no issue of a non descendant project that is assigned
253 # to one of the project or descendant versions
253 # to one of the project or descendant versions
254 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
254 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
255 if v_ids.any? && Issue.find(:first, :include => :project,
255 if v_ids.any? && Issue.find(:first, :include => :project,
256 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
256 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
257 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
257 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
258 return false
258 return false
259 end
259 end
260 Project.transaction do
260 Project.transaction do
261 archive!
261 archive!
262 end
262 end
263 true
263 true
264 end
264 end
265
265
266 # Unarchives the project
266 # Unarchives the project
267 # All its ancestors must be active
267 # All its ancestors must be active
268 def unarchive
268 def unarchive
269 return false if ancestors.detect {|a| !a.active?}
269 return false if ancestors.detect {|a| !a.active?}
270 update_attribute :status, STATUS_ACTIVE
270 update_attribute :status, STATUS_ACTIVE
271 end
271 end
272
272
273 # Returns an array of projects the project can be moved to
273 # Returns an array of projects the project can be moved to
274 # by the current user
274 # by the current user
275 def allowed_parents
275 def allowed_parents
276 return @allowed_parents if @allowed_parents
276 return @allowed_parents if @allowed_parents
277 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
277 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
278 @allowed_parents = @allowed_parents - self_and_descendants
278 @allowed_parents = @allowed_parents - self_and_descendants
279 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
279 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
280 @allowed_parents << nil
280 @allowed_parents << nil
281 end
281 end
282 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
282 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
283 @allowed_parents << parent
283 @allowed_parents << parent
284 end
284 end
285 @allowed_parents
285 @allowed_parents
286 end
286 end
287
287
288 # Sets the parent of the project with authorization check
288 # Sets the parent of the project with authorization check
289 def set_allowed_parent!(p)
289 def set_allowed_parent!(p)
290 unless p.nil? || p.is_a?(Project)
290 unless p.nil? || p.is_a?(Project)
291 if p.to_s.blank?
291 if p.to_s.blank?
292 p = nil
292 p = nil
293 else
293 else
294 p = Project.find_by_id(p)
294 p = Project.find_by_id(p)
295 return false unless p
295 return false unless p
296 end
296 end
297 end
297 end
298 if p.nil?
298 if p.nil?
299 if !new_record? && allowed_parents.empty?
299 if !new_record? && allowed_parents.empty?
300 return false
300 return false
301 end
301 end
302 elsif !allowed_parents.include?(p)
302 elsif !allowed_parents.include?(p)
303 return false
303 return false
304 end
304 end
305 set_parent!(p)
305 set_parent!(p)
306 end
306 end
307
307
308 # Sets the parent of the project
308 # Sets the parent of the project
309 # Argument can be either a Project, a String, a Fixnum or nil
309 # Argument can be either a Project, a String, a Fixnum or nil
310 def set_parent!(p)
310 def set_parent!(p)
311 unless p.nil? || p.is_a?(Project)
311 unless p.nil? || p.is_a?(Project)
312 if p.to_s.blank?
312 if p.to_s.blank?
313 p = nil
313 p = nil
314 else
314 else
315 p = Project.find_by_id(p)
315 p = Project.find_by_id(p)
316 return false unless p
316 return false unless p
317 end
317 end
318 end
318 end
319 if p == parent && !p.nil?
319 if p == parent && !p.nil?
320 # Nothing to do
320 # Nothing to do
321 true
321 true
322 elsif p.nil? || (p.active? && move_possible?(p))
322 elsif p.nil? || (p.active? && move_possible?(p))
323 # Insert the project so that target's children or root projects stay alphabetically sorted
323 # Insert the project so that target's children or root projects stay alphabetically sorted
324 sibs = (p.nil? ? self.class.roots : p.children)
324 sibs = (p.nil? ? self.class.roots : p.children)
325 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
325 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
326 if to_be_inserted_before
326 if to_be_inserted_before
327 move_to_left_of(to_be_inserted_before)
327 move_to_left_of(to_be_inserted_before)
328 elsif p.nil?
328 elsif p.nil?
329 if sibs.empty?
329 if sibs.empty?
330 # move_to_root adds the project in first (ie. left) position
330 # move_to_root adds the project in first (ie. left) position
331 move_to_root
331 move_to_root
332 else
332 else
333 move_to_right_of(sibs.last) unless self == sibs.last
333 move_to_right_of(sibs.last) unless self == sibs.last
334 end
334 end
335 else
335 else
336 # move_to_child_of adds the project in last (ie.right) position
336 # move_to_child_of adds the project in last (ie.right) position
337 move_to_child_of(p)
337 move_to_child_of(p)
338 end
338 end
339 Issue.update_versions_from_hierarchy_change(self)
339 Issue.update_versions_from_hierarchy_change(self)
340 true
340 true
341 else
341 else
342 # Can not move to the given target
342 # Can not move to the given target
343 false
343 false
344 end
344 end
345 end
345 end
346
346
347 # Returns an array of the trackers used by the project and its active sub projects
347 # Returns an array of the trackers used by the project and its active sub projects
348 def rolled_up_trackers
348 def rolled_up_trackers
349 @rolled_up_trackers ||=
349 @rolled_up_trackers ||=
350 Tracker.find(:all, :include => :projects,
350 Tracker.find(:all, :include => :projects,
351 :select => "DISTINCT #{Tracker.table_name}.*",
351 :select => "DISTINCT #{Tracker.table_name}.*",
352 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
352 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
353 :order => "#{Tracker.table_name}.position")
353 :order => "#{Tracker.table_name}.position")
354 end
354 end
355
355
356 # Closes open and locked project versions that are completed
356 # Closes open and locked project versions that are completed
357 def close_completed_versions
357 def close_completed_versions
358 Version.transaction do
358 Version.transaction do
359 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
359 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
360 if version.completed?
360 if version.completed?
361 version.update_attribute(:status, 'closed')
361 version.update_attribute(:status, 'closed')
362 end
362 end
363 end
363 end
364 end
364 end
365 end
365 end
366
366
367 # Returns a scope of the Versions on subprojects
367 # Returns a scope of the Versions on subprojects
368 def rolled_up_versions
368 def rolled_up_versions
369 @rolled_up_versions ||=
369 @rolled_up_versions ||=
370 Version.scoped(:include => :project,
370 Version.scoped(:include => :project,
371 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
371 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
372 end
372 end
373
373
374 # Returns a scope of the Versions used by the project
374 # Returns a scope of the Versions used by the project
375 def shared_versions
375 def shared_versions
376 @shared_versions ||=
376 @shared_versions ||=
377 Version.scoped(:include => :project,
377 Version.scoped(:include => :project,
378 :conditions => "#{Project.table_name}.id = #{id}" +
378 :conditions => "#{Project.table_name}.id = #{id}" +
379 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
379 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
380 " #{Version.table_name}.sharing = 'system'" +
380 " #{Version.table_name}.sharing = 'system'" +
381 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
381 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
382 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
382 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
383 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
383 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
384 "))")
384 "))")
385 end
385 end
386
386
387 # Returns a hash of project users grouped by role
387 # Returns a hash of project users grouped by role
388 def users_by_role
388 def users_by_role
389 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
389 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
390 m.roles.each do |r|
390 m.roles.each do |r|
391 h[r] ||= []
391 h[r] ||= []
392 h[r] << m.user
392 h[r] << m.user
393 end
393 end
394 h
394 h
395 end
395 end
396 end
396 end
397
397
398 # Deletes all project's members
398 # Deletes all project's members
399 def delete_all_members
399 def delete_all_members
400 me, mr = Member.table_name, MemberRole.table_name
400 me, mr = Member.table_name, MemberRole.table_name
401 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
401 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
402 Member.delete_all(['project_id = ?', id])
402 Member.delete_all(['project_id = ?', id])
403 end
403 end
404
404
405 # Users issues can be assigned to
405 # Users issues can be assigned to
406 def assignable_users
406 def assignable_users
407 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
407 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
408 end
408 end
409
409
410 # Returns the mail adresses of users that should be always notified on project events
410 # Returns the mail adresses of users that should be always notified on project events
411 def recipients
411 def recipients
412 notified_users.collect {|user| user.mail}
412 notified_users.collect {|user| user.mail}
413 end
413 end
414
414
415 # Returns the users that should be notified on project events
415 # Returns the users that should be notified on project events
416 def notified_users
416 def notified_users
417 # TODO: User part should be extracted to User#notify_about?
417 # TODO: User part should be extracted to User#notify_about?
418 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
418 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
419 end
419 end
420
420
421 # Returns an array of all custom fields enabled for project issues
421 # Returns an array of all custom fields enabled for project issues
422 # (explictly associated custom fields and custom fields enabled for all projects)
422 # (explictly associated custom fields and custom fields enabled for all projects)
423 def all_issue_custom_fields
423 def all_issue_custom_fields
424 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
424 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
425 end
425 end
426
426
427 def project
427 def project
428 self
428 self
429 end
429 end
430
430
431 def <=>(project)
431 def <=>(project)
432 name.downcase <=> project.name.downcase
432 name.downcase <=> project.name.downcase
433 end
433 end
434
434
435 def to_s
435 def to_s
436 name
436 name
437 end
437 end
438
438
439 # Returns a short description of the projects (first lines)
439 # Returns a short description of the projects (first lines)
440 def short_description(length = 255)
440 def short_description(length = 255)
441 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
441 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
442 end
442 end
443
443
444 def css_classes
444 def css_classes
445 s = 'project'
445 s = 'project'
446 s << ' root' if root?
446 s << ' root' if root?
447 s << ' child' if child?
447 s << ' child' if child?
448 s << (leaf? ? ' leaf' : ' parent')
448 s << (leaf? ? ' leaf' : ' parent')
449 s
449 s
450 end
450 end
451
451
452 # The earliest start date of a project, based on it's issues and versions
452 # The earliest start date of a project, based on it's issues and versions
453 def start_date
453 def start_date
454 [
454 [
455 issues.minimum('start_date'),
455 issues.minimum('start_date'),
456 shared_versions.collect(&:effective_date),
456 shared_versions.collect(&:effective_date),
457 shared_versions.collect(&:start_date)
457 shared_versions.collect(&:start_date)
458 ].flatten.compact.min
458 ].flatten.compact.min
459 end
459 end
460
460
461 # The latest due date of an issue or version
461 # The latest due date of an issue or version
462 def due_date
462 def due_date
463 [
463 [
464 issues.maximum('due_date'),
464 issues.maximum('due_date'),
465 shared_versions.collect(&:effective_date),
465 shared_versions.collect(&:effective_date),
466 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
466 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
467 ].flatten.compact.max
467 ].flatten.compact.max
468 end
468 end
469
469
470 def overdue?
470 def overdue?
471 active? && !due_date.nil? && (due_date < Date.today)
471 active? && !due_date.nil? && (due_date < Date.today)
472 end
472 end
473
473
474 # Returns the percent completed for this project, based on the
474 # Returns the percent completed for this project, based on the
475 # progress on it's versions.
475 # progress on it's versions.
476 def completed_percent(options={:include_subprojects => false})
476 def completed_percent(options={:include_subprojects => false})
477 if options.delete(:include_subprojects)
477 if options.delete(:include_subprojects)
478 total = self_and_descendants.collect(&:completed_percent).sum
478 total = self_and_descendants.collect(&:completed_percent).sum
479
479
480 total / self_and_descendants.count
480 total / self_and_descendants.count
481 else
481 else
482 if versions.count > 0
482 if versions.count > 0
483 total = versions.collect(&:completed_pourcent).sum
483 total = versions.collect(&:completed_pourcent).sum
484
484
485 total / versions.count
485 total / versions.count
486 else
486 else
487 100
487 100
488 end
488 end
489 end
489 end
490 end
490 end
491
491
492 # Return true if this project is allowed to do the specified action.
492 # Return true if this project is allowed to do the specified action.
493 # action can be:
493 # action can be:
494 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
494 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
495 # * a permission Symbol (eg. :edit_project)
495 # * a permission Symbol (eg. :edit_project)
496 def allows_to?(action)
496 def allows_to?(action)
497 if action.is_a? Hash
497 if action.is_a? Hash
498 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
498 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
499 else
499 else
500 allowed_permissions.include? action
500 allowed_permissions.include? action
501 end
501 end
502 end
502 end
503
503
504 def module_enabled?(module_name)
504 def module_enabled?(module_name)
505 module_name = module_name.to_s
505 module_name = module_name.to_s
506 enabled_modules.detect {|m| m.name == module_name}
506 enabled_modules.detect {|m| m.name == module_name}
507 end
507 end
508
508
509 def enabled_module_names=(module_names)
509 def enabled_module_names=(module_names)
510 if module_names && module_names.is_a?(Array)
510 if module_names && module_names.is_a?(Array)
511 module_names = module_names.collect(&:to_s).reject(&:blank?)
511 module_names = module_names.collect(&:to_s).reject(&:blank?)
512 # remove disabled modules
512 # remove disabled modules
513 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
513 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
514 # add new modules
514 # add new modules
515 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
515 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
516 else
516 else
517 enabled_modules.clear
517 enabled_modules.clear
518 end
518 end
519 end
519 end
520
520
521 # Returns an array of the enabled modules names
521 # Returns an array of the enabled modules names
522 def enabled_module_names
522 def enabled_module_names
523 enabled_modules.collect(&:name)
523 enabled_modules.collect(&:name)
524 end
524 end
525
525
526 safe_attributes 'name',
526 safe_attributes 'name',
527 'description',
527 'description',
528 'homepage',
528 'homepage',
529 'is_public',
529 'is_public',
530 'identifier',
530 'identifier',
531 'custom_field_values',
531 'custom_field_values',
532 'custom_fields',
532 'custom_fields',
533 'tracker_ids',
533 'tracker_ids',
534 'issue_custom_field_ids'
534 'issue_custom_field_ids'
535
535
536 safe_attributes 'enabled_module_names',
537 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
538
536 # Returns an array of projects that are in this project's hierarchy
539 # Returns an array of projects that are in this project's hierarchy
537 #
540 #
538 # Example: parents, children, siblings
541 # Example: parents, children, siblings
539 def hierarchy
542 def hierarchy
540 parents = project.self_and_ancestors || []
543 parents = project.self_and_ancestors || []
541 descendants = project.descendants || []
544 descendants = project.descendants || []
542 project_hierarchy = parents | descendants # Set union
545 project_hierarchy = parents | descendants # Set union
543 end
546 end
544
547
545 # Returns an auto-generated project identifier based on the last identifier used
548 # Returns an auto-generated project identifier based on the last identifier used
546 def self.next_identifier
549 def self.next_identifier
547 p = Project.find(:first, :order => 'created_on DESC')
550 p = Project.find(:first, :order => 'created_on DESC')
548 p.nil? ? nil : p.identifier.to_s.succ
551 p.nil? ? nil : p.identifier.to_s.succ
549 end
552 end
550
553
551 # Copies and saves the Project instance based on the +project+.
554 # Copies and saves the Project instance based on the +project+.
552 # Duplicates the source project's:
555 # Duplicates the source project's:
553 # * Wiki
556 # * Wiki
554 # * Versions
557 # * Versions
555 # * Categories
558 # * Categories
556 # * Issues
559 # * Issues
557 # * Members
560 # * Members
558 # * Queries
561 # * Queries
559 #
562 #
560 # Accepts an +options+ argument to specify what to copy
563 # Accepts an +options+ argument to specify what to copy
561 #
564 #
562 # Examples:
565 # Examples:
563 # project.copy(1) # => copies everything
566 # project.copy(1) # => copies everything
564 # project.copy(1, :only => 'members') # => copies members only
567 # project.copy(1, :only => 'members') # => copies members only
565 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
568 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
566 def copy(project, options={})
569 def copy(project, options={})
567 project = project.is_a?(Project) ? project : Project.find(project)
570 project = project.is_a?(Project) ? project : Project.find(project)
568
571
569 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
572 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
570 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
573 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
571
574
572 Project.transaction do
575 Project.transaction do
573 if save
576 if save
574 reload
577 reload
575 to_be_copied.each do |name|
578 to_be_copied.each do |name|
576 send "copy_#{name}", project
579 send "copy_#{name}", project
577 end
580 end
578 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
581 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
579 save
582 save
580 end
583 end
581 end
584 end
582 end
585 end
583
586
584
587
585 # Copies +project+ and returns the new instance. This will not save
588 # Copies +project+ and returns the new instance. This will not save
586 # the copy
589 # the copy
587 def self.copy_from(project)
590 def self.copy_from(project)
588 begin
591 begin
589 project = project.is_a?(Project) ? project : Project.find(project)
592 project = project.is_a?(Project) ? project : Project.find(project)
590 if project
593 if project
591 # clear unique attributes
594 # clear unique attributes
592 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
595 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
593 copy = Project.new(attributes)
596 copy = Project.new(attributes)
594 copy.enabled_modules = project.enabled_modules
597 copy.enabled_modules = project.enabled_modules
595 copy.trackers = project.trackers
598 copy.trackers = project.trackers
596 copy.custom_values = project.custom_values.collect {|v| v.clone}
599 copy.custom_values = project.custom_values.collect {|v| v.clone}
597 copy.issue_custom_fields = project.issue_custom_fields
600 copy.issue_custom_fields = project.issue_custom_fields
598 return copy
601 return copy
599 else
602 else
600 return nil
603 return nil
601 end
604 end
602 rescue ActiveRecord::RecordNotFound
605 rescue ActiveRecord::RecordNotFound
603 return nil
606 return nil
604 end
607 end
605 end
608 end
606
609
607 # Yields the given block for each project with its level in the tree
610 # Yields the given block for each project with its level in the tree
608 def self.project_tree(projects, &block)
611 def self.project_tree(projects, &block)
609 ancestors = []
612 ancestors = []
610 projects.sort_by(&:lft).each do |project|
613 projects.sort_by(&:lft).each do |project|
611 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
614 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
612 ancestors.pop
615 ancestors.pop
613 end
616 end
614 yield project, ancestors.size
617 yield project, ancestors.size
615 ancestors << project
618 ancestors << project
616 end
619 end
617 end
620 end
618
621
619 private
622 private
620
623
621 # Destroys children before destroying self
624 # Destroys children before destroying self
622 def destroy_children
625 def destroy_children
623 children.each do |child|
626 children.each do |child|
624 child.destroy
627 child.destroy
625 end
628 end
626 end
629 end
627
630
628 # Copies wiki from +project+
631 # Copies wiki from +project+
629 def copy_wiki(project)
632 def copy_wiki(project)
630 # Check that the source project has a wiki first
633 # Check that the source project has a wiki first
631 unless project.wiki.nil?
634 unless project.wiki.nil?
632 self.wiki ||= Wiki.new
635 self.wiki ||= Wiki.new
633 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
636 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
634 wiki_pages_map = {}
637 wiki_pages_map = {}
635 project.wiki.pages.each do |page|
638 project.wiki.pages.each do |page|
636 # Skip pages without content
639 # Skip pages without content
637 next if page.content.nil?
640 next if page.content.nil?
638 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
641 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
639 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
642 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
640 new_wiki_page.content = new_wiki_content
643 new_wiki_page.content = new_wiki_content
641 wiki.pages << new_wiki_page
644 wiki.pages << new_wiki_page
642 wiki_pages_map[page.id] = new_wiki_page
645 wiki_pages_map[page.id] = new_wiki_page
643 end
646 end
644 wiki.save
647 wiki.save
645 # Reproduce page hierarchy
648 # Reproduce page hierarchy
646 project.wiki.pages.each do |page|
649 project.wiki.pages.each do |page|
647 if page.parent_id && wiki_pages_map[page.id]
650 if page.parent_id && wiki_pages_map[page.id]
648 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
651 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
649 wiki_pages_map[page.id].save
652 wiki_pages_map[page.id].save
650 end
653 end
651 end
654 end
652 end
655 end
653 end
656 end
654
657
655 # Copies versions from +project+
658 # Copies versions from +project+
656 def copy_versions(project)
659 def copy_versions(project)
657 project.versions.each do |version|
660 project.versions.each do |version|
658 new_version = Version.new
661 new_version = Version.new
659 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
662 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
660 self.versions << new_version
663 self.versions << new_version
661 end
664 end
662 end
665 end
663
666
664 # Copies issue categories from +project+
667 # Copies issue categories from +project+
665 def copy_issue_categories(project)
668 def copy_issue_categories(project)
666 project.issue_categories.each do |issue_category|
669 project.issue_categories.each do |issue_category|
667 new_issue_category = IssueCategory.new
670 new_issue_category = IssueCategory.new
668 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
671 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
669 self.issue_categories << new_issue_category
672 self.issue_categories << new_issue_category
670 end
673 end
671 end
674 end
672
675
673 # Copies issues from +project+
676 # Copies issues from +project+
674 def copy_issues(project)
677 def copy_issues(project)
675 # Stores the source issue id as a key and the copied issues as the
678 # Stores the source issue id as a key and the copied issues as the
676 # value. Used to map the two togeather for issue relations.
679 # value. Used to map the two togeather for issue relations.
677 issues_map = {}
680 issues_map = {}
678
681
679 # Get issues sorted by root_id, lft so that parent issues
682 # Get issues sorted by root_id, lft so that parent issues
680 # get copied before their children
683 # get copied before their children
681 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
684 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
682 new_issue = Issue.new
685 new_issue = Issue.new
683 new_issue.copy_from(issue)
686 new_issue.copy_from(issue)
684 new_issue.project = self
687 new_issue.project = self
685 # Reassign fixed_versions by name, since names are unique per
688 # Reassign fixed_versions by name, since names are unique per
686 # project and the versions for self are not yet saved
689 # project and the versions for self are not yet saved
687 if issue.fixed_version
690 if issue.fixed_version
688 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
691 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
689 end
692 end
690 # Reassign the category by name, since names are unique per
693 # Reassign the category by name, since names are unique per
691 # project and the categories for self are not yet saved
694 # project and the categories for self are not yet saved
692 if issue.category
695 if issue.category
693 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
696 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
694 end
697 end
695 # Parent issue
698 # Parent issue
696 if issue.parent_id
699 if issue.parent_id
697 if copied_parent = issues_map[issue.parent_id]
700 if copied_parent = issues_map[issue.parent_id]
698 new_issue.parent_issue_id = copied_parent.id
701 new_issue.parent_issue_id = copied_parent.id
699 end
702 end
700 end
703 end
701
704
702 self.issues << new_issue
705 self.issues << new_issue
703 if new_issue.new_record?
706 if new_issue.new_record?
704 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
707 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
705 else
708 else
706 issues_map[issue.id] = new_issue unless new_issue.new_record?
709 issues_map[issue.id] = new_issue unless new_issue.new_record?
707 end
710 end
708 end
711 end
709
712
710 # Relations after in case issues related each other
713 # Relations after in case issues related each other
711 project.issues.each do |issue|
714 project.issues.each do |issue|
712 new_issue = issues_map[issue.id]
715 new_issue = issues_map[issue.id]
713 unless new_issue
716 unless new_issue
714 # Issue was not copied
717 # Issue was not copied
715 next
718 next
716 end
719 end
717
720
718 # Relations
721 # Relations
719 issue.relations_from.each do |source_relation|
722 issue.relations_from.each do |source_relation|
720 new_issue_relation = IssueRelation.new
723 new_issue_relation = IssueRelation.new
721 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
724 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
722 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
725 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
723 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
726 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
724 new_issue_relation.issue_to = source_relation.issue_to
727 new_issue_relation.issue_to = source_relation.issue_to
725 end
728 end
726 new_issue.relations_from << new_issue_relation
729 new_issue.relations_from << new_issue_relation
727 end
730 end
728
731
729 issue.relations_to.each do |source_relation|
732 issue.relations_to.each do |source_relation|
730 new_issue_relation = IssueRelation.new
733 new_issue_relation = IssueRelation.new
731 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
734 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
732 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
735 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
733 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
736 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
734 new_issue_relation.issue_from = source_relation.issue_from
737 new_issue_relation.issue_from = source_relation.issue_from
735 end
738 end
736 new_issue.relations_to << new_issue_relation
739 new_issue.relations_to << new_issue_relation
737 end
740 end
738 end
741 end
739 end
742 end
740
743
741 # Copies members from +project+
744 # Copies members from +project+
742 def copy_members(project)
745 def copy_members(project)
743 # Copy users first, then groups to handle members with inherited and given roles
746 # Copy users first, then groups to handle members with inherited and given roles
744 members_to_copy = []
747 members_to_copy = []
745 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
748 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
746 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
749 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
747
750
748 members_to_copy.each do |member|
751 members_to_copy.each do |member|
749 new_member = Member.new
752 new_member = Member.new
750 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
753 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
751 # only copy non inherited roles
754 # only copy non inherited roles
752 # inherited roles will be added when copying the group membership
755 # inherited roles will be added when copying the group membership
753 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
756 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
754 next if role_ids.empty?
757 next if role_ids.empty?
755 new_member.role_ids = role_ids
758 new_member.role_ids = role_ids
756 new_member.project = self
759 new_member.project = self
757 self.members << new_member
760 self.members << new_member
758 end
761 end
759 end
762 end
760
763
761 # Copies queries from +project+
764 # Copies queries from +project+
762 def copy_queries(project)
765 def copy_queries(project)
763 project.queries.each do |query|
766 project.queries.each do |query|
764 new_query = Query.new
767 new_query = Query.new
765 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
768 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
766 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
769 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
767 new_query.project = self
770 new_query.project = self
768 self.queries << new_query
771 self.queries << new_query
769 end
772 end
770 end
773 end
771
774
772 # Copies boards from +project+
775 # Copies boards from +project+
773 def copy_boards(project)
776 def copy_boards(project)
774 project.boards.each do |board|
777 project.boards.each do |board|
775 new_board = Board.new
778 new_board = Board.new
776 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
779 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
777 new_board.project = self
780 new_board.project = self
778 self.boards << new_board
781 self.boards << new_board
779 end
782 end
780 end
783 end
781
784
782 def allowed_permissions
785 def allowed_permissions
783 @allowed_permissions ||= begin
786 @allowed_permissions ||= begin
784 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
787 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
785 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
788 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
786 end
789 end
787 end
790 end
788
791
789 def allowed_actions
792 def allowed_actions
790 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
793 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
791 end
794 end
792
795
793 # Returns all the active Systemwide and project specific activities
796 # Returns all the active Systemwide and project specific activities
794 def active_activities
797 def active_activities
795 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
798 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
796
799
797 if overridden_activity_ids.empty?
800 if overridden_activity_ids.empty?
798 return TimeEntryActivity.shared.active
801 return TimeEntryActivity.shared.active
799 else
802 else
800 return system_activities_and_project_overrides
803 return system_activities_and_project_overrides
801 end
804 end
802 end
805 end
803
806
804 # Returns all the Systemwide and project specific activities
807 # Returns all the Systemwide and project specific activities
805 # (inactive and active)
808 # (inactive and active)
806 def all_activities
809 def all_activities
807 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
810 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
808
811
809 if overridden_activity_ids.empty?
812 if overridden_activity_ids.empty?
810 return TimeEntryActivity.shared
813 return TimeEntryActivity.shared
811 else
814 else
812 return system_activities_and_project_overrides(true)
815 return system_activities_and_project_overrides(true)
813 end
816 end
814 end
817 end
815
818
816 # Returns the systemwide active activities merged with the project specific overrides
819 # Returns the systemwide active activities merged with the project specific overrides
817 def system_activities_and_project_overrides(include_inactive=false)
820 def system_activities_and_project_overrides(include_inactive=false)
818 if include_inactive
821 if include_inactive
819 return TimeEntryActivity.shared.
822 return TimeEntryActivity.shared.
820 find(:all,
823 find(:all,
821 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
824 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
822 self.time_entry_activities
825 self.time_entry_activities
823 else
826 else
824 return TimeEntryActivity.shared.active.
827 return TimeEntryActivity.shared.active.
825 find(:all,
828 find(:all,
826 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
829 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
827 self.time_entry_activities.active
830 self.time_entry_activities.active
828 end
831 end
829 end
832 end
830
833
831 # Archives subprojects recursively
834 # Archives subprojects recursively
832 def archive!
835 def archive!
833 children.each do |subproject|
836 children.each do |subproject|
834 subproject.send :archive!
837 subproject.send :archive!
835 end
838 end
836 update_attribute :status, STATUS_ARCHIVED
839 update_attribute :status, STATUS_ARCHIVED
837 end
840 end
838 end
841 end
@@ -1,19 +1,19
1 <h2><%=l(:label_project_new)%></h2>
1 <h2><%=l(:label_project_new)%></h2>
2
2
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "create" } do |f| %>
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "create" } do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5
5
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 <label class="floating">
8 <label class="floating">
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
9 <%= check_box_tag 'project[enabled_module_names][]', m, @project.module_enabled?(m) %>
10 <%= l_or_humanize(m, :prefix => "project_module_") %>
10 <%= l_or_humanize(m, :prefix => "project_module_") %>
11 </label>
11 </label>
12 <% end %>
12 <% end %>
13 <%= hidden_field_tag 'enabled_modules[]', '' %>
13 <%= hidden_field_tag 'project[enabled_module_names][]', '' %>
14
14
15 </fieldset>
15 </fieldset>
16
16
17 <%= submit_tag l(:button_save) %>
17 <%= submit_tag l(:button_save) %>
18 <%= javascript_tag "Form.Element.focus('project_name');" %>
18 <%= javascript_tag "Form.Element.focus('project_name');" %>
19 <% end %>
19 <% end %>
@@ -1,187 +1,188
1 ---
1 ---
2 roles_001:
2 roles_001:
3 name: Manager
3 name: Manager
4 id: 1
4 id: 1
5 builtin: 0
5 builtin: 0
6 permissions: |
6 permissions: |
7 ---
7 ---
8 - :add_project
8 - :add_project
9 - :edit_project
9 - :edit_project
10 - :select_project_modules
10 - :manage_members
11 - :manage_members
11 - :manage_versions
12 - :manage_versions
12 - :manage_categories
13 - :manage_categories
13 - :view_issues
14 - :view_issues
14 - :add_issues
15 - :add_issues
15 - :edit_issues
16 - :edit_issues
16 - :manage_issue_relations
17 - :manage_issue_relations
17 - :manage_subtasks
18 - :manage_subtasks
18 - :add_issue_notes
19 - :add_issue_notes
19 - :move_issues
20 - :move_issues
20 - :delete_issues
21 - :delete_issues
21 - :view_issue_watchers
22 - :view_issue_watchers
22 - :add_issue_watchers
23 - :add_issue_watchers
23 - :delete_issue_watchers
24 - :delete_issue_watchers
24 - :manage_public_queries
25 - :manage_public_queries
25 - :save_queries
26 - :save_queries
26 - :view_gantt
27 - :view_gantt
27 - :view_calendar
28 - :view_calendar
28 - :log_time
29 - :log_time
29 - :view_time_entries
30 - :view_time_entries
30 - :edit_time_entries
31 - :edit_time_entries
31 - :delete_time_entries
32 - :delete_time_entries
32 - :manage_news
33 - :manage_news
33 - :comment_news
34 - :comment_news
34 - :view_documents
35 - :view_documents
35 - :manage_documents
36 - :manage_documents
36 - :view_wiki_pages
37 - :view_wiki_pages
37 - :export_wiki_pages
38 - :export_wiki_pages
38 - :view_wiki_edits
39 - :view_wiki_edits
39 - :edit_wiki_pages
40 - :edit_wiki_pages
40 - :delete_wiki_pages_attachments
41 - :delete_wiki_pages_attachments
41 - :protect_wiki_pages
42 - :protect_wiki_pages
42 - :delete_wiki_pages
43 - :delete_wiki_pages
43 - :rename_wiki_pages
44 - :rename_wiki_pages
44 - :add_messages
45 - :add_messages
45 - :edit_messages
46 - :edit_messages
46 - :delete_messages
47 - :delete_messages
47 - :manage_boards
48 - :manage_boards
48 - :view_files
49 - :view_files
49 - :manage_files
50 - :manage_files
50 - :browse_repository
51 - :browse_repository
51 - :manage_repository
52 - :manage_repository
52 - :view_changesets
53 - :view_changesets
53 - :manage_project_activities
54 - :manage_project_activities
54
55
55 position: 1
56 position: 1
56 roles_002:
57 roles_002:
57 name: Developer
58 name: Developer
58 id: 2
59 id: 2
59 builtin: 0
60 builtin: 0
60 permissions: |
61 permissions: |
61 ---
62 ---
62 - :edit_project
63 - :edit_project
63 - :manage_members
64 - :manage_members
64 - :manage_versions
65 - :manage_versions
65 - :manage_categories
66 - :manage_categories
66 - :view_issues
67 - :view_issues
67 - :add_issues
68 - :add_issues
68 - :edit_issues
69 - :edit_issues
69 - :manage_issue_relations
70 - :manage_issue_relations
70 - :manage_subtasks
71 - :manage_subtasks
71 - :add_issue_notes
72 - :add_issue_notes
72 - :move_issues
73 - :move_issues
73 - :delete_issues
74 - :delete_issues
74 - :view_issue_watchers
75 - :view_issue_watchers
75 - :save_queries
76 - :save_queries
76 - :view_gantt
77 - :view_gantt
77 - :view_calendar
78 - :view_calendar
78 - :log_time
79 - :log_time
79 - :view_time_entries
80 - :view_time_entries
80 - :edit_own_time_entries
81 - :edit_own_time_entries
81 - :manage_news
82 - :manage_news
82 - :comment_news
83 - :comment_news
83 - :view_documents
84 - :view_documents
84 - :manage_documents
85 - :manage_documents
85 - :view_wiki_pages
86 - :view_wiki_pages
86 - :view_wiki_edits
87 - :view_wiki_edits
87 - :edit_wiki_pages
88 - :edit_wiki_pages
88 - :protect_wiki_pages
89 - :protect_wiki_pages
89 - :delete_wiki_pages
90 - :delete_wiki_pages
90 - :add_messages
91 - :add_messages
91 - :edit_own_messages
92 - :edit_own_messages
92 - :delete_own_messages
93 - :delete_own_messages
93 - :manage_boards
94 - :manage_boards
94 - :view_files
95 - :view_files
95 - :manage_files
96 - :manage_files
96 - :browse_repository
97 - :browse_repository
97 - :view_changesets
98 - :view_changesets
98
99
99 position: 2
100 position: 2
100 roles_003:
101 roles_003:
101 name: Reporter
102 name: Reporter
102 id: 3
103 id: 3
103 builtin: 0
104 builtin: 0
104 permissions: |
105 permissions: |
105 ---
106 ---
106 - :edit_project
107 - :edit_project
107 - :manage_members
108 - :manage_members
108 - :manage_versions
109 - :manage_versions
109 - :manage_categories
110 - :manage_categories
110 - :view_issues
111 - :view_issues
111 - :add_issues
112 - :add_issues
112 - :edit_issues
113 - :edit_issues
113 - :manage_issue_relations
114 - :manage_issue_relations
114 - :add_issue_notes
115 - :add_issue_notes
115 - :move_issues
116 - :move_issues
116 - :view_issue_watchers
117 - :view_issue_watchers
117 - :save_queries
118 - :save_queries
118 - :view_gantt
119 - :view_gantt
119 - :view_calendar
120 - :view_calendar
120 - :log_time
121 - :log_time
121 - :view_time_entries
122 - :view_time_entries
122 - :manage_news
123 - :manage_news
123 - :comment_news
124 - :comment_news
124 - :view_documents
125 - :view_documents
125 - :manage_documents
126 - :manage_documents
126 - :view_wiki_pages
127 - :view_wiki_pages
127 - :view_wiki_edits
128 - :view_wiki_edits
128 - :edit_wiki_pages
129 - :edit_wiki_pages
129 - :delete_wiki_pages
130 - :delete_wiki_pages
130 - :add_messages
131 - :add_messages
131 - :manage_boards
132 - :manage_boards
132 - :view_files
133 - :view_files
133 - :manage_files
134 - :manage_files
134 - :browse_repository
135 - :browse_repository
135 - :view_changesets
136 - :view_changesets
136
137
137 position: 3
138 position: 3
138 roles_004:
139 roles_004:
139 name: Non member
140 name: Non member
140 id: 4
141 id: 4
141 builtin: 1
142 builtin: 1
142 permissions: |
143 permissions: |
143 ---
144 ---
144 - :view_issues
145 - :view_issues
145 - :add_issues
146 - :add_issues
146 - :edit_issues
147 - :edit_issues
147 - :manage_issue_relations
148 - :manage_issue_relations
148 - :add_issue_notes
149 - :add_issue_notes
149 - :move_issues
150 - :move_issues
150 - :save_queries
151 - :save_queries
151 - :view_gantt
152 - :view_gantt
152 - :view_calendar
153 - :view_calendar
153 - :log_time
154 - :log_time
154 - :view_time_entries
155 - :view_time_entries
155 - :comment_news
156 - :comment_news
156 - :view_documents
157 - :view_documents
157 - :manage_documents
158 - :manage_documents
158 - :view_wiki_pages
159 - :view_wiki_pages
159 - :view_wiki_edits
160 - :view_wiki_edits
160 - :edit_wiki_pages
161 - :edit_wiki_pages
161 - :add_messages
162 - :add_messages
162 - :view_files
163 - :view_files
163 - :manage_files
164 - :manage_files
164 - :browse_repository
165 - :browse_repository
165 - :view_changesets
166 - :view_changesets
166
167
167 position: 4
168 position: 4
168 roles_005:
169 roles_005:
169 name: Anonymous
170 name: Anonymous
170 id: 5
171 id: 5
171 builtin: 2
172 builtin: 2
172 permissions: |
173 permissions: |
173 ---
174 ---
174 - :view_issues
175 - :view_issues
175 - :add_issue_notes
176 - :add_issue_notes
176 - :view_gantt
177 - :view_gantt
177 - :view_calendar
178 - :view_calendar
178 - :view_time_entries
179 - :view_time_entries
179 - :view_documents
180 - :view_documents
180 - :view_wiki_pages
181 - :view_wiki_pages
181 - :view_wiki_edits
182 - :view_wiki_edits
182 - :view_files
183 - :view_files
183 - :browse_repository
184 - :browse_repository
184 - :view_changesets
185 - :view_changesets
185
186
186 position: 5
187 position: 5
187
188
@@ -1,471 +1,477
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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
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, :time_entries
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
37 def test_index
38 get :index
38 get :index
39 assert_response :success
39 assert_response :success
40 assert_template 'index'
40 assert_template 'index'
41 assert_not_nil assigns(:projects)
41 assert_not_nil assigns(:projects)
42
42
43 assert_tag :ul, :child => {:tag => 'li',
43 assert_tag :ul, :child => {:tag => 'li',
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
45 :child => { :tag => 'ul',
45 :child => { :tag => 'ul',
46 :descendant => { :tag => 'a',
46 :descendant => { :tag => 'a',
47 :content => 'Child of private child'
47 :content => 'Child of private child'
48 }
48 }
49 }
49 }
50 }
50 }
51
51
52 assert_no_tag :a, :content => /Private child of eCookbook/
52 assert_no_tag :a, :content => /Private child of eCookbook/
53 end
53 end
54
54
55 def test_index_atom
55 def test_index_atom
56 get :index, :format => 'atom'
56 get :index, :format => 'atom'
57 assert_response :success
57 assert_response :success
58 assert_template 'common/feed.atom.rxml'
58 assert_template 'common/feed.atom.rxml'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
61 end
61 end
62
62
63 context "#index" do
63 context "#index" do
64 context "by non-admin user with view_time_entries permission" do
64 context "by non-admin user with view_time_entries permission" do
65 setup do
65 setup do
66 @request.session[:user_id] = 3
66 @request.session[:user_id] = 3
67 end
67 end
68 should "show overall spent time link" do
68 should "show overall spent time link" do
69 get :index
69 get :index
70 assert_template 'index'
70 assert_template 'index'
71 assert_tag :a, :attributes => {:href => '/time_entries'}
71 assert_tag :a, :attributes => {:href => '/time_entries'}
72 end
72 end
73 end
73 end
74
74
75 context "by non-admin user without view_time_entries permission" do
75 context "by non-admin user without view_time_entries permission" do
76 setup do
76 setup do
77 Role.find(2).remove_permission! :view_time_entries
77 Role.find(2).remove_permission! :view_time_entries
78 Role.non_member.remove_permission! :view_time_entries
78 Role.non_member.remove_permission! :view_time_entries
79 Role.anonymous.remove_permission! :view_time_entries
79 Role.anonymous.remove_permission! :view_time_entries
80 @request.session[:user_id] = 3
80 @request.session[:user_id] = 3
81 end
81 end
82 should "not show overall spent time link" do
82 should "not show overall spent time link" do
83 get :index
83 get :index
84 assert_template 'index'
84 assert_template 'index'
85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
86 end
86 end
87 end
87 end
88 end
88 end
89
89
90 context "#new" do
90 context "#new" do
91 context "by admin user" do
91 context "by admin user" do
92 setup do
92 setup do
93 @request.session[:user_id] = 1
93 @request.session[:user_id] = 1
94 end
94 end
95
95
96 should "accept get" do
96 should "accept get" do
97 get :new
97 get :new
98 assert_response :success
98 assert_response :success
99 assert_template 'new'
99 assert_template 'new'
100 end
100 end
101
101
102 end
102 end
103
103
104 context "by non-admin user with add_project permission" do
104 context "by non-admin user with add_project permission" do
105 setup do
105 setup do
106 Role.non_member.add_permission! :add_project
106 Role.non_member.add_permission! :add_project
107 @request.session[:user_id] = 9
107 @request.session[:user_id] = 9
108 end
108 end
109
109
110 should "accept get" do
110 should "accept get" do
111 get :new
111 get :new
112 assert_response :success
112 assert_response :success
113 assert_template 'new'
113 assert_template 'new'
114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
115 end
115 end
116 end
116 end
117
117
118 context "by non-admin user with add_subprojects permission" do
118 context "by non-admin user with add_subprojects permission" do
119 setup do
119 setup do
120 Role.find(1).remove_permission! :add_project
120 Role.find(1).remove_permission! :add_project
121 Role.find(1).add_permission! :add_subprojects
121 Role.find(1).add_permission! :add_subprojects
122 @request.session[:user_id] = 2
122 @request.session[:user_id] = 2
123 end
123 end
124
124
125 should "accept get" do
125 should "accept get" do
126 get :new, :parent_id => 'ecookbook'
126 get :new, :parent_id => 'ecookbook'
127 assert_response :success
127 assert_response :success
128 assert_template 'new'
128 assert_template 'new'
129 # parent project selected
129 # parent project selected
130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
132 # no empty value
132 # no empty value
133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
134 :child => {:tag => 'option', :attributes => {:value => ''}}
134 :child => {:tag => 'option', :attributes => {:value => ''}}
135 end
135 end
136 end
136 end
137
137
138 end
138 end
139
139
140 context "POST :create" do
140 context "POST :create" do
141 context "by admin user" do
141 context "by admin user" do
142 setup do
142 setup do
143 @request.session[:user_id] = 1
143 @request.session[:user_id] = 1
144 end
144 end
145
145
146 should "create a new project" do
146 should "create a new project" do
147 post :create,
147 post :create,
148 :project => {
148 :project => {
149 :name => "blog",
149 :name => "blog",
150 :description => "weblog",
150 :description => "weblog",
151 :homepage => 'http://weblog',
151 :homepage => 'http://weblog',
152 :identifier => "blog",
152 :identifier => "blog",
153 :is_public => 1,
153 :is_public => 1,
154 :custom_field_values => { '3' => 'Beta' },
154 :custom_field_values => { '3' => 'Beta' },
155 :tracker_ids => ['1', '3'],
155 :tracker_ids => ['1', '3'],
156 # an issue custom field that is not for all project
156 # an issue custom field that is not for all project
157 :issue_custom_field_ids => ['9']
157 :issue_custom_field_ids => ['9'],
158 :enabled_module_names => ['issue_tracking', 'news', 'repository']
158 }
159 }
159 assert_redirected_to '/projects/blog/settings'
160 assert_redirected_to '/projects/blog/settings'
160
161
161 project = Project.find_by_name('blog')
162 project = Project.find_by_name('blog')
162 assert_kind_of Project, project
163 assert_kind_of Project, project
163 assert project.active?
164 assert project.active?
164 assert_equal 'weblog', project.description
165 assert_equal 'weblog', project.description
165 assert_equal 'http://weblog', project.homepage
166 assert_equal 'http://weblog', project.homepage
166 assert_equal true, project.is_public?
167 assert_equal true, project.is_public?
167 assert_nil project.parent
168 assert_nil project.parent
168 assert_equal 'Beta', project.custom_value_for(3).value
169 assert_equal 'Beta', project.custom_value_for(3).value
169 assert_equal [1, 3], project.trackers.map(&:id).sort
170 assert_equal [1, 3], project.trackers.map(&:id).sort
171 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
170 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
172 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
171 end
173 end
172
174
173 should "create a new subproject" do
175 should "create a new subproject" do
174 post :create, :project => { :name => "blog",
176 post :create, :project => { :name => "blog",
175 :description => "weblog",
177 :description => "weblog",
176 :identifier => "blog",
178 :identifier => "blog",
177 :is_public => 1,
179 :is_public => 1,
178 :custom_field_values => { '3' => 'Beta' },
180 :custom_field_values => { '3' => 'Beta' },
179 :parent_id => 1
181 :parent_id => 1
180 }
182 }
181 assert_redirected_to '/projects/blog/settings'
183 assert_redirected_to '/projects/blog/settings'
182
184
183 project = Project.find_by_name('blog')
185 project = Project.find_by_name('blog')
184 assert_kind_of Project, project
186 assert_kind_of Project, project
185 assert_equal Project.find(1), project.parent
187 assert_equal Project.find(1), project.parent
186 end
188 end
187 end
189 end
188
190
189 context "by non-admin user with add_project permission" do
191 context "by non-admin user with add_project permission" do
190 setup do
192 setup do
191 Role.non_member.add_permission! :add_project
193 Role.non_member.add_permission! :add_project
192 @request.session[:user_id] = 9
194 @request.session[:user_id] = 9
193 end
195 end
194
196
195 should "accept create a Project" do
197 should "accept create a Project" do
196 post :create, :project => { :name => "blog",
198 post :create, :project => { :name => "blog",
197 :description => "weblog",
199 :description => "weblog",
198 :identifier => "blog",
200 :identifier => "blog",
199 :is_public => 1,
201 :is_public => 1,
200 :custom_field_values => { '3' => 'Beta' }
202 :custom_field_values => { '3' => 'Beta' },
203 :tracker_ids => ['1', '3'],
204 :enabled_module_names => ['issue_tracking', 'news', 'repository']
201 }
205 }
202
206
203 assert_redirected_to '/projects/blog/settings'
207 assert_redirected_to '/projects/blog/settings'
204
208
205 project = Project.find_by_name('blog')
209 project = Project.find_by_name('blog')
206 assert_kind_of Project, project
210 assert_kind_of Project, project
207 assert_equal 'weblog', project.description
211 assert_equal 'weblog', project.description
208 assert_equal true, project.is_public?
212 assert_equal true, project.is_public?
213 assert_equal [1, 3], project.trackers.map(&:id).sort
214 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
209
215
210 # User should be added as a project member
216 # User should be added as a project member
211 assert User.find(9).member_of?(project)
217 assert User.find(9).member_of?(project)
212 assert_equal 1, project.members.size
218 assert_equal 1, project.members.size
213 end
219 end
214
220
215 should "fail with parent_id" do
221 should "fail with parent_id" do
216 assert_no_difference 'Project.count' do
222 assert_no_difference 'Project.count' do
217 post :create, :project => { :name => "blog",
223 post :create, :project => { :name => "blog",
218 :description => "weblog",
224 :description => "weblog",
219 :identifier => "blog",
225 :identifier => "blog",
220 :is_public => 1,
226 :is_public => 1,
221 :custom_field_values => { '3' => 'Beta' },
227 :custom_field_values => { '3' => 'Beta' },
222 :parent_id => 1
228 :parent_id => 1
223 }
229 }
224 end
230 end
225 assert_response :success
231 assert_response :success
226 project = assigns(:project)
232 project = assigns(:project)
227 assert_kind_of Project, project
233 assert_kind_of Project, project
228 assert_not_nil project.errors.on(:parent_id)
234 assert_not_nil project.errors.on(:parent_id)
229 end
235 end
230 end
236 end
231
237
232 context "by non-admin user with add_subprojects permission" do
238 context "by non-admin user with add_subprojects permission" do
233 setup do
239 setup do
234 Role.find(1).remove_permission! :add_project
240 Role.find(1).remove_permission! :add_project
235 Role.find(1).add_permission! :add_subprojects
241 Role.find(1).add_permission! :add_subprojects
236 @request.session[:user_id] = 2
242 @request.session[:user_id] = 2
237 end
243 end
238
244
239 should "create a project with a parent_id" do
245 should "create a project with a parent_id" do
240 post :create, :project => { :name => "blog",
246 post :create, :project => { :name => "blog",
241 :description => "weblog",
247 :description => "weblog",
242 :identifier => "blog",
248 :identifier => "blog",
243 :is_public => 1,
249 :is_public => 1,
244 :custom_field_values => { '3' => 'Beta' },
250 :custom_field_values => { '3' => 'Beta' },
245 :parent_id => 1
251 :parent_id => 1
246 }
252 }
247 assert_redirected_to '/projects/blog/settings'
253 assert_redirected_to '/projects/blog/settings'
248 project = Project.find_by_name('blog')
254 project = Project.find_by_name('blog')
249 end
255 end
250
256
251 should "fail without parent_id" do
257 should "fail without parent_id" do
252 assert_no_difference 'Project.count' do
258 assert_no_difference 'Project.count' do
253 post :create, :project => { :name => "blog",
259 post :create, :project => { :name => "blog",
254 :description => "weblog",
260 :description => "weblog",
255 :identifier => "blog",
261 :identifier => "blog",
256 :is_public => 1,
262 :is_public => 1,
257 :custom_field_values => { '3' => 'Beta' }
263 :custom_field_values => { '3' => 'Beta' }
258 }
264 }
259 end
265 end
260 assert_response :success
266 assert_response :success
261 project = assigns(:project)
267 project = assigns(:project)
262 assert_kind_of Project, project
268 assert_kind_of Project, project
263 assert_not_nil project.errors.on(:parent_id)
269 assert_not_nil project.errors.on(:parent_id)
264 end
270 end
265
271
266 should "fail with unauthorized parent_id" do
272 should "fail with unauthorized parent_id" do
267 assert !User.find(2).member_of?(Project.find(6))
273 assert !User.find(2).member_of?(Project.find(6))
268 assert_no_difference 'Project.count' do
274 assert_no_difference 'Project.count' do
269 post :create, :project => { :name => "blog",
275 post :create, :project => { :name => "blog",
270 :description => "weblog",
276 :description => "weblog",
271 :identifier => "blog",
277 :identifier => "blog",
272 :is_public => 1,
278 :is_public => 1,
273 :custom_field_values => { '3' => 'Beta' },
279 :custom_field_values => { '3' => 'Beta' },
274 :parent_id => 6
280 :parent_id => 6
275 }
281 }
276 end
282 end
277 assert_response :success
283 assert_response :success
278 project = assigns(:project)
284 project = assigns(:project)
279 assert_kind_of Project, project
285 assert_kind_of Project, project
280 assert_not_nil project.errors.on(:parent_id)
286 assert_not_nil project.errors.on(:parent_id)
281 end
287 end
282 end
288 end
283 end
289 end
284
290
285 def test_show_by_id
291 def test_show_by_id
286 get :show, :id => 1
292 get :show, :id => 1
287 assert_response :success
293 assert_response :success
288 assert_template 'show'
294 assert_template 'show'
289 assert_not_nil assigns(:project)
295 assert_not_nil assigns(:project)
290 end
296 end
291
297
292 def test_show_by_identifier
298 def test_show_by_identifier
293 get :show, :id => 'ecookbook'
299 get :show, :id => 'ecookbook'
294 assert_response :success
300 assert_response :success
295 assert_template 'show'
301 assert_template 'show'
296 assert_not_nil assigns(:project)
302 assert_not_nil assigns(:project)
297 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
303 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
298
304
299 assert_tag 'li', :content => /Development status/
305 assert_tag 'li', :content => /Development status/
300 end
306 end
301
307
302 def test_show_should_not_display_hidden_custom_fields
308 def test_show_should_not_display_hidden_custom_fields
303 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
309 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
304 get :show, :id => 'ecookbook'
310 get :show, :id => 'ecookbook'
305 assert_response :success
311 assert_response :success
306 assert_template 'show'
312 assert_template 'show'
307 assert_not_nil assigns(:project)
313 assert_not_nil assigns(:project)
308
314
309 assert_no_tag 'li', :content => /Development status/
315 assert_no_tag 'li', :content => /Development status/
310 end
316 end
311
317
312 def test_show_should_not_fail_when_custom_values_are_nil
318 def test_show_should_not_fail_when_custom_values_are_nil
313 project = Project.find_by_identifier('ecookbook')
319 project = Project.find_by_identifier('ecookbook')
314 project.custom_values.first.update_attribute(:value, nil)
320 project.custom_values.first.update_attribute(:value, nil)
315 get :show, :id => 'ecookbook'
321 get :show, :id => 'ecookbook'
316 assert_response :success
322 assert_response :success
317 assert_template 'show'
323 assert_template 'show'
318 assert_not_nil assigns(:project)
324 assert_not_nil assigns(:project)
319 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
325 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
320 end
326 end
321
327
322 def show_archived_project_should_be_denied
328 def show_archived_project_should_be_denied
323 project = Project.find_by_identifier('ecookbook')
329 project = Project.find_by_identifier('ecookbook')
324 project.archive!
330 project.archive!
325
331
326 get :show, :id => 'ecookbook'
332 get :show, :id => 'ecookbook'
327 assert_response 403
333 assert_response 403
328 assert_nil assigns(:project)
334 assert_nil assigns(:project)
329 assert_tag :tag => 'p', :content => /archived/
335 assert_tag :tag => 'p', :content => /archived/
330 end
336 end
331
337
332 def test_private_subprojects_hidden
338 def test_private_subprojects_hidden
333 get :show, :id => 'ecookbook'
339 get :show, :id => 'ecookbook'
334 assert_response :success
340 assert_response :success
335 assert_template 'show'
341 assert_template 'show'
336 assert_no_tag :tag => 'a', :content => /Private child/
342 assert_no_tag :tag => 'a', :content => /Private child/
337 end
343 end
338
344
339 def test_private_subprojects_visible
345 def test_private_subprojects_visible
340 @request.session[:user_id] = 2 # manager who is a member of the private subproject
346 @request.session[:user_id] = 2 # manager who is a member of the private subproject
341 get :show, :id => 'ecookbook'
347 get :show, :id => 'ecookbook'
342 assert_response :success
348 assert_response :success
343 assert_template 'show'
349 assert_template 'show'
344 assert_tag :tag => 'a', :content => /Private child/
350 assert_tag :tag => 'a', :content => /Private child/
345 end
351 end
346
352
347 def test_settings
353 def test_settings
348 @request.session[:user_id] = 2 # manager
354 @request.session[:user_id] = 2 # manager
349 get :settings, :id => 1
355 get :settings, :id => 1
350 assert_response :success
356 assert_response :success
351 assert_template 'settings'
357 assert_template 'settings'
352 end
358 end
353
359
354 def test_update
360 def test_update
355 @request.session[:user_id] = 2 # manager
361 @request.session[:user_id] = 2 # manager
356 post :update, :id => 1, :project => {:name => 'Test changed name',
362 post :update, :id => 1, :project => {:name => 'Test changed name',
357 :issue_custom_field_ids => ['']}
363 :issue_custom_field_ids => ['']}
358 assert_redirected_to '/projects/ecookbook/settings'
364 assert_redirected_to '/projects/ecookbook/settings'
359 project = Project.find(1)
365 project = Project.find(1)
360 assert_equal 'Test changed name', project.name
366 assert_equal 'Test changed name', project.name
361 end
367 end
362
368
363 def test_get_destroy
369 def test_get_destroy
364 @request.session[:user_id] = 1 # admin
370 @request.session[:user_id] = 1 # admin
365 get :destroy, :id => 1
371 get :destroy, :id => 1
366 assert_response :success
372 assert_response :success
367 assert_template 'destroy'
373 assert_template 'destroy'
368 assert_not_nil Project.find_by_id(1)
374 assert_not_nil Project.find_by_id(1)
369 end
375 end
370
376
371 def test_post_destroy
377 def test_post_destroy
372 @request.session[:user_id] = 1 # admin
378 @request.session[:user_id] = 1 # admin
373 post :destroy, :id => 1, :confirm => 1
379 post :destroy, :id => 1, :confirm => 1
374 assert_redirected_to '/admin/projects'
380 assert_redirected_to '/admin/projects'
375 assert_nil Project.find_by_id(1)
381 assert_nil Project.find_by_id(1)
376 end
382 end
377
383
378 def test_archive
384 def test_archive
379 @request.session[:user_id] = 1 # admin
385 @request.session[:user_id] = 1 # admin
380 post :archive, :id => 1
386 post :archive, :id => 1
381 assert_redirected_to '/admin/projects'
387 assert_redirected_to '/admin/projects'
382 assert !Project.find(1).active?
388 assert !Project.find(1).active?
383 end
389 end
384
390
385 def test_unarchive
391 def test_unarchive
386 @request.session[:user_id] = 1 # admin
392 @request.session[:user_id] = 1 # admin
387 Project.find(1).archive
393 Project.find(1).archive
388 post :unarchive, :id => 1
394 post :unarchive, :id => 1
389 assert_redirected_to '/admin/projects'
395 assert_redirected_to '/admin/projects'
390 assert Project.find(1).active?
396 assert Project.find(1).active?
391 end
397 end
392
398
393 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
399 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
394 CustomField.delete_all
400 CustomField.delete_all
395 parent = nil
401 parent = nil
396 6.times do |i|
402 6.times do |i|
397 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
403 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
398 p.set_parent!(parent)
404 p.set_parent!(parent)
399 get :show, :id => p
405 get :show, :id => p
400 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
406 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
401 :children => { :count => [i, 3].min,
407 :children => { :count => [i, 3].min,
402 :only => { :tag => 'a' } }
408 :only => { :tag => 'a' } }
403
409
404 parent = p
410 parent = p
405 end
411 end
406 end
412 end
407
413
408 def test_copy_with_project
414 def test_copy_with_project
409 @request.session[:user_id] = 1 # admin
415 @request.session[:user_id] = 1 # admin
410 get :copy, :id => 1
416 get :copy, :id => 1
411 assert_response :success
417 assert_response :success
412 assert_template 'copy'
418 assert_template 'copy'
413 assert assigns(:project)
419 assert assigns(:project)
414 assert_equal Project.find(1).description, assigns(:project).description
420 assert_equal Project.find(1).description, assigns(:project).description
415 assert_nil assigns(:project).id
421 assert_nil assigns(:project).id
416 end
422 end
417
423
418 def test_copy_without_project
424 def test_copy_without_project
419 @request.session[:user_id] = 1 # admin
425 @request.session[:user_id] = 1 # admin
420 get :copy
426 get :copy
421 assert_response :redirect
427 assert_response :redirect
422 assert_redirected_to :controller => 'admin', :action => 'projects'
428 assert_redirected_to :controller => 'admin', :action => 'projects'
423 end
429 end
424
430
425 context "POST :copy" do
431 context "POST :copy" do
426 should "TODO: test the rest of the method"
432 should "TODO: test the rest of the method"
427
433
428 should "redirect to the project settings when successful" do
434 should "redirect to the project settings when successful" do
429 @request.session[:user_id] = 1 # admin
435 @request.session[:user_id] = 1 # admin
430 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
436 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
431 assert_response :redirect
437 assert_response :redirect
432 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
438 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
433 end
439 end
434 end
440 end
435
441
436 def test_jump_should_redirect_to_active_tab
442 def test_jump_should_redirect_to_active_tab
437 get :show, :id => 1, :jump => 'issues'
443 get :show, :id => 1, :jump => 'issues'
438 assert_redirected_to '/projects/ecookbook/issues'
444 assert_redirected_to '/projects/ecookbook/issues'
439 end
445 end
440
446
441 def test_jump_should_not_redirect_to_inactive_tab
447 def test_jump_should_not_redirect_to_inactive_tab
442 get :show, :id => 3, :jump => 'documents'
448 get :show, :id => 3, :jump => 'documents'
443 assert_response :success
449 assert_response :success
444 assert_template 'show'
450 assert_template 'show'
445 end
451 end
446
452
447 def test_jump_should_not_redirect_to_unknown_tab
453 def test_jump_should_not_redirect_to_unknown_tab
448 get :show, :id => 3, :jump => 'foobar'
454 get :show, :id => 3, :jump => 'foobar'
449 assert_response :success
455 assert_response :success
450 assert_template 'show'
456 assert_template 'show'
451 end
457 end
452
458
453 # A hook that is manually registered later
459 # A hook that is manually registered later
454 class ProjectBasedTemplate < Redmine::Hook::ViewListener
460 class ProjectBasedTemplate < Redmine::Hook::ViewListener
455 def view_layouts_base_html_head(context)
461 def view_layouts_base_html_head(context)
456 # Adds a project stylesheet
462 # Adds a project stylesheet
457 stylesheet_link_tag(context[:project].identifier) if context[:project]
463 stylesheet_link_tag(context[:project].identifier) if context[:project]
458 end
464 end
459 end
465 end
460 # Don't use this hook now
466 # Don't use this hook now
461 Redmine::Hook.clear_listeners
467 Redmine::Hook.clear_listeners
462
468
463 def test_hook_response
469 def test_hook_response
464 Redmine::Hook.add_listener(ProjectBasedTemplate)
470 Redmine::Hook.add_listener(ProjectBasedTemplate)
465 get :show, :id => 1
471 get :show, :id => 1
466 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
472 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
467 :parent => {:tag => 'head'}
473 :parent => {:tag => 'head'}
468
474
469 Redmine::Hook.clear_listeners
475 Redmine::Hook.clear_listeners
470 end
476 end
471 end
477 end
@@ -1,216 +1,261
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::ProjectsTest < ActionController::IntegrationTest
20 class ApiTest::ProjectsTest < ActionController::IntegrationTest
21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
23 :attachments, :custom_fields, :custom_values, :time_entries
23 :attachments, :custom_fields, :custom_values, :time_entries
24
24
25 def setup
25 def setup
26 Setting.rest_api_enabled = '1'
26 Setting.rest_api_enabled = '1'
27 end
27 end
28
28
29 context "GET /projects" do
29 context "GET /projects" do
30 context ".xml" do
30 context ".xml" do
31 should "return projects" do
31 should "return projects" do
32 get '/projects.xml'
32 get '/projects.xml'
33 assert_response :success
33 assert_response :success
34 assert_equal 'application/xml', @response.content_type
34 assert_equal 'application/xml', @response.content_type
35
35
36 assert_tag :tag => 'projects',
36 assert_tag :tag => 'projects',
37 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
37 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
38 end
38 end
39 end
39 end
40
40
41 context ".json" do
41 context ".json" do
42 should "return projects" do
42 should "return projects" do
43 get '/projects.json'
43 get '/projects.json'
44 assert_response :success
44 assert_response :success
45 assert_equal 'application/json', @response.content_type
45 assert_equal 'application/json', @response.content_type
46
46
47 json = ActiveSupport::JSON.decode(response.body)
47 json = ActiveSupport::JSON.decode(response.body)
48 assert_kind_of Hash, json
48 assert_kind_of Hash, json
49 assert_kind_of Array, json['projects']
49 assert_kind_of Array, json['projects']
50 assert_kind_of Hash, json['projects'].first
50 assert_kind_of Hash, json['projects'].first
51 assert json['projects'].first.has_key?('id')
51 assert json['projects'].first.has_key?('id')
52 end
52 end
53 end
53 end
54 end
54 end
55
55
56 context "GET /projects/:id" do
56 context "GET /projects/:id" do
57 context ".xml" do
57 context ".xml" do
58 # TODO: A private project is needed because should_allow_api_authentication
58 # TODO: A private project is needed because should_allow_api_authentication
59 # actually tests that authentication is *required*, not just allowed
59 # actually tests that authentication is *required*, not just allowed
60 should_allow_api_authentication(:get, "/projects/2.xml")
60 should_allow_api_authentication(:get, "/projects/2.xml")
61
61
62 should "return requested project" do
62 should "return requested project" do
63 get '/projects/1.xml'
63 get '/projects/1.xml'
64 assert_response :success
64 assert_response :success
65 assert_equal 'application/xml', @response.content_type
65 assert_equal 'application/xml', @response.content_type
66
66
67 assert_tag :tag => 'project',
67 assert_tag :tag => 'project',
68 :child => {:tag => 'id', :content => '1'}
68 :child => {:tag => 'id', :content => '1'}
69 assert_tag :tag => 'custom_field',
69 assert_tag :tag => 'custom_field',
70 :attributes => {:name => 'Development status'}, :content => 'Stable'
70 :attributes => {:name => 'Development status'}, :content => 'Stable'
71 end
71 end
72
72
73 context "with hidden custom fields" do
73 context "with hidden custom fields" do
74 setup do
74 setup do
75 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
75 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
76 end
76 end
77
77
78 should "not display hidden custom fields" do
78 should "not display hidden custom fields" do
79 get '/projects/1.xml'
79 get '/projects/1.xml'
80 assert_response :success
80 assert_response :success
81 assert_equal 'application/xml', @response.content_type
81 assert_equal 'application/xml', @response.content_type
82
82
83 assert_no_tag 'custom_field',
83 assert_no_tag 'custom_field',
84 :attributes => {:name => 'Development status'}
84 :attributes => {:name => 'Development status'}
85 end
85 end
86 end
86 end
87 end
87 end
88
88
89 context ".json" do
89 context ".json" do
90 should_allow_api_authentication(:get, "/projects/2.json")
90 should_allow_api_authentication(:get, "/projects/2.json")
91
91
92 should "return requested project" do
92 should "return requested project" do
93 get '/projects/1.json'
93 get '/projects/1.json'
94
94
95 json = ActiveSupport::JSON.decode(response.body)
95 json = ActiveSupport::JSON.decode(response.body)
96 assert_kind_of Hash, json
96 assert_kind_of Hash, json
97 assert_kind_of Hash, json['project']
97 assert_kind_of Hash, json['project']
98 assert_equal 1, json['project']['id']
98 assert_equal 1, json['project']['id']
99 end
99 end
100 end
100 end
101 end
101 end
102
102
103 context "POST /projects" do
103 context "POST /projects" do
104 context "with valid parameters" do
104 context "with valid parameters" do
105 setup do
105 setup do
106 Setting.default_projects_modules = ['issue_tracking', 'repository']
106 Setting.default_projects_modules = ['issue_tracking', 'repository']
107 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
107 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
108 end
108 end
109
109
110 context ".xml" do
110 context ".xml" do
111 should_allow_api_authentication(:post,
111 should_allow_api_authentication(:post,
112 '/projects.xml',
112 '/projects.xml',
113 {:project => {:name => 'API test', :identifier => 'api-test'}},
113 {:project => {:name => 'API test', :identifier => 'api-test'}},
114 {:success_code => :created})
114 {:success_code => :created})
115
115
116
116
117 should "create a project with the attributes" do
117 should "create a project with the attributes" do
118 assert_difference('Project.count') do
118 assert_difference('Project.count') do
119 post '/projects.xml', @parameters, :authorization => credentials('admin')
119 post '/projects.xml', @parameters, :authorization => credentials('admin')
120 end
120 end
121
121
122 project = Project.first(:order => 'id DESC')
122 project = Project.first(:order => 'id DESC')
123 assert_equal 'API test', project.name
123 assert_equal 'API test', project.name
124 assert_equal 'api-test', project.identifier
124 assert_equal 'api-test', project.identifier
125 assert_equal ['issue_tracking', 'repository'], project.enabled_module_names
125 assert_equal ['issue_tracking', 'repository'], project.enabled_module_names.sort
126 assert_equal Tracker.all.size, project.trackers.size
126
127
127 assert_response :created
128 assert_response :created
128 assert_equal 'application/xml', @response.content_type
129 assert_equal 'application/xml', @response.content_type
129 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
130 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
130 end
131 end
132
133 should "accept enabled_module_names attribute" do
134 @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
135
136 assert_difference('Project.count') do
137 post '/projects.xml', @parameters, :authorization => credentials('admin')
138 end
139
140 project = Project.first(:order => 'id DESC')
141 assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
142 end
143
144 should "accept tracker_ids attribute" do
145 @parameters[:project].merge!({:tracker_ids => [1, 3]})
146
147 assert_difference('Project.count') do
148 post '/projects.xml', @parameters, :authorization => credentials('admin')
149 end
150
151 project = Project.first(:order => 'id DESC')
152 assert_equal [1, 3], project.trackers.map(&:id).sort
153 end
131 end
154 end
132 end
155 end
133
156
134 context "with invalid parameters" do
157 context "with invalid parameters" do
135 setup do
158 setup do
136 @parameters = {:project => {:name => 'API test'}}
159 @parameters = {:project => {:name => 'API test'}}
137 end
160 end
138
161
139 context ".xml" do
162 context ".xml" do
140 should "return errors" do
163 should "return errors" do
141 assert_no_difference('Project.count') do
164 assert_no_difference('Project.count') do
142 post '/projects.xml', @parameters, :authorization => credentials('admin')
165 post '/projects.xml', @parameters, :authorization => credentials('admin')
143 end
166 end
144
167
145 assert_response :unprocessable_entity
168 assert_response :unprocessable_entity
146 assert_equal 'application/xml', @response.content_type
169 assert_equal 'application/xml', @response.content_type
147 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
170 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
148 end
171 end
149 end
172 end
150 end
173 end
151 end
174 end
152
175
153 context "PUT /projects/:id" do
176 context "PUT /projects/:id" do
154 context "with valid parameters" do
177 context "with valid parameters" do
155 setup do
178 setup do
156 @parameters = {:project => {:name => 'API update'}}
179 @parameters = {:project => {:name => 'API update'}}
157 end
180 end
158
181
159 context ".xml" do
182 context ".xml" do
160 should_allow_api_authentication(:put,
183 should_allow_api_authentication(:put,
161 '/projects/2.xml',
184 '/projects/2.xml',
162 {:project => {:name => 'API update'}},
185 {:project => {:name => 'API update'}},
163 {:success_code => :ok})
186 {:success_code => :ok})
164
187
165 should "update the project" do
188 should "update the project" do
166 assert_no_difference 'Project.count' do
189 assert_no_difference 'Project.count' do
167 put '/projects/2.xml', @parameters, :authorization => credentials('jsmith')
190 put '/projects/2.xml', @parameters, :authorization => credentials('jsmith')
168 end
191 end
169 assert_response :ok
192 assert_response :ok
170 assert_equal 'application/xml', @response.content_type
193 assert_equal 'application/xml', @response.content_type
171 project = Project.find(2)
194 project = Project.find(2)
172 assert_equal 'API update', project.name
195 assert_equal 'API update', project.name
173 end
196 end
197
198 should "accept enabled_module_names attribute" do
199 @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
200
201 assert_no_difference 'Project.count' do
202 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
203 end
204 assert_response :ok
205 project = Project.find(2)
206 assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
207 end
208
209 should "accept tracker_ids attribute" do
210 @parameters[:project].merge!({:tracker_ids => [1, 3]})
211
212 assert_no_difference 'Project.count' do
213 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
214 end
215 assert_response :ok
216 project = Project.find(2)
217 assert_equal [1, 3], project.trackers.map(&:id).sort
218 end
174 end
219 end
175 end
220 end
176
221
177 context "with invalid parameters" do
222 context "with invalid parameters" do
178 setup do
223 setup do
179 @parameters = {:project => {:name => ''}}
224 @parameters = {:project => {:name => ''}}
180 end
225 end
181
226
182 context ".xml" do
227 context ".xml" do
183 should "return errors" do
228 should "return errors" do
184 assert_no_difference('Project.count') do
229 assert_no_difference('Project.count') do
185 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
230 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
186 end
231 end
187
232
188 assert_response :unprocessable_entity
233 assert_response :unprocessable_entity
189 assert_equal 'application/xml', @response.content_type
234 assert_equal 'application/xml', @response.content_type
190 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
235 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
191 end
236 end
192 end
237 end
193 end
238 end
194 end
239 end
195
240
196 context "DELETE /projects/:id" do
241 context "DELETE /projects/:id" do
197 context ".xml" do
242 context ".xml" do
198 should_allow_api_authentication(:delete,
243 should_allow_api_authentication(:delete,
199 '/projects/2.xml',
244 '/projects/2.xml',
200 {},
245 {},
201 {:success_code => :ok})
246 {:success_code => :ok})
202
247
203 should "delete the project" do
248 should "delete the project" do
204 assert_difference('Project.count',-1) do
249 assert_difference('Project.count',-1) do
205 delete '/projects/2.xml', {}, :authorization => credentials('admin')
250 delete '/projects/2.xml', {}, :authorization => credentials('admin')
206 end
251 end
207 assert_response :ok
252 assert_response :ok
208 assert_nil Project.find_by_id(2)
253 assert_nil Project.find_by_id(2)
209 end
254 end
210 end
255 end
211 end
256 end
212
257
213 def credentials(user, password=nil)
258 def credentials(user, password=nil)
214 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
259 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
215 end
260 end
216 end
261 end
General Comments 0
You need to be logged in to leave comments. Login now