From 1e340ce845deeb040d24bb79c1f77cb497097bde Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 15:43:07 -0300 Subject: [PATCH 01/12] Feat: add 'food#total_price' model-method --- app/models/food.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/food.rb b/app/models/food.rb index 9108308..7e7af9a 100644 --- a/app/models/food.rb +++ b/app/models/food.rb @@ -2,4 +2,8 @@ class Food < ApplicationRecord belongs_to :user has_many :recipe_foods has_many :recipes, through: :recipe_foods + + def total_price + (price * quantity).round(2) + end end From ddef1330d21c9660dcdccc5fe06c54f8bd89dff8 Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 15:44:27 -0300 Subject: [PATCH 02/12] Feat: update 'foods' controller actions - Require user authentication for all actions. - Add 'current_user' to 'foods#create' action. - Replace notices with flash messages. - Remove 'user_id' from private 'foods#food_params'. --- app/controllers/foods_controller.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/foods_controller.rb b/app/controllers/foods_controller.rb index 2c6a3ac..489f50a 100644 --- a/app/controllers/foods_controller.rb +++ b/app/controllers/foods_controller.rb @@ -1,4 +1,5 @@ class FoodsController < ApplicationController + before_action :authenticate_user! before_action :set_food, only: %i[show edit update destroy] # GET /foods or /foods.json @@ -20,10 +21,12 @@ def edit; end # POST /foods or /foods.json def create @food = Food.new(food_params) + @food.user = current_user respond_to do |format| if @food.save - format.html { redirect_to food_url(@food), notice: 'Food was successfully created.' } + flash[:success] = 'Food was successfully created.' + format.html { redirect_to foods_path } format.json { render :show, status: :created, location: @food } else format.html { render :new, status: :unprocessable_entity } @@ -50,7 +53,8 @@ def destroy @food.destroy! respond_to do |format| - format.html { redirect_to foods_url, notice: 'Food was successfully destroyed.' } + flash[:success] = 'Food was successfully destroyed.' + format.html { redirect_to foods_url } format.json { head :no_content } end end @@ -64,6 +68,6 @@ def set_food # Only allow a list of trusted parameters through. def food_params - params.require(:food).permit(:name, :measurement_unit, :price, :quantity, :user_id) + params.require(:food).permit(:name, :measurement_unit, :price, :quantity) end end From 453a5d7b1181b23a10002eb92b85e102bd0dba65 Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 15:47:37 -0300 Subject: [PATCH 03/12] Feat: update 'application.html.erb' to include flash messages - Also wrap yield in a div with class 'container'. --- app/views/layouts/application.html.erb | 39 +++++++++++++++----------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e01706b..9ae51f7 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -19,23 +19,30 @@ -
- <% if user_signed_in? %> - <%= button_to("Sign Out", destroy_user_session_path, - method: :delete, - class: "btn btn-outline-success px-2 py-1", - style: "font-size: 0.9rem;") %> - <% else %> - <%= button_to("Sign in", new_user_session_path, - method: :get, - class: "btn btn-outline-success px-2 py-1", - style: "font-size: 0.9rem;") %> - <% end %> -
+
+ <% if user_signed_in? %> + <%= button_to("Sign Out", destroy_user_session_path, + method: :delete, + class: "btn btn-outline-success px-2 py-1", + style: "font-size: 0.9rem;") %> + <% else %> + <%= button_to("Sign in", new_user_session_path, + method: :get, + class: "btn btn-outline-success px-2 py-1", + style: "font-size: 0.9rem;") %> + <% end %> +
-

<%= notice %>

-

<%= alert %>

+
+ <% flash.each do |key, message| %> +
+ <%= message %> +
+ <% end %> +
- <%= yield %> +
+ <%= yield %> +
From 907273ce6e7bc255ab2d8762ee97262d01947a81 Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 15:48:37 -0300 Subject: [PATCH 04/12] Feat: Update some of the food views - _food partial. - _form partial. - index view. - new view. --- app/views/foods/_food.html.erb | 36 ++++++++---------------------- app/views/foods/_form.html.erb | 39 +++++++++++++++++++-------------- app/views/foods/index.html.erb | 40 ++++++++++++++++++++++++---------- app/views/foods/new.html.erb | 14 +++++++----- 4 files changed, 69 insertions(+), 60 deletions(-) diff --git a/app/views/foods/_food.html.erb b/app/views/foods/_food.html.erb index 7f7d637..0b5fb8f 100644 --- a/app/views/foods/_food.html.erb +++ b/app/views/foods/_food.html.erb @@ -1,27 +1,9 @@ -
-

- Name: - <%= food.name %> -

- -

- Measurement unit: - <%= food.measurement_unit %> -

- -

- Price: - <%= food.price %> -

- -

- Quantity: - <%= food.quantity %> -

- -

- User: - <%= food.user_id %> -

- -
+ + <%= food.name %> + <%= food.measurement_unit %> + <%= "$#{food.price}" %> + <%= food.quantity %> + <%= button_to "Delete", food, + method: :delete, + class: "btn btn-outline-danger px-2 py-1" %> + \ No newline at end of file diff --git a/app/views/foods/_form.html.erb b/app/views/foods/_form.html.erb index c59e73b..f221d18 100644 --- a/app/views/foods/_form.html.erb +++ b/app/views/foods/_form.html.erb @@ -11,32 +11,39 @@ <% end %> -
- <%= form.label :name, style: "display: block" %> - <%= form.text_field :name %> +
+ <%= form.text_field :name, + placeholder: "Name", + aria: { label: "Food Name" }, + class: "form-control mb-2" %>
-
- <%= form.label :measurement_unit, style: "display: block" %> - <%= form.text_field :measurement_unit %> +
+ <%= form.text_field :measurement_unit, + placeholder: "Measurement unit", + aria: { label: "Measurement unit" }, + class: "form-control mb-2" %>
-
- <%= form.label :price, style: "display: block" %> - <%= form.text_field :price %> +
+ <%= form.text_field :price, + placeholder: "Unit price", + aria: { label: "Unit price" }, + class: "form-control mb-2" %>
-
- <%= form.label :quantity, style: "display: block" %> - <%= form.number_field :quantity %> +
+ <%= form.number_field :quantity, + placeholder: "Quantity", + aria: { label: "Quantity" }, + class: "form-control mb-2" %>
- <%= form.label :user_id, style: "display: block" %> - <%= form.text_field :user_id %> + <%= form.hidden_field :user_id, value: current_user.id %>
-
- <%= form.submit %> +
+ <%= form.submit "Save", class: "btn btn-success w-100" %>
<% end %> diff --git a/app/views/foods/index.html.erb b/app/views/foods/index.html.erb index c028cee..18f5c86 100644 --- a/app/views/foods/index.html.erb +++ b/app/views/foods/index.html.erb @@ -1,14 +1,32 @@ -

<%= notice %>

+
-

Foods

+
+
+ <%= link_to "New food", new_food_path, class: "btn btn-outline-primary m-1" %> +
+ <% if @foods.empty? %> +
+

There are no foods in your inventory.

+
+ <% else %> + + + + + + + + + + -
- <% @foods.each do |food| %> - <%= render food %> -

- <%= link_to "Show this food", food %> -

- <% end %> -
+ + <% @foods.each do |food| %> + <%= render food %> + <% end %> + +
FoodMeasurement unitUnit PriceQuantityActions
+ <% end %> +
+
-<%= link_to "New food", new_food_path %> diff --git a/app/views/foods/new.html.erb b/app/views/foods/new.html.erb index 84bc27f..7370375 100644 --- a/app/views/foods/new.html.erb +++ b/app/views/foods/new.html.erb @@ -1,9 +1,11 @@ -

New food

+

Add a new item to inventory

+
-<%= render "form", food: @food %> + <%= render "form", food: @food %> -
+
-
- <%= link_to "Back to foods", foods_path %> -
+
+ <%= link_to "Back to food list", foods_path, class: "btn btn-secondary px-2 py-1" %> +
+
\ No newline at end of file From f22c1f7db36166ba8653fa2b3bfdee887cf19d3e Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 18:49:28 -0300 Subject: [PATCH 05/12] Fix: foods_controller#index to fetch foods by user id --- app/controllers/foods_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/foods_controller.rb b/app/controllers/foods_controller.rb index 489f50a..09b26f0 100644 --- a/app/controllers/foods_controller.rb +++ b/app/controllers/foods_controller.rb @@ -4,7 +4,7 @@ class FoodsController < ApplicationController # GET /foods or /foods.json def index - @foods = Food.all + @foods = Food.where(user_id: current_user.id) end # GET /foods/1 or /foods/1.json From cca9ce748ad97a577fce08fe783c3ab90985880c Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 18:58:14 -0300 Subject: [PATCH 06/12] Feat: Add data validation to 'food' model --- app/models/food.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/food.rb b/app/models/food.rb index 7e7af9a..7176457 100644 --- a/app/models/food.rb +++ b/app/models/food.rb @@ -3,6 +3,11 @@ class Food < ApplicationRecord has_many :recipe_foods has_many :recipes, through: :recipe_foods + validates :name, presence: true + validates :price, presence: true, numericality: { greater_than: 0 } + validates :measurement_unit, presence: true + validates :quantity, presence: true, numericality: { greater_than: 0 } + def total_price (price * quantity).round(2) end From dd77d9dc5db147e0b099d9e5df90e3f6411718cc Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 18:59:20 -0300 Subject: [PATCH 07/12] Feat: add drop-down for food-measurement_unit on food#form view --- app/views/foods/_form.html.erb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/views/foods/_form.html.erb b/app/views/foods/_form.html.erb index f221d18..4cde320 100644 --- a/app/views/foods/_form.html.erb +++ b/app/views/foods/_form.html.erb @@ -18,11 +18,12 @@ class: "form-control mb-2" %>
-
- <%= form.text_field :measurement_unit, - placeholder: "Measurement unit", - aria: { label: "Measurement unit" }, - class: "form-control mb-2" %> +
+ <%= form.select :measurement_unit, + options_for_select(["g", "ml", "unit"]), + { prompt: "Select Measurement Unit" }, + aria: { label: "Measurement unit" }, + class: "form-control mb-2" %>
From 77ff84670b78dff52c24dc7c6e954fdd538ceb01 Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 19:00:13 -0300 Subject: [PATCH 08/12] Feat: add 'measurement_unit' data to '_food' partial --- app/views/foods/_food.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/foods/_food.html.erb b/app/views/foods/_food.html.erb index 0b5fb8f..a2fc7e8 100644 --- a/app/views/foods/_food.html.erb +++ b/app/views/foods/_food.html.erb @@ -2,7 +2,7 @@ <%= food.name %> <%= food.measurement_unit %> <%= "$#{food.price}" %> - <%= food.quantity %> + <%= "#{food.quantity}#{food.measurement_unit}" %> <%= button_to "Delete", food, method: :delete, class: "btn btn-outline-danger px-2 py-1" %> From 928866306139e403e82c411483febffb6addcb19 Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 20:28:14 -0300 Subject: [PATCH 09/12] Update: measurement_unit types on 'food' model and '_form' view partial --- app/models/food.rb | 2 +- app/views/foods/_form.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/food.rb b/app/models/food.rb index 7176457..283d309 100644 --- a/app/models/food.rb +++ b/app/models/food.rb @@ -5,7 +5,7 @@ class Food < ApplicationRecord validates :name, presence: true validates :price, presence: true, numericality: { greater_than: 0 } - validates :measurement_unit, presence: true + validates :measurement_unit, presence: true, inclusion: { in: %w[mg g kg l ml] } validates :quantity, presence: true, numericality: { greater_than: 0 } def total_price diff --git a/app/views/foods/_form.html.erb b/app/views/foods/_form.html.erb index 4cde320..6533093 100644 --- a/app/views/foods/_form.html.erb +++ b/app/views/foods/_form.html.erb @@ -20,7 +20,7 @@
<%= form.select :measurement_unit, - options_for_select(["g", "ml", "unit"]), + options_for_select(["mg", "g", "kg", "l", "ml"]), { prompt: "Select Measurement Unit" }, aria: { label: "Measurement unit" }, class: "form-control mb-2" %> From 18a524f14828c6e05462283d73449b0096cf4b02 Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 20:35:17 -0300 Subject: [PATCH 10/12] Update: 'user' and 'food' spec factories --- spec/factories/foods.rb | 10 ++++++---- spec/factories/users.rb | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/spec/factories/foods.rb b/spec/factories/foods.rb index dcc2383..904207a 100644 --- a/spec/factories/foods.rb +++ b/spec/factories/foods.rb @@ -1,9 +1,11 @@ +require 'faker' + FactoryBot.define do factory :food do - name { 'MyString' } - measurement_unit { 'MyString' } - price { '9.99' } + name { Faker::Food.ingredient } + measurement_unit { 'kg' } + price { 1.5 } quantity { 1 } - user { nil } + user { create(:user) } end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 13e6a8c..b9ec52b 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,8 +1,11 @@ +require 'faker' + FactoryBot.define do factory :user do name { Faker::Name.name } email { Faker::Internet.email } password { '123456' } password_confirmation { '123456' } + confirmed_at { Time.zone.now } end end From c13d5d0c4a788089afc0fa4b9bd18cceeb7a4131 Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 20:36:06 -0300 Subject: [PATCH 11/12] Tests: Add 'food' model unit-test --- spec/models/food_spec.rb | 67 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/spec/models/food_spec.rb b/spec/models/food_spec.rb index 7c6ea23..ec73643 100644 --- a/spec/models/food_spec.rb +++ b/spec/models/food_spec.rb @@ -1,5 +1,70 @@ require 'rails_helper' RSpec.describe Food, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + let(:food) { build(:food) } # ! non persisting object. It's not saved in the database. + + describe 'validations' do + context 'object itself' do + it 'should be valid' do + expect(food).to be_valid + end + end + + context 'name validations' do + it 'should be valid' do + expect(food.name).to be_present + end + + it 'should be invalid' do + food.name = nil + expect(food).to_not be_valid + end + end + + context 'measurement_unit validations' do + it 'should be valid' do + expect(food.measurement_unit).to be_present + expect(food.measurement_unit).to be_in(%w[mg g kg l ml]) + end + + it 'should be invalid' do + food.measurement_unit = nil + expect(food).to_not be_valid + end + end + + context 'price validations' do + it 'should be valid' do + expect(food.price).to be_present + expect(food.price).to be_a(BigDecimal) + expect(food.price).to be > 0 + end + + it 'should be invalid' do + food.price = nil + expect(food).to_not be_valid + end + end + + context 'quantity validations' do + it 'should be valid' do + expect(food.quantity).to be_present + expect(food.quantity).to be_a(Integer) + expect(food.quantity).to be > 0 + end + + it 'should be invalid' do + food.quantity = nil + expect(food).to_not be_valid + end + end + + context 'total_price validations' do + it 'should be valid' do + expect(food.total_price).to be_present + expect(food.total_price).to be_a(BigDecimal) + expect(food.total_price).to be > 0 + end + end + end end From 5564d15b54076616e38820437d1cda78fc9ad70a Mon Sep 17 00:00:00 2001 From: ITurres Date: Tue, 19 Dec 2023 20:36:33 -0300 Subject: [PATCH 12/12] Tests: Add 'food#index view' integration-test --- spec/views/foods/index.html.erb_spec.rb | 62 ++++++++++++++----------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/spec/views/foods/index.html.erb_spec.rb b/spec/views/foods/index.html.erb_spec.rb index 6a0206a..f1f5ad8 100644 --- a/spec/views/foods/index.html.erb_spec.rb +++ b/spec/views/foods/index.html.erb_spec.rb @@ -1,32 +1,40 @@ require 'rails_helper' -RSpec.describe 'foods/index', type: :view do - before(:each) do - assign(:foods, [ - Food.create!( - name: 'Name', - measurement_unit: 'Measurement Unit', - price: '9.99', - quantity: 2, - user: nil - ), - Food.create!( - name: 'Name', - measurement_unit: 'Measurement Unit', - price: '9.99', - quantity: 2, - user: nil - ) - ]) - end +RSpec.describe 'Foods', type: :system do + it 'displays a list of foods or a message if empty' do + new_user = create(:user) + + visit new_user_session_path + + fill_in 'Email', with: new_user.email + fill_in 'Password', with: new_user.password + + click_button 'Log in' + + create(:food, name: 'Pizza', measurement_unit: 'kg', price: 10.99, quantity: 5, user: new_user) + create(:food, name: 'Salad', measurement_unit: 'g', price: 5.5, quantity: 10, user: new_user) + + visit foods_path + + within('#foods', wait: 5) do + expect(page).to have_link('New food', href: new_food_path) + + expect(page).to have_selector('.p-2 a.btn', text: 'New food') + + if Food.all.empty? + expect(page).to have_selector('.alert.alert-info', text: 'There are no foods in your inventory.') + else + expect(page).to have_selector('table') + expect(page).to have_selector('thead th', text: 'Food') + expect(page).to have_selector('thead th', text: 'Measurement unit') + expect(page).to have_selector('thead th', text: 'Unit Price') + expect(page).to have_selector('thead th', text: 'Quantity') + expect(page).to have_selector('thead th', text: 'Actions') - it 'renders a list of foods' do - render - cell_selector = Rails::VERSION::STRING >= '7' ? 'div>p' : 'tr>td' - assert_select cell_selector, text: Regexp.new('Name'.to_s), count: 2 - assert_select cell_selector, text: Regexp.new('Measurement Unit'.to_s), count: 2 - assert_select cell_selector, text: Regexp.new('9.99'.to_s), count: 2 - assert_select cell_selector, text: Regexp.new(2.to_s), count: 2 - assert_select cell_selector, text: Regexp.new(nil.to_s), count: 2 + expect(page).to have_selector('tbody tr', count: 2) # Assuming there are two food items created + expect(page).to have_content('Pizza') + expect(page).to have_content('Salad') + end + end end end