Skip to content

Commit

Permalink
Merge pull request #179 from fastruby/feature/advanced-project-cloning
Browse files Browse the repository at this point in the history
Advanced project cloning
  • Loading branch information
lubc authored Jan 19, 2022
2 parents 30e08e2 + 75dc2de commit 432aef3
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 44 deletions.
3 changes: 2 additions & 1 deletion app/assets/stylesheets/3-atoms/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ label {
display: block;
}
input,
textarea {
textarea,
select {
display: block;
border: none;
background: #fff;
Expand Down
22 changes: 14 additions & 8 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ def toggle_archive
@project.toggle_archived!
end

def duplicate
original = Project.includes(stories: :estimates).find(params[:id])
duplicate = original.dup
duplicate.title = "Copy of #{original.title}"
duplicate.save
def new_clone
@original = Project.includes(:projects, stories: :estimates).find(params[:id])
end

original.stories.each { |x| duplicate.stories.create(x.dup.attributes) }
def clone
original = Project.includes(stories: :estimates).find(params[:id])
clone = Project.create(clone_params)
original.clone_stories_into(clone)
original.clone_projects_into(clone) if clone.parent.nil? && original.projects

flash[:success] = "Project created!"
redirect_to "/projects/#{duplicate.id}"
flash[:success] = "Project cloned!"
redirect_to "/projects/#{clone.id}"
end

def create
Expand Down Expand Up @@ -86,4 +88,8 @@ def new_sub_project
def projects_params
params.require(:project).permit(:title, :status, :parent_id)
end

def clone_params
params.require(:project).permit(:title, :parent_id)
end
end
14 changes: 14 additions & 0 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ class Project < ApplicationRecord
belongs_to :parent, class_name: "Project", required: false
has_many :projects, class_name: "Project", foreign_key: :parent_id, dependent: :destroy

scope :active, -> { where.not(status: "archived").or(where(status: nil)) }
scope :parents, -> { where(parent: nil) }

def best_estimate_total
stories.includes(:estimates).sum(&:best_estimate_average)
end
Expand Down Expand Up @@ -53,4 +56,15 @@ def toggle_archived!
def siblings
parent_id ? Project.where(parent_id: parent_id).where.not(id: id) : []
end

def clone_stories_into(clone)
stories.each { |story| clone.stories.create(story.dup.attributes) }
end

def clone_projects_into(clone)
projects.each do |sub_project|
sub_project_clone = clone.projects.create(sub_project.dup.attributes)
sub_project.clone_stories_into(sub_project_clone)
end
end
end
20 changes: 20 additions & 0 deletions app/views/projects/new_clone.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="container">
<h2>Clone project <%= @original.title %></h2>

<%= form_for @original, url: clone_project_path(@original), method: :post do |f| %>
<div class="field">
<%= f.label :title %>
<%= f.text_field :title, placeholder: "Project Title", class: "project-story-title" %>
</div>

<div class="field">
<%= f.label :parent_id %>
<%= f.select :parent_id, options_from_collection_for_select(Project.active.parents, :id, :title, selected: @original.parent_id), include_blank: "None" %>
</div>

<div class="actions">
<%= button_tag "Clone", class: "button", type: "submit" %>
<%= link_to 'Back', project_path(@original.id), id:"back", class: "button" %>
</div>
<% end %>
</div>
2 changes: 1 addition & 1 deletion app/views/projects/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
<% unless @project.parent_id %>
<%= link_to 'Add Sub-Project', project_new_sub_project_path(@project.id), id:"subproject", class: "button magenta" %>
<% end %>
<%= link_to 'Clone Project', duplicate_project_path(@project.id), method: :post, disable_with: 'Cloning...', class: "button magenta" %>
<%= link_to 'Clone Project', new_clone_project_path(@project.id), class: "button magenta" %>
<%= link_to "#{@project.archived? ? 'Unarchive' : 'Archive'} Project", toggle_archive_project_path(@project.id), method: "patch", id: "toggle_archive", class: "button magenta", remote: true %>
<%= link_to "Generate Action Plan", project_action_plan_path(@project.id), class: "button" %>
<% if current_user.admin? %>
Expand Down
10 changes: 7 additions & 3 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
end

resources :projects do
patch :sort, on: :member
patch :toggle_archive, on: :member
member do
patch :sort
patch :toggle_archive
get :new_clone
post :clone
end
\
get :new_sub_project
post :duplicate, on: :member

resource :report do
get "download", to: "reports#download"
Expand Down
55 changes: 41 additions & 14 deletions spec/controllers/projects_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,30 +144,57 @@
end
end

describe "duplicate" do
context "with a project" do
before do
post :duplicate, params: {id: project.id}
end
describe "cloning" do
it "redirects to cloned project" do
expect {
post :clone, params: {id: project.id, project: {title: "New project"}}
}.to change(Project.parents, :count).by(1)

expect(Project.parents.last.title).to eq("New project")
expect(response).to redirect_to "/projects/#{Project.last.id}"
expect(flash[:success]).to eq "Project cloned!"
end

context "with a sub project" do
let!(:sub_project) { FactoryBot.create(:project, parent: project) }

it "clones the sub projects when cloning a parent" do
expect {
post :clone, params: {id: project.id, project: {title: "New title"}}
}.to change(Project.parents, :count).by(1)

it "creates a duplicate project" do
expect(Project.last.title).to eq "Copy of #{project.title}"
last_project = Project.parents.last
expect(last_project.id).not_to eq(project.id)
expect(project.projects.reload.count).to eq(1)
expect(last_project.projects.reload.count).to eq project.projects.count
end

it "redirects to new project" do
expect(response).to redirect_to "/projects/#{Project.last.id}"
it "clones a sub project as a parent project" do
expect {
post :clone, params: {id: sub_project.id, project: {title: "New title", parent_id: nil}}
}.to change(Project, :count).by(1)

last_project = Project.last
expect(last_project.parent).to be_nil
end

it "adds a success message" do
expect(flash[:success]).to be_present
it "clones a sub project in another parent" do
other_project = FactoryBot.create(:project)

expect {
post :clone, params: {id: sub_project.id, project: {title: "New title", parent_id: other_project.id}}
}.to change(other_project.projects.reload, :count).by(1)

last_project = other_project.projects.reload.last
expect(last_project.parent).to eq(other_project)
end
end

context "with stories" do
it "creates a duplicate project with matching stories" do
it "creates a cloned project with matching stories" do
story = project.stories.create({title: "Story 1"})

post :duplicate, params: {id: project.id}
post :clone, params: {id: project.id, project: {title: "New title"}}

expect(Project.last.stories.first.id).not_to eq story.id
expect(Project.last.stories.first.title).to eq story.title
Expand All @@ -177,7 +204,7 @@
story = project.stories.create({title: "Story 1"})
story.estimates.create({best_case_points: 1, worst_case_points: 3})

post :duplicate, params: {id: project.id}
post :clone, params: {id: project.id, project: {title: "New title"}}

expect(Project.last.stories.first.estimates).to be_empty
end
Expand Down
63 changes: 46 additions & 17 deletions spec/features/projects_manage_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@
expect(Project.count).to eq 1
end

it "allows me to clone a project" do
visit project_path(id: project.id)
click_link "Clone Project"
expect(Project.count).to eq 2
expect(Project.last.title).to eq "Copy of #{project.title}"
end

it "allows me to edit a project" do
visit project_path(id: project.id)
click_link "Edit or Delete Project"
Expand Down Expand Up @@ -101,6 +94,42 @@ def adjust_csv_descriptions(csv)
end.to_csv
end

context "cloning", js: true do
it "allows cloning a project" do
visit project_path(id: project.id)

click_link "Clone Project"

expect(page).to have_text("Clone project #{project.title}")

fill_in :project_title, with: "Cloned Project"

expect {
click_button "Clone"
}.to change(Project, :count).by(1)

expect(page).to have_text("Project cloned")

last_project = Project.last
expect(last_project.id).not_to eq(project.id)
expect(last_project.stories.count).to eq project.stories.count

expect(page).to have_text(last_project.title)
end

it "defaults to same parent" do
sub_project = FactoryBot.create(:project, parent: project)

# None if the project is a parent
visit new_clone_project_path(project)
expect(page).to have_select(:project_parent_id, selected: "None")

# The parent if the project is a sub project
visit new_clone_project_path(sub_project)
expect(page).to have_select(:project_parent_id, selected: project.title)
end
end

context "hierarchy sidebar" do
context "with sub projects" do
let!(:sub_project1) { FactoryBot.create(:project, parent: project) }
Expand All @@ -109,23 +138,23 @@ def adjust_csv_descriptions(csv)
it "renders a sidebar" do
visit project_path(project)
within "aside.hierarchy" do
expect(page).to have_selector('a', text: project.title)
expect(page).to have_selector('a', text: sub_project1.title)
expect(page).to have_selector('a', text: sub_project2.title)
expect(page).to have_selector("a", text: project.title)
expect(page).to have_selector("a", text: sub_project1.title)
expect(page).to have_selector("a", text: sub_project2.title)
end

visit project_path(sub_project1)
within "aside.hierarchy" do
expect(page).to have_selector('a', text: project.title)
expect(page).to have_selector('a', text: sub_project1.title)
expect(page).to have_selector('a', text: sub_project2.title)
expect(page).to have_selector("a", text: project.title)
expect(page).to have_selector("a", text: sub_project1.title)
expect(page).to have_selector("a", text: sub_project2.title)
end

visit project_path(sub_project2)
within "aside.hierarchy" do
expect(page).to have_selector('a', text: project.title)
expect(page).to have_selector('a', text: sub_project1.title)
expect(page).to have_selector('a', text: sub_project2.title)
expect(page).to have_selector("a", text: project.title)
expect(page).to have_selector("a", text: sub_project1.title)
expect(page).to have_selector("a", text: sub_project2.title)
end
end
end
Expand All @@ -134,7 +163,7 @@ def adjust_csv_descriptions(csv)
it "renders no sidebar" do
visit project_path(project)

expect(page).not_to have_selector('aside.hierarchy')
expect(page).not_to have_selector("aside.hierarchy")
end
end
end
Expand Down

0 comments on commit 432aef3

Please sign in to comment.