##// END OF EJS Templates
Show subproject versions on the Roadmap....
Eric Davis -
r3646:f3cc84b3437a
parent child
Show More
@@ -1,387 +1,389
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :activity, :only => :activity
21 21 menu_item :roadmap, :only => :roadmap
22 22 menu_item :files, :only => [:list_files, :add_file]
23 23 menu_item :settings, :only => :settings
24 24
25 25 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
26 26 before_filter :find_optional_project, :only => :activity
27 27 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
28 28 before_filter :authorize_global, :only => :add
29 29 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
30 30 accept_key_auth :activity
31 31
32 32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 33 if controller.request.post?
34 34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 35 end
36 36 end
37 37
38 38 helper :sort
39 39 include SortHelper
40 40 helper :custom_fields
41 41 include CustomFieldsHelper
42 42 helper :issues
43 43 helper :queries
44 44 include QueriesHelper
45 45 helper :repositories
46 46 include RepositoriesHelper
47 47 include ProjectsHelper
48 48
49 49 # Lists visible projects
50 50 def index
51 51 respond_to do |format|
52 52 format.html {
53 53 @projects = Project.visible.find(:all, :order => 'lft')
54 54 }
55 55 format.xml {
56 56 @projects = Project.visible.find(:all, :order => 'lft')
57 57 }
58 58 format.atom {
59 59 projects = Project.visible.find(:all, :order => 'created_on DESC',
60 60 :limit => Setting.feeds_limit.to_i)
61 61 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
62 62 }
63 63 end
64 64 end
65 65
66 66 # Add a new project
67 67 def add
68 68 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
69 69 @trackers = Tracker.all
70 70 @project = Project.new(params[:project])
71 71 if request.get?
72 72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
73 73 @project.trackers = Tracker.all
74 74 @project.is_public = Setting.default_projects_public?
75 75 @project.enabled_module_names = Setting.default_projects_modules
76 76 else
77 77 @project.enabled_module_names = params[:enabled_modules]
78 78 if validate_parent_id && @project.save
79 79 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
80 80 # Add current user as a project member if he is not admin
81 81 unless User.current.admin?
82 82 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
83 83 m = Member.new(:user => User.current, :roles => [r])
84 84 @project.members << m
85 85 end
86 86 respond_to do |format|
87 87 format.html {
88 88 flash[:notice] = l(:notice_successful_create)
89 89 redirect_to :controller => 'projects', :action => 'settings', :id => @project
90 90 }
91 91 format.xml { head :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 92 end
93 93 else
94 94 respond_to do |format|
95 95 format.html
96 96 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
97 97 end
98 98 end
99 99 end
100 100 end
101 101
102 102 def copy
103 103 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
104 104 @trackers = Tracker.all
105 105 @root_projects = Project.find(:all,
106 106 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
107 107 :order => 'name')
108 108 @source_project = Project.find(params[:id])
109 109 if request.get?
110 110 @project = Project.copy_from(@source_project)
111 111 if @project
112 112 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
113 113 else
114 114 redirect_to :controller => 'admin', :action => 'projects'
115 115 end
116 116 else
117 117 Mailer.with_deliveries(params[:notifications] == '1') do
118 118 @project = Project.new(params[:project])
119 119 @project.enabled_module_names = params[:enabled_modules]
120 120 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
121 121 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
122 122 flash[:notice] = l(:notice_successful_create)
123 123 redirect_to :controller => 'admin', :action => 'projects'
124 124 elsif !@project.new_record?
125 125 # Project was created
126 126 # But some objects were not copied due to validation failures
127 127 # (eg. issues from disabled trackers)
128 128 # TODO: inform about that
129 129 redirect_to :controller => 'admin', :action => 'projects'
130 130 end
131 131 end
132 132 end
133 133 rescue ActiveRecord::RecordNotFound
134 134 redirect_to :controller => 'admin', :action => 'projects'
135 135 end
136 136
137 137 # Show @project
138 138 def show
139 139 if params[:jump]
140 140 # try to redirect to the requested menu item
141 141 redirect_to_project_menu_item(@project, params[:jump]) && return
142 142 end
143 143
144 144 @users_by_role = @project.users_by_role
145 145 @subprojects = @project.children.visible
146 146 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
147 147 @trackers = @project.rolled_up_trackers
148 148
149 149 cond = @project.project_condition(Setting.display_subprojects_issues?)
150 150
151 151 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
152 152 :include => [:project, :status, :tracker],
153 153 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
154 154 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
155 155 :include => [:project, :status, :tracker],
156 156 :conditions => cond)
157 157
158 158 TimeEntry.visible_by(User.current) do
159 159 @total_hours = TimeEntry.sum(:hours,
160 160 :include => :project,
161 161 :conditions => cond).to_f
162 162 end
163 163 @key = User.current.rss_key
164 164
165 165 respond_to do |format|
166 166 format.html
167 167 format.xml
168 168 end
169 169 end
170 170
171 171 def settings
172 172 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
173 173 @issue_category ||= IssueCategory.new
174 174 @member ||= @project.members.new
175 175 @trackers = Tracker.all
176 176 @repository ||= @project.repository
177 177 @wiki ||= @project.wiki
178 178 end
179 179
180 180 # Edit @project
181 181 def edit
182 182 if request.get?
183 183 else
184 184 @project.attributes = params[:project]
185 185 if validate_parent_id && @project.save
186 186 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
187 187 respond_to do |format|
188 188 format.html {
189 189 flash[:notice] = l(:notice_successful_update)
190 190 redirect_to :action => 'settings', :id => @project
191 191 }
192 192 format.xml { head :ok }
193 193 end
194 194 else
195 195 respond_to do |format|
196 196 format.html {
197 197 settings
198 198 render :action => 'settings'
199 199 }
200 200 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
201 201 end
202 202 end
203 203 end
204 204 end
205 205
206 206 def modules
207 207 @project.enabled_module_names = params[:enabled_modules]
208 208 flash[:notice] = l(:notice_successful_update)
209 209 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
210 210 end
211 211
212 212 def archive
213 213 if request.post?
214 214 unless @project.archive
215 215 flash[:error] = l(:error_can_not_archive_project)
216 216 end
217 217 end
218 218 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
219 219 end
220 220
221 221 def unarchive
222 222 @project.unarchive if request.post? && !@project.active?
223 223 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
224 224 end
225 225
226 226 # Delete @project
227 227 def destroy
228 228 @project_to_destroy = @project
229 229 if request.get?
230 230 # display confirmation view
231 231 else
232 232 if params[:format] == 'xml' || params[:confirm]
233 233 @project_to_destroy.destroy
234 234 respond_to do |format|
235 235 format.html { redirect_to :controller => 'admin', :action => 'projects' }
236 236 format.xml { head :ok }
237 237 end
238 238 end
239 239 end
240 240 # hide project in layout
241 241 @project = nil
242 242 end
243 243
244 244 def add_file
245 245 if request.post?
246 246 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
247 247 attachments = Attachment.attach_files(container, params[:attachments])
248 248 render_attachment_warning_if_needed(container)
249 249
250 250 if !attachments.empty? && Setting.notified_events.include?('file_added')
251 251 Mailer.deliver_attachments_added(attachments[:files])
252 252 end
253 253 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
254 254 return
255 255 end
256 256 @versions = @project.versions.sort
257 257 end
258 258
259 259 def save_activities
260 260 if request.post? && params[:enumerations]
261 261 Project.transaction do
262 262 params[:enumerations].each do |id, activity|
263 263 @project.update_or_create_time_entry_activity(id, activity)
264 264 end
265 265 end
266 266 flash[:notice] = l(:notice_successful_update)
267 267 end
268 268
269 269 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
270 270 end
271 271
272 272 def reset_activities
273 273 @project.time_entry_activities.each do |time_entry_activity|
274 274 time_entry_activity.destroy(time_entry_activity.parent)
275 275 end
276 276 flash[:notice] = l(:notice_successful_update)
277 277 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
278 278 end
279 279
280 280 def list_files
281 281 sort_init 'filename', 'asc'
282 282 sort_update 'filename' => "#{Attachment.table_name}.filename",
283 283 'created_on' => "#{Attachment.table_name}.created_on",
284 284 'size' => "#{Attachment.table_name}.filesize",
285 285 'downloads' => "#{Attachment.table_name}.downloads"
286 286
287 287 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
288 288 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
289 289 render :layout => !request.xhr?
290 290 end
291 291
292 292 def roadmap
293 293 @trackers = @project.trackers.find(:all, :order => 'position')
294 294 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
295 295 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
296 296 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
297 297
298 @versions = @project.shared_versions.sort
298 @versions = @project.shared_versions || []
299 @versions += @project.rolled_up_versions.visible if @with_subprojects
300 @versions = @versions.uniq.sort
299 301 @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
300 302
301 303 @issues_by_version = {}
302 304 unless @selected_tracker_ids.empty?
303 305 @versions.each do |version|
304 306 issues = version.fixed_issues.visible.find(:all,
305 307 :include => [:project, :status, :tracker, :priority],
306 308 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
307 309 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
308 310 @issues_by_version[version] = issues
309 311 end
310 312 end
311 313 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
312 314 end
313 315
314 316 def activity
315 317 @days = Setting.activity_days_default.to_i
316 318
317 319 if params[:from]
318 320 begin; @date_to = params[:from].to_date + 1; rescue; end
319 321 end
320 322
321 323 @date_to ||= Date.today + 1
322 324 @date_from = @date_to - @days
323 325 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
324 326 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
325 327
326 328 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
327 329 :with_subprojects => @with_subprojects,
328 330 :author => @author)
329 331 @activity.scope_select {|t| !params["show_#{t}"].nil?}
330 332 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
331 333
332 334 events = @activity.events(@date_from, @date_to)
333 335
334 336 if events.empty? || stale?(:etag => [events.first, User.current])
335 337 respond_to do |format|
336 338 format.html {
337 339 @events_by_day = events.group_by(&:event_date)
338 340 render :layout => false if request.xhr?
339 341 }
340 342 format.atom {
341 343 title = l(:label_activity)
342 344 if @author
343 345 title = @author.name
344 346 elsif @activity.scope.size == 1
345 347 title = l("label_#{@activity.scope.first.singularize}_plural")
346 348 end
347 349 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
348 350 }
349 351 end
350 352 end
351 353
352 354 rescue ActiveRecord::RecordNotFound
353 355 render_404
354 356 end
355 357
356 358 private
357 359 def find_optional_project
358 360 return true unless params[:id]
359 361 @project = Project.find(params[:id])
360 362 authorize
361 363 rescue ActiveRecord::RecordNotFound
362 364 render_404
363 365 end
364 366
365 367 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
366 368 if ids = params[:tracker_ids]
367 369 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
368 370 else
369 371 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
370 372 end
371 373 end
372 374
373 375 # Validates parent_id param according to user's permissions
374 376 # TODO: move it to Project model in a validation that depends on User.current
375 377 def validate_parent_id
376 378 return true if User.current.admin?
377 379 parent_id = params[:project] && params[:project][:parent_id]
378 380 if parent_id || @project.new_record?
379 381 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
380 382 unless @project.allowed_parents.include?(parent)
381 383 @project.errors.add :parent_id, :invalid
382 384 return false
383 385 end
384 386 end
385 387 true
386 388 end
387 389 end
@@ -1,706 +1,713
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 # Specific overidden Activities
24 24 has_many :time_entry_activities
25 25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 26 has_many :memberships, :class_name => 'Member'
27 27 has_many :member_principals, :class_name => 'Member',
28 28 :include => :principal,
29 29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 30 has_many :users, :through => :members
31 31 has_many :principals, :through => :member_principals, :source => :principal
32 32
33 33 has_many :enabled_modules, :dependent => :delete_all
34 34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 36 has_many :issue_changes, :through => :issues, :source => :journals
37 37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 38 has_many :time_entries, :dependent => :delete_all
39 39 has_many :queries, :dependent => :delete_all
40 40 has_many :documents, :dependent => :destroy
41 41 has_many :news, :dependent => :delete_all, :include => :author
42 42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 44 has_one :repository, :dependent => :destroy
45 45 has_many :changesets, :through => :repository
46 46 has_one :wiki, :dependent => :destroy
47 47 # Custom field for the project issues
48 48 has_and_belongs_to_many :issue_custom_fields,
49 49 :class_name => 'IssueCustomField',
50 50 :order => "#{CustomField.table_name}.position",
51 51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 52 :association_foreign_key => 'custom_field_id'
53 53
54 54 acts_as_nested_set :order => 'name'
55 55 acts_as_attachable :view_permission => :view_files,
56 56 :delete_permission => :manage_files
57 57
58 58 acts_as_customizable
59 59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
60 60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
62 62 :author => nil
63 63
64 64 attr_protected :status, :enabled_module_names
65 65
66 66 validates_presence_of :name, :identifier
67 67 validates_uniqueness_of :name, :identifier
68 68 validates_associated :repository, :wiki
69 69 validates_length_of :name, :maximum => 30
70 70 validates_length_of :homepage, :maximum => 255
71 71 validates_length_of :identifier, :in => 1..20
72 72 # donwcase letters, digits, dashes but not digits only
73 73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 74 # reserved words
75 75 validates_exclusion_of :identifier, :in => %w( new )
76 76
77 77 before_destroy :delete_all_members, :destroy_children
78 78
79 79 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] } }
80 80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 81 named_scope :all_public, { :conditions => { :is_public => true } }
82 82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83 83
84 84 def identifier=(identifier)
85 85 super unless identifier_frozen?
86 86 end
87 87
88 88 def identifier_frozen?
89 89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 90 end
91 91
92 92 # returns latest created projects
93 93 # non public projects will be returned only if user is a member of those
94 94 def self.latest(user=nil, count=5)
95 95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 96 end
97 97
98 98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 99 #
100 100 # Examples:
101 101 # Projects.visible_by(admin) => "projects.status = 1"
102 102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 103 def self.visible_by(user=nil)
104 104 user ||= User.current
105 105 if user && user.admin?
106 106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 107 elsif user && user.memberships.any?
108 108 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(',')}))"
109 109 else
110 110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 111 end
112 112 end
113 113
114 114 def self.allowed_to_condition(user, permission, options={})
115 115 statements = []
116 116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 117 if perm = Redmine::AccessControl.permission(permission)
118 118 unless perm.project_module.nil?
119 119 # If the permission belongs to a project module, make sure the module is enabled
120 120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 121 end
122 122 end
123 123 if options[:project]
124 124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 127 end
128 128 if user.admin?
129 129 # no restriction
130 130 else
131 131 statements << "1=0"
132 132 if user.logged?
133 133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 135 end
136 136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 138 else
139 139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 140 # anonymous user allowed on public project
141 141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 142 end
143 143 end
144 144 end
145 145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 146 end
147 147
148 148 # Returns the Systemwide and project specific activities
149 149 def activities(include_inactive=false)
150 150 if include_inactive
151 151 return all_activities
152 152 else
153 153 return active_activities
154 154 end
155 155 end
156 156
157 157 # Will create a new Project specific Activity or update an existing one
158 158 #
159 159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 160 # does not successfully save.
161 161 def update_or_create_time_entry_activity(id, activity_hash)
162 162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 163 self.create_time_entry_activity_if_needed(activity_hash)
164 164 else
165 165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 166 activity.update_attributes(activity_hash) if activity
167 167 end
168 168 end
169 169
170 170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 171 #
172 172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 173 # does not successfully save.
174 174 def create_time_entry_activity_if_needed(activity)
175 175 if activity['parent_id']
176 176
177 177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 178 activity['name'] = parent_activity.name
179 179 activity['position'] = parent_activity.position
180 180
181 181 if Enumeration.overridding_change?(activity, parent_activity)
182 182 project_activity = self.time_entry_activities.create(activity)
183 183
184 184 if project_activity.new_record?
185 185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 186 else
187 187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 188 end
189 189 end
190 190 end
191 191 end
192 192
193 193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 194 #
195 195 # Examples:
196 196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 197 # project.project_condition(false) => "projects.id = 1"
198 198 def project_condition(with_subprojects)
199 199 cond = "#{Project.table_name}.id = #{id}"
200 200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 201 cond
202 202 end
203 203
204 204 def self.find(*args)
205 205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 206 project = find_by_identifier(*args)
207 207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 208 project
209 209 else
210 210 super
211 211 end
212 212 end
213 213
214 214 def to_param
215 215 # id is used for projects with a numeric identifier (compatibility)
216 216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 217 end
218 218
219 219 def active?
220 220 self.status == STATUS_ACTIVE
221 221 end
222 222
223 223 # Archives the project and its descendants
224 224 def archive
225 225 # Check that there is no issue of a non descendant project that is assigned
226 226 # to one of the project or descendant versions
227 227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228 228 if v_ids.any? && Issue.find(:first, :include => :project,
229 229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230 230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
231 231 return false
232 232 end
233 233 Project.transaction do
234 234 archive!
235 235 end
236 236 true
237 237 end
238 238
239 239 # Unarchives the project
240 240 # All its ancestors must be active
241 241 def unarchive
242 242 return false if ancestors.detect {|a| !a.active?}
243 243 update_attribute :status, STATUS_ACTIVE
244 244 end
245 245
246 246 # Returns an array of projects the project can be moved to
247 247 # by the current user
248 248 def allowed_parents
249 249 return @allowed_parents if @allowed_parents
250 250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
251 251 @allowed_parents = @allowed_parents - self_and_descendants
252 252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
253 253 @allowed_parents << nil
254 254 end
255 255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
256 256 @allowed_parents << parent
257 257 end
258 258 @allowed_parents
259 259 end
260 260
261 261 # Sets the parent of the project with authorization check
262 262 def set_allowed_parent!(p)
263 263 unless p.nil? || p.is_a?(Project)
264 264 if p.to_s.blank?
265 265 p = nil
266 266 else
267 267 p = Project.find_by_id(p)
268 268 return false unless p
269 269 end
270 270 end
271 271 if p.nil?
272 272 if !new_record? && allowed_parents.empty?
273 273 return false
274 274 end
275 275 elsif !allowed_parents.include?(p)
276 276 return false
277 277 end
278 278 set_parent!(p)
279 279 end
280 280
281 281 # Sets the parent of the project
282 282 # Argument can be either a Project, a String, a Fixnum or nil
283 283 def set_parent!(p)
284 284 unless p.nil? || p.is_a?(Project)
285 285 if p.to_s.blank?
286 286 p = nil
287 287 else
288 288 p = Project.find_by_id(p)
289 289 return false unless p
290 290 end
291 291 end
292 292 if p == parent && !p.nil?
293 293 # Nothing to do
294 294 true
295 295 elsif p.nil? || (p.active? && move_possible?(p))
296 296 # Insert the project so that target's children or root projects stay alphabetically sorted
297 297 sibs = (p.nil? ? self.class.roots : p.children)
298 298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
299 299 if to_be_inserted_before
300 300 move_to_left_of(to_be_inserted_before)
301 301 elsif p.nil?
302 302 if sibs.empty?
303 303 # move_to_root adds the project in first (ie. left) position
304 304 move_to_root
305 305 else
306 306 move_to_right_of(sibs.last) unless self == sibs.last
307 307 end
308 308 else
309 309 # move_to_child_of adds the project in last (ie.right) position
310 310 move_to_child_of(p)
311 311 end
312 312 Issue.update_versions_from_hierarchy_change(self)
313 313 true
314 314 else
315 315 # Can not move to the given target
316 316 false
317 317 end
318 318 end
319 319
320 320 # Returns an array of the trackers used by the project and its active sub projects
321 321 def rolled_up_trackers
322 322 @rolled_up_trackers ||=
323 323 Tracker.find(:all, :include => :projects,
324 324 :select => "DISTINCT #{Tracker.table_name}.*",
325 325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
326 326 :order => "#{Tracker.table_name}.position")
327 327 end
328 328
329 329 # Closes open and locked project versions that are completed
330 330 def close_completed_versions
331 331 Version.transaction do
332 332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
333 333 if version.completed?
334 334 version.update_attribute(:status, 'closed')
335 335 end
336 336 end
337 337 end
338 338 end
339
340 # Returns a scope of the Versions on subprojects
341 def rolled_up_versions
342 @rolled_up_versions ||=
343 Version.scoped(:include => :project,
344 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
345 end
339 346
340 347 # Returns a scope of the Versions used by the project
341 348 def shared_versions
342 349 @shared_versions ||=
343 350 Version.scoped(:include => :project,
344 351 :conditions => "#{Project.table_name}.id = #{id}" +
345 352 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
346 353 " #{Version.table_name}.sharing = 'system'" +
347 354 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
348 355 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
349 356 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
350 357 "))")
351 358 end
352 359
353 360 # Returns a hash of project users grouped by role
354 361 def users_by_role
355 362 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
356 363 m.roles.each do |r|
357 364 h[r] ||= []
358 365 h[r] << m.user
359 366 end
360 367 h
361 368 end
362 369 end
363 370
364 371 # Deletes all project's members
365 372 def delete_all_members
366 373 me, mr = Member.table_name, MemberRole.table_name
367 374 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
368 375 Member.delete_all(['project_id = ?', id])
369 376 end
370 377
371 378 # Users issues can be assigned to
372 379 def assignable_users
373 380 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
374 381 end
375 382
376 383 # Returns the mail adresses of users that should be always notified on project events
377 384 def recipients
378 385 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
379 386 end
380 387
381 388 # Returns the users that should be notified on project events
382 389 def notified_users
383 390 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
384 391 end
385 392
386 393 # Returns an array of all custom fields enabled for project issues
387 394 # (explictly associated custom fields and custom fields enabled for all projects)
388 395 def all_issue_custom_fields
389 396 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
390 397 end
391 398
392 399 def project
393 400 self
394 401 end
395 402
396 403 def <=>(project)
397 404 name.downcase <=> project.name.downcase
398 405 end
399 406
400 407 def to_s
401 408 name
402 409 end
403 410
404 411 # Returns a short description of the projects (first lines)
405 412 def short_description(length = 255)
406 413 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
407 414 end
408 415
409 416 # Return true if this project is allowed to do the specified action.
410 417 # action can be:
411 418 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
412 419 # * a permission Symbol (eg. :edit_project)
413 420 def allows_to?(action)
414 421 if action.is_a? Hash
415 422 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
416 423 else
417 424 allowed_permissions.include? action
418 425 end
419 426 end
420 427
421 428 def module_enabled?(module_name)
422 429 module_name = module_name.to_s
423 430 enabled_modules.detect {|m| m.name == module_name}
424 431 end
425 432
426 433 def enabled_module_names=(module_names)
427 434 if module_names && module_names.is_a?(Array)
428 435 module_names = module_names.collect(&:to_s)
429 436 # remove disabled modules
430 437 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
431 438 # add new modules
432 439 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
433 440 else
434 441 enabled_modules.clear
435 442 end
436 443 end
437 444
438 445 # Returns an auto-generated project identifier based on the last identifier used
439 446 def self.next_identifier
440 447 p = Project.find(:first, :order => 'created_on DESC')
441 448 p.nil? ? nil : p.identifier.to_s.succ
442 449 end
443 450
444 451 # Copies and saves the Project instance based on the +project+.
445 452 # Duplicates the source project's:
446 453 # * Wiki
447 454 # * Versions
448 455 # * Categories
449 456 # * Issues
450 457 # * Members
451 458 # * Queries
452 459 #
453 460 # Accepts an +options+ argument to specify what to copy
454 461 #
455 462 # Examples:
456 463 # project.copy(1) # => copies everything
457 464 # project.copy(1, :only => 'members') # => copies members only
458 465 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
459 466 def copy(project, options={})
460 467 project = project.is_a?(Project) ? project : Project.find(project)
461 468
462 469 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
463 470 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
464 471
465 472 Project.transaction do
466 473 if save
467 474 reload
468 475 to_be_copied.each do |name|
469 476 send "copy_#{name}", project
470 477 end
471 478 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
472 479 save
473 480 end
474 481 end
475 482 end
476 483
477 484
478 485 # Copies +project+ and returns the new instance. This will not save
479 486 # the copy
480 487 def self.copy_from(project)
481 488 begin
482 489 project = project.is_a?(Project) ? project : Project.find(project)
483 490 if project
484 491 # clear unique attributes
485 492 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
486 493 copy = Project.new(attributes)
487 494 copy.enabled_modules = project.enabled_modules
488 495 copy.trackers = project.trackers
489 496 copy.custom_values = project.custom_values.collect {|v| v.clone}
490 497 copy.issue_custom_fields = project.issue_custom_fields
491 498 return copy
492 499 else
493 500 return nil
494 501 end
495 502 rescue ActiveRecord::RecordNotFound
496 503 return nil
497 504 end
498 505 end
499 506
500 507 private
501 508
502 509 # Destroys children before destroying self
503 510 def destroy_children
504 511 children.each do |child|
505 512 child.destroy
506 513 end
507 514 end
508 515
509 516 # Copies wiki from +project+
510 517 def copy_wiki(project)
511 518 # Check that the source project has a wiki first
512 519 unless project.wiki.nil?
513 520 self.wiki ||= Wiki.new
514 521 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
515 522 wiki_pages_map = {}
516 523 project.wiki.pages.each do |page|
517 524 # Skip pages without content
518 525 next if page.content.nil?
519 526 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
520 527 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
521 528 new_wiki_page.content = new_wiki_content
522 529 wiki.pages << new_wiki_page
523 530 wiki_pages_map[page.id] = new_wiki_page
524 531 end
525 532 wiki.save
526 533 # Reproduce page hierarchy
527 534 project.wiki.pages.each do |page|
528 535 if page.parent_id && wiki_pages_map[page.id]
529 536 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
530 537 wiki_pages_map[page.id].save
531 538 end
532 539 end
533 540 end
534 541 end
535 542
536 543 # Copies versions from +project+
537 544 def copy_versions(project)
538 545 project.versions.each do |version|
539 546 new_version = Version.new
540 547 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
541 548 self.versions << new_version
542 549 end
543 550 end
544 551
545 552 # Copies issue categories from +project+
546 553 def copy_issue_categories(project)
547 554 project.issue_categories.each do |issue_category|
548 555 new_issue_category = IssueCategory.new
549 556 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
550 557 self.issue_categories << new_issue_category
551 558 end
552 559 end
553 560
554 561 # Copies issues from +project+
555 562 def copy_issues(project)
556 563 # Stores the source issue id as a key and the copied issues as the
557 564 # value. Used to map the two togeather for issue relations.
558 565 issues_map = {}
559 566
560 567 # Get issues sorted by root_id, lft so that parent issues
561 568 # get copied before their children
562 569 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
563 570 new_issue = Issue.new
564 571 new_issue.copy_from(issue)
565 572 new_issue.project = self
566 573 # Reassign fixed_versions by name, since names are unique per
567 574 # project and the versions for self are not yet saved
568 575 if issue.fixed_version
569 576 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
570 577 end
571 578 # Reassign the category by name, since names are unique per
572 579 # project and the categories for self are not yet saved
573 580 if issue.category
574 581 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
575 582 end
576 583 # Parent issue
577 584 if issue.parent_id
578 585 if copied_parent = issues_map[issue.parent_id]
579 586 new_issue.parent_issue_id = copied_parent.id
580 587 end
581 588 end
582 589
583 590 self.issues << new_issue
584 591 issues_map[issue.id] = new_issue
585 592 end
586 593
587 594 # Relations after in case issues related each other
588 595 project.issues.each do |issue|
589 596 new_issue = issues_map[issue.id]
590 597
591 598 # Relations
592 599 issue.relations_from.each do |source_relation|
593 600 new_issue_relation = IssueRelation.new
594 601 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
595 602 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
596 603 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
597 604 new_issue_relation.issue_to = source_relation.issue_to
598 605 end
599 606 new_issue.relations_from << new_issue_relation
600 607 end
601 608
602 609 issue.relations_to.each do |source_relation|
603 610 new_issue_relation = IssueRelation.new
604 611 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
605 612 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
606 613 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
607 614 new_issue_relation.issue_from = source_relation.issue_from
608 615 end
609 616 new_issue.relations_to << new_issue_relation
610 617 end
611 618 end
612 619 end
613 620
614 621 # Copies members from +project+
615 622 def copy_members(project)
616 623 project.memberships.each do |member|
617 624 new_member = Member.new
618 625 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
619 626 # only copy non inherited roles
620 627 # inherited roles will be added when copying the group membership
621 628 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
622 629 next if role_ids.empty?
623 630 new_member.role_ids = role_ids
624 631 new_member.project = self
625 632 self.members << new_member
626 633 end
627 634 end
628 635
629 636 # Copies queries from +project+
630 637 def copy_queries(project)
631 638 project.queries.each do |query|
632 639 new_query = Query.new
633 640 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
634 641 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
635 642 new_query.project = self
636 643 self.queries << new_query
637 644 end
638 645 end
639 646
640 647 # Copies boards from +project+
641 648 def copy_boards(project)
642 649 project.boards.each do |board|
643 650 new_board = Board.new
644 651 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
645 652 new_board.project = self
646 653 self.boards << new_board
647 654 end
648 655 end
649 656
650 657 def allowed_permissions
651 658 @allowed_permissions ||= begin
652 659 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
653 660 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
654 661 end
655 662 end
656 663
657 664 def allowed_actions
658 665 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
659 666 end
660 667
661 668 # Returns all the active Systemwide and project specific activities
662 669 def active_activities
663 670 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
664 671
665 672 if overridden_activity_ids.empty?
666 673 return TimeEntryActivity.shared.active
667 674 else
668 675 return system_activities_and_project_overrides
669 676 end
670 677 end
671 678
672 679 # Returns all the Systemwide and project specific activities
673 680 # (inactive and active)
674 681 def all_activities
675 682 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
676 683
677 684 if overridden_activity_ids.empty?
678 685 return TimeEntryActivity.shared
679 686 else
680 687 return system_activities_and_project_overrides(true)
681 688 end
682 689 end
683 690
684 691 # Returns the systemwide active activities merged with the project specific overrides
685 692 def system_activities_and_project_overrides(include_inactive=false)
686 693 if include_inactive
687 694 return TimeEntryActivity.shared.
688 695 find(:all,
689 696 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
690 697 self.time_entry_activities
691 698 else
692 699 return TimeEntryActivity.shared.active.
693 700 find(:all,
694 701 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
695 702 self.time_entry_activities.active
696 703 end
697 704 end
698 705
699 706 # Archives subprojects recursively
700 707 def archive!
701 708 children.each do |subproject|
702 709 subproject.send :archive!
703 710 end
704 711 update_attribute :status, STATUS_ARCHIVED
705 712 end
706 713 end
@@ -1,715 +1,717
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'projects_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class ProjectsController; def rescue_action(e) raise e end; end
23 23
24 24 class ProjectsControllerTest < ActionController::TestCase
25 25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 27 :attachments, :custom_fields, :custom_values, :time_entries
28 28
29 29 def setup
30 30 @controller = ProjectsController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 @request.session[:user_id] = nil
34 34 Setting.default_language = 'en'
35 35 end
36 36
37 37 def test_index
38 38 get :index
39 39 assert_response :success
40 40 assert_template 'index'
41 41 assert_not_nil assigns(:projects)
42 42
43 43 assert_tag :ul, :child => {:tag => 'li',
44 44 :descendant => {:tag => 'a', :content => 'eCookbook'},
45 45 :child => { :tag => 'ul',
46 46 :descendant => { :tag => 'a',
47 47 :content => 'Child of private child'
48 48 }
49 49 }
50 50 }
51 51
52 52 assert_no_tag :a, :content => /Private child of eCookbook/
53 53 end
54 54
55 55 def test_index_atom
56 56 get :index, :format => 'atom'
57 57 assert_response :success
58 58 assert_template 'common/feed.atom.rxml'
59 59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
60 60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
61 61 end
62 62
63 63 context "#add" do
64 64 context "by admin user" do
65 65 setup do
66 66 @request.session[:user_id] = 1
67 67 end
68 68
69 69 should "accept get" do
70 70 get :add
71 71 assert_response :success
72 72 assert_template 'add'
73 73 end
74 74
75 75 should "accept post" do
76 76 post :add, :project => { :name => "blog",
77 77 :description => "weblog",
78 78 :identifier => "blog",
79 79 :is_public => 1,
80 80 :custom_field_values => { '3' => 'Beta' }
81 81 }
82 82 assert_redirected_to '/projects/blog/settings'
83 83
84 84 project = Project.find_by_name('blog')
85 85 assert_kind_of Project, project
86 86 assert_equal 'weblog', project.description
87 87 assert_equal true, project.is_public?
88 88 assert_nil project.parent
89 89 end
90 90
91 91 should "accept post with parent" do
92 92 post :add, :project => { :name => "blog",
93 93 :description => "weblog",
94 94 :identifier => "blog",
95 95 :is_public => 1,
96 96 :custom_field_values => { '3' => 'Beta' },
97 97 :parent_id => 1
98 98 }
99 99 assert_redirected_to '/projects/blog/settings'
100 100
101 101 project = Project.find_by_name('blog')
102 102 assert_kind_of Project, project
103 103 assert_equal Project.find(1), project.parent
104 104 end
105 105 end
106 106
107 107 context "by non-admin user with add_project permission" do
108 108 setup do
109 109 Role.non_member.add_permission! :add_project
110 110 @request.session[:user_id] = 9
111 111 end
112 112
113 113 should "accept get" do
114 114 get :add
115 115 assert_response :success
116 116 assert_template 'add'
117 117 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
118 118 end
119 119
120 120 should "accept post" do
121 121 post :add, :project => { :name => "blog",
122 122 :description => "weblog",
123 123 :identifier => "blog",
124 124 :is_public => 1,
125 125 :custom_field_values => { '3' => 'Beta' }
126 126 }
127 127
128 128 assert_redirected_to '/projects/blog/settings'
129 129
130 130 project = Project.find_by_name('blog')
131 131 assert_kind_of Project, project
132 132 assert_equal 'weblog', project.description
133 133 assert_equal true, project.is_public?
134 134
135 135 # User should be added as a project member
136 136 assert User.find(9).member_of?(project)
137 137 assert_equal 1, project.members.size
138 138 end
139 139
140 140 should "fail with parent_id" do
141 141 assert_no_difference 'Project.count' do
142 142 post :add, :project => { :name => "blog",
143 143 :description => "weblog",
144 144 :identifier => "blog",
145 145 :is_public => 1,
146 146 :custom_field_values => { '3' => 'Beta' },
147 147 :parent_id => 1
148 148 }
149 149 end
150 150 assert_response :success
151 151 project = assigns(:project)
152 152 assert_kind_of Project, project
153 153 assert_not_nil project.errors.on(:parent_id)
154 154 end
155 155 end
156 156
157 157 context "by non-admin user with add_subprojects permission" do
158 158 setup do
159 159 Role.find(1).remove_permission! :add_project
160 160 Role.find(1).add_permission! :add_subprojects
161 161 @request.session[:user_id] = 2
162 162 end
163 163
164 164 should "accept get" do
165 165 get :add, :parent_id => 'ecookbook'
166 166 assert_response :success
167 167 assert_template 'add'
168 168 # parent project selected
169 169 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
170 170 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
171 171 # no empty value
172 172 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
173 173 :child => {:tag => 'option', :attributes => {:value => ''}}
174 174 end
175 175
176 176 should "accept post with parent_id" do
177 177 post :add, :project => { :name => "blog",
178 178 :description => "weblog",
179 179 :identifier => "blog",
180 180 :is_public => 1,
181 181 :custom_field_values => { '3' => 'Beta' },
182 182 :parent_id => 1
183 183 }
184 184 assert_redirected_to '/projects/blog/settings'
185 185 project = Project.find_by_name('blog')
186 186 end
187 187
188 188 should "fail without parent_id" do
189 189 assert_no_difference 'Project.count' do
190 190 post :add, :project => { :name => "blog",
191 191 :description => "weblog",
192 192 :identifier => "blog",
193 193 :is_public => 1,
194 194 :custom_field_values => { '3' => 'Beta' }
195 195 }
196 196 end
197 197 assert_response :success
198 198 project = assigns(:project)
199 199 assert_kind_of Project, project
200 200 assert_not_nil project.errors.on(:parent_id)
201 201 end
202 202
203 203 should "fail with unauthorized parent_id" do
204 204 assert !User.find(2).member_of?(Project.find(6))
205 205 assert_no_difference 'Project.count' do
206 206 post :add, :project => { :name => "blog",
207 207 :description => "weblog",
208 208 :identifier => "blog",
209 209 :is_public => 1,
210 210 :custom_field_values => { '3' => 'Beta' },
211 211 :parent_id => 6
212 212 }
213 213 end
214 214 assert_response :success
215 215 project = assigns(:project)
216 216 assert_kind_of Project, project
217 217 assert_not_nil project.errors.on(:parent_id)
218 218 end
219 219 end
220 220 end
221 221
222 222 def test_show_by_id
223 223 get :show, :id => 1
224 224 assert_response :success
225 225 assert_template 'show'
226 226 assert_not_nil assigns(:project)
227 227 end
228 228
229 229 def test_show_by_identifier
230 230 get :show, :id => 'ecookbook'
231 231 assert_response :success
232 232 assert_template 'show'
233 233 assert_not_nil assigns(:project)
234 234 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
235 235 end
236 236
237 237 def test_show_should_not_fail_when_custom_values_are_nil
238 238 project = Project.find_by_identifier('ecookbook')
239 239 project.custom_values.first.update_attribute(:value, nil)
240 240 get :show, :id => 'ecookbook'
241 241 assert_response :success
242 242 assert_template 'show'
243 243 assert_not_nil assigns(:project)
244 244 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
245 245 end
246 246
247 247 def test_private_subprojects_hidden
248 248 get :show, :id => 'ecookbook'
249 249 assert_response :success
250 250 assert_template 'show'
251 251 assert_no_tag :tag => 'a', :content => /Private child/
252 252 end
253 253
254 254 def test_private_subprojects_visible
255 255 @request.session[:user_id] = 2 # manager who is a member of the private subproject
256 256 get :show, :id => 'ecookbook'
257 257 assert_response :success
258 258 assert_template 'show'
259 259 assert_tag :tag => 'a', :content => /Private child/
260 260 end
261 261
262 262 def test_settings
263 263 @request.session[:user_id] = 2 # manager
264 264 get :settings, :id => 1
265 265 assert_response :success
266 266 assert_template 'settings'
267 267 end
268 268
269 269 def test_edit
270 270 @request.session[:user_id] = 2 # manager
271 271 post :edit, :id => 1, :project => {:name => 'Test changed name',
272 272 :issue_custom_field_ids => ['']}
273 273 assert_redirected_to 'projects/ecookbook/settings'
274 274 project = Project.find(1)
275 275 assert_equal 'Test changed name', project.name
276 276 end
277 277
278 278 def test_get_destroy
279 279 @request.session[:user_id] = 1 # admin
280 280 get :destroy, :id => 1
281 281 assert_response :success
282 282 assert_template 'destroy'
283 283 assert_not_nil Project.find_by_id(1)
284 284 end
285 285
286 286 def test_post_destroy
287 287 @request.session[:user_id] = 1 # admin
288 288 post :destroy, :id => 1, :confirm => 1
289 289 assert_redirected_to 'admin/projects'
290 290 assert_nil Project.find_by_id(1)
291 291 end
292 292
293 293 def test_add_file
294 294 set_tmp_attachments_directory
295 295 @request.session[:user_id] = 2
296 296 Setting.notified_events = ['file_added']
297 297 ActionMailer::Base.deliveries.clear
298 298
299 299 assert_difference 'Attachment.count' do
300 300 post :add_file, :id => 1, :version_id => '',
301 301 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
302 302 end
303 303 assert_redirected_to 'projects/ecookbook/files'
304 304 a = Attachment.find(:first, :order => 'created_on DESC')
305 305 assert_equal 'testfile.txt', a.filename
306 306 assert_equal Project.find(1), a.container
307 307
308 308 mail = ActionMailer::Base.deliveries.last
309 309 assert_kind_of TMail::Mail, mail
310 310 assert_equal "[eCookbook] New file", mail.subject
311 311 assert mail.body.include?('testfile.txt')
312 312 end
313 313
314 314 def test_add_version_file
315 315 set_tmp_attachments_directory
316 316 @request.session[:user_id] = 2
317 317 Setting.notified_events = ['file_added']
318 318
319 319 assert_difference 'Attachment.count' do
320 320 post :add_file, :id => 1, :version_id => '2',
321 321 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
322 322 end
323 323 assert_redirected_to 'projects/ecookbook/files'
324 324 a = Attachment.find(:first, :order => 'created_on DESC')
325 325 assert_equal 'testfile.txt', a.filename
326 326 assert_equal Version.find(2), a.container
327 327 end
328 328
329 329 def test_list_files
330 330 get :list_files, :id => 1
331 331 assert_response :success
332 332 assert_template 'list_files'
333 333 assert_not_nil assigns(:containers)
334 334
335 335 # file attached to the project
336 336 assert_tag :a, :content => 'project_file.zip',
337 337 :attributes => { :href => '/attachments/download/8/project_file.zip' }
338 338
339 339 # file attached to a project's version
340 340 assert_tag :a, :content => 'version_file.zip',
341 341 :attributes => { :href => '/attachments/download/9/version_file.zip' }
342 342 end
343 343
344 344 def test_roadmap
345 345 get :roadmap, :id => 1
346 346 assert_response :success
347 347 assert_template 'roadmap'
348 348 assert_not_nil assigns(:versions)
349 349 # Version with no date set appears
350 350 assert assigns(:versions).include?(Version.find(3))
351 351 # Completed version doesn't appear
352 352 assert !assigns(:versions).include?(Version.find(1))
353 353 end
354 354
355 355 def test_roadmap_with_completed_versions
356 356 get :roadmap, :id => 1, :completed => 1
357 357 assert_response :success
358 358 assert_template 'roadmap'
359 359 assert_not_nil assigns(:versions)
360 360 # Version with no date set appears
361 361 assert assigns(:versions).include?(Version.find(3))
362 362 # Completed version appears
363 363 assert assigns(:versions).include?(Version.find(1))
364 364 end
365 365
366 366 def test_roadmap_showing_subprojects_versions
367 @subproject_version = Version.generate!(:project => Project.find(3))
367 368 get :roadmap, :id => 1, :with_subprojects => 1
368 369 assert_response :success
369 370 assert_template 'roadmap'
370 371 assert_not_nil assigns(:versions)
371 # Version on subproject appears
372 assert assigns(:versions).include?(Version.find(4))
372
373 assert assigns(:versions).include?(Version.find(4)), "Shared version not found"
374 assert assigns(:versions).include?(@subproject_version), "Subproject version not found"
373 375 end
374 376 def test_project_activity
375 377 get :activity, :id => 1, :with_subprojects => 0
376 378 assert_response :success
377 379 assert_template 'activity'
378 380 assert_not_nil assigns(:events_by_day)
379 381
380 382 assert_tag :tag => "h3",
381 383 :content => /#{2.days.ago.to_date.day}/,
382 384 :sibling => { :tag => "dl",
383 385 :child => { :tag => "dt",
384 386 :attributes => { :class => /issue-edit/ },
385 387 :child => { :tag => "a",
386 388 :content => /(#{IssueStatus.find(2).name})/,
387 389 }
388 390 }
389 391 }
390 392 end
391 393
392 394 def test_previous_project_activity
393 395 get :activity, :id => 1, :from => 3.days.ago.to_date
394 396 assert_response :success
395 397 assert_template 'activity'
396 398 assert_not_nil assigns(:events_by_day)
397 399
398 400 assert_tag :tag => "h3",
399 401 :content => /#{3.day.ago.to_date.day}/,
400 402 :sibling => { :tag => "dl",
401 403 :child => { :tag => "dt",
402 404 :attributes => { :class => /issue/ },
403 405 :child => { :tag => "a",
404 406 :content => /#{Issue.find(1).subject}/,
405 407 }
406 408 }
407 409 }
408 410 end
409 411
410 412 def test_global_activity
411 413 get :activity
412 414 assert_response :success
413 415 assert_template 'activity'
414 416 assert_not_nil assigns(:events_by_day)
415 417
416 418 assert_tag :tag => "h3",
417 419 :content => /#{5.day.ago.to_date.day}/,
418 420 :sibling => { :tag => "dl",
419 421 :child => { :tag => "dt",
420 422 :attributes => { :class => /issue/ },
421 423 :child => { :tag => "a",
422 424 :content => /#{Issue.find(5).subject}/,
423 425 }
424 426 }
425 427 }
426 428 end
427 429
428 430 def test_user_activity
429 431 get :activity, :user_id => 2
430 432 assert_response :success
431 433 assert_template 'activity'
432 434 assert_not_nil assigns(:events_by_day)
433 435
434 436 assert_tag :tag => "h3",
435 437 :content => /#{3.day.ago.to_date.day}/,
436 438 :sibling => { :tag => "dl",
437 439 :child => { :tag => "dt",
438 440 :attributes => { :class => /issue/ },
439 441 :child => { :tag => "a",
440 442 :content => /#{Issue.find(1).subject}/,
441 443 }
442 444 }
443 445 }
444 446 end
445 447
446 448 def test_activity_atom_feed
447 449 get :activity, :format => 'atom'
448 450 assert_response :success
449 451 assert_template 'common/feed.atom.rxml'
450 452 assert_tag :tag => 'entry', :child => {
451 453 :tag => 'link',
452 454 :attributes => {:href => 'http://test.host/issues/11'}}
453 455 end
454 456
455 457 def test_archive
456 458 @request.session[:user_id] = 1 # admin
457 459 post :archive, :id => 1
458 460 assert_redirected_to 'admin/projects'
459 461 assert !Project.find(1).active?
460 462 end
461 463
462 464 def test_unarchive
463 465 @request.session[:user_id] = 1 # admin
464 466 Project.find(1).archive
465 467 post :unarchive, :id => 1
466 468 assert_redirected_to 'admin/projects'
467 469 assert Project.find(1).active?
468 470 end
469 471
470 472 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
471 473 CustomField.delete_all
472 474 parent = nil
473 475 6.times do |i|
474 476 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
475 477 p.set_parent!(parent)
476 478 get :show, :id => p
477 479 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
478 480 :children => { :count => [i, 3].min,
479 481 :only => { :tag => 'a' } }
480 482
481 483 parent = p
482 484 end
483 485 end
484 486
485 487 def test_copy_with_project
486 488 @request.session[:user_id] = 1 # admin
487 489 get :copy, :id => 1
488 490 assert_response :success
489 491 assert_template 'copy'
490 492 assert assigns(:project)
491 493 assert_equal Project.find(1).description, assigns(:project).description
492 494 assert_nil assigns(:project).id
493 495 end
494 496
495 497 def test_copy_without_project
496 498 @request.session[:user_id] = 1 # admin
497 499 get :copy
498 500 assert_response :redirect
499 501 assert_redirected_to :controller => 'admin', :action => 'projects'
500 502 end
501 503
502 504 def test_jump_should_redirect_to_active_tab
503 505 get :show, :id => 1, :jump => 'issues'
504 506 assert_redirected_to 'projects/ecookbook/issues'
505 507 end
506 508
507 509 def test_jump_should_not_redirect_to_inactive_tab
508 510 get :show, :id => 3, :jump => 'documents'
509 511 assert_response :success
510 512 assert_template 'show'
511 513 end
512 514
513 515 def test_jump_should_not_redirect_to_unknown_tab
514 516 get :show, :id => 3, :jump => 'foobar'
515 517 assert_response :success
516 518 assert_template 'show'
517 519 end
518 520
519 521 def test_reset_activities
520 522 @request.session[:user_id] = 2 # manager
521 523 project_activity = TimeEntryActivity.new({
522 524 :name => 'Project Specific',
523 525 :parent => TimeEntryActivity.find(:first),
524 526 :project => Project.find(1),
525 527 :active => true
526 528 })
527 529 assert project_activity.save
528 530 project_activity_two = TimeEntryActivity.new({
529 531 :name => 'Project Specific Two',
530 532 :parent => TimeEntryActivity.find(:last),
531 533 :project => Project.find(1),
532 534 :active => true
533 535 })
534 536 assert project_activity_two.save
535 537
536 538 delete :reset_activities, :id => 1
537 539 assert_response :redirect
538 540 assert_redirected_to 'projects/ecookbook/settings/activities'
539 541
540 542 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
541 543 assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
542 544 end
543 545
544 546 def test_reset_activities_should_reassign_time_entries_back_to_the_system_activity
545 547 @request.session[:user_id] = 2 # manager
546 548 project_activity = TimeEntryActivity.new({
547 549 :name => 'Project Specific Design',
548 550 :parent => TimeEntryActivity.find(9),
549 551 :project => Project.find(1),
550 552 :active => true
551 553 })
552 554 assert project_activity.save
553 555 assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9])
554 556 assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size
555 557
556 558 delete :reset_activities, :id => 1
557 559 assert_response :redirect
558 560 assert_redirected_to 'projects/ecookbook/settings/activities'
559 561
560 562 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
561 563 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity"
562 564 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity"
563 565 end
564 566
565 567 def test_save_activities_to_override_system_activities
566 568 @request.session[:user_id] = 2 # manager
567 569 billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
568 570
569 571 post :save_activities, :id => 1, :enumerations => {
570 572 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
571 573 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
572 574 "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
573 575 "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
574 576 }
575 577
576 578 assert_response :redirect
577 579 assert_redirected_to 'projects/ecookbook/settings/activities'
578 580
579 581 # Created project specific activities...
580 582 project = Project.find('ecookbook')
581 583
582 584 # ... Design
583 585 design = project.time_entry_activities.find_by_name("Design")
584 586 assert design, "Project activity not found"
585 587
586 588 assert_equal 9, design.parent_id # Relate to the system activity
587 589 assert_not_equal design.parent.id, design.id # Different records
588 590 assert_equal design.parent.name, design.name # Same name
589 591 assert !design.active?
590 592
591 593 # ... Development
592 594 development = project.time_entry_activities.find_by_name("Development")
593 595 assert development, "Project activity not found"
594 596
595 597 assert_equal 10, development.parent_id # Relate to the system activity
596 598 assert_not_equal development.parent.id, development.id # Different records
597 599 assert_equal development.parent.name, development.name # Same name
598 600 assert development.active?
599 601 assert_equal "0", development.custom_value_for(billable_field).value
600 602
601 603 # ... Inactive Activity
602 604 previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
603 605 assert previously_inactive, "Project activity not found"
604 606
605 607 assert_equal 14, previously_inactive.parent_id # Relate to the system activity
606 608 assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
607 609 assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
608 610 assert previously_inactive.active?
609 611 assert_equal "1", previously_inactive.custom_value_for(billable_field).value
610 612
611 613 # ... QA
612 614 assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
613 615 end
614 616
615 617 def test_save_activities_will_update_project_specific_activities
616 618 @request.session[:user_id] = 2 # manager
617 619
618 620 project_activity = TimeEntryActivity.new({
619 621 :name => 'Project Specific',
620 622 :parent => TimeEntryActivity.find(:first),
621 623 :project => Project.find(1),
622 624 :active => true
623 625 })
624 626 assert project_activity.save
625 627 project_activity_two = TimeEntryActivity.new({
626 628 :name => 'Project Specific Two',
627 629 :parent => TimeEntryActivity.find(:last),
628 630 :project => Project.find(1),
629 631 :active => true
630 632 })
631 633 assert project_activity_two.save
632 634
633 635
634 636 post :save_activities, :id => 1, :enumerations => {
635 637 project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
636 638 project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
637 639 }
638 640
639 641 assert_response :redirect
640 642 assert_redirected_to 'projects/ecookbook/settings/activities'
641 643
642 644 # Created project specific activities...
643 645 project = Project.find('ecookbook')
644 646 assert_equal 2, project.time_entry_activities.count
645 647
646 648 activity_one = project.time_entry_activities.find_by_name(project_activity.name)
647 649 assert activity_one, "Project activity not found"
648 650 assert_equal project_activity.id, activity_one.id
649 651 assert !activity_one.active?
650 652
651 653 activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
652 654 assert activity_two, "Project activity not found"
653 655 assert_equal project_activity_two.id, activity_two.id
654 656 assert !activity_two.active?
655 657 end
656 658
657 659 def test_save_activities_when_creating_new_activities_will_convert_existing_data
658 660 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
659 661
660 662 @request.session[:user_id] = 2 # manager
661 663 post :save_activities, :id => 1, :enumerations => {
662 664 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate
663 665 }
664 666 assert_response :redirect
665 667
666 668 # No more TimeEntries using the system activity
667 669 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities"
668 670 # All TimeEntries using project activity
669 671 project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1)
670 672 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity"
671 673 end
672 674
673 675 def test_save_activities_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
674 676 # TODO: Need to cause an exception on create but these tests
675 677 # aren't setup for mocking. Just create a record now so the
676 678 # second one is a dupicate
677 679 parent = TimeEntryActivity.find(9)
678 680 TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true})
679 681 TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
680 682
681 683 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
682 684 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size
683 685
684 686 @request.session[:user_id] = 2 # manager
685 687 post :save_activities, :id => 1, :enumerations => {
686 688 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design
687 689 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value
688 690 }
689 691 assert_response :redirect
690 692
691 693 # TimeEntries shouldn't have been reassigned on the failed record
692 694 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities"
693 695 # TimeEntries shouldn't have been reassigned on the saved record either
694 696 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities"
695 697 end
696 698
697 699 # A hook that is manually registered later
698 700 class ProjectBasedTemplate < Redmine::Hook::ViewListener
699 701 def view_layouts_base_html_head(context)
700 702 # Adds a project stylesheet
701 703 stylesheet_link_tag(context[:project].identifier) if context[:project]
702 704 end
703 705 end
704 706 # Don't use this hook now
705 707 Redmine::Hook.clear_listeners
706 708
707 709 def test_hook_response
708 710 Redmine::Hook.add_listener(ProjectBasedTemplate)
709 711 get :show, :id => 1
710 712 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
711 713 :parent => {:tag => 'head'}
712 714
713 715 Redmine::Hook.clear_listeners
714 716 end
715 717 end
@@ -1,792 +1,845
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class ProjectTest < ActiveSupport::TestCase
21 21 fixtures :all
22 22
23 23 def setup
24 24 @ecookbook = Project.find(1)
25 25 @ecookbook_sub1 = Project.find(3)
26 26 User.current = nil
27 27 end
28 28
29 29 should_validate_presence_of :name
30 30 should_validate_presence_of :identifier
31 31
32 32 should_validate_uniqueness_of :name
33 33 should_validate_uniqueness_of :identifier
34 34
35 35 context "associations" do
36 36 should_have_many :members
37 37 should_have_many :users, :through => :members
38 38 should_have_many :member_principals
39 39 should_have_many :principals, :through => :member_principals
40 40 should_have_many :enabled_modules
41 41 should_have_many :issues
42 42 should_have_many :issue_changes, :through => :issues
43 43 should_have_many :versions
44 44 should_have_many :time_entries
45 45 should_have_many :queries
46 46 should_have_many :documents
47 47 should_have_many :news
48 48 should_have_many :issue_categories
49 49 should_have_many :boards
50 50 should_have_many :changesets, :through => :repository
51 51
52 52 should_have_one :repository
53 53 should_have_one :wiki
54 54
55 55 should_have_and_belong_to_many :trackers
56 56 should_have_and_belong_to_many :issue_custom_fields
57 57 end
58 58
59 59 def test_truth
60 60 assert_kind_of Project, @ecookbook
61 61 assert_equal "eCookbook", @ecookbook.name
62 62 end
63 63
64 64 def test_update
65 65 assert_equal "eCookbook", @ecookbook.name
66 66 @ecookbook.name = "eCook"
67 67 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
68 68 @ecookbook.reload
69 69 assert_equal "eCook", @ecookbook.name
70 70 end
71 71
72 72 def test_validate_identifier
73 73 to_test = {"abc" => true,
74 74 "ab12" => true,
75 75 "ab-12" => true,
76 76 "12" => false,
77 77 "new" => false}
78 78
79 79 to_test.each do |identifier, valid|
80 80 p = Project.new
81 81 p.identifier = identifier
82 82 p.valid?
83 83 assert_equal valid, p.errors.on('identifier').nil?
84 84 end
85 85 end
86 86
87 87 def test_members_should_be_active_users
88 88 Project.all.each do |project|
89 89 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
90 90 end
91 91 end
92 92
93 93 def test_users_should_be_active_users
94 94 Project.all.each do |project|
95 95 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
96 96 end
97 97 end
98 98
99 99 def test_archive
100 100 user = @ecookbook.members.first.user
101 101 @ecookbook.archive
102 102 @ecookbook.reload
103 103
104 104 assert !@ecookbook.active?
105 105 assert !user.projects.include?(@ecookbook)
106 106 # Subproject are also archived
107 107 assert !@ecookbook.children.empty?
108 108 assert @ecookbook.descendants.active.empty?
109 109 end
110 110
111 111 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
112 112 # Assign an issue of a project to a version of a child project
113 113 Issue.find(4).update_attribute :fixed_version_id, 4
114 114
115 115 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
116 116 assert_equal false, @ecookbook.archive
117 117 end
118 118 @ecookbook.reload
119 119 assert @ecookbook.active?
120 120 end
121 121
122 122 def test_unarchive
123 123 user = @ecookbook.members.first.user
124 124 @ecookbook.archive
125 125 # A subproject of an archived project can not be unarchived
126 126 assert !@ecookbook_sub1.unarchive
127 127
128 128 # Unarchive project
129 129 assert @ecookbook.unarchive
130 130 @ecookbook.reload
131 131 assert @ecookbook.active?
132 132 assert user.projects.include?(@ecookbook)
133 133 # Subproject can now be unarchived
134 134 @ecookbook_sub1.reload
135 135 assert @ecookbook_sub1.unarchive
136 136 end
137 137
138 138 def test_destroy
139 139 # 2 active members
140 140 assert_equal 2, @ecookbook.members.size
141 141 # and 1 is locked
142 142 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
143 143 # some boards
144 144 assert @ecookbook.boards.any?
145 145
146 146 @ecookbook.destroy
147 147 # make sure that the project non longer exists
148 148 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
149 149 # make sure related data was removed
150 150 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
151 151 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
152 152 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
153 153 end
154 154
155 155 def test_move_an_orphan_project_to_a_root_project
156 156 sub = Project.find(2)
157 157 sub.set_parent! @ecookbook
158 158 assert_equal @ecookbook.id, sub.parent.id
159 159 @ecookbook.reload
160 160 assert_equal 4, @ecookbook.children.size
161 161 end
162 162
163 163 def test_move_an_orphan_project_to_a_subproject
164 164 sub = Project.find(2)
165 165 assert sub.set_parent!(@ecookbook_sub1)
166 166 end
167 167
168 168 def test_move_a_root_project_to_a_project
169 169 sub = @ecookbook
170 170 assert sub.set_parent!(Project.find(2))
171 171 end
172 172
173 173 def test_should_not_move_a_project_to_its_children
174 174 sub = @ecookbook
175 175 assert !(sub.set_parent!(Project.find(3)))
176 176 end
177 177
178 178 def test_set_parent_should_add_roots_in_alphabetical_order
179 179 ProjectCustomField.delete_all
180 180 Project.delete_all
181 181 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
182 182 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
183 183 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
184 184 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
185 185
186 186 assert_equal 4, Project.count
187 187 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
188 188 end
189 189
190 190 def test_set_parent_should_add_children_in_alphabetical_order
191 191 ProjectCustomField.delete_all
192 192 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
193 193 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
194 194 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
195 195 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
196 196 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
197 197
198 198 parent.reload
199 199 assert_equal 4, parent.children.size
200 200 assert_equal parent.children.sort_by(&:name), parent.children
201 201 end
202 202
203 203 def test_rebuild_should_sort_children_alphabetically
204 204 ProjectCustomField.delete_all
205 205 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
206 206 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
207 207 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
208 208 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
209 209 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
210 210
211 211 Project.update_all("lft = NULL, rgt = NULL")
212 212 Project.rebuild!
213 213
214 214 parent.reload
215 215 assert_equal 4, parent.children.size
216 216 assert_equal parent.children.sort_by(&:name), parent.children
217 217 end
218 218
219 219
220 220 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
221 221 # Parent issue with a hierarchy project's fixed version
222 222 parent_issue = Issue.find(1)
223 223 parent_issue.update_attribute(:fixed_version_id, 4)
224 224 parent_issue.reload
225 225 assert_equal 4, parent_issue.fixed_version_id
226 226
227 227 # Should keep fixed versions for the issues
228 228 issue_with_local_fixed_version = Issue.find(5)
229 229 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
230 230 issue_with_local_fixed_version.reload
231 231 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
232 232
233 233 # Local issue with hierarchy fixed_version
234 234 issue_with_hierarchy_fixed_version = Issue.find(13)
235 235 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
236 236 issue_with_hierarchy_fixed_version.reload
237 237 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
238 238
239 239 # Move project out of the issue's hierarchy
240 240 moved_project = Project.find(3)
241 241 moved_project.set_parent!(Project.find(2))
242 242 parent_issue.reload
243 243 issue_with_local_fixed_version.reload
244 244 issue_with_hierarchy_fixed_version.reload
245 245
246 246 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
247 247 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
248 248 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
249 249 end
250 250
251 251 def test_parent
252 252 p = Project.find(6).parent
253 253 assert p.is_a?(Project)
254 254 assert_equal 5, p.id
255 255 end
256 256
257 257 def test_ancestors
258 258 a = Project.find(6).ancestors
259 259 assert a.first.is_a?(Project)
260 260 assert_equal [1, 5], a.collect(&:id)
261 261 end
262 262
263 263 def test_root
264 264 r = Project.find(6).root
265 265 assert r.is_a?(Project)
266 266 assert_equal 1, r.id
267 267 end
268 268
269 269 def test_children
270 270 c = Project.find(1).children
271 271 assert c.first.is_a?(Project)
272 272 assert_equal [5, 3, 4], c.collect(&:id)
273 273 end
274 274
275 275 def test_descendants
276 276 d = Project.find(1).descendants
277 277 assert d.first.is_a?(Project)
278 278 assert_equal [5, 6, 3, 4], d.collect(&:id)
279 279 end
280 280
281 281 def test_allowed_parents_should_be_empty_for_non_member_user
282 282 Role.non_member.add_permission!(:add_project)
283 283 user = User.find(9)
284 284 assert user.memberships.empty?
285 285 User.current = user
286 286 assert Project.new.allowed_parents.compact.empty?
287 287 end
288 288
289 289 def test_allowed_parents_with_add_subprojects_permission
290 290 Role.find(1).remove_permission!(:add_project)
291 291 Role.find(1).add_permission!(:add_subprojects)
292 292 User.current = User.find(2)
293 293 # new project
294 294 assert !Project.new.allowed_parents.include?(nil)
295 295 assert Project.new.allowed_parents.include?(Project.find(1))
296 296 # existing root project
297 297 assert Project.find(1).allowed_parents.include?(nil)
298 298 # existing child
299 299 assert Project.find(3).allowed_parents.include?(Project.find(1))
300 300 assert !Project.find(3).allowed_parents.include?(nil)
301 301 end
302 302
303 303 def test_allowed_parents_with_add_project_permission
304 304 Role.find(1).add_permission!(:add_project)
305 305 Role.find(1).remove_permission!(:add_subprojects)
306 306 User.current = User.find(2)
307 307 # new project
308 308 assert Project.new.allowed_parents.include?(nil)
309 309 assert !Project.new.allowed_parents.include?(Project.find(1))
310 310 # existing root project
311 311 assert Project.find(1).allowed_parents.include?(nil)
312 312 # existing child
313 313 assert Project.find(3).allowed_parents.include?(Project.find(1))
314 314 assert Project.find(3).allowed_parents.include?(nil)
315 315 end
316 316
317 317 def test_allowed_parents_with_add_project_and_subprojects_permission
318 318 Role.find(1).add_permission!(:add_project)
319 319 Role.find(1).add_permission!(:add_subprojects)
320 320 User.current = User.find(2)
321 321 # new project
322 322 assert Project.new.allowed_parents.include?(nil)
323 323 assert Project.new.allowed_parents.include?(Project.find(1))
324 324 # existing root project
325 325 assert Project.find(1).allowed_parents.include?(nil)
326 326 # existing child
327 327 assert Project.find(3).allowed_parents.include?(Project.find(1))
328 328 assert Project.find(3).allowed_parents.include?(nil)
329 329 end
330 330
331 331 def test_users_by_role
332 332 users_by_role = Project.find(1).users_by_role
333 333 assert_kind_of Hash, users_by_role
334 334 role = Role.find(1)
335 335 assert_kind_of Array, users_by_role[role]
336 336 assert users_by_role[role].include?(User.find(2))
337 337 end
338 338
339 339 def test_rolled_up_trackers
340 340 parent = Project.find(1)
341 341 parent.trackers = Tracker.find([1,2])
342 342 child = parent.children.find(3)
343 343
344 344 assert_equal [1, 2], parent.tracker_ids
345 345 assert_equal [2, 3], child.trackers.collect(&:id)
346 346
347 347 assert_kind_of Tracker, parent.rolled_up_trackers.first
348 348 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
349 349
350 350 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
351 351 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
352 352 end
353 353
354 354 def test_rolled_up_trackers_should_ignore_archived_subprojects
355 355 parent = Project.find(1)
356 356 parent.trackers = Tracker.find([1,2])
357 357 child = parent.children.find(3)
358 358 child.trackers = Tracker.find([1,3])
359 359 parent.children.each(&:archive)
360 360
361 361 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
362 362 end
363
364 context "#rolled_up_versions" do
365 setup do
366 @project = Project.generate!
367 @parent_version_1 = Version.generate!(:project => @project)
368 @parent_version_2 = Version.generate!(:project => @project)
369 end
370
371 should "include the versions for the current project" do
372 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
373 end
374
375 should "include versions for a subproject" do
376 @subproject = Project.generate!
377 @subproject.set_parent!(@project)
378 @subproject_version = Version.generate!(:project => @subproject)
379
380 assert_same_elements [
381 @parent_version_1,
382 @parent_version_2,
383 @subproject_version
384 ], @project.rolled_up_versions
385 end
386
387 should "include versions for a sub-subproject" do
388 @subproject = Project.generate!
389 @subproject.set_parent!(@project)
390 @sub_subproject = Project.generate!
391 @sub_subproject.set_parent!(@subproject)
392 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
393
394 @project.reload
395
396 assert_same_elements [
397 @parent_version_1,
398 @parent_version_2,
399 @sub_subproject_version
400 ], @project.rolled_up_versions
401 end
402
403
404 should "only check active projects" do
405 @subproject = Project.generate!
406 @subproject.set_parent!(@project)
407 @subproject_version = Version.generate!(:project => @subproject)
408 assert @subproject.archive
409
410 @project.reload
411
412 assert !@subproject.active?
413 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
414 end
415 end
363 416
364 417 def test_shared_versions_none_sharing
365 418 p = Project.find(5)
366 419 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
367 420 assert p.shared_versions.include?(v)
368 421 assert !p.children.first.shared_versions.include?(v)
369 422 assert !p.root.shared_versions.include?(v)
370 423 assert !p.siblings.first.shared_versions.include?(v)
371 424 assert !p.root.siblings.first.shared_versions.include?(v)
372 425 end
373 426
374 427 def test_shared_versions_descendants_sharing
375 428 p = Project.find(5)
376 429 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
377 430 assert p.shared_versions.include?(v)
378 431 assert p.children.first.shared_versions.include?(v)
379 432 assert !p.root.shared_versions.include?(v)
380 433 assert !p.siblings.first.shared_versions.include?(v)
381 434 assert !p.root.siblings.first.shared_versions.include?(v)
382 435 end
383 436
384 437 def test_shared_versions_hierarchy_sharing
385 438 p = Project.find(5)
386 439 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
387 440 assert p.shared_versions.include?(v)
388 441 assert p.children.first.shared_versions.include?(v)
389 442 assert p.root.shared_versions.include?(v)
390 443 assert !p.siblings.first.shared_versions.include?(v)
391 444 assert !p.root.siblings.first.shared_versions.include?(v)
392 445 end
393 446
394 447 def test_shared_versions_tree_sharing
395 448 p = Project.find(5)
396 449 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
397 450 assert p.shared_versions.include?(v)
398 451 assert p.children.first.shared_versions.include?(v)
399 452 assert p.root.shared_versions.include?(v)
400 453 assert p.siblings.first.shared_versions.include?(v)
401 454 assert !p.root.siblings.first.shared_versions.include?(v)
402 455 end
403 456
404 457 def test_shared_versions_system_sharing
405 458 p = Project.find(5)
406 459 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
407 460 assert p.shared_versions.include?(v)
408 461 assert p.children.first.shared_versions.include?(v)
409 462 assert p.root.shared_versions.include?(v)
410 463 assert p.siblings.first.shared_versions.include?(v)
411 464 assert p.root.siblings.first.shared_versions.include?(v)
412 465 end
413 466
414 467 def test_shared_versions
415 468 parent = Project.find(1)
416 469 child = parent.children.find(3)
417 470 private_child = parent.children.find(5)
418 471
419 472 assert_equal [1,2,3], parent.version_ids.sort
420 473 assert_equal [4], child.version_ids
421 474 assert_equal [6], private_child.version_ids
422 475 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
423 476
424 477 assert_equal 6, parent.shared_versions.size
425 478 parent.shared_versions.each do |version|
426 479 assert_kind_of Version, version
427 480 end
428 481
429 482 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
430 483 end
431 484
432 485 def test_shared_versions_should_ignore_archived_subprojects
433 486 parent = Project.find(1)
434 487 child = parent.children.find(3)
435 488 child.archive
436 489 parent.reload
437 490
438 491 assert_equal [1,2,3], parent.version_ids.sort
439 492 assert_equal [4], child.version_ids
440 493 assert !parent.shared_versions.collect(&:id).include?(4)
441 494 end
442 495
443 496 def test_shared_versions_visible_to_user
444 497 user = User.find(3)
445 498 parent = Project.find(1)
446 499 child = parent.children.find(5)
447 500
448 501 assert_equal [1,2,3], parent.version_ids.sort
449 502 assert_equal [6], child.version_ids
450 503
451 504 versions = parent.shared_versions.visible(user)
452 505
453 506 assert_equal 4, versions.size
454 507 versions.each do |version|
455 508 assert_kind_of Version, version
456 509 end
457 510
458 511 assert !versions.collect(&:id).include?(6)
459 512 end
460 513
461 514
462 515 def test_next_identifier
463 516 ProjectCustomField.delete_all
464 517 Project.create!(:name => 'last', :identifier => 'p2008040')
465 518 assert_equal 'p2008041', Project.next_identifier
466 519 end
467 520
468 521 def test_next_identifier_first_project
469 522 Project.delete_all
470 523 assert_nil Project.next_identifier
471 524 end
472 525
473 526
474 527 def test_enabled_module_names_should_not_recreate_enabled_modules
475 528 project = Project.find(1)
476 529 # Remove one module
477 530 modules = project.enabled_modules.slice(0..-2)
478 531 assert modules.any?
479 532 assert_difference 'EnabledModule.count', -1 do
480 533 project.enabled_module_names = modules.collect(&:name)
481 534 end
482 535 project.reload
483 536 # Ids should be preserved
484 537 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
485 538 end
486 539
487 540 def test_copy_from_existing_project
488 541 source_project = Project.find(1)
489 542 copied_project = Project.copy_from(1)
490 543
491 544 assert copied_project
492 545 # Cleared attributes
493 546 assert copied_project.id.blank?
494 547 assert copied_project.name.blank?
495 548 assert copied_project.identifier.blank?
496 549
497 550 # Duplicated attributes
498 551 assert_equal source_project.description, copied_project.description
499 552 assert_equal source_project.enabled_modules, copied_project.enabled_modules
500 553 assert_equal source_project.trackers, copied_project.trackers
501 554
502 555 # Default attributes
503 556 assert_equal 1, copied_project.status
504 557 end
505 558
506 559 def test_activities_should_use_the_system_activities
507 560 project = Project.find(1)
508 561 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
509 562 end
510 563
511 564
512 565 def test_activities_should_use_the_project_specific_activities
513 566 project = Project.find(1)
514 567 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
515 568 assert overridden_activity.save!
516 569
517 570 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
518 571 end
519 572
520 573 def test_activities_should_not_include_the_inactive_project_specific_activities
521 574 project = Project.find(1)
522 575 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
523 576 assert overridden_activity.save!
524 577
525 578 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
526 579 end
527 580
528 581 def test_activities_should_not_include_project_specific_activities_from_other_projects
529 582 project = Project.find(1)
530 583 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
531 584 assert overridden_activity.save!
532 585
533 586 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
534 587 end
535 588
536 589 def test_activities_should_handle_nils
537 590 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
538 591 TimeEntryActivity.delete_all
539 592
540 593 # No activities
541 594 project = Project.find(1)
542 595 assert project.activities.empty?
543 596
544 597 # No system, one overridden
545 598 assert overridden_activity.save!
546 599 project.reload
547 600 assert_equal [overridden_activity], project.activities
548 601 end
549 602
550 603 def test_activities_should_override_system_activities_with_project_activities
551 604 project = Project.find(1)
552 605 parent_activity = TimeEntryActivity.find(:first)
553 606 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
554 607 assert overridden_activity.save!
555 608
556 609 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
557 610 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
558 611 end
559 612
560 613 def test_activities_should_include_inactive_activities_if_specified
561 614 project = Project.find(1)
562 615 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
563 616 assert overridden_activity.save!
564 617
565 618 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
566 619 end
567 620
568 621 test 'activities should not include active System activities if the project has an override that is inactive' do
569 622 project = Project.find(1)
570 623 system_activity = TimeEntryActivity.find_by_name('Design')
571 624 assert system_activity.active?
572 625 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
573 626 assert overridden_activity.save!
574 627
575 628 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
576 629 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
577 630 end
578 631
579 632 def test_close_completed_versions
580 633 Version.update_all("status = 'open'")
581 634 project = Project.find(1)
582 635 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
583 636 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
584 637 project.close_completed_versions
585 638 project.reload
586 639 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
587 640 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
588 641 end
589 642
590 643 context "Project#copy" do
591 644 setup do
592 645 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
593 646 Project.destroy_all :identifier => "copy-test"
594 647 @source_project = Project.find(2)
595 648 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
596 649 @project.trackers = @source_project.trackers
597 650 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
598 651 end
599 652
600 653 should "copy issues" do
601 654 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
602 655 :subject => "copy issue status",
603 656 :tracker_id => 1,
604 657 :assigned_to_id => 2,
605 658 :project_id => @source_project.id)
606 659 assert @project.valid?
607 660 assert @project.issues.empty?
608 661 assert @project.copy(@source_project)
609 662
610 663 assert_equal @source_project.issues.size, @project.issues.size
611 664 @project.issues.each do |issue|
612 665 assert issue.valid?
613 666 assert ! issue.assigned_to.blank?
614 667 assert_equal @project, issue.project
615 668 end
616 669
617 670 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
618 671 assert copied_issue
619 672 assert copied_issue.status
620 673 assert_equal "Closed", copied_issue.status.name
621 674 end
622 675
623 676 should "change the new issues to use the copied version" do
624 677 User.current = User.find(1)
625 678 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
626 679 @source_project.versions << assigned_version
627 680 assert_equal 3, @source_project.versions.size
628 681 Issue.generate_for_project!(@source_project,
629 682 :fixed_version_id => assigned_version.id,
630 683 :subject => "change the new issues to use the copied version",
631 684 :tracker_id => 1,
632 685 :project_id => @source_project.id)
633 686
634 687 assert @project.copy(@source_project)
635 688 @project.reload
636 689 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
637 690
638 691 assert copied_issue
639 692 assert copied_issue.fixed_version
640 693 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
641 694 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
642 695 end
643 696
644 697 should "copy issue relations" do
645 698 Setting.cross_project_issue_relations = '1'
646 699
647 700 second_issue = Issue.generate!(:status_id => 5,
648 701 :subject => "copy issue relation",
649 702 :tracker_id => 1,
650 703 :assigned_to_id => 2,
651 704 :project_id => @source_project.id)
652 705 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
653 706 :issue_to => second_issue,
654 707 :relation_type => "relates")
655 708 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
656 709 :issue_to => second_issue,
657 710 :relation_type => "duplicates")
658 711
659 712 assert @project.copy(@source_project)
660 713 assert_equal @source_project.issues.count, @project.issues.count
661 714 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
662 715 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
663 716
664 717 # First issue with a relation on project
665 718 assert_equal 1, copied_issue.relations.size, "Relation not copied"
666 719 copied_relation = copied_issue.relations.first
667 720 assert_equal "relates", copied_relation.relation_type
668 721 assert_equal copied_second_issue.id, copied_relation.issue_to_id
669 722 assert_not_equal source_relation.id, copied_relation.id
670 723
671 724 # Second issue with a cross project relation
672 725 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
673 726 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
674 727 assert_equal "duplicates", copied_relation.relation_type
675 728 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
676 729 assert_not_equal source_relation_cross_project.id, copied_relation.id
677 730 end
678 731
679 732 should "copy memberships" do
680 733 assert @project.valid?
681 734 assert @project.members.empty?
682 735 assert @project.copy(@source_project)
683 736
684 737 assert_equal @source_project.memberships.size, @project.memberships.size
685 738 @project.memberships.each do |membership|
686 739 assert membership
687 740 assert_equal @project, membership.project
688 741 end
689 742 end
690 743
691 744 should "copy project specific queries" do
692 745 assert @project.valid?
693 746 assert @project.queries.empty?
694 747 assert @project.copy(@source_project)
695 748
696 749 assert_equal @source_project.queries.size, @project.queries.size
697 750 @project.queries.each do |query|
698 751 assert query
699 752 assert_equal @project, query.project
700 753 end
701 754 end
702 755
703 756 should "copy versions" do
704 757 @source_project.versions << Version.generate!
705 758 @source_project.versions << Version.generate!
706 759
707 760 assert @project.versions.empty?
708 761 assert @project.copy(@source_project)
709 762
710 763 assert_equal @source_project.versions.size, @project.versions.size
711 764 @project.versions.each do |version|
712 765 assert version
713 766 assert_equal @project, version.project
714 767 end
715 768 end
716 769
717 770 should "copy wiki" do
718 771 assert_difference 'Wiki.count' do
719 772 assert @project.copy(@source_project)
720 773 end
721 774
722 775 assert @project.wiki
723 776 assert_not_equal @source_project.wiki, @project.wiki
724 777 assert_equal "Start page", @project.wiki.start_page
725 778 end
726 779
727 780 should "copy wiki pages and content with hierarchy" do
728 781 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
729 782 assert @project.copy(@source_project)
730 783 end
731 784
732 785 assert @project.wiki
733 786 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
734 787
735 788 @project.wiki.pages.each do |wiki_page|
736 789 assert wiki_page.content
737 790 assert !@source_project.wiki.pages.include?(wiki_page)
738 791 end
739 792
740 793 parent = @project.wiki.find_page('Parent_page')
741 794 child1 = @project.wiki.find_page('Child_page_1')
742 795 child2 = @project.wiki.find_page('Child_page_2')
743 796 assert_equal parent, child1.parent
744 797 assert_equal parent, child2.parent
745 798 end
746 799
747 800 should "copy issue categories" do
748 801 assert @project.copy(@source_project)
749 802
750 803 assert_equal 2, @project.issue_categories.size
751 804 @project.issue_categories.each do |issue_category|
752 805 assert !@source_project.issue_categories.include?(issue_category)
753 806 end
754 807 end
755 808
756 809 should "copy boards" do
757 810 assert @project.copy(@source_project)
758 811
759 812 assert_equal 1, @project.boards.size
760 813 @project.boards.each do |board|
761 814 assert !@source_project.boards.include?(board)
762 815 end
763 816 end
764 817
765 818 should "change the new issues to use the copied issue categories" do
766 819 issue = Issue.find(4)
767 820 issue.update_attribute(:category_id, 3)
768 821
769 822 assert @project.copy(@source_project)
770 823
771 824 @project.issues.each do |issue|
772 825 assert issue.category
773 826 assert_equal "Stock management", issue.category.name # Same name
774 827 assert_not_equal IssueCategory.find(3), issue.category # Different record
775 828 end
776 829 end
777 830
778 831 should "limit copy with :only option" do
779 832 assert @project.members.empty?
780 833 assert @project.issue_categories.empty?
781 834 assert @source_project.issues.any?
782 835
783 836 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
784 837
785 838 assert @project.members.any?
786 839 assert @project.issue_categories.any?
787 840 assert @project.issues.empty?
788 841 end
789 842
790 843 end
791 844
792 845 end
General Comments 0
You need to be logged in to leave comments. Login now