From 8f7cfde27ed5724dd12b7f4c1b6f26bd37dba4bb Mon Sep 17 00:00:00 2001 From: Diego Calvo Castillo Date: Mon, 30 Oct 2023 14:58:04 +0100 Subject: [PATCH] UI for open questions --- app/assets/javascripts/custom/polls.js | 36 ++++++++++++ app/assets/stylesheets/custom.scss | 7 +++ .../questions/answers_component.html.erb | 57 +++++++++++++++++++ .../polls/questions/answers_component.rb | 37 ++++++++++++ .../custom/admin/poll/questions_controller.rb | 2 +- app/models/custom/concerns/questionable.rb | 16 ++++++ app/models/custom/poll/answer.rb | 54 ++++++++++++++++++ app/models/custom/votation_type.rb | 17 ++++++ app/models/votation_type.rb | 2 +- config/locales/custom/es/admin.yml | 5 +- config/locales/custom/es/general.yml | 3 + 11 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/custom/polls.js create mode 100644 app/components/custom/polls/questions/answers_component.html.erb create mode 100644 app/components/custom/polls/questions/answers_component.rb create mode 100644 app/models/custom/concerns/questionable.rb create mode 100644 app/models/custom/poll/answer.rb create mode 100644 app/models/custom/votation_type.rb diff --git a/app/assets/javascripts/custom/polls.js b/app/assets/javascripts/custom/polls.js new file mode 100644 index 000000000000..8c737c4ace21 --- /dev/null +++ b/app/assets/javascripts/custom/polls.js @@ -0,0 +1,36 @@ +(function () { + "use strict"; + App.Polls = { + text_input_listener: function () { + $(".text-input-form textarea").on("input", function (event) { + let submitButton = $(event.target).next(); + if (submitButton.hasClass("answered")) { + submitButton.removeClass("answered"); + submitButton.addClass("secondary hollow"); + submitButton.val("Enviar"); + } + }); + }, + initialize: function () { + $(".zoom-link").on("click", function (event) { + var answer; + answer = $(event.target).closest("div.answer"); + + if ($(answer).hasClass("medium-6")) { + $(answer).removeClass("medium-6"); + $(answer).addClass("answer-divider"); + if (!$(answer).hasClass("first")) { + $(answer).insertBefore($(answer).prev("div.answer")); + } + } else { + $(answer).addClass("medium-6"); + $(answer).removeClass("answer-divider"); + if (!$(answer).hasClass("first")) { + $(answer).insertAfter($(answer).next("div.answer")); + } + } + }); + App.Polls.text_input_listener(); + }, + }; +}).call(this); diff --git a/app/assets/stylesheets/custom.scss b/app/assets/stylesheets/custom.scss index 5208717cd822..68db74c4bda2 100644 --- a/app/assets/stylesheets/custom.scss +++ b/app/assets/stylesheets/custom.scss @@ -451,6 +451,13 @@ z-index: 1; } +// POLLS OPEN QUESTION + +.poll-question-answers { + .text-input-form { + width: 100%; + } +} // /* This controls how many elements will be on each row of the block grid. */ // /* Set this to whatever number you need, up to the max allowed in the variable */ // $per-row: false; diff --git a/app/components/custom/polls/questions/answers_component.html.erb b/app/components/custom/polls/questions/answers_component.html.erb new file mode 100644 index 000000000000..54e0299c03c4 --- /dev/null +++ b/app/components/custom/polls/questions/answers_component.html.erb @@ -0,0 +1,57 @@ +
+ <% if question.vote_type == "open" %> + <% if can?(:answer, question) && !question.poll.voted_in_booth?(current_user) %> + <%= form_tag answer_question_path(question), remote: true, :"data-turbolinks"=>"true", id: "question-#{question.id}", class: "text-input-form" do %> + <%= text_area_tag :answer, open_answer, rows: 4, required: true %> + <% if open_answer.empty? %> + <%= submit_tag("Enviar", :class => "button secondary hollow") %> + <% else %> + <%= submit_tag("Enviado", :class => "button answered") %> + <% end %> + <% end %> + <% elsif !user_signed_in? %> + <%= form_tag answer_question_path(question), remote: true, class: "text-input-form" do %> + <%= text_area_tag :answer, nil, rows: 4, required: true, disabled: true %> + <%= submit_tag("Enviar", :class => "button secondary hollow") %> + <% end %> + <% else %> + <%= text_area_tag :answer, open_answer, rows: 4, required: true, disabled: true %> + <% end %> + <% end %> + + <% if can?(:answer, question) && !question.poll.voted_in_booth?(current_user) %> + <% question_answers.each do |question_answer| %> + <% if already_answered?(question_answer) %> + <%= button_to question_answer_path(question, user_answer(question_answer)), + method: :delete, + remote: true, + title: t("poll_questions.show.voted", answer: question_answer.title), + class: "button answered", + "aria-pressed": true do %> + <%= question_answer.title %> + <% end %> + <% else %> + <%= button_to answer_question_path(question, answer: question_answer.title), + remote: true, + title: t("poll_questions.show.vote_answer", answer: question_answer.title), + class: "button secondary hollow", + "aria-pressed": false, + disabled: disable_answer?(question_answer) do %> + <%= question_answer.title %> + <% end %> + <% end %> + <% end %> + <% elsif !user_signed_in? %> + <% question_answers.each do |question_answer| %> + <%= link_to question_answer.title, new_user_session_path, class: "button secondary hollow" %> + <% end %> + <% elsif !current_user.level_two_or_three_verified? %> + <% question_answers.each do |question_answer| %> + <%= link_to question_answer.title, verification_path, class: "button secondary hollow" %> + <% end %> + <% else %> + <% question_answers.each do |question_answer| %> + <%= question_answer.title %> + <% end %> + <% end %> +
diff --git a/app/components/custom/polls/questions/answers_component.rb b/app/components/custom/polls/questions/answers_component.rb new file mode 100644 index 000000000000..ef985e165c0a --- /dev/null +++ b/app/components/custom/polls/questions/answers_component.rb @@ -0,0 +1,37 @@ +class Polls::Questions::AnswersComponent < ApplicationComponent + attr_reader :question + delegate :can?, :current_user, :user_signed_in?, to: :helpers + + def initialize(question) + @question = question + end + + def already_answered?(question_answer) + user_answer(question_answer).present? + end + + def question_answers + question.question_answers + end + + def open_answer + if question.answers.nil? || question.answers.empty? + return '' + end + question.answers.by_author(current_user).first.answer + end + + def user_answer(question_answer) + user_answers.find_by(answer: question_answer.title) + end + + def disable_answer?(question_answer) + question.multiple? && user_answers.count == question.max_votes + end + + private + + def user_answers + @user_answers ||= question.answers.by_author(current_user) + end +end diff --git a/app/controllers/custom/admin/poll/questions_controller.rb b/app/controllers/custom/admin/poll/questions_controller.rb index b47d2d881d3c..395c2ddc9ac8 100644 --- a/app/controllers/custom/admin/poll/questions_controller.rb +++ b/app/controllers/custom/admin/poll/questions_controller.rb @@ -1,4 +1,4 @@ -require_dependency Rails.root.join("app", "controllers", "admin", "poll", "question_controller").to_s +require_dependency Rails.root.join("app", "controllers", "admin", "poll", "questions_controller").to_s class Admin::Poll::QuestionsController def create diff --git a/app/models/custom/concerns/questionable.rb b/app/models/custom/concerns/questionable.rb new file mode 100644 index 000000000000..0e83e4f1fcd2 --- /dev/null +++ b/app/models/custom/concerns/questionable.rb @@ -0,0 +1,16 @@ +require_dependency Rails.root.join("app", "models", "concerns", "questionable").to_s + +module Questionable + private + + def find_by_attributes(user, title) + case vote_type + when "unique", nil + { author: user } + when "multiple" + { author: user, answer: title } + when "open" + { author: user } + end + end +end diff --git a/app/models/custom/poll/answer.rb b/app/models/custom/poll/answer.rb new file mode 100644 index 000000000000..9d3f5721f584 --- /dev/null +++ b/app/models/custom/poll/answer.rb @@ -0,0 +1,54 @@ +class Poll::Answer < ApplicationRecord + belongs_to :question, -> { with_hidden }, inverse_of: :answers + belongs_to :author, -> { with_hidden }, class_name: "User", inverse_of: :poll_answers + + delegate :poll, :poll_id, to: :question + + validates :question, presence: true + validates :author, presence: true + validates :answer, presence: true + validate :max_votes + + validate :answer_inclusion_if_not_open + + scope :by_author, ->(author_id) { where(author_id: author_id) } + scope :by_question, ->(question_id) { where(question_id: question_id) } + + def answer_inclusion_if_not_open + if question.present? && question.vote_type != 'open' + unless question.possible_answers.include?(answer) + errors.add(:answer, 'is not included in the list of possible answers') + end + end + end + + def save_and_record_voter_participation + transaction do + touch if persisted? + 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 || question&.unique? || question.vote_type == 'open' || persisted? + + author.lock! + + if question.answers.by_author(author).count >= question.max_votes + errors.add(:answer, "Maximum number of votes per user exceeded") + end + end +end diff --git a/app/models/custom/votation_type.rb b/app/models/custom/votation_type.rb new file mode 100644 index 000000000000..54fbf85552d0 --- /dev/null +++ b/app/models/custom/votation_type.rb @@ -0,0 +1,17 @@ +class VotationType < ApplicationRecord + belongs_to :questionable, polymorphic: true + + QUESTIONABLE_TYPES = %w[Poll::Question].freeze + + enum vote_type: %w[unique multiple open] + + validates :questionable, presence: true + validates :questionable_type, inclusion: { in: ->(*) { QUESTIONABLE_TYPES }} + validates :max_votes, presence: true, if: :max_votes_required? + + private + + def max_votes_required? + multiple? + end +end diff --git a/app/models/votation_type.rb b/app/models/votation_type.rb index 54fbf85552d0..00fca0ecfa5f 100644 --- a/app/models/votation_type.rb +++ b/app/models/votation_type.rb @@ -3,7 +3,7 @@ class VotationType < ApplicationRecord QUESTIONABLE_TYPES = %w[Poll::Question].freeze - enum vote_type: %w[unique multiple open] + enum vote_type: %w[unique multiple] validates :questionable, presence: true validates :questionable_type, inclusion: { in: ->(*) { QUESTIONABLE_TYPES }} diff --git a/config/locales/custom/es/admin.yml b/config/locales/custom/es/admin.yml index 279c8a5b50bd..535a2950247d 100644 --- a/config/locales/custom/es/admin.yml +++ b/config/locales/custom/es/admin.yml @@ -56,9 +56,6 @@ es: author_date_of_birth: Fecha de nacimiento del autor author_geozone: Localidad del autor author_gender: Género del autor - polls: - votation_type: - open_description: "Permite que el usuario responda libremente en un cuadro de texto." proposals: export_list: id: ID @@ -74,6 +71,8 @@ es: audits: Registro de cambios legislation: Propuestas del Cabildo polls: + votation_type: + open_description: "Permite que el usuario responda libremente en un cuadro de texto." results: export_list: id: ID diff --git a/config/locales/custom/es/general.yml b/config/locales/custom/es/general.yml index 4e779812a1b6..37146a86f969 100644 --- a/config/locales/custom/es/general.yml +++ b/config/locales/custom/es/general.yml @@ -34,6 +34,9 @@ es: services: Servicios Digitales collaborative_legislation: Propuestas del Cabildo tenerife_logo: "Cabildo Tenerife" + poll_questions: + description: + open: "Escribe aquí tu respuesta." shared: main_links: cabildo: Cabildo Abierto