Rails, implementing voting system that only allows 1 upvote/downvote per user - ruby-on-rails

Can someone tell me why this code doesn't work? On my local server for testing, it keeps flashing "You have already upvoted this" when I haven't.
This is in my code for the votes controller.
def upvote
#vote = Vote.find(params[:post_id])
if current_user.votes.where(post_id: params[:post_id], value: 1)
flash[:notice] = "You have already upvoted this!"
redirect_to :back
else
#vote.update_attributes(value: 1)
#vote.user_id = current_user.id
end
end
Is the 4th line if current_user.votes.where(post_id: params[:post_id], value: 1) the correct way to implement the where method?

You should use exists?
if current_user.votes.where(post_id: params[:post_id], value: 1).exists?
If you use only current_user.votes.where(...), you get a Relation object that will always be interpreted as a true value in the if, even if tyhe Relation do not match any line (only false and nil are considered as falsy values in Ruby).

Related

Ruby on Rails beginner question : equality

I'm starting to know ROR and I was doing a kind of blog with articles, etc...
I did this code :
def show
id = params[:id]
list = Article.all
is_valid = false
list.all.each do |article|
if article.id == id
#is_valid = true
break
end
end
As you can see, this code just wants to check if the article ID exists or not. So I'm testing equality between id and article.id (which's a model linked to the appropriated table in the database) BUT when I try to use or display #is_valid boolean I saw that article.id == id is FALSE every time, even if article.id = 2 and id = 2. I tried to think about everything that can make this occuring, but I admit I still misunderstand this.
Then I ask you if you know why this is occuring. Of course, an equality like 2 == 2 will change #is_valid to true.
Thank you for your help !
Maybe its because params[:id] it's a string and article.id it's an Integer
(byebug) params
{"controller"=>"admin/my_controller", "action"=>"edit", "id"=>"1"}
And yes it is... "id" is a string "1", so you may try this:
def show
id = params[:id].to_i
list = Article.all
is_valid = false
list.all.each do |article|
if article.id == id
#is_valid = true
break
end
end
end
And maybe could work.
This is the answer to your question,
But if you want to learn a little more about Activerecord you can do this
Article.exists?(params[:id])
and that will do what you are trying to do just with a query against db.
and if you want to get just a simple article
record = Article.find_by(id: params[:id]) #return nil when not exist
if record # if nil will threat like false on ruby
#my code when exist
else
#my code when not exist
end
will work (you also can use find but find will throw an exception ActiveRecord::RecordNotFound when not exists so you have to catch that exception.
Activerecord has many ways to check this you dont need to do it by hand.
def show
#article = Article.find(params[:id])
end
This will create a database query which returns a single row. .find raises a ActiveRecord::NotFound exception if the record is not found. Rails catches this error and shows a 404 page. Article.find_by(id: params[:id]) is the "safe" alternative that does not raise.
Your code is problematic since list = Article.all will load all the records out of the database which is slow and will exhaust the memory on the server if you have enough articles. Its the least effective way possible to solve the task.
If you want to just test for existence use .exists? or .any?. This creates a COUNT query instead of selecting the rows.
Article.where(title: 'Hello World').exists?

Rails: update existing has_many through record via controller?

So two thirds of this works. Every time a User reads an Article, a History record is created (has_many through), which just says "User read Article at Read_Date_X".
The database is ok, the models are ok, the read_date param is permitted in the History controller, and the following operation works both 1) to check if a User has read an article before and 2) to create a new History record if it is the first time on this article.
But I cannot work out why the middle bit (to just update the read_date on an existing record) is not working. It doesn't matter if I try it with h.save! or h.update().
h = History.where(article_id: #article, user_id: current_user)
if h.exists?
h = History.where(article_id: #article, user_id: current_user)
h.read_date = Time.now
h.save!
else
h = History.new
h.article_id = #article.id
h.user_id = current_user.id
h.read_date = Time.now
h.save!
end
The error it throws if it finds an existing record is:
undefined method `read_date=' for #<History::ActiveRecord_Relation:0x007fe7f30a5e50>
UPDATE: working answer
So Derek was right, and this version works. The middle bit needed a single instance, not an array, which is what the top conditional (without .first) was checking for. Using that to return a single record, though, means you need to swap "exists?" to "present?" in the second part.
h = History.where(article_id: #article, user_id: current_user).first
if h.present?
h.read_date = Time.now
h.save!
else
h = History.new
h.article_id = #article.id
h.user_id = current_user.id
h.read_date = Time.now
h.save!
end
History.where(article_id: #article, user_id: current_user) is returning a History::ActiveRecord_Relation. If you want to set the read_date, you'll want to get a single record.
Here's one way you could do this with what you have currently:
h = History.where(article_id: #article, user_id: current_user).first
Another way you could handle this is by using find_by instead of where. This would return a single record. Like this:
h = History.find_by(article_id: #article, user_id: current_user)
However, if it's possible for a user to have many history records for an article, I would stick to the way you're doing things and make one change. If for some reason you have a lot of history records, this may not be very efficient though.
histories = History.where(article_id: #article, user_id: current_user)
histories.each { |history| history.update(read_date: Time.now) }
I realize this question is already answered. Here are a couple of additional thoughts and suggestions.
I would not have a separate read_date attribute. Just use updated_at instead. It's already there for you. And, the way your code works, read_date and updated_at will always be (essentially) the same.
When looking up whether the history exists, you can do current_user.histories.where(article: #article). IMO, that seems cleaner than: History.where(article_id: #article, user_id: current_user).first.
You can avoid all that exists? and present? business by just checking if the h assignment was successful. Thus, if h = current_user.histories.where(article: #article).
If you go the route of using updated_at instead of read_date, then you can set updated_at to Time.now by simply doing h.touch.
I would use the << method provided by has_many :through (instead of building the history record by hand). Again, if you use updated_at instead of read_date, then you can use this approach.
So, you could boil your code down to:
if h = current_user.histories.where(article: #article)
h.touch
else
current_user.articles << #article
end
You could use a ternary operator instead of that if then else, in which case it might look something like:
current_user.histories.where(article: #article).tap do |h|
h ? h.touch : current_user.articles << #article
end

Rails - Updating Boolean Attribute in a model on Create

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.

Rails Activerecord: Update where conditions... else create

I need to check multiple columns of a table to see if I find a match. If I find a match I need to "updateattributes" of the matching record with all of my form params... Else I need to add a new record with all of my form params.
if #somethingtoupdate = Model.where("column1 = ? and column2 = ?", params[:something][:column1], params[:something][:column2])
if #somethingtoupdate = Model.update_attributes(params[:something])
redirect_to somewhere_path, :notice => "The existing record was updated"
else
render "myformlocation"
end
else
#added = Model.new(params[:something])
if #added.save
redirect_to somewhere_path, :notice => "The new record was created"
else
render "myformlocation"
end
end
Update
#somethingtoupdate = Model.where("this_id = ? and that_id = ?", params[:something][:this_id], params[:something][:that_id])
if ! #somethingtoupdate.empty?
if #somethingtoupdate.update_attributes(params[:something])
redirect_to some_path, :notice => "The existing record was updated"
else
render "myformlocation"
end
else
#added = Model.new(params[:something])
if #added.save
redirect_to some_path, :notice => "The new record was created"
else
render "myformlocation"
end
end
This is where I stand now thanks to #micahbf.
But, I am still getting an error on my "update_attributes" when there is a matching record.
Seems like this should work.... What am I missing or doing wrong?
This is because where does not return nil if it doesn't find anything, it returns an empty array, which is still truthy, so the block gets executed.
You can use empty? to check whether to run the block or not.
Note also that if it finds a match, the match will still be returned inside of an array (even if there was only one match). So you will have to do something like call first on the result to take the first returned model and update it.
So, the top might look like:
#somethingtoupdate = Model.where("column1 = ? and column2 = ?", params[:something][:column1], params[:something][:column2])
if ! #somethingtoupdate.empty?
if #somethingtoupdate.first.update_attributes(params[:something])
redirect_to some_path, :notice => "The existing record was updated"
else
render "myformlocation"
end
else
// do stuff if the query found no matches
end
I think here is short method to find record and if found then update record and if record not found then create it.
#somethingtoupdate = Model.where("column1 = ? and column2 = ?", params[:something][:column1], params[:something][:column2]).first_or_initialize
#somethingtoupdate.update_attributes(params[:something])
First of all, Model.update_attributes(params[:something]) is not working (at least in Rails 3.2.12). It should be #somethingtoupdate.update_attributes(params[:something]).
Also, there is an existing method for this kind of purpose: first_or_create.
#somethingtoupdate = Model.where("column1 = ? and column2 = ?", params[:something][:column1], params[:something][:column2]).first_or_create

How do I ensure that there is only one instance of saved model with mongoid?

I have run into an issue and I think that my solution is very ugly at the moment, what is a better way I can do the following with rails/mongoid? Basically, a user can come in and provide a 'nil' answer_id, but as soon as they answer the question, we want to lock in their first, non-nil answer.
controller.rb
r = Response.new(user: current_user, question_id: qid, answer_id: aid)
r.save_now!
And the following response.rb model:
def save_now!
user = self.user
qid = self.question_id
aid = self.answer_id
resp = Response.where({user_id: user._id, question_id: qid}).first
# We accept the first answer that is non-nil,
# so a user can skip the question (answer_id=nil)
# And then return and update the answer_id from nil to 'xyz'
if resp.nil?
resp = Response.new(user: user, question_id: qid, answer_id: aid)
else
if resp.answer_id.nil? && aid.present?
resp.answer_id = aid
end
end
resp.save!
end
So I would like to allow for answer_id to be nil initially (if a user skipped the question), and then take the first answer that is non-nil.
I really don't think it's intuitive and clean to instantiate the Response object twice, once in controller and once in model but I'm not sure on the best way to do this? Thanks.
Create a unique index over (user, question_id, answer_id). This way only the first insert will succeed. Subsequent inserts will fail with an error. This eliminates the need for the find query in your save_now! method.
Remember to run this insert in safe mode, or else you won't get an exception, it will just fail silently.
Update
Seems that your problem might be solved by renaming the method. :) Take a look:
class Response
def self.save_answer(user, qid, aid)
resp = Response.where({user_id: user._id, question_id: qid}).first
if resp.nil?
resp = Response.new(user: user, question_id: qid, answer_id: aid)
else
if resp.answer_id.nil? && aid.present?
resp.answer_id = aid
end
end
resp.save!
end
end
# controller.rb
r = Response.save_answer(current_user, qid, aid)
Mongoid has a validation on uniqueness that you could use. In your case, you could create a compound index on user, question_id, and answer_id and there would be no need to write a save_answer method.
For example you could put this in the Response model:
validates_uniqueness_of :user_id, :question_id
To ensure that you can only have one response for a question per user.

Resources