diff --git a/Gemfile b/Gemfile
index b465a1b..1d66744 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,11 +2,6 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '3.2.2'
-gem 'bootstrap', '~> 5.3'
-gem 'cancancan'
-gem 'devise'
-gem 'rspec-rails'
-gem 'rubocop', '>= 1.0', '< 2.0'
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem 'rails', '~> 7.0.8'
@@ -14,7 +9,7 @@ gem 'rails', '~> 7.0.8'
gem 'sprockets-rails'
# Use postgresql as the database for Active Record
-gem 'pg', '~> 1.1'
+gem 'pg', '~> 1.5', '>= 1.5.4'
# Use the Puma web server [https://github.com/puma/puma]
gem 'puma', '~> 5.0'
@@ -46,6 +41,7 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
+gem 'rubocop', '>= 1.0', '< 2.0'
# Use Sass to process CSS
# gem "sassc-rails"
@@ -60,7 +56,6 @@ end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem 'web-console'
-
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"
@@ -74,4 +69,7 @@ group :test do
gem 'selenium-webdriver'
end
-gem 'cssbundling-rails', '~> 1.3'
+gem 'cancancan'
+gem 'devise', '~> 4.9'
+gem 'factory_bot_rails'
+gem 'rspec-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7509784..2f19169 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -69,17 +69,11 @@ GEM
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
- autoprefixer-rails (10.4.15.0)
- execjs (~> 2)
base64 (0.1.1)
bcrypt (3.1.19)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
- bootstrap (5.3.0)
- autoprefixer-rails (>= 9.1.0)
- popper_js (>= 2.11.7, < 3)
- sassc-rails (>= 2.0.0)
builder (3.2.4)
cancancan (3.5.0)
capybara (3.39.2)
@@ -93,8 +87,6 @@ GEM
xpath (~> 3.2)
concurrent-ruby (1.2.2)
crass (1.0.6)
- cssbundling-rails (1.3.3)
- railties (>= 6.0.0)
date (3.3.3)
debug (1.8.0)
irb (>= 1.5.0)
@@ -107,8 +99,11 @@ GEM
warden (~> 1.2.3)
diff-lcs (1.5.0)
erubi (1.12.0)
- execjs (2.9.1)
- ffi (1.16.3)
+ factory_bot (6.2.1)
+ activesupport (>= 5.0.0)
+ factory_bot_rails (6.2.0)
+ factory_bot (~> 6.2.0)
+ railties (>= 5.0.0)
globalid (1.2.1)
activesupport (>= 6.1)
i18n (1.14.1)
@@ -157,7 +152,6 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.4)
- popper_js (2.11.8)
psych (5.1.1)
stringio
public_suffix (5.0.3)
@@ -239,14 +233,6 @@ GEM
parser (>= 3.2.1.0)
ruby-progressbar (1.13.0)
rubyzip (2.3.2)
- sassc (2.4.0)
- ffi (~> 1.9)
- sassc-rails (2.1.2)
- railties (>= 4.0.0)
- sassc (>= 2.0)
- sprockets (> 3.0)
- sprockets-rails
- tilt
selenium-webdriver (4.14.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
@@ -262,7 +248,6 @@ GEM
railties (>= 6.0.0)
stringio (3.0.8)
thor (1.2.2)
- tilt (2.3.0)
timeout (0.4.0)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
@@ -292,15 +277,14 @@ PLATFORMS
DEPENDENCIES
bootsnap
- bootstrap (~> 5.3)
cancancan
capybara
- cssbundling-rails (~> 1.3)
debug
- devise
+ devise (~> 4.9)
+ factory_bot_rails
importmap-rails
jbuilder
- pg (~> 1.1)
+ pg (~> 1.5, >= 1.5.4)
puma (~> 5.0)
rails (~> 7.0.8)
rspec-rails
diff --git a/app/controllers/recipe_controller.rb b/app/controllers/recipe_controller.rb
index e28047d..e60f784 100644
--- a/app/controllers/recipe_controller.rb
+++ b/app/controllers/recipe_controller.rb
@@ -1,6 +1,11 @@
class RecipeController < ApplicationController
+ load_and_authorize_resource
def index
- @recipes = Recipe.all
+ @recipes = if user_signed_in?
+ Recipe.where(public: true).or(Recipe.where(user_id: current_user.id))
+ else
+ Recipe.where(public: true)
+ end
end
def new
diff --git a/app/models/ability.rb b/app/models/ability.rb
new file mode 100644
index 0000000..8e517c7
--- /dev/null
+++ b/app/models/ability.rb
@@ -0,0 +1,32 @@
+class Ability
+ include CanCan::Ability
+
+ def initialize(user)
+ user ||= User.new
+ can :manage, Recipe, user_id: user.id
+ # Define abilities for the user here. For example:
+ #
+ # return unless user.present?
+ # can :read, :all
+ # return unless user.admin?
+ # can :manage, :all
+ #
+ # The first argument to `can` is the action you are giving the user
+ # permission to do.
+ # If you pass :manage it will apply to every action. Other common actions
+ # here are :read, :create, :update and :destroy.
+ #
+ # The second argument is the resource the user can perform the action on.
+ # If you pass :all it will apply to every resource. Otherwise pass a Ruby
+ # class of the resource.
+ #
+ # The third argument is an optional hash of conditions to further filter the
+ # objects.
+ # For example, here the user can only update published articles.
+ #
+ # can :update, Article, published: true
+ #
+ # See the wiki for details:
+ # https://github.com/CanCanCommunity/cancancan/blob/develop/docs/define_check_abilities.md
+ end
+end
diff --git a/app/models/recipe.rb b/app/models/recipe.rb
index ba33aef..3a539c3 100644
--- a/app/models/recipe.rb
+++ b/app/models/recipe.rb
@@ -3,9 +3,11 @@ class Recipe < ApplicationRecord
has_many :recipe_foods
has_many :foods, through: :recipe_foods
- validates :name, presence: true
- validates :preparation_time, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
- validates :cooking_time, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
- validates :description, presence: true
+ validates :name, presence: { message: 'Cannot be empty' }
+ validates :preparation_time, presence: { message: 'Cannot be empty' },
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :cooking_time, presence: { message: 'Cannot be empty' },
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :description, presence: { message: 'Cannot be empty' }
validates :public, inclusion: { in: [true, false] }
end
diff --git a/app/views/recipe/index.html.erb b/app/views/recipe/index.html.erb
index 0e2a5b6..ec899ec 100644
--- a/app/views/recipe/index.html.erb
+++ b/app/views/recipe/index.html.erb
@@ -8,7 +8,10 @@
<%= link_to recipe.name, recipe_path(recipe) %>
- <%= button_to "Remove", recipe, method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-danger" %>
+ <% if can? :manage, recipe %>
+ <%= button_to "Remove", recipe, method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-danger" %>
+<% end %>
+
<%= recipe.description %>
diff --git a/app/views/recipe/new.html.erb b/app/views/recipe/new.html.erb
index 301c45a..a5080d4 100644
--- a/app/views/recipe/new.html.erb
+++ b/app/views/recipe/new.html.erb
@@ -1,28 +1,32 @@
-Create a New Recipe
<%= form_for @recipe, url: new_recipe_path, method: :post do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
+ <%= f.object.errors.full_messages_for(:name).join(', ') if f.object.errors.include?(:name) %>
<%= f.label :preparation_time %>
<%= f.number_field :preparation_time %>
+ <%= f.object.errors.full_messages_for(:preparation_time).join(', ') if f.object.errors.include?(:preparation_time) %>
<%= f.label :cooking_time %>
<%= f.number_field :cooking_time %>
+ <%= f.object.errors.full_messages_for(:cooking_time).join(', ') if f.object.errors.include?(:cooking_time) %>
<%= f.label :description %>
<%= f.text_area :description %>
+ <%= f.object.errors.full_messages_for(:description).join(', ') if f.object.errors.include?(:description) %>
<%= f.label :public %>
<%= f.check_box :public %>
+ <%= f.object.errors.full_messages_for(:public).join(', ') if f.object.errors.include?(:public) %>
diff --git a/app/views/recipe_foods/index.html.erb b/app/views/recipe_foods/index.html.erb
deleted file mode 100644
index 57cb460..0000000
--- a/app/views/recipe_foods/index.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-
Recipe Food
\ No newline at end of file
diff --git a/app/views/recipe_foods/show.html.erb b/app/views/recipe_foods/show.html.erb
deleted file mode 100644
index e69de29..0000000
diff --git a/spec/features/public_recipes_spec.rb b/spec/features/public_recipes_spec.rb
new file mode 100644
index 0000000..1259690
--- /dev/null
+++ b/spec/features/public_recipes_spec.rb
@@ -0,0 +1,11 @@
+require 'rails_helper'
+
+RSpec.feature 'Public Recipes Page', type: :feature do
+ scenario 'all users can see public recipes' do
+ user = create(:user)
+ login_as(user, scope: :user)
+ recipe = create(:recipe, user:)
+ visit public_recipes_path
+ expect(page).to have_content(recipe.name)
+ end
+end
diff --git a/spec/features/recipe_index_spec.rb b/spec/features/recipe_index_spec.rb
new file mode 100644
index 0000000..b3b1cc3
--- /dev/null
+++ b/spec/features/recipe_index_spec.rb
@@ -0,0 +1,33 @@
+require 'rails_helper'
+
+RSpec.feature 'Recipe#index Page', type: :feature do
+ scenario 'authorized user can add a new recipe' do
+ user = create(:user)
+ login_as(user, scope: :user)
+ visit recipe_index_path
+ click_button('Add Recipe')
+ expect(page).to have_current_path(new_recipe_path)
+ end
+
+ scenario 'user can remove their recipe' do
+ user = create(:user)
+ login_as(user, scope: :user)
+ visit recipe_index_path
+ expect(page).not_to have_button('Remove', exact: true)
+ end
+
+ scenario 'user can add a new recipe' do
+ user = create(:user)
+ login_as(user, scope: :user)
+ visit recipe_index_path
+ expect(page).to have_button('Add Recipe', exact: true)
+ end
+
+ scenario 'user can view their own private and public recipes' do
+ user = create(:user)
+ login_as(user, scope: :user)
+ recipe = create(:recipe, user:)
+ visit recipe_index_path
+ expect(page).to have_content(recipe.name)
+ end
+end
diff --git a/spec/models/recipe_spec.rb b/spec/models/recipe_spec.rb
index 63f198b..d249950 100644
--- a/spec/models/recipe_spec.rb
+++ b/spec/models/recipe_spec.rb
@@ -1 +1,59 @@
require 'rails_helper'
+
+RSpec.describe Recipe, type: :model do
+ describe Recipe, type: :model do
+ it 'is valid with valid attributes' do
+ user = User.create(name: 'johndoes', email: 'john@email.com', password: '123456')
+ recipe = Recipe.new(
+ name: 'Sample Recipe',
+ preparation_time: 30,
+ cooking_time: 60,
+ description: 'Recipe description',
+ public: true,
+ user:
+ )
+ expect(recipe).to be_valid
+ end
+
+ it 'is not valid without a name' do
+ recipe = Recipe.new(name: nil)
+ expect(recipe).not_to be_valid
+ end
+
+ it 'is not valid without a user' do
+ recipe = Recipe.new(
+ name: 'MyRecipe',
+ preparation_time: 30,
+ cooking_time: 60,
+ description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
+ public: true
+ )
+ expect(recipe).not_to be_valid
+ end
+ it 'is not valid with a negative preparation time' do
+ user = User.create(name: 'johndoes', email: 'john@email.com', password: '123456')
+ recipe = Recipe.new(
+ name: 'Sample Recipe',
+ preparation_time: -5,
+ cooking_time: 60,
+ description: 'Recipe description',
+ public: true,
+ user:
+ )
+ expect(recipe).not_to be_valid
+ end
+
+ it 'is not valid with a negative cooking time' do
+ user = User.create(name: 'johndoes', email: 'john@email.com', password: '123456')
+ recipe = Recipe.new(
+ name: 'Sample Recipe',
+ preparation_time: 30,
+ cooking_time: -10,
+ description: 'Recipe description',
+ public: true,
+ user:
+ )
+ expect(recipe).not_to be_valid
+ end
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 6584c98..5c5847b 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -4,7 +4,13 @@
require_relative '../config/environment'
# Prevent database truncation if the environment is production
abort('The Rails environment is running in production mode!') if Rails.env.production?
+Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
require 'rspec/rails'
+require 'capybara/rails'
+require 'capybara/rspec'
+require 'factory_bot_rails'
+require 'devise'
+
# Add additional requires below this line. Rails is not loaded until this point!
# Requires supporting ruby files with custom matchers and macros, etc, in
@@ -30,8 +36,16 @@
abort e.to_s.strip
end
RSpec.configure do |config|
+ config.before(:each, type: :feature) do
+ default_url_options[:host] = 'http://127.0.0.1:3000/' # Replace with your application's host
+ end
+
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{Rails.root}/spec/fixtures"
+ config.include FactoryBot::Syntax::Methods
+ config.include Devise::Test::IntegrationHelpers, type: :request
+ config.include Devise::Test::IntegrationHelpers, type: :feature
+ config.include Devise::Test::ControllerHelpers, type: :controller
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
@@ -61,3 +75,5 @@
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
end
+
+Capybara.javascript_driver = :selenium_chrome_headless
diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb
new file mode 100644
index 0000000..b2d5a13
--- /dev/null
+++ b/spec/support/factory_bot.rb
@@ -0,0 +1,45 @@
+FactoryBot.define do
+ factory :recipe_food do
+ quantity { 500 }
+ end
+end
+
+FactoryBot.define do
+ factory :user do
+ name { 'Riley' }
+ email { 'riley@example.com' }
+ password { 'password123' }
+ end
+end
+
+FactoryBot.define do
+ factory :food do
+ name { 'Broccoli' }
+ measurement_unit { 'g' }
+ price { 0.7 }
+ quantity { 1000 }
+ user
+ end
+end
+
+FactoryBot.define do
+ factory :recipe do
+ name { 'Vegetable Soup' }
+ preparation_time { 10 }
+ cooking_time { 20 }
+ description { 'A delicious vegetable soup' }
+ public { true }
+ user
+ end
+end
+
+FactoryBot.define do
+ factory :public_recipe, class: 'Recipe' do
+ name { 'Public Recipe' }
+ preparation_time { 15 }
+ cooking_time { 25 }
+ description { 'A public recipe' }
+ public { true }
+ user
+ end
+end