Merge pull request #6061 from consuldemocracy/poll_text_answers

Add support for essay poll questions
This commit is contained in:
Sebastia
2025-10-16 15:30:22 +02:00
committed by GitHub
43 changed files with 856 additions and 293 deletions

View File

@@ -101,7 +101,7 @@ module Abilities
end
can [:read, :order_options], Poll::Question::Option
can [:create, :update, :destroy], Poll::Question::Option do |option|
can?(:update, option.question)
can?(:update, option.question) && option.question.accepts_options?
end
can :read, Poll::Question::Option::Video
can [:create, :update, :destroy], Poll::Question::Option::Video do |video|

View File

@@ -9,21 +9,9 @@ class Poll::Answer < ApplicationRecord
validates :author, presence: true
validates :answer, presence: true
validates :option, uniqueness: { scope: :author_id }, allow_nil: true
validate :max_votes
validates :answer, inclusion: { in: ->(poll_answer) { poll_answer.option.possible_answers }},
if: ->(poll_answer) { poll_answer.option.present? }
scope :by_author, ->(author_id) { where(author_id: author_id) }
scope :by_question, ->(question_id) { where(question_id: question_id) }
private
def max_votes
return if !question || !author || persisted?
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

@@ -27,10 +27,11 @@ class Poll::Question < ApplicationRecord
accepts_nested_attributes_for :question_options, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :votation_type
delegate :multiple?, :vote_type, to: :votation_type, allow_nil: true
delegate :multiple?, :open?, :vote_type, to: :votation_type, allow_nil: true
scope :sort_for_list, -> { order(Arel.sql("poll_questions.proposal_id IS NULL"), :created_at) }
scope :for_render, -> { includes(:author, :proposal) }
scope :for_physical_votes, -> { left_joins(:votation_type).merge(VotationType.accepts_options) }
def copy_attributes_from_proposal(proposal)
if proposal.present?
@@ -61,6 +62,10 @@ class Poll::Question < ApplicationRecord
votation_type.nil? || votation_type.unique?
end
def accepts_options?
votation_type.nil? || votation_type.accepts_options?
end
def max_votes
if multiple?
votation_type.max_votes
@@ -69,23 +74,51 @@ class Poll::Question < ApplicationRecord
end
end
def find_or_initialize_user_answer(user, option_id)
option = question_options.find(option_id)
def find_or_initialize_user_answer(user, option_id: nil, answer_text: nil)
answer = answers.find_or_initialize_by(find_by_attributes(user, option_id))
if accepts_options?
option = question_options.find(option_id)
answer.option = option
answer.answer = option.title
else
answer.answer = answer_text
end
answer = answers.find_or_initialize_by(find_by_attributes(user, option))
answer.option = option
answer.answer = option.title
answer
end
def open_ended_valid_answers_count
answers.count
end
def open_ended_blank_answers_count
poll.voters.count - open_ended_valid_answers_count
end
def open_ended_valid_percentage
return 0.0 if open_ended_total_answers.zero?
(open_ended_valid_answers_count * 100.0) / open_ended_total_answers
end
def open_ended_blank_percentage
return 0.0 if open_ended_total_answers.zero?
(open_ended_blank_answers_count * 100.0) / open_ended_total_answers
end
private
def find_by_attributes(user, option)
case vote_type
when "unique", nil
def find_by_attributes(user, option_id)
if multiple?
{ author: user, option_id: option_id }
else
{ author: user }
when "multiple"
{ author: user, answer: option.title }
end
end
def open_ended_total_answers
open_ended_valid_answers_count + open_ended_blank_answers_count
end
end

View File

@@ -63,8 +63,17 @@ class Poll::WebVote
def answers_for_question(question, question_params)
return [] unless question_params
Array(question_params[:option_id]).map do |option_id|
question.find_or_initialize_user_answer(user, option_id)
if question.open?
answer_text = question_params[:answer].to_s.strip
if answer_text.present?
[question.find_or_initialize_user_answer(user, answer_text: answer_text)]
else
[]
end
else
Array(question_params[:option_id]).map do |option_id|
question.find_or_initialize_user_answer(user, option_id: option_id)
end
end
end

View File

@@ -1,17 +1,31 @@
class VotationType < ApplicationRecord
belongs_to :questionable, polymorphic: true
validate :cannot_be_open_ended_if_question_has_options
QUESTIONABLE_TYPES = %w[Poll::Question].freeze
enum :vote_type, { unique: 0, multiple: 1 }
enum :vote_type, { unique: 0, multiple: 1, open: 2 }
validates :questionable, presence: true
validates :questionable_type, inclusion: { in: ->(*) { QUESTIONABLE_TYPES }}
validates :max_votes, presence: true, if: :max_votes_required?
scope :accepts_options, -> { where.not(vote_type: "open") }
def accepts_options?
!open?
end
private
def max_votes_required?
multiple?
end
def cannot_be_open_ended_if_question_has_options
if questionable&.question_options&.any? && !accepts_options?
errors.add(:vote_type, :cannot_change_to_open_ended)
end
end
end