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