Skip to content

Commit

Permalink
Move Vote context content into RankedVoting.
Browse files Browse the repository at this point in the history
  • Loading branch information
zorn committed Jul 25, 2024
1 parent d79d09c commit 093cb71
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 188 deletions.
44 changes: 38 additions & 6 deletions lib/flick/ranked_voting.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ defmodule Flick.RankedVoting do
"""

alias Flick.RankedVoting.Ballot
alias Flick.RankedVoting.Vote
alias Flick.Repo

@typep changeset :: Ecto.Changeset.t(Ballot.t())

@doc """
Creates a new `Flick.RankedVoting.Ballot` entity with the given `title` and `questions`.
"""
@spec create_ballot(map()) :: {:ok, Ballot.t()} | {:error, changeset()}
@spec create_ballot(map()) :: {:ok, Ballot.t()} | {:error, Ecto.Changeset.t(Ballot.t())}
def create_ballot(attrs) when is_map(attrs) do
%Ballot{}
|> change_ballot(attrs)
Expand All @@ -25,7 +24,7 @@ defmodule Flick.RankedVoting do
"""
@spec update_ballot(Ballot.t(), map()) ::
{:ok, Ballot.t()}
| {:error, changeset()}
| {:error, Ecto.Changeset.t(Ballot.t())}
| {:error, :can_not_update_published_ballot}
def update_ballot(%Ballot{published_at: published_at}, _attrs)
when not is_nil(published_at) do
Expand All @@ -46,7 +45,7 @@ defmodule Flick.RankedVoting do
"""
@spec publish_ballot(Ballot.t(), DateTime.t()) ::
{:ok, Ballot.t()}
| {:error, changeset()}
| {:error, Ecto.Changeset.t(Ballot.t())}
| {:error, :ballot_already_published}
def publish_ballot(ballot, published_at \\ DateTime.utc_now())

Expand Down Expand Up @@ -96,8 +95,41 @@ defmodule Flick.RankedVoting do
@doc """
Returns an `Ecto.Changeset` representing changes to a `Flick.RankedVoting.Ballot` entity.
"""
@spec change_ballot(Ballot.t() | Ballot.struct_t(), map()) :: changeset()
@spec change_ballot(Ballot.t() | Ballot.struct_t(), map()) :: Ecto.Changeset.t(Ballot.t())
def change_ballot(%Ballot{} = ballot, attrs) do
Ballot.changeset(ballot, attrs)
end

@doc """
Records a vote for the given `Flick.RankedVoting.Ballot` entity.
"""
@spec record_vote(Ballot.t(), map()) :: {:ok, Vote.t()} | {:error, Ecto.Changeset.t(Vote.t())}
def record_vote(ballot, attrs) do
attrs = Map.put(attrs, "ballot_id", ballot.id)

%Vote{}
|> Vote.changeset(attrs)
|> Repo.insert()
end

@doc """
Returns an `Ecto.Changeset` representing changes to a `Flick.RankedVoting.Vote`
entity.
## Options
* `:action` - An optional atom applied to the changeset, useful for forms that
look to a changeset's action to influence form behavior.
"""
@spec change_vote(Vote.t() | Vote.struct_t(), map()) :: Ecto.Changeset.t(Vote.t())
def change_vote(%Vote{} = vote, attrs, opts \\ []) do
opts = Keyword.validate!(opts, action: nil)
changeset = Vote.changeset(vote, attrs)

if opts[:action] do
Map.put(changeset, :action, opts[:action])
else
changeset
end
end
end
2 changes: 1 addition & 1 deletion lib/flick/ranked_voting/ranked_answer.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Flick.Votes.RankedAnswer do
defmodule Flick.RankedVoting.RankedAnswer do
@moduledoc """
An embedded value that represents a ranked answer to a question of a ballot.
"""
Expand Down
2 changes: 1 addition & 1 deletion lib/flick/ranked_voting/vote.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule Flick.RankedVoting.Vote do
import FlickWeb.Gettext

alias Flick.RankedVoting.Ballot
alias Flick.Votes.RankedAnswer
alias Flick.RankedVoting.RankedAnswer

@type id :: Ecto.UUID.t()

Expand Down
45 changes: 0 additions & 45 deletions lib/flick/votes.ex

This file was deleted.

7 changes: 3 additions & 4 deletions lib/flick_web/live/vote/vote_capture_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ defmodule FlickWeb.Vote.VoteCaptureLive do

alias Flick.RankedVoting
alias Flick.RankedVoting.Ballot
alias Flick.Votes
alias Flick.RankedVoting.Vote
alias Phoenix.LiveView.Socket

Expand All @@ -35,20 +34,20 @@ defmodule FlickWeb.Vote.VoteCaptureLive do
ranked_answers = Enum.map(1..ranked_answer_count, fn _ -> %{value: nil} end)

vote_params = %{ballot_id: ballot.id, ranked_answers: ranked_answers}
changeset = Votes.change_vote(%Vote{}, vote_params)
changeset = RankedVoting.change_vote(%Vote{}, vote_params)
assign(socket, form: to_form(changeset))
end

@impl Phoenix.LiveView
def handle_event("validate", %{"vote" => vote_params}, socket) do
changeset = Votes.change_vote(%Vote{}, vote_params, action: :validate)
changeset = RankedVoting.change_vote(%Vote{}, vote_params, action: :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end

def handle_event("save", %{"vote" => vote_params}, socket) do
%{ballot: ballot} = socket.assigns

case Votes.record_vote(ballot, vote_params) do
case RankedVoting.record_vote(ballot, vote_params) do
{:ok, _vote} ->
socket
|> put_flash(:info, "Vote recorded.")
Expand Down
2 changes: 1 addition & 1 deletion priv/repo/seeds.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
})

{:ok, _vote} =
Flick.Votes.record_vote(ballot, %{
Flick.RankedVoting.record_vote(ballot, %{
"ranked_answers" => [
%{"value" => "Turkey"},
%{"value" => "Roast Beef"},
Expand Down
127 changes: 126 additions & 1 deletion test/flick/ranked_voting_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ defmodule Flick.RankedVotingTest do

alias Flick.RankedVoting
alias Flick.RankedVoting.Ballot
alias Flick.RankedVoting.Vote
alias Flick.RankedVoting.RankedAnswer

describe "create_ballot/1" do
test "success: creates a unpublished ballot that is retrievable from the repo" do
Expand Down Expand Up @@ -157,7 +159,9 @@ defmodule Flick.RankedVotingTest do
describe "fetch_ballot/1" do
test "success: returns a ballot" do
%Ballot{id: id, question_title: question_title} = ballot_fixture()
assert {:ok, %Ballot{id: ^id, question_title: ^question_title}} = RankedVoting.fetch_ballot(id)

assert {:ok, %Ballot{id: ^id, question_title: ^question_title}} =
RankedVoting.fetch_ballot(id)
end

test "failure: returns `:not_found` when the ballot does not exist" do
Expand All @@ -176,4 +180,125 @@ defmodule Flick.RankedVotingTest do
} = RankedVoting.change_ballot(ballot, change)
end
end

describe "record_vote/2" do
setup do
ballot =
ballot_fixture(
question_title: "What's for dinner?",
possible_answers: "Pizza, Tacos, Sushi, Burgers"
)

{:ok, ballot} = RankedVoting.publish_ballot(ballot)

{:ok, published_ballot: ballot}
end

test "success: creates a vote recording the passed in answers", ~M{published_ballot} do
published_ballot_id = published_ballot.id

assert {:ok, vote} =
RankedVoting.record_vote(published_ballot, %{
"ranked_answers" => [
%{"value" => "Tacos"},
%{"value" => "Pizza"},
%{"value" => "Burgers"},
%{"value" => "Sushi"}
]
})

assert %Vote{
ballot_id: ^published_ballot_id,
ranked_answers: [
%RankedAnswer{value: "Tacos"},
%RankedAnswer{value: "Pizza"},
%RankedAnswer{value: "Burgers"},
%RankedAnswer{value: "Sushi"}
]
} = vote
end

test "success: a vote does not need to rank every possible answer", ~M{published_ballot} do
published_ballot_id = published_ballot.id

assert {:ok, vote} =
RankedVoting.record_vote(published_ballot, %{
"ranked_answers" => [
%{"value" => "Sushi"},
%{"value" => "Pizza"},
%{"value" => ""},
%{"value" => ""}
]
})

assert %Vote{
ballot_id: ^published_ballot_id,
ranked_answers: [
%RankedAnswer{value: "Sushi"},
%RankedAnswer{value: "Pizza"},
%RankedAnswer{value: nil},
%RankedAnswer{value: nil}
]
} = vote
end

test "failure: a vote should not include an answer value that is not present in the ballot",
%{
published_ballot: published_ballot
} do
attrs = %{
"ranked_answers" => [
%{"value" => "Forbidden Hot Dogs"},
%{"value" => "Illegal Cookies"}
]
}

assert {:error, changeset} = RankedVoting.record_vote(published_ballot, attrs)

assert "invalid answers: Forbidden Hot Dogs, Illegal Cookies" in errors_on(changeset).ranked_answers
end

test "failure: a vote should not include duplicate answer values",
%{
published_ballot: published_ballot
} do
attrs = %{
"ranked_answers" => [
%{"value" => "Pizza"},
%{"value" => "Tacos"},
%{"value" => "Pizza"}
]
}

assert {:error, changeset} = RankedVoting.record_vote(published_ballot, attrs)
%Ecto.Changeset{changes: %{ranked_answers: ranked_answers_changesets}} = changeset
pizza_1 = Enum.at(ranked_answers_changesets, 0)
tacos = Enum.at(ranked_answers_changesets, 1)
pizza_2 = Enum.at(ranked_answers_changesets, 2)

assert "answers must not be duplicated" in errors_on(pizza_1).value
assert %{} == errors_on(tacos)
assert "answers must not be duplicated" in errors_on(pizza_2).value
end

test "failure: a vote needs to include at least one ranked answer", ~M{published_ballot} do
attrs = %{
"ranked_answers" => [
%{"value" => ""},
%{"value" => ""},
%{"value" => ""},
%{"value" => ""}
]
}

assert {:error, changeset} = RankedVoting.record_vote(published_ballot, attrs)
%Ecto.Changeset{changes: %{ranked_answers: ranked_answers_changesets}} = changeset
first_ranked_answer = Enum.at(ranked_answers_changesets, 0)
assert "first answer is required" in errors_on(first_ranked_answer).value
end
end

describe "change_vote/2" do
# TODO
end
end
Loading

0 comments on commit 093cb71

Please sign in to comment.