@@ -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 | 23 | menu_item :settings, :only => :settings |
|
24 | 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 | 27 | before_filter :find_optional_project, :only => :activity |
|
28 | before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ] | |
|
29 | before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ] | |
|
28 | before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ] | |
|
29 | before_filter :require_admin, :only => [ :add, :copy, :archive, :unarchive, :destroy ] | |
|
30 | 30 | accept_key_auth :activity |
|
31 | 31 | |
|
32 | 32 | after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller| |
@@ -80,6 +80,30 class ProjectsController < ApplicationController | |||
|
80 | 80 | end |
|
81 | 81 | end |
|
82 | 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 | 108 | # Show @project |
|
85 | 109 | def show |
@@ -318,6 +318,66 class Project < ActiveRecord::Base | |||
|
318 | 318 | p.nil? ? nil : p.identifier.to_s.succ |
|
319 | 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 | 381 | protected |
|
322 | 382 | def validate |
|
323 | 383 | errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/) |
@@ -23,6 +23,7 | |||
|
23 | 23 | <th><%=l(:field_created_on)%></th> |
|
24 | 24 | <th></th> |
|
25 | 25 | <th></th> |
|
26 | <th></th> | |
|
26 | 27 | </tr></thead> |
|
27 | 28 | <tbody> |
|
28 | 29 | <% for project in @projects %> |
@@ -38,6 +39,9 | |||
|
38 | 39 | </small> |
|
39 | 40 | </td> |
|
40 | 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 | 45 | <small><%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %></small> |
|
42 | 46 | </td> |
|
43 | 47 | </tr> |
@@ -351,6 +351,7 en: | |||
|
351 | 351 | label_user_new: New user |
|
352 | 352 | label_project: Project |
|
353 | 353 | label_project_new: New project |
|
354 | label_project_copy: Copy project | |
|
354 | 355 | label_project_plural: Projects |
|
355 | 356 | label_x_projects: |
|
356 | 357 | zero: no projects |
@@ -58,7 +58,7 issues_004: | |||
|
58 | 58 | category_id: |
|
59 | 59 | description: Issue on project 2 |
|
60 | 60 | tracker_id: 1 |
|
61 | assigned_to_id: | |
|
61 | assigned_to_id: 2 | |
|
62 | 62 | author_id: 2 |
|
63 | 63 | status_id: 1 |
|
64 | 64 | issues_005: |
@@ -125,4 +125,4 issues_008: | |||
|
125 | 125 | start_date: |
|
126 | 126 | due_date: |
|
127 | 127 | lock_version: 0 |
|
128 | No newline at end of file | |
|
128 |
@@ -106,4 +106,32 queries_006: | |||
|
106 | 106 | --- |
|
107 | 107 | - - priority |
|
108 | 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 | 453 | 6.times do |i| |
|
454 | 454 | p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}") |
|
455 | 455 | p.set_parent!(parent) |
|
456 | ||
|
457 | 456 | get :show, :id => p |
|
458 | 457 | assert_tag :h1, :parent => { :attributes => {:id => 'header'}}, |
|
459 | 458 | :children => { :count => [i, 3].min, |
@@ -462,7 +461,24 class ProjectsControllerTest < Test::Unit::TestCase | |||
|
462 | 461 | parent = p |
|
463 | 462 | end |
|
464 | 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 | 482 | def test_jump_should_redirect_to_active_tab |
|
467 | 483 | get :show, :id => 1, :jump => 'issues' |
|
468 | 484 | assert_redirected_to 'projects/ecookbook/issues' |
@@ -20,7 +20,8 require File.dirname(__FILE__) + '/../test_helper' | |||
|
20 | 20 | class ProjectTest < Test::Unit::TestCase |
|
21 | 21 | fixtures :projects, :enabled_modules, |
|
22 | 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 | 26 | def setup |
|
26 | 27 | @ecookbook = Project.find(1) |
@@ -221,6 +222,7 class ProjectTest < Test::Unit::TestCase | |||
|
221 | 222 | assert_nil Project.next_identifier |
|
222 | 223 | end |
|
223 | 224 | |
|
225 | ||
|
224 | 226 | def test_enabled_module_names_should_not_recreate_enabled_modules |
|
225 | 227 | project = Project.find(1) |
|
226 | 228 | # Remove one module |
@@ -233,4 +235,86 class ProjectTest < Test::Unit::TestCase | |||
|
233 | 235 | # Ids should be preserved |
|
234 | 236 | assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort |
|
235 | 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 | 320 | end |
General Comments 0
You need to be logged in to leave comments.
Login now