From 751e166a2987bbb96d84275274df49148fb09442 Mon Sep 17 00:00:00 2001 From: Karinna Iniguez Date: Tue, 19 Jun 2018 10:25:13 -0700 Subject: [PATCH 1/5] Add create method to movies controller and route for it --- Gemfile.lock | 2 +- app/controllers/movies_controller.rb | 31 ++++++++++++++++++++++++++++ config/routes.rb | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9f7601a8..22779079 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -118,7 +118,7 @@ GEM slop (~> 3.4) pry-rails (0.3.4) pry (>= 0.9.10) - puma (3.6.2) + puma (3.11.4) rack (2.0.1) rack-test (0.6.3) rack (>= 1.0) diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb index 362e2791..f4aa54c4 100644 --- a/app/controllers/movies_controller.rb +++ b/app/controllers/movies_controller.rb @@ -17,8 +17,34 @@ def show json: @movie.as_json( only: [:title, :overview, :release_date, :inventory], methods: [:available_inventory] + ) + ) + end + + def create + @movie = Movie.new(movie_params) + @movie.inventory = rand(0..10) + + if @movie.save + render( + status: :ok, + json: @movie.as_json( + only: [:title, :overview, :release_date, :inventory], + methods: [:available_inventory] ) ) + + else + render( + status: :bad_request, + json: { + errors: { + movie: ["Movie #{@movie.title} could not be added to our database."] + } + } + ) + + end end private @@ -29,4 +55,9 @@ def require_movie render status: :not_found, json: { errors: { title: ["No movie with title #{params["title"]}"] } } end end + + def movie_params + return params.permit(:title, :overview, :release_date, :image_url, :external_id) + + end end diff --git a/config/routes.rb b/config/routes.rb index 54bf033e..95c816fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,6 +4,7 @@ resources :customers, only: [:index] resources :movies, only: [:index, :show], param: :title + post '/movies', to: 'movies#create', as: "new_movie" post "/rentals/:title/check-out", to: "rentals#check_out", as: "check_out" post "/rentals/:title/return", to: "rentals#check_in", as: "check_in" From e11a63439eaedc40669405ec3e345cb51368792e Mon Sep 17 00:00:00 2001 From: Caroline Nardi Date: Tue, 19 Jun 2018 10:26:44 -0700 Subject: [PATCH 2/5] Don't think I changed anything --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9f7601a8..22779079 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -118,7 +118,7 @@ GEM slop (~> 3.4) pry-rails (0.3.4) pry (>= 0.9.10) - puma (3.6.2) + puma (3.11.4) rack (2.0.1) rack-test (0.6.3) rack (>= 1.0) From 8b29c4c189bd83f006e0d36e57a05dca983eca28 Mon Sep 17 00:00:00 2001 From: Karinna Iniguez Date: Wed, 20 Jun 2018 15:53:22 -0700 Subject: [PATCH 3/5] Add rental and movie tests and pass with validations --- app/models/movie.rb | 5 +- app/models/rental.rb | 43 +++++++- test/models/movie_test.rb | 42 +++++++- test/models/rental_test.rb | 197 +++++++++++++++++++++++-------------- 4 files changed, 208 insertions(+), 79 deletions(-) diff --git a/app/models/movie.rb b/app/models/movie.rb index 0016080b..dea89b52 100644 --- a/app/models/movie.rb +++ b/app/models/movie.rb @@ -2,8 +2,11 @@ class Movie < ApplicationRecord has_many :rentals has_many :customers, through: :rentals + validates :external_id, uniqueness: true + validates :title, presence: true + def available_inventory - self.inventory - Rental.where(movie: self, returned: false).length + self.inventory - Rental.where(movie: self, returned: nil).length end def image_url diff --git a/app/models/rental.rb b/app/models/rental.rb index fbdb716b..aebc6e27 100644 --- a/app/models/rental.rb +++ b/app/models/rental.rb @@ -1,3 +1,34 @@ +class RentalValidator < ActiveModel::Validator + + def validate(record) + + if duplicate_rental(record) + record.errors.add(:base, "Customer has not returned this movie yet.") + end + + if no_availability(record) + record.errors.add(:base, "No copies of this movie are currently available") + end + + end + + private + + def duplicate_rental(record) + active_previous_rental = Rental.find_by(customer_id: record.customer_id, movie_id: record.movie_id, returned: nil) + return active_previous_rental + end + + def no_availability(record) + return record.movie.available_inventory == 0 + end + +end + + +#################################################################### + + class Rental < ApplicationRecord belongs_to :movie belongs_to :customer @@ -5,12 +36,14 @@ class Rental < ApplicationRecord # validates :movie, uniqueness: { scope: :customer } validates :due_date, presence: true validate :due_date_in_future, on: :create + validates_with RentalValidator, on: :create after_initialize :set_checkout_date + after_initialize :set_due_date - def self.first_outstanding(movie, customer) - self.where(movie: movie, customer: customer, returned: false).order(:due_date).first - end + # def self.first_outstanding(movie, customer) + # self.where(movie: movie, customer: customer, returned: false).order(:due_date).first + # end def self.overdue self.where(returned: false).where("due_date < ?", Date.today) @@ -27,4 +60,8 @@ def due_date_in_future def set_checkout_date self.checkout_date ||= Date.today end + + def set_due_date + self.due_date ||= self.checkout_date + 7 + end end diff --git a/test/models/movie_test.rb b/test/models/movie_test.rb index 70b6a7c6..fee05bfe 100644 --- a/test/models/movie_test.rb +++ b/test/models/movie_test.rb @@ -16,7 +16,7 @@ class MovieTest < ActiveSupport::TestCase describe "Constructor" do it "Can be created" do - Movie.create!(movie_data) + Movie.create!(title: "Dumbo", external_id: 900) end it "Has rentals" do @@ -47,7 +47,7 @@ class MovieTest < ActiveSupport::TestCase customer: customers(:one), movie: movie, due_date: Date.today + 7, - returned: false + returned: nil ) movie.reload @@ -64,7 +64,7 @@ class MovieTest < ActiveSupport::TestCase customer: customers(:one), movie: movie, due_date: Date.today + 7, - returned: false + returned: nil ) movie.reload @@ -78,4 +78,40 @@ class MovieTest < ActiveSupport::TestCase after_ai.must_equal before_ai + 1 end end + + describe "validations" do + it "cannot be created without a title" do + old_count = Movie.count + movie = Movie.new + movie.valid?.must_equal false + + movie.save + movie.errors.messages.must_include :title + + Movie.count.must_equal old_count + end + + it "can successfully be created with title" do + title = "some title" + old_count = Movie.count + movie = Movie.new(title: "300", external_id: 867) + movie.valid?.must_equal true + + movie.save + + Movie.count.must_equal old_count + 1 + end + + it "cannot be created with duplicate external id" do + reused_id = 123 + Movie.create!(title: "something", external_id: reused_id) + old_count = Movie.count + + new_movie = Movie.new(title: "whatever", external_id: reused_id) + new_movie.valid?.must_equal false + new_movie.save + new_movie.errors.messages.must_include :external_id + Movie.count.must_equal old_count + end + end end diff --git a/test/models/rental_test.rb b/test/models/rental_test.rb index 3a83449c..0e00f432 100644 --- a/test/models/rental_test.rb +++ b/test/models/rental_test.rb @@ -16,6 +16,7 @@ class RentalTest < ActiveSupport::TestCase describe "Constructor" do it "Has a constructor" do + Rental.destroy_all Rental.create!(rental_data) end @@ -26,9 +27,9 @@ class RentalTest < ActiveSupport::TestCase it "Cannot be created without a customer" do data = rental_data.clone() data.delete :customer - c = Rental.new(data) - c.valid?.must_equal false - c.errors.messages.must_include :customer + proc{ + Rental.create!(data) + }.must_raise end it "Has a movie" do @@ -38,10 +39,61 @@ class RentalTest < ActiveSupport::TestCase it "Cannot be created without a movie" do data = rental_data.clone data.delete :movie - c = Rental.new(data) - c.valid?.must_equal false - c.errors.messages.must_include :movie + proc{ + Rental.create!(data) + }.must_raise end + + it "Cannot be created if movie is already checked out by customer" do + Rental.destroy_all + + rental1 = Rental.create!(customer: customers(:one), movie: movies(:one)) + rental2 = Rental.new(customer: customers(:one), movie: movies(:one)) + rental2.valid?.must_equal false + rental2.save + + Rental.count.must_equal 1 + end + + it "Can be created if customer has returned movie before" do + + Rental.destroy_all + + rental1 = Rental.create!(customer: customers(:one), movie: movies(:one)) + + rental1.returned = true + rental1.save + rental1.returned.must_equal true + + rental2 = Rental.new(customer: customers(:one), movie: movies(:one)) + rental2.valid?.must_equal true + rental2.save + Rental.count.must_equal 2 + end + + it "Cannot be checked out if no available copies" do + old_count = Rental.count + movie = Movie.create!(title: "testing movie", external_id: 200, inventory: 0) + + rental = Rental.new(customer: Customer.last, movie: movie) + rental.valid?.must_equal false + rental.save + Rental.count.must_equal old_count + movie.inventory.must_equal 0 + end + + it "Can check out a movie if at least one available" do + old_count = Rental.count + movie = Movie.create!(title: "testing movie", external_id: 200, inventory: 1) + + rental = Rental.new(customer: Customer.last, movie: movie) + rental.valid?.must_equal true + rental.save + Rental.count.must_equal old_count + 1 + movie.reload + movie.available_inventory.must_equal 0 + end + end describe "due_date" do @@ -75,72 +127,73 @@ class RentalTest < ActiveSupport::TestCase end end - describe "first_outstanding" do - it "returns the only un-returned rental" do - Rental.count.must_equal 1 - Rental.first.returned.must_equal false - Rental.first_outstanding(Rental.first.movie, Rental.first.customer).must_equal Rental.first - end - - it "returns nil if no rentals are un-returned" do - Rental.all.each do |rental| - rental.returned = true - rental.save! - end - Rental.first_outstanding(Rental.first.movie, Rental.first.customer).must_be_nil - end - - it "prefers rentals with earlier due dates" do - # Start with a clean slate - Rental.destroy_all - - last = Rental.create!( - movie: movies(:one), - customer: customers(:one), - due_date: Date.today + 30, - returned: false - ) - first = Rental.create!( - movie: movies(:one), - customer: customers(:one), - due_date: Date.today + 10, - returned: false - ) - middle = Rental.create!( - movie: movies(:one), - customer: customers(:one), - due_date: Date.today + 20, - returned: false - ) - Rental.first_outstanding( - movies(:one), - customers(:one) - ).must_equal first - end - - it "ignores returned rentals" do - # Start with a clean slate - Rental.destroy_all - - returned = Rental.create!( - movie: movies(:one), - customer: customers(:one), - due_date: Date.today + 10, - returned: true - ) - outstanding = Rental.create!( - movie: movies(:one), - customer: customers(:one), - due_date: Date.today + 30, - returned: false - ) - - Rental.first_outstanding( - movies(:one), - customers(:one) - ).must_equal outstanding - end - end + # describe "first_outstanding" do + # it "returns the only un-returned rental" do + # Rental.count.must_equal 1 + # Rental.first.returned.must_equal false + # Rental.first_outstanding(Rental.first.movie, Rental.first.customer).must_equal Rental.first + # end + + # it "returns nil if no rentals are un-returned" do + # Rental.all.each do |rental| + # rental.returned = true + # rental.save! + # end + # Rental.first_outstanding(Rental.first.movie, Rental.first.customer).must_be_nil + # end + + # it "prefers rentals with earlier due dates" do + # # Start with a clean slate + # Rental.destroy_all + # + # last = Rental.create!( + # movie: movies(:one), + # customer: customers(:one), + # due_date: Date.today + 30, + # returned: false + # ) + # first = Rental.create!( + # movie: movies(:one), + # customer: customers(:one), + # due_date: Date.today + 10, + # returned: false + # ) + # middle = Rental.create!( + # movie: movies(:one), + # customer: customers(:one), + # due_date: Date.today + 20, + # returned: false + # ) + # Rental.first_outstanding( + # movies(:one), + # customers(:one) + # ).must_equal first + # end + + # it "ignores returned rentals" do + # Start with a clean slate + # Rental.destroy_all + # + # returned = Rental.create!( + # movie: movies(:one), + # customer: customers(:one), + # due_date: Date.today + 10, + # returned: true + # ) + # outstanding = Rental.create!( + # movie: movies(:one), + # customer: customers(:one), + # due_date: Date.today + 30, + # returned: false + # ) + # + # Rental.first_outstanding( + # movies(:one), + # customers(:one) + # ).must_equal outstanding + # + # end + # end describe "overdue" do it "returns all overdue rentals" do From 8928d6a8847b65e01c598374c8683eb6055740f2 Mon Sep 17 00:00:00 2001 From: Caroline Nardi Date: Wed, 20 Jun 2018 16:13:53 -0700 Subject: [PATCH 4/5] Fixed rentals controllers tests to account for new model validations --- app/controllers/rentals_controller.rb | 2 +- test/controllers/rentals_controller_test.rb | 102 ++++++++++---------- test/models/rental_test.rb | 1 - 3 files changed, 52 insertions(+), 53 deletions(-) diff --git a/app/controllers/rentals_controller.rb b/app/controllers/rentals_controller.rb index 67e77073..5505c3bc 100644 --- a/app/controllers/rentals_controller.rb +++ b/app/controllers/rentals_controller.rb @@ -14,7 +14,7 @@ def check_out end def check_in - rental = Rental.first_outstanding(@movie, @customer) + rental = Rental.find_by(movie: @movie, customer: @customer, returned: nil) unless rental return render status: :not_found, json: { errors: { diff --git a/test/controllers/rentals_controller_test.rb b/test/controllers/rentals_controller_test.rb index 8f669a38..63d86b48 100644 --- a/test/controllers/rentals_controller_test.rb +++ b/test/controllers/rentals_controller_test.rb @@ -75,7 +75,7 @@ class RentalsControllerTest < ActionDispatch::IntegrationTest customer: customers(:two), checkout_date: Date.today - 5, due_date: Date.today + 5, - returned: false + returned: nil ) end @@ -136,56 +136,56 @@ class RentalsControllerTest < ActionDispatch::IntegrationTest data["errors"].must_include "rental" end - it "if multiple rentals match, ignores returned ones" do - returned_rental = Rental.create!( - movie: @rental.movie, - customer: @rental.customer, - checkout_date: Date.today - 5, - due_date: @rental.due_date - 2, - returned: true - ) - - post check_in_url(title: @rental.movie.title), params: { - customer_id: @rental.customer.id - } - assert_response :success - - returned_rental.reload - @rental.reload - - @rental.returned.must_equal true - end - - it "returns the rental with the closest due_date" do - soon_rental = Rental.create!( - movie: @rental.movie, - customer: @rental.customer, - checkout_date: Date.today - 5, - due_date: @rental.due_date - 2, - returned: false - ) - - far_rental = Rental.create!( - movie: @rental.movie, - customer: @rental.customer, - checkout_date: Date.today - 5, - due_date: @rental.due_date + 10, - returned: false - ) - - post check_in_url(title: @rental.movie.title), params: { - customer_id: @rental.customer.id - } - assert_response :success - - soon_rental.reload - @rental.reload - far_rental.reload - - soon_rental.returned.must_equal true - @rental.returned.must_equal false - far_rental.returned.must_equal false - end + # it "if multiple rentals match, ignores returned ones" do + # returned_rental = Rental.create!( + # movie: @rental.movie, + # customer: @rental.customer, + # checkout_date: Date.today - 5, + # due_date: @rental.due_date - 2, + # returned: true + # ) + # + # post check_in_url(title: @rental.movie.title), params: { + # customer_id: @rental.customer.id + # } + # assert_response :success + # + # returned_rental.reload + # @rental.reload + # + # @rental.returned.must_equal true + # end + + # it "returns the rental with the closest due_date" do + # soon_rental = Rental.create!( + # movie: @rental.movie, + # customer: @rental.customer, + # checkout_date: Date.today - 5, + # due_date: @rental.due_date - 2, + # returned: false + # ) + # + # far_rental = Rental.create!( + # movie: @rental.movie, + # customer: @rental.customer, + # checkout_date: Date.today - 5, + # due_date: @rental.due_date + 10, + # returned: false + # ) + # + # post check_in_url(title: @rental.movie.title), params: { + # customer_id: @rental.customer.id + # } + # assert_response :success + # + # soon_rental.reload + # @rental.reload + # far_rental.reload + # + # soon_rental.returned.must_equal true + # @rental.returned.must_equal false + # far_rental.returned.must_equal false + # end end describe "overdue" do diff --git a/test/models/rental_test.rb b/test/models/rental_test.rb index 0e00f432..1c11e316 100644 --- a/test/models/rental_test.rb +++ b/test/models/rental_test.rb @@ -90,7 +90,6 @@ class RentalTest < ActiveSupport::TestCase rental.valid?.must_equal true rental.save Rental.count.must_equal old_count + 1 - movie.reload movie.available_inventory.must_equal 0 end From e2baf3da96702defe062cd57b153c503569e92a2 Mon Sep 17 00:00:00 2001 From: Caroline Nardi Date: Wed, 20 Jun 2018 16:56:38 -0700 Subject: [PATCH 5/5] Changed format of registration date in customer API response --- app/controllers/customers_controller.rb | 4 +-- app/controllers/rentals_controller.rb | 6 ++-- app/models/customer.rb | 4 +++ test/controllers/customers_controller_test.rb | 34 +++++++++---------- test/controllers/rentals_controller_test.rb | 8 ++--- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/app/controllers/customers_controller.rb b/app/controllers/customers_controller.rb index be25f1be..3eb8da18 100644 --- a/app/controllers/customers_controller.rb +++ b/app/controllers/customers_controller.rb @@ -13,8 +13,8 @@ def index data = data.paginate(page: params[:p], per_page: params[:n]) render json: data.as_json( - only: [:id, :name, :registered_at, :address, :city, :state, :postal_code, :phone, :account_credit], - methods: [:movies_checked_out_count] + only: [:id, :name, :address, :city, :state, :postal_code, :phone, :account_credit], + methods: [:movies_checked_out_count, :registration_date] ) end diff --git a/app/controllers/rentals_controller.rb b/app/controllers/rentals_controller.rb index 5505c3bc..5bb7ec6b 100644 --- a/app/controllers/rentals_controller.rb +++ b/app/controllers/rentals_controller.rb @@ -18,7 +18,7 @@ def check_in unless rental return render status: :not_found, json: { errors: { - rental: ["Customer #{@customer.id} does not have #{@movie.title} checked out"] + rental: ["#{@customer.name} does not have #{@movie.title} checked out"] } } end @@ -49,14 +49,14 @@ def overdue def require_movie @movie = Movie.find_by title: params[:title] unless @movie - render status: :not_found, json: { errors: { title: ["No movie with title #{params[:title]}"] } } + render status: :not_found, json: { errors: { rental: ["Movie not found"] } } end end def require_customer @customer = Customer.find_by id: params[:customer_id] unless @customer - render status: :not_found, json: { errors: { customer_id: ["No such customer #{params[:customer_id]}"] } } + render status: :not_found, json: { errors: { rental: ["Customer not found"] } } end end end diff --git a/app/models/customer.rb b/app/models/customer.rb index 6fc89447..d95210c5 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -5,4 +5,8 @@ class Customer < ApplicationRecord def movies_checked_out_count self.rentals.where(returned: false).length end + + def registration_date + self.registered_at.strftime("%B %d, %Y") + end end diff --git a/test/controllers/customers_controller_test.rb b/test/controllers/customers_controller_test.rb index 80486bf9..622e7c57 100644 --- a/test/controllers/customers_controller_test.rb +++ b/test/controllers/customers_controller_test.rb @@ -21,7 +21,7 @@ class CustomersControllerTest < ActionDispatch::IntegrationTest data.each do |customer| customer.must_include "id" customer.must_include "name" - customer.must_include "registered_at" + customer.must_include "registration_date" customer.must_include "postal_code" customer.must_include "phone" customer.must_include "account_credit" @@ -67,22 +67,22 @@ class CustomersControllerTest < ActionDispatch::IntegrationTest end end - it "can sort by registered_at" do - get customers_url, params: { sort: 'registered_at' } - assert_response :success - - data = JSON.parse @response.body - data.length.must_equal Customer.count - - # Verify sorted order - data.each_with_index do |customer, i| - if i + 1 >= data.length - break - end - - DateTime.parse(customer['registered_at']).must_be :<=, DateTime.parse(data[i+1]['registered_at']) - end - end + # it "can sort by registered_at" do + # get customers_url, params: { sort: 'registered_at' } + # assert_response :success + # + # data = JSON.parse @response.body + # data.length.must_equal Customer.count + # + # # Verify sorted order + # data.each_with_index do |customer, i| + # if i + 1 >= data.length + # break + # end + # + # DateTime.parse(customer['registered_at']).must_be :<=, DateTime.parse(data[i+1]['registered_at']) + # end + # end it "can sort by postal_code" do get customers_url, params: { sort: 'postal_code' } diff --git a/test/controllers/rentals_controller_test.rb b/test/controllers/rentals_controller_test.rb index 63d86b48..8735a788 100644 --- a/test/controllers/rentals_controller_test.rb +++ b/test/controllers/rentals_controller_test.rb @@ -37,7 +37,7 @@ class RentalsControllerTest < ActionDispatch::IntegrationTest assert_response :not_found data = JSON.parse @response.body data.must_include "errors" - data["errors"].must_include "title" + data["errors"].must_include "rental" end it "requires a valid customer ID" do @@ -51,7 +51,7 @@ class RentalsControllerTest < ActionDispatch::IntegrationTest assert_response :not_found data = JSON.parse @response.body data.must_include "errors" - data["errors"].must_include "customer_id" + data["errors"].must_include "rental" end it "requires a due-date in the future" do @@ -97,7 +97,7 @@ class RentalsControllerTest < ActionDispatch::IntegrationTest assert_response :not_found data = JSON.parse @response.body data.must_include "errors" - data["errors"].must_include "title" + data["errors"].must_include "rental" end it "requires a valid customer ID" do @@ -110,7 +110,7 @@ class RentalsControllerTest < ActionDispatch::IntegrationTest assert_response :not_found data = JSON.parse @response.body data.must_include "errors" - data["errors"].must_include "customer_id" + data["errors"].must_include "rental" end it "requires there to be a rental for that customer-movie pair" do