Rails custom attribute validation not allowing creation of new object - ruby-on-rails

I have an answer model which belongs to a question which has a "correct" boolean column. Ideally a question can have only 1 correct answer ( much like the stackoverflow system).
I have the following controller + model code which uses a toggle_correct method to toggle the "correct" boolean value in the view (all of which works nicely).
When i try and create a new answer the one_correct_answer validation error is raised even though the correct column is set to default: false in the migration and the value is set to 0 (false) in the application POST trace
How can I amend my code so that a this validation only allows there to be 1 correct answer per question and doesn't interrupt the creation of a new an answer object?
answer.rb
validate :one_correct_answer
def one_correct_answer
answers = self.question.answers.map(&:correct)
errors.add(:user_id, "You can't have more than 1 correct answer #{answers}") if answers & [true]
logger.debug("Answers array #{answers}")
end
def toggle_correct(attribute)
toggle(attribute).update_attributes({attribute => self[attribute]})
end
answers_controller.rb
def correct
#answer = Answer.find(params[:id])
if #answer.toggle_correct(:correct)
respond_to do |format|
format.html { redirect_to :back, notice: "Answer submitted" }
format.js
end
end
end
_answer.html.erb
<div id="correct_answer_<%= answer.id %>" class="<%= answer.correct == true ? 'green-tick' : 'default-tick' %>">
<% if answer.question.user == current_user %>
<%= link_to "✓", correct_answer_path(answer), id: "tick", class: "correct_#{answer.id}", remote: true, method: :put %>
<% else %>
<% if answer.correct == true %>
<div id="tick", class='correct_<% answer.id %>'> ✓</div>
<% end %>
<% end %>
</div>

The reason it will fail is that you are adding an error if any answer associated to the question is correct. And you test this even though the answer you are trying to save is correct or not. So the first thing you should do is to only check if there are any correct answers if the answer you are trying to save is indeed correct, like this:
validate :one_correct_answer, if: :correct?
This way, the method one_correct_answer will only be validated if the current answer is correct.
However, you still have one additional problem. If the answer you are trying to save is correct, then the method will be called and it will add an error if there is any answer that is correct... which it will probably be since the current answer should also be listed in that association. So what you want to do is to check if there is an additional answer that is correct.
So in the end, I would probably end up validating it like this instead:
validates_uniqueness_of :correct, scope: :question_id, if: :correct?
What this will do is that it validates the unique combination of the question_id column and the correct column, but only if correct is true. That makes it so that you can have multiple false but only one true correct column per question.

Your problem is probably here:
errors.add(:user_id, "You can't have more than 1 correct answer #{answers}") if answers & [true]
answers & [true] will always return an array(since answers is an array), and blank arrays are true values in Ruby.
Even if they were false values, your condition wouldn't work, since there has to be one correct answer, and your condition would check that there are none.
I would use this condition:
self.question.answers.count(&:correct) <= 1

Related

Ruby on Rails 4, form inside a view

I am currently trying to build a view that shows a question to the user and if they answer the question right, allows the user to check a checkbox which sends a request to persist that in the database. In other words we keep track of what questions have been answered correctly by the user in the database.
Now my issue is that (besides being a complete newbie in RoR and front end in general), I don't know how to insert the checkbox (form) inside my view along with the question.
I'm using rails 4.
Thank you!
I would start adding a Boolean value to all the answers.
$ rails g migration AddCorrectToAnswers
now that migration should look like
class AddCorrectToAnswers < ActiveRecord::Migration[5.0]
def change
add_column :answers, :correct, :boolean, default: false
end
end
now we can create a new route that will mark the answers correct
# config/routes.rb
Rails.application.routes.draw do
...
resources :questions do
resources :answers do
match "/correct" => "answers#correct", :as => :correct, via: :all
end
end
...
end
Now you should have a new route
question_answer_correct /questions/:question_id/answers/:answer_id/correct(.:format) answers#correct
I am assuming that the answer are in the questions show page
now in your show page you can do something like this
# app/views/questions/show.html.erb
<% #question.answers.each do |answer| %>
<%= answer.answer %>
<%= form_for #user, :url => url_for(:controller => 'answers', :action => 'correct') %>
<%= f.label "Correct Answer" %> <br />
<%= f.check_box :correct %> <br />
....
<%end %>
<% end %>
Now the last thing you have to do is create a method in the answers controller called correct to mark the answers as correct
# app/controllers/answers_controller.rb
class AnswersControlle < ApplicationController
...
def correct
#answer = Answer.find(params[:answer_id])
#answer.correct = true
#answer.save
redirect_to :back
end
end
I hope that this helps
Happy coding
Assuming you already have form setup,
you can add one button which says submit or anything you want to diplay
<button class="check">Click me</button>
Now you can write an event listener for this button which can reveal the answer and if it matches to the answer which User has given, then fire and Ajax call to your backend and save whatever you want to.
example code:
$('.check').on('click', function(){
var real = $('.reveal-answer').val();
$('.reveal-answer').show(); // you need to protect this part from being abused
var answer = $('.user-input-answer').val(); //Assuming its a input field
var questionId = $('.question').data('question-id');
if(answer === real){
url = '/submit_answer';
data = { question_id: questionId, answer: answer };
$.post( url, data , function() {
}, 'json');
}
});
Roughly it should work like this but there are lot to do from the security prospective. For ex: You can not have all answers on view side, You should do the backend validation once the answer is submitted(to check whether it is actually correct or someone tried to hack it.)

How to get Rails 4 if a record has two conditions return true else false?

I'm setting up my vote system, and trying to have a helper model so I can check if a user has voted for a card. I'm new to rails and can't seem to figure this one out.
How do I have the model check votes for a record that has the user_id of the current_user and the card_id?
I'm also trying to limit calling the helper many times for each iteration of _cards.html.erb by setting the voted variable. Not sure how to do this, trying to set the variable is just printing true for every card, even the ones that have no votes.
Setting the variable is not working and neither is the helper, as it is always true.
cards_controller.rb:
def if_voted(card_id)
if Vote.where(:user_id => current_user.id, :card_id => card_id) then
true
else
false
end
end
helper_method :if_voted
_cards.html.erb:
<td>
<%= #voted = if_voted(card.id) %>
<% if #voted == true %>
<span class="green"><center>
<% elsif #voted == false %>
<span class="red"><center>
<% else %>
<span class="gray"><center>
<% end %>
<%= card.up_votes - card.down_votes %>
</center></span>
</td>
With the help of #tadman
cards_controller.rb
def if_voted(card_id)
if Vote.where(:user_id => current_user.id, :card_id => card_id).any? then
#vote = Vote.find_by(:user_id => current_user.id, :card_id => card_id)
return #vote.voted
else
return nil
end
end
helper_method :if_voted
_cards.html.erb
<td>
<% #voted = if_voted(card.id) %>
<% if #voted == true %>
<span class="green"><center>
<% elsif #voted == false %>
<span class="red"><center>
<% else %>
<span class="gray"><center>
<% end %>
<%= card.up_votes - card.down_votes %>
</center></span>
</td>
Thank you
The where method always returns a scope even if that scope does not contain any records. The find_by method uses the same options but returns either the first matching record or nil if none are found.
That's not quite what you want here, though. You don't actually want to retrieve any of the records, but instead just check if they exist. The any? method on a scope is true if one or more records exist, or false otherwise.
You should update your code to look like this:
def if_voted(card_id)
Vote.where(:user_id => current_user.id, :card_id => card_id).any?
end
It's worth noting a few things about your Ruby style:
Using then at the end of an if clause, while supported, is extraneous and generally not done.
Comparing things == true is usually a sign your logic is confused. If you're concerned about something being literal true rather than just logically true, use === true instead. In this case, close enough counts, so if (if_voted(...)) should suffice.
Your method returned either true or false but you had three conditions as if you were expecting a maybe to pop up one day.
Method names like if_voted are a little clumsy, especially if used inside an if. Something like has_voted? is much more in line with Ruby and Rails in general, so you get if (has_voted?(...)) which reads a lot better.
Even better would be to migrate this method into the User class so you can eliminate the helper and end up with if (current_user.has_voted?(card_id)) as a very clear way of expressing your intent.

Helper Method always returning true even when proven wrong

I have the following helper method in rails 4.
def is_blog_owner?(blog_id)
if current_user && blog_id
blog = Blog.find_by(id: blog_id)
roles = current_user.roles_for_blog(blog)
roles.each do |role|
if role.role == 'Blog-Owner'
true
end
end
end
end
It has one problem, if the roles for a current user are nil it always seems to return true.
The way this currently works is that if a current user has a role of blog owner for a specific blog, then return true.
So if I visit (as user id 1) users/1/blogs/2 I will see edit and delete as shown below in the show.html.erb How ever if I then log out and log in as user id 2 and visit users/1/blogs/2 I still see edit and delete. Which I should not.
So I threw a binding.pry after roles gets set and found roles for user id 2 on user id 1's blog id of 2 to be nil This should mean that I should not see the edit and delete buttons, but I do ... What is going on?!
<h2><%=#blog.title%> Profile</h2>
<p class="text-muted">
Welcome to your blog porfile page. Here you can manage the blog. You can edit or
delete the specific blog.
</p>
<% if is_blog_owner?(params[:id]) %>
<hr>
<h3>Action</h3>
<p class="text-muted">You can currently do the following actions: </p>
<%= link_to "Edit", edit_user_blog_path(current_user.id, #blog.id), :class => 'btn btn-success' %> |
<%= link_to "Delete", user_blog_path(current_user.id, #blog.id),
data: { confirm: "This is permenent. Are you sure?" },
:method => :delete,
:class => 'btn btn-danger'%>
<% end %>
I should ad that I did a <%= is_blog_owner?(params[:id]).inspect and got a [] returned ... oO. Should it not return false?
This construct is a problem for you:
roles.each do |role|
if role.role == 'Blog-Owner'
true
end
end
it returns the value of roles, which presumably will be an Array and thus always a true value. The standalone true inside the block is not returned, that's not how .each works. Generally you use .each to process items in an array to change, or output based on each item, or perhaps perform some side-effect based on each one. The return value is always the Array object, and not related to what you do inside the block.
Instead, you could use the method .any? which seems to match your intent:
roles.any? do |role|
role.role == 'Blog-Owner'
end
Your problem is that roles.each ... returns the enumerator it was called on - so essentially your method always returns roles.
To sort it, change it thus:
def is_blog_owner?(blog_id)
if current_user && blog_id
blog = Blog.find_by(id: blog_id)
roles = current_user.roles_for_blog(blog)
roles.each do |role|
if role.role == 'Blog-Owner'
return true
end
end
end
return false
end
but it might be better to re-write it to make more sense about what it's doing. So it's looking through the roles the current users has for the blog, and returns true if any of them are 'Blog-Owner'?
Firstly, it might be better to use some authorisation process (like CanCan) to isolate this, but if you insist on a method of your own you could streamline it with .detect:
def is_blog_owner?(blog_id)
if current_user && blog_id
blog = Blog.find_by(id: blog_id)
roles = current_user.roles_for_blog(blog)
roles.detect do |role|
role.role == 'Blog-Owner'
end
end
end
Which returns the first element in the enumerator that matches the block, otherwise it return nil if no element matches.

Is this validation almost correct?

I have a form that students are using to rank 6 classes from 1 to 6. If they select Math as "1" (the hardest), then I don't want them to be able to select another subject as the hardest. The form will obviously give them the option to select "1" for each subject, but I want to use validations to protect against submission of a form that doesn't follow instructions
This is a snippet from the form
<div class="field">
<%= f.label(:math, "Mathp") %>
<%= f.select:math, 1..6 %> </br>
</div>
<div class="field">
<%= f.label(:french, "French") %>
<%= f.select:french, 1..6 %> </br>
</div>
I was going to use a validation method like this, but I don't think the logic of it works (i.e. it doesn't guard against every possible situation) and it's probably shitty (non-functional) code too, because i'm just learning how to code (actually I've been failing at it for quite a while now). Can you suggest improvements?
validates :rank_favorites
...
def rank_favorites
unless :math != :french && :french != :history && :history != :spanish && :spanish != :art && :art != :physed return false
end
Your rank_favorites is, sadly, way off but ignorance can be fixed through learning. You're just comparing a bunch of symbols and that doesn't do anything useful (at least not as far as you're concerned), you're validator reduces to this:
unless false && false && false && false && false return false
which is equivalent to:
unless false return false
You probably want to use validate :rank_favorites (not validates) and your validator would add error messages instead of simply return a boolean:
validate :rank_favorites
#...
def rank_favorites
ranks = [math, french, history, spanish, art, physed]
if(ranks.include?(nil))
errors[:base] << 'Rank all of them you lazy person!'
elsif(ranks.uniq.length != ranks.length)
errors[:base] << 'You fail at ranking, no duplicates allowed.'
end
end
The Array#uniq method will produce a copy of your array with the duplicates removed, if the lengths don't match then something was removed and you had duplicate entries.
You might want to spend some time reading the validations guide:
Active Record Validations and Callbacks
You could always do something like this:
validate do
unless [math, french, history, spanish, art, physed].uniq.length == 6
errors.add(:base, :doh_theyre_not_unique_error)
end
end
This really feels like it could use some JS form love though.
so what you really want to do is ensure no subject gets the same ranking :)
:math != :french # => true ALWAYS because they internalized constant strings which are obviously different
If you did..
self.math != self.french # => this is comparing variables now. much better. BUT Still wrong in terms of the logic you want
How about
if [self.math,self.french,self.history,self.spanish,self.art,self.physed].uniq.sort != [1,2,3,4,5,6])
errors.add(:base,"Repeated rankings")
end

Dealing with nil in views (ie nil author in #post.author.name)

I want to show a post author's name; <% #post.author.name %> works unless author is nil. So I either use unless #post.author.nil? or add a author_name method that checks for nil as in <% #post.author_name %>. The latter I try to avoid.
The problem is that I may need to add/remove words depending on whether there is a value or not. For instance, "Posted on 1/2/3 by " would be the content if I simply display nil. I need to remove the " by " if author is nil.
Null object pattern is one way to avoid this. In your class:
def author
super || build_author
end
This way you will get an empty author no matter what. However, since you don't actually want to have an empty object sometimes when you do expect nil, you can use presenter of some kind.
class PostPresenter
def initialize(post)
#post = post
end
def post_author
(#post.author && #post.author.name) || 'Anonymous'
end
end
Another way is using try, as in #post.author.try(:name), if you can get used to that.
You can use try:
<%= #post.author.try(:name) %>
It will attempt to call the name method on #post.author if it is non-nil. Otherwise it will return nil, and no exception will be raised.
Answer to your second question: In principle there is nothing wrong with the following:
<% if #post.author %>
written by <%= #post.author.name %>
<% end %>
or
<%= "written by #{#post.author.name}" if #post.author %>
But if this is a recurring pattern, you might want to write a helper method for it.
# app/helpers/authors_helper.rb or app/helpers/people_helper.rb
class AuthorsHelper
def written_by(author)
"written by #{author.name}" if author
end
end
# in your views
<%= written_by(#post.author) %>
Write a method which accepts any variable and checks to see if it is nuil first, and if it is not displays it. Then you only have to write one method.
I found your question interesting as I have often come across similar situations, so I thought I'd try out making my first Rails plugin.
I'm afraid I haven't put in any tests yet but you can try it out http://github.com/reubenmallaby/acts_as_nothing (I'm using Ruby 1.9.1 so let me know if you get any problems in the comments or on Github!)

Resources