From d9e1f4f13bd50486bdd8a56f107aeaaecfaffaa8 Mon Sep 17 00:00:00 2001 From: loiswells97 Date: Fri, 4 Mar 2022 15:42:06 +0000 Subject: [PATCH 01/16] Create draft PR for #27 From d1b73ba955502f074f270bfd2b2010d310f7f94b Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Fri, 4 Mar 2022 15:43:42 +0000 Subject: [PATCH 02/16] Allow saving of new files --- app/controllers/api/projects/phrases_controller.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/projects/phrases_controller.rb b/app/controllers/api/projects/phrases_controller.rb index d92373ad8..1939506df 100644 --- a/app/controllers/api/projects/phrases_controller.rb +++ b/app/controllers/api/projects/phrases_controller.rb @@ -17,8 +17,13 @@ def update components = project_params[:components] components.each do |comp_params| - component = Component.find(comp_params[:id]) - component.update(comp_params) + if !comp_params[:id].nil? + component = Component.find(comp_params[:id]) + component.update(comp_params) + else + @project.components << Component.new(comp_params) + @project.save + end end head :ok else From 1a761b8ac36a7ccbfe3eb817beff7475ef928022 Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Fri, 4 Mar 2022 15:46:27 +0000 Subject: [PATCH 03/16] Provisionally deal with case where empty file is saved --- app/controllers/api/projects/phrases_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/projects/phrases_controller.rb b/app/controllers/api/projects/phrases_controller.rb index 1939506df..2dbe66b1f 100644 --- a/app/controllers/api/projects/phrases_controller.rb +++ b/app/controllers/api/projects/phrases_controller.rb @@ -20,9 +20,11 @@ def update if !comp_params[:id].nil? component = Component.find(comp_params[:id]) component.update(comp_params) - else + elsif !comp_params[:content].nil? @project.components << Component.new(comp_params) @project.save + else + next end end head :ok From df532fa202d2855179746f23004bbadf87a5c301 Mon Sep 17 00:00:00 2001 From: Lois Wells Date: Fri, 4 Mar 2022 16:08:07 +0000 Subject: [PATCH 04/16] Refactoring to please rubocop --- .../api/projects/phrases_controller.rb | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/controllers/api/projects/phrases_controller.rb b/app/controllers/api/projects/phrases_controller.rb index 2dbe66b1f..aaaa88eae 100644 --- a/app/controllers/api/projects/phrases_controller.rb +++ b/app/controllers/api/projects/phrases_controller.rb @@ -17,15 +17,7 @@ def update components = project_params[:components] components.each do |comp_params| - if !comp_params[:id].nil? - component = Component.find(comp_params[:id]) - component.update(comp_params) - elsif !comp_params[:content].nil? - @project.components << Component.new(comp_params) - @project.save - else - next - end + update_component(comp_params) end head :ok else @@ -38,6 +30,16 @@ def update def project_params params.require(:project).permit(:identifier, :type, components: %i[id name extension content]) end + + def update_component(comp_params) + if !comp_params[:id].nil? + component = Component.find(comp_params[:id]) + component.update(comp_params) + elsif !comp_params[:content].nil? + @project.components << Component.new(comp_params) + @project.save + end + end end end end From ce88a04ab45527014f8468ad8549f6b4aa4c82fb Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Mon, 7 Mar 2022 15:04:55 +0000 Subject: [PATCH 05/16] Make component content nullable Add validations for component Use cancancan load_and_authorize_resource in projects controller. --- app/controllers/api/projects_controller.rb | 9 ++++++--- app/models/ability.rb | 2 ++ app/models/component.rb | 3 +++ app/views/api/projects/show.json.jbuilder | 2 +- .../20220307144811_allow_nil_content_on_components.rb | 9 +++++++++ db/schema.rb | 4 ++-- spec/factories/component.rb | 2 +- spec/models/component_spec.rb | 5 +++++ 8 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20220307144811_allow_nil_content_on_components.rb diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index f773d8b66..15b4ecbdc 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -3,16 +3,15 @@ module Api class ProjectsController < ApiController require 'phrase_identifier' - before_action :require_oauth_user, only: %i[update] + before_action :load_project + load_and_authorize_resource def show - @project = Project.find_by!(identifier: params[:id]) render :show, formats: [:json] end def update - @project = Project.find_by!(identifier: params[:id]) authorize! :update, @project components = project_params[:components] @@ -25,6 +24,10 @@ def update private + def load_project + @project = Project.find_by!(identifier: params[:id]) + end + def project_params params.require(:project) .permit(:identifier, diff --git a/app/models/ability.rb b/app/models/ability.rb index 438d12901..fa724636c 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -4,6 +4,8 @@ class Ability include CanCan::Ability def initialize(user) + can :read, Project + return if user.blank? can :update, Project, user_id: user diff --git a/app/models/component.rb b/app/models/component.rb index f8b53364f..555294023 100644 --- a/app/models/component.rb +++ b/app/models/component.rb @@ -2,4 +2,7 @@ class Component < ApplicationRecord belongs_to :project + validates :name, presence: true + validates :extension, presence: true + validates :index, uniqueness: { scope: :project_id } end diff --git a/app/views/api/projects/show.json.jbuilder b/app/views/api/projects/show.json.jbuilder index a3929b9af..da085ad97 100644 --- a/app/views/api/projects/show.json.jbuilder +++ b/app/views/api/projects/show.json.jbuilder @@ -4,4 +4,4 @@ json.call(@project, :identifier, :project_type, :name, :user_id) json.parent(@project.parent, :name, :identifier) if @project.parent -json.components @project.components, :id, :name, :extension, :content +json.components @project.components, :id, :name, :extension, :content, :index diff --git a/db/migrate/20220307144811_allow_nil_content_on_components.rb b/db/migrate/20220307144811_allow_nil_content_on_components.rb new file mode 100644 index 000000000..bab5a858e --- /dev/null +++ b/db/migrate/20220307144811_allow_nil_content_on_components.rb @@ -0,0 +1,9 @@ +class AllowNilContentOnComponents < ActiveRecord::Migration[7.0] + def up + change_column :components, :content, :string, null: true + end + + def down + change_column :components, :content, :string, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 7ebc5b8a1..d20b67cf0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_02_28_094815) do +ActiveRecord::Schema.define(version: 2022_03_07_144811) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -20,7 +20,7 @@ t.uuid "project_id" t.string "name", null: false t.string "extension", null: false - t.text "content", null: false + t.string "content" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.integer "index" diff --git a/spec/factories/component.rb b/spec/factories/component.rb index 74c43dba1..7ffaf0833 100644 --- a/spec/factories/component.rb +++ b/spec/factories/component.rb @@ -4,6 +4,6 @@ factory :component do name { Faker::Lorem.word } extension { 'py' } - content { Faker::Lorem.paragraph(sentence_count: 2) } + sequence (:index) { |n| n } end end diff --git a/spec/models/component_spec.rb b/spec/models/component_spec.rb index b041ddad8..94802cd27 100644 --- a/spec/models/component_spec.rb +++ b/spec/models/component_spec.rb @@ -3,5 +3,10 @@ require 'rails_helper' RSpec.describe Component, type: :model do + subject { build(:component) } it { is_expected.to belong_to(:project) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:extension) } + it { is_expected.to validate_uniqueness_of(:index).scoped_to(:project_id) } + end From 8fab82495f3caaee18ede597dd1605550a1065bc Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Mon, 7 Mar 2022 15:09:04 +0000 Subject: [PATCH 06/16] Tidy up --- spec/factories/component.rb | 2 +- spec/models/component_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/factories/component.rb b/spec/factories/component.rb index 7ffaf0833..b881816fd 100644 --- a/spec/factories/component.rb +++ b/spec/factories/component.rb @@ -4,6 +4,6 @@ factory :component do name { Faker::Lorem.word } extension { 'py' } - sequence (:index) { |n| n } + sequence(:index) { |n| n } end end diff --git a/spec/models/component_spec.rb b/spec/models/component_spec.rb index 94802cd27..b3959644d 100644 --- a/spec/models/component_spec.rb +++ b/spec/models/component_spec.rb @@ -4,9 +4,9 @@ RSpec.describe Component, type: :model do subject { build(:component) } + it { is_expected.to belong_to(:project) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:extension) } it { is_expected.to validate_uniqueness_of(:index).scoped_to(:project_id) } - end From 9dd023d1a6ee86ccb049c1fc46328a3ea78036e1 Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Tue, 8 Mar 2022 09:20:05 +0000 Subject: [PATCH 07/16] Amend routes --- app/controllers/api/projects_controller.rb | 2 -- config/routes.rb | 5 ----- 2 files changed, 7 deletions(-) diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index 15b4ecbdc..b6a70f132 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -12,8 +12,6 @@ def show end def update - authorize! :update, @project - components = project_params[:components] components.each do |comp_params| component = Component.find(comp_params[:id]) diff --git a/config/routes.rb b/config/routes.rb index 0b07b8978..aa0d8afc9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true Rails.application.routes.draw do - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - - # Defines the root path route ("/") - # root "articles#index" - namespace :api do resource :default_project, only: %i[show create] do get '/html', to: 'default_projects#html' From 6db8f98e6b345f0b322ae8c952307b6b137649ba Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Tue, 8 Mar 2022 17:03:51 +0000 Subject: [PATCH 08/16] Initial work on project update operation --- app/concepts/project/operation/update.rb | 51 +++++++++ app/controllers/api/projects_controller.rb | 6 +- app/models/component.rb | 2 +- app/models/project.rb | 1 + ...7144811_allow_nil_content_on_components.rb | 2 +- ...08115005_add_default_flag_to_components.rb | 5 + db/schema.rb | 3 +- .../hello_world_example/project_config.yml | 5 +- .../hello_world_starter/project_config.yml | 5 +- .../project_config.yml | 3 +- .../project_config.yml | 3 +- lib/tasks/projects.rake | 3 +- .../update_default_component_spec.rb | 47 ++++++++ .../update_delete_components_spec.rb | 41 +++++++ .../concepts/project/operation/update_spec.rb | 106 ++++++++++++++++++ spec/factories/component.rb | 8 ++ spec/factories/project.rb | 14 ++- spec/models/component_spec.rb | 1 + spec/spec_helper.rb | 12 +- 19 files changed, 300 insertions(+), 18 deletions(-) create mode 100644 app/concepts/project/operation/update.rb create mode 100644 db/migrate/20220308115005_add_default_flag_to_components.rb create mode 100644 spec/concepts/project/operation/update_default_component_spec.rb create mode 100644 spec/concepts/project/operation/update_delete_components_spec.rb create mode 100644 spec/concepts/project/operation/update_spec.rb diff --git a/app/concepts/project/operation/update.rb b/app/concepts/project/operation/update.rb new file mode 100644 index 000000000..3042e7e04 --- /dev/null +++ b/app/concepts/project/operation/update.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class Project + module Operation + class Update + require 'operation_response' + + def self.call(params, project) + response = OperationResponse.new + response[:project] = project + + setup_deletions(response, params) + return response if response.failure? + + response[:project].assign_attributes(params.slice(:name)) + params[:components].each do |component_params| + if component_params[:id].present? + component = response[:project].components.select { |c| c.id == component_params[:id] }.first + component.assign_attributes(component_params) + else + response[:project].components.build(component_params) + end + end + + response[:project].save! + response[:project].components.where(id: component_ids_to_delete).destroy_all + + response + end + + class << self + private + + def setup_deletions(response, params) + existing_component_ids = response[:project].components.pluck(:id) + updated_component_ids = params[:components].pluck(:id) + response[:component_ids_to_delete] = existing_component_ids - updated_component_ids + + validate_deletions(response) + end + + def validate_deletions(response) + default_component_id = response[:project].components.find_by(default: true)&.id + return unless response[:component_ids_to_delete].include?(default_component_id) + + response[:error] = 'Can not delete protected component' + end + end + end + end +end diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index b6a70f132..568a202a2 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -12,6 +12,7 @@ def show end def update + result = Project::Operation::Update.(project_params, @project) components = project_params[:components] components.each do |comp_params| component = Component.find(comp_params[:id]) @@ -28,9 +29,8 @@ def load_project def project_params params.require(:project) - .permit(:identifier, - :type, - components: %i[id name extension content]) + .permit(:name, + components: %i[id name extension content index]) end end end diff --git a/app/models/component.rb b/app/models/component.rb index 555294023..4152cc78f 100644 --- a/app/models/component.rb +++ b/app/models/component.rb @@ -4,5 +4,5 @@ class Component < ApplicationRecord belongs_to :project validates :name, presence: true validates :extension, presence: true - validates :index, uniqueness: { scope: :project_id } + validates :index, presence: true, uniqueness: { scope: :project_id } end diff --git a/app/models/project.rb b/app/models/project.rb index 8830427c6..094d3c7d5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -8,6 +8,7 @@ class Project < ApplicationRecord belongs_to :parent, class_name: 'Project', foreign_key: 'remixed_from_id', optional: true, inverse_of: :children has_many :components, -> { order(:index) }, dependent: :destroy, inverse_of: :project has_many :children, class_name: 'Project', foreign_key: 'remixed_from_id', dependent: :nullify, inverse_of: :parent + accepts_nested_attributes_for :components private diff --git a/db/migrate/20220307144811_allow_nil_content_on_components.rb b/db/migrate/20220307144811_allow_nil_content_on_components.rb index bab5a858e..b1a233764 100644 --- a/db/migrate/20220307144811_allow_nil_content_on_components.rb +++ b/db/migrate/20220307144811_allow_nil_content_on_components.rb @@ -4,6 +4,6 @@ def up end def down - change_column :components, :content, :string, null: false + raise ActiveRecord::IrreversibleMigration end end diff --git a/db/migrate/20220308115005_add_default_flag_to_components.rb b/db/migrate/20220308115005_add_default_flag_to_components.rb new file mode 100644 index 000000000..9a2dce29d --- /dev/null +++ b/db/migrate/20220308115005_add_default_flag_to_components.rb @@ -0,0 +1,5 @@ +class AddDefaultFlagToComponents < ActiveRecord::Migration[7.0] + def change + add_column :components, :default, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index d20b67cf0..1c0c66ecd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_03_07_144811) do +ActiveRecord::Schema.define(version: 2022_03_08_115005) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -24,6 +24,7 @@ t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.integer "index" + t.boolean "default", default: false, null: false t.index ["project_id"], name: "index_components_on_project_id" end diff --git a/lib/tasks/project_components/hello_world_example/project_config.yml b/lib/tasks/project_components/hello_world_example/project_config.yml index 0d29c4267..0fa21cbad 100644 --- a/lib/tasks/project_components/hello_world_example/project_config.yml +++ b/lib/tasks/project_components/hello_world_example/project_config.yml @@ -5,11 +5,14 @@ COMPONENTS: extension: "py" location: "main.py" index: 0 + default: true - name: "emoji" extension: "py" location: "emoji.py" index: 1 + default: false - name: "noemoji" extension: "py" location: "noemoji.py" - index: 2 \ No newline at end of file + index: 2 + default: false diff --git a/lib/tasks/project_components/hello_world_starter/project_config.yml b/lib/tasks/project_components/hello_world_starter/project_config.yml index 8e1bc82ce..ae1bd5a8d 100644 --- a/lib/tasks/project_components/hello_world_starter/project_config.yml +++ b/lib/tasks/project_components/hello_world_starter/project_config.yml @@ -5,11 +5,14 @@ COMPONENTS: extension: "py" location: "main.py" index: 0 + default: true - name: "emoji" extension: "py" location: "emoji.py" index: 1 + default: false - name: "noemoji" extension: "py" location: "noemoji.py" - index: 2 \ No newline at end of file + index: 2 + default: false diff --git a/lib/tasks/project_components/target_practice_example/project_config.yml b/lib/tasks/project_components/target_practice_example/project_config.yml index b221ea73c..3fbd1fd5d 100644 --- a/lib/tasks/project_components/target_practice_example/project_config.yml +++ b/lib/tasks/project_components/target_practice_example/project_config.yml @@ -4,4 +4,5 @@ COMPONENTS: - name: "main" extension: "py" location: "main.py" - index: 0 \ No newline at end of file + index: 0 + default: true diff --git a/lib/tasks/project_components/target_practice_starter/project_config.yml b/lib/tasks/project_components/target_practice_starter/project_config.yml index 12de0e7f3..0ea0f287b 100644 --- a/lib/tasks/project_components/target_practice_starter/project_config.yml +++ b/lib/tasks/project_components/target_practice_starter/project_config.yml @@ -4,4 +4,5 @@ COMPONENTS: - name: "main" extension: "py" location: "main.py" - index: 0 \ No newline at end of file + index: 0 + default: true diff --git a/lib/tasks/projects.rake b/lib/tasks/projects.rake index 0e17816ff..d19c3bf24 100644 --- a/lib/tasks/projects.rake +++ b/lib/tasks/projects.rake @@ -15,7 +15,8 @@ namespace :projects do extension = component['extension'] code = File.read(File.dirname(__FILE__) + "/project_components/#{dir}/#{component['location']}") index = component['index'] - project_component = Component.new(name: name, extension: extension, content: code, index: index) + default = component['default'] + project_component = Component.new(name: name, extension: extension, content: code, index: index, default: default) new_project.components << project_component end new_project.save diff --git a/spec/concepts/project/operation/update_default_component_spec.rb b/spec/concepts/project/operation/update_default_component_spec.rb new file mode 100644 index 000000000..4330eda7c --- /dev/null +++ b/spec/concepts/project/operation/update_default_component_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Project::Operation::Update, type: :unit do + subject(:update) { described_class.call(project_params, project) } + + let!(:project) { create(:project, :with_default_component) } + let(:default_component) { project.components.first } + + let(:default_component_params) do + default_component.attributes.symbolize_keys.slice( + :id, + :name, + :content, + :extension, + :index + ) + end + + describe '.call' do + context 'when default file is removed' do + let(:project_params) do + { + name: 'updated project name', + components: [] + } + end + + it 'returns failure? true' do + expect(update.failure?).to eq(true) + end + + it 'does not delete the default component' do + expect { update }.not_to change(Component, :count) + end + end + + context 'when default file name is changed' do + it 'does not update file name' + end + end + + def component_properties_hash(component) + component.attributes.symbolize_keys.slice(:name, :content, :extension, :index) + end +end diff --git a/spec/concepts/project/operation/update_delete_components_spec.rb b/spec/concepts/project/operation/update_delete_components_spec.rb new file mode 100644 index 000000000..b33ec43d7 --- /dev/null +++ b/spec/concepts/project/operation/update_delete_components_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Project::Operation::Update, type: :unit do + subject(:update) { described_class.call(project_params, project) } + + let!(:project) { create(:project, :with_default_component, :with_components) } + let(:component_to_delete) { project.components.last } + let(:default_component) { project.components.first } + + let(:default_component_params) do + default_component.attributes.symbolize_keys.slice( + :id, + :name, + :content, + :extension, + :index + ) + end + + describe '.call' do + context 'when an existing component has been removed' do + let(:project_params) do + { + name: 'updated project name', + components: [default_component_params] + } + end + + it 'deletes a component' do + expect { update }.to change(Component, :count).by(-1) + end + + it 'deletes the correct component' do + update + expect(Component.find_by(id: component_to_delete.id)).to eq(nil) + end + end + end +end diff --git a/spec/concepts/project/operation/update_spec.rb b/spec/concepts/project/operation/update_spec.rb new file mode 100644 index 000000000..5c10d6585 --- /dev/null +++ b/spec/concepts/project/operation/update_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Project::Operation::Update, type: :unit do + subject(:update) { described_class.call(project_params, project) } + + let!(:project) { create(:project, :with_default_component, :with_components) } + let(:editable_component) { project.components.last } + let(:default_component) { project.components.first } + + let(:default_component_params) do + default_component.attributes.symbolize_keys.slice( + :id, + :name, + :content, + :extension, + :index + ) + end + + let(:edited_component_params) do + { + id: editable_component.id, + name: 'updated component name', + content: 'updated content', + extension: 'py', + index: 5 + } + end + + let(:new_component_params) do + { + name: 'new component', + content: 'new component content', + extension: 'py', + index: 99 + } + end + + describe '.call' do + let(:project_params) do + { + name: 'updated project name', + components: [default_component_params, edited_component_params] + } + end + + it 'returns success? true' do + expect(update.success?).to eq(true) + end + + it 'updates project properties' do + expect { update }.to change(project, :name).to('updated project name') + end + + it 'updates component properties' do + expect { update } + .to change { component_properties_hash(editable_component.reload) } + .to(edited_component_params.slice(:name, :content, :extension, :index)) + end + + context 'when a new component has been added' do + let(:project_params) do + { + name: 'updated project name', + components: [ + default_component_params, + edited_component_params, + new_component_params + ] + } + end + + it 'creates a new component' do + expect { update }.to change(Component, :count).by(1) + end + + it 'creates component with correct properties' do + update + created_component = project.components.find_by(**new_component_params) + expect(created_component).not_to eq(nil) + end + end + + context 'when default file is removed' do + it 'does not delete the component' + end + + context 'when default file name is changed' do + it 'does not update file name' + end + + context 'when updated project is invalid' do + it 'returns failure? true' + it 'does not amend any project properties' + it 'does not amend any component properties' + it 'does not create new components' + it 'does not delete removed components' + end + end + + def component_properties_hash(component) + component.attributes.symbolize_keys.slice(:name, :content, :extension, :index) + end +end diff --git a/spec/factories/component.rb b/spec/factories/component.rb index b881816fd..20b3b7841 100644 --- a/spec/factories/component.rb +++ b/spec/factories/component.rb @@ -5,5 +5,13 @@ name { Faker::Lorem.word } extension { 'py' } sequence(:index) { |n| n } + default { false } + project + + factory :default_python_component do + name { 'main' } + index { 0 } + default { true } + end end end diff --git a/spec/factories/project.rb b/spec/factories/project.rb index 7badbdcb8..293b326eb 100644 --- a/spec/factories/project.rb +++ b/spec/factories/project.rb @@ -8,8 +8,20 @@ project_type { 'python' } trait :with_components do + transient do + component_count { 1 } + end + + after(:create) do |object, evaluator| + object.components << FactoryBot.create_list(:component, + evaluator.component_count, + project: object) + end + end + + trait :with_default_component do after(:create) do |object| - object.components = FactoryBot.create_list(:component, 2, project: object) + object.components << FactoryBot.create(:default_python_component, project: object) end end end diff --git a/spec/models/component_spec.rb b/spec/models/component_spec.rb index b3959644d..099a98957 100644 --- a/spec/models/component_spec.rb +++ b/spec/models/component_spec.rb @@ -8,5 +8,6 @@ it { is_expected.to belong_to(:project) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:extension) } + it { is_expected.to validate_presence_of(:index) } it { is_expected.to validate_uniqueness_of(:index).scoped_to(:project_id) } end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 01f7c9744..5d227ba75 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -70,12 +70,12 @@ # # Many RSpec users commonly either run the entire suite or an individual # # file, and it's useful to allow more verbose output when running an # # individual spec file. - # if config.files_to_run.one? - # # Use the documentation formatter for detailed output, - # # unless a formatter has already been configured - # # (e.g. via a command-line flag). - # config.default_formatter = "doc" - # end + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end # # # Print the 10 slowest examples and example groups at the # # end of the spec run, to help surface which specs are running From 26f7e50bcfea407281a47bc5e13c05de8abddd2c Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Thu, 10 Mar 2022 12:23:25 +0000 Subject: [PATCH 09/16] Update operation working --- app/concepts/project/operation/update.rb | 61 ++++++--- app/controllers/api/projects_controller.rb | 13 +- app/models/component.rb | 10 ++ config/locales/en.yml | 6 + ...20310120419_add_unique_index_components.rb | 5 + db/schema.rb | 3 +- lib/tasks/projects.rake | 3 +- .../update_default_component_spec.rb | 56 ++++++--- .../project/operation/update_invalid_spec.rb | 70 +++++++++++ .../concepts/project/operation/update_spec.rb | 116 +++++++----------- spec/factories/project.rb | 4 +- spec/models/component_spec.rb | 16 +++ 12 files changed, 252 insertions(+), 111 deletions(-) create mode 100644 db/migrate/20220310120419_add_unique_index_components.rb create mode 100644 spec/concepts/project/operation/update_invalid_spec.rb diff --git a/app/concepts/project/operation/update.rb b/app/concepts/project/operation/update.rb index 3042e7e04..6e984a186 100644 --- a/app/concepts/project/operation/update.rb +++ b/app/concepts/project/operation/update.rb @@ -6,24 +6,16 @@ class Update require 'operation_response' def self.call(params, project) - response = OperationResponse.new - response[:project] = project + response = setup_response(project) setup_deletions(response, params) - return response if response.failure? - response[:project].assign_attributes(params.slice(:name)) - params[:components].each do |component_params| - if component_params[:id].present? - component = response[:project].components.select { |c| c.id == component_params[:id] }.first - component.assign_attributes(component_params) - else - response[:project].components.build(component_params) - end - end + update_project_attributes(response, params) + update_component_attributes(response, params) + + return response if response.failure? - response[:project].save! - response[:project].components.where(id: component_ids_to_delete).destroy_all + persist_changes(response) response end @@ -31,6 +23,12 @@ def self.call(params, project) class << self private + def setup_response(project) + response = OperationResponse.new + response[:project] = project + response + end + def setup_deletions(response, params) existing_component_ids = response[:project].components.pluck(:id) updated_component_ids = params[:components].pluck(:id) @@ -41,9 +39,40 @@ def setup_deletions(response, params) def validate_deletions(response) default_component_id = response[:project].components.find_by(default: true)&.id - return unless response[:component_ids_to_delete].include?(default_component_id) + return unless response[:component_ids_to_delete]&.include?(default_component_id) + + response[:error] = I18n.t 'errors.project.editing.delete_default_component' + end - response[:error] = 'Can not delete protected component' + def update_project_attributes(response, params) + return if response[:error] + + response[:project].assign_attributes(params.slice(:name)) + end + + def update_component_attributes(response, params) + return if response[:error] + + params[:components].each do |component_params| + if component_params[:id].present? + component = response[:project].components.select { |c| c.id == component_params[:id] }.first + component.assign_attributes(component_params) + else + response[:project].components.build(component_params) + end + end + end + + def persist_changes(response) + return if response[:error] + + ActiveRecord::Base.transaction do + response[:project].save! + response[:project].components.where(id: response[:component_ids_to_delete]).destroy_all + rescue StandardError + # TODO: log error? + response[:error] = 'Error persisting changes' + end end end end diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index 568a202a2..b56d76104 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -12,13 +12,14 @@ def show end def update - result = Project::Operation::Update.(project_params, @project) - components = project_params[:components] - components.each do |comp_params| - component = Component.find(comp_params[:id]) - component.update(comp_params) + result = Project::Operation::Update.call(project_params, @project) + + if result.success? + # TODO: render the updated project + head :ok + else + render json: { error: result[:error] }, status: :bad_request end - head :ok end private diff --git a/app/models/component.rb b/app/models/component.rb index 4152cc78f..92f4b049c 100644 --- a/app/models/component.rb +++ b/app/models/component.rb @@ -5,4 +5,14 @@ class Component < ApplicationRecord validates :name, presence: true validates :extension, presence: true validates :index, presence: true, uniqueness: { scope: :project_id } + validate :default_component_protected_properties, on: :update + + private + + def default_component_protected_properties + return unless default? + + errors.add(:name, I18n.t('errors.project.editing.change_default_name')) if name_changed? + errors.add(:name, I18n.t('errors.project.editing.change_default_extension')) if extension_changed? + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 8ca56fc74..acd63f926 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -31,3 +31,9 @@ en: hello: "Hello world" + errors: + project: + editing: + delete_default_component: 'Can not delete default file' + change_default_name: 'Can not amend default file name' + change_default_extension: 'Can not amend default file extension' diff --git a/db/migrate/20220310120419_add_unique_index_components.rb b/db/migrate/20220310120419_add_unique_index_components.rb new file mode 100644 index 000000000..78d958ec0 --- /dev/null +++ b/db/migrate/20220310120419_add_unique_index_components.rb @@ -0,0 +1,5 @@ +class AddUniqueIndexComponents < ActiveRecord::Migration[7.0] + def change + add_index :components, [:index, :project_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 1c0c66ecd..8d5dbec3b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_03_08_115005) do +ActiveRecord::Schema.define(version: 2022_03_10_120419) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -25,6 +25,7 @@ t.datetime "updated_at", precision: 6, null: false t.integer "index" t.boolean "default", default: false, null: false + t.index ["index", "project_id"], name: "index_components_on_index_and_project_id", unique: true t.index ["project_id"], name: "index_components_on_project_id" end diff --git a/lib/tasks/projects.rake b/lib/tasks/projects.rake index d19c3bf24..3a5e7d33c 100644 --- a/lib/tasks/projects.rake +++ b/lib/tasks/projects.rake @@ -16,7 +16,8 @@ namespace :projects do code = File.read(File.dirname(__FILE__) + "/project_components/#{dir}/#{component['location']}") index = component['index'] default = component['default'] - project_component = Component.new(name: name, extension: extension, content: code, index: index, default: default) + project_component = Component.new(name: name, extension: extension, content: code, index: index, + default: default) new_project.components << project_component end new_project.save diff --git a/spec/concepts/project/operation/update_default_component_spec.rb b/spec/concepts/project/operation/update_default_component_spec.rb index 4330eda7c..7d888c323 100644 --- a/spec/concepts/project/operation/update_default_component_spec.rb +++ b/spec/concepts/project/operation/update_default_component_spec.rb @@ -8,16 +8,6 @@ let!(:project) { create(:project, :with_default_component) } let(:default_component) { project.components.first } - let(:default_component_params) do - default_component.attributes.symbolize_keys.slice( - :id, - :name, - :content, - :extension, - :index - ) - end - describe '.call' do context 'when default file is removed' do let(:project_params) do @@ -31,17 +21,51 @@ expect(update.failure?).to eq(true) end + it 'returns error message' do + expect(update[:error]).to eq(I18n.t('errors.project.editing.delete_default_component')) + end + it 'does not delete the default component' do expect { update }.not_to change(Component, :count) end - end - context 'when default file name is changed' do - it 'does not update file name' + it 'does not update project' do + expect { update }.not_to change { project.reload.name } + end end - end - def component_properties_hash(component) - component.attributes.symbolize_keys.slice(:name, :content, :extension, :index) + context 'when default file properties are changed' do + let(:default_component_params) do + default_component.attributes.symbolize_keys.slice( + :id, + :name, + :content, + :extension, + :index + ) + end + + let(:project_params) do + { + name: 'updated project name', + components: [default_component_params] + } + end + + it 'does not update file name' do + default_component_params[:name] = 'Updated name' + expect { update }.not_to change { default_component.reload.name } + end + + it 'does not update file extension' do + default_component_params[:extension] = 'txt' + expect { update }.not_to change { default_component.reload.extension } + end + + it 'does not update project' do + default_component_params[:name] = 'Updated name' + expect { update }.not_to change { project.reload.name } + end + end end end diff --git a/spec/concepts/project/operation/update_invalid_spec.rb b/spec/concepts/project/operation/update_invalid_spec.rb new file mode 100644 index 000000000..2725c1fac --- /dev/null +++ b/spec/concepts/project/operation/update_invalid_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Project::Operation::Update, type: :unit do + subject(:update) do + params = { + name: 'updated project name', + components: [default_component_params, edited_component_params, new_component_params] + } + described_class.call(params, project) + end + + let!(:project) { create(:project, :with_default_component, :with_components, component_count: 2) } + let(:editable_component) { project.components.last } + let(:default_component) { project.components.first } + + let(:edited_component_params) do + { + id: editable_component.id, + name: nil, + content: 'updated content', + extension: 'py', + index: 5 + } + end + + describe '.call' do + context 'when updated project component is invalid' do + it 'returns failure? true' do + expect(update.failure?).to eq(true) + end + + it 'does not amend any project properties' do + expect { update }.not_to change { project.reload.name } + end + + it 'does not amend any component properties' do + expect { update }.not_to change { component_properties_hash(editable_component.reload) } + end + + it 'does not create or delete components' do + expect { update }.not_to change(Component, :count) + end + end + end + + def component_properties_hash(component) + component.attributes.symbolize_keys.slice(:name, :content, :extension, :index) + end + + def default_component_params + default_component.attributes.symbolize_keys.slice( + :id, + :name, + :content, + :extension, + :index + ) + end + + def new_component_params + { + name: 'new component', + content: 'new component content', + extension: 'py', + index: 99 + } + end +end diff --git a/spec/concepts/project/operation/update_spec.rb b/spec/concepts/project/operation/update_spec.rb index 5c10d6585..e815ccdd2 100644 --- a/spec/concepts/project/operation/update_spec.rb +++ b/spec/concepts/project/operation/update_spec.rb @@ -3,74 +3,49 @@ require 'rails_helper' RSpec.describe Project::Operation::Update, type: :unit do - subject(:update) { described_class.call(project_params, project) } + subject(:update) do + params = { + name: 'updated project name', + components: component_params + } + described_class.call(params, project) + end let!(:project) { create(:project, :with_default_component, :with_components) } let(:editable_component) { project.components.last } let(:default_component) { project.components.first } - let(:default_component_params) do - default_component.attributes.symbolize_keys.slice( - :id, - :name, - :content, - :extension, - :index - ) - end - - let(:edited_component_params) do - { - id: editable_component.id, - name: 'updated component name', - content: 'updated content', - extension: 'py', - index: 5 - } - end - - let(:new_component_params) do - { - name: 'new component', - content: 'new component content', - extension: 'py', - index: 99 - } - end - describe '.call' do - let(:project_params) do + let(:edited_component_params) do { - name: 'updated project name', - components: [default_component_params, edited_component_params] + id: editable_component.id, + name: 'updated component name', + content: 'updated content', + extension: 'py', + index: 5 } end - it 'returns success? true' do - expect(update.success?).to eq(true) - end + context 'when only amending components' do + let(:component_params) { [default_component_params, edited_component_params] } - it 'updates project properties' do - expect { update }.to change(project, :name).to('updated project name') - end + it 'returns success? true' do + expect(update.success?).to eq(true) + end - it 'updates component properties' do - expect { update } - .to change { component_properties_hash(editable_component.reload) } - .to(edited_component_params.slice(:name, :content, :extension, :index)) + it 'updates project properties' do + expect { update }.to change { project.reload.name }.to('updated project name') + end + + it 'updates component properties' do + expect { update } + .to change { component_properties_hash(editable_component.reload) } + .to(edited_component_params.slice(:name, :content, :extension, :index)) + end end context 'when a new component has been added' do - let(:project_params) do - { - name: 'updated project name', - components: [ - default_component_params, - edited_component_params, - new_component_params - ] - } - end + let(:component_params) { [default_component_params, edited_component_params, new_component_params] } it 'creates a new component' do expect { update }.to change(Component, :count).by(1) @@ -82,25 +57,28 @@ expect(created_component).not_to eq(nil) end end - - context 'when default file is removed' do - it 'does not delete the component' - end - - context 'when default file name is changed' do - it 'does not update file name' - end - - context 'when updated project is invalid' do - it 'returns failure? true' - it 'does not amend any project properties' - it 'does not amend any component properties' - it 'does not create new components' - it 'does not delete removed components' - end end def component_properties_hash(component) component.attributes.symbolize_keys.slice(:name, :content, :extension, :index) end + + def default_component_params + default_component.attributes.symbolize_keys.slice( + :id, + :name, + :content, + :extension, + :index + ) + end + + def new_component_params + { + name: 'new component', + content: 'new component content', + extension: 'py', + index: 99 + } + end end diff --git a/spec/factories/project.rb b/spec/factories/project.rb index 293b326eb..fbcc39fd1 100644 --- a/spec/factories/project.rb +++ b/spec/factories/project.rb @@ -14,8 +14,8 @@ after(:create) do |object, evaluator| object.components << FactoryBot.create_list(:component, - evaluator.component_count, - project: object) + evaluator.component_count, + project: object) end end diff --git a/spec/models/component_spec.rb b/spec/models/component_spec.rb index 099a98957..f9c5a5f84 100644 --- a/spec/models/component_spec.rb +++ b/spec/models/component_spec.rb @@ -10,4 +10,20 @@ it { is_expected.to validate_presence_of(:extension) } it { is_expected.to validate_presence_of(:index) } it { is_expected.to validate_uniqueness_of(:index).scoped_to(:project_id) } + + context 'when default component' do + let(:component) { create(:default_python_component) } + + describe 'validations' do + it 'returns valid? false when name changed' do + component.name = 'updated' + expect(component.valid?).to eq(false) + end + + it 'returns valid? false when extension changed' do + component.extension = 'txt' + expect(component.valid?).to eq(false) + end + end + end end From a561ff78daeb6c8bbc286ee4c6b7d7d0dbd64a51 Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Thu, 10 Mar 2022 13:51:28 +0000 Subject: [PATCH 10/16] Return project json after update --- app/controllers/api/projects_controller.rb | 3 +- spec/request/projects/update_spec.rb | 44 +++++++++++++++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index b56d76104..0de3c02c5 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -15,8 +15,7 @@ def update result = Project::Operation::Update.call(project_params, @project) if result.success? - # TODO: render the updated project - head :ok + render :show, formats: [:json] else render json: { error: result[:error] }, status: :bad_request end diff --git a/spec/request/projects/update_spec.rb b/spec/request/projects/update_spec.rb index 0c97d046e..0c9dfba7f 100644 --- a/spec/request/projects/update_spec.rb +++ b/spec/request/projects/update_spec.rb @@ -1,16 +1,32 @@ # frozen_string_literal: true require 'rails_helper' +require '../../../app/lib/operation_response' RSpec.describe 'Project update requests', type: :request do let(:user_id) { 'e0675b6c-dc48-4cd6-8c04-0f7ac05af51a' } let(:project) { create(:project, user_id: user_id) } context 'when authed user is project creator' do - let(:project) { create(:project, user_id: user_id) } + let(:project) { create(:project, :with_default_component, user_id: user_id) } let!(:component) { create(:component, project: project) } - let(:changes) { { name: 'updated', extension: 'py', content: 'updated content' } } - let(:params) { { project: { components: [{ id: component.id, **changes }] } } } + let(:default_component_params) do + project.components.first.attributes.symbolize_keys.slice( + :id, + :name, + :content, + :extension, + :index + ) + end + + let(:params) do + { project: + { components: [ + default_component_params, + { id: component.id, name: 'updated', extension: 'py', content: 'updated component content' } + ] } } + end before do mock_oauth_user(user_id) @@ -21,10 +37,26 @@ expect(response.status).to eq(200) end - it 'updates component' do + it 'returns updated project json' do put "/api/projects/#{project.identifier}", params: params - updated = component.reload.attributes.symbolize_keys.slice(:name, :content, :extension) - expect(updated).to eq(changes) + expect(response.body).to include('updated component content') + end + + it 'calls update operation' do + mock_response = instance_double(OperationResponse) + allow(mock_response).to receive(:success?).and_return(true) + allow(Project::Operation::Update).to receive(:call).and_return(mock_response) + put "/api/projects/#{project.identifier}", params: params + expect(Project::Operation::Update).to have_received(:call) + end + + context 'when update is invalid' do + let(:params) { { project: { components: [] } } } + + it 'returns error response' do + put "/api/projects/#{project.identifier}", params: params + expect(response.status).to eq(400) + end end end From 01787069d54df9dae7ffad2b5a6613e814cede3c Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Thu, 10 Mar 2022 13:58:25 +0000 Subject: [PATCH 11/16] Remove redundant guard clause --- app/concepts/project/operation/update.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/concepts/project/operation/update.rb b/app/concepts/project/operation/update.rb index 6e984a186..d210d321f 100644 --- a/app/concepts/project/operation/update.rb +++ b/app/concepts/project/operation/update.rb @@ -9,12 +9,8 @@ def self.call(params, project) response = setup_response(project) setup_deletions(response, params) - update_project_attributes(response, params) update_component_attributes(response, params) - - return response if response.failure? - persist_changes(response) response From b6f8b2920ed37f393bff4f373410a5e39f4c808c Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Thu, 10 Mar 2022 14:05:12 +0000 Subject: [PATCH 12/16] Add specs for custom validation errors --- app/models/component.rb | 2 +- spec/models/component_spec.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/models/component.rb b/app/models/component.rb index 92f4b049c..eb8eb2dc8 100644 --- a/app/models/component.rb +++ b/app/models/component.rb @@ -13,6 +13,6 @@ def default_component_protected_properties return unless default? errors.add(:name, I18n.t('errors.project.editing.change_default_name')) if name_changed? - errors.add(:name, I18n.t('errors.project.editing.change_default_extension')) if extension_changed? + errors.add(:extension, I18n.t('errors.project.editing.change_default_extension')) if extension_changed? end end diff --git a/spec/models/component_spec.rb b/spec/models/component_spec.rb index f9c5a5f84..9fd7812d9 100644 --- a/spec/models/component_spec.rb +++ b/spec/models/component_spec.rb @@ -20,10 +20,24 @@ expect(component.valid?).to eq(false) end + it 'sets error message when name changed' do + component.name = 'updated' + component.valid? + expect(component.errors[:name]) + .to include(I18n.t('errors.project.editing.change_default_name')) + end + it 'returns valid? false when extension changed' do component.extension = 'txt' expect(component.valid?).to eq(false) end + + it 'sets error message when extension changed' do + component.extension = 'txt' + component.valid? + expect(component.errors[:extension]) + .to include(I18n.t('errors.project.editing.change_default_extension')) + end end end end From cea2a2d45294d728efdb31dd70194c6be441897a Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Thu, 10 Mar 2022 14:06:51 +0000 Subject: [PATCH 13/16] Tidy up en.yml --- config/locales/en.yml | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index acd63f926..2167ee3d4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,36 +1,4 @@ -# Files in the config/locales directory are used for internationalization -# and are automatically loaded by Rails. If you want to use locales other -# than English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t "hello" -# -# In views, this is aliased to just `t`: -# -# <%= t("hello") %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# The following keys must be escaped otherwise they will not be retrieved by -# the default I18n backend: -# -# true, false, on, off, yes, no -# -# Instead, surround them with single quotes. -# -# en: -# "true": "foo" -# -# To learn more, please read the Rails Internationalization guide -# available at https://guides.rubyonrails.org/i18n.html. - en: - hello: "Hello world" errors: project: editing: From d364806b1821c8ebf4db29daf723bd3633d25d4b Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Thu, 10 Mar 2022 14:12:55 +0000 Subject: [PATCH 14/16] Fix require --- spec/request/projects/update_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/request/projects/update_spec.rb b/spec/request/projects/update_spec.rb index 0c9dfba7f..d54705c9e 100644 --- a/spec/request/projects/update_spec.rb +++ b/spec/request/projects/update_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rails_helper' -require '../../../app/lib/operation_response' +require_relative '../../../app/lib/operation_response' RSpec.describe 'Project update requests', type: :request do let(:user_id) { 'e0675b6c-dc48-4cd6-8c04-0f7ac05af51a' } From 250ddc9dd6b55204e69b449ab159d955832c3d17 Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Thu, 10 Mar 2022 14:20:24 +0000 Subject: [PATCH 15/16] fix require properly --- spec/request/projects/update_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/request/projects/update_spec.rb b/spec/request/projects/update_spec.rb index d54705c9e..dcaaa422f 100644 --- a/spec/request/projects/update_spec.rb +++ b/spec/request/projects/update_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rails_helper' -require_relative '../../../app/lib/operation_response' +require_relative '../../../lib/operation_response' RSpec.describe 'Project update requests', type: :request do let(:user_id) { 'e0675b6c-dc48-4cd6-8c04-0f7ac05af51a' } From e6f2134794f2b20be1ef951ff4a5fdeae577e93c Mon Sep 17 00:00:00 2001 From: Ant Barnes Date: Thu, 10 Mar 2022 16:15:31 +0000 Subject: [PATCH 16/16] Update error messages --- config/locales/en.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 2167ee3d4..d5379a51c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,6 +2,6 @@ en: errors: project: editing: - delete_default_component: 'Can not delete default file' - change_default_name: 'Can not amend default file name' - change_default_extension: 'Can not amend default file extension' + delete_default_component: 'Cannot delete default file' + change_default_name: 'Cannot amend default file name' + change_default_extension: 'Cannot amend default file extension'