Use checkboxes and radio buttons on poll forms

Our original interface to vote in a poll had a few issues:

* Since there was no button to send the form, it wasn't clear that
  selecting an option would automatically store it in the database.
* The interface was almost identical for single-choice questions and
  multiple-choice questions, which made it hard to know which type of
  question we were answering.
* Adding other type of questions, like open answers, was hard since we
  would have to add a different submit button for each answer.

So we're now using radio buttons for single-choice questions and
checkboxes for multiple-choice questions, which are the native controls
designed for these purposes, and a button to send the whole form.

Since we don't have a database table for poll ballots like we have for
budget ballots, we're adding a new `Poll::WebVote` model to manage poll
ballots. We're using WebVote instead of Ballot or Vote because they
could be mistaken with other vote classes.

Note that browsers don't allow removing answers with radio buttons, so
once somebody has voted in a single-choice question, they can't remove
the vote unless they manually edit their HTML. This is the same behavior
we had before commit 7df0e9a96.

As mentioned in c2010f975, we're now adding the `ChangeByZero` rubocop
rule, since we've removed the test that used `and change`.
This commit is contained in:
Javi Martín
2024-05-16 02:43:55 +02:00
parent fd14c55615
commit a7e1b42b6c
35 changed files with 612 additions and 687 deletions

View File

@@ -110,12 +110,6 @@ module Abilities
can :answer, Poll do |poll|
poll.answerable_by?(user)
end
can :answer, Poll::Question do |question|
question.answerable_by?(user)
end
can :destroy, Poll::Answer do |answer|
answer.author == user && answer.question.answerable_by?(user)
end
end
can [:create, :show], ProposalNotification, proposal: { author_id: user.id }

View File

@@ -17,32 +17,13 @@ class Poll::Answer < ApplicationRecord
scope :by_author, ->(author_id) { where(author_id: author_id) }
scope :by_question, ->(question_id) { where(question_id: question_id) }
def save_and_record_voter_participation
author.with_lock do
save!
Poll::Voter.find_or_create_by!(user: author, poll: poll, origin: "web")
end
end
def destroy_and_remove_voter_participation
transaction do
destroy!
if author.poll_answers.where(question_id: poll.question_ids).none?
Poll::Voter.find_by(user: author, poll: poll, origin: "web").destroy!
end
end
end
private
def max_votes
return if !question || !author || persisted?
author.with_lock do
if question.answers.by_author(author).count >= question.max_votes
errors.add(:answer, "Maximum number of votes per user exceeded")
end
if question.answers.by_author(author).count >= question.max_votes
errors.add(:answer, "Maximum number of votes per user exceeded")
end
end
end

View File

@@ -41,8 +41,6 @@ class Poll::Question < ApplicationRecord
end
end
delegate :answerable_by?, to: :poll
def options_total_votes
question_options.reduce(0) { |total, question_option| total + question_option.total_votes }
end

View File

@@ -0,0 +1,43 @@
class Poll::WebVote
attr_reader :poll, :user
def initialize(poll, user)
@poll = poll
@user = user
end
def questions
poll.questions.for_render.sort_for_list
end
def update(params)
all_valid = true
user.with_lock do
unless questions.any? { |question| params.dig(question.id.to_s, :option_id).present? }
Poll::Voter.find_by(user: user, poll: poll, origin: "web")&.destroy!
end
questions.each do |question|
question.answers.where(author: user).destroy_all
next unless params[question.id.to_s]
option_ids = params[question.id.to_s][:option_id]
answers = Array(option_ids).map do |option_id|
question.find_or_initialize_user_answer(user, option_id)
end
if answers.map(&:valid?).all?(true)
Poll::Voter.find_or_create_by!(user: user, poll: poll, origin: "web")
answers.each(&:save!)
else
all_valid = false
raise ActiveRecord::Rollback
end
end
end
all_valid
end
end