code refactory on a small controller(thin controller fat model) - ruby-on-rails

I have a controller action that is doing product listing, pagination and some filters, like category(from a dropdown), title(from a text-field), stock(from a checkbox)
This is my controller:
class ProductsController < ApplicationController
def index
#products = Product.where(active:1).where("title LIKE ?","%#{params[:title]}%")
if params[:stock]
#products=#products.where("stock = 0")
end
if params[:category]
#products=#products.where("category_id LIKE ?","#{params[:category]}")
end
#products= #products.paginate(:page => params[:page])
#categories= Category.all
end
And my model is:
class Product < ActiveRecord::Base
belongs_to :category
...some validations...
end
What could I change in order that my controller would become thinner? Thanks

Model
class Product < ActiveRecord:::Base
scope :active, where(active: 1)
def self.with_stock(stock=nil)
return where(stock: 0) if stock
self
end
def self.categorized(category=nil)
return self.where(category: category) if category
self
end
def self.titled(title=nil)
return self.where("title LIKE ?", 'title') if title
self
end
def self.list(params)
title = params[:title]
category = params[:category]
page = params[:page]
self.titled(title).with_stock(stock).categorized(category)
.paginate(page).active
end
end
Controller
def index
#products = Product.list(params)
end
Do not ship Category in controller. Do it in template/partial. ONE instance variable from controller only.

I propose the specific refactoring style:
controller
class ProductsController < ApplicationController
def index
#products = Product.titled params[:title]
#products = #products.in_stock if params[:stock]
#products = #products.category params[:category] if params[:category]
#products = #products.paginate :page => params[:page]
#categories = Category.all
end
end
model
class Product < ActiveRecord::Base
belongs_to :category
...
scope :titled, proc {| title | where(active:1).where("title LIKE ?","%#{title}%")
scope :in_stock, proc { where("stock = 0") }
scope :category, proc {| category | where("category_id LIKE ?","#{category}") }
end

If your intent is just to the controller become thinner, you could move the logic to the model.
ProductController.rb
#products = Product.some_method(params)
Product.rb
def self.some_method(params)
if params[:stock]
where("stock = 0 AND active = 1 AND title LIKE ?","%#{params[:title]}%")
end
if params[:category]
where("active = 1 AND category_id LIKE ? AND title LIKE ?", "#{params[:category]}", "%#{params[:title]}%")
end

Using thin controller, fat model principle.
controller:
class ProductsController < ApplicationController
def index
#products = Product.active(params).paginate(page: params[:page])
#categories = Category.all
end
end
model:
class Product < ActiveRecord::Base
belongs_to :category
def self.active(params)
products = where(active:1).where("title LIKE ?","%#{params[:title]}%")
if params[:stock]
products = products.where("stock = 0")
end
if params[:category]
products = products.where("category_id LIKE ?","#{params[:category]}")
end
end
end

Related

Issue with HomeController showing undefined method

I am trying to pass stored_products from shopify into a Rails app but keep getting a home controller error at https://f588240c.ngrok.io/ i have made updates, with no luck and restarted the server a number of times with no luck.
Any help would be welcomed. Heres the code
class Api::V1::HomeController < ShopifyApp::AuthenticatedController
def index
#products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
#products.each do |product|
StoredProduct.where(shopify_id: product.id)
.first_or_create do |stored_product|
stored_product.shopify_id = product.id
stored_product.shopify_title = product.title
stored_product.shopify_handle = product.handle
stored_product.shopify_image_url = product.image.src
stored_product.shop_id = #shop.id
stored_product.save
product.images.each do |image|
ProductImage.where(shopify_id: image.id)
.first_or_create do |product_image|
product_image.image_url = image.src
product_image.stored_product_id = stored_product_id
product_image.shopify_id = image.id
end
end
end
end
#stored_products = StoredProduct.belongs_to_shop(#shop.id)
end
end
From the authenticated controller
private
def set_shop
#shop = Shop.find_by(id: session[:shopify])
set_locale
end
from the store_products.rb file
class StoredProduct < ApplicationRecord
belongs_to :shop
has_many :product_images
scope :belongs_to_shop, -> shop_id { where(shop_id: shop_id) }
end
For this specific issue/code tutorial, the private set_shop method should be set like follows:
def set_shop
#shop = Shop.find_by(id: session[:shop_id])
set_locale
end
The other answer has params instead of session
The problem is that #shop is nil. The error message says it cannot call the method .id on NilClass.
In the image I can see that you have a shop_id in the params so you might just need to change your code here:
def set_shop
#shop = Shop.find_by(id: params[:shop_id])
set_locale
end
But that depends on your code, so please double check.

How to set karma to appropriate user

I'm trying to add a user karma feature to my app and I'm almost done, just that the karma is being awarded to a different user.
NB, My like system is from scratch and not acts_as_votable.
What I want:
When a user upvotes a book, I want a +1 karma be awarded to the
book.user
If a user's books are downvoted more then they upvoted, I want such
user have negative karma.
What I'm getting:
When a book is upvoted, the user who upvoted the book gets the +1
karma instead of the book.user.
When a user with 0 karma gets his/her book downvoted, the karma incrment by 1 instead of decrementing.
class AddKarmaToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :karma, :integer, default: 0
end
end
My code:
vote.rb
class Vote < ApplicationRecord
belongs_to :user
belongs_to :book
validates_uniqueness_of :user_id, scope: :book_id
after_create :increment_vote, :add_karma
after_destroy :decrement_vote, :remove_karma
private
def increment_vote
field = self.upvote ? :upvotes : :downvotes
Book.find(self.book_id).increment(field).save
end
def decrement_vote
field = self.upvote ? :upvotes : :downvotes
Book.find(self.book_id).decrement(field).save
end
def add_karma
user = User.find(self.user_id)
user.increment(:karma, 1).save
end
def remove_karma
user = User.find(self.user_id)
user.decrement(:karma, 1).save
end
end
votes_controller.rb
class VotesController < ApplicationController
def create
book_id = params[:book_id]
vote = Vote.new
vote.book_id = params[:book_id]
vote.upvote = params[:upvote]
vote.user_id = current_user.id
#check if vote by this user exists
existing_vote = Vote.where(user_id: current_user.id, book_id: book_id)
#new_vote = existing_vote.size < 1
respond_to do |format|
format.js {
if existing_vote.size > 0
#destroy existing vote
existing_vote.first.destroy
else
#save new vote
if vote.save
#success = true
else
#success = false
end
# #total_upvotes = #book.upvotes
# #total_downvotes = #book.downvotes
end
#book = Book.find(book_id)
#is_upvote = params[:upvote]
render "votes/create"
}
end
end
private
def vote_params
params.require(:vote).permit(:upvote, :book_id)
end
end
First of all when using active record relations you don't need to call Model.find in the class, just call the relation with it's name:
def increment_vote
field = self.upvote ? :upvotes : :downvotes
book.increment(field).save
end
def add_karma
user.increment(:karma, 1).save
end
In add_karma and remove_karma you are referencing the user that the vote belongs to, and not the user that owns the book. To achieve your goal you should also increment and decrement karma on the book's owner:
def add_karma
user.increment(:karma, 1).save
book.user.increment(:karma, self.upvote ? 1 : -1).save
end
def remove_karma
user.increment(:karma, 1).save
book.user.decrement(:karma, 1).save
end
You could rewrite your controller to simplify the code:
class VotesController < ApplicationController
def create
#vote = current_user.votes.find_or_initialize_by vote_params[:book_id]
#vote.assign_attributes vote_params
#success = #vote.save
# instead of #book = #vote.book just use #vote.book in your view
#book = #vote.book
# instead of #is_upvote you can use #vote.upvote in your view
#is_upvote = #vote.upvote
respond_to do |format|
format.js { render 'votes/create'}
end
end
private
def vote_params
params.require(:vote).permit(:upvote, :book_id)
end
end

accepts_nested_attributes_for: destroy all not included in array

Is there a clean way to destroy all children NOT included in array of passed nested attributes?
Now I have to find difference between actual children and nested attributes array, and then set _destroy: true for each, but it looks ugly.
class Report < ActiveRecord::Base
has_many :consumed_products
accepts_nested_attributes_for :consumed_products, allow_destroy: true
def nested_attributes_destroy_difference(attrs)
combined = attrs.reduce({}) {|h,pairs| pairs.each {|k,v| (h[k] ||= []) << v}; h}
diff = consumed_products - consumed_products.where(combined)
attrs + diff.map{|i| {id: i.id, _destroy: true} }
end
end
class Api::V2::ReportsController < Api::V2::BaseController
def update
report = Report.find(params[:id])
report_attributes = report_params
if params[:consumed_products]
report_attributes.merge!(consumed_products_attributes: report.nested_attributes_destroy_difference(consumed_products_attributes))
end
report.assign_attributes report_attributes
end
private
def consumed_products_attributes
params[:consumed_products].map do |p|
{product_id: p[:id], product_measure_id: p[:measure_id], quantity: p[:quantity]}
end
end
def report_params
#...
end
end

Rails - Object values not being accessible on a attribute writer method

I have a Study model which have many fields, but I'm having troubles with 1
profesion_name
so in my study model I have this
class Study < ActiveRecord::Base
attr_accessible :profesion_related, :profesion_name
attr_accessor :profesion_related
def profesion_related=(id)
if id.present?
if self.study_type_id == 4
if self.country_id == 170
#some code here
else
profesion_parent = Profesion.find(id)
new_profesion = Profesion.create({g_code: profesion_parent.g_code, mg_code: profesion_parent.mg_code, name: self.profesion_name})
self.profesion = new_profesion
end
end
end
end
end
but I'm getting an error on the line that create a Profesion, because self.profesion_name is nil
if in my controller I do this
def create
#study = Study.new(params[:study])
respond_to do |format|
#here
puts #study.to_yaml
if #study.save
.....
end
I will see in the console that profesion_name has a value
but if I do this
class Study < ActiveRecord::Base
...
def profesion_related=(id)
puts self.to_yaml
....
end
end
I can see that self.profesion_name is empty
Why could this be happening?

Relationships; Cannot get all events from DB

My models are;
class region
has_many :cities
end
class event
belongs_to :city
end
class city
has_many :events
belongs_to :region
end
Eventcontroller
def index
#region = Region.find(params[:region_id])
#cities = #region.cities
#city = City.find(params[:city_id])
#events = #city.events
#events_by_date = #events.group_by(&:start_on)
end
Page:
region/x/cities/x/events
Shows all the events from the city.
Question: how can i show all events from all the cities on my /region/events page? I created a resources
resources :regions do
resources :events do
collection do
get 'all_events'
end
end
But how I define the "all_event" action in the eventcontroller?
I cannot do this in my eventcontroller #events = #cities.events because event belongs_to :city. Is there a solution for this?
I tried this:
controller:
#region = Region.find(1)
#events = Event.includes(:city)
#events_by_date = #events.group_by(&:published_on)
View:
%ul.property_list
- #events_by_date[date].each do |event|
%li.restaurant
%span.icon
%h5
= link_to event.title, polymorphic_path([#region, event.city.name, event])
error : undefined method `region_Assisi_event_path'
If I am understanding this correctly all you would need to do:
In your controller
def all_events
#events = Event.includes(:city)
end
Then in your view, use a block to access all the instances
#events.each do |event|
event.name
event.city.name #not sure if you have a name attribute

Resources