T
We're Happier Together! NYC Devshop has joined the HappyFunCorp family! Learn more!

Devshop

Voting

Voting

Development

Voting

Posted by: Jared Rader

Friday, Jun 27th, 2014

Monday, we fellows at DevShop hit the deadline for our first project - Selfies From Last Night, a fun spin on the Texts From Last Night idea.

It provided a great way to jump in on some technologies that DevShop uses, such as the inherited resources gem and DigitalOcean.

With about an hour left until our deadline, we had one more feature we wanted to knock out - upvoting and downvoting selfies, because a selfie app just wouldn't be complete without the ability to express your utter displeasure (or delight) over random strangers' ridiculous selfies. We also wanted pages that would show the most upvoted selfies and the most downvoted selfies.

We created two models, Upvotes and Downvotes, that belong to users and selfies. We created just one votes controller to handle voting actions, of which there are only two - creating upvote and downvote records. This required a couple custom routes:

    Rails.application.routes.draw do

resources :selfies, except: :update do
resources :comments, only: [:new, :create, :destroy]
post 'upvote', to: 'votes#create_upvote'
post 'downvote', to: 'votes#create_downvote'
end
end

We didn't want the the page to reload every time a user voted on a selfie, as that would get annoyingly interrupt the user's selfie browsing experience. So we created a bit of Ajax to handle the voting and updating the page to show the number of upvotes and downvotes.

In Rails, there are actually a couple ways to do Ajax - the original way and a more Railsy way. Generally, I prefer the original way, because Ajax isn't all that difficult and I'm not sure if the abstraction Rails provides is all that worth it.

We've nested the votes action underneath our routes, so we need the ID of the specific selfie in our Ajax post request. We can get this by adding it as a data attribute on the upvote/downvote links:

    .selfie-details

%a.selfie-comments-count{ href: selfie_path(selfie) }
%i.fa.fa-comment= " #{selfie.comments_count} "
%a.selfie-upvotes{ href: '#', data: { selfie_id: "#{selfie.id}" } }
%i.fa.fa-thumbs-up
%span.upvote-count= selfie.upvotes.count
%a.selfie-downvotes{ href: '#', data: { selfie_id: "#{selfie.id}" } }
%i.fa.fa-thumbs-down
%span.downvote-count= selfie.downvotes.count

This makes it easy to post to the correct route in our Ajax call in selfies.js:

    $('.selfie-upvotes').on('click', function(e) {

e.preventDefault();

var $this = $(this);
var selfieId = $this.data('selfie-id');

$.post('/selfies/' + selfieId + '/upvote')
.done(function(resp) {
$this.find('.upvote-count').html(resp.upvotes_count);
})
})

You can see our `$.post()` action expects a response, so in our controller action, we simply respond with JSON:

    class VotesController < ApplicationController

before_action :set_selfie

def create_upvote
upvote = @selfie.upvotes.build
upvote.user = current_user

if upvote.save
render json: { upvotes_count: @selfie.upvotes.count }
end
end

def create_downvote
downvote = @selfie.downvotes.build
downvote.user = current_user

if downvote.save
render json: { downvotes_count: @selfie.downvotes.count }
end
end

private

def set_selfie
@selfie = Selfie.find(params[:selfie_id])
end

end

So that handles voting. But we've still got a couple things to do to tighten this up. For one, we want to make sure users can only submit one upvote or downvote on selfies.

Rails validations provide an easy way to handle this with scoping. In both of our vote models, we can verify the uniqueness of a vote record scoped on the selfie ID (meaning that a record with that particular selfie ID can only appear once):

    validates_uniqueness_of :user_id, scope: :selfie_id

Lastly, we need to be able to show users the Hall of Fame and Hall of Shame. We needed to show the selfies ordered by number of votes, so we created custom scopes of best and worst. We were running out of time and I couldn't quite remember the right ActiveRecord methods to use. No worry - SQL here to save the day:

    scope :best, -> { find_by_sql("SELECT selfies.*, 

COUNT(upvotes.id) AS num_upvotes FROM selfies
JOIN upvotes ON upvotes.selfie_id = selfies.id
GROUP BY selfies.id ORDER BY num_upvotes DESC")}

This creates a scope called `:best` that grabs all the selfie records and the number of upvotes for each, grouping them under their particular selfie records and ordered by number of upvotes in descending order.

Then in our view, we can make the Hall of Fame link go to our root path, passing in a custom parameter that tells our controller which scope to use:

    %ul.navigation

%li
= link_to "> HALL OF FAME", root_path(best: true)
%li
= link_to "> HALL OF SHAME", root_path(worst: true)

    def index

if params[:best]
@selfies = Selfie.best
elsif params[:worst]
@selfies = Selfie.worst
else
@selfies = Selfie.all
end
end

And there you have it - an MVP of Ajaxified upvote and downvote creation and ActiveRecord scopes to show you the best and worst selfies of all time.

Devshop

About Us

Devshop is a highly motivated group of entrepreneurs, developers, and designers that aim to work with companies that are looking for an edge. Each member of our team brings something unique to the table allowing us to cater our services specifically to meet your needs and exceed your expectations.