mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-01-19 14:17:59 +01:00
poll votes
This commit is contained in:
parent
b4fbec1f66
commit
48eda4ff5d
10 changed files with 219 additions and 31 deletions
|
@ -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
|
||||||
|
|
|
@ -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 """
|
||||||
|
|
61
lib/philomena_web/controllers/topic/poll/vote_controller.ex
Normal file
61
lib/philomena_web/controllers/topic/poll/vote_controller.ex
Normal 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
|
|
@ -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
|
||||||
|
|
33
lib/philomena_web/plugs/load_poll_plug.ex
Normal file
33
lib/philomena_web/plugs/load_poll_plug.ex
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
24
lib/philomena_web/templates/topic/poll/_vote_form.html.slime
Normal file
24
lib/philomena_web/templates/topic/poll/_vote_form.html.slime
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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)])
|
||||||
|
|
Loading…
Reference in a new issue