poll votes

This commit is contained in:
byte[] 2019-12-19 22:41:19 -05:00
parent b4fbec1f66
commit 48eda4ff5d
10 changed files with 219 additions and 31 deletions

View file

@ -2,10 +2,12 @@ defmodule Philomena.PollOptions.PollOption do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Philomena.PollVotes.PollVote
alias Philomena.Polls.Poll alias Philomena.Polls.Poll
schema "poll_options" do schema "poll_options" do
belongs_to :poll, Poll belongs_to :poll, Poll
has_many :poll_votes, PollVote
field :label, :string field :label, :string
field :vote_count, :integer, default: 0 field :vote_count, :integer, default: 0

View file

@ -4,22 +4,12 @@ defmodule Philomena.PollVotes do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Ecto.Multi
alias Philomena.Repo alias Philomena.Repo
alias Philomena.Polls.Poll
alias Philomena.PollVotes.PollVote alias Philomena.PollVotes.PollVote
alias Philomena.PollOptions.PollOption
@doc """
Returns the list of poll_votes.
## Examples
iex> list_poll_votes()
[%PollVote{}, ...]
"""
def list_poll_votes do
Repo.all(PollVote)
end
@doc """ @doc """
Gets a single poll_vote. Gets a single poll_vote.
@ -49,10 +39,70 @@ defmodule Philomena.PollVotes do
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def create_poll_vote(attrs \\ %{}) do def create_poll_votes(user, poll, attrs) do
%PollVote{} now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|> PollVote.changeset(attrs) poll_votes = filter_options(user, poll, now, attrs)
|> Repo.insert()
Multi.new()
|> Multi.run(:existing_votes, fn _repo, _changes ->
# Don't proceed if any votes exist
case voted?(poll, user) do
true -> {:error, []}
_false -> {:ok, []}
end
end)
|> Multi.run(:new_votes, fn repo, _changes ->
{_count, votes} =
repo.insert_all(PollVote, poll_votes, returning: true)
{:ok, votes}
end)
|> Multi.run(:update_option_counts, fn repo, %{new_votes: new_votes} ->
option_ids = Enum.map(new_votes, & &1.poll_option_id)
{count, nil} =
PollOption
|> where([po], po.id in ^option_ids)
|> repo.update_all(inc: [vote_count: 1])
{:ok, count}
end)
|> Multi.run(:update_poll_votes_count, fn repo, %{new_votes: new_votes} ->
length = length(new_votes)
{count, nil} =
Poll
|> where(id: ^poll.id)
|> repo.update_all(inc: [total_votes: length])
{:ok, count}
end)
|> Repo.isolated_transaction(:serializable)
end
defp filter_options(user, poll, now, %{"option_ids" => options}) when is_list(options) do
# TODO: enforce integrity at the constraint level
votes =
options
|> Enum.map(&String.to_integer/1)
|> Enum.uniq()
|> Enum.map(&%{poll_option_id: &1, user_id: user.id, created_at: now})
case poll.vote_method do
"single" -> Enum.take(votes, 1)
_other -> votes
end
end
defp filter_options(_user, _poll, _now, _attrs), do: []
def voted?(nil, _user), do: false
def voted?(_poll, nil), do: false
def voted?(%{id: poll_id}, %{id: user_id}) do
PollVote
|> join(:inner, [pv], _ in assoc(pv, :poll_option))
|> where([pv, po], po.poll_id == ^poll_id and pv.user_id == ^user_id)
|> Repo.exists?()
end end
@doc """ @doc """

View file

@ -0,0 +1,61 @@
defmodule PhilomenaWeb.Topic.Poll.VoteController do
use PhilomenaWeb, :controller
alias Philomena.Forums.Forum
alias Philomena.PollOptions.PollOption
alias Philomena.PollVotes
alias Philomena.Repo
import Ecto.Query
plug :verify_authorized when action in [:index, :delete]
plug :load_and_authorize_resource, model: Forum, id_name: "forum_id", id_field: "short_name", persisted: true
plug PhilomenaWeb.LoadTopicPlug
plug PhilomenaWeb.LoadPollPlug
def index(conn, _params) do
poll = conn.assigns.poll
options =
PollOption
|> where(poll_id: ^poll.id)
|> preload(poll_votes: :user)
|> Repo.all()
render(conn, "index.html", layout: false, options: options)
end
def create(conn, %{"poll" => poll_params}) do
poll = conn.assigns.poll
topic = conn.assigns.topic
case PollVotes.create_poll_votes(conn.assigns.current_user, poll, poll_params) do
{:ok, _votes} ->
conn
|> put_flash(:info, "Your vote has been recorded.")
|> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic))
_error ->
conn
|> put_flash(:error, "Your vote was not recorded.")
|> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic))
end
end
def delete(conn, %{"id" => poll_vote_id}) do
topic = conn.assigns.topic
poll_vote = PollVotes.get_poll_vote!(poll_vote_id)
{:ok, _poll_vote} = PollVotes.delete_poll_vote(poll_vote)
conn
|> put_flash(:info, "Vote successfully removed.")
|> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic))
end
defp verify_authorized(conn, _opts) do
case Canada.Can.can?(conn.assigns.current_user, :index, PollVote) do
true -> conn
_false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
end

View file

@ -4,6 +4,7 @@ defmodule PhilomenaWeb.TopicController do
alias PhilomenaWeb.NotificationCountPlug alias PhilomenaWeb.NotificationCountPlug
alias Philomena.{Forums.Forum, Topics.Topic, Posts.Post, Polls.Poll, PollOptions.PollOption} alias Philomena.{Forums.Forum, Topics.Topic, Posts.Post, Polls.Poll, PollOptions.PollOption}
alias Philomena.{Forums, Topics, Posts} alias Philomena.{Forums, Topics, Posts}
alias Philomena.PollVotes
alias Philomena.Textile.Renderer alias Philomena.Textile.Renderer
alias Philomena.Repo alias Philomena.Repo
import Ecto.Query import Ecto.Query
@ -68,13 +69,16 @@ defmodule PhilomenaWeb.TopicController do
watching = watching =
Topics.subscribed?(topic, conn.assigns.current_user) Topics.subscribed?(topic, conn.assigns.current_user)
voted =
PollVotes.voted?(topic.poll, conn.assigns.current_user)
changeset = changeset =
%Post{} %Post{}
|> Posts.change_post() |> Posts.change_post()
title = "#{topic.title} - #{forum.name} - Forums" title = "#{topic.title} - #{forum.name} - Forums"
render(conn, "show.html", title: title, posts: posts, changeset: changeset, watching: watching) render(conn, "show.html", title: title, posts: posts, changeset: changeset, watching: watching, voted: voted)
end end
def new(conn, _params) do def new(conn, _params) do

View file

@ -0,0 +1,33 @@
defmodule PhilomenaWeb.LoadPollPlug do
alias Philomena.Polls.Poll
alias Philomena.Repo
import Plug.Conn, only: [assign: 3]
import Canada.Can, only: [can?: 3]
import Ecto.Query
def init(opts),
do: opts
def call(%{assigns: %{topic: topic}} = conn, opts) do
show_hidden = Keyword.get(opts, :show_hidden, false)
Poll
|> where(topic_id: ^topic.id)
|> Repo.one()
|> maybe_hide_poll(conn, show_hidden)
end
defp maybe_hide_poll(nil, conn, _show_hidden),
do: PhilomenaWeb.NotFoundPlug.call(conn)
defp maybe_hide_poll(%{hidden_from_users: false} = poll, conn, _show_hidden),
do: assign(conn, :poll, poll)
defp maybe_hide_poll(poll, %{assigns: %{current_user: user}} = conn, show_hidden) do
case show_hidden or can?(user, :show, poll) do
true -> assign(conn, :poll, poll)
false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
end

View file

@ -146,6 +146,7 @@ defmodule PhilomenaWeb.Router do
resources "/hide", Topic.Post.HideController, only: [:create, :delete], singleton: true resources "/hide", Topic.Post.HideController, only: [:create, :delete], singleton: true
resources "/delete", Topic.Post.DeleteController, only: [:create], singleton: true resources "/delete", Topic.Post.DeleteController, only: [:create], singleton: true
end end
resources "/poll/votes", Topic.Poll.VoteController, as: :poll_vote, only: [:index, :create, :delete]
end end
resources "/subscription", Forum.SubscriptionController, only: [:create, :delete], singleton: true resources "/subscription", Forum.SubscriptionController, only: [:create, :delete], singleton: true

View file

@ -1,13 +1,19 @@
= if @poll.hidden_from_users do = cond do
.walloftext - @poll.hidden_from_users ->
.block.block--fixed.block--warning .walloftext
h1 This poll has been deleted .block.block--fixed.block--warning
p h1 This poll has been deleted
' Reason: p
strong ' Reason:
= @poll.deletion_reason || "Unknown (likely deleted in error). Please contact a moderator." strong
= @poll.deletion_reason || "Unknown (likely deleted in error). Please contact a moderator."
- else - not @voted and not is_nil(@conn.assigns.current_user) ->
.poll .poll
.poll-area .poll-area
= render PhilomenaWeb.Topic.PollView, "_results.html", poll: @poll, conn: @conn = render PhilomenaWeb.Topic.PollView, "_vote_form.html", assigns
- true ->
.poll
.poll-area
= render PhilomenaWeb.Topic.PollView, "_results.html", assigns

View file

@ -0,0 +1,24 @@
= form_for :poll_vote, Routes.forum_topic_poll_vote_path(@conn, :create, @forum, @topic), [class: "poll-vote-form"], fn _f ->
h4.poll__header
' Poll:
= @poll.title
.poll-form__options
elixir:
input_type = input_type(@poll)
input_name = input_name(@poll)
require_answer? = require_answer?(@poll)
= for option <- @poll.options do
label.poll-form__options__label
input.button--separate-right> type=input_type name=input_name value=option.id required=require_answer?
span = option.label
.poll-form__status
p
' Voting ends
= pretty_time(@poll.active_until)
button.button.button--state-success>
i.fa.fa-check>
' Vote

View file

@ -54,7 +54,7 @@ h1 = @topic.title
= if not @topic.hidden_from_users or can?(@conn, :hide, @topic) do = if not @topic.hidden_from_users or can?(@conn, :hide, @topic) do
/ Display the poll, if any / Display the poll, if any
= if @topic.poll do = if @topic.poll do
= render PhilomenaWeb.Topic.PollView, "_display.html", poll: @topic.poll, conn: @conn = render PhilomenaWeb.Topic.PollView, "_display.html", Map.put(assigns, :poll, @topic.poll)
/ The actual posts / The actual posts
.posts-area .posts-area

View file

@ -16,6 +16,13 @@ defmodule PhilomenaWeb.Topic.PollView do
not poll.hidden_from_users and DateTime.diff(poll.active_until, DateTime.utc_now()) > 0 not poll.hidden_from_users and DateTime.diff(poll.active_until, DateTime.utc_now()) > 0
end end
def require_answer?(%{vote_method: vote_method}), do: vote_method == "single"
def input_type(%{vote_method: "single"}), do: "radio"
def input_type(_poll), do: "checkbox"
def input_name(_poll), do: "poll[option_ids][]"
def percent_of_total(_option, %{total_votes: 0}), do: "0%" def percent_of_total(_option, %{total_votes: 0}), do: "0%"
def percent_of_total(%{vote_count: vote_count}, %{total_votes: total_votes}) do def percent_of_total(%{vote_count: vote_count}, %{total_votes: total_votes}) do
:io_lib.format("~.2f%", [(vote_count / total_votes * 100)]) :io_lib.format("~.2f%", [(vote_count / total_votes * 100)])