Rails: update existing has_many through record via controller? - ruby-on-rails

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

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?

Add Conditions in the Controller to filter through records to pass the correct instance to the view

This should be a small question. I have a students table and a classifieds table in my schema. The model/table relationship is hook up in a way that when I do
#student = Student.first.classifieds.all
in the rails console I will get all the classifieds ad for this particular student
[#<Classified id: 3, ad_content: "在BC附近BU也可以、需要女生一起租房子、看了几处、俩人去租非常合算、限女生", ad_title: "BU和BC旁边的房子求室友一起租 ", student_id: 16, created_at: "2013-09-17 19:20:43", updated_at: "2013-09-17 19:49:31", location: "Allston">, #<Classified id: 1, ad_content: "Malden Towers 宽敞客厅出租,附带阳台,窗外是公寓的花园,客厅可用窗帘或木板隔开, 每月4...", ad_title: "Malden Towers 客厅出租 400/月", student_id: 16, created_at: nil, updated_at: "2013-09-17 19:47:55", location: "Malden">]
I am trying to filter through the records with specific conditions so only the records that satisfy this specific condition can be passed to the view therefore appear on that particular page.
I want to display the record only if the location is equal to malden.
in my students_controller.rb I have this
def malden_index
#student = Student.first
for classified in #student.classifieds.all
return classified if classified['location'] == 'Malden'
end
I have this in my view
<%= classified.ad_content %>
I am getting this error
undefined local variable or method `classified'
I have three questions
can I add the conditions in my view ? or does it have to be in my controller?
are my records returned to me in array data type?
What is the problem in my code? (I think its pretty straight forward)(the classified should be each record, then return the record only if the location key is equal to malden)
You shouldn't add this condition filtering in the view. It's much better when done in the controller.
The filtering can be done in multiple ways. It is usually best and fastest to let the database do the work:
#student.classifieds.where(:location => 'Malden').all
You can either forward variables by making them an instance variable #classifieds or pass it as local variable to your view with render 'malden_index', :locals => {:classifieds => #student.classifieds.all}
In general, the approach with return in your for-loop doesn't result in your desired filter. Either use my suggestion from #2 or build your array like this
#classifieds = []
for classified in #student.classifieds.all
#classifieds << classified if classified['location'] == 'Malden'
end
Or shorter and more 'ruby-way':
#classifieds = #student.classifieds.keep_if{|cf| cf['location'] == 'Malden'}
You could then access the #classifieds array in your view. Still, I very much suggest you rather use a database filter if possible.
First off: in the view you can only reach instance variables defined in the controller. So the for loop does not gather anything that is reachable in the view.
So you could fix that by doing
def malden_index
#student = Student.first
#classifieds = #student.classifieds.where('location="Malden"')
end
and in your view iterate over all the #classifieds.
Now notice: this is completely hardcoded.
I would solve this as follows: instead of using a separate index method, use the show action (of a student), check if a location is given, and if so, filter the classifieds accordingly.
That would look like this
def show
#student = Student.find(params[:id])
#classifieds = #student.classifieds
if params[:location]
#classifieds = #classifieds.where('location = ?', params[:location]
end
end
and then you would build the url as follows /students/1?location=malden.
If you then add the following route to config/routes.rb
get '/students/:id/:location', to: 'students#show'
you could improve that to /students/1/malden.
def malden_index
#student = Student.first
#classified = #student.classifieds.find_by_location("Malden")
end
In view:
<%= #classified.ad_content %>
Try this...
def malden_index
#student = Student.first
#classified = #student.classifieds.where(location: 'Malden').first
end
in view:
<%= #classified.ad_content %>

Rails, implementing voting system that only allows 1 upvote/downvote per user

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).

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.

In Rails, what is the best way to update a record or create a new one if it doesn't exist?

I have a create statement for some models, but it’s creating a record within a join table regardless of whether the record already exists.
Here is what my code looks like:
#user = User.find(current_user)
#event = Event.find(params[:id])
for interest in #event.interests
#user.choices.create(:interest => interest, :score => 4)
end
The problem is that it creates records no matter what. I would like it to create a record only if no record already exists; if a record does exist, I would like it to take the attribute of the found record and add or subtract 1.
I’ve been looking around have seen something called find_or_create_by. What does this do when it finds a record? I would like it to take the current :score attribute and add 1.
Is it possible to find or create by id? I’m not sure what attribute I would find by, since the model I’m looking at is a join model which only has id foreign keys and the score attribute.
I tried
#user.choices.find_or_create_by_user(:user => #user.id, :interest => interest, :score => 4)
but got
undefined method find_by_user
What should I do?
my_class = ClassName.find_or_initialize_by_name(name)
my_class.update_attributes({
:street_address => self.street_address,
:city_name => self.city_name,
:zip_code => self.zip_code
})
Assuming that the Choice model has a user_id (to associate with a user) and an interest_id (to associate with an interest), something like this should do the trick:
#user = User.find(current_user)
#event = Event.find(params[:id])
#event.interests.each do |interest|
choice = #user.choices.find_or_initialize_by_interest_id(interest.id) do |c|
c.score = 0 # Or whatever you want the initial value to be - 1
end
choice.score += 1
choice.save!
end
Some notes:
You don't need to include the user_id column in the find_or_*_by_*, as you've already instructed Rails to only fetch choices belonging to #user.
I'm using find_or_initialize_by_*, which is essentially the same as find_or_create_by_*, with the one key difference being that initialize doesn't actually create the record. This would be similar to Model.new as opposed to Model.create.
The block that sets c.score = 0 is only executed if the record does not exist.
choice.score += 1 will update the score value for the record, regardless if it exists or not. Hence, the default score c.score = 0 should be the initial value minus one.
Finally, choice.save! will either update the record (if it already existed) or create the initiated record (if it didn't).
find_or_create_by_user_id sounds better
Also, in Rails 3 you can do:
#user.choices.where(:user => #user.id, :interest => interest, :score => 4).first_or_create
If you're using rails 4 I don't think it creates the finder methods like it used to, so find_or_create_by_user isn't created for you. Instead you'd do it like this:
#user = User.find(current_user)
#event = Event.find(params[:id])
for interest in #event.interests
#user.choices.find_or_create_by(:interest => interest) do |c|
c.score ||= 0
c.score += 1
end
end
In Rails 4
You can use find_or_create_by to get an object(if not exist,it will create), then use update to save or update the record, the update method will persist record if it is not exist, otherwise update record.
For example
#edu = current_user.member_edu_basics.find_or_create_by(params.require(:member).permit(:school))
if #edu.update(params.require(:member).permit(:school, :majoy, :started, :ended))

Resources