If a user updates the quantity of an item, I want to save the cart (so I can redirect to it) and destroy that line item. I also want to check to make sure that there is enough inventory in stock of that item as well, if they were to increase the quantity. So my update function looks like this (trust that the response functions work, as they seem to. It cuts the code about in half so it's easier to view):
def update
#line_item = LineItem.find(params[:id])
#line_item_new_qty = params[:item][:qty]
unless LineItem.get_inventory_check?( #line_item.id , params[:item][:qty] )
respond 'Insufficient Inventory'
else
if #line_item_new_qty == 0
#line_item_cart = #line_item.cart
#line_item.destroy
respond 'Item Deleted'
else
respond 'Quantity Updated'
end
end
end
get_inventory_check seems to be working, if there is enough, it changes and saves the quantity of the line_item, and return true. Otherwise, it strictly returns false.
I have tried putting the if == 0 statement in every spot possible, and the only thing that I can get to work is if I have #line_item.qty == 0, then it will delete it will first update it to 0, then if you try to update it from their deletes it. I assume that means that it can't tell that the line_item has been updated in the get_inventory_check method, but using params in the if statement doesn't work either.
More than likely, params[:item][:qty] is a String and therefore #line_item_new_qty == 0 will return false. One way to potentially fix this is by converting #line_item_new_qty to an integer:
if #line_item_new_qty.to_i == 0
Alternatively you could just compare it to "0", but in general, the problem is likely one of comparing different types.
Related
The Use Case
If users haven't filled their box with products up to their credit limit (6 by default), a method is called on the box model which fills it for them.
Code guide
The number of credits in the box is given by box_credits, which loops through all products in the box and returns the total value of them. This seems to work.
The boolean method box_filled? checks if the box_credits method is equal to or greater than the number of credits available (the subscription credits).
The fill_once method should add products to the box until the box is filled (box_filled? returns true). This will happen when box_credits equals the number of credits available.
The Code
def fill_once
unless self.box_filled?
# Get a random product from the user's recommendations
product = self.subscription.user.recommended_product_records[rand(self.subscription.user.recommended_product_records.length - 1)]
# Make sure the product hasn't already been included in the box
unless self.added_product_ids.include? product.id
# If fresh, add the product to the box, size-dependently
unless product.sample_price_credits.nil?
product.add_to_box_credits(self.subscription, "sample")
else
unless product.full_price_credits.nil?
product.add_to_box_credits(self.subscription, "full")
end
end
self.save!
end
self.fill_once # Here's the recursion
end
end
The box_filled? method looks like this:
def box_filled?
subscription = self.subscription
if self.box_credits >= subscription.credits
return true
else
return false
end
end
box_credits are determined by this method:
def box_credits
count = 0
unless self.added_product_hashes.nil?
# Takes product hashes in the form {id, size, method}
self.added_product_hashes.each do |product_hash|
# Add credits to the count accordingly
if product_hash["method"] == "credits"
# Depending on the product size, add the corresponding amount of credits
if product_hash["size"] == "sample"
# Get the credit cost for a product sample
cost = Product.find(product_hash["id"].to_i).sample_price_credits
count += cost
elsif product_hash["size"] == "full"
# Get the credit cost for a full product
cost = Product.find(product_hash["id"].to_i).full_price_credits
count += cost
else
next
end
else
next
end
end
end
return count
end
The Problem
fill_once runs forever: it seems to ignore the unless self.box_filled? conditional.
Attempted solutions
I tried removing the recursive call to fill_once from the fill_once method, and split it into an until loop (until box_filled? ... fill_once ...), but no joy.
Update
Multiple identical products are being added, too. I believe the issue is that the updated record isn't being operated on – only the original instance. E.g. unless self.added_product_ids.include? product.id checks against the original box instance, not the updated record, sees no products in the added_product_ids, and chucks in every product it finds.
Solution
OK, this is solved. As suspected, the updated record wasn't being passed into the iterator. Here's how I solved it:
# Add one random user recommended product to the box
def fill_once(box=self)
unless box.box_filled?
# Get a random product from the user's recommendations
product = box.subscription.user.recommended_product_records[rand(box.subscription.user.recommended_product_records.length - 1)]
# Make sure the product hasn't already been included in the box
unless box.added_product_ids.include? product.id
# If fresh, add the product to the box, size-dependently
unless product.sample_price_credits.nil?
box = product.add_to_box_credits(box.subscription, "sample")
else
unless product.full_price_credits.nil?
box = product.add_to_box_credits(box.subscription, "full")
end
end
end
fill_once(box)
end
end
Using Ruby's default arguments with a default of self, but the option to use the updated record instead, allows me to pass the record through the flow as many times as needed.
unless self.added_product_ids.include? product.id means no duplicate product will be add to box. So if all products recommend is add to box but the total credits is less than box_credits , may cause infinite loop. I'm not sure, but it could be the reason.
You could add
puts "Box credits #{self.box_credits} vs. credits: #{self.subscription.credits} "
before
self.fill_once # Here's the recursion
to see if this happens.
Solution
OK, this is solved. As suspected, the updated record wasn't being passed into the iterator. Here's how I solved it:
# Add one random user recommended product to the box
def fill_once(box=self)
unless box.box_filled?
# Get a random product from the user's recommendations
product = box.subscription.user.recommended_product_records[rand(box.subscription.user.recommended_product_records.length - 1)]
# Make sure the product hasn't already been included in the box
unless box.added_product_ids.include? product.id
# If fresh, add the product to the box, size-dependently
unless product.sample_price_credits.nil?
box = product.add_to_box_credits(box.subscription, "sample")
else
unless product.full_price_credits.nil?
box = product.add_to_box_credits(box.subscription, "full")
end
end
end
fill_once(box)
end
end
Using Ruby's default arguments with a default of self, but the option to use the updated record instead, allows me to pass the record through the flow as many times as needed.
I'm creating a picture-rating app where users can click on pictures and rate them on a scale from 1 to 5. I'm trying to calculate the average rating of a picture. Before when users clicked on a rating value, that value became the picture's rating.
Rating: 5
If a user clicked on 1, the rating would change to 1
Rating: 1
When reality, the rating should have been 3.
(5 + 1) / 2
=> 3
Here's what I've accomplished so far in implementing this feature.
I added a migration to create two new columns for my Pictures Table
rails g migration AddRatingsToPictures ratings_count: integer, rating_total: integer
Both the new attributes, ratings_count and rating_total are integer types, meaning they are assigned a nil value at default.
p = Picture.first
p.attribute_names
=> ['id', 'title', 'category', 'stars', 'updated_at', 'created_at',
'ratings_count', 'rating_total']
p.ratings_count
=> nil
p.rating_total
=> nil
My only problem is the NilClass Error.
Here is my update method in my PicturesController.
def update
#picture = Picture.find(params[:id])
#picture.ratings_count = 0 if #picture.stars.nil?
#picture.rating_total = #picture.stars
#picture.rating_total += #picture.stars if #picture.stars_changed?
#picture.ratings_count += 1 if #picture.rating_total_changed?
if #picture.update_attributes(picture_params)
unless current_user.pictures.include?(#picture)
#picture = Picture.find(params[:id])
current_user.pictures << #picture
redirect_to #picture, :flash => { :success => "Thank you! This picture has been added to your Favorites List" }
else
redirect_to :action => 'index'
flash[:success] = 'Thank you! This picture has been updated'
end
else
render 'edit'
end
end
Here is my picture_param method in my PicturesController
def picture_params
params.require(:picture).permit(:title, :category, :genre, :stars)
end
Here is what the two new columns do
ratings_count: Calculates the number of times a picture has been rated
rating_total: Calculates the sum of the stars a picture has received
In the above code, I first set the ratings_count to 0 if the picture doesn't have a rating. This means that the picture hasn't been rated yet.
I then need to initially set the rating_total to the number of stars a picture has. If a user changed the star rating, I would add those stars to the rating_total. And if the total increased, that's my cue to increase the number of ratings.
Obviously, to calculate the average, I'd do something like this.
(#picture.rating_total / #picture.ratings_count).to_f
Now, I think I have the right idea but I know why this doesn't work. When columns are created with an integer value, by default they are set to nil. This leads to a NilClass Error when I load the web page.
undefined method `/' for nil:NilClass
Here is my code in the View
<li><strong>Rating:</strong> <%= pluralize((#picture.rating_total / #picture.ratings_count), 'Star') %></li>
Ok, the main reason it is not working is because
you fetch the picture
you check the stars from the database, and the NOT the passed form-parameters
you do update_attributes, which if I am not mistaken, used to set attributes and then save the complete object, but since rails 4 only updates the passed attributes (which is what you would expect)
One small remark: keeping the rating correct is a function I would place in the model, NOT in the controller.
Furthermore, how to handle the if nil, initialise to zero I wrote a short blogpost about. In short: overrule the getter.
So I would propose the following solution. In your model write
class Picture < ActiveRecord::Base
def ratings_count
self[:ratings_count] || 0
end
def ratings_total
self[:ratings_total] || 0
end
def add_rating(rating)
return if rating.nil? || rating == 0
self.ratings_count += 1
self.ratings_total += rating
self.stars = self.ratings_total.to_f / self.ratings_count
self.save
end
def rating
return 0 if self.ratings_count == 0
self.ratings_total.to_f / self.ratings_count
end
and then the code in your controller becomes much cleaner and readable:
def update
#picture = Picture.find(params[:id])
stars = picture_params.delete(:stars)
if #picture.update_attributes(picture_params)
#picture.add_rating stars
unless current_user.pictures.include?(#picture)
current_user.pictures << #picture
redirect_to #picture, :flash => { :success => "Thank you! This picture has been added to your Favorites List" }
else
redirect_to :action => 'index'
flash[:success] = 'Thank you! This picture has been updated'
end
else
render 'edit'
end
end
I first delete the :stars from the parameters, because I do not want to save those, I want to use those for the add_rating. I then try to update_attributes, which will fail if there are any failing validations, and if that is ok, I will add_rating which itself will handle nil or zero correctly. Well granted: I do not know how you handle a "non-rating" (nil? zero?). It is possible a rating of zero should be added, because it will add a rating, but most UI I know do not allow to select 0 as rating, so you might want to change the zero handling.
This will handle the case of uninitialized (nil) values in your attributes...
def update
#picture = Picture.find(params[:id])
if #picture.stars_changed?
#picture.ratings_count = (#picture.ratings_count || 0) + 1
#picture.rating_total = (#picture.rating_total || 0) + ( #picture.stars || 0)
end
You don't need an array of ratings or ratings persisted to database, assuming you only count votes where the rating changes, you can accumulate the count and the total and divide the two (which is, in fact, what you're doing so I'm preaching to the converted).
Although it seems to me that if I change a picture from 5 to 1 and it only changes to 3, I'm gonna keep clicking 1 :)
You could set the default value on the migration when you created it. But no worries, you can create a new migration to change it:
# Console
rails g migration change_default_for_ratings_count_and_rating_total
# Migration Code
class ChangeDefaultForRatingsCountAndRatingTotal < ActiveRecord::Migration
def change
change_column :pictures, :ratings_count, :integer, default: 0
change_column :pictures, :rating_total, :integer, default: 0
end
end
Keep in mind that some databases don't automatically assign newly updated default values to existing column entries, so maybe you will have to iterate over every picture already created with nil values and set to 0.
Ok, an alternative...
Do an after_initialize so the fields are never, never, ever nil. Even if you're creating a new Picture object, they'll be initialized as zero. Problem will go away.
class Picture << ActiveRecord::Base
after_initialize do |picture|
picture.ratings_count ||= 0
picture.rating_total ||= 0
end
...
end
I hope someone can help. I suspect this is an embarrassingly newbie question, but then, I'm embarassingly new. I've tried everywhere I can think of for an answer to this and tried every permutation I can think of.
Task:
I'm working through an example in a Ruby tutorial that manages a online shopping cart. The task is to create a button next to each item displayed in the cart, enabling you to decrement the amount of a single item by 1.
Problem:
When I click on the "Decrement" button in my view next to an item with a quantity of one, it disappears from the cart, as expected. But when I click on the button next to an item with a quantity of more than one, nothing happens.
Code
View:
<td><%= button_to("Decrement", decrement_line_item_path(line_item), method: :post ) %>
</td>
(I have set a member route in routes.rb so that it accesses the decrement action on "POST")
Controller:
def decrement
#cart = set_cart
#line_item = #cart.line_items.find_by(params[:id])
#line_item = #cart.decrement_line_item_quantity(#line_item.id)
respond_to do |format|
format.html {redirect_to store_url}
end
end
(there's a set_cart method in a concerns file that just gets or sets the cart based on a session, and I know that works).
Model:
def decrement_line_item_quantity(item_id)
item_to_decrement = line_items.find_by(id: item_id)
if item_to_decrement.quantity > 1
item_to_decrement.quantity -= 1
else
item_to_decrement.destroy
end
item_to_decrement
end
My debugger says that:
item_to_decrement.quantity decreases in the model method, as expected
#line_item.quantity is also decreased in the controller method, as expected
But the value for the line item's quantity in the actual cart appears to be unchanged in the controller method. I check #cart.line_items.find_by(params[:id]).quantity in my debugger after returning from the model method, and it hasn't changed.
This means that when I come to render my cart later, the quantity of an item with multiple units is not decreased, and so nothing happens.
Can someone tell me what I missed? This is the very first time I've used Stack Overflow, so I'm still learning. Have read the guidelines, and I hope I'm following them appropriately. Apologies for any transgressions and thanks so much in advance.
You have to save the record after you decrement the quantity, otherwise the change in the quantity will not get persisted to the database:
def decrement_line_item_quantity(item_id)
item_to_decrement = line_items.find_by(id: item_id)
if item_to_decrement.quantity > 1
item_to_decrement.quantity -= 1
item_to_decrement.save
else
item_to_decrement.destroy
end
item_to_decrement
end
I am having problems with modifying a function in spree. The function is called copy_price
The original version is something like this:
def copy_price
if variant
self.price = variant.price if price.nil?
self.currency = variant.currency if currency.nil?
end
end
which if I understand right will update the line_item's unit price only if the price is null, which I believe it shouldn't be inside the orders page (after the order is completed).
I noticed that order changes if the master price is changed inside the admin section even after the order is complete.
So i thought that the copy_price function was to blame, but each time i try to modify it there is no change.
E.g.
def copy_price
#price_run = true
self.price = 30.00
end
def get_price_run
if #price_run == true
return "true"
else
return "false"
end
end
and call get_price_run inside my view to print out if the price run was actually run. and It keeps outputting false. Does anyone know why that would be.
I have figured out the problem. The function copy_price is only called when the line item is first created (e.g. when you put it into the cart). So when I was trying to find out if it was called while looking at the admin orders page it was never getting called.
I currently have the following code:
events.detect do |event|
#detect does the block until the statement goes false
self.event_status(event) == "no status"
end
What this does is output the instance of event (where events is a string of different Models that all collectively call Events) when the event_status method outputs a "no status".
I would like the output to also include the value for delay where:
delay = delay + contact.event_delay(event)
event_delay method hasn't been written, but it would be similar (maybe redundant but I'll deal with that later) to event_status in looking at the delay between when an event was done and when it was supposed to be done.
Here is how event_status looks currently for reference:
def event_status target
# check Ticket #78 for source
target_class= target.class.name
target_id = target_class.foreign_key.to_sym
assoc_name = "contact_#{target_class.tableize}"
r = send(assoc_name).send("find_by_#{target_id}", target.id)
return "no status" unless r
"sent (#{r.date_sent.to_s(:long)})"
end
My concept of output should be [event,delay] so that, for example, I can access it as Array[:event] or Array[:delay] to get at the value.
****I was thinking maybe I should use yield on a method, but haven't quite put the pieces together (should the block passed to the method be the delay =+ for example, I think it is).**
I am not wed to the .detect method, it's what I started with and it appears to work, but it isn't allowing me to run the tally alongside it.
It's not entirely clear what you're asking for, but it sounds like you're trying to add up a delay until you reach a certain condition, and return the record that triggered the condition at the same time.
You might approach that using Enumerable#detect like you have, but by keeping a tally on the side:
def next_event_info
next_event = nil
delay = 0
events.detect do |event|
case (self.event_status(event))
when "no status"
true
else
delay += contact.event_delay(event)
false
end
end
[ next_event, delay ]
end
Update for if you want to add up all delays for all events, but also find the first event with the status of "no status":
def next_event_info
next_event = nil
delay = 0.0
events.each do |event|
case (self.event_status(event))
when "no status"
# Only assign to next_event if it has not been previously
# assigned in this method call.
next_event ||= event
end
# Tally up the delays for all events, converting to floating
# point to ensure they're not native DB number types.
delay += contact.event_delay(event).to_f
end
{
:event => next_event,
:delay => delay
}
end
This will give you a Hash in return that you can interrogate as info[:event] or info[:delay]. Keep in mind to not abuse this method, for example:
# Each of these makes a method call, which is somewhat expensive
next_event = next_event_info[:event]
delay_to_event = next_event_info[:delay]
This will make two calls to this method, both of which will iterate over all the records and do the calculations. If you need to use it this way, you might as well make a special purpose function for each operation, or cache the result in a variable and use that:
# Make the method call once, save the results
event_info = next_event_info
# Use these results as required
next_event = event_info[:event]
delay_to_event = event_info[:delay]