Allowing users to vote on polls

We currently don't have any code in place that will actually allow anonymous users to vote, even outside of our socket code, so that's something we'll want to add in first. The reason that we're going to write in some non-socket code for voting is that everything WebSocket-based should generally have some way of manually performing whatever operations it is performing. That way the testing of your application is a little bit simpler and it also makes it easier for you to support older browsers and not just lock someone out of your application entirely.

First, we'll need to add the new route to be able to support voting on individual options. We'll make our own lives easier here by avoiding a complicated nested routing scheme that would give us a URL like /polls/:vote_id/options/:id/vote, as we can work directly with the Options and their primary key IDs. 

Open up lib/vocial_web/router.ex and add the following to our listing of resources, fetching an option by its ID:

get "/options/:id/vote", PollController, :vote 

This is referencing something that doesn't actually exist yet, so we'll need to get on that next. This is not by-the-books RESTful design, but this is by design; we want to make it dead simple for people to vote on things, and by avoiding the route of needing the vote ID, the option ID, and sending the whole thing over PUT, we can make it very simple to test our application. The nice thing about prototyping an application is that you can always improve on things later; keeping things as simple as possible as you're building up is often more important than nailing your implementation on your first pass. Now let's move on to our Poll controller over at lib/vocial_web/controllers/poll_controller.ex:

def vote(conn, %{"id" => id}) do
with {:ok, option} <- Votes.vote_on_option(id) do
conn
|> put_flash(:info, "Placed a vote for #{option.title}!")
    |> redirect(to: poll_path(conn, :index))
end
end

This is pretty standard code that we've written a hundred times, but we don't actually have a Votes.vote_on_option/1 function yet in our context. This is another important part of building up a web application; learning how to prototype effectively. Sometimes you want to build your code in a way that's more intuitive than working backward; this helps you get better at naming your functions and modules in a way that's more conducive to what real developers would actually do and expect if they're ever working with your code!

The other nice thing about this is it helps us figure out where we need to go next with our code. Every time we build a piece we create a dependency on the next piece of our implementation, which can help you avoid turning your code into a scatter-brained spaghetti code mess! Our next dependency is implementing the vote_on_option function in our context, so navigate over to lib/vocial/votes/votes.ex and we'll add our new function:

def vote_on_option(option_id) do
end

Working from the code in the controller, remember that we had implied its implementation was a single argument. Given that we suffixed our function with _on_option, it's pretty easy to infer that our code is operating on options and should take an option ID as the single argument. We'll use a with statement for this code since that tends to result in very readable code, so the next line in our function is going to look something like this:

    with option <- Repo.get!(Option, option_id),

Here we're expecting to receive an option back from our call to Repo.get!/2. When you see a bang as part of the function name, that means that if it fails to find the appropriate resource, it will error out instead of returning a nil. In our case, we don't want the user to be able to vote on a non-existing option, so that's the right choice for us. Repo.get takes in the query or schema as its first argument and the primary key (in our case, the ID) as the second argument. From the docs (for Repo.get, which shares an almost identical usage except that it does not throw the Ecto.NoResultsError if it fails):

Fetches a single struct from the data store where the primary key matches the
given ID.

We've fetched our option (assuming everything has gone well up to this point), so now we'll need to figure out the current vote count and add one to it:

votes <- option.votes + 1

Finally, we can close out our function with the actual update operation. This will very closely match the code that we already have in place to do updates for the polls themselves:

do
update_option(option, %{votes: votes})
end

Following our dependency chain down, here's yet another function that we haven't actually implemented yet! The only value that we're changing the option is the number of votes, so we just include that as the updated attributes passed into our option update function. This one is also almost identical to the other updated code, so let's implement our update_option/2 function:

def update_option(option, attrs) do
option
|> Option.changeset(attrs)
|> Repo.update()
end

That's all that we need to implement in our manual update code to serve as the actual infrastructure and backbone and make it all work! If you followed everything along you should have some new code like this in your votes context:

  def vote_on_option(option_id) do
with option <- Repo.get!(Option, option_id),
votes <- option.votes + 1
do
update_option(option, %{votes: votes})
end
end

def update_option(option, attrs) do
option
|> Option.changeset(attrs)
|> Repo.update()
end

The last piece is for us to jump into our interface and add some sort of buttons to allow users to vote on options in polls as they appear. Again, we're going to opt for the simplest implementation for this, even if it is not the most aesthetically-pleasing option!

Remember to keep starting simple and building up, instead of jumping immediately into the most complicated solutions! In all of my experience doing development, the best experiences I've ever had building projects have been the ones where I started off small and gave myself the room and agility to iterate and adjust my initial assumptions as I went along!

Let's spend some time editing lib/vocial_web/templates/poll/index.html.eex and change the option display a little bit to add our new buttons. We'll just modify the loop responsible for displaying the options instead of the entire template:

<%= for option <- poll.options do %>
<strong><%= option.title %></strong>:
<span id="vote-count-<%= option.id %>" class="vote-count"><%= option.votes %></span>
<a href="/options/<%= option.id %>/vote" class="btn btn-primary vote-button-manual" data-option-id="<%= option.id %>">Vote</a>
<br />
<% end %>

We had to change quite a bit to really make this all work the way we'll need it to later on. If we're thinking about how our voting system might be implemented, we will need a simple way to update the number of votes in the element, as well as a way to figure out/reassign what those buttons will do. To address this, we start by wrapping the count of votes inside of a span with a very specific ID (mapped to the option's primary key). This will allow us to very quickly find and change the number of votes displayed in the user's browser.

Also, the vote button has been given an extra CSS class (outside of the Bootstrap styling) called vote-button-manual. We've also assigned a data attribute to it to store the current option ID; we will need to do this to be able to modify the behavior of the button and have it interact with the server (specifically, to cast the vote for that one specific option)! For now, though, go back to your UI, and you should see something like this:

It's not perfect, but if you go through and test each of the buttons, you'll see that they DO work! You may run into one issue with the UI, though, that's a little strange: every time you vote and the page refreshes, the order of the options changes! That's because the default ordering is never guaranteed for us! We can go back to the line responsible for handling the iteration of our options and modify the for statement slightly to fix this little bug:

  <%= for option <- Enum.sort(poll.options, &(&1.id >= &2.id)) do %>

This will give us a list of sorted options by their primary key instead of whatever random ordering we're happening to get at the time, which is a more consistent experience and definitely less jarring for the user!

If you do not specify any ordering in your database queries, you should always be very cautious about making assumptions about the order that things will appear in!

In the process of doing this, we'll actually break a test back in test/vocial_web/controllers/poll_controller_test.exs, since we're changing the formatting. We'll correct the GET /polls test:

  test "GET /polls", %{conn: conn, user: user} do
{:ok, poll} =
Vocial.Votes.create_poll_with_options(%{title: "Poll 1", user_id: user.id}, [
"Choice 1",
"Choice 2",
"Choice 3"
])

conn = get(conn, "/polls")
assert html_response(conn, 200) =~ poll.title

Enum.each(poll.options, fn option ->
assert html_response(conn, 200) =~ "#{option.title}"
assert html_response(conn, 200) =~ "#{option.votes}"
end)
end

We'll want to write up a few tests to cover this new behavior (especially our Votes.vote_on_option/1 function as we'll be reusing that for our socket code as well)! First, we'll write up our new Votes context test by adding a new test to the describe "options" block in test/vocial/votes/votes_test.exs:

test "vote_on_option/1 adds a vote to a particular option", %{user: user} do
# Code goes in here...
end

We'll need the user to create an appropriate poll to use; otherwise, this is a pretty standard test. We'll use the create_poll_with_options function we made earlier to make our lives simpler and create an appropriate poll with an option for us:

with {:ok, poll} = Votes.create_poll(%{ title: "Sample Poll", user_id: user.id }),
{:ok, option} = Votes.create_option(%{ title: "Sample Choice", votes: 0, poll_id: poll.id }),
option <- Repo.preload(option, :poll)
do
# Next bits of code go here...
end

The next bit is the actual asserts of the test itself. We've created a poll, we've created an option, and now we want to verify that when we vote on the option its vote count goes up by one:

votes_before = option.votes

First, we store the current value of the option's votes before we do any further actions. This will give us our baseline assertion to see how the data changes over the course of this test. Next, we'll actually update the option with one additional vote:

{:ok, updated_option} = Votes.vote_on_option(option.id)

This will give us the output we can use to verify the overall functionality of our test. Since the function we wrote has the express and sole purpose of just adding one to the overall vote count of the options, we write that as our specific assertion:

assert (votes_before + 1) == updated_option.votes

Now we'll run our test to verify the results! Our test should be passing here, so we've verified the functionality of our context separate from that of our controller. Let's finish this functionality up by also adding a test for our controller since we're already doing a pretty good job of keeping the total test coverage decently high! Open up test/vocial_web/controllers/poll_controller_test.exs and we'll start implementing our test. First, we'll need to modify our setup block so that there is a good pre-existing poll with options we can use. Add this line (and change the return at the end of the function to include this new variable):

{:ok, poll} = Vocial.Votes.create_poll_with_options(
%{ "title" => "My New Test Poll", "user_id" => user.id },
["One", "Two", "Three"]
)
{:ok, conn: conn, user: user, poll: poll}

Now we can reference the pre-created poll by pattern matching inside of our test declarations for poll. To that point, let's now start adding our test further down in this file:

test "GET /options/:id/vote", %{conn: conn, poll: poll} do
end

We start off by declaring our test in the same way that we've gone through previously and described our tests and set them up. Next, similar to the other test that we wrote that covers this same functionality, we'll need to make it so that we can start with a baseline count of votes for a particular option:

option = Enum.at(poll.options, 0)
before_votes = option.votes

Then, we can make a request to the actual endpoint that is responsible for incrementing the vote count for that particular option, and record the newly modified option vote count:

conn = get(conn, "/options/#{option.id}/vote")
after_option = Vocial.Repo.get!(Vocial.Votes.Option, option.id)

We have everything we need as the framework for this test to be able to finally write our final assertions! We do a redirect as part of that controller, so we'll want to double-check that our returned HTTP status code is 302 and that we're getting redirected to a particular endpoint. We'll also want to verify that the number of votes increased by one:

assert html_response(conn, 302)
assert redirected_to(conn) == "/polls"
assert after_option.votes == (before_votes + 1)

If we run our controller tests we should also likewise see green tests! Our code is now ready, our test suite is green, and we can start working on the much cooler functionality by implementing some real-time application code!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset