diff --git a/app/blueprinter/bench_blueprint.rb b/app/blueprinter/bench_blueprint.rb index 9ffdd8b..4270b6d 100644 --- a/app/blueprinter/bench_blueprint.rb +++ b/app/blueprinter/bench_blueprint.rb @@ -2,5 +2,5 @@ class BenchBlueprint < Base # Fields - fields :name, :dimensions, :positions + fields :name, :dimensions, :positions, :greenhouse_id end diff --git a/app/blueprinter/request_distribution_blueprint.rb b/app/blueprinter/request_distribution_blueprint.rb index a0a699a..553acaf 100644 --- a/app/blueprinter/request_distribution_blueprint.rb +++ b/app/blueprinter/request_distribution_blueprint.rb @@ -2,7 +2,7 @@ class RequestDistributionBlueprint < Base # Fields - fields :bench_id, :plant_stage_id, :pot_id, :pot_quantity, :area + fields :bench_id, :plant_stage_id, :pot_id, :pot_quantity, :positions_on_bench, :dimensions, :request_id field :greenhouse_id do |request_distribution| request_distribution.bench&.greenhouse_id diff --git a/app/controllers/api/v1/request_distributions_controller.rb b/app/controllers/api/v1/request_distributions_controller.rb index 5808656..aac80e7 100644 --- a/app/controllers/api/v1/request_distributions_controller.rb +++ b/app/controllers/api/v1/request_distributions_controller.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class Api::V1::RequestDistributionsController < ApiController - before_action :set_request, only: %i[index create] + before_action :set_request, only: %i[create] before_action :set_distribution, only: %i[show update destroy] def index - request_distributions = policy_scope(@request.request_distributions) + request_distributions = policy_scope(RequestDistribution.all) authorize request_distributions render json: apply_fetcheable(request_distributions).to_blueprint @@ -19,7 +19,6 @@ def create authorize RequestDistribution distribution = @request.request_distributions.new(distribution_params) - distribution.area = compute_area(distribution) if distribution.save render json: distribution.to_blueprint, status: :created @@ -30,7 +29,6 @@ def create def update @distribution.assign_attributes(distribution_params) - @distribution.area = compute_area(@distribution) if @distribution.save render json: @distribution.to_blueprint @@ -59,17 +57,6 @@ def set_distribution end def distribution_params - params.permit(:bench_id, :plant_stage_id, :pot_id, :pot_quantity) - end - - def compute_area(distribution) - return params[:area] if params[:area].present? - - pot_quantity = distribution.pot_quantity - pot_area = distribution.pot&.area - - return if pot_quantity.nil? || pot_area.nil? - - pot_quantity * pot_area + params.permit(:bench_id, :plant_stage_id, :pot_id, :pot_quantity, positions_on_bench: [], dimensions: []) end end diff --git a/app/models/bench.rb b/app/models/bench.rb index 3e1e9f4..590c54d 100644 --- a/app/models/bench.rb +++ b/app/models/bench.rb @@ -2,23 +2,19 @@ class Bench < ApplicationRecord # Validations - validates :dimensions, - presence: true, - length: { is: 2, message: I18n.t('activerecord.errors.models.bench.attributes.dimensions.incorrect_size') } - validate :dimensions_must_be_strictly_positive + include ValidateDimensionsConcern validates :positions, presence: true, length: { is: 2, message: I18n.t('activerecord.errors.models.bench.attributes.positions.incorrect_size') } validate :positions_must_be_positive - validates_associated :request_distributions, - message: I18n.t( - 'activerecord.errors.models.bench.attributes.request_distributions.invalid_distribution' - ) + validates_associated :request_distributions, if: :dimensions_and_positions_valid? validate :overlapping_bench_exists, on: %i[create update] + validate :distributions_areas_lower_than_bench_area, on: %i[update] + # Associations belongs_to :greenhouse, class_name: 'Greenhouse', @@ -29,12 +25,19 @@ class Bench < ApplicationRecord inverse_of: :bench, dependent: :restrict_with_error # Checks - def dimensions_must_be_strictly_positive - return unless dimensions + def distributions_areas_lower_than_bench_area + return if errors[:dimensions].any? + + width1, height1 = dimensions - return unless dimensions.any? { |d| d <= 0 } + if request_distributions.any? do |request_distribution| + x2, y2 = request_distribution.positions_on_bench + width2, height2 = request_distribution.dimensions - errors.add(:dimensions, 'each dimension must be greater than 0') + width1 < x2 + width2 || height1 < y2 + height2 + end + errors.add(:dimensions, 'sum of distributions areas can\'t be greater than bench area') + end end def positions_must_be_positive @@ -66,6 +69,10 @@ def positions_overlap?(other_bench) x1 < x2 + width2 && x1 + width1 > x2 && y1 < y2 + height2 && y1 + height1 > y2 end + + def dimensions_and_positions_valid? + errors[:dimensions].empty? && errors[:positions].empty? + end end # == Schema Information diff --git a/app/models/concerns/validate_dimensions_concern.rb b/app/models/concerns/validate_dimensions_concern.rb new file mode 100644 index 0000000..601348f --- /dev/null +++ b/app/models/concerns/validate_dimensions_concern.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'active_support/concern' + +module ValidateDimensionsConcern + extend ActiveSupport::Concern + + included do + validates :dimensions, + presence: true, + length: { is: 2, + message: I18n.t('activerecord.errors.concern.attributes.dimensions.incorrect_size') } + validate :dimensions_must_be_strictly_positive + + private + + def dimensions_must_be_strictly_positive + return unless dimensions + + return unless dimensions.any? { |d| d <= 0 } + + errors.add(:dimensions, I18n.t('activerecord.errors.concern.attributes.dimensions.not_positive')) + end + end +end diff --git a/app/models/request_distribution.rb b/app/models/request_distribution.rb index 541e1a9..c045b58 100644 --- a/app/models/request_distribution.rb +++ b/app/models/request_distribution.rb @@ -2,12 +2,22 @@ class RequestDistribution < ApplicationRecord # Validations - validates :area, numericality: { greater_than: 0 } + include ValidateDimensionsConcern - validates :pot_quantity, presence: true, if: -> { pot.present? } + validates :pot_quantity, presence: true, numericality: { greater_than: 0 } - validate :plant_stage_from_request, - :distributions_areas_lower_than_bench_area + validate :plant_stage_from_request + + validates :positions_on_bench, + presence: true, + length: { is: 2, message: I18n.t('activerecord.errors.models.bench.attributes.positions.incorrect_size') } + validate :positions_must_be_positive + + validate :validate_seeds_left_to_plant, on: %i[create update] + + validate :overlapping_distribution_exists, on: %i[create update] + + validate :distribution_within_bench_bounds, on: %i[create update] # Associations belongs_to :request, @@ -24,11 +34,22 @@ class RequestDistribution < ApplicationRecord belongs_to :pot, class_name: 'Pot', - inverse_of: :request_distributions, - optional: true + inverse_of: :request_distributions # Private instance methods + def validate_seeds_left_to_plant + return if errors[:pot_quantity].any? + + quantity_change = pot_quantity - (pot_quantity_was || 0) + + total_pot_quantity = request.request_distributions.where.not(id:).sum(:pot_quantity) + + return if request.quantity >= quantity_change + total_pot_quantity + + errors.add(:pot_quantity, 'not enough seeds left to plant for this request') + end + private def plant_stage_from_request @@ -38,16 +59,47 @@ def plant_stage_from_request errors.add(:plant_stage, 'must be from requested plant') end - def distributions_areas_lower_than_bench_area - return if area.nil? - return if bench&.dimensions.nil? + def positions_must_be_positive + return unless positions_on_bench + + return unless positions_on_bench.any?(&:negative?) + + errors.add(:positions_on_bench, 'each position must be positive') + end + + def overlapping_distribution_exists + return if errors[:dimensions].any? || errors[:positions_on_bench].any? + return unless bench + + if bench.request_distributions.any? do |other_distribution| + next if other_distribution == self + + positions_overlap?(other_distribution) + end + errors.add(:positions_on_bench, 'distribution overlaps with an existing distribution') + end + end + + def positions_overlap?(other_distribution) + x1, y1 = positions_on_bench + width1, height1 = dimensions + x2, y2 = other_distribution.positions_on_bench + width2, height2 = other_distribution.dimensions + + x1 < x2 + width2 && x1 + width1 > x2 && y1 < y2 + height2 && y1 + height1 > y2 + end + + def distribution_within_bench_bounds + return if errors[:dimensions].any? || errors[:positions_on_bench].any? + return unless bench + + bench_dimensions = bench.dimensions + x, y = positions_on_bench + width, height = dimensions - bench_area = bench.dimensions.inject(:*) - sum = bench.request_distributions.sum(&:area) - sum += area if new_record? # area isn't included in previous operation if record isn't persisted - return if sum <= bench_area + return unless x + width > bench_dimensions[0] || y + height > bench_dimensions[1] - errors.add(:bench, 'sum of distributions areas can\'t be greater than bench area') + errors.add(:positions_on_bench, 'distribution exceeds the bounds of the bench') end end @@ -55,15 +107,16 @@ def distributions_areas_lower_than_bench_area # # Table name: request_distributions # -# id :bigint not null, primary key -# request_id :bigint -# bench_id :bigint -# plant_stage_id :bigint -# pot_id :bigint -# pot_quantity :integer -# area :decimal(, ) -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint not null, primary key +# request_id :bigint +# bench_id :bigint +# plant_stage_id :bigint +# pot_id :bigint +# pot_quantity :integer +# created_at :datetime not null +# updated_at :datetime not null +# positions_on_bench :integer is an Array +# dimensions :integer is an Array # # Indexes # diff --git a/config/locales/en.yml b/config/locales/en.yml index 04ad98c..f0c3a00 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -20,12 +20,12 @@ en: at_least_one: should have at least 1 plant_stage bench: attributes: - dimensions: - incorrect_size: "should contain exactly two elements: length and width" - not_positive: "each dimension must be greater than 0" positions: incorrect_size: "should contain exactly two elements: x and y" not_positive: "each position must be positive" - request_distributions: - invalid_distribution: "sum of distributions areas can't be greater than bench area" - positions_overlap: "bench overlaps with an existing bench" \ No newline at end of file + positions_overlap: "bench overlaps with an existing bench" + concern: + attributes: + dimensions: + incorrect_size: "should contain exactly two elements: length and width" + not_positive: "each dimension must be greater than 0" \ No newline at end of file diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 92ffcbc..7090568 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -29,15 +29,15 @@ fr: at_least_one: Au moins 1 request_distribution est requise bench: attributes: - dimensions: - incorrect_size: "doit contenir exactement deux éléments: longueur et largeur" - not_positive: "chaque dimension doit être supérieure à 0" positions: incorrect_size: "doit contenir exactement deux éléments : x et y" not_positive: "chaque position doit être positive" - request_distributions: - invalid_distribution: "la somme des aires de distribution ne peut pas être supérieure à l'aire du banc" positions_overlap: "le banc chevauche un banc existant" + concern: + attributes: + dimensions: + incorrect_size: "doit contenir exactement deux éléments: longueur et largeur" + not_positive: "chaque dimension doit être supérieure à 0" date: abbr_day_names: - dim diff --git a/config/routes.rb b/config/routes.rb index 258e335..72c513f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -30,8 +30,11 @@ post :refuse, on: :member post :cancel, on: :member post :complete, on: :member - resources :request_distributions, only: %i[index show create update destroy] + resources :request_distributions, only: %i[show create update destroy] end + + resources :request_distributions, only: :index + resources :buildings, only: %i[index show create update destroy], shallow: true do resources :greenhouses, only: %i[index show create update destroy], shallow: true do resources :benches, only: %i[index show create update destroy] diff --git a/db/migrate/20241209193343_update_request_distributions.rb b/db/migrate/20241209193343_update_request_distributions.rb new file mode 100644 index 0000000..82d7227 --- /dev/null +++ b/db/migrate/20241209193343_update_request_distributions.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class UpdateRequestDistributions < ActiveRecord::Migration[7.2] + def change + change_table :request_distributions, bulk: true do |t| + t.remove :area, type: :decimal + t.integer :positions_on_bench, array: true + t.integer :dimensions, array: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f8ebb91..fd14fd9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -115,9 +115,10 @@ t.bigint "plant_stage_id" t.bigint "pot_id" t.integer "pot_quantity" - t.decimal "area" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "positions_on_bench", array: true + t.integer "dimensions", array: true t.index ["bench_id"], name: "index_request_distributions_on_bench_id" t.index ["plant_stage_id"], name: "index_request_distributions_on_plant_stage_id" t.index ["pot_id"], name: "index_request_distributions_on_pot_id" diff --git a/db/seeds.rb b/db/seeds.rb index f43b359..95ef205 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -148,8 +148,9 @@ requester_email: 'alice.smith@example.com', laboratory: 'My other lab', name: 'My new request', - plant_name: Faker::Food.vegetables, - plant_stage_name: 'budding', + plant_name: Plant.last.name, + plant_stage_name: Plant.last.plant_stages.last.name, + plant_stage: Plant.last.plant_stages.last, comment: Faker::Movies::LordOfTheRings.quote, due_date: Date.current + 2.months, quantity: 200 @@ -162,7 +163,8 @@ plant_stage: Request.first.plant_stage, pot: Pot.first, pot_quantity: 30, - area: Pot.first.area * 30 + positions_on_bench: [0, 0], + dimensions: [50, 50] ) RequestDistribution.create!( @@ -171,15 +173,19 @@ plant_stage: Request.first.plant_stage, pot: Pot.second, pot_quantity: 20, - area: Pot.second.area * 20 + positions_on_bench: [100, 50], + dimensions: [50, 20] ) Request.first.update!(status: :accepted) RequestDistribution.create!( request: Request.second, - bench: Bench.first, - plant_stage: Plant.second.plant_stages.first, - area: 100 + bench: Bench.second, + plant_stage: Request.second.plant_stage, + pot: Pot.second, + pot_quantity: 20, + positions_on_bench: [0, 10], + dimensions: [20, 30] ) end diff --git a/spec/acceptance/api/v1/benches_controller_spec.rb b/spec/acceptance/api/v1/benches_controller_spec.rb index 074c545..20b7247 100644 --- a/spec/acceptance/api/v1/benches_controller_spec.rb +++ b/spec/acceptance/api/v1/benches_controller_spec.rb @@ -87,6 +87,7 @@ expect(response['name']).to eq(name) expect(response['dimensions']).to eq(dimensions) expect(response['positions']).to eq(positions) + expect(response['greenhouse_id']).to eq(greenhouse_id) end end @@ -98,8 +99,8 @@ items: { type: :integer } let(:name) { 'my square bench' } - let(:dimensions) { [100, 300] } - let(:positions) { [100, 800] } + let(:dimensions) { [300, 300] } + let(:positions) { [1200, 50] } let(:raw_post) { params.to_json } example 'Update a bench' do @@ -114,6 +115,7 @@ expect(bench.name).to eq(name) expect(bench.dimensions).to eq(dimensions) expect(bench.positions).to eq(positions) + expect(bench.greenhouse_id).to eq(greenhouse_id) end end diff --git a/spec/acceptance/api/v1/request_distributions_controller_spec.rb b/spec/acceptance/api/v1/request_distributions_controller_spec.rb index 5148c97..0dddf21 100644 --- a/spec/acceptance/api/v1/request_distributions_controller_spec.rb +++ b/spec/acceptance/api/v1/request_distributions_controller_spec.rb @@ -16,7 +16,7 @@ let!(:distribution) { request.request_distributions.first } let!(:id) { distribution.id } - get '/api/v1/requests/:request_id/request_distributions' do + get '/api/v1/request_distributions' do parameter :'page[number]', "The number of the desired page\n\n" \ "If used, additional information is returned in the response headers:\n" \ @@ -45,8 +45,8 @@ expect(status).to eq(200) - expect(response_body).to eq(request.request_distributions.to_blueprint) - expect(JSON.parse(response_body).count).to eq(request.request_distributions.count) + expect(response_body).to eq(RequestDistribution.all.to_blueprint) + expect(JSON.parse(response_body).count).to eq(RequestDistribution.count) end end @@ -73,16 +73,19 @@ 'If used, previous param `pot_id` is required', with_example: true, type: :integer - parameter :area, - "(Optional) Total area of the experiment\n" \ - 'If used, previous params `pot_id` and `pot_quantity` will be ignored', - with_example: true, - type: :number + parameter :positions_on_bench, 'Positions on Bench', with_example: true, type: :array, items: { type: :object } + parameter :dimensions, 'Dimensions', with_example: true, type: :array, items: { type: :object } let(:bench_id) { Bench.first.id } let(:plant_stage_id) { request.plant_stage.plant.plant_stages.first.id } let(:pot_id) { Pot.first.id } let(:pot_quantity) { 30 } + let(:positions_on_bench) do + [150, 50] + end + let(:dimensions) do + [10, 10] + end let(:raw_post) { params.to_json } @@ -96,12 +99,15 @@ expect(response_body).to eq(RequestDistribution.last.to_blueprint) response = JSON.parse(response_body) + expect(response['bench_id']).to eq(bench_id) expect(response['greenhouse_id']).to eq(Bench.first.greenhouse_id) expect(response['plant_stage_id']).to eq(plant_stage_id) expect(response['pot_id']).to eq(pot_id) expect(response['pot_quantity']).to eq(pot_quantity) - expect(response['area']).not_to be_blank + expect(response['positions_on_bench']).to eq(positions_on_bench) + expect(response['dimensions']).to eq(dimensions) + expect(response['request_id']).to eq(request_id) end end @@ -117,16 +123,19 @@ 'If used, previous param `pot_id` is required', with_example: true, type: :integer - parameter :area, - "(Optional) Total area of the experiment\n" \ - 'If used, previous params `pot_id` and `pot_quantity` will be ignored', - with_example: true, - type: :number + parameter :positions_on_bench, 'Positions on Bench', with_example: true, type: :array, items: { type: :object } + parameter :dimensions, 'Dimensions', with_example: true, type: :array, items: { type: :object } let(:bench_id) { Bench.last.id } let(:plant_stage_id) { request.plant_stage.plant.plant_stages.second.id } let(:pot_id) { Pot.second.id } let(:pot_quantity) { 20 } + let(:positions_on_bench) do + [150, 50] + end + let(:dimensions) do + [10, 10] + end let(:raw_post) { params.to_json } @@ -143,7 +152,9 @@ expect(distribution.plant_stage_id).to eq(plant_stage_id) expect(distribution.pot_id).to eq(pot_id) expect(distribution.pot_quantity).to eq(pot_quantity) - expect(distribution.area).not_to be_blank + expect(distribution.positions_on_bench).to eq(positions_on_bench) + expect(distribution.dimensions).to eq(dimensions) + expect(distribution.request_id).to eq(request_id) end end diff --git a/spec/requests/api/v1/benches_spec.rb b/spec/requests/api/v1/benches_spec.rb index cd01a01..d892016 100644 --- a/spec/requests/api/v1/benches_spec.rb +++ b/spec/requests/api/v1/benches_spec.rb @@ -50,9 +50,7 @@ expect(status).to eq(422) expect(response.parsed_body.dig('error', 'message')).not_to be_blank end - end - it_behaves_like 'with authenticated grower' do it 'can\'t have a negative position' do post( "/api/v1/greenhouses/#{greenhouse.id}/benches", @@ -68,11 +66,8 @@ expect(status).to eq(422) expect(response.parsed_body.dig('error', 'message')).not_to be_blank end - end - it_behaves_like 'with authenticated grower' do - before do - # Create a bench that overlaps the existing bench + it 'returns an error about overlapping bench' do post( "/api/v1/greenhouses/#{greenhouse.id}/benches", headers:, @@ -83,9 +78,7 @@ }, as: :json ) - end - it 'returns an error about overlapping bench' do expect(status).to eq(422) expect(response.parsed_body.dig('error', 'message', 'positions').first) .to eq('bench overlaps with an existing bench') @@ -97,42 +90,33 @@ describe 'PUT api/v1/benches/:id' do context 'when 422' do it_behaves_like 'with authenticated grower' do - it 'can\'t have an area lower than the sum of distributions areas' do + it 'returns an error about overlapping bench' do put( "/api/v1/benches/#{bench.id}", headers:, params: { - name: :bench_name, - dimensions: [25, 30], - positions: [10, 10] + positions: [300, 300] }, as: :json ) expect(status).to eq(422) - expect(response.parsed_body.dig('error', 'message')).not_to be_blank + expect(response.parsed_body.dig('error', 'message', 'positions').first) + .to eq('bench overlaps with an existing bench') end - end - it_behaves_like 'with authenticated grower' do - before do - # Attempt to update a bench to overlap another existing bench + it 'can\'t have an area lower than the sum of distributions areas' do put( "/api/v1/benches/#{bench.id}", headers:, params: { - name: 'Updated Bench', - dimensions: [500, 200], - positions: [bench.positions[0], bench.positions[1]] # same position as an existing bench + dimensions: [5, 3] }, as: :json ) - end - it 'returns an error about overlapping bench' do expect(status).to eq(422) - expect(response.parsed_body.dig('error', 'message', - 'positions').first).to eq('bench overlaps with an existing bench') + expect(response.parsed_body.dig('error', 'message')).not_to be_blank end end end diff --git a/spec/requests/api/v1/request_distributions_spec.rb b/spec/requests/api/v1/request_distributions_spec.rb index f8b5f78..a466647 100644 --- a/spec/requests/api/v1/request_distributions_spec.rb +++ b/spec/requests/api/v1/request_distributions_spec.rb @@ -8,12 +8,12 @@ let!(:distribution) { request.request_distributions.first } let!(:id) { distribution.id } - describe 'GET api/v1/requests/:request_id/request_distributions' do + describe 'GET api/v1/request_distributions' do context 'when 200' do it_behaves_like 'with authenticated grower' do it 'gets request distributions with pagination params' do get( - "/api/v1/requests/#{request_id}/request_distributions", + '/api/v1/request_distributions', headers:, params: { page: { @@ -28,8 +28,8 @@ expect(response.parsed_body.count).to eq(2) expect(response.headers['Pagination-Current-Page']).to eq(1) expect(response.headers['Pagination-Per']).to eq(2) - expect(response.headers['Pagination-Total-Pages']).to eq(1) - expect(response.headers['Pagination-Total-Count']).to eq(2) + expect(response.headers['Pagination-Total-Pages']).to eq(2) + expect(response.headers['Pagination-Total-Count']).to eq(3) end end end @@ -46,7 +46,9 @@ bench_id: Bench.first.id, plant_stage_id: request.plant_stage_id, pot_id: Pot.first.id, - pot_quantity: 10 + pot_quantity: 10, + dimensions: [10, 10], + positions_on_bench: [150, 150] } ) @@ -60,48 +62,97 @@ expect(distribution.plant_stage).to eq(request.plant_stage) expect(distribution.pot).to eq(Pot.first) expect(distribution.pot_quantity).to eq(10) - expect(distribution.area).not_to be_blank + expect(distribution.dimensions).to eq([10, 10]) + expect(distribution.positions_on_bench).to eq([150, 150]) end + end + end - it 'can create a request distribution given its area' do + context 'when 422' do + it_behaves_like 'with authenticated grower' do + it 'fails to create a request distribution with missing params' do + post( + "/api/v1/requests/#{request_id}/request_distributions", + headers:, + params: { + bench_id: nil, + plant_stage_id: nil, + pot_id: nil, + pot_quantity: nil, + dimensions: nil, + positions_on_bench: nil + } + ) + + expect(status).to eq(422) + expect(response.parsed_body.dig('error', 'message')).not_to be_blank + end + + it 'fails to create a request distribution without enough seeds left to plant' do post( "/api/v1/requests/#{request_id}/request_distributions", headers:, params: { bench_id: Bench.first.id, plant_stage_id: request.plant_stage_id, - area: 150 + pot_id: Pot.first.id, + pot_quantity: 500, + dimensions: [5, 5], + positions_on_bench: [150, 150] } ) - expect(status).to eq(201) + expect(status).to eq(422) + expect(response.parsed_body.dig('error', 'message')).not_to be_blank + end - distribution = RequestDistribution.last - expect(response.body).to eq(distribution.to_blueprint) - expect(distribution.request).to eq(request) - expect(distribution.bench).to eq(Bench.first) - expect(response.parsed_body['greenhouse_id']).to eq(Bench.first.greenhouse_id) - expect(distribution.plant_stage).to eq(request.plant_stage) - expect(distribution.pot).to be_nil - expect(distribution.pot_quantity).to be_nil - expect(distribution.area).to eq(150) + it 'fails to create a request distribution with invalid dimensions' do + post( + "/api/v1/requests/#{request_id}/request_distributions", + headers:, + params: { + bench_id: Bench.first.id, + plant_stage_id: request.plant_stage_id, + pot_id: Pot.first.id, + pot_quantity: 5, + dimensions: [0, -2], + positions_on_bench: [150, 150] + } + ) + + expect(status).to eq(422) + expect(response.parsed_body.dig('error', 'message')).not_to be_blank end - end - end - context 'when 422' do - it_behaves_like 'with authenticated grower' do - it 'distributions can\'t have a sum of areas greater than their bench area' do - dimensions = Bench.first.dimensions - area = dimensions[0] * dimensions[1] + it 'fails to create a request distribution with invalid positions' do + post( + "/api/v1/requests/#{request_id}/request_distributions", + headers:, + params: { + bench_id: Bench.first.id, + plant_stage_id: request.plant_stage_id, + pot_id: Pot.first.id, + pot_quantity: 5, + dimensions: [5, 5], + positions_on_bench: [-1, -5] + } + ) + + expect(status).to eq(422) + expect(response.parsed_body.dig('error', 'message')).not_to be_blank + end + it 'fails to create a request distribution with overlapping exists distribution' do post( "/api/v1/requests/#{request_id}/request_distributions", headers:, params: { bench_id: Bench.first.id, plant_stage_id: request.plant_stage_id, - area: + pot_id: Pot.first.id, + pot_quantity: 5, + dimensions: [5, 5], + positions_on_bench: [10, 10] } ) @@ -109,14 +160,17 @@ expect(response.parsed_body.dig('error', 'message')).not_to be_blank end - it 'fails to create a request distribution with missing params' do + it 'fails to create a request distribution when it outside bench' do post( "/api/v1/requests/#{request_id}/request_distributions", headers:, params: { - bench_id: nil, - plant_stage_id: nil, - area: nil + bench_id: Bench.first.id, + plant_stage_id: request.plant_stage_id, + pot_id: Pot.first.id, + pot_quantity: 5, + dimensions: [5, 5], + positions_on_bench: [1000, 1000] } ) @@ -148,39 +202,84 @@ expect(distribution.bench).to eq(Bench.second) expect(distribution.pot).to eq(Pot.second) expect(distribution.pot_quantity).to eq(20) - expect(distribution.area).not_to be_blank end + end + end - it 'can update a request distribution given its area' do + context 'when 422' do + it_behaves_like 'with authenticated grower' do + it 'fails to update a request distribution with missing params' do put( "/api/v1/request_distributions/#{id}", headers:, params: { - bench_id: Bench.second.id, - area: 100 + bench_id: nil, + plant_stage_id: nil } ) - expect(status).to eq(200) + expect(status).to eq(422) + expect(response.parsed_body.dig('error', 'message')).not_to be_blank + end - distribution.reload - expect(response.body).to eq(distribution.to_blueprint) - expect(distribution.bench).to eq(Bench.second) - expect(distribution.area).to eq(100) + it 'fails to update a request distribution without enough seeds left to plant' do + put( + "/api/v1/request_distributions/#{id}", + headers:, + params: { + pot_quantity: 500 + } + ) + + expect(status).to eq(422) + expect(response.parsed_body.dig('error', 'message')).not_to be_blank end - end - end - context 'when 422' do - it_behaves_like 'with authenticated grower' do - it 'fails to update a request distribution with missing params' do + it 'fails to update a request distribution with invalid dimensions' do put( "/api/v1/request_distributions/#{id}", headers:, params: { - bench_id: nil, - plant_stage_id: nil, - area: nil + dimensions: [0, -2] + } + ) + + expect(status).to eq(422) + expect(response.parsed_body.dig('error', 'message')).not_to be_blank + end + + it 'fails to update a request distribution with invalid positions' do + put( + "/api/v1/request_distributions/#{id}", + headers:, + params: { + positions_on_bench: [-1, -5] + } + ) + + expect(status).to eq(422) + expect(response.parsed_body.dig('error', 'message')).not_to be_blank + end + + it 'fails to update a request distribution when overlapping with another distribution in the same bench' do + put( + "/api/v1/request_distributions/#{id}", + headers:, + params: { + positions_on_bench: [60, 60] + } + ) + + expect(status).to eq(422) + expect(response.parsed_body.dig('error', 'message')).not_to be_blank + end + + it 'fails to update a request distribution when it outside bench' do + put( + "/api/v1/request_distributions/#{id}", + headers:, + params: { + positions_on_bench: [1000, 1000] } ) diff --git a/test/fixtures/benches.yml b/test/fixtures/benches.yml index 7958599..8d4880d 100644 --- a/test/fixtures/benches.yml +++ b/test/fixtures/benches.yml @@ -4,7 +4,7 @@ bench1: name: My big greenhouse - bench 1 dimensions: - 200 - - 100 + - 200 positions: - 10 - 50 @@ -26,8 +26,8 @@ bench2: - 100 - 100 positions: - - 200 - - 200 + - 300 + - 300 created_at: !ruby/object:ActiveSupport::TimeWithZone utc: &1 2019-12-30 18:06:47.133846000 Z zone: &2 !ruby/object:ActiveSupport::TimeZone diff --git a/test/fixtures/request_distributions.yml b/test/fixtures/request_distributions.yml index c70f652..750514c 100644 --- a/test/fixtures/request_distributions.yml +++ b/test/fixtures/request_distributions.yml @@ -6,7 +6,12 @@ request_distribution1: plant_stage_id: 6 pot_id: 1 pot_quantity: 30 - area: !ruby/object:BigDecimal 18:0.147e4 + dimensions: + - 40 + - 40 + positions_on_bench: + - 0 + - 0 created_at: !ruby/object:ActiveSupport::TimeWithZone utc: &1 2020-02-25 21:03:27.754298000 Z zone: &2 !ruby/object:ActiveSupport::TimeZone @@ -24,7 +29,12 @@ request_distribution2: plant_stage_id: 6 pot_id: 2 pot_quantity: 20 - area: !ruby/object:BigDecimal 18:0.16e4 + dimensions: + - 40 + - 40 + positions_on_bench: + - 50 + - 50 created_at: !ruby/object:ActiveSupport::TimeWithZone utc: &1 2020-02-25 21:03:27.843552000 Z zone: &2 !ruby/object:ActiveSupport::TimeZone @@ -40,9 +50,14 @@ request_distribution3: request_id: 2 bench_id: 1 plant_stage_id: 7 - pot_id: - pot_quantity: - area: !ruby/object:BigDecimal 18:0.1e3 + pot_id: 2 + pot_quantity: 10 + dimensions: + - 20 + - 20 + positions_on_bench: + - 100 + - 100 created_at: !ruby/object:ActiveSupport::TimeWithZone utc: &1 2020-02-25 21:04:53.101797000 Z zone: &2 !ruby/object:ActiveSupport::TimeZone @@ -57,15 +72,16 @@ request_distribution3: # # Table name: request_distributions # -# id :bigint not null, primary key -# request_id :bigint -# bench_id :bigint -# plant_stage_id :bigint -# pot_id :bigint -# pot_quantity :integer -# area :decimal(, ) -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint not null, primary key +# request_id :bigint +# bench_id :bigint +# plant_stage_id :bigint +# pot_id :bigint +# pot_quantity :integer +# created_at :datetime not null +# updated_at :datetime not null +# positions_on_bench :integer is an Array +# dimensions :integer is an Array # # Indexes # diff --git a/test/fixtures/requests.yml b/test/fixtures/requests.yml index c6a4b18..6e3754a 100644 --- a/test/fixtures/requests.yml +++ b/test/fixtures/requests.yml @@ -13,7 +13,7 @@ request1: status: accepted comment: I didn't think it would end this way. due_date: 2020-05-13 - quantity: 50 + quantity: 100 temperature: 20 photoperiod: 8 created_at: !ruby/object:ActiveSupport::TimeWithZone diff --git a/test/models/bench_test.rb b/test/models/bench_test.rb index ddabe54..543fa6c 100644 --- a/test/models/bench_test.rb +++ b/test/models/bench_test.rb @@ -37,11 +37,11 @@ def setup test 'invalid with non-positive dimensions' do @bench.dimensions = [10, -20] assert_not @bench.valid? - assert_includes @bench.errors[:dimensions], 'each dimension must be greater than 0' + assert_includes @bench.errors[:dimensions], 'chaque dimension doit être supérieure à 0' @bench.dimensions = [0, 30] assert_not @bench.valid? - assert_includes @bench.errors[:dimensions], 'each dimension must be greater than 0' + assert_includes @bench.errors[:dimensions], 'chaque dimension doit être supérieure à 0' end test 'invalid without position' do @@ -60,6 +60,22 @@ def setup assert_includes @bench.errors[:positions], 'each position must be positive' end + test 'invalid with wrong number of positions' do + @bench.positions = [10] + assert_not @bench.valid? + assert_includes @bench.errors[:positions], 'doit contenir exactement deux éléments : x et y' + + @bench.positions = [10, 20, 30] + assert_not @bench.valid? + assert_includes @bench.errors[:positions], 'doit contenir exactement deux éléments : x et y' + end + + test 'invalid when distributions areas is lower than bench area' do + @bench.dimensions = [10, 20] + assert_not @bench.valid? + assert_includes @bench.errors[:dimensions], 'sum of distributions areas can\'t be greater than bench area' + end + test 'invalid when overlapping with another bench in the same greenhouse' do overlapping_bench = Bench.new( greenhouse: @bench.greenhouse, diff --git a/test/models/request_distribution_test.rb b/test/models/request_distribution_test.rb index 3d61d3d..1336b72 100644 --- a/test/models/request_distribution_test.rb +++ b/test/models/request_distribution_test.rb @@ -6,6 +6,7 @@ class RequestDistributionTest < ActiveSupport::TestCase # Setups def setup @request_distribution = request_distributions(:request_distribution1) + @plant_stage = plant_stages(:plant_stage18) end # Validations @@ -13,29 +14,32 @@ def setup assert @request_distribution.valid?, @request_distribution.errors.messages end - test 'valid without pot' do - @request_distribution.pot = nil - assert @request_distribution.valid?, @request_distribution.errors.messages + test 'invalid without bench' do + @request_distribution.bench = nil + assert_not @request_distribution.valid? + assert_not_empty @request_distribution.errors[:bench] end - test 'valid without pot and pot_quantity' do + test 'invalid without pot' do @request_distribution.pot = nil - @request_distribution.pot_quantity = nil - assert @request_distribution.valid?, @request_distribution.errors.messages + assert_not @request_distribution.valid? + assert_not_empty @request_distribution.errors[:pot] end - test 'invalid without pot_quantity if pot is present' do - assert @request_distribution.pot.present? - + test 'invalid without pot quantity' do @request_distribution.pot_quantity = nil assert_not @request_distribution.valid? assert_not_empty @request_distribution.errors[:pot_quantity] end - test 'invalid without bench' do - @request_distribution.bench = nil + test 'invalid wit pot quantity less than 0' do + @request_distribution.pot_quantity = -1 assert_not @request_distribution.valid? - assert_not_empty @request_distribution.errors[:bench] + assert_not_empty @request_distribution.errors[:pot_quantity] + + @request_distribution.pot_quantity = 0 + assert_not @request_distribution.valid? + assert_not_empty @request_distribution.errors[:pot_quantity] end test 'invalid without plant_stage' do @@ -44,32 +48,118 @@ def setup assert_not_empty @request_distribution.errors[:plant_stage] end - test 'invalid without area' do - @request_distribution.area = nil - assert_not @request_distribution.valid? - assert_not_empty @request_distribution.errors[:area] - end - test 'invalid with plant_stage from the non-requested plant' do @request_distribution.plant_stage = PlantStage.last assert_not @request_distribution.valid? assert_not_empty @request_distribution.errors[:plant_stage] end + + test 'invalid without enough seeds left to plant' do + @request_distribution.pot_quantity = 500 + assert_not @request_distribution.valid? + assert_not_empty @request_distribution.errors[:pot_quantity] + end + + test 'invalid without dimensions' do + @request_distribution.dimensions = nil + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:dimensions], 'doit être rempli(e)' + end + + test 'invalid with wrong number of dimensions' do + @request_distribution.dimensions = [10] + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:dimensions], + 'doit contenir exactement deux éléments: longueur et largeur' + + @request_distribution.dimensions = [10, 20, 30] + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:dimensions], + 'doit contenir exactement deux éléments: longueur et largeur' + end + + test 'invalid with non-positive dimensions' do + @request_distribution.dimensions = [10, -20] + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:dimensions], 'chaque dimension doit être supérieure à 0' + + @request_distribution.dimensions = [0, 30] + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:dimensions], 'chaque dimension doit être supérieure à 0' + end + + test 'invalid without positions_on_bench' do + @request_distribution.positions_on_bench = nil + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:positions_on_bench], 'doit être rempli(e)' + end + + test 'invalid with wrong number of positions_on_bench' do + @request_distribution.positions_on_bench = [10] + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:positions_on_bench], 'doit contenir exactement deux éléments : x et y' + + @request_distribution.positions_on_bench = [10, 20, 30] + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:positions_on_bench], 'doit contenir exactement deux éléments : x et y' + end + + test 'invalid with non-positive positions_on_bench' do + @request_distribution.positions_on_bench = [10, -20] + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:positions_on_bench], 'each position must be positive' + + @request_distribution.positions_on_bench = [-1, 30] + assert_not @request_distribution.valid? + assert_includes @request_distribution.errors[:positions_on_bench], 'each position must be positive' + end + + test 'invalid when overlapping with another distribution in the same bench' do + overlapping_distribution = RequestDistribution.new( + request: Request.first, + bench: Bench.first, + pot: Pot.first, + plant_stage: @plant_stage, + pot_quantity: 10, + positions_on_bench: [0, 0], + dimensions: [10, 20] + ) + + assert_not overlapping_distribution.valid? + assert_includes overlapping_distribution.errors[:positions_on_bench], + 'distribution overlaps with an existing distribution' + end + + test 'invalid when distribution outside bench' do + distribution = RequestDistribution.new( + request: Request.first, + bench: Bench.first, + pot: Pot.first, + plant_stage: @plant_stage, + pot_quantity: 10, + positions_on_bench: [1000, 1000], + dimensions: [10, 20] + ) + + assert_not distribution.valid? + assert_includes distribution.errors[:positions_on_bench], 'distribution exceeds the bounds of the bench' + end end # == Schema Information # # Table name: request_distributions # -# id :bigint not null, primary key -# request_id :bigint -# bench_id :bigint -# plant_stage_id :bigint -# pot_id :bigint -# pot_quantity :integer -# area :decimal(, ) -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint not null, primary key +# request_id :bigint +# bench_id :bigint +# plant_stage_id :bigint +# pot_id :bigint +# pot_quantity :integer +# created_at :datetime not null +# updated_at :datetime not null +# positions_on_bench :integer is an Array +# dimensions :integer is an Array # # Indexes #