0

I'm looking to build a basic app that provides a score based on how enjoyable users found a given lunch.

In my view, the voting buttons look like this:

<%= form_for :lunch, url: upvote_lunch_lunch_path(params[:id]), method: :put, :html => {:class => 'form-inline'} do |f| %>
  <%= f.hidden_field :liked %>
  <%= f.hidden_field :id %>
  <%= f.submit "Like Lunch", class: "btn btn-large btn-success" %>
<% end %>

<%= form_for :lunch, url: downvote_lunch_lunch_path(params[:id]), method: :put, :html => {:class => 'form-inline'} do |f| %>
  <%= f.hidden_field :disliked %>
  <%= f.hidden_field :id %>
  <%= f.submit "Dislike Lunch", class: "btn btn-large btn-danger" %>
<% end %>

Displaying the value (in a partial) looks like this:

<%= number_to_percentage(@enjoy_score * 100, precision: 0, format: "%n") %>

and finally, in my "lunches" controller, I have the following:

def show
    @lunch = Lunch.find(params[:id])
    @provider = @lunch.provider
    @total_enjoy_votes = (@lunch.liked + @lunch.disliked)
    @enjoy_score = (@lunch.liked.to_f / @total_enjoy_votes)

  end

  def downvote_lunch
    @lunch = Lunch.find(params[:id])
    @lunch.increment!(:disliked)
    redirect_to @lunch
  end

  def upvote_lunch
    @lunch = Lunch.find(params[:id])
    @lunch.increment!(:liked)
    redirect_to @lunch
  end

Everything works as expected, so long as the database already has values for liked and disliked against that particular lunch ID. If, for example, you are the first person to attempt to answer this question (lets say just after a lunch was created) the application errors out with the message "undefined method +' for nil:NilClass" at this line:@total_enjoy_votes = (@lunch.liked + @lunch.disliked)`

Two questions:

  1. Why is it if I open the database (with sqlitebrowser) and "seed" a row of data with liked = 1 and disliked = 1 the methods will work as expected, but if I don't, it errors out? What should I do to "initialize" @lunch.liked and @lunch.disliked so that there isn't an error for the initial user?
  2. (Bonus Point) How can I keep the controller code DRY so I don't have to type @lunch = Lunch.find(params[:id]) at the beginning of every method?

Thanks so much in advance. I apologize if this is a insanely simple question.

2 Answers 2

1

1.

@total_enjoy_votes = (@lunch.liked + @lunch.disliked) errors, because @lunch.liked is nil, since it is never set to anything, as well, as @lunch.disliked

To avoid this error you should check if liked and disliked are present.

liked = @lunch.liked ? @lunch.liked : 0
disliked = @lunch.disliked ? @lunch.disliked : 0
@total_enjoy_votes = (liked + disliked)
@enjoy_score = (liked.to_f / @total_enjoy_votes)

2.

before_filter :find_lunch!, only: [ :update, :destroy ] # list of actions where to perform the method.
private  
 def find_lunch!
   @lunch = Lunch.find(params[:id])
 end

EDIT

explanation to the line: @lunch.liked ? @lunch.liked : 0

It is ternary operator which comes in handy really often.

Syntax

boolean_expression ? true_expression : false_expression

Example

grade = 88
status = grade >= 70 ? "pass" : "fail"
#=> pass

It is the same, as if I wrote something like:

if @lunch.liked.nil?
  liked = 0
else
  liked = @lunch.liked
end
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for this! What does the " : 0" do at the end of the first and second line? Also, there were two small errors in your answer. with the second line - it should be "disliked" instead of "unliked" for the 3rd line to work. 2nd - I needed to add this to the controller to prevent an additional error: @enjoy_score = (@total_enjoy_votes > 0) ? (@lunch.liked.to_f / @total_enjoy_votes) : 0
Sorry for the ill formed comment. the line unliked = @lunch.disliked ? @lunch.disliked : 0 needs to change to disliked = @lunch.disliked ? @lunch.disliked : 0 for the page to render. also you need to add this to the enjoy_score calculation: @enjoy_score = (@total_enjoy_votes > 0) ? (@lunch.liked.to_f / @total_enjoy_votes) : 0
0

First approach:-

def show
  @lunch = Lunch.find(params[:id])
  @provider = @lunch.provider
  liked = ((@lunch.liked.present? and @lunch.liked >= 0) ? @lunch.liked : 0)
  @total_enjoy_votes = liked + ((@lunch.disliked.present? and @lunch.disliked >= 0) ? @lunch.disliked : 0)
  @enjoy_score =  (@total_enjoy_votes > 0 and liked > 0) ? (@lunch.liked.to_f / @total_enjoy_votes) : 0
end

Second approach:-

In Lunch model set a calback as

class Lunch <  < ActiveRecord::Base
  before_save :initialize_liked_disliked

  private
  def initialize_liked_disliked
    if self.new_record?
      self.liked = 0
      self.disliked = 0
    end
  end
end

It will initialize both the fields with 0, then make the changes in controller as:

def show
  @lunch = Lunch.find(params[:id])
  @provider = @lunch.provider
  @total_enjoy_votes = @lunch.liked + @lunch.disliked
  @enjoy_score =  (@total_enjoy_votes > 0) ? (@lunch.liked.to_f / @total_enjoy_votes) : 0
end

2 Comments

I attempted to use the second approach, which has eliminated the error (page loads as expected as the "first" voter) because the values have been initialized in the model. However, nothing ever increments. I assume this is because in the model, you have the call back "before_save" which if I understand it properly, is saying "before each save set the liked and disliked to 0" which would never allow the liked and disliked values to increment.
Thanks. Went with the first solution, but may go back to yours to move more of the logic to the model.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.