]> source.dussan.org Git - redmine.git/commitdiff
Added the ability to copy a project in the Project Administration panel.
authorEric Davis <edavis@littlestreamsoftware.com>
Sun, 3 May 2009 21:25:37 +0000 (21:25 +0000)
committerEric Davis <edavis@littlestreamsoftware.com>
Sun, 3 May 2009 21:25:37 +0000 (21:25 +0000)
* Added Copy project button.
* Added Project#copy_from to duplicate a project to be modified and saved by the user
* Added a ProjectsController#copy based off the add method
** Used Project#copy_from to create a duplicate project in memory
* Implemented Project#copy to copy data for a project from another and save it.
** Members
** Project level queries
** Project custom fields
* Added a plugin hook for Project#copy.

  #1125  #1556  #886  #309

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2704 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/controllers/projects_controller.rb
app/models/project.rb
app/views/admin/projects.rhtml
app/views/projects/copy.rhtml [new file with mode: 0644]
config/locales/en.yml
test/fixtures/issues.yml
test/fixtures/queries.yml
test/functional/projects_controller_test.rb
test/unit/project_test.rb

index f9c537cfcf13d3736ef28afcd92b9d9fd75ed3b2..b663291de990737d5c34c210d627cb53ebbfd8e2 100644 (file)
@@ -23,10 +23,10 @@ class ProjectsController < ApplicationController
   menu_item :settings, :only => :settings
   menu_item :issues, :only => [:changelog]
   
-  before_filter :find_project, :except => [ :index, :list, :add, :activity ]
+  before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
   before_filter :find_optional_project, :only => :activity
-  before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
-  before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
+  before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
+  before_filter :require_admin, :only => [ :add, :copy, :archive, :unarchive, :destroy ]
   accept_key_auth :activity
   
   after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
@@ -80,6 +80,30 @@ class ProjectsController < ApplicationController
          end           
     end        
   end
+  
+  def copy
+    @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
+    @trackers = Tracker.all
+    @root_projects = Project.find(:all,
+                                  :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
+                                  :order => 'name')
+    if request.get?
+      @project = Project.copy_from(params[:id])
+      if @project
+        @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
+      else
+        redirect_to :controller => 'admin', :action => 'projects'
+      end  
+    else
+      @project = Project.new(params[:project])
+      @project.enabled_module_names = params[:enabled_modules]
+      if @project.copy(params[:id])
+        flash[:notice] = l(:notice_successful_create)
+        redirect_to :controller => 'admin', :action => 'projects'
+      end              
+    end        
+  end
+
        
   # Show @project
   def show
index bddd66362c6e5ec956143c2fd6f09b19d1db8f2d..261844e8ebc3de2190bff2fd9124a6508286fc55 100644 (file)
@@ -318,6 +318,66 @@ class Project < ActiveRecord::Base
     p.nil? ? nil : p.identifier.to_s.succ
   end
 
+  # Copies and saves the Project instance based on the +project+.
+  # Will duplicate the source project's:
+  # * Issues
+  # * Members
+  # * Queries
+  def copy(project)
+    project = project.is_a?(Project) ? project : Project.find(project)
+
+    Project.transaction do
+      # Issues
+      project.issues.each do |issue|
+        new_issue = Issue.new
+        new_issue.copy_from(issue)
+        self.issues << new_issue
+      end
+    
+      # Members
+      project.members.each do |member|
+        new_member = Member.new
+        new_member.attributes = member.attributes.dup.except("project_id")
+        new_member.project = self
+        self.members << new_member
+      end
+      
+      # Queries
+      project.queries.each do |query|
+        new_query = Query.new
+        new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
+        new_query.sort_criteria = query.sort_criteria if query.sort_criteria
+        new_query.project = self
+        self.queries << new_query
+      end
+
+      Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
+      self.save
+    end
+  end
+
+  
+  # Copies +project+ and returns the new instance.  This will not save
+  # the copy
+  def self.copy_from(project)
+    begin
+      project = project.is_a?(Project) ? project : Project.find(project)
+      if project
+        # clear unique attributes
+        attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
+        copy = Project.new(attributes)
+        copy.enabled_modules = project.enabled_modules
+        copy.trackers = project.trackers
+        copy.custom_values = project.custom_values.collect {|v| v.clone}
+        return copy
+      else
+        return nil
+      end
+    rescue ActiveRecord::RecordNotFound
+      return nil
+    end
+  end
+  
 protected
   def validate
     errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/)
index 40177a63b3c29ec62de72fb0b3aa5161ca679b95..1acef091fb0363dbad223cf2fe5670a6d91a5136 100644 (file)
@@ -23,6 +23,7 @@
        <th><%=l(:field_created_on)%></th>
     <th></th>
     <th></th>
+    <th></th>
   </tr></thead>
   <tbody>
 <% for project in @projects %>
@@ -37,6 +38,9 @@
     <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
     </small>
   </td>
+  <td align="center" style="width:10%">
+    <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
+  </td>
   <td align="center" style="width:10%">
     <small><%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %></small>
   </td>
diff --git a/app/views/projects/copy.rhtml b/app/views/projects/copy.rhtml
new file mode 100644 (file)
index 0000000..a5c4e14
--- /dev/null
@@ -0,0 +1,16 @@
+<h2><%=l(:label_project_copy)%></h2>
+
+<% labelled_tabular_form_for :project, @project, :url => { :action => "copy" } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+
+<fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
+<% Redmine::AccessControl.available_project_modules.each do |m| %>
+    <label class="floating">
+    <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
+    <%= l_or_humanize(m, :prefix => "project_module_") %>
+    </label>
+<% end %>
+</fieldset>
+
+<%= submit_tag l(:button_copy) %>
+<% end %>
index cdc505a53b8ce222650c2991ecf85a49df56fcb8..f3d81516734f1b58f86048fedd11f4c98302fde9 100644 (file)
@@ -351,6 +351,7 @@ en:
   label_user_new: New user
   label_project: Project
   label_project_new: New project
+  label_project_copy: Copy project
   label_project_plural: Projects
   label_x_projects:
     zero:  no projects
index 921ba40c44f12a25882a8a8df5c3f6a698646fb0..856f8028902683990acd750319dffa5a9dcfa6c9 100644 (file)
@@ -58,7 +58,7 @@ issues_004:
   category_id: \r
   description: Issue on project 2\r
   tracker_id: 1\r
-  assigned_to_id: \r
+  assigned_to_id: 2\r
   author_id: 2\r
   status_id: 1\r
 issues_005: \r
@@ -125,4 +125,4 @@ issues_008:
   start_date: \r
   due_date: \r
   lock_version: 0\r
-  
\ No newline at end of file
+
index a1bb08eff512dd729148af8973bdd74003810736..563bf583aa031ae9e6afae2725475f17d833c077 100644 (file)
@@ -106,4 +106,32 @@ queries_006:
     --- \r
     - - priority\r
       - desc\r
-  
\ No newline at end of file
+queries_007: \r
+  id: 7\r
+  project_id: 2\r
+  is_public: true\r
+  name: Public query for project 2\r
+  filters: |\r
+    --- \r
+    tracker_id: \r
+      :values: \r
+      - "3"\r
+      :operator: "="\r
+\r
+  user_id: 2\r
+  column_names: \r
+queries_008: \r
+  id: 8\r
+  project_id: 2\r
+  is_public: false\r
+  name: Private query for project 2\r
+  filters: |\r
+    --- \r
+    tracker_id: \r
+      :values: \r
+      - "3"\r
+      :operator: "="\r
+\r
+  user_id: 2\r
+  column_names: \r
+
index 1aded6429bd80b239c639dbbf2ca579e22ecf4af..4393ac07513cdd3efcc1aa4e4985a3052c93206d 100644 (file)
@@ -453,7 +453,6 @@ class ProjectsControllerTest < Test::Unit::TestCase
     6.times do |i|
       p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
       p.set_parent!(parent)
-      
       get :show, :id => p
       assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
                       :children => { :count => [i, 3].min,
@@ -462,7 +461,24 @@ class ProjectsControllerTest < Test::Unit::TestCase
       parent = p
     end
   end
-  
+
+  def test_copy_with_project
+    @request.session[:user_id] = 1 # admin
+    get :copy, :id => 1
+    assert_response :success
+    assert_template 'copy'
+    assert assigns(:project)
+    assert_equal Project.find(1).description, assigns(:project).description
+    assert_nil assigns(:project).id
+  end
+
+  def test_copy_without_project
+    @request.session[:user_id] = 1 # admin
+    get :copy
+    assert_response :redirect
+    assert_redirected_to :controller => 'admin', :action => 'projects'
+  end
+
   def test_jump_should_redirect_to_active_tab
     get :show, :id => 1, :jump => 'issues'
     assert_redirected_to 'projects/ecookbook/issues'
index f579e14ffbc85b96513a5eca70e2071ca66723bb..f9a17e2ec57d29f0a45f573de129710cd0ad3c23 100644 (file)
@@ -20,7 +20,8 @@ require File.dirname(__FILE__) + '/../test_helper'
 class ProjectTest < Test::Unit::TestCase\r
   fixtures :projects, :enabled_modules, \r
            :issues, :issue_statuses, :journals, :journal_details,\r
-           :users, :members, :roles, :projects_trackers, :trackers, :boards\r
+           :users, :members, :roles, :projects_trackers, :trackers, :boards,\r
+           :queries
 \r
   def setup\r
     @ecookbook = Project.find(1)\r
@@ -221,6 +222,7 @@ class ProjectTest < Test::Unit::TestCase
     assert_nil Project.next_identifier\r
   end\r
   \r
+
   def test_enabled_module_names_should_not_recreate_enabled_modules\r
     project = Project.find(1)\r
     # Remove one module\r
@@ -233,4 +235,86 @@ class ProjectTest < Test::Unit::TestCase
     # Ids should be preserved\r
     assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort\r
   end\r
+
+  def test_copy_from_existing_project\r
+    source_project = Project.find(1)\r
+    copied_project = Project.copy_from(1)\r
+\r
+    assert copied_project\r
+    # Cleared attributes\r
+    assert copied_project.id.blank?\r
+    assert copied_project.name.blank?\r
+    assert copied_project.identifier.blank?\r
+    \r
+    # Duplicated attributes\r
+    assert_equal source_project.description, copied_project.description\r
+    assert_equal source_project.enabled_modules, copied_project.enabled_modules\r
+    assert_equal source_project.trackers, copied_project.trackers\r
+\r
+    # Default attributes\r
+    assert_equal 1, copied_project.status\r
+  end\r
+  \r
+  # Context: Project#copy\r
+  def test_copy_should_copy_issues\r
+    # Setup\r
+    ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests\r
+    source_project = Project.find(2)\r
+    Project.destroy_all :identifier => "copy-test"\r
+    project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')\r
+    project.trackers = source_project.trackers\r
+    assert project.valid?\r
+    \r
+    assert project.issues.empty?\r
+    assert project.copy(source_project)\r
+\r
+    # Tests\r
+    assert_equal source_project.issues.size, project.issues.size\r
+    project.issues.each do |issue|\r
+      assert issue.valid?\r
+      assert ! issue.assigned_to.blank?\r
+      assert_equal project, issue.project\r
+    end\r
+  end\r
+  \r
+  def test_copy_should_copy_members\r
+    # Setup\r
+    ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests\r
+    source_project = Project.find(2)\r
+    project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')\r
+    project.trackers = source_project.trackers\r
+    project.enabled_modules = source_project.enabled_modules\r
+    assert project.valid?\r
+\r
+    assert project.members.empty?\r
+    assert project.copy(source_project)\r
+\r
+    # Tests\r
+    assert_equal source_project.members.size, project.members.size\r
+    project.members.each do |member|\r
+      assert member\r
+      assert_equal project, member.project\r
+    end\r
+  end\r
+\r
+  def test_copy_should_copy_project_level_queries\r
+    # Setup\r
+    ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests\r
+    source_project = Project.find(2)\r
+    project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')\r
+    project.trackers = source_project.trackers\r
+    project.enabled_modules = source_project.enabled_modules\r
+    assert project.valid?\r
+\r
+    assert project.queries.empty?\r
+    assert project.copy(source_project)\r
+\r
+    # Tests\r
+    assert_equal source_project.queries.size, project.queries.size\r
+    project.queries.each do |query|\r
+      assert query\r
+      assert_equal project, query.project\r
+    end\r
+  end\r
+\r
 end\r