Rails: Same "Create" action conditional between two models - ruby-on-rails

Making a site in with three main models: Users, Posts, and Gyms. Users should be able to post either from their own model (User.post), or, if they are the admin of a gym, from the Gym's model (Gym.post).
I'm using the same post controller and post form to post fro either the gym or the user, but the controller "Create" action can't distinguish between the two.
class PostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy
def create
if (gym.gym_admin == current_user.id)
#post = gym.posts.build(post_params)
if #post.save
flash[:success] = "Post!"
redirect_to "/gyms/#{gym.id}"
else
#feed_items = []
render 'static_pages/home'
end
else
#post = current_user.posts.build(post_params)
if #post.save
flash[:success] = "Post!"
redirect_to root_url
else
#feed_items = []
render 'static_pages/home'
end
end
end
def destroy
#post.destroy
flash[:notice] = "Post deleted"
redirect_to request.referrer || root_url
end
private
def post_params
params.require(:post).permit(:post_type, :title, :content, :picture, :body_parts,
:duration, :equipment, :calories, :protein,
:fat, :carbs, :ingredients, :tag_list,
:postable_id, :postable_type)
end
def correct_user
#post = current_user.posts.find_by(id: params[:postable_id])
redirect_to root_url if #post.nil?
end
def gym
#gym = Gym.find_by(params[:id])
end
end
And the Models:
class Post < ApplicationRecord
belongs_to :user
belongs_to :gym
belongs_to :postable, polymorphic: true
class User < ApplicationRecord
has_many :posts, as: :postable, dependent: :destroy
has_many :gyms
class Gym < ApplicationRecord
has_many :posts, as: :postable, dependent: :destroy
belongs_to :user
Rught now, this create action only creates posts from the gym's model; if I remove the first half of the conditional, it will only post from the User model.
Any help is greatly appreciated, thank you

I would be curious what gym.gym_admin (and consequently the whole line below 'def create') evaluates to since I don't see in referenced anywhere else.
My suspicion is that you would want to change
if (gym.gym_admin == current_user.id)
to
if (gym.gym_admin.id == current_user.id)
or
if (gym.gym_admin == current_user)
once that relationship is working correctly.
Also, could post be built independently of whether a user is a gym admin and send the post params the gym_id if applicable. Then accessed either through:
/gym/:id
#posts = Post.where('gym_id = ?', params[:id])
or
/user/:id
#posts = Post.where('user_id = ?', params[:id])

I fixed it my removing the conditional logic altogether; I just made two separate custom actions in the controller and called them from different links. ie. :actions => create_gym / create_user

Related

Can not create or build nested model in Rails 6

I have issue when create nested model in Rails 6:
post.rb
class Post < ApplicationRecord
has_many :post_votes, dependent: :destroy
end
post_vote.rb
class PostVote < ApplicationRecord
belongs_to :posts
end
routes.rb
resources :posts do
resources :post_votes
end
views:
<%= button_to post_post_votes_path(post), method: :post, remote: true, form_class: "post_vote" do%>
<%= bootstrap_icon "arrow-up-circle", width: 20, height: 20, fill: "#333" %>
<%end%>
PostVost Controller
def create
#post = Post.find(params[:post_id])
#post_vote = PostVote.new
if already_voted?
# flash[:notice] = "You can't vote more than once"
redirect_to root_path
else
#post_vote = #post.post_votes.build(user_id: current_user.id)
end
# redirect_to post_path(#post)
respond_to do |format|
format.html {}
format.js
end
end
def already_voted?
PostVote.where(user_id: current_user.id, post_id: params[:post_id]).exists?
end
I check the log file, no record was update in database
Any one known why i can not create new post_vote model?
Thank you so much!
On this line:
#post_vote = #post.post_votes.build(user_id: current_user.id)
.build only creates the object in memory. It does not persist it to the database.
Try:
#post_vote = #post.post_votes.create(user_id: current_user.id)
or
#post_vote = #post.post_votes.create!(user_id: current_user.id)
if you want an exception to be thrown if persistence fails.
The problem is using belong_to without optional
class PostVote < ApplicationRecord
belongs_to :posts
end
After:
class PostVote < ApplicationRecord
belongs_to :posts, optional: true
end

How do I create a page without a model?

I'm working on an app which has many 'Activities'. Each 'Activity' has many 'Ranks'. I'd like each 'Activity' to have a page called grading, where the user can see a list of all of that activity's ranks and conveniently update them. I imagine the URL would be something like http://localhost:3000/activities/21/grading
I'm already using http://localhost:3000/activities/21/edit for its intended purpose.
I don't need a model for gradings, as I don't need to save any grading records.
I know exactly what to put in the view, I'm just unsure what to add to the controller and routes files. Other people have worked on this app but I'm unable to contact them.
Routes
resources :activities do
collection do
get 'scheduled_classes'
end
end
resources :ranks
end
activities_controller
class ActivitiesController < ApplicationController
def new
#activity = Activity.new
#activity.timeslots.build
#activity.ranks.build
end
def create
#activity = current_club.activities.new(activity_params)
if #activity.save
flash[:success] = "New class created!"
redirect_to activity_path(#activity)
else
render 'new'
end
end
def edit
#activity = current_club.activities.find_by(id: params[:id])
#active_ranks = #activity.ranks.where(active: true)
if !#activity.active?
redirect_to activities_path
else
#activity.timeslots.build
end
end
def update
#activity = current_club.activities.find_by(id: params[:id])
if #activity.update_attributes(activity_params)
flash[:success] = "Class updated!"
redirect_to edit_activity_path(#activity)
else
render 'edit'
end
end
def show
#activity = current_club.activities.find_by(id: params[:id])
#active_ranks = #activity.ranks.where(active: true)
if #activity.nil?
redirect_to root_url
elsif !#activity.active?
redirect_to activities_path
end
end
def index
#activities = current_club.activities.all
end
def destroy
#activity = current_club.activities.find_by(id: params[:id])
if #activity.nil?
redirect_to root_url
else
#activity.destroy
flash[:success] = "Class deleted"
redirect_to activities_path
end
end
end
private
def activity_params
params.require(:activity).permit(:name, :active,
:timeslots_attributes => [:id,
:time_start,
:time_end,
:day,
:active,
:schedule],
:ranks_attributes => [:id,
:name,
:position,
:active])
end
end
activity
class Activity < ApplicationRecord
belongs_to :club
has_many :timeslots, dependent: :destroy
accepts_nested_attributes_for :timeslots,:allow_destroy => true
has_many :ranks, dependent: :destroy
has_many :attendances, dependent: :destroy
accepts_nested_attributes_for :ranks
validates :club_id, presence: true
validates :name, presence: true, length: { maximum: 50 }
end
Your routes don't need to have an associated model or resource.
resources :activities do
collection do
get 'scheduled_classes'
end
member do
get :grading
end
end
will match to activities#grading
See https://guides.rubyonrails.org/routing.html#adding-member-routes for more info.
As you want to add a route on a particular activity, you should add member route on the activity like below,
resources :activities do
collection do
get 'scheduled_classes'
end
get :grading, on: :member
end
Apart from this, you have to add method in ActivitiesController for this route like below,
def grading
#activity = Activity.find_by(id: params[:id])
# do more here
end
In view files, you can create grading.html.erb under activities resources and put your view code there.

DB rolls back on create action

I'm trying to create a form with a series of checks to prevent duplicates during the simultaneous creation of three model records: one for the parent (assuming it doesn't exist), one for its child (assuming it doesn't exist), and one for a join table between the child and the User (to allow the User to have their own copy of the Song object).
In the current state of the code, The checks seemingly pass, but
the server logs show ROLLBACK, and nothing gets saved
to the database EXCEPT the parent object (artist).
When I try to use the ids of the object, I get the error undefined method id for nil:NilClass, or "couldn't find object without an ID".
The following code is in my controller:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_by(name: params[:artist][:name].strip.titleize) #look for the artist
#song = Song.find_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize)
if #artist.present? && #song.present?
#user_song = current_user.user_songs.find(#song_id)
if #user_song.present?
render html: "THIS SONG IS ALREADY IN YOUR PLAYLIST"
render action: :new
else
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
end
elsif #artist.present? && !#song.present?
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
elsif !#artist.present?
#artist = Artist.create(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
else
render html: "SOMETHING WENT WRONG. CONTACT ME TO LET ME KNOW IF YOU SEE THIS MESSAGE"
end
end
def index
#songs = Song.all
end
def new
#artist = Artist.new
#artist.songs.build
#user_song = UserSong.new(user_id: current_user.id, song_id: #song_id)
end
def show
#song_id = params["song_id"]
#song = Song.find(params[:id])
end
def destroy
UserSong.where(:song_id => params[:id]).first.destroy
flash[:success] = "The song has been from your playlist"
redirect_to root_path
end
def edit
#song = Song.find(params[:id])
#artist = Artist.find(#song.artist_id)
end
def update
end
private
def set_artist
#artist = Artist.find(params[:id])
end
def artist_params
params.require(:artist).permit(:name, songs_attributes: [:id, :title, :lyrics])
end
def set_song
#song = Song.find(params["song_id"])
end
end
The models:
class Artist < ApplicationRecord
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class Song < ApplicationRecord
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class UserSong < ApplicationRecord
belongs_to :song
belongs_to :user
end
Sorry if I haven't abstracted enough. Not really sure how, given that there's no error message, just a rollback (without any validations present in any of the controllers).
Thanks to #coreyward and his pointing out of the fat-model skinny-controller lemma (never knew that was a thing), I was able to cut the code down and arrive at a solution immediately. In my models, I used validates_uniqueness_of and scope in order to prevent duplication of records. In my controller, I used find_or_create_by to seal the deal.
To whom it may concern, the final code is as follows:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_or_create_by(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.find_or_create_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize) do |song|
song.lyrics = params[:artist][:songs_attributes]["0"][:lyrics].strip
end
#user_song = current_user.user_songs.find_or_create_by(song_id: #song.id) do |user_id|
user_id.user_id = current_user.id
end
redirect_to root_path
end
class Song < ApplicationRecord
validates_uniqueness_of :title, scope: :artist_id
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class Artist < ApplicationRecord
validates_uniqueness_of :name
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class UserSong < ApplicationRecord
validates_uniqueness_of :song_id, scope: :user_id
belongs_to :song
belongs_to :user
end

Can't add Products back to Products model after deleting it from Cart, Ruby on rails

In an E commerce Rails App I'm building products that is deleted from the ShoppingCart are not added back to the production model after deletion.
When I add Products to the Cart the App is using this controller below to decrease the number of products from the Product model( see the create method)
controllers/product_item_controller.rb
class ProductItemsController < ApplicationController
include CurrentCart
before_action :set_cart, only: [:create]
before_action :set_product_item, only: [:show, :destroy]
def create
#product = Product.find(params[:product_id])
#product_item = #cart.add_product(#product.id)
if #product_item.save
redirect_to root_url, notice:'Product added to Cart'
product = Product.find params[:product_id]
product.update_columns(stock_quantity: product.stock_quantity - 1)
else
render :new
end
end
private
def set_product_item
#product_item = ProductItem.find(params[:id])
end
def product_item_params
params.require(:product_item).permit(:product_id)
end
end
That is woking fine.
But when I delete the Cart it gets deleted but the products are not added to the products model. And I also get this messages : Invalid Cart
this is the carts_controller.rb
class CartsController < ApplicationController
before_action :set_cart, only: [:show, :destroy]
rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
def new
#cart = Cart.new
end
def show
#images = ["1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg"]
#random_no = rand(5)
#random_image = #images[#random_no]
end
def destroy
#cart.destroy if #cart.id == session[:cart_id]
session[:cart_id] = nil
product = Product.find params[:product_id]
product.update_columns(stock_quantity: product.stock_quantity + 1)
redirect_to root_url, notice: 'Your Cart is Empty'
end
def remove
cart = session['cart']
item = cart['items'].find { |item| item['product_id'] == params[:id] }
product = Product.find(item['product_id'])
product.update_columns(stock_quantity: product.stock_quantity + 1)
if item
cart['items'].delete item
end
redirect_to cart_path
end
private
def set_cart
#cart = Cart.find(params[:id])
end
def cart_params
params[:cart]
end
def invalid_cart
logger_error = 'You are trying to access invalid cart'
redirect_to root_url, notice: 'Invalid Cart'
end
end
I Can't see what is wrong with this code and why the products are not added to the product.rb after being deleted from the Cart.
Am I missing something here? Could someone advise me here?
Below are other relevant models and controllers
products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy]
def show
end
def search
#product = Product.search(params[:query]).order("created_at DESC")
#categories = Category.joins(:products).where(:products => {:id => #product.map{|x| x.id }}).distinct
end
private
# Use callbacks to share common setup or constraints between actions.
def set_product
#product = Product.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:title, :description, :price_usd, :price_isl, :image, :category_id, :stock_quantity, :label_id, :query)
end
end
Cart.rbmodel
class Cart < ActiveRecord::Base
has_many :product_items, dependent: :destroy
def add_product(product_id)
current_item = product_items.find_by(product_id: product_id)
if current_item
current_item.quantity += 1
else
current_item = product_items.build(product_id: product_id)
end
current_item
end
def total_price_usd
product_items.to_a.sum{|item| item.total_price_usd}
end
def total_price_isl
product_items.to_a.sum{|item| item.total_price_isl}
end
end
product.rbmodel
Class Product < ActiveRecord::Base
belongs_to :category
belongs_to :label
has_many :product_item, :dependent => :destroy
#before_destroy :ensure_not_product_item
validates :title, :description, presence: true
validates :price_usd, :price_isl, numericality: {greater_than_or_equal_to: 0.01}
validates :title, uniqueness: true
has_attached_file :image, styles: { medium: "500x500#", thumb: "100x100#" }
validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/
#def ensure_not_product_item
# if product_item.empty?
# return true
# else
# errors.add(:base, 'You have Product Items')
# return false
# end
#end
def self.search(query)
where("title LIKE ? OR description LIKE ?", "%#{query}%", "%#{query}%")
end
end
You are rescuing from ActiveRecord::RecordNotFound
rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
But you're probably rescuing inappropriately... from the Product.find... in the destroy method. I'm not sure why you would expect the product_id to be in params.
Your code...
def destroy
#cart.destroy if #cart.id == session[:cart_id]
session[:cart_id] = nil
product = Product.find params[:product_id]
product.update_columns(stock_quantity: product.stock_quantity + 1)
redirect_to root_url, notice: 'Your Cart is Empty'
end
A better alternative might be...
def destroy
if #card.id == session[:cart_id]
#cart.product_items each do |product_item|
product_item.product.update_columns(stock_quantity: product_item.product.stock_quantity + 1)
end
#cart.destroy
end
end
However this might better be done as a before_destroy action for product_item model, so that destroying a product_item will automatically increment the stock total.
I'm not going to give a line by line solution as there are quite a few points about this application that not quite right and require a bit of rethinking. Lets look at how a shopping cart commonly is done.
The models:
class User < ApplicationRecord
has_many :orders
has_many :products, through: :orders
def current_order
orders.find_or_create_by(status: :open)
end
end
class Order < ApplicationRecord
enum status: [:in_cart, :processing, :shipped]
belongs_to :user
has_many :line_items
has_many :products, through: :line_items
end
# The join model between a Order and Product
# The name line item comes from the lines on a order form.
class LineItem < ApplicationRecord
belongs_to :order
belongs_to :product
end
class Product < ApplicationRecord
has_many :line_items
has_many :orders, through: :line_items
end
The naming here is not a mistake or sloppy copy pasting. A cart is only a concept in web app which exists as a "user aid" in creating an order.
The join between a Order and Product is commonly called a line-item. Note that we use has_many though: so that we can query:
User.find(1).order
Product.find(1).orders
Order.find(1).products
The Controllers
When building something as complicated as a checkout you will want to pay attention to the Single Responsibility Principle and KISS. Having many classes is not a bad thing. Having huge tangled controllers that do far too much is.
So for example create a controller that has adding and removing items from the cart as its sole responsibility.
# routes.rb
resource :cart
resources :line_items,
only: [:create, :destroy, :update] do
collection do
delete :clear
end
end
end
# app/controllers/line_items.rb
class LineItemsController < ApplicationController
before_action :set_cart
before_action :set_item
rescue_from Orders::NotOpenError, -> { redirect_to #order, error: 'Order is locked and cannot be edited' }
# Add an item to cart
# POST /cart/line_items
def create
#cart.product_items.create(create_params)
# ...
end
# Remove an item from cart
# DESTROY /cart/line_items/:id
def destroy
#item.destroy
if #item.destroyed?
redirect_to cart_path, success: 'Item removed.'
else
redirect_to cart_path, alert: 'Could not remove item.'
end
end
# Remove all items from cart
# DESTROY /cart/line_items
def clear
#order.line_items.destroy_all
if #order.items.count.zero?
redirect_to cart_path, success: 'All items destroyed'
else
redirect_to cart_path, alert: 'Could not remove all items.'
end
end
# Update a line in the order
# PATCH /cart/line_items/:id
def update
#line_item.update(update_params)
end
private
def set_order
#order = current_user.current_order
# Ensure that order is not processed in some way
raise Orders::NotOpenError unless #order.open?
end
def set_line_item
#line_item = #order.line_items.find(params[:id])
end
def create_params
params.require(:line_item).permit(:product_id, :quantity)
end
def update_params
params.require(:line_item).permit(:quantity)
end
end
Notice how nicely the path for route each clearly tells us what it does and how we can write a description of the controller in a single line without using the word and.
In addition to this you will want a ProductsController, CartController, OrderController, PaymentsController etc. each of should do a single job - and do it well.
Don't do it all in your controllers!
When we add a line item to a order the available stock of the product should of course decrease. This is a clear cut example of business logic.
In MVC business logic belongs in the model layer.
A user adding a item to the cart should only create a reservation. The actual inventory of a product should only be altered when the order is processed or ships:
# No callbacks needed!
class Product < ApplicationRecord
has_many :line_items
has_many :orders, through: :line_items
def reservations
line_items.joins(:order)
.where
.not(line_items: {
order: Order.statuses[:shipped]
})
.sum(:quantity)
end
def availibity
stock - reservations
end
end
You've got
before_action :set_cart, only: [:show, :destroy]
rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
As soon as the CartsController#destroy method is invoked the private method set_cart is called. What it tries to do is to initialize an instance variable #cart = Cart.find(params[:id]).
The first line of your #destroy method is #cart.destroy if #cart.id == session[:cart_id]. Isn't the #cart = Cart.find(params[:id]) a problem here? What is the value of params[:id]? I guess it's not the same as session[:cart_id] and might probably be a nil or some Intreger value by which the DB cannot find a Cart record, hence the error.
Edit 1:
The same applies to the product = Product.find params[:product_id] as Steve mentioned in his answer.
Max posted a very informative report on how it should be done properly. If you have the time stick to his answer and try to redesign your app in accordance to his suggestion.

Is it correct architecture for adding user's posts?

Create action in posts controller:
def create
#post = current_user.posts.build(post_params)
if #post.not_exists?(current_user)
if #post.save
#flash
redirect_to root_path
else
#flash[:error]
redirect_to root_path
end
else
#flash[:error]
redirect_to root_path
end
end
Post model:
class Post < ActiveRecord::Base
belongs_to :user
##validations
def not_exists?(user)
return true unless user.posts.find_by(name: self.name)
end
end
My question: is it correct to build my create action like this? Or there is a better architectural design? I think it is too fat action.
Why not use a validation instead ?
class Post < ActiveRecord::Base
belongs_to :user
validates_uniqueness_of :name, :scope => :user_id

Resources