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
Related
I have a resource :posts, which I show one at a time in show.html.erb
Suppose I have ten posts, each with an :id going from 1-10. If I delete post #2, then my posts will be 1,3,4,5,6,7,8,9,10. If I create ten posts and delete them all, then the next post :id would be [1,3..10,21] but I would only have 11 posts.
I want to show the post number that's in the application and put it in the view against a total number of posts. So if you were looking at post #3, it might have an :id of 3, but it is post #2 in the database.
Here's what I tried so far:
posts_controller.rb
def show
...
#post = Post.friendly.find(params[:id])
#total_posts = Post.all.count.to_i
#posts_array = Post.pluck(:id).to_a
...
end
views/posts/show.html.erb
<%= #post.id %> of <%= #total_posts %> /
models/post.rb
def next
Post.where("id > ?", id).order(id: :asc).limit(1).first
end
def prev
Post.where("id < ?", id).order(id: :desc).limit(1).first
end
However, showing the :id of a resource is a security issue so I don't know how to do it better.
How can I make it so the show.html.erb view only shows the current index order of the total amount of resources as compared to the post_id?
An efficient way to do this could be
# app/controllers/posts_controller.rb
def show
#post = Post.friendly.find(params[:id])
#total_posts = Post.count
#post_index = Post.where("id <= ?", #post.id).count
end
# app/views/posts/show.html.erb
. . .
<%= #post_index %> of <%= #total_posts %>
. . .
You should avoid loading all posts (or even their id) if you can. This will become more and more expensive as the number of posts grows and will eventually become a bad bottleneck for performance.
If you're trying to find the 'array index' of a record (so to speak) you can do this:
Agency.order(id: :asc).offset(params[:index]).limit(1)
You don't really want to do any other way because then it will load EVERY record into rails which will be very slow. It's better to ask the database for only a single record (which is what 'offset' does). Just replace params[:index] with whatever the name of the params is, whether its params[:id], etc.
I did just want to address one thing you said:
However, showing the :id of a resource is a security issue so I don't know how to do it better
That's not a security issue. The app should be designed in a way where the ID of a resource is not special or "secret." If you have an ID of a record, your controller should work such that it "authorizes" certain actions and won't let you do something you're not supposed to (like a user deleting a post).
If you REALLY need to do this, then just hide the ID and use a slug instead, like example.com/this-is-a-post-slug. This can be done quite easily
Edit To answer your specific question...
ids = Agency.order(id: :asc).pluck(:id)
#post_index = ids.find_index(#post.id)
#next_post = ids[#post_index + 1]
#prev_post = ids[#post_index - 1]
You can now use #post_index in your view.
Note: #prev_post and #next_post will be nil when the page doesn't exist (i.e. the "next post" when you're on the last page), so you will need to check that.
Just try it:
def show
...
#post = Post.friendly.find(params[:id])
#total_posts = Post.count # this will return integer type data
#posts_array = Post.pluck(:id) # you don't need to_a as .pluck returns array
...
For the next part you could write:
def next
self.class.where("id > ?", id).limit(1).first # this use of id is secured.
end
def prev
self.class.where("id < ?", id).order(id: :desc).limit(1).first
end
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 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.
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.