Rails - Updating Boolean Attribute in a model on Create - ruby-on-rails

I'm creating an app that lets users purchase items from an online store. I followed the RailsCasts episodes, and built my OrdersController like so.
def create
#order = current_cart.build_order(order_params)
#order.ip_address = request.remote_ip
if #order.save
if #order.purchase
Item.where(email: Order.last.email).last.purchased == true
PurchaseMailer.confirmation_email(Item.last.email).deliver
flash[:notice] = "Thanks for your purchase"
redirect_to root_path
else
flash[:danger] = "Something was wrong"
redirect_to :back
end
else
render :action => 'new'
end
end
I recently decided to add an attribute to my items, which says whether or not they've been purchased or not. Items in the cart have not yet been purchased. I created a migration, giving all items a purchased attribute, that is a boolean.
By default, items are not purchased, so the default value is false.
class AddPurchasedToItem < ActiveRecord::Migration
def change
add_column :items, :purchased, :boolean, :default => false
end
end
That's why I added this line of code to my Orders#Create action.
Item.where(email: Order.last.email).last.purchased == true
Here I was setting the value of purchased from false to true. However, when I load up rails console
Item.last.purchased
=> false
It looks like the value still isn't being stored

As another response points out, you're using the == to assign a value, which isn't right. You need = instead.
And you have to save an item after you assign a value to it.
An example:
conditions = {email: Order.last.email} # using your conditions
item = Item.find_by(conditions)
item.purchased = true
item.save # this is what you're missing
Item.find(item.id).purchased # will be true
Another way to update is the following:
item.update_attribute(:purchased, true)
Yet another way is to call update_all on the ActiveRecord::Relation object like so:
# update all items that match conditions:
Item.where(conditions).update_all(purchased: true)
Which method you choose may depend on the scenario as update_all doesn't run the callbacks you specify in the model.
In your case however, all you're missing is the item.save line.

Item.where(email: Order.last.email).last.purchased == true
You're using a == operator to try to assign a value. Try using = instead.

Related

Checking record before update

Goal: To check if a record has already been updated and either allow or not allow the record to be updated if it already has been.
This is in case a buyer is on a page that doesn't have updated information and attempts to cancel an order once it's already been completed.
I have the following code, which works but also doesn't work correctly:
private
def prevent_order_update
#order = Order.find(params[:id])
if ( #order.order_status[2] || #order.order_status[3] )
redirect_to #order, notice: "Your request status for Order:#{#order.id} has already been updated."
end
end
with:
before_action :prevent_order_update, :only => [:update]
This works, but also "works" if the :order_status is 1, which is shouldn't.
I only want a block in the update IF the order status is anything but 1.
The order status is from a model enum of 1,2,3.
I have also tried using:
if ( #order.order_status[2] || #order.order_status[3] ) && #order.order_status_previously_changed?
which completely blocks the prevent_order_update from working all together.
And:
( #order.order_status[2] || #order.order_status[3] ) != #order.order_status[1]
Which then blocks my update method all together and still gives me the prevent_order_update method notice when the order status is 1
#order.order_status is corresponding to string when it comes to rails enums.
In your case, say #order.order_status is charged. When you execute #order.order_status[2] it actually produces a which is the third item of charged string. In this case the comparison always returns true.
So try the following code:
def prevent_order_update
#order = Order.find(params[:id])
if ( #order.charged? || #order.canceled? )
redirect_to #order, notice: "Your request status for Order:#{#order.id} has already been updated."
end
end
You can use aasm gem, so that you no need any before_action. You can solve it in model level using aasm transitions
https://github.com/aasm/aasm

what var type to dynamically access Model's attribute from another controller? (Rails 4.2)

Goal: dynamically update another Model's properties (Tracker) from Controller (cards_controller.rb), when cards_controller is running the def update action.
Error I receive : NameError in CardsController#update, and it calls out the 2nd last line in the
def update_tracker(card_attribute) :
updated_array = #tracker.instance_variable_get("#{string_tracker_column}")[Time.zone.now, #card.(eval(card_attribute.to_s))]
Perceived problem: I have everything working except that I don't know the appropriate way to 'call' the attribute of Tracker correctly, when using dynamic attributes.
The attribute of the Tracker is an array (using PG as db works fine), I want to
figure out what property has been changed (works)
read the corresponding property array from Tracker's model, and make a local var from it. (works I think, )
push() a new array to the local var. This new array contains the datetime (of now) and, a string (with the value of the updated string of the Card) (works)
updated the Tracker with the correct attribute.
With the following code from the cards_controller.rb
it's the if #card.deck.tracked in the update method that makes the process start
cards_controller.rb
...
def update
#card = Card.find(params[:id])
if #card.deck.tracked
detect_changes
end
if #card.update_attributes(card_params)
if #card.deck.tracked
prop_changed?
end
flash[:success] = "Card info updated."
respond_to do |format|
format.html { render 'show' }
end
else
render 'edit'
end
end
...
private
def detect_changes
#changed = []
#changed << :front if #card.front != params[:card][:front]
#changed << :hint if #card.hint != params[:card][:hint]
#changed << :back if #card.back != params[:card][:back]
end
def prop_changed?
#changed.each do |check|
#changed.include? check
puts "Following property has been changed : #{check}"
update_tracker(check)
end
end
def update_tracker(card_attribute)
tracker_attribute = case card_attribute
when :front; :front_changed
when :back; :back_changed
when :hint; :hint_changed
end
string_tracker_column = tracker_attribute.to_s
#tracker ||= Tracker.find_by(card_id: #card.id)
updated_array = #tracker.instance_variable_get("#{string_tracker_column}")[Time.zone.now, #card.(eval(card_attribute.to_s))]
#tracker.update_attribute(tracker_attribute, updated_array)
end
Edit: For clarity here's the app/models/tracker.rb:
class Tracker < ActiveRecord::Base
belongs_to :card
end
Your use of instance_variable_get has been corrected, however this approach is destined to fail because ActiveRecord column values aren't stored as individual instance variables.
You can use
#tracker[string_column_changed]
#card[card_attribute]
To retrieve attribute values by name. If you want to get an association, use public_send. The latter is also useful if there is some accessor wrapping the column value (eg carrierwave)
From your error it seem your issue is this:
#tracker.instance_variable_get("#{string_tracker_column}")
evaluates to this after string interpolation:
#tracker.instance_variable_get("front_changed")
which is incorrect use of instance_variable_get. It needs an # prepended:
#tracker.instance_variable_get("#front_changed")
Seems like using instance_variable_get is unnecessary, though, if you set attr_reader :front_changed on the Tracker model.

Ruby on Rails - Undefined methods for NilClass

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

What's the right way to modify the params passed to controller in rails 3.1.0?

In our Rails 3.1.0 app, we need to modify params passed to rfq controller in create and update. For example, we want to record the current user id under input_by_id. What we did was:
#rfq.input_by_id = session[:user_id]
It worked as expected. Also when need_report field is false, then report_language field should be nil. We decide to add the following line in rfq controller to make sure the nil is passed to report_language when need_report is false:
#rfq.report_language = nil unless params[:need_report]
However this addition causes the rspec case failure (in create/update of the controller) because of the data validation failure. However when we fire up the app, it behaves fine without saving the report_language when need_report is false. I am wondering if the line above is not the right way to use params[:need_report] for #rfq updating.
Thanks so much.
UPDATE:
Controller code:
def create
if has_create_right?
#rfq = Rfq.new(params[:rfq], :as => :roles_new )
#rfq.input_by_id = session[:user_id]
#save sales_id selected
if sales? && member? && !team_lead?
#rfq.sales_id = session[:user_id]
end
#view page may carry the hidden report language even if need_report == false
#rfq.report_language = nil unless params[:need_report]
#save into join table rfqs_standards
params[:rfq][:standard_ids].each do |sid|
#rfq.standards << Standard.find(sid.to_i) if !sid.nil? && sid.to_i > 0
end unless params[:rfq][:standard_ids].nil?
#save into join table rfqs_test_items
params[:rfq][:test_item_ids].each do |tid|
#rfq.test_items << TestItem.find(tid.to_i) if !tid.nil? && tid.to_i > 0
end unless params[:rfq][:test_item_ids].nil?
if #rfq.save!
redirect_to URI.escape("/view_handler?index=0&msg=RFQ saved!")
else
flash.now[:error] = "RFQ not saved!"
render 'new'
end
else
redirect_to URI.escape("/view_handler?index=0&msg=No rights!")
end
end
Test case failed after addition of #rfq.report_language = nil unless params[:need_report]
it "should be successful for corp head" do
session[:corp_head] = true
session[:user_id] = 1
s = Factory(:standard)
rfq = Factory.attributes_for(:rfq, :need_report => true, :report_language => 'EN')
rfq[:standard_ids] = [s.id] # attach standard_id's to mimic the POST'ed form data
get 'create', :rfq => rfq
#response.should redirect_to URI.escape("/view_handler?index=0&msg=RFQ saved!")
response.should render_template('new')
end
the problem ist that you are simply not looking at the right value.
get 'create', :rfq => rfq will result in a params-hash like {:rfq => {...}}
so you need to #rfq.report_language = nil unless params[:rfq][:need_report] == 'true'

Intercepting creation of new object

I'm adding a categorization functionality to my app and struggling with it. Objects have many categories through categorizations. I'm trying to intercept the creation of a new categorization, check if theres a similar one, if so, increment it's count, if not, create a new object. Here's what I have so far.
validate :check_unique
protected
def check_unique
categorization = Categorization.where(:category_id => self.category_id, :categorizable_id => self.categorizable_id, :categorizable_type => self.categorizable_type)
if categorization.first
categorization.first.increment(:count)
end
end
This kind of logic should not exist in the controller. This is really business domain and should be in the model. Here's how you should go about it:
categorization = Categorization.find_or_create_by_category_id_and_categorizable_id_and_categorizable_type(self.category_id, self.categorizable_id, self.categorizable_type)
categorization.increment!(:count)
find_or_create will try to find the category in the DB, and if it doesn't exist, it'll create it. Now just make sure that count defaults to zero, and this code will do what you want. (when initially created the count would be 1, then later it'll increment)
PS: I'm not sure if find_or_create has changed in rails 3. But this is the main idea
I decided to move it out of the model object and put it into the controller method creating the categorization. It now works (Yay!) and here's the code if anyone is interested.
def add_tag
object = params[:controller].classify.constantize
#item = object.find(params[:id])
#categories = Category.find(params[:category_ids])
#categories.each do |c|
categorization = #item.categorizations.find(:first, :conditions => "category_id = #{c.id}")
if categorization
categorization.increment!(:count)
else
#item.categorizations.create(:category_id => c.id, :user_id => current_user.id)
end
end
if #item.save
current_user.update_attribute(:points, current_user.points + 15) unless #item.categorizations.exists?(:user_id => current_user.id)
flash[:notice] = "Categories added"
redirect_to #item
else
flash[:notice] = "Error"
redirect_to 'categorize'
end
end

Resources