Skip to content

Commit

Permalink
[58105] Added update_item and delete_branch methods to HierarchicalIt…
Browse files Browse the repository at this point in the history
…emService
  • Loading branch information
Andreas Pfohl committed Oct 11, 2024
1 parent 787a249 commit f2c75a2
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 18 deletions.
61 changes: 61 additions & 0 deletions app/contracts/custom_fields/hierarchy/update_item_contract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module CustomFields
module Hierarchy
class UpdateItemContract < Dry::Validation::Contract
params do
required(:item).filled
optional(:label).filled(:string)
optional(:short).filled(:string)
end

rule(:item) do
if value.is_a?(CustomField::Hierarchy::Item)
if !value.persisted?
key.failure("Item must exist")
elsif value.parent.nil?
key.failure("Item must not be a root item")
end
else
key.failure("Item must be of type 'Item'")
end
end

rule(:label) do
next unless key?

if CustomField::Hierarchy::Item.exists?(parent_id: values[:item].parent, label: value)
key.failure("Label must be unique within the same hierarchy level")
end
end
end
end
end
2 changes: 1 addition & 1 deletion app/models/custom_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class CustomField < ApplicationRecord

has_one :hierarchy_root,
class_name: "CustomField::Hierarchy::Item",
dependent: :delete, # todo: cascade into children with service
dependent: :destroy,
inverse_of: "custom_field"

acts_as_list scope: [:type]
Expand Down
2 changes: 1 addition & 1 deletion app/models/custom_field/hierarchy/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ class CustomField::Hierarchy::Item < ApplicationRecord
self.table_name = "hierarchical_items"

belongs_to :custom_field
has_closure_tree order: "sort_order", numeric_order: true
has_closure_tree order: "sort_order", numeric_order: true, dependent: :delete_all
end
28 changes: 24 additions & 4 deletions app/services/custom_fields/hierarchy/hierarchical_item_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,22 @@ def insert_item(parent:, label:, short: nil)
.new
.call({ parent:, label:, short: }.compact)
.to_monad
.bind { |validation| create_child_item(validation) }
.bind { |validation| create_child_item(validation:) }
end

def update_item(item:, label: nil, short: nil)
CustomFields::Hierarchy::UpdateItemContract
.new
.call({ item:, label:, short: }.compact)
.to_monad
.bind { |attributes| update_item_attributes(item:, attributes:) }
end

def delete_branch(item:)
return Failure(:item_is_root) if item.root?

# CustomField::Hierarchy::Item sets "dependent: :destroy"
item.destroy ? Success() : Failure(item.errors)
end

private
Expand All @@ -67,13 +82,18 @@ def create_root_item
Success(item)
end

def create_child_item(validation)
item = CustomField::Hierarchy::Item
.create(parent: validation[:parent], label: validation[:label], short: validation[:short])
def create_child_item(validation:)
item = validation[:parent].children.create(label: validation[:label], short: validation[:short])
return Failure(item.errors) unless item.persisted?

Success(item)
end

def update_item_attributes(item:, attributes:)
item.update(label: attributes[:label], short: attributes[:short])

item.errors.empty? ? Success(item) : Failure(item.errors)
end
end
end
end
115 changes: 115 additions & 0 deletions spec/contracts/custom_fields/hierarchy/update_item_contract_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require "rails_helper"

RSpec.describe CustomFields::Hierarchy::UpdateItemContract do
subject { described_class.new }

# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
describe "#call" do
let(:vader) { create(:hierarchy_item) }
let(:luke) { create(:hierarchy_item, label: "luke", short: "ls", parent: vader) }
let(:leia) { create(:hierarchy_item, label: "leia", short: "lo", parent: vader) }

before do
luke
leia
end

context "when all required fields are valid" do
it "is valid" do
[
{ item: luke, label: "Luke Skywalker", short: "LS" },
{ item: luke, label: "Luke Skywalker" },
{ item: luke, short: "LS" },
{ item: luke, short: "lo" },
{ item: luke }
].each { |params| expect(subject.call(params)).to be_success }
end
end

context "when item is a root item" do
let(:params) { { item: vader } }

it("is invalid") do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(item: ["Item must not be a root item"])
end
end

context "when item is not of type 'Item'" do
let(:invalid_item) { create(:custom_field) }
let(:params) { { item: invalid_item } }

it("is invalid") do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(item: ["Item must be of type 'Item'"])
end
end

context "when item is not persisted" do
let(:item) { build(:hierarchy_item) }
let(:params) { { item: } }

it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(item: ["Item must exist"])
end
end

context "when the label already exist in the same hierarchy level" do
let(:params) { { item: luke, label: "leia" } }

it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(label: ["Label must be unique within the same hierarchy level"])
end
end

context "when fields are invalid" do
it "is invalid" do
[
{},
{ item: nil },
{ item: luke, label: nil },
{ item: luke, label: 42 },
{ item: luke, short: nil },
{ item: luke, short: 42 }
].each { |params| expect(subject.call(params)).to be_failure }
end
end
end
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
end
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,20 @@
# See COPYRIGHT and LICENSE files for more details.
#++

require "dry/monads/all"

require "rails_helper"

RSpec.describe CustomFields::Hierarchy::HierarchicalItemService do
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
let(:invalid_custom_field) { create(:custom_field, field_format: "text", hierarchy_root: nil) }

subject { described_class.new(custom_field) }

describe "#initialize" do
context "with valid custom field" do
it "initializes successfully" do
expect { described_class.new(custom_field) }.not_to raise_error
expect { subject }.not_to raise_error
end
end

Expand All @@ -49,11 +53,9 @@
end

describe "#generate_root" do
let(:service) { described_class.new(custom_field) }

context "with valid hierarchy root" do
it "creates a root item successfully" do
expect(service.generate_root).to be_success
expect(subject.generate_root).to be_success
end
end

Expand All @@ -63,39 +65,99 @@
.to receive(:create)
.and_return(instance_double(CustomField::Hierarchy::Item, persisted?: false, errors: "some errors"))

result = service.generate_root
result = subject.generate_root
expect(result).to be_failure
end
end
end

describe "#insert_item" do
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: parent) }
let(:service) { described_class.new(custom_field) }

let(:parent) { create(:hierarchy_item) }
let(:label) { "Child Item" }
let(:short) { "Short Description" }

context "with valid parameters" do
it "inserts an item successfully without short" do
result = service.insert_item(parent:, label:)
result = subject.insert_item(parent:, label:)
expect(result).to be_success
end

it "inserts an item successfully with short" do
result = service.insert_item(parent:, label:, short:)
result = subject.insert_item(parent:, label:, short:)
expect(result).to be_success
end
end

context "with invalid item" do
it "fails to insert an item" do
allow(CustomField::Hierarchy::Item)
.to receive(:create).and_return(instance_double(CustomField::Hierarchy::Item,
persisted?: false, errors: "some errors"))
# rubocop:disable RSpec/VerifiedDoubles
children = double(create: instance_double(CustomField::Hierarchy::Item, persisted?: false, errors: "some errors"))
# rubocop:enable RSpec/VerifiedDoubles

allow(parent).to receive(:children).and_return(children)

result = subject.insert_item(parent:, label:, short:)
expect(result).to be_failure
end
end
end

describe "#update_item" do
let(:items) do
Dry::Monads::Do.() do
root = Dry::Monads::Do.bind subject.generate_root
luke = Dry::Monads::Do.bind subject.insert_item(parent: root, label: "luke")
leia = Dry::Monads::Do.bind subject.insert_item(parent: root, label: "leia")

Dry::Monads::Success({ root:, luke:, leia: })
end
end

context "with valid parameters" do
it "updates the item with new attributes" do
result = subject.update_item(item: items.value![:luke], label: "Luke Skywalker", short: "LS")
expect(result).to be_success
end
end

context "with invalid parameters" do
it "refuses to update the item with new attributes" do
result = subject.update_item(item: items.value![:luke], label: "leia", short: "LS")
expect(result).to be_failure
end
end
end

describe "#delete_branch" do
let(:items) do
Dry::Monads::Do.() do
root = Dry::Monads::Do.bind subject.generate_root
luke = Dry::Monads::Do.bind subject.insert_item(parent: root, label: "luke")
leia = Dry::Monads::Do.bind subject.insert_item(parent: luke, label: "leia")

Dry::Monads::Success({ root:, luke:, leia: })
end
end

before do
items
end

context "with valid item to destroy" do
it "deletes the entire branch" do
result = subject.delete_branch(item: items.value![:luke])
expect(result).to be_success
expect(items.value![:luke]).to be_frozen
expect(CustomField::Hierarchy::Item.all).to be_one
expect(items.value![:root].reload.children).to be_empty
end
end

result = service.insert_item(parent:, label:, short:)
context "with root item" do
it "refuses to delete the item" do
result = subject.delete_branch(item: items.value![:root])
expect(result).to be_failure
end
end
Expand Down

0 comments on commit f2c75a2

Please sign in to comment.