Destroying the result of two subtracted arrays of objects - ruby-on-rails

A little bit of background:
Every product has a set of variants, the amount of variants a product has should change depending on its attributes.
A method called find_valid_variants returns the variants that the product should have, this is then passed to the destroy_redundant_variants method which is supposed to destroy any variants that the product has but are not included in the valid_variants.
def self.find_valid_variants(product)
product.product_options.blank? ? options = [nil] : options = product.product_options
product.product_sides.blank? ? sides = [nil] : sides = product.product_sides
product.product_sizes.blank? ? sizes = [nil] : sizes = product.product_sizes
valid_variants = []
sides.product(sizes, options).each do |side, size, option|
valid_variants << Variant.where(product_id: product.id, product_side_id: side, product_size_id: size, product_option_id: option)
end
valid_variants
end
def self.find_redundant_variants(product)
destroy_redundant_variants(product, find_valid_variants(product))
end
def self.destroy_redundant_variants(product, valid_variants)
(product.variants - valid_variants).each { |valid_variant| valid_variant.destroy }
end
The problem is that the method destroys all of the products variants when in my test case it should only destroy 2.
Here is the Rspec test:
it "destroys the redundant variants" do
#product = Fabricate(:product, product_size_ids: ["#{#product_size.id}", "#{#product_size_2.id}"], product_options_attributes: [{name: 'Rounded', description: 'Add a round edge'}, {name: 'None', description: 'Nothing'}])
#product.variants.count.should == 4
#product.product_options.each { |p| p.destroy }
#product.reload
ProductVariationService.find_redundant_variants(#product)
#product.variants.count.should == 2
end

Related

Refactor if-else with a logic of exists condition check

I have this two methods I want to refactor
def construct_discount_hash(product_adjustments)
discounts = {
events: {},
subjects: {},
products: {}
}
# product_adjustments is a model
# This is a problem from legacy database structure where event_id, subject_id, product_id is in their own column
product_adjustments.each do |adjustment|
if (adjustment.event_id.present?)
discounts[:events][adjustment.event_id] = {'$' => adjustment.amount, '%' => adjustment.percentage}
end
if (adjustment.subject_id.present?)
discounts[:subjects][adjustment.subject_id] = {'$' => adjustment.amount, '%' => adjustment.percentage}
end
if (adjustment.product_id.present?)
discounts[:products][adjustment.product_id] = {'$' => adjustment.amount, '%' => adjustment.percentage}
end
end
discounts
end
and I will use above method results within below method
# discounts is a hash generated from above code, item is a rails model
def calculate_order_content_price(discounts, item)
product = item.product
if (item.product_variant.present?)
price = item.product_variant.price
else
price = product.price
end
price_adjustments = {}
popped_from = []
if (discounts[:products][item.product_id])
price_adjustments = discounts[:products][item.product_id]
discounts[:products].delete(item.product_id)
popped_from = [:products, item.product_id]
elsif (discounts[:subjects][product.subject_id])
price_adjustments = discounts[:subjects][product.subject_id]
discounts[:subjects].delete(product.subject_id)
popped_from = [:subjects, product.subject_id]
elsif (discounts[:events][product.event_id])
price_adjustments = discounts[:events][product.event_id]
discounts[:events].delete(product.event_id)
popped_from = [:events, product.event_id]
end
if (adjustment = price_adjustments['$'])
adjusted_price = price + adjustment
elsif (adjustment = price_adjustments['%'])
adjusted_price = price + price * (adjustment / 100.0)
discounts[popped_from[0]][popped_from[1]] = price_adjustments
else
adjusted_price = price
end
{ price: adjusted_price, discount: (price - adjusted_price) }
end
I know from above code there's a lot of code-smell there. Firstly I'm thinking that the if-else logic can be refactored somehow. Can someone give an advice a technique that I can use to refactor that if-else condition? I'm confused because of the if-condition is checking for the existences of the value.
Any suggestions will be helpful.
I've tried to simplify a little bit your construct_discount_hash with two helpers:
def amount_and_percentage(adjustment, model)
{ adjustment.attributes["#{model}_id"] => { '$': adjustment.amount, '%': adjustment.percentage } }
end
def construct_discount_hash(product_adjustments)
product_adjustments.each_with_object({}) do |adjustment, hash|
case
when adjustment.event_id.present?
hash[:event] = amount_and_percentage(adjustment, 'event')
when adjustment.subject_id.present?
hash[:subject] = amount_and_percentage(adjustment, 'subject')
when adjustment.product_id.present?
hash[:product] = amount_and_percentage(adjustment, 'product')
end
end
end
A refacto for you.
def construct_discount_hash(product_adjustments)
# product_adjustments is a model
# This is a problem from legacy database structure where event_id, subject_id, product_id is in their own column
product_adjustments.each do |adjustment|
adjustement_hash = {'$' => adjustment.amount, '%' => adjustment.percentage} if adjustement
(discounts ||= {})[:events][adjustment.event_id] = adjustement_hash if adjustment.event_id.present?
(discounts ||= {})[:subjects][adjustment.subject_id] = adjustement_hash if adjustment.subject_id.present?
(discounts ||= {})[:products][adjustment.product_id] = adjustement_hash if adjustment.product_id.present?
end
discounts
end
I hope it will help you. Have fun.

Clean up messy code that query's based on multiple options

I'm using Rails, but the underlying question here applies more broadly. I have a report page on my web app that allows the user to specify what they're filtering on, and query the database based on those filters (MongoDB).
The data is based around hotels, the user must first select the regions of the hotels (state_one, state_two, state_three), then the statuses of the hotels (planning, under_construction, operational), then an optional criteria, price range (200, 300, 400). Users can select multiple of each of these options.
My way of doing this currently is to create an empty array, iterate through each region, and push the region into the array if the user selected that region. Then, I'm iterating through THAT array, and assessing the status of the hotels in those regions, if any hotel has the status the user has selected, then I'm adding that hotel to a new empty array. Then I do the same thing for price range.
This works, but the code is offensively messy, here's an example of the code:
def find_hotel
hotels = find_all_hotels
first_array = []
hotels.each do |hotel|
if params[:options][:region].include? 'state_one' and hotel.state == :one
first_array.push(hotel)
elsif params[:options][:region].include? 'state_two' and hotel.state == :two
first_array.push(hotel)
elsif params[:options][:region].include? 'state_three' and hotel.state == :three
first_array.push(hotel)
end
end
second_array = []
first_array.each do |hotel|
if params[:options][:region].include? 'planning' and hotel.status == :planning
first_array.push(hotel)
elsif params[:options][:region].include? 'under_construction' and hotel.status == :under_construction
first_array.push(hotel)
elsif params[:options][:region].include? 'operational' and hotel.status == :operational
first_array.push(hotel)
end
end
third_array = []
second_array.each do |hotel|
# More of the same here, this could go on forever
end
end
What are some better ways of achieving this?
How about this:
STATES = [:one, :two, :three]
STATUSES = [:planning, :under_construction, :operational]
PRICES = [200, 300, 400]
def find_hotel
region = params[:options][:region]
first_array = set_array(region, find_all_hotels, STATES, :state)
second_array = set_array(region, first_array, STATUSES, :status)
third_array = set_array(region, second_array, PRICES, :price_range)
end
def set_array(region, array, options, attribute)
array.each_with_object([]) do |element, result|
options.each do |option|
result << element if region.include?(option) && element[attribute] == option
end
end
end
UPDATE
Added attribute parameter to set_array in order to make the code work with your updated example.
Since second_array is empty, whatever you get by iterating over it (perhaps third_array) would also be empty.
def find_hotel
hotels = find_all_hotels
first_array = hotels
.select{|hotel| params[:options][:region].include?("state_#{hotel.state}")}
first_array += first_array
.select{|hotel| params[:options][:region].include?(hotel.status.to_s)}
second_array = third_array = []
...
end

Dynamically creating an object from the product of a variable number of arrays

I'm trying to create a Variant from a Product and its associations. The following code works if the product has some associated sides, sizes and options but this is not always the case. If any one of those are not provided then #product produces nil.
def self.create_from_product_attrs(product_id, sides, sizes, options)
sides.product(sizes, options).collect do |side, size, option|
Variant.create(product_side_id: side.id, product_size_id: size.id, product_option_id: option.id, product_id: product_id)
end
end
I have also tried:
array = [sides, sizes, options]
array.first.product(*array[1..-1]).map(&:flatten).collect do |side, size, option|
Variant.create(product_side_id: side.id, product_size_id: size.id, product_option_id: option.id, product_id: product_id)
end
in an attempt to exclude any empty values being passed to #product.
I have had more success with the following but its still failing some tests:
array = [sides, sizes, options]
array.reject! { |c| c.empty? }
if array.empty?
Variant.create(product_id: product_id)
else
array.first.product(*array.drop(1)).map(&:flatten).collect do |side, size, option|
variant = Variant.new
variant.assign_attributes(product_side_id: side.id) if side.present?
variant.assign_attributes(product_size_id: size.id) if size.present?
variant.assign_attributes(product_option_id: option.id) if option.present?
variant.assign_attributes(product_id: product_id)
variant.save
end
end
Updated to include Rspec tests:
before do
#product_side = Fabricate(:product_side)
#product_side_2 = Fabricate(:product_side, name: '2 Sides')
#product_size = Fabricate(:product_size)
#product_size_2 = Fabricate(:product_size, name: 'A3')
end
it "should create a product variant when the product is created" do
#product = Fabricate(:product)
#product.variants.count.should == 1
end
it "should create 2 variants for a product with 2 sides" do
#product = Fabricate(:product, product_side_ids: ["#{#product_side.id}", "#{#product_side_2.id}"])
#product.variants.count.should == 2
end
it "should create 2 variants for a product with 2 sizes" do
#product = Fabricate(:product, product_size_ids: ["#{#product_size.id}", "#{#product_size_2.id}"])
#product.variants.count.should == 2
end
it "should create 2 variants for a product with 2 options" do
#product = Fabricate.build(:product)
#product_option = Fabricate(:product_option, product_id: #product.id)
#product_option_2 = Fabricate(:product_option, name: 'None', product_id: #product.id)
#product.save
#product.variants.count.should == 2
end
it "should create 4 variants for a product with 2 sides and 2 options" do
#product = Fabricate.build(:product, product_side_ids: ["#{#product_side.id}", "#{#product_side_2.id}"])
#product_option = Fabricate(:product_option, product_id: #product.id)
#product_option_2 = Fabricate(:product_option, name: 'None', product_id: #product.id)
#product.save
#product.variants.count.should == 4
end
it "should create 8 variants for a product with 2 sides and 2 options and 2 sizes" do
#product = Fabricate(:product, product_side_ids: ["#{#product_side.id}", "#{#product_side_2.id}"], product_size_ids: ["#{#product_size.id}", "#{#product_size_2.id}"])
#product_option = #product.product_options.create(name: 'Rounded', description: 'Add a round edge')
#product_option_2 = #product.product_options.create(name: 'None', description: 'Nothing')
#product.save
#product.variants.count.should == 8
end
What you want to do is build an array with a nil element for each empty set, and iterate over the sets. Instead of assigning the id, just assign the object directly (which should work just as well in ActiveRecord).
def self.create_from_product_attrs(product_id, sides, sizes, options)
sides = [nil] if sides.blank?
sizes = [nil] if sizes.blank?
options = [nil] if options.blank?
sides.product(sizes, options).each do |side, size, option|
Variant.create(product_id: product_id, product_side: side, product_size: size, product_option: option)
end
end

autocomplete through Array of strings in Rails 4

I'm using this gist to build autocomplete functionality in my Rails app.
I'm saving record in Shoe model attribute like below
"nike air, nike steam,nike softy ,nike strength" #comma separated words
My controller code is below
def shoes
shoes_list = []
shoes = Shoe.all
shoes.each do |shoe|
shoes_list << shoe.model.split(',')
end unless shoes.blank?
if params[:term]
like = "%".concat(params[:term].concat("%"))
# shoes = Shoe.where("model like ?", like)
# **How i make like query to "shoes_list" same like above commented line?**
else
shoes = Shoe.all
end
list = shoes.map { |u| Hash[id: u.id, label: u.model, model: u.model] }
render json: list
end
How do I render it in json format?
At last this code works for me.
def shoes
shoes_list = []
shoes = Shoe.all
shoes.each do |shoe|
shoes_list << shoe.model.split(',')
end unless shoes.blank?
shoes_list.flatten!
if params[:term]
shoes = shoes_list.grep(Regexp.new( Regexp.escape(params[:term]), "i" ))
else
shoes = shoes_list
end
list = shoes.map {|u| Hash[id: u, label: u, name: u]}
render json: list
end
Also see How get value from array of strings in Ruby 2.0

Repacking query result from multiple models. Rails-way

I have a controller that renders json. Here's code:
class AppLaunchDataController < ApiController
def index
service_types = []
vendors = []
tariffs = []
fields = []
vendors_hash = {}
service_types_hash = {}
tariffs_hash = {}
fields_hash = {}
#service_types = ServiceType.select("title, id").all.each do |service_type|
service_types_hash = {id: service_type.id, title: service_type.title}
service_types << service_types_hash
#vendors = service_type.vendors.select("title, id").all.each do |vendor|
vendors_hash = {id: vendor.id, title: vendor.title}
vendors << vendors_hash
#tariff = vendor.tariffs.select("title, id").all.each do |tariff|
tariffs_hash = {id: tariff.id, title: tariff.title}
tariffs << tariffs_hash
#fields = tariff.fields.select("id, current_value, value_list").all.each do |field|
fields_hash = {id: field.id, current_value: field.current_value, value_list: field.value_list}
fields << fields_hash
end
tariffs_hash[:fields] = fields
fields = []
end
vendors_hash[:tariffs] = tariffs
tariffs = []
end
service_types_hash[:vendors] = vendors
vendors = []
end
render json: service_types
end
end
Return value looks like this:
[{"id":1,"title":"Water",
"vendors":[{"id":1,"title":"Vendor_1",
"tariffs":[{"id":1,"title":"Unlim",
"fields":[{"id":1,"current_value":"200","value_list":null},{"id":2,"current_value":"Value_1","value_list":"Value_1, Value_2, Value_3"}]},{"id":2,"title":"Volume",
"fields":[]}]},
{"id":2,"title":"Vendor_2",
"tariffs":[]}]},
{"id":2,"title":"Gas",
"vendors":[]},
{"id":3,"title":"Internet",
"vendors":[]}]
It works, but I'm sure there's another (more rails-) way to get the result.
If anyone dealt with it before, please help. Thanks.
just use
# for eager-loading :
#service_types = ServiceType.includes( vendors: {tariffs: :fields} )
# now for the json :
#service_types.to_json( include: {vendors: {include: {tariffs: { include: :fields}}}} )
if your ServiceType object will always have this kind of representation, just override the model's as_json method:
class ServiceType
def as_json( options={} )
super( {include: :vendors }.merge(options) ) # vendors, etc.
end
end
this is encouraged way to do it in rails : calling to_json on the model will just call as_json, possibly with additional options. In fact, as_json describes the canonical json representation for this model. See the api dock on to_json for more insight.
If your needs are more peculiar ( as using selects for a faster query ), you can always roll your own to_json_for_app_launch_data method on the model (using or not as_json), or even better on a presenter

Resources