diff --git a/app/controllers/api/products_controller.rb b/app/controllers/api/products_controller.rb new file mode 100644 index 0000000000..2c04c52697 --- /dev/null +++ b/app/controllers/api/products_controller.rb @@ -0,0 +1,124 @@ +require 'open_food_network/permissions' + +module Api + class ProductsController < Api::BaseController + respond_to :json + + skip_authorization_check only: [:show, :bulk_products, :overridable] + + def show + @product = find_product(params[:id]) + render json: @product, serializer: Api::Admin::ProductSerializer + end + + def create + authorize! :create, Spree::Product + params[:product][:available_on] ||= Time.zone.now + @product = Spree::Product.new(params[:product]) + begin + if @product.save + render json: @product, serializer: Api::Admin::ProductSerializer, status: 201 + else + invalid_resource!(@product) + end + rescue ActiveRecord::RecordNotUnique + @product.permalink = nil + retry + end + end + + def update + authorize! :update, Spree::Product + @product = find_product(params[:id]) + if @product.update_attributes(params[:product]) + render json: @product, serializer: Api::Admin::ProductSerializer, status: 200 + else + invalid_resource!(@product) + end + end + + def destroy + authorize! :delete, Spree::Product + @product = find_product(params[:id]) + @product.update_attribute(:deleted_at, Time.zone.now) + @product.variants_including_master.update_all(deleted_at: Time.zone.now) + render json: @product, serializer: Api::Admin::ProductSerializer, status: 204 + end + + # TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it. + def bulk_products + @products = OpenFoodNetwork::Permissions.new(current_api_user).editable_products. + merge(product_scope). + order('created_at DESC'). + ransack(params[:q]).result. + page(params[:page]).per(params[:per_page]) + + render_paged_products @products + end + + def overridable + producers = OpenFoodNetwork::Permissions.new(current_api_user). + variant_override_producers.by_name + + @products = paged_products_for_producers producers + + render_paged_products @products + end + + def soft_delete + authorize! :delete, Spree::Product + @product = find_product(params[:product_id]) + authorize! :delete, @product + @product.destroy + render json: @product, serializer: Api::Admin::ProductSerializer, status: 204 + end + + # POST /api/products/:product_id/clone + # + def clone + authorize! :create, Spree::Product + original_product = find_product(params[:product_id]) + authorize! :update, original_product + + @product = original_product.duplicate + + render json: @product, serializer: Api::Admin::ProductSerializer, status: 201 + end + + private + + # Copied and modified from SpreeApi::BaseController to allow + # enterprise users to access inactive products + def product_scope + # This line modified + if current_api_user.has_spree_role?("admin") || current_api_user.enterprises.present? + scope = Spree::Product + if params[:show_deleted] + scope = scope.with_deleted + end + else + scope = Spree::Product.active + end + + scope.includes(:master) + end + + def paged_products_for_producers(producers) + Spree::Product.scoped. + merge(product_scope). + where(supplier_id: producers). + by_producer.by_name. + ransack(params[:q]).result. + page(params[:page]).per(params[:per_page]) + end + + def render_paged_products(products) + serializer = ActiveModel::ArraySerializer.new( + products, + each_serializer: Api::Admin::ProductSerializer + ) + + render text: { products: serializer, pages: products.num_pages }.to_json + end + end +end diff --git a/app/controllers/api/variants_controller.rb b/app/controllers/api/variants_controller.rb new file mode 100644 index 0000000000..b196270927 --- /dev/null +++ b/app/controllers/api/variants_controller.rb @@ -0,0 +1,79 @@ +module Api + class VariantsController < Api::BaseController + respond_to :json + + skip_authorization_check only: [:index, :show] + before_filter :product + + def index + @variants = scope.includes(:option_values).ransack(params[:q]).result + render json: @variants, each_serializer: Api::VariantSerializer + end + + def show + @variant = scope.includes(:option_values).find(params[:id]) + render json: @variant, serializer: Api::VariantSerializer + end + + def create + authorize! :create, Spree::Variant + @variant = scope.new(params[:variant]) + if @variant.save + render json: @variant, serializer: Api::VariantSerializer, status: 201 + else + invalid_resource!(@variant) + end + end + + def update + authorize! :update, Spree::Variant + @variant = scope.find(params[:id]) + if @variant.update_attributes(params[:variant]) + render json: @variant, serializer: Api::VariantSerializer, status: 200 + else + invalid_resource!(@product) + end + end + + def soft_delete + @variant = scope.find(params[:variant_id]) + authorize! :delete, @variant + + VariantDeleter.new.delete(@variant) + render json: @variant, serializer: Api::VariantSerializer, status: 204 + end + + def destroy + authorize! :delete, Spree::Variant + @variant = scope.find(params[:id]) + @variant.destroy + render json: @variant, serializer: Api::VariantSerializer, status: 204 + end + + private + + def product + @product ||= Spree::Product.find_by_permalink(params[:product_id]) if params[:product_id] + end + + def scope + if @product + unless current_api_user.has_spree_role?("admin") || params[:show_deleted] + variants = @product.variants_including_master + else + variants = @product.variants_including_master.with_deleted + end + else + variants = Spree::Variant.scoped + if current_api_user.has_spree_role?("admin") + unless params[:show_deleted] + variants = Spree::Variant.active + end + else + variants = variants.active + end + end + variants + end + end +end diff --git a/app/controllers/spree/api/products_controller.rb b/app/controllers/spree/api/products_controller.rb deleted file mode 100644 index 615596383e..0000000000 --- a/app/controllers/spree/api/products_controller.rb +++ /dev/null @@ -1,149 +0,0 @@ -require 'open_food_network/permissions' - -module Spree - module Api - class ProductsController < Spree::Api::BaseController - respond_to :json - - def index - if params[:ids] - @products = product_scope.where(id: params[:ids]) - else - @products = product_scope.ransack(params[:q]).result - end - - @products = @products.page(params[:page]).per(params[:per_page]) - - respond_with(@products) - end - - def show - @product = find_product(params[:id]) - respond_with(@product) - end - - def new; end - - def create - authorize! :create, Product - params[:product][:available_on] ||= Time.zone.now - @product = Product.new(params[:product]) - begin - if @product.save - respond_with(@product, status: 201, default_template: :show) - else - invalid_resource!(@product) - end - rescue ActiveRecord::RecordNotUnique - @product.permalink = nil - retry - end - end - - def update - authorize! :update, Product - @product = find_product(params[:id]) - if @product.update_attributes(params[:product]) - respond_with(@product, status: 200, default_template: :show) - else - invalid_resource!(@product) - end - end - - def destroy - authorize! :delete, Product - @product = find_product(params[:id]) - @product.update_attribute(:deleted_at, Time.zone.now) - @product.variants_including_master.update_all(deleted_at: Time.zone.now) - respond_with(@product, status: 204) - end - - def managed - authorize! :admin, Spree::Product - authorize! :read, Spree::Product - - @products = product_scope. - ransack(params[:q]).result. - managed_by(current_api_user). - page(params[:page]).per(params[:per_page]) - respond_with(@products, default_template: :index) - end - - # TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it. - def bulk_products - @products = OpenFoodNetwork::Permissions.new(current_api_user).editable_products. - merge(product_scope). - order('created_at DESC'). - ransack(params[:q]).result. - page(params[:page]).per(params[:per_page]) - - render_paged_products @products - end - - def overridable - producers = OpenFoodNetwork::Permissions.new(current_api_user). - variant_override_producers.by_name - - @products = paged_products_for_producers producers - - render_paged_products @products - end - - def soft_delete - authorize! :delete, Spree::Product - @product = find_product(params[:product_id]) - authorize! :delete, @product - @product.destroy - respond_with(@product, status: 204) - end - - # POST /api/products/:product_id/clone - # - def clone - authorize! :create, Spree::Product - original_product = find_product(params[:product_id]) - authorize! :update, original_product - - @product = original_product.duplicate - - respond_with(@product, status: 201, default_template: :show) - end - - private - - # Copied and modified from Spree::Api::BaseController to allow - # enterprise users to access inactive products - def product_scope - # This line modified - if current_api_user.has_spree_role?("admin") || current_api_user.enterprises.present? - scope = Spree::Product - if params[:show_deleted] - scope = scope.with_deleted - end - else - scope = Spree::Product.active - end - - scope.includes(:master) - end - - def paged_products_for_producers(producers) - Spree::Product.scoped. - merge(product_scope). - where(supplier_id: producers). - by_producer.by_name. - ransack(params[:q]).result. - page(params[:page]).per(params[:per_page]) - end - - def render_paged_products(products) - serializer = ActiveModel::ArraySerializer.new( - products, - each_serializer: ::Api::Admin::ProductSerializer - ) - - render text: { products: serializer, pages: products.num_pages }.to_json - end - end - end -end diff --git a/app/controllers/spree/api/variants_controller.rb b/app/controllers/spree/api/variants_controller.rb deleted file mode 100644 index fe0fa1d793..0000000000 --- a/app/controllers/spree/api/variants_controller.rb +++ /dev/null @@ -1,83 +0,0 @@ -module Spree - module Api - class VariantsController < Spree::Api::BaseController - respond_to :json - - before_filter :product - - def index - @variants = scope.includes(:option_values).ransack(params[:q]).result. - page(params[:page]).per(params[:per_page]) - respond_with(@variants) - end - - def show - @variant = scope.includes(:option_values).find(params[:id]) - respond_with(@variant) - end - - def new; end - - def create - authorize! :create, Variant - @variant = scope.new(params[:variant]) - if @variant.save - respond_with(@variant, status: 201, default_template: :show) - else - invalid_resource!(@variant) - end - end - - def update - authorize! :update, Variant - @variant = scope.find(params[:id]) - if @variant.update_attributes(params[:variant]) - respond_with(@variant, status: 200, default_template: :show) - else - invalid_resource!(@product) - end - end - - def soft_delete - @variant = scope.find(params[:variant_id]) - authorize! :delete, @variant - - VariantDeleter.new.delete(@variant) - respond_with @variant, status: 204 - end - - def destroy - authorize! :delete, Variant - @variant = scope.find(params[:id]) - @variant.destroy - respond_with(@variant, status: 204) - end - - private - - def product - @product ||= Spree::Product.find_by_permalink(params[:product_id]) if params[:product_id] - end - - def scope - if @product - unless current_api_user.has_spree_role?("admin") || params[:show_deleted] - variants = @product.variants_including_master - else - variants = @product.variants_including_master.with_deleted - end - else - variants = Variant.scoped - if current_api_user.has_spree_role?("admin") - unless params[:show_deleted] - variants = Variant.active - end - else - variants = variants.active - end - end - variants - end - end - end -end diff --git a/app/models/spree/price_decorator.rb b/app/models/spree/price_decorator.rb index 79e809b8ba..00219eeedd 100644 --- a/app/models/spree/price_decorator.rb +++ b/app/models/spree/price_decorator.rb @@ -4,6 +4,12 @@ module Spree private + def check_price + if currency.nil? + self.currency = Spree::Config[:currency] + end + end + def refresh_products_cache variant.andand.refresh_products_cache end diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index b7cc365120..518dd60d7c 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -1,6 +1,8 @@ class Api::VariantSerializer < ActiveModel::Serializer - attributes :id, :is_master, :on_hand, :name_to_display, :unit_to_display, :unit_value - attributes :options_text, :on_demand, :price, :fees, :price_with_fees, :product_name + attributes :id, :is_master, :product_name, :sku + attributes :options_text, :unit_value, :unit_description, :unit_to_display + attributes :display_as, :display_name, :name_to_display + attributes :price, :on_demand, :on_hand, :fees, :price_with_fees attributes :tag_list delegate :price, to: :object diff --git a/app/views/api/enterprises/bulk_show.v1.rabl b/app/views/api/enterprises/bulk_show.v1.rabl deleted file mode 100644 index 4cc671a3f1..0000000000 --- a/app/views/api/enterprises/bulk_show.v1.rabl +++ /dev/null @@ -1,3 +0,0 @@ -object @enterprise - -attributes :id, :name diff --git a/app/views/spree/api/products/bulk_index.v1.rabl b/app/views/spree/api/products/bulk_index.v1.rabl deleted file mode 100644 index dfb9f20f2a..0000000000 --- a/app/views/spree/api/products/bulk_index.v1.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @products.order('id ASC') -extends "spree/api/products/bulk_show" diff --git a/app/views/spree/api/products/bulk_show.v1.rabl b/app/views/spree/api/products/bulk_show.v1.rabl deleted file mode 100644 index 3baf5a5069..0000000000 --- a/app/views/spree/api/products/bulk_show.v1.rabl +++ /dev/null @@ -1,27 +0,0 @@ -object @product - -# TODO: This is used by bulk product edit when a product is cloned. -# But the list of products is serialized by Api::Admin::ProductSerializer. -# This should probably be unified. - -attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties -attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id - -# Infinity is not a valid JSON object, but Rails encodes it anyway -node( :taxon_ids ) { |p| p.taxons.map(&:id).join(",") } -node( :on_hand ) { |p| p.on_hand.nil? ? 0 : p.on_hand.to_f.finite? ? p.on_hand : t(:on_demand) } -node( :price ) { |p| p.price.nil? ? '0.0' : p.price } - -node( :available_on ) { |p| p.available_on.blank? ? "" : p.available_on.strftime("%F %T") } -node( :permalink_live, &:permalink ) -node( :producer_id, &:supplier_id ) -node( :category_id, &:primary_taxon_id ) -node( :supplier ) do |p| - partial 'api/enterprises/bulk_show', object: p.supplier -end -node( :variants ) do |p| - partial 'spree/api/variants/bulk_index', object: p.variants.reorder('spree_variants.id ASC') -end -node( :master ) do |p| - partial 'spree/api/variants/bulk_show', object: p.master -end diff --git a/app/views/spree/api/variants/bulk_index.v1.rabl b/app/views/spree/api/variants/bulk_index.v1.rabl deleted file mode 100644 index 69f1b82f5f..0000000000 --- a/app/views/spree/api/variants/bulk_index.v1.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @variants -extends "spree/api/variants/bulk_show" diff --git a/app/views/spree/api/variants/bulk_show.v1.rabl b/app/views/spree/api/variants/bulk_show.v1.rabl deleted file mode 100644 index 8044ded0a0..0000000000 --- a/app/views/spree/api/variants/bulk_show.v1.rabl +++ /dev/null @@ -1,7 +0,0 @@ -object @variant - -attributes :id, :options_text, :unit_value, :unit_description, :on_demand, :display_as, :display_name - -# Infinity is not a valid JSON object, but Rails encodes it anyway -node( :on_hand ) { |v| v.on_hand.nil? ? 0 : ( v.on_hand.to_f.finite? ? v.on_hand : t(:on_demand) ) } -node( :price ) { |v| v.price.nil? ? 0.to_f : v.price } diff --git a/config/routes/api.rb b/config/routes/api.rb index 99c7f76eda..bb8c98ba3b 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -1,5 +1,20 @@ Openfoodnetwork::Application.routes.draw do namespace :api do + resources :products do + collection do + get :bulk_products + get :overridable + end + delete :soft_delete + post :clone + + resources :variants do + delete :soft_delete + end + end + + resources :variants, :only => [:index] + resources :enterprises do post :update_image, on: :member get :managed, on: :collection diff --git a/config/routes/spree.rb b/config/routes/spree.rb index 1dc3c32071..1689d6d0d6 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -62,22 +62,6 @@ Spree::Core::Engine.routes.prepend do get :authorise_api, on: :collection end - resources :products do - collection do - get :managed - get :bulk_products - get :overridable - end - delete :soft_delete - post :clone - - resources :variants do - delete :soft_delete - end - end - - resources :variants, :only => [:index] - resources :orders do get :managed, on: :collection diff --git a/spec/controllers/api/products_controller_spec.rb b/spec/controllers/api/products_controller_spec.rb new file mode 100644 index 0000000000..32efb09ddc --- /dev/null +++ b/spec/controllers/api/products_controller_spec.rb @@ -0,0 +1,225 @@ +require 'spec_helper' + +describe Api::ProductsController, type: :controller do + render_views + + let(:supplier) { create(:supplier_enterprise) } + let(:supplier2) { create(:supplier_enterprise) } + let!(:product) { create(:product, supplier: supplier) } + let!(:inactive_product) { create(:product, available_on: Time.zone.now.tomorrow, name: "inactive") } + let(:product_other_supplier) { create(:product, supplier: supplier2) } + let(:product_with_image) { create(:product_with_image, supplier: supplier) } + let(:attributes) { ["id", "name", "supplier", "price", "on_hand", "available_on", "permalink_live"] } + let(:all_attributes) { ["id", "name", "price", "available_on", "variants"] } + let(:variants_attributes) { ["id", "options_text", "unit_value", "unit_description", "unit_to_display", "on_demand", "display_as", "display_name", "name_to_display", "sku", "on_hand", "price"] } + + let(:current_api_user) { build(:user) } + + before do + allow(controller).to receive(:spree_current_user) { current_api_user } + end + + context "as a normal user" do + before do + allow(current_api_user) + .to receive(:has_spree_role?).with("admin").and_return(false) + end + + it "gets a single product" do + product.master.images.create!(attachment: image("thinking-cat.jpg")) + product.variants.create!(unit_value: "1", unit_description: "thing") + product.variants.first.images.create!(attachment: image("thinking-cat.jpg")) + product.set_property("spree", "rocks") + api_get :show, id: product.to_param + + expect(all_attributes.all?{ |attr| json_response.keys.include? attr }).to eq(true) + expect(variants_attributes.all?{ |attr| json_response['variants'].first.keys.include? attr }).to eq(true) + end + + context "finds a product by permalink first then by id" do + let!(:other_product) { create(:product, permalink: "these-are-not-the-droids-you-are-looking-for") } + + before do + product.update_attribute(:permalink, "#{other_product.id}-and-1-ways") + end + + specify do + api_get :show, id: product.to_param + + expect(json_response["permalink_live"]).to match(/and-1-ways/) + product.destroy + + api_get :show, id: other_product.id + expect(json_response["permalink_live"]).to match(/droids/) + end + end + + it "cannot see inactive products" do + api_get :show, id: inactive_product.to_param + + expect(json_response["error"]).to eq("The resource you were looking for could not be found.") + expect(response.status).to eq(404) + end + + it "returns a 404 error when it cannot find a product" do + api_get :show, id: "non-existant" + + expect(json_response["error"]).to eq("The resource you were looking for could not be found.") + expect(response.status).to eq(404) + end + + include_examples "modifying product actions are restricted" + end + + context "as an enterprise user" do + let(:current_api_user) do + user = create(:user) + user.enterprise_roles.create(enterprise: supplier) + user + end + + it "soft deletes my products" do + spree_delete :soft_delete, product_id: product.to_param, format: :json + + expect(response.status).to eq(204) + expect { product.reload }.not_to raise_error + expect(product.deleted_at).not_to be_nil + end + + it "is denied access to soft deleting another enterprises' product" do + spree_delete :soft_delete, product_id: product_other_supplier.to_param, format: :json + + assert_unauthorized! + expect { product_other_supplier.reload }.not_to raise_error + expect(product_other_supplier.deleted_at).to be_nil + end + end + + context "as an administrator" do + before do + allow(current_api_user) + .to receive(:has_spree_role?).with("admin").and_return(true) + end + + it "soft deletes a product" do + spree_delete :soft_delete, product_id: product.to_param, format: :json + + expect(response.status).to eq(204) + expect { product.reload }.not_to raise_error + expect(product.deleted_at).not_to be_nil + end + + it "can create a new product" do + api_post :create, product: { name: "The Other Product", + price: 19.99, + shipping_category_id: create(:shipping_category).id, + supplier_id: supplier.id, + primary_taxon_id: FactoryBot.create(:taxon).id, + variant_unit: "items", + variant_unit_name: "things", + unit_description: "things" } + + expect(all_attributes.all?{ |attr| json_response.keys.include? attr }).to eq(true) + expect(response.status).to eq(201) + end + + it "cannot create a new product with invalid attributes" do + api_post :create, product: {} + + expect(response.status).to eq(422) + expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") + errors = json_response["errors"] + expect(errors.keys).to match_array(["name", "price", "primary_taxon", "shipping_category_id", "supplier", "variant_unit"]) + end + + it "can update a product" do + api_put :update, id: product.to_param, product: { name: "New and Improved Product!" } + + expect(response.status).to eq(200) + end + + it "cannot update a product with an invalid attribute" do + api_put :update, id: product.to_param, product: { name: "" } + + expect(response.status).to eq(422) + expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") + expect(json_response["errors"]["name"]).to eq(["can't be blank"]) + end + + it "can delete a product" do + expect(product.deleted_at).to be_nil + api_delete :destroy, id: product.to_param + + expect(response.status).to eq(204) + expect(product.reload.deleted_at).not_to be_nil + end + end + + describe '#clone' do + context 'as a normal user' do + before do + allow(current_api_user) + .to receive(:has_spree_role?).with("admin").and_return(false) + end + + it 'denies access' do + spree_post :clone, product_id: product.id, format: :json + + assert_unauthorized! + end + end + + context 'as an enterprise user' do + let(:current_api_user) do + user = create(:user) + user.enterprise_roles.create(enterprise: supplier) + user + end + + it 'responds with a successful response' do + spree_post :clone, product_id: product.id, format: :json + + expect(response.status).to eq(201) + end + + it 'clones the product' do + spree_post :clone, product_id: product.id, format: :json + + expect(json_response['name']).to eq("COPY OF #{product.name}") + end + + it 'clones a product with image' do + spree_post :clone, product_id: product_with_image.id, format: :json + + expect(response.status).to eq(201) + expect(json_response['name']).to eq("COPY OF #{product_with_image.name}") + end + end + + context 'as an administrator' do + before do + allow(current_api_user) + .to receive(:has_spree_role?).with("admin").and_return(true) + end + + it 'responds with a successful response' do + spree_post :clone, product_id: product.id, format: :json + + expect(response.status).to eq(201) + end + + it 'clones the product' do + spree_post :clone, product_id: product.id, format: :json + + expect(json_response['name']).to eq("COPY OF #{product.name}") + end + + it 'clones a product with image' do + spree_post :clone, product_id: product_with_image.id, format: :json + + expect(response.status).to eq(201) + expect(json_response['name']).to eq("COPY OF #{product_with_image.name}") + end + end + end +end diff --git a/spec/controllers/api/variants_controller_spec.rb b/spec/controllers/api/variants_controller_spec.rb new file mode 100644 index 0000000000..9964d40c1b --- /dev/null +++ b/spec/controllers/api/variants_controller_spec.rb @@ -0,0 +1,197 @@ +require 'spec_helper' + +describe Api::VariantsController, type: :controller do + render_views + + let(:supplier) { FactoryBot.create(:supplier_enterprise) } + let!(:variant1) { FactoryBot.create(:variant) } + let!(:variant2) { FactoryBot.create(:variant) } + let!(:variant3) { FactoryBot.create(:variant) } + let(:attributes) { [:id, :options_text, :price, :on_hand, :unit_value, :unit_description, :on_demand, :display_as, :display_name] } + + before do + allow(controller).to receive(:spree_current_user) { current_api_user } + end + + context "as a normal user" do + sign_in_as_user! + + let!(:product) { create(:product) } + let!(:variant) do + variant = product.master + variant.option_values << create(:option_value) + variant + end + + it "retrieves a list of variants with appropriate attributes" do + spree_get :index, format: :json + + keys = json_response.first.keys.map(&:to_sym) + expect(attributes.all?{ |attr| keys.include? attr }).to eq(true) + end + + it "is denied access when trying to delete a variant" do + product = create(:product) + variant = product.master + spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json + + assert_unauthorized! + expect { variant.reload }.not_to raise_error + expect(variant.deleted_at).to be_nil + end + + it 'can query the results through a parameter' do + expected_result = create(:variant, sku: 'FOOBAR') + api_get :index, q: { sku_cont: 'FOO' } + + expect(json_response.size).to eq(1) + expect(json_response.first['sku']).to eq expected_result.sku + end + + # Regression test for spree#2141 + context "a deleted variant" do + before do + variant.update_column(:deleted_at, Time.zone.now) + end + + it "is not returned in the results" do + api_get :index + expect(json_response.count).to eq(10) # there are 11 variants + end + + it "is not returned even when show_deleted is passed" do + api_get :index, show_deleted: true + expect(json_response.count).to eq(10) # there are 11 variants + end + end + + it "can see a single variant" do + api_get :show, id: variant.to_param + + keys = json_response.keys.map(&:to_sym) + expect((attributes).all?{ |attr| keys.include? attr }).to eq(true) + end + + it "cannot create a new variant if not an admin" do + api_post :create, variant: { sku: "12345" } + + assert_unauthorized! + end + + it "cannot update a variant" do + api_put :update, id: variant.to_param, variant: { sku: "12345" } + + assert_unauthorized! + end + + it "cannot delete a variant" do + api_delete :destroy, id: variant.to_param + + assert_unauthorized! + expect { variant.reload }.not_to raise_error + end + end + + context "as an enterprise user" do + sign_in_as_enterprise_user! [:supplier] + let(:supplier_other) { create(:supplier_enterprise) } + let(:product) { create(:product, supplier: supplier) } + let(:variant) { product.master } + let(:product_other) { create(:product, supplier: supplier_other) } + let(:variant_other) { product_other.master } + + it "soft deletes a variant" do + spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json + + expect(response.status).to eq(204) + expect { variant.reload }.not_to raise_error + expect(variant.deleted_at).to be_present + end + + it "is denied access to soft deleting another enterprises' variant" do + spree_delete :soft_delete, variant_id: variant_other.to_param, product_id: product_other.to_param, format: :json + + assert_unauthorized! + expect { variant.reload }.not_to raise_error + expect(variant.deleted_at).to be_nil + end + + context 'when the variant is not the master' do + before { variant.update_attribute(:is_master, false) } + + it 'refreshes the cache' do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant) + spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json + end + end + end + + context "as an administrator" do + sign_in_as_admin! + + let(:product) { create(:product) } + let(:variant) { product.master } + let(:resource_scoping) { { product_id: variant.product.to_param } } + + it "soft deletes a variant" do + spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json + + expect(response.status).to eq(204) + expect { variant.reload }.not_to raise_error + expect(variant.deleted_at).not_to be_nil + end + + it "doesn't delete the only variant of the product" do + product = create(:product) + variant = product.variants.first + spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json + + expect(variant.reload).to_not be_deleted + expect(assigns(:variant).errors[:product]).to include "must have at least one variant" + end + + context 'when the variant is not the master' do + before { variant.update_attribute(:is_master, false) } + + it 'refreshes the cache' do + expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant) + spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json + end + end + + context "deleted variants" do + before do + variant.update_column(:deleted_at, Time.zone.now) + end + + it "are visible by admin" do + api_get :index, show_deleted: 1 + + expect(json_response.count).to eq(2) + end + end + + it "can create a new variant" do + original_number_of_variants = variant.product.variants.count + api_post :create, variant: { sku: "12345", unit_value: "weight", unit_description: "L" } + + expect(attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true) + expect(response.status).to eq(201) + expect(json_response["sku"]).to eq("12345") + expect(variant.product.variants.count).to eq(original_number_of_variants + 1) + end + + it "can update a variant" do + api_put :update, id: variant.to_param, variant: { sku: "12345" } + + expect(response.status).to eq(200) + end + + it "can delete a variant" do + api_delete :destroy, id: variant.to_param + + expect(response.status).to eq(204) + expect { Spree::Variant.find(variant.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/controllers/spree/api/products_controller_spec.rb b/spec/controllers/spree/api/products_controller_spec.rb deleted file mode 100644 index c726487052..0000000000 --- a/spec/controllers/spree/api/products_controller_spec.rb +++ /dev/null @@ -1,361 +0,0 @@ -require 'spec_helper' - -module Spree - describe Spree::Api::ProductsController, type: :controller do - render_views - - let(:supplier) { create(:supplier_enterprise) } - let(:supplier2) { create(:supplier_enterprise) } - let!(:product) { create(:product, supplier: supplier) } - let!(:inactive_product) { create(:product, available_on: Time.zone.now.tomorrow, name: "inactive") } - let(:product_other_supplier) { create(:product, supplier: supplier2) } - let(:product_with_image) { create(:product_with_image, supplier: supplier) } - let(:attributes) { ["id", "name", "supplier", "price", "on_hand", "available_on", "permalink_live"] } - let(:all_attributes) { ["id", "name", "description", "price", "available_on", "permalink", "meta_description", "meta_keywords", "shipping_category_id", "taxon_ids", "variants", "option_types", "product_properties"] } - - let(:current_api_user) { build(:user) } - - before do - allow(controller).to receive(:spree_current_user) { current_api_user } - end - - context "as a normal user" do - before do - allow(current_api_user) - .to receive(:has_spree_role?).with("admin").and_return(false) - end - - it "should deny me access to managed products" do - spree_get :managed, template: 'bulk_index', format: :json - assert_unauthorized! - end - - it "retrieves a list of products" do - api_get :index - expect(json_response["products"].first).to have_attributes(keys: all_attributes) - - expect(json_response["count"]).to eq(1) - expect(json_response["current_page"]).to eq(1) - expect(json_response["pages"]).to eq(1) - end - - it "retrieves a list of products by id" do - api_get :index, ids: [product.id] - expect(json_response["products"].first).to have_attributes(keys: all_attributes) - expect(json_response["count"]).to eq(1) - expect(json_response["current_page"]).to eq(1) - expect(json_response["pages"]).to eq(1) - end - - it "does not return inactive products when queried by ids" do - api_get :index, ids: [inactive_product.id] - expect(json_response["count"]).to eq(0) - end - - it "does not list unavailable products" do - api_get :index - expect(json_response["products"].first["name"]).not_to eq("inactive") - end - - context "pagination" do - it "can select the next page of products" do - second_product = create(:product) - api_get :index, page: 2, per_page: 1 - expect(json_response["products"].first).to have_attributes(keys: all_attributes) - expect(json_response["total_count"]).to eq(2) - expect(json_response["current_page"]).to eq(2) - expect(json_response["pages"]).to eq(2) - end - - it 'can control the page size through a parameter' do - create(:product) - api_get :index, per_page: 1 - expect(json_response['count']).to eq(1) - expect(json_response['total_count']).to eq(2) - expect(json_response['current_page']).to eq(1) - expect(json_response['pages']).to eq(2) - end - end - - context "jsonp" do - it "retrieves a list of products of jsonp" do - api_get :index, callback: 'callback' - expect(response.body).to match(/^callback\(.*\)$/) - expect(response.header['Content-Type']).to include('application/javascript') - end - end - - it "can search for products" do - create(:product, name: "The best product in the world") - api_get :index, q: { name_cont: "best" } - expect(json_response["products"].first).to have_attributes(keys: all_attributes) - expect(json_response["count"]).to eq(1) - end - - it "gets a single product" do - product.master.images.create!(attachment: image("thinking-cat.jpg")) - product.variants.create!(unit_value: "1", unit_description: "thing") - product.variants.first.images.create!(attachment: image("thinking-cat.jpg")) - product.set_property("spree", "rocks") - api_get :show, id: product.to_param - expect(json_response).to have_attributes(keys: all_attributes) - expect(json_response['variants'].first).to have_attributes(keys: ["id", "name", "sku", "price", "weight", "height", "width", "depth", "is_master", "cost_price", "permalink", "option_values", "images"]) - expect(json_response['variants'].first['images'].first).to have_attributes(keys: ["id", "position", "attachment_content_type", "attachment_file_name", "type", "attachment_updated_at", "attachment_width", "attachment_height", "alt", "viewable_type", "viewable_id", "attachment_url"]) - expect(json_response["product_properties"].first).to have_attributes(keys: ["id", "product_id", "property_id", "value", "property_name"]) - end - - context "finds a product by permalink first then by id" do - let!(:other_product) { create(:product, permalink: "these-are-not-the-droids-you-are-looking-for") } - - before do - product.update_attribute(:permalink, "#{other_product.id}-and-1-ways") - end - - specify do - api_get :show, id: product.to_param - expect(json_response["permalink"]).to match(/and-1-ways/) - product.destroy - - api_get :show, id: other_product.id - expect(json_response["permalink"]).to match(/droids/) - end - end - - it "cannot see inactive products" do - api_get :show, id: inactive_product.to_param - expect(json_response["error"]).to eq("The resource you were looking for could not be found.") - expect(response.status).to eq(404) - end - - it "returns a 404 error when it cannot find a product" do - api_get :show, id: "non-existant" - expect(json_response["error"]).to eq("The resource you were looking for could not be found.") - expect(response.status).to eq(404) - end - - it "can learn how to create a new product" do - api_get :new - expect(json_response["attributes"]).to eq(["id", "name", "description", "price", "available_on", "permalink", "meta_description", "meta_keywords", "shipping_category_id", "taxon_ids"]) - required_attributes = json_response["required_attributes"] - expect(required_attributes).to include("name") - expect(required_attributes).to include("price") - expect(required_attributes).to include("shipping_category_id") - end - - include_examples "modifying product actions are restricted" - end - - context "as an enterprise user" do - let(:current_api_user) do - user = create(:user) - user.enterprise_roles.create(enterprise: supplier) - user - end - - it "retrieves a list of managed products" do - spree_get :managed, template: 'bulk_index', format: :json - response_keys = json_response.first.keys - expect(attributes.all?{ |attr| response_keys.include? attr }).to eq(true) - end - - it "soft deletes my products" do - spree_delete :soft_delete, product_id: product.to_param, format: :json - expect(response.status).to eq(204) - expect { product.reload }.not_to raise_error - expect(product.deleted_at).not_to be_nil - end - - it "is denied access to soft deleting another enterprises' product" do - spree_delete :soft_delete, product_id: product_other_supplier.to_param, format: :json - assert_unauthorized! - expect { product_other_supplier.reload }.not_to raise_error - expect(product_other_supplier.deleted_at).to be_nil - end - end - - context "as an administrator" do - before do - allow(current_api_user) - .to receive(:has_spree_role?).with("admin").and_return(true) - end - - it "retrieves a list of managed products" do - spree_get :managed, template: 'bulk_index', format: :json - response_keys = json_response.first.keys - expect(attributes.all?{ |attr| response_keys.include? attr }).to eq(true) - end - - it "retrieves a list of products with appropriate attributes" do - spree_get :index, template: 'bulk_index', format: :json - response_keys = json_response.first.keys - expect(attributes.all?{ |attr| response_keys.include? attr }).to eq(true) - end - - it "sorts products in ascending id order" do - FactoryBot.create(:product, supplier: supplier) - FactoryBot.create(:product, supplier: supplier) - - spree_get :index, template: 'bulk_index', format: :json - - ids = json_response.map{ |product| product['id'] } - expect(ids[0]).to be < ids[1] - expect(ids[1]).to be < ids[2] - end - - it "formats available_on to 'yyyy-mm-dd hh:mm'" do - spree_get :index, template: 'bulk_index', format: :json - expect(json_response.map{ |product| product['available_on'] }.all?{ |a| a.match("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$") }).to eq(true) - end - - it "returns permalink as permalink_live" do - spree_get :index, template: 'bulk_index', format: :json - expect(json_response.detect{ |product_in_response| product_in_response['id'] == product.id }['permalink_live']).to eq(product.permalink) - end - - it "should allow available_on to be nil" do - spree_get :index, template: 'bulk_index', format: :json - expect(json_response.size).to eq(2) - - another_product = FactoryBot.create(:product) - another_product.available_on = nil - another_product.save! - - spree_get :index, template: 'bulk_index', format: :json - expect(json_response.size).to eq(3) - end - - it "soft deletes a product" do - spree_delete :soft_delete, product_id: product.to_param, format: :json - expect(response.status).to eq(204) - expect { product.reload }.not_to raise_error - expect(product.deleted_at).not_to be_nil - end - - it "can see all products" do - api_get :index - expect(json_response["products"].count).to eq(2) - expect(json_response["count"]).to eq(2) - expect(json_response["current_page"]).to eq(1) - expect(json_response["pages"]).to eq(1) - end - - # Regression test for #1626 - context "deleted products" do - before do - create(:product, deleted_at: 1.day.ago) - end - - it "does not include deleted products" do - api_get :index - expect(json_response["products"].count).to eq(2) - end - - it "can include deleted products" do - api_get :index, show_deleted: 1 - expect(json_response["products"].count).to eq(3) - end - end - - it "can create a new product" do - api_post :create, product: { name: "The Other Product", - price: 19.99, - shipping_category_id: create(:shipping_category).id, - supplier_id: supplier.id, - primary_taxon_id: FactoryBot.create(:taxon).id, - variant_unit: "items", - variant_unit_name: "things", - unit_description: "things" } - expect(json_response).to have_attributes(keys: all_attributes) - expect(response.status).to eq(201) - end - - it "cannot create a new product with invalid attributes" do - api_post :create, product: {} - expect(response.status).to eq(422) - expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") - errors = json_response["errors"] - expect(errors.keys).to match_array(["name", "price", "primary_taxon", "shipping_category_id", "supplier", "variant_unit"]) - end - - it "can update a product" do - api_put :update, id: product.to_param, product: { name: "New and Improved Product!" } - expect(response.status).to eq(200) - end - - it "cannot update a product with an invalid attribute" do - api_put :update, id: product.to_param, product: { name: "" } - expect(response.status).to eq(422) - expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") - expect(json_response["errors"]["name"]).to eq(["can't be blank"]) - end - - it "can delete a product" do - expect(product.deleted_at).to be_nil - api_delete :destroy, id: product.to_param - expect(response.status).to eq(204) - expect(product.reload.deleted_at).not_to be_nil - end - end - - describe '#clone' do - context 'as a normal user' do - before do - allow(current_api_user) - .to receive(:has_spree_role?).with("admin").and_return(false) - end - - it 'denies access' do - spree_post :clone, product_id: product.id, format: :json - assert_unauthorized! - end - end - - context 'as an enterprise user' do - let(:current_api_user) do - user = create(:user) - user.enterprise_roles.create(enterprise: supplier) - user - end - - it 'responds with a successful response' do - spree_post :clone, product_id: product.id, format: :json - expect(response.status).to eq(201) - end - - it 'clones the product' do - spree_post :clone, product_id: product.id, format: :json - expect(json_response['name']).to eq("COPY OF #{product.name}") - end - - it 'clones a product with image' do - spree_post :clone, product_id: product_with_image.id, format: :json - expect(response.status).to eq(201) - expect(json_response['name']).to eq("COPY OF #{product_with_image.name}") - end - end - - context 'as an administrator' do - before do - allow(current_api_user) - .to receive(:has_spree_role?).with("admin").and_return(true) - end - - it 'responds with a successful response' do - spree_post :clone, product_id: product.id, format: :json - expect(response.status).to eq(201) - end - - it 'clones the product' do - spree_post :clone, product_id: product.id, format: :json - expect(json_response['name']).to eq("COPY OF #{product.name}") - end - - it 'clones a product with image' do - spree_post :clone, product_id: product_with_image.id, format: :json - expect(response.status).to eq(201) - expect(json_response['name']).to eq("COPY OF #{product_with_image.name}") - end - end - end - end -end diff --git a/spec/controllers/spree/api/variants_controller_spec.rb b/spec/controllers/spree/api/variants_controller_spec.rb deleted file mode 100644 index e32f34809a..0000000000 --- a/spec/controllers/spree/api/variants_controller_spec.rb +++ /dev/null @@ -1,273 +0,0 @@ -require 'spec_helper' - -module Spree - describe Spree::Api::VariantsController, type: :controller do - render_views - - let(:supplier) { FactoryBot.create(:supplier_enterprise) } - let!(:variant1) { FactoryBot.create(:variant) } - let!(:variant2) { FactoryBot.create(:variant) } - let!(:variant3) { FactoryBot.create(:variant) } - let(:attributes) { [:id, :options_text, :price, :on_hand, :unit_value, :unit_description, :on_demand, :display_as, :display_name] } - let!(:standard_attributes) { - [:id, :name, :sku, :price, :weight, :height, - :width, :depth, :is_master, :cost_price, :permalink] - } - - before do - allow(controller).to receive(:spree_current_user) { current_api_user } - end - - context "as a normal user" do - sign_in_as_user! - - let!(:product) { create(:product) } - let!(:variant) do - variant = product.master - variant.option_values << create(:option_value) - variant - end - - it "retrieves a list of variants with appropriate attributes" do - spree_get :index, template: 'bulk_index', format: :json - - keys = json_response.first.keys.map(&:to_sym) - expect(attributes.all?{ |attr| keys.include? attr }).to eq(true) - end - - it "is denied access when trying to delete a variant" do - product = create(:product) - variant = product.master - spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json - - assert_unauthorized! - expect { variant.reload }.not_to raise_error - expect(variant.deleted_at).to be_nil - end - - it "can see a paginated list of variants" do - api_get :index - - keys = json_response["variants"].first.keys.map(&:to_sym) - expect(standard_attributes.all?{ |attr| keys.include? attr }).to eq(true) - expect(json_response["count"]).to eq(11) - expect(json_response["current_page"]).to eq(1) - expect(json_response["pages"]).to eq(1) - end - - it 'can control the page size through a parameter' do - create(:variant) - api_get :index, per_page: 1 - - expect(json_response['count']).to eq(1) - expect(json_response['current_page']).to eq(1) - expect(json_response['pages']).to eq(14) - end - - it 'can query the results through a paramter' do - expected_result = create(:variant, sku: 'FOOBAR') - api_get :index, q: { sku_cont: 'FOO' } - - expect(json_response['count']).to eq(1) - expect(json_response['variants'].first['sku']).to eq expected_result.sku - end - - it "variants returned contain option values data" do - api_get :index - - option_values = json_response["variants"].last["option_values"] - expect(option_values.first).to have_attributes(keys: ["id", - "name", - "presentation", - "option_type_name", - "option_type_id"]) - end - - it "variants returned contain images data" do - variant.images.create!(attachment: image("thinking-cat.jpg")) - - api_get :index - - expect(json_response["variants"].last["images"]).not_to be_nil - end - - # Regression test for spree#2141 - context "a deleted variant" do - before do - variant.update_column(:deleted_at, Time.zone.now) - end - - it "is not returned in the results" do - api_get :index - expect(json_response["variants"].count).to eq(10) # there are 11 variants - end - - it "is not returned even when show_deleted is passed" do - api_get :index, show_deleted: true - expect(json_response["variants"].count).to eq(10) # there are 11 variants - end - end - - context "pagination" do - it "can select the next page of variants" do - second_variant = create(:variant) - api_get :index, page: 2, per_page: 1 - - keys = json_response["variants"].first.keys.map(&:to_sym) - expect(standard_attributes.all?{ |attr| keys.include? attr }).to eq(true) - expect(json_response["total_count"]).to eq(14) - expect(json_response["current_page"]).to eq(2) - expect(json_response["pages"]).to eq(14) - end - end - - it "can see a single variant" do - api_get :show, id: variant.to_param - - keys = json_response.keys.map(&:to_sym) - expect((standard_attributes + [:options_text, :option_values, :images]).all?{ |attr| keys.include? attr }).to eq(true) - option_values = json_response["option_values"] - expect(option_values.first).to have_attributes(keys: ["id", "name", "presentation", "option_type_name", "option_type_id"]) - end - - it "can see a single variant with images" do - variant.images.create!(attachment: image("thinking-cat.jpg")) - api_get :show, id: variant.to_param - - keys = json_response.keys.map(&:to_sym) - expect((standard_attributes + [:images]).all?{ |attr| keys.include? attr }).to eq(true) - option_values_keys = json_response["option_values"].first.keys.map(&:to_sym) - expect([:name, :presentation, :option_type_id].all?{ |attr| option_values_keys.include? attr }).to eq(true) - end - - it "can learn how to create a new variant" do - api_get :new - - expect(json_response["attributes"]).to eq(standard_attributes.map(&:to_s)) - expect(json_response["required_attributes"]).to be_empty - end - - it "cannot create a new variant if not an admin" do - api_post :create, variant: { sku: "12345" } - - assert_unauthorized! - end - - it "cannot update a variant" do - api_put :update, id: variant.to_param, variant: { sku: "12345" } - - assert_unauthorized! - end - - it "cannot delete a variant" do - api_delete :destroy, id: variant.to_param - - assert_unauthorized! - expect { variant.reload }.not_to raise_error - end - end - - context "as an enterprise user" do - sign_in_as_enterprise_user! [:supplier] - let(:supplier_other) { create(:supplier_enterprise) } - let(:product) { create(:product, supplier: supplier) } - let(:variant) { product.master } - let(:product_other) { create(:product, supplier: supplier_other) } - let(:variant_other) { product_other.master } - - it "soft deletes a variant" do - spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json - - expect(response.status).to eq(204) - expect { variant.reload }.not_to raise_error - expect(variant.deleted_at).to be_present - end - - it "is denied access to soft deleting another enterprises' variant" do - spree_delete :soft_delete, variant_id: variant_other.to_param, product_id: product_other.to_param, format: :json - - assert_unauthorized! - expect { variant.reload }.not_to raise_error - expect(variant.deleted_at).to be_nil - end - - context 'when the variant is not the master' do - before { variant.update_attribute(:is_master, false) } - - it 'refreshes the cache' do - expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant) - spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json - end - end - end - - context "as an administrator" do - sign_in_as_admin! - - let(:product) { create(:product) } - let(:variant) { product.master } - let(:resource_scoping) { { product_id: variant.product.to_param } } - - it "soft deletes a variant" do - spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json - - expect(response.status).to eq(204) - expect { variant.reload }.not_to raise_error - expect(variant.deleted_at).not_to be_nil - end - - it "doesn't delete the only variant of the product" do - product = create(:product) - variant = product.variants.first - spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json - - expect(variant.reload).to_not be_deleted - expect(assigns(:variant).errors[:product]).to include "must have at least one variant" - end - - context 'when the variant is not the master' do - before { variant.update_attribute(:is_master, false) } - - it 'refreshes the cache' do - expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant) - spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json - end - end - - context "deleted variants" do - before do - variant.update_column(:deleted_at, Time.zone.now) - end - - it "are visible by admin" do - api_get :index, show_deleted: 1 - - expect(json_response["variants"].count).to eq(2) - end - end - - it "can create a new variant" do - original_number_of_variants = variant.product.variants.count - api_post :create, variant: { sku: "12345", unit_value: "weight", unit_description: "L" } - - expect(standard_attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true) - expect(response.status).to eq(201) - expect(json_response["sku"]).to eq("12345") - expect(variant.product.variants.count).to eq(original_number_of_variants + 1) - end - - it "can update a variant" do - api_put :update, id: variant.to_param, variant: { sku: "12345" } - - expect(response.status).to eq(200) - end - - it "can delete a variant" do - api_delete :destroy, id: variant.to_param - - expect(response.status).to eq(204) - expect { Spree::Variant.find(variant.id) }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end