@@ -0,0 +1,16 | |||||
|
1 | <h2><%=l(:label_project_copy)%></h2> | |||
|
2 | ||||
|
3 | <% labelled_tabular_form_for :project, @project, :url => { :action => "copy" } do |f| %> | |||
|
4 | <%= render :partial => 'form', :locals => { :f => f } %> | |||
|
5 | ||||
|
6 | <fieldset class="box"><legend><%= l(:label_module_plural) %></legend> | |||
|
7 | <% Redmine::AccessControl.available_project_modules.each do |m| %> | |||
|
8 | <label class="floating"> | |||
|
9 | <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %> | |||
|
10 | <%= l_or_humanize(m, :prefix => "project_module_") %> | |||
|
11 | </label> | |||
|
12 | <% end %> | |||
|
13 | </fieldset> | |||
|
14 | ||||
|
15 | <%= submit_tag l(:button_copy) %> | |||
|
16 | <% end %> |
@@ -23,10 +23,10 class ProjectsController < ApplicationController | |||||
23 | menu_item :settings, :only => :settings |
|
23 | menu_item :settings, :only => :settings | |
24 | menu_item :issues, :only => [:changelog] |
|
24 | menu_item :issues, :only => [:changelog] | |
25 |
|
25 | |||
26 | before_filter :find_project, :except => [ :index, :list, :add, :activity ] |
|
26 | before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ] | |
27 | before_filter :find_optional_project, :only => :activity |
|
27 | before_filter :find_optional_project, :only => :activity | |
28 | before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ] |
|
28 | before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ] | |
29 | before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ] |
|
29 | before_filter :require_admin, :only => [ :add, :copy, :archive, :unarchive, :destroy ] | |
30 | accept_key_auth :activity |
|
30 | accept_key_auth :activity | |
31 |
|
31 | |||
32 | after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller| |
|
32 | after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller| | |
@@ -80,6 +80,30 class ProjectsController < ApplicationController | |||||
80 | end |
|
80 | end | |
81 | end |
|
81 | end | |
82 | end |
|
82 | end | |
|
83 | ||||
|
84 | def copy | |||
|
85 | @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") | |||
|
86 | @trackers = Tracker.all | |||
|
87 | @root_projects = Project.find(:all, | |||
|
88 | :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", | |||
|
89 | :order => 'name') | |||
|
90 | if request.get? | |||
|
91 | @project = Project.copy_from(params[:id]) | |||
|
92 | if @project | |||
|
93 | @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? | |||
|
94 | else | |||
|
95 | redirect_to :controller => 'admin', :action => 'projects' | |||
|
96 | end | |||
|
97 | else | |||
|
98 | @project = Project.new(params[:project]) | |||
|
99 | @project.enabled_module_names = params[:enabled_modules] | |||
|
100 | if @project.copy(params[:id]) | |||
|
101 | flash[:notice] = l(:notice_successful_create) | |||
|
102 | redirect_to :controller => 'admin', :action => 'projects' | |||
|
103 | end | |||
|
104 | end | |||
|
105 | end | |||
|
106 | ||||
83 |
|
107 | |||
84 | # Show @project |
|
108 | # Show @project | |
85 | def show |
|
109 | def show |
@@ -318,6 +318,66 class Project < ActiveRecord::Base | |||||
318 | p.nil? ? nil : p.identifier.to_s.succ |
|
318 | p.nil? ? nil : p.identifier.to_s.succ | |
319 | end |
|
319 | end | |
320 |
|
320 | |||
|
321 | # Copies and saves the Project instance based on the +project+. | |||
|
322 | # Will duplicate the source project's: | |||
|
323 | # * Issues | |||
|
324 | # * Members | |||
|
325 | # * Queries | |||
|
326 | def copy(project) | |||
|
327 | project = project.is_a?(Project) ? project : Project.find(project) | |||
|
328 | ||||
|
329 | Project.transaction do | |||
|
330 | # Issues | |||
|
331 | project.issues.each do |issue| | |||
|
332 | new_issue = Issue.new | |||
|
333 | new_issue.copy_from(issue) | |||
|
334 | self.issues << new_issue | |||
|
335 | end | |||
|
336 | ||||
|
337 | # Members | |||
|
338 | project.members.each do |member| | |||
|
339 | new_member = Member.new | |||
|
340 | new_member.attributes = member.attributes.dup.except("project_id") | |||
|
341 | new_member.project = self | |||
|
342 | self.members << new_member | |||
|
343 | end | |||
|
344 | ||||
|
345 | # Queries | |||
|
346 | project.queries.each do |query| | |||
|
347 | new_query = Query.new | |||
|
348 | new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria") | |||
|
349 | new_query.sort_criteria = query.sort_criteria if query.sort_criteria | |||
|
350 | new_query.project = self | |||
|
351 | self.queries << new_query | |||
|
352 | end | |||
|
353 | ||||
|
354 | Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self) | |||
|
355 | self.save | |||
|
356 | end | |||
|
357 | end | |||
|
358 | ||||
|
359 | ||||
|
360 | # Copies +project+ and returns the new instance. This will not save | |||
|
361 | # the copy | |||
|
362 | def self.copy_from(project) | |||
|
363 | begin | |||
|
364 | project = project.is_a?(Project) ? project : Project.find(project) | |||
|
365 | if project | |||
|
366 | # clear unique attributes | |||
|
367 | attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status') | |||
|
368 | copy = Project.new(attributes) | |||
|
369 | copy.enabled_modules = project.enabled_modules | |||
|
370 | copy.trackers = project.trackers | |||
|
371 | copy.custom_values = project.custom_values.collect {|v| v.clone} | |||
|
372 | return copy | |||
|
373 | else | |||
|
374 | return nil | |||
|
375 | end | |||
|
376 | rescue ActiveRecord::RecordNotFound | |||
|
377 | return nil | |||
|
378 | end | |||
|
379 | end | |||
|
380 | ||||
321 | protected |
|
381 | protected | |
322 | def validate |
|
382 | def validate | |
323 | errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/) |
|
383 | errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/) |
@@ -23,6 +23,7 | |||||
23 | <th><%=l(:field_created_on)%></th> |
|
23 | <th><%=l(:field_created_on)%></th> | |
24 | <th></th> |
|
24 | <th></th> | |
25 | <th></th> |
|
25 | <th></th> | |
|
26 | <th></th> | |||
26 | </tr></thead> |
|
27 | </tr></thead> | |
27 | <tbody> |
|
28 | <tbody> | |
28 | <% for project in @projects %> |
|
29 | <% for project in @projects %> | |
@@ -38,6 +39,9 | |||||
38 | </small> |
|
39 | </small> | |
39 | </td> |
|
40 | </td> | |
40 | <td align="center" style="width:10%"> |
|
41 | <td align="center" style="width:10%"> | |
|
42 | <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %> | |||
|
43 | </td> | |||
|
44 | <td align="center" style="width:10%"> | |||
41 | <small><%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %></small> |
|
45 | <small><%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %></small> | |
42 | </td> |
|
46 | </td> | |
43 | </tr> |
|
47 | </tr> |
@@ -351,6 +351,7 en: | |||||
351 | label_user_new: New user |
|
351 | label_user_new: New user | |
352 | label_project: Project |
|
352 | label_project: Project | |
353 | label_project_new: New project |
|
353 | label_project_new: New project | |
|
354 | label_project_copy: Copy project | |||
354 | label_project_plural: Projects |
|
355 | label_project_plural: Projects | |
355 | label_x_projects: |
|
356 | label_x_projects: | |
356 | zero: no projects |
|
357 | zero: no projects |
@@ -58,7 +58,7 issues_004: | |||||
58 | category_id: |
|
58 | category_id: | |
59 | description: Issue on project 2 |
|
59 | description: Issue on project 2 | |
60 | tracker_id: 1 |
|
60 | tracker_id: 1 | |
61 | assigned_to_id: |
|
61 | assigned_to_id: 2 | |
62 | author_id: 2 |
|
62 | author_id: 2 | |
63 | status_id: 1 |
|
63 | status_id: 1 | |
64 | issues_005: |
|
64 | issues_005: | |
@@ -125,4 +125,4 issues_008: | |||||
125 | start_date: |
|
125 | start_date: | |
126 | due_date: |
|
126 | due_date: | |
127 | lock_version: 0 |
|
127 | lock_version: 0 | |
128 | No newline at end of file |
|
128 |
@@ -106,4 +106,32 queries_006: | |||||
106 | --- |
|
106 | --- | |
107 | - - priority |
|
107 | - - priority | |
108 | - desc |
|
108 | - desc | |
109 | No newline at end of file |
|
109 | queries_007: | |
|
110 | id: 7 | |||
|
111 | project_id: 2 | |||
|
112 | is_public: true | |||
|
113 | name: Public query for project 2 | |||
|
114 | filters: | | |||
|
115 | --- | |||
|
116 | tracker_id: | |||
|
117 | :values: | |||
|
118 | - "3" | |||
|
119 | :operator: "=" | |||
|
120 | ||||
|
121 | user_id: 2 | |||
|
122 | column_names: | |||
|
123 | queries_008: | |||
|
124 | id: 8 | |||
|
125 | project_id: 2 | |||
|
126 | is_public: false | |||
|
127 | name: Private query for project 2 | |||
|
128 | filters: | | |||
|
129 | --- | |||
|
130 | tracker_id: | |||
|
131 | :values: | |||
|
132 | - "3" | |||
|
133 | :operator: "=" | |||
|
134 | ||||
|
135 | user_id: 2 | |||
|
136 | column_names: | |||
|
137 |
@@ -453,7 +453,6 class ProjectsControllerTest < Test::Unit::TestCase | |||||
453 | 6.times do |i| |
|
453 | 6.times do |i| | |
454 | p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}") |
|
454 | p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}") | |
455 | p.set_parent!(parent) |
|
455 | p.set_parent!(parent) | |
456 |
|
||||
457 | get :show, :id => p |
|
456 | get :show, :id => p | |
458 | assert_tag :h1, :parent => { :attributes => {:id => 'header'}}, |
|
457 | assert_tag :h1, :parent => { :attributes => {:id => 'header'}}, | |
459 | :children => { :count => [i, 3].min, |
|
458 | :children => { :count => [i, 3].min, | |
@@ -462,7 +461,24 class ProjectsControllerTest < Test::Unit::TestCase | |||||
462 | parent = p |
|
461 | parent = p | |
463 | end |
|
462 | end | |
464 | end |
|
463 | end | |
465 |
|
464 | |||
|
465 | def test_copy_with_project | |||
|
466 | @request.session[:user_id] = 1 # admin | |||
|
467 | get :copy, :id => 1 | |||
|
468 | assert_response :success | |||
|
469 | assert_template 'copy' | |||
|
470 | assert assigns(:project) | |||
|
471 | assert_equal Project.find(1).description, assigns(:project).description | |||
|
472 | assert_nil assigns(:project).id | |||
|
473 | end | |||
|
474 | ||||
|
475 | def test_copy_without_project | |||
|
476 | @request.session[:user_id] = 1 # admin | |||
|
477 | get :copy | |||
|
478 | assert_response :redirect | |||
|
479 | assert_redirected_to :controller => 'admin', :action => 'projects' | |||
|
480 | end | |||
|
481 | ||||
466 | def test_jump_should_redirect_to_active_tab |
|
482 | def test_jump_should_redirect_to_active_tab | |
467 | get :show, :id => 1, :jump => 'issues' |
|
483 | get :show, :id => 1, :jump => 'issues' | |
468 | assert_redirected_to 'projects/ecookbook/issues' |
|
484 | assert_redirected_to 'projects/ecookbook/issues' |
@@ -20,7 +20,8 require File.dirname(__FILE__) + '/../test_helper' | |||||
20 | class ProjectTest < Test::Unit::TestCase |
|
20 | class ProjectTest < Test::Unit::TestCase | |
21 | fixtures :projects, :enabled_modules, |
|
21 | fixtures :projects, :enabled_modules, | |
22 | :issues, :issue_statuses, :journals, :journal_details, |
|
22 | :issues, :issue_statuses, :journals, :journal_details, | |
23 | :users, :members, :roles, :projects_trackers, :trackers, :boards |
|
23 | :users, :members, :roles, :projects_trackers, :trackers, :boards, | |
|
24 | :queries | |||
24 |
|
25 | |||
25 | def setup |
|
26 | def setup | |
26 | @ecookbook = Project.find(1) |
|
27 | @ecookbook = Project.find(1) | |
@@ -221,6 +222,7 class ProjectTest < Test::Unit::TestCase | |||||
221 | assert_nil Project.next_identifier |
|
222 | assert_nil Project.next_identifier | |
222 | end |
|
223 | end | |
223 |
|
224 | |||
|
225 | ||||
224 | def test_enabled_module_names_should_not_recreate_enabled_modules |
|
226 | def test_enabled_module_names_should_not_recreate_enabled_modules | |
225 | project = Project.find(1) |
|
227 | project = Project.find(1) | |
226 | # Remove one module |
|
228 | # Remove one module | |
@@ -233,4 +235,86 class ProjectTest < Test::Unit::TestCase | |||||
233 | # Ids should be preserved |
|
235 | # Ids should be preserved | |
234 | assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort |
|
236 | assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort | |
235 | end |
|
237 | end | |
|
238 | ||||
|
239 | def test_copy_from_existing_project | |||
|
240 | source_project = Project.find(1) | |||
|
241 | copied_project = Project.copy_from(1) | |||
|
242 | ||||
|
243 | assert copied_project | |||
|
244 | # Cleared attributes | |||
|
245 | assert copied_project.id.blank? | |||
|
246 | assert copied_project.name.blank? | |||
|
247 | assert copied_project.identifier.blank? | |||
|
248 | ||||
|
249 | # Duplicated attributes | |||
|
250 | assert_equal source_project.description, copied_project.description | |||
|
251 | assert_equal source_project.enabled_modules, copied_project.enabled_modules | |||
|
252 | assert_equal source_project.trackers, copied_project.trackers | |||
|
253 | ||||
|
254 | # Default attributes | |||
|
255 | assert_equal 1, copied_project.status | |||
|
256 | end | |||
|
257 | ||||
|
258 | # Context: Project#copy | |||
|
259 | def test_copy_should_copy_issues | |||
|
260 | # Setup | |||
|
261 | ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests | |||
|
262 | source_project = Project.find(2) | |||
|
263 | Project.destroy_all :identifier => "copy-test" | |||
|
264 | project = Project.new(:name => 'Copy Test', :identifier => 'copy-test') | |||
|
265 | project.trackers = source_project.trackers | |||
|
266 | assert project.valid? | |||
|
267 | ||||
|
268 | assert project.issues.empty? | |||
|
269 | assert project.copy(source_project) | |||
|
270 | ||||
|
271 | # Tests | |||
|
272 | assert_equal source_project.issues.size, project.issues.size | |||
|
273 | project.issues.each do |issue| | |||
|
274 | assert issue.valid? | |||
|
275 | assert ! issue.assigned_to.blank? | |||
|
276 | assert_equal project, issue.project | |||
|
277 | end | |||
|
278 | end | |||
|
279 | ||||
|
280 | def test_copy_should_copy_members | |||
|
281 | # Setup | |||
|
282 | ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests | |||
|
283 | source_project = Project.find(2) | |||
|
284 | project = Project.new(:name => 'Copy Test', :identifier => 'copy-test') | |||
|
285 | project.trackers = source_project.trackers | |||
|
286 | project.enabled_modules = source_project.enabled_modules | |||
|
287 | assert project.valid? | |||
|
288 | ||||
|
289 | assert project.members.empty? | |||
|
290 | assert project.copy(source_project) | |||
|
291 | ||||
|
292 | # Tests | |||
|
293 | assert_equal source_project.members.size, project.members.size | |||
|
294 | project.members.each do |member| | |||
|
295 | assert member | |||
|
296 | assert_equal project, member.project | |||
|
297 | end | |||
|
298 | end | |||
|
299 | ||||
|
300 | def test_copy_should_copy_project_level_queries | |||
|
301 | # Setup | |||
|
302 | ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests | |||
|
303 | source_project = Project.find(2) | |||
|
304 | project = Project.new(:name => 'Copy Test', :identifier => 'copy-test') | |||
|
305 | project.trackers = source_project.trackers | |||
|
306 | project.enabled_modules = source_project.enabled_modules | |||
|
307 | assert project.valid? | |||
|
308 | ||||
|
309 | assert project.queries.empty? | |||
|
310 | assert project.copy(source_project) | |||
|
311 | ||||
|
312 | # Tests | |||
|
313 | assert_equal source_project.queries.size, project.queries.size | |||
|
314 | project.queries.each do |query| | |||
|
315 | assert query | |||
|
316 | assert_equal project, query.project | |||
|
317 | end | |||
|
318 | end | |||
|
319 | ||||
236 | end |
|
320 | end |
General Comments 0
You need to be logged in to leave comments.
Login now