From 13612ba8906197aef5f5f6fe79752bc01e64d446 Mon Sep 17 00:00:00 2001 From: Tamas Soos Date: Tue, 25 Aug 2020 15:33:47 +0100 Subject: [PATCH] Adding publish/unpublish feature + endpoints --- lib/ex_poll/polls.ex | 46 ++++++++++++++++--- lib/ex_poll/polls/option.ex | 10 ++++ lib/ex_poll/polls/poll.ex | 31 +++++++++++++ lib/ex_poll/polls/vote.ex | 11 ++++- .../controllers/poll_controller.ex | 16 +++++++ lib/ex_poll_web/router.ex | 2 + lib/ex_poll_web/views/poll_view.ex | 4 +- .../20200814130356_update_polls_table.exs | 9 ++++ 8 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 priv/repo/migrations/20200814130356_update_polls_table.exs diff --git a/lib/ex_poll/polls.ex b/lib/ex_poll/polls.ex index dc9e313..f7b5526 100644 --- a/lib/ex_poll/polls.ex +++ b/lib/ex_poll/polls.ex @@ -4,9 +4,9 @@ defmodule ExPoll.Polls do """ import Ecto.Query, warn: false + alias Ecto.Multi alias ExPoll.Repo - - alias ExPoll.Polls.{Poll, Option} + alias ExPoll.Polls.{Poll, Option, Vote} # POLL @@ -39,6 +39,32 @@ defmodule ExPoll.Polls do Repo.delete(poll) end + def publish_poll(%Poll{} = poll) do + poll + |> Poll.publish_changeset() + |> Repo.update() + end + + def unpublish_poll(%Poll{} = poll) do + poll_changeset = Poll.unpublish_changeset(poll) + option_ids = Enum.map(poll.options, fn option -> option.id end) + poll_votes_query = from(v in Vote, where: v.option_id in ^option_ids) + + result = + Multi.new() + |> Multi.update(:update_poll, poll_changeset) + |> Multi.delete_all(:delete_votes, poll_votes_query) + |> Repo.transaction() + + case result do + {:ok, %{update_poll: poll}} -> + {:ok, get_poll!(poll.id)} + + {:error, _failed_operation, _failed_value, _changes_so_far} -> + {:error, "Something went wrong while unpublishing a poll"} + end + end + def change_poll(%Poll{} = poll, attrs \\ %{}) do Poll.changeset(poll, attrs) end @@ -55,14 +81,15 @@ defmodule ExPoll.Polls do def get_option!(id) do query = from o in options_query(), - where: o.id == ^id + where: o.id == ^id, + preload: [:poll] Repo.one!(query) end def create_option(%Poll{} = poll, attrs \\ %{}) do poll - |> Ecto.build_assoc(:options) + |> Ecto.build_assoc(:options, poll: poll) |> Option.changeset(attrs) |> Repo.insert() end @@ -74,7 +101,9 @@ defmodule ExPoll.Polls do end def delete_option(%Option{} = option) do - Repo.delete(option) + option + |> change_option() + |> Repo.delete() end def change_option(%Option{} = option, attrs \\ %{}) do @@ -85,7 +114,12 @@ defmodule ExPoll.Polls do def create_vote(%Option{} = option) do option - |> Ecto.build_assoc(:votes) + |> Ecto.build_assoc(:votes, option: option) + |> change_vote() |> Repo.insert() end + + def change_vote(%Vote{} = vote, attrs \\ %{}) do + Vote.changeset(vote, attrs) + end end diff --git a/lib/ex_poll/polls/option.ex b/lib/ex_poll/polls/option.ex index c1ec2ff..4ab2ac2 100644 --- a/lib/ex_poll/polls/option.ex +++ b/lib/ex_poll/polls/option.ex @@ -18,5 +18,15 @@ defmodule ExPoll.Polls.Option do |> cast(attrs, [:value]) |> validate_required([:value]) |> assoc_constraint(:poll) + |> validate_poll_is_not_published() + end + + defp validate_poll_is_not_published(changeset) do + poll = get_field(changeset, :poll) + + case poll.is_published do + true -> add_error(changeset, :is_published, "poll can't be modified when it's published") + false -> changeset + end end end diff --git a/lib/ex_poll/polls/poll.ex b/lib/ex_poll/polls/poll.ex index 12b5e83..e32791d 100644 --- a/lib/ex_poll/polls/poll.ex +++ b/lib/ex_poll/polls/poll.ex @@ -5,6 +5,7 @@ defmodule ExPoll.Polls.Poll do schema "polls" do field :question, :string + field :is_published, :boolean, default: false timestamps() has_many(:options, Option, on_replace: :delete) @@ -16,5 +17,35 @@ defmodule ExPoll.Polls.Poll do |> cast(attrs, [:question]) |> cast_assoc(:options) |> validate_required([:question]) + |> validate_poll_is_not_published() + end + + def publish_changeset(poll) do + poll + |> cast(%{is_published: true}, [:is_published]) + |> validate_min_options_count() + end + + def unpublish_changeset(poll) do + poll + |> cast(%{is_published: false}, [:is_published]) + end + + defp validate_poll_is_not_published(changeset) do + is_published = get_field(changeset, :is_published) + + case is_published do + true -> add_error(changeset, :is_published, "poll can't be modified when it's published") + false -> changeset + end + end + + defp validate_min_options_count(changeset, count \\ 2) do + options = get_field(changeset, :options, []) + + case length(options) >= count do + true -> changeset + false -> add_error(changeset, :options, "should have at least #{count} option(s)") + end end end diff --git a/lib/ex_poll/polls/vote.ex b/lib/ex_poll/polls/vote.ex index 344731f..ec403a5 100644 --- a/lib/ex_poll/polls/vote.ex +++ b/lib/ex_poll/polls/vote.ex @@ -14,6 +14,15 @@ defmodule ExPoll.Polls.Vote do vote |> cast(attrs, []) |> validate_required([]) - |> assoc_constraint(:option) + |> validate_poll_is_published() + end + + defp validate_poll_is_published(changeset) do + option = get_field(changeset, :option) + + case option.poll.is_published do + true -> changeset + false -> add_error(changeset, :is_published, "poll can't be voted on when it's unpublished") + end end end diff --git a/lib/ex_poll_web/controllers/poll_controller.ex b/lib/ex_poll_web/controllers/poll_controller.ex index dd749bb..4b2b9d2 100644 --- a/lib/ex_poll_web/controllers/poll_controller.ex +++ b/lib/ex_poll_web/controllers/poll_controller.ex @@ -40,4 +40,20 @@ defmodule ExPollWeb.PollController do send_resp(conn, :no_content, "") end end + + def publish(conn, %{"id" => id}) do + poll = Polls.get_poll!(id) + + with {:ok, %Poll{} = poll} <- Polls.publish_poll(poll) do + render(conn, "show.json", poll: poll) + end + end + + def unpublish(conn, %{"id" => id}) do + poll = Polls.get_poll!(id) + + with {:ok, %Poll{} = poll} <- Polls.unpublish_poll(poll) do + render(conn, "show.json", poll: poll) + end + end end diff --git a/lib/ex_poll_web/router.ex b/lib/ex_poll_web/router.ex index fda3297..b2b4c13 100644 --- a/lib/ex_poll_web/router.ex +++ b/lib/ex_poll_web/router.ex @@ -13,6 +13,8 @@ defmodule ExPollWeb.Router do end post("/polls/:id/vote", VoteController, :create) + post("/polls/:id/publish", PollController, :publish) + post("/polls/:id/unpublish", PollController, :unpublish) end # Enables LiveDashboard only for development diff --git a/lib/ex_poll_web/views/poll_view.ex b/lib/ex_poll_web/views/poll_view.ex index 1aeaa5e..c6a5839 100644 --- a/lib/ex_poll_web/views/poll_view.ex +++ b/lib/ex_poll_web/views/poll_view.ex @@ -13,7 +13,8 @@ defmodule ExPollWeb.PollView do def render("poll.json", %{poll: poll}) do %{ id: poll.id, - question: poll.question + question: poll.question, + is_publised: poll.is_published } end @@ -21,6 +22,7 @@ defmodule ExPollWeb.PollView do %{ id: poll.id, question: poll.question, + is_publised: poll.is_published, options: render_many(poll.options, OptionView, "option.json") } end diff --git a/priv/repo/migrations/20200814130356_update_polls_table.exs b/priv/repo/migrations/20200814130356_update_polls_table.exs new file mode 100644 index 0000000..17e525b --- /dev/null +++ b/priv/repo/migrations/20200814130356_update_polls_table.exs @@ -0,0 +1,9 @@ +defmodule ExPoll.Repo.Migrations.UpdatePollsTable do + use Ecto.Migration + + def change do + alter table(:polls) do + add :is_published, :boolean, default: false + end + end +end