diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 21a287ca6b9f5..42f16e8b6dcd7 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -133,6 +133,7 @@ ignore_unused: - booleans.* - '{date,time.formats}.*' - datetime.distance_in_words.* + - decidim.admin.actions.share - decidim.admin.participatory_process_steps.default_title - decidim.admin.admin_log.changeset.* - decidim.authorization_handlers.{direct,multistep} @@ -147,6 +148,10 @@ ignore_unused: - decidim.forms.file_help.* - decidim.forms.user_answers_serializer.* - decidim.forms.admin_log.questionnaire.update + - decidim.surveys.admin_log.survey.create + - decidim.surveys.admin_log.survey.delete + - decidim.surveys.admin_log.survey.publish + - decidim.surveys.admin_log.survey.update - decidim.proposals.answers.* - decidim.proposals.collaborative_drafts.show.see_other_versions - decidim.proposals.collaborative_drafts.wizard_aside.back diff --git a/decidim-core/config/locales/en.yml b/decidim-core/config/locales/en.yml index 836c6b84071db..449f69b5e3c1c 100644 --- a/decidim-core/config/locales/en.yml +++ b/decidim-core/config/locales/en.yml @@ -1794,6 +1794,7 @@ en: no_stats: There are no statistics yet. pages_count: Pages participants_count: Participants + surveys_count: Surveys users_count: Participants tags: filter_results_for_taxonomy: 'Filter results for: %{resource}' diff --git a/decidim-core/lib/decidim/core/engine.rb b/decidim-core/lib/decidim/core/engine.rb index 754ada3fc4a12..8a72effa5ea8c 100644 --- a/decidim-core/lib/decidim/core/engine.rb +++ b/decidim-core/lib/decidim/core/engine.rb @@ -153,6 +153,7 @@ class Engine < ::Rails::Engine Decidim.icons.register(name: "treasure-map-line", icon: "treasure-map-line", category: "system", description: "", engine: :core) Decidim.icons.register(name: "chat-new-line", icon: "chat-new-line", category: "system", description: "", engine: :core) Decidim.icons.register(name: "history", icon: "history-line", category: "system", description: "History timeline", engine: :core) + Decidim.icons.register(name: "survey-line", icon: "survey-line", category: "system", description: "Survey line", engine: :core) Decidim.icons.register(name: "draft-line", icon: "draft-line", category: "system", description: "", engine: :core) Decidim.icons.register(name: "user-voice-line", icon: "user-voice-line", category: "system", description: "", engine: :core) diff --git a/decidim-forms/app/commands/decidim/forms/admin/update_questionnaire.rb b/decidim-forms/app/commands/decidim/forms/admin/update_questionnaire.rb index caf07f5d56e4c..72a74df6ab681 100644 --- a/decidim-forms/app/commands/decidim/forms/admin/update_questionnaire.rb +++ b/decidim-forms/app/commands/decidim/forms/admin/update_questionnaire.rb @@ -25,14 +25,7 @@ def call Decidim.traceability.perform_action!("update", @questionnaire, @user) do - Decidim::Forms::Questionnaire.transaction do - if @questionnaire.questions_editable? - update_questionnaire_questions - delete_answers unless @questionnaire.published? - end - - update_questionnaire - end + update_questionnaire end broadcast(:ok) @@ -40,85 +33,11 @@ def call private - def update_questionnaire_questions - @form.questions.each do |form_question| - update_questionnaire_question(form_question) - end - end - - def update_questionnaire_question(form_question) - question_attributes = { - body: form_question.body, - description: form_question.description, - position: form_question.position, - mandatory: form_question.mandatory, - question_type: form_question.question_type, - max_choices: form_question.max_choices, - max_characters: form_question.max_characters - } - - update_nested_model(form_question, question_attributes, @questionnaire.questions) do |question| - form_question.answer_options.each do |form_answer_option| - answer_option_attributes = { - body: form_answer_option.body, - free_text: form_answer_option.free_text - } - - update_nested_model(form_answer_option, answer_option_attributes, question.answer_options) - end - - form_question.display_conditions.each do |form_display_condition| - type = form_display_condition.condition_type - - display_condition_attributes = { - condition_question: form_display_condition.condition_question, - condition_type: form_display_condition.condition_type, - condition_value: type == "match" ? form_display_condition.condition_value : nil, - answer_option: %w(equal not_equal).include?(type) ? form_display_condition.answer_option : nil, - mandatory: form_display_condition.mandatory - } - - next if form_display_condition.deleted? && form_display_condition.id.blank? - - update_nested_model(form_display_condition, display_condition_attributes, question.display_conditions) - end - - form_question.matrix_rows_by_position.each_with_index do |form_matrix_row, idx| - matrix_row_attributes = { - body: form_matrix_row.body, - position: form_matrix_row.position || idx - } - - update_nested_model(form_matrix_row, matrix_row_attributes, question.matrix_rows) - end - end - end - - def update_nested_model(form, attributes, parent_association) - record = parent_association.find_by(id: form.id) || parent_association.build(attributes) - - yield record if block_given? - - if record.persisted? - if form.deleted? - record.destroy! - else - record.update!(attributes) - end - else - record.save! - end - end - def update_questionnaire @questionnaire.update!(title: @form.title, description: @form.description, tos: @form.tos) end - - def delete_answers - @questionnaire.answers.destroy_all - end end end end diff --git a/decidim-forms/app/commands/decidim/forms/admin/update_questions.rb b/decidim-forms/app/commands/decidim/forms/admin/update_questions.rb new file mode 100644 index 0000000000000..27d1a9f90a54e --- /dev/null +++ b/decidim-forms/app/commands/decidim/forms/admin/update_questions.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module Decidim + module Forms + module Admin + # This command is executed when the user changes a Questionnaire questions from the admin + # panel. + class UpdateQuestions < Decidim::Command + # Initializes a UpdateQuestions Command. + # + # form - The form from which to get the data. + # questionnaire - The current instance of the questionnaire questions to be updated. + def initialize(form, questionnaire) + @form = form + @questionnaire = questionnaire + end + + # Updates the questionnaire if valid. + # + # Broadcasts :ok if successful, :invalid otherwise. + def call + return broadcast(:invalid) if @form.invalid? + + Decidim.traceability.perform_action!("update", + @questionnaire, + @form.current_user) do + Decidim::Forms::Questionnaire.transaction do + update_questionnaire_questions if @questionnaire.questions_editable? + end + end + + broadcast(:ok) + end + + private + + def update_questionnaire_questions + @form.questions.each do |form_question| + update_questionnaire_question(form_question) + end + end + + def update_questionnaire_question(form_question) + question_attributes = { + body: form_question.body, + description: form_question.description, + position: form_question.position, + mandatory: form_question.mandatory, + question_type: form_question.question_type, + max_choices: form_question.max_choices, + max_characters: form_question.max_characters + } + + update_nested_model(form_question, question_attributes, @questionnaire.questions) do |question| + form_question.answer_options.each do |form_answer_option| + answer_option_attributes = { + body: form_answer_option.body, + free_text: form_answer_option.free_text + } + + update_nested_model(form_answer_option, answer_option_attributes, question.answer_options) + end + + form_question.display_conditions.each do |form_display_condition| + type = form_display_condition.condition_type + + display_condition_attributes = { + condition_question: form_display_condition.condition_question, + condition_type: form_display_condition.condition_type, + condition_value: type == "match" ? form_display_condition.condition_value : nil, + answer_option: %w(equal not_equal).include?(type) ? form_display_condition.answer_option : nil, + mandatory: form_display_condition.mandatory + } + + next if form_display_condition.deleted? && form_display_condition.id.blank? + + update_nested_model(form_display_condition, display_condition_attributes, question.display_conditions) + end + + form_question.matrix_rows_by_position.each_with_index do |form_matrix_row, idx| + matrix_row_attributes = { + body: form_matrix_row.body, + position: form_matrix_row.position || idx + } + + update_nested_model(form_matrix_row, matrix_row_attributes, question.matrix_rows) + end + end + end + + def update_nested_model(form, attributes, parent_association) + record = parent_association.find_by(id: form.id) || parent_association.build(attributes) + + yield record if block_given? + + if record.persisted? + if form.deleted? + record.destroy! + else + record.update!(attributes) + end + else + record.save! + end + end + end + end + end +end diff --git a/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb b/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb index bf52ccfff36cf..81a27b9fa4e02 100644 --- a/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +++ b/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb @@ -38,13 +38,12 @@ def edit @form = form(Admin::QuestionnaireForm).from_model(questionnaire) - render template: "decidim/forms/admin/questionnaires/edit" + render template: edit_template end def update enforce_permission_to(:update, :questionnaire, questionnaire:) - params["published_at"] = Time.current if params.has_key? "save_and_publish" @form = form(Admin::QuestionnaireForm).from_params(params) Admin::UpdateQuestionnaire.call(@form, questionnaire, current_user) do @@ -57,7 +56,30 @@ def update on(:invalid) do # i18n-tasks-use t("decidim.forms.admin.questionnaires.update.invalid") flash.now[:alert] = I18n.t("update.invalid", scope: i18n_flashes_scope) - render template: "decidim/forms/admin/questionnaires/edit" + render template: edit_template + end + end + end + + def edit_questions + @form = form(Admin::QuestionsForm).from_model(questionnaire) + + render template: edit_questions_template + end + + # i18n-tasks-use t("decidim.forms.admin.questionnaires.questions_form.update.success") + # i18n-tasks-use t("decidim.forms.admin.questionnaires.update.invalid") + def update_questions + @form = form(Admin::QuestionsForm).from_params(params) + Admin::UpdateQuestions.call(@form, questionnaire) do + on(:ok) do + flash[:notice] = I18n.t("update.success", scope: i18n_questions_flashes_scope) + redirect_to after_update_url + end + + on(:invalid) do + flash.now[:alert] = I18n.t("update.invalid", scope: i18n_flashes_scope) + render template: edit_questions_template end end end @@ -96,6 +118,12 @@ def public_url raise "#{self.class.name} is expected to implement #public_url" end + # Implement this method in your controller to set the URL + # where the user will be render while editing the questionnaire questions + def edit_questions_template + "decidim/forms/admin/questionnaires/edit_questions" + end + # Returns the url to get the answer options json (for the display conditions form) # for the question with id = params[:id] def answer_options_url(params) @@ -110,10 +138,18 @@ def edit_questionnaire_title private + def edit_template + "decidim/forms/admin/questionnaires/edit" + end + def i18n_flashes_scope "decidim.forms.admin.questionnaires" end + def i18n_questions_flashes_scope + "decidim.forms.admin.questionnaires.questions_form" + end + def questionnaire @questionnaire ||= Questionnaire.find_by(questionnaire_for:) end diff --git a/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire_answers.rb b/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire_answers.rb index dac0672eb4112..9bfcdfeba2a2b 100644 --- a/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire_answers.rb +++ b/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire_answers.rb @@ -37,7 +37,7 @@ def index def show enforce_permission_to :show, :questionnaire_answers - @participant = participant(participants_query.participant(params[:session_token])) + @participant = participant(participants_query.participant(params[:id])) render template: "decidim/forms/admin/questionnaires/answers/show" end @@ -45,7 +45,7 @@ def show def export_response enforce_permission_to :export_response, :questionnaire_answers - session_token = params[:session_token] + session_token = params[:id] answers = QuestionnaireUserAnswers.for(questionnaire) # i18n-tasks-use t("decidim.forms.admin.questionnaires.answers.export_response.title") diff --git a/decidim-forms/app/forms/decidim/forms/admin/questionnaire_form.rb b/decidim-forms/app/forms/decidim/forms/admin/questionnaire_form.rb index f360103318be0..1b2421b3edf8f 100644 --- a/decidim-forms/app/forms/decidim/forms/admin/questionnaire_form.rb +++ b/decidim-forms/app/forms/decidim/forms/admin/questionnaire_form.rb @@ -11,16 +11,7 @@ class QuestionnaireForm < Decidim::Form translatable_attribute :description, Decidim::Attributes::RichText translatable_attribute :tos, Decidim::Attributes::RichText - attribute :published_at, Decidim::Attributes::TimeWithZone - attribute :questions, Array[QuestionForm] - validates :title, :tos, translatable_presence: true - - def map_model(model) - self.questions = model.questions.map do |question| - QuestionForm.from_model(question) - end - end end end end diff --git a/decidim-forms/app/forms/decidim/forms/admin/questions_form.rb b/decidim-forms/app/forms/decidim/forms/admin/questions_form.rb new file mode 100644 index 0000000000000..90c3c64d98310 --- /dev/null +++ b/decidim-forms/app/forms/decidim/forms/admin/questions_form.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Decidim + module Forms + module Admin + # This class holds a Form to update questionnaires questions from Decidim's admin panel. + class QuestionsForm < Decidim::Form + attribute :questions, Array[QuestionForm] + + def map_model(model) + self.questions = model.questions.map do |question| + QuestionForm.from_model(question) + end + end + end + end + end +end diff --git a/decidim-forms/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_pagination_helper.rb b/decidim-forms/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_pagination_helper.rb index cc6a17e9db99f..577ab02e7f4e8 100644 --- a/decidim-forms/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_pagination_helper.rb +++ b/decidim-forms/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_pagination_helper.rb @@ -40,7 +40,7 @@ def participant_ids end def current_idx - participant_ids.index(params[:session_token]) + participant_ids.index(params[:id]) end end end diff --git a/decidim-forms/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper.rb b/decidim-forms/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper.rb index 375f5605762e4..cdfa0472a264a 100644 --- a/decidim-forms/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper.rb +++ b/decidim-forms/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper.rb @@ -21,17 +21,17 @@ def questionnaire_url # You can implement this method in your controller to change the URL # where the questionnaire participants' info will be shown. def questionnaire_participants_url - url_for([:index, questionnaire.questionnaire_for, { format: nil }]) + url_for([questionnaire.questionnaire_for, { format: nil }]) end # You can implement this method in your controller to change the URL # where the user's questionnaire answers will be shown. - def questionnaire_participant_answers_url(session_token) - url_for([:show, questionnaire.questionnaire_for, { session_token: }]) + def questionnaire_participant_answers_url(id) + url_for([questionnaire.questionnaire_for, { id: }]) end - def questionnaire_export_response_url(session_token) - url_for([:export_response, questionnaire.questionnaire_for, { session_token:, format: "pdf" }]) + def questionnaire_export_response_url(id) + url_for([questionnaire.questionnaire_for, { id:, format: "pdf" }]) end end end diff --git a/decidim-forms/app/queries/decidim/forms/questionnaire_user_answers.rb b/decidim-forms/app/queries/decidim/forms/questionnaire_user_answers.rb index d8c327c2e4639..a1f37bbd0e705 100644 --- a/decidim-forms/app/queries/decidim/forms/questionnaire_user_answers.rb +++ b/decidim-forms/app/queries/decidim/forms/questionnaire_user_answers.rb @@ -25,7 +25,7 @@ def query .joins(:question) .where(questionnaire: @questionnaire) - answers.sort_by { |answer| answer.question.position }.group_by { |a| a.user || a.session_token }.values + answers.sort_by { |answer| answer.question.position.to_i }.group_by { |a| a.user || a.session_token }.values end end end diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_answer_option_template.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_answer_option_template.html.erb index e5e25d6b63de1..62ed642303b91 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_answer_option_template.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_answer_option_template.html.erb @@ -1,7 +1,7 @@ <% question = form.object %> diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_display_condition_template.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_display_condition_template.html.erb index 899f0ebc2cb56..fb16e8df76438 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_display_condition_template.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_display_condition_template.html.erb @@ -1,7 +1,7 @@ <% question = form.object %> diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_form.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_form.html.erb index bf84e1a6c7f8c..2552b01ecb499 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_form.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_form.html.erb @@ -13,92 +13,3 @@ - -
-
- - -
- - <% if questionnaire.questions_editable? %> - <% if questionnaire.answers.any? && !questionnaire.published? %> - <%= cell("decidim/announcement", t(".unpublished_warning"), callout_class: "warning" ) %> - <% end %> - - <%= fields_for "questionnaire[questions][#{blank_question.to_param}]", blank_question do |question_form| %> - - - - <%= render "decidim/forms/admin/questionnaires/answer_option_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "answer-option-template-dummy" %> - <%= render "decidim/forms/admin/questionnaires/display_condition_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "display-condition-template-dummy" %> - <%= render "decidim/forms/admin/questionnaires/matrix_row_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "matrix-row-template-dummy" %> - <% end %> - <% else %> - <%= cell("decidim/announcement", t(".already_answered_warning"), callout_class: "warning" ) %> - <% end %> - -
- <% @form.questions.each_with_index do |question, index| %> - <%= fields_for "questionnaire[questions][]", question do |question_form| %> - <% if question.separator? %> - <%= render "decidim/forms/admin/questionnaires/separator", - form: question_form, - id: tabs_id_for_question(question), - editable: questionnaire.questions_editable? %> - <% elsif question.title_and_description? %> - <%= render "decidim/forms/admin/questionnaires/title_and_description", - form: question_form, - id: tabs_id_for_question(question), - editable: questionnaire.questions_editable? %> - <% else %> - <%= render "decidim/forms/admin/questionnaires/question", - form: question_form, - id: tabs_id_for_question(question), - editable: questionnaire.questions_editable?, - display_condition_template_selector: "#display-condition-template-#{index}", - answer_option_template_selector: "#answer-option-template-#{index}", - matrix_row_template_selector: "#matrix-row-template-#{index}" %> - <%= render "decidim/forms/admin/questionnaires/display_condition_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "display-condition-template-#{index}" %> - <%= render "decidim/forms/admin/questionnaires/answer_option_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "answer-option-template-#{index}" %> - <%= render "decidim/forms/admin/questionnaires/matrix_row_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "matrix-row-template-#{index}" %> - <% end %> - <% end %> - <% end %> -
- - <% if questionnaire.questions_editable? %> -
- - - -
- <% end %> -
- -<%= append_javascript_pack_tag "decidim_forms_admin" %> - -<% if questionnaire.questions_editable? %> - -<% end %> diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_matrix_row_template.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_matrix_row_template.html.erb index 5e2a4f85904fa..daecc1bd7cd7f 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_matrix_row_template.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_matrix_row_template.html.erb @@ -1,7 +1,7 @@ <% question = form.object %> diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb index 645096a79c783..9fd10b0b97b17 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb @@ -111,7 +111,7 @@
<% question.matrix_rows_by_position.each do |matrix_row| %> - <%= fields_for "questionnaire[questions][#{question.to_param}][matrix_rows][]", matrix_row do |matrix_row_form| %> + <%= fields_for "questions[questions][#{question.to_param}][matrix_rows][]", matrix_row do |matrix_row_form| %> <%= render "decidim/forms/admin/questionnaires/matrix_row", form: matrix_row_form, question:, editable: %> <% end %> <% end %> @@ -126,7 +126,7 @@
<% question.answer_options.each do |answer_option| %> - <%= fields_for "questionnaire[questions][#{question.to_param}][answer_options][]", answer_option do |answer_option_form| %> + <%= fields_for "questions[questions][#{question.to_param}][answer_options][]", answer_option do |answer_option_form| %> <%= render "decidim/forms/admin/questionnaires/answer_option", form: answer_option_form, question:, editable: %> <% end %> <% end %> @@ -153,7 +153,7 @@
<% question.display_conditions.each do |display_condition| %> - <%= fields_for "questionnaire[questions][#{question.to_param}][display_conditions][]", display_condition do |display_condition_form| %> + <%= fields_for "questions[questions][#{question.to_param}][display_conditions][]", display_condition do |display_condition_form| %> <%= render "decidim/forms/admin/questionnaires/display_condition", form: display_condition_form, question:, editable: %> <% end %> <% end %> diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_questions_form.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_questions_form.html.erb new file mode 100644 index 0000000000000..574051256a1e8 --- /dev/null +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_questions_form.html.erb @@ -0,0 +1,80 @@ +
+
+ + +
+ + <% if questionnaire.questions_editable? %> + <% if questionnaire.answers.any? && !questionnaire.published? %> + <%= cell("decidim/announcement", t(".unpublished_warning"), callout_class: "warning" ) %> + <% end %> + + <%= fields_for "questions[questions][#{blank_question.to_param}]", blank_question do |question_form| %> + + + + <%= render "decidim/forms/admin/questionnaires/answer_option_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "answer-option-template-dummy" %> + <%= render "decidim/forms/admin/questionnaires/display_condition_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "display-condition-template-dummy" %> + <%= render "decidim/forms/admin/questionnaires/matrix_row_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "matrix-row-template-dummy" %> + <% end %> + <% else %> + <%= cell("decidim/announcement", t(".already_answered_warning"), callout_class: "warning" ) %> + <% end %> + +
+ <% @form.questions.each_with_index do |question, index| %> + <%= fields_for "questions[questions][]", question do |question_form| %> + <% if question.separator? %> + <%= render "decidim/forms/admin/questionnaires/separator", + form: question_form, + id: tabs_id_for_question(question), + editable: questionnaire.questions_editable? %> + <% elsif question.title_and_description? %> + <%= render "decidim/forms/admin/questionnaires/title_and_description", + form: question_form, + id: tabs_id_for_question(question), + editable: questionnaire.questions_editable? %> + <% else %> + <%= render "decidim/forms/admin/questionnaires/question", + form: question_form, + id: tabs_id_for_question(question), + editable: questionnaire.questions_editable?, + display_condition_template_selector: "#display-condition-template-#{index}", + answer_option_template_selector: "#answer-option-template-#{index}", + matrix_row_template_selector: "#matrix-row-template-#{index}" %> + <%= render "decidim/forms/admin/questionnaires/display_condition_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "display-condition-template-#{index}" %> + <%= render "decidim/forms/admin/questionnaires/answer_option_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "answer-option-template-#{index}" %> + <%= render "decidim/forms/admin/questionnaires/matrix_row_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "matrix-row-template-#{index}" %> + <% end %> + <% end %> + <% end %> +
+
+ +<%= append_javascript_pack_tag "decidim_forms_admin" %> + +<% if questionnaire.questions_editable? %> + +<% end %> diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/edit.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/edit.html.erb index 18a331463b74f..9bc6489f087b7 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/edit.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/edit.html.erb @@ -4,26 +4,6 @@ <%= render partial: "decidim/templates/admin/questionnaire_templates/choose", locals: { target: questionnaire, form_title: t("decidim.forms.admin.questionnaires.edit.title") } %> <% else %> -
-

- <%= edit_questionnaire_title %> - - <% unless template? questionnaire.questionnaire_for %> - <% if allowed_to? :preview, :questionnaire %> - <%= link_to t("preview", scope: "decidim.forms.admin.questionnaires.form"), public_url, class: "button button__sm button__secondary", target: :_blank, data: { "external-link": false } %> - <% end %> - <% if questionnaire.answers.any? %> - <%= export_dropdown(current_component, questionnaire.id) if allowed_to? :export_answers, :questionnaire %> - <% if allowed_to? :show, :questionnaire_answers %> - <%= link_to t("actions.show", scope: "decidim.forms.admin.questionnaires"), questionnaire_participants_url, class: "button button__sm button__secondary new whitespace-nowrap" %> - <% end %> - <% else %> - - <% end %> - <% end %> -

-
-
<%= decidim_form_for(@form, url: update_url, method: :put, html: { class: "form-defaults form edit_questionnaire" }) do |form| %> diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/edit_questions.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/edit_questions.html.erb new file mode 100644 index 0000000000000..ed3c1440b1b9f --- /dev/null +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/edit_questions.html.erb @@ -0,0 +1,47 @@ +<% add_decidim_page_title(t("decidim.forms.admin.questionnaires.edit.title")) %> + +<% if templates_defined? && choose_template? %> + <%= render partial: "decidim/templates/admin/questionnaire_templates/choose", locals: { target: questionnaire, form_title: t("decidim.forms.admin.questionnaires.edit.title") } %> +<% else %> + +
+
+

+ <%= t("decidim.forms.admin.questionnaires.edit_questions.title") %> + + <% unless template? questionnaire.questionnaire_for %> + <% if questionnaire.questions_editable? %> + + + + <% end %> + <% if allowed_to? :preview, :questionnaire %> + <%= link_to t("preview", scope: "decidim.forms.admin.questionnaires.form"), public_url, class: "button button__sm button__transparent-secondary", target: :_blank, data: { "external-link": false } %> + <% end %> + <% if questionnaire.answers.any? %> + <%= export_dropdown(current_component, questionnaire.id) if allowed_to? :export_answers, :questionnaire %> + <% if allowed_to? :show, :questionnaire_answers %> + <%= link_to t("actions.show", scope: "decidim.forms.admin.questionnaires"), questionnaire_participants_url, class: "button button__sm button__secondary new whitespace-nowrap" %> + <% end %> + <% else %> + + <% end %> + <% end %> +

+
+
+ +
+
+ <%= decidim_form_for(@form, url: update_url, method: :put, html: { class: "form-defaults form edit_questionnaire" }) do |form| %> + <%= render partial: "decidim/forms/admin/questionnaires/questions_form", object: form %> +
+
+ <%= form.submit t(".save"), class: "button button__sm button__secondary" %> +
+
+ <% end %> +
+
+ +<% end %> diff --git a/decidim-forms/app/views/decidim/forms/questionnaires/_questionnaire.html.erb b/decidim-forms/app/views/decidim/forms/questionnaires/_questionnaire.html.erb index 6feb4c3526b83..dcdc029501b14 100644 --- a/decidim-forms/app/views/decidim/forms/questionnaires/_questionnaire.html.erb +++ b/decidim-forms/app/views/decidim/forms/questionnaires/_questionnaire.html.erb @@ -6,6 +6,10 @@ <%= cell("decidim/announcement", { title: t("decidim.forms.questionnaires.show.questionnaire_for_private_users.title"), body: t("decidim.forms.questionnaires.show.questionnaire_for_private_users.body") }, callout_class: "alert") %> <% end %> +<% if questionnaire_for.respond_to?(:announcement) and questionnaire_for.announcement.present? %> + <%= cell("decidim/announcement", translated_attribute(questionnaire_for.announcement), callout_class: "warning") %> +<% end %> + <%= decidim_form_for(@form, url: update_url, method: :post, html: { multipart: true, class: "form answer-questionnaire" }, data: { "safe-path" => form_path }) do |form| %> <%= form_required_explanation %> diff --git a/decidim-forms/config/locales/en.yml b/decidim-forms/config/locales/en.yml index 8f41dc9ce7757..2d160af2df18a 100644 --- a/decidim-forms/config/locales/en.yml +++ b/decidim-forms/config/locales/en.yml @@ -41,7 +41,16 @@ en: admin: models: components: + allow_answers: Allow answers + allow_unregistered: Allow unregistered users to answer the survey + allow_unregistered_help: If active, no login will be required in order to answer the survey. This may lead to poor or unreliable data and it will be more vulnerable to automated attacks. Use with caution! Mind that a participant could answer the same survey multiple times, by using different browsers or the "private browsing" feature of her web browser. + announcement: Announcement + clean_after_publish: Delete answers when publishing the survey description: Description + ends_at: Answers accepted until + ends_at_help: Leave blank for no specific date + starts_at: Answers accepted from + starts_at_help: Leave blank for no specific date tos: Terms of service questionnaires: actions: @@ -90,16 +99,18 @@ en: edit: save: Save title: Edit questionnaire - form: + edit_questions: add_question: Add question add_separator: Add separator add_title_and_description: Add title and description - already_answered_warning: The form is already answered by some users so you cannot modify its questions. + save: Save + title: Est error.> Questions + form: + add_question: Add question collapse: Collapse all questions expand: Expand all questions preview: Preview title: Edit form for %{questionnaire_for} - unpublished_warning: The form is not published. You may modify its questions, but doing so will delete current answers. matrix_row: matrix_row: Row remove: Remove @@ -118,6 +129,13 @@ en: remove: Remove statement: Statement up: Up + questions_form: + already_answered_warning: The form is already answered by some users so you cannot modify its questions. + collapse: Collapse all questions + expand: Expand all questions + unpublished_warning: The form is not published. You may modify its questions, but doing so will delete current answers. + update: + success: Survey questions successfully saved. separator: down: Down remove: Remove diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/has_questionnaire.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/has_questionnaire.rb index 4f3db425d3100..e3509739a4bdf 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/has_questionnaire.rb @@ -6,6 +6,7 @@ context "when the user is not logged in" do it "does not allow answering the questionnaire" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description, strip_tags: true) @@ -30,6 +31,7 @@ it "shows an empty page with a message" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("No questions configured for this form yet.") end @@ -41,6 +43,8 @@ expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description, strip_tags: true) + see_questionnaire_questions + fill_in question.body["en"], with: "My first answer" check "questionnaire_tos_agreement" @@ -52,6 +56,7 @@ expect(page).to have_admin_callout(callout_success) visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("You have already answered this form.") expect(page).to have_no_i18n_content(question.body) @@ -73,6 +78,7 @@ end it "it renders the asterisk as a separated element" do + see_questionnaire_questions within "label.answer-questionnaire__question-label" do expect(page).to have_content(translated_attribute(question.body).to_s) within "span.label-required.has-tip" do @@ -89,6 +95,7 @@ before do visit questionnaire_public_path + see_questionnaire_questions end it "allows answering the first questionnaire" do @@ -121,6 +128,7 @@ expect(page).to have_admin_callout(callout_success) visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("You have already answered this form.") end @@ -138,14 +146,13 @@ def answer_first_questionnaire it "requires confirmation when exiting mid-answering" do visit questionnaire_public_path + see_questionnaire_questions fill_in question.body["en"], with: "My first answer" - dismiss_page_unload do - page.find(".main-bar__logo a").click - end + click_on translated_attribute(component.name) - expect(page).to have_current_path questionnaire_public_path + expect(page).to have_current_path(questionnaire_public_path) end context "when the questionnaire has already been answered by someone else" do @@ -173,6 +180,7 @@ def answer_first_questionnaire it "does not leak defaults from other answers" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_no_field(type: "radio", checked: true) end @@ -195,6 +203,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions end it_behaves_like "a correctly ordered questionnaire" @@ -205,6 +214,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions accept_confirm { click_on "Submit" } end @@ -224,6 +234,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions check "questionnaire_tos_agreement" end @@ -235,6 +246,7 @@ def answer_first_questionnaire it "shows a message indicating number of characters left" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("30 characters left") end @@ -298,6 +310,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions check "questionnaire_tos_agreement" @@ -315,6 +328,7 @@ def answer_first_questionnaire it "properly interprets HTML descriptions" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_css("b", text: "This question is important") end @@ -355,6 +369,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions end context "when question is single_option type" do @@ -448,6 +463,7 @@ def answer_first_questionnaire it "renders the answer as a textarea" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_css("textarea#questionnaire_responses_0") end @@ -461,6 +477,7 @@ def answer_first_questionnaire it "renders the answer as a text field" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_field(id: "questionnaire_responses_0") end @@ -474,6 +491,7 @@ def answer_first_questionnaire it "renders answers as a collection of radio buttons" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_css(".js-radio-button-collection input[type=radio]", count: 2) @@ -486,6 +504,7 @@ def answer_first_questionnaire expect(page).to have_admin_callout(callout_success) visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("You have already answered this form.") expect(page).to have_no_i18n_content(question.body) @@ -498,6 +517,7 @@ def answer_first_questionnaire it "renders answers as a collection of radio buttons" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_css(".js-check-box-collection input[type=checkbox]", count: 3) @@ -513,6 +533,7 @@ def answer_first_questionnaire expect(page).to have_admin_callout(callout_success) visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("You have already answered this form.") expect(page).to have_no_i18n_content(question.body) @@ -522,6 +543,7 @@ def answer_first_questionnaire question.update!(max_choices: 2) visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("Max choices: 2") @@ -564,6 +586,7 @@ def answer_first_questionnaire it "renders the question answers as a collection of divs sortable on drag and drop" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_css("div.answer-questionnaire__sorting.js-collection-input", count: 5) @@ -574,6 +597,7 @@ def answer_first_questionnaire it "properly saves valid sortings" do visit questionnaire_public_path + see_questionnaire_questions %w(We all like dark chocolate).reverse.each do |text| find("div.answer-questionnaire__sorting", text:).drag_to(find("div.answer-questionnaire__sorting", match: :first)) @@ -608,6 +632,7 @@ def answer_first_questionnaire it "renders the question answers as a collection of radio buttons" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_css(".js-radio-button-collection input[type=radio]", count: 4) @@ -626,6 +651,7 @@ def answer_first_questionnaire expect(page).to have_admin_callout(callout_success) visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("You have already answered this form.") expect(page).to have_no_i18n_content(question.body) @@ -638,6 +664,7 @@ def answer_first_questionnaire it "preserves the chosen answers if submission not correct" do visit questionnaire_public_path + see_questionnaire_questions radio_buttons = page.all(".js-radio-button-collection input[type=radio]") choose radio_buttons[1][:id] @@ -655,6 +682,7 @@ def answer_first_questionnaire it "shows an error if the question is mandatory and the answer is not complete" do visit questionnaire_public_path + see_questionnaire_questions radio_buttons = page.all(".js-radio-button-collection input[type=radio]") choose radio_buttons[0][:id] @@ -688,6 +716,7 @@ def answer_first_questionnaire it "renders the question answers as a collection of check boxes" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_css(".js-check-box-collection input[type=checkbox]", count: 6) @@ -707,6 +736,7 @@ def answer_first_questionnaire expect(page).to have_admin_callout(callout_success) visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("You have already answered this form.") expect(page).to have_no_i18n_content(question.body) @@ -723,6 +753,7 @@ def answer_first_questionnaire it "respects the max number of choices" do visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_content("Max choices: 2") @@ -767,6 +798,7 @@ def answer_first_questionnaire it "shows an error" do visit questionnaire_public_path + see_questionnaire_questions checkboxes = page.all(".js-check-box-collection input[type=checkbox]") check checkboxes[0][:id] @@ -784,6 +816,7 @@ def answer_first_questionnaire it "preserves the chosen answers" do visit questionnaire_public_path + see_questionnaire_questions checkboxes = page.all(".js-check-box-collection input[type=checkbox]") check checkboxes[0][:id] @@ -833,6 +866,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions end context "when the condition_question type is short answer" do @@ -925,6 +959,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions end context "when the condition_question type is short answer" do @@ -1006,6 +1041,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions end context "when the condition_question type is single option" do @@ -1062,6 +1098,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions end context "when the condition_question type is single option" do @@ -1119,6 +1156,7 @@ def answer_first_questionnaire before do visit questionnaire_public_path + see_questionnaire_questions end context "when the condition_question type is short answer" do @@ -1239,6 +1277,7 @@ def answer_first_questionnaire context "when a question has multiple display conditions" do before do visit questionnaire_public_path + see_questionnaire_questions end context "when all conditions are mandatory" do @@ -1327,6 +1366,7 @@ def answer_first_questionnaire it "does not throw error" do visit questionnaire_public_path + see_questionnaire_questions fill_in condition_question.body["en"], with: "My first answer" diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaire_answers.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaire_answers.rb index 03f1f7da6b59f..5a5b6006fe452 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaire_answers.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaire_answers.rb @@ -19,7 +19,7 @@ context "when there are no answers" do it "do not answer admin link" do - visit questionnaire_edit_path + click_on "Manage questions" expect(page).to have_content("No answers yet") end end @@ -31,13 +31,13 @@ let!(:file_answer) { create(:answer, :with_attachments, questionnaire:, question: third, body: nil, user: answer3.user, session_token: answer3.session_token) } it "shows the answer admin link" do - visit questionnaire_edit_path + click_on "Manage questions" expect(page).to have_content("Show responses") end context "and managing answers page" do before do - visit questionnaire_edit_path + click_on "Manage questions" click_on "Show responses" end @@ -97,7 +97,7 @@ let!(:answer11) { create(:answer, questionnaire:, body: "", user: answer1.user, question: second) } before do - visit questionnaire_edit_path + click_on "Manage questions" click_on "Show responses" end diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb index 9ab85092a08a0..ad69735e2c876 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb @@ -24,30 +24,9 @@ } end - it "updates the questionnaire" do - visit questionnaire_edit_path - - new_description = { - en: "

New description

", - ca: "

Nova descripció

", - es: "

Nueva descripción

" - } - - within "form.edit_questionnaire" do - fill_in_i18n_editor(:questionnaire_description, "#questionnaire-description-tabs", new_description) - click_on "Save" - end - - expect(page).to have_admin_callout("successfully") - - visit questionnaire_public_path - - expect(page).to have_content("New description") - end - context "when the questionnaire is not already answered" do before do - visit questionnaire_edit_path + visit manage_questions_path end it_behaves_like "add questions" @@ -61,7 +40,7 @@ let!(:answer) { create(:answer, questionnaire:, question:) } it "cannot modify questionnaire questions" do - visit questionnaire_edit_path + visit manage_questions_path expect(page).to have_no_content("Add question") expect(page).to have_no_content("Remove") @@ -112,8 +91,8 @@ def expand_all_questions find(".button.expand-all").click end - def visit_questionnaire_edit_path_and_expand_all - visit questionnaire_edit_path + def visit_manage_questions_and_expand_all + click_on "Manage questions" expand_all_questions end end diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_display_conditions.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_display_conditions.rb index 7a486f09997a6..69365459606b2 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_display_conditions.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_display_conditions.rb @@ -17,7 +17,8 @@ let!(:question) { create(:questionnaire_question, questionnaire:, body:, question_type: "short_answer") } before do - visit_questionnaire_edit_path_and_expand_all + click_on "Save" + visit_manage_questions_and_expand_all end it "does not display an add display condition button" do @@ -26,12 +27,10 @@ context "when creating a new question" do it "disables the add display condition button if the question has not been saved" do - within "form.edit_questionnaire" do - click_on "Add question" - expand_all_questions + click_on "Add question" + expand_all_questions - expect(page).to have_button("Add display condition", disabled: true) - end + expect(page).to have_button("Add display condition", disabled: true) end end end @@ -71,29 +70,28 @@ let(:questions) { [question_short_answer, question_long_answer, question_single_option, question_multiple_option] } before do - visit_questionnaire_edit_path_and_expand_all + click_on "Save" + visit_manage_questions_and_expand_all end context "when clicking add display condition button" do it "adds a new display condition form with all correct elements" do - within "form.edit_questionnaire" do - within_add_display_condition do - expect(page).to have_select("Question") - expect(page).to have_select("Condition") - expect(page).to have_css("[id$=mandatory]") + within_add_display_condition do + expect(page).to have_select("Question") + expect(page).to have_select("Condition") + expect(page).to have_css("[id$=mandatory]") - select question_single_option.body["en"], from: "Question" - select "Answered", from: "Condition" + select question_single_option.body["en"], from: "Question" + select "Answered", from: "Condition" - expect(page).to have_no_select("Answer option") - expect(page).to have_no_css("[id$=condition_value_en]", visible: :visible) + expect(page).to have_no_select("Answer option") + expect(page).to have_no_css("[id$=condition_value_en]", visible: :visible) - select question_single_option.body["en"], from: "Question" - select "Equal", from: "Condition" + select question_single_option.body["en"], from: "Question" + select "Equal", from: "Condition" - expect(page).to have_select("Answer option") - expect(page).to have_no_css("[id$=condition_value_en]", visible: :visible) - end + expect(page).to have_select("Answer option") + expect(page).to have_no_css("[id$=condition_value_en]", visible: :visible) end end diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb index 60ec5b61b2481..8a3814a01de13 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb @@ -37,29 +37,26 @@ it "adds a few questions and separators to the questionnaire" do fields_body = ["This is the first question", "This is the second question", "This is the first title and description"] + click_on "Add question" + click_on "Add separator" + click_on "Add title and description" + click_on "Add question" - within "form.edit_questionnaire" do - click_on "Add question" - click_on "Add separator" - click_on "Add title and description" - click_on "Add question" - - expect(page).to have_css(".questionnaire-question", count: 4) + expect(page).to have_css(".questionnaire-question", count: 4) - expand_all_questions + expand_all_questions - page.all(".questionnaire-question .collapsible").each_with_index do |field, idx| - within field do - fill_in find_nested_form_field_locator("body_en"), with: fields_body[idx] - end + page.all(".questionnaire-question .collapsible").each_with_index do |field, idx| + within field do + fill_in find_nested_form_field_locator("body_en"), with: fields_body[idx] end - - click_on "Save" end + click_on "Save" + expect(page).to have_admin_callout("successfully") - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all expect(page).to have_css("input[value='This is the first question']") expect(page).to have_css("input[value='This is the second question']") @@ -68,59 +65,45 @@ end it "adds a question with a rich text description" do - within "form.edit_questionnaire" do - click_on "Add question" - expand_all_questions - - within ".questionnaire-question" do - fill_in find_nested_form_field_locator("body_en"), with: "Body" + click_on "Add question" + expand_all_questions - fill_in_editor find_nested_form_field_locator("description_en", visible: false), with: "

\nSuperkalifragilistic description\n

" - end + within ".questionnaire-question" do + fill_in find_nested_form_field_locator("body_en"), with: "Body" - click_on "Save" + fill_in_editor find_nested_form_field_locator("description_en", visible: false), with: "

\nSuperkalifragilistic description\n

" end + click_on "Save" + expect(page).to have_admin_callout("successfully") - component.update!( - step_settings: { - component.participatory_space.active_step.id => { - allow_answers: true - } - } - ) + update_component_settings_or_attributes visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_css("strong", text: "Superkalifragilistic description") end it "adds a title-and-description" do - within "form.edit_questionnaire" do - click_on "Add title and description" - expand_all_questions + click_on "Add title and description" + expand_all_questions - within ".questionnaire-question" do - fill_in find_nested_form_field_locator("body_en"), with: "Body" + within ".questionnaire-question" do + fill_in find_nested_form_field_locator("body_en"), with: "Body" - fill_in_editor find_nested_form_field_locator("description_en", visible: false), with: "

\nSuperkalifragilistic description\n

" - end - - click_on "Save" + fill_in_editor find_nested_form_field_locator("description_en", visible: false), with: "

\nSuperkalifragilistic description\n

" end + click_on "Save" + expect(page).to have_admin_callout("successfully") - component.update!( - step_settings: { - component.participatory_space.active_step.id => { - allow_answers: true - } - } - ) + update_component_settings_or_attributes visit questionnaire_public_path + see_questionnaire_questions expect(page).to have_css("strong", text: "Superkalifragilistic description") end @@ -140,40 +123,38 @@ ] ] - within "form.edit_questionnaire" do - click_on "Add question" - click_on "Add question" - expand_all_questions + click_on "Add question" + click_on "Add question" + expand_all_questions - page.all(".questionnaire-question").each_with_index do |question, idx| - within question do - fill_in find_nested_form_field_locator("body_en"), with: question_body[idx] - end + page.all(".questionnaire-question").each_with_index do |question, idx| + within question do + fill_in find_nested_form_field_locator("body_en"), with: question_body[idx] end + end - expect(page).to have_no_content "Add answer option" + expect(page).to have_no_content "Add answer option" - page.all(".questionnaire-question").each do |question| - within question do - select "Single option", from: "Type" - click_on "Add answer option" - end + page.all(".questionnaire-question").each do |question| + within question do + select "Single option", from: "Type" + click_on "Add answer option" end + end - page.all(".questionnaire-question").each_with_index do |question, question_idx| - question.all(".questionnaire-question-answer-option").each_with_index do |question_answer_option, answer_option_idx| - within question_answer_option do - fill_in find_nested_form_field_locator("body_en"), with: answer_options_body[question_idx][answer_option_idx] - end + page.all(".questionnaire-question").each_with_index do |question, question_idx| + question.all(".questionnaire-question-answer-option").each_with_index do |question_answer_option, answer_option_idx| + within question_answer_option do + fill_in find_nested_form_field_locator("body_en"), with: answer_options_body[question_idx][answer_option_idx] end end - - click_on "Save" end + click_on "Save" + expect(page).to have_admin_callout("successfully") - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all expect(page).to have_css("input[value='This is the first question']") expect(page).to have_css("input[value='This is the Q1 first option']") @@ -278,11 +259,12 @@ click_on "Add question" expand_all_questions + expect(page).to have_text("Type") select "Long answer", from: "Type" click_on "Save" expand_all_questions - expect(page).to have_select("Type", selected: "Long answer") + expect(page).to have_select("Type", selected: "Long answer", wait: 10) end it "does not preserve spurious answer options from previous type selections" do @@ -300,6 +282,7 @@ click_on "Save" expand_all_questions + expect(page).to have_text("Type") select "Single option", from: "Type" within ".questionnaire-question-answer-option:first-of-type" do @@ -322,6 +305,7 @@ click_on "Save" expand_all_questions + expect(page).to have_text("Type") select "Matrix (Single option)", from: "Type" within ".questionnaire-question-matrix-row:first-of-type" do @@ -350,6 +334,7 @@ click_on "Save" expand_all_questions + expect(page).to have_css(".questionnaire-question-answer-option") within ".questionnaire-question-answer-option:first-of-type" do expect(page).to have_nested_field("body_en", with: "Something") end @@ -383,7 +368,6 @@ it "allows switching translated field tabs after form failures" do click_on "Add question" - click_on "Save" expand_all_questions @@ -405,20 +389,16 @@ let(:single_option_string) { "Single option" } before do - visit questionnaire_edit_path - - within "form.edit_questionnaire" do - click_on "Add question" - - expand_all_questions + click_on "Add question" - within ".questionnaire-question" do - fill_in find_nested_form_field_locator("body_en"), with: "This is the first question" - end + expand_all_questions - expect(page).to have_no_content "Add answer option" - expect(page).to have_no_select("Maximum number of choices") + within ".questionnaire-question" do + fill_in find_nested_form_field_locator("body_en"), with: "This is the first question" end + + expect(page).to have_no_content "Add answer option" + expect(page).to have_no_select("Maximum number of choices") end it "updates the free text option selector according to the selected question type" do @@ -442,20 +422,16 @@ let(:single_option_string) { "Matrix (Single option)" } before do - visit questionnaire_edit_path - - within "form.edit_questionnaire" do - click_on "Add question" - expand_all_questions - - within ".questionnaire-question" do - fill_in find_nested_form_field_locator("body_en"), with: "This is the first question" - end + click_on "Add question" + expand_all_questions - expect(page).to have_no_content "Add answer option" - expect(page).to have_no_content "Add row" - expect(page).to have_no_select("Maximum number of choices") + within ".questionnaire-question" do + fill_in find_nested_form_field_locator("body_en"), with: "This is the first question" end + + expect(page).to have_no_content "Add answer option" + expect(page).to have_no_content "Add row" + expect(page).to have_no_select("Maximum number of choices") end it "updates the free text option selector according to the selected question type" do diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_display_conditions.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_display_conditions.rb index 4f991412b5962..87f7da12c5f99 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_display_conditions.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_display_conditions.rb @@ -19,7 +19,8 @@ end before do - visit_questionnaire_edit_path_and_expand_all + click_on "Save" + visit_manage_questions_and_expand_all end it "the related form appears" do @@ -59,35 +60,31 @@ click_on "Save" - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all expect(page).to have_css(".questionnaire-question-display-condition", count: 0) end it "still removes the question even if previous editions rendered the conditions invalid" do - within "form.edit_questionnaire" do - expect(page).to have_css(".questionnaire-question", count: 2) - - within ".questionnaire-question-display-condition:first-of-type" do - select condition_question.body["en"], from: "Question" - select "Includes text", from: "Condition" - fill_in find_nested_form_field_locator("condition_value_en"), with: "" - end + expect(page).to have_css(".questionnaire-question", count: 2) - within ".questionnaire-question:last-of-type" do - click_on "Remove", match: :first - end + within ".questionnaire-question-display-condition:first-of-type" do + select condition_question.body["en"], from: "Question" + select "Includes text", from: "Condition" + fill_in find_nested_form_field_locator("condition_value_en"), with: "" + end - click_on "Save" + within ".questionnaire-question:last-of-type" do + click_on "Remove", match: :first end + click_on "Save" + expect(page).to have_admin_callout("successfully") - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all - within "form.edit_questionnaire" do - expect(page).to have_css(".questionnaire-question", count: 1) - end + expect(page).to have_css(".questionnaire-question", count: 1) end end end diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_questions.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_questions.rb index b9964a1c92270..fbce42a920187 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_questions.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_questions.rb @@ -7,68 +7,61 @@ let!(:question) { create(:questionnaire_question, questionnaire:, body:) } before do - visit questionnaire_edit_path - expand_all_questions + click_on "Save" + visit_manage_questions_and_expand_all end it "modifies the question when the information is valid" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - fill_in "questionnaire_questions_#{question.id}_body_en", with: "Modified question" - fill_in "questionnaire_questions_#{question.id}_max_characters", with: 30 - check "Mandatory" - select "Long answer", from: "Type" - end - - click_on "Save" + within ".questionnaire-question" do + fill_in "questions_questions_#{question.id}_body_en", with: "Modified question" + fill_in "questions_questions_#{question.id}_max_characters", with: 30 + check "Mandatory" + select "Long answer", from: "Type" end + click_on "Save" + expect(page).to have_admin_callout("successfully") - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all expect(page).to have_css("input[value='Modified question']") expect(page).to have_no_css("input[value='This is the first question']") - expect(page).to have_css("input#questionnaire_questions_#{question.id}_mandatory[checked]") - expect(page).to have_css("input#questionnaire_questions_#{question.id}_max_characters[value='30']") - expect(page).to have_css("select#questionnaire_questions_#{question.id}_question_type option[value='long_answer'][selected]") + expect(page).to have_css("input#questions_questions_#{question.id}_mandatory[checked]") + expect(page).to have_css("input#questions_questions_#{question.id}_max_characters[value='30']") + expect(page).to have_css("select#questions_questions_#{question.id}_question_type option[value='long_answer'][selected]") end it "re-renders the form when the information is invalid and displays errors" do expand_all_questions - within "form.edit_questionnaire" do - within ".questionnaire-question" do - expect(page).to have_content("Statement*") - fill_in "questionnaire_questions_#{question.id}_body_en", with: "" - fill_in "questionnaire_questions_#{question.id}_max_characters", with: -3 - check "Mandatory" - select "Matrix (Multiple option)", from: "Type" - select "2", from: "Maximum number of choices" - end - - click_on "Save" + within ".questionnaire-question" do + expect(page).to have_content("Statement*") + fill_in "questions_questions_#{question.id}_body_en", with: "" + fill_in "questions_questions_#{question.id}_max_characters", with: -3 + check "Mandatory" + select "Matrix (Multiple option)", from: "Type" + select "2", from: "Maximum number of choices" end - expand_all_questions + click_on "Save" + click_on "Expand all questions" expect(page).to have_admin_callout("There was a problem saving") - expect(page).to have_content("cannot be blank", count: 5) # empty question, 2 empty default answer options, 2 empty default matrix rows + expect(page).to have_content("cannot be blank", count: 5) expect(page).to have_content("must be greater than or equal to 0", count: 1) expect(page).to have_css("input[value='']") expect(page).to have_no_css("input[value='This is the first question']") - expect(page).to have_css("input#questionnaire_questions_#{question.id}_mandatory[checked]") - expect(page).to have_css("input#questionnaire_questions_#{question.id}_max_characters[value='-3']") + expect(page).to have_css("input#questions_questions_#{question.id}_mandatory[checked]") + expect(page).to have_css("input#questions_questions_#{question.id}_max_characters[value='-3']") + expect(page).to have_css("select#questions_questions_#{question.id}_question_type option[value='matrix_multiple'][selected]") expect(page).to have_select("Maximum number of choices", selected: "2") - expect(page).to have_css("select#questionnaire_questions_#{question.id}_question_type option[value='matrix_multiple'][selected]") end it "preserves deleted status across submission failures" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - click_on "Remove" - end + within ".questionnaire-question" do + click_on "Remove" end click_on "Add question" @@ -84,36 +77,28 @@ end it "removes the question" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - click_on "Remove" - end - - click_on "Save" + within ".questionnaire-question" do + click_on "Remove" end + click_on "Save" + expect(page).to have_admin_callout("successfully") - visit questionnaire_edit_path + click_on "Manage questions" - within "form.edit_questionnaire" do - expect(page).to have_css(".questionnaire-question", count: 0) - end + expect(page).to have_css(".questionnaire-question", count: 0) end it "cannot be moved up" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - expect(page).to have_no_button("Up") - end + within ".questionnaire-question" do + expect(page).to have_no_button("Up") end end it "cannot be moved down" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - expect(page).to have_no_button("Down") - end + within ".questionnaire-question" do + expect(page).to have_no_button("Down") end end end @@ -122,22 +107,20 @@ let!(:question) { create(:questionnaire_question, :title_and_description, questionnaire:, body: title_and_description_body) } before do - visit questionnaire_edit_path - expand_all_questions + click_on "Save" + visit_manage_questions_and_expand_all end it "modifies the question when the information is valid" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - fill_in "questionnaire_questions_#{question.id}_body_en", with: "Modified title and description" - end - - click_on "Save" + within ".questionnaire-question" do + fill_in "questions_questions_#{question.id}_body_en", with: "Modified title and description" end + click_on "Save" + expect(page).to have_admin_callout("successfully") - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all expect(page).to have_css("input[value='Modified title and description']") expect(page).to have_no_css("input[value='This is the first title and description']") @@ -146,14 +129,12 @@ it "re-renders the form when the information is invalid and displays errors" do expand_all_questions - within "form.edit_questionnaire" do - within ".questionnaire-question" do - fill_in "questionnaire_questions_#{question.id}_body_en", with: "" - end - - click_on "Save" + within ".questionnaire-question" do + fill_in "questions_questions_#{question.id}_body_en", with: "" end + click_on "Save" + expand_all_questions expect(page).to have_admin_callout("There was a problem saving") @@ -163,10 +144,8 @@ end it "preserves deleted status across submission failures" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - click_on "Remove" - end + within ".questionnaire-question" do + click_on "Remove" end click_on "Add question" @@ -182,36 +161,28 @@ end it "removes the question" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - click_on "Remove" - end - - click_on "Save" + within ".questionnaire-question" do + click_on "Remove" end + click_on "Save" + expect(page).to have_admin_callout("successfully") - visit questionnaire_edit_path + click_on "Manage questions" - within "form.edit_questionnaire" do - expect(page).to have_css(".questionnaire-question", count: 0) - end + expect(page).to have_css(".questionnaire-question", count: 0) end it "cannot be moved up" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - expect(page).to have_no_button("Up") - end + within ".questionnaire-question" do + expect(page).to have_no_button("Up") end end it "cannot be moved down" do - within "form.edit_questionnaire" do - within ".questionnaire-question" do - expect(page).to have_no_button("Down") - end + within ".questionnaire-question" do + expect(page).to have_no_button("Down") end end end @@ -233,7 +204,8 @@ end before do - visit questionnaire_edit_path + click_on "Save" + click_on "Manage questions" end it "allows deleting answer options" do @@ -245,35 +217,31 @@ click_on "Save" - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all expect(page).to have_css(".questionnaire-question-answer-option", count: 2) end it "still removes the question even if previous editions rendered the options invalid" do - within "form.edit_questionnaire" do - expect(page).to have_css(".questionnaire-question", count: 1) - - expand_all_questions + expect(page).to have_css(".questionnaire-question", count: 1) - within ".questionnaire-question-answer-option:first-of-type" do - fill_in find_nested_form_field_locator("body_en"), with: "" - end + expand_all_questions - within ".questionnaire-question" do - click_on "Remove", match: :first - end + within ".questionnaire-question-answer-option:first-of-type" do + fill_in find_nested_form_field_locator("body_en"), with: "" + end - click_on "Save" + within ".questionnaire-question" do + click_on "Remove", match: :first end + click_on "Save" + expect(page).to have_admin_callout("successfully") - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all - within "form.edit_questionnaire" do - expect(page).to have_css(".questionnaire-question", count: 0) - end + expect(page).to have_css(".questionnaire-question", count: 0) end end @@ -300,7 +268,8 @@ end before do - visit_questionnaire_edit_path_and_expand_all + click_on "Save" + visit_manage_questions_and_expand_all end it "allows deleting matrix rows" do @@ -310,7 +279,7 @@ click_on "Save" - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all within ".questionnaire-question:last-of-type" do expect(page).to have_css(".questionnaire-question-matrix-row", count: 2) @@ -319,27 +288,23 @@ end it "still removes the question even if previous editions rendered the rows invalid" do - within "form.edit_questionnaire" do - expect(page).to have_css(".questionnaire-question", count: 2) - - within ".questionnaire-question-matrix-row:first-of-type" do - fill_in find_nested_form_field_locator("body_en"), with: "" - end + expect(page).to have_css(".questionnaire-question", count: 2) - within ".questionnaire-question:last-of-type" do - click_on "Remove", match: :first - end + within ".questionnaire-question-matrix-row:first-of-type" do + fill_in find_nested_form_field_locator("body_en"), with: "" + end - click_on "Save" + within ".questionnaire-question:last-of-type" do + click_on "Remove", match: :first end + click_on "Save" + expect(page).to have_admin_callout("successfully") - visit_questionnaire_edit_path_and_expand_all + visit_manage_questions_and_expand_all - within "form.edit_questionnaire" do - expect(page).to have_css(".questionnaire-question", count: 1) - end + expect(page).to have_css(".questionnaire-question", count: 1) end end @@ -361,8 +326,8 @@ end before do - visit questionnaire_edit_path - expand_all_questions + click_on "Save" + visit_manage_questions_and_expand_all end shared_examples_for "switching questions order" do diff --git a/decidim-forms/spec/commands/decidim/forms/admin/update_questionnaire_spec.rb b/decidim-forms/spec/commands/decidim/forms/admin/update_questionnaire_spec.rb index 4883c0693bcca..e2f30be5e7c94 100644 --- a/decidim-forms/spec/commands/decidim/forms/admin/update_questionnaire_spec.rb +++ b/decidim-forms/spec/commands/decidim/forms/admin/update_questionnaire_spec.rb @@ -27,173 +27,7 @@ module Admin "en" => "

Content

", "ca" => "

Contingut

", "es" => "

Contenido

" - }, - "questions" => { - "0" => { - "body" => { - "en" => "First question", - "ca" => "Primera pregunta", - "es" => "Primera pregunta" - }, - "position" => "0", - "question_type" => "short_answer", - "max_characters" => "0", - "answer_options" => {} - }, - "1" => { - "body" => { - "en" => "Second question", - "ca" => "Segona pregunta", - "es" => "Segunda pregunta" - }, - "description" => { "en" => "Description" }, - "position" => "1", - "mandatory" => "1", - "question_type" => "long_answer", - "max_characters" => "100", - "answer_options" => {} - }, - "2" => { - "body" => { - "en" => "Third question", - "ca" => "Tercera pregunta", - "es" => "Tercera pregunta" - }, - "position" => "2", - "question_type" => "single_option", - "max_characters" => "0", - "answer_options" => { - "0" => { - "body" => { - "en" => "First answer", - "ca" => "Primera resposta", - "es" => "Primera respuesta" - }, - "free_text" => "0" - }, - "1" => { - "body" => { - "en" => "Second answer", - "ca" => "Segona resposta", - "es" => "Segunda respuesta" - } - } - } - }, - "3" => { - "body" => { - "en" => "Fourth question", - "ca" => "Quarta pregunta", - "es" => "Cuarta pregunta" - }, - "position" => "3", - "question_type" => "multiple_option", - "max_choices" => "2", - "answer_options" => { - "0" => { - "body" => { - "en" => "First answer", - "ca" => "Primera resposta", - "es" => "Primera respuesta" - }, - "free_text" => "1" - }, - "1" => { - "body" => { - "en" => "Second answer", - "ca" => "Segona resposta", - "es" => "Segunda respuesta" - } - } - } - }, - "4" => { - "body" => { - "en" => "Fifth question", - "ca" => "Cinquena pregunta", - "es" => "Quinta pregunta" - }, - "position" => "4", - "question_type" => "matrix_single", - "answer_options" => { - "0" => { - "body" => { - "en" => "First answer", - "ca" => "Primera resposta", - "es" => "Primera respuesta" - }, - "free_text" => "1" - }, - "1" => { - "body" => { - "en" => "Second answer", - "ca" => "Segona resposta", - "es" => "Segunda respuesta" - } - } - }, - "matrix_rows" => { - "0" => { - "body" => { - "en" => "First row", - "ca" => "Primera fila", - "es" => "Primera fila" - } - }, - "1" => { - "body" => { - "en" => "Second row", - "ca" => "Segona fila", - "es" => "Segunda fila" - } - } - } - }, - "5" => { - "body" => { - "en" => "Sixth question", - "ca" => "Sisena pregunta", - "es" => "Sexta pregunta" - }, - "position" => "5", - "question_type" => "matrix_multiple", - "max_choices" => "2", - "answer_options" => { - "0" => { - "body" => { - "en" => "First answer", - "ca" => "Primera resposta", - "es" => "Primera respuesta" - }, - "free_text" => "1" - }, - "1" => { - "body" => { - "en" => "Second answer", - "ca" => "Segona resposta", - "es" => "Segunda respuesta" - } - } - }, - "matrix_rows" => { - "0" => { - "body" => { - "en" => "First row", - "ca" => "Primera fila", - "es" => "Primera fila" - } - }, - "1" => { - "body" => { - "en" => "Second row", - "ca" => "Segona fila", - "es" => "Segunda fila" - } - } - } - } - }, - "published_at" => published_at + } } end let(:form) do @@ -221,6 +55,10 @@ module Admin end describe "when the form is valid" do + before do + allow(form).to receive(:invalid?).and_return(false) + end + it "broadcasts ok" do expect { command.call }.to broadcast(:ok) end @@ -230,40 +68,8 @@ module Admin questionnaire.reload expect(questionnaire.description["en"]).to eq("

Content

") - expect(questionnaire.questions.length).to eq(6) - - questionnaire.questions.each_with_index do |question, idx| - expect(question.body["en"]).to eq(form_params["questions"][idx.to_s]["body"]["en"]) - end - - expect(questionnaire.questions[1]).to be_mandatory - expect(questionnaire.questions[1].description["en"]).to eq(form_params["questions"]["1"]["description"]["en"]) - expect(questionnaire.questions[1].question_type).to eq("long_answer") - expect(questionnaire.questions[1].max_characters).to eq(100) - expect(questionnaire.questions[2].answer_options[1]["body"]["en"]).to eq(form_params["questions"]["2"]["answer_options"]["1"]["body"]["en"]) - - expect(questionnaire.questions[2].question_type).to eq("single_option") - expect(questionnaire.questions[2].max_choices).to be_nil - expect(questionnaire.questions[2].max_characters).to eq(0) - - expect(questionnaire.questions[3].question_type).to eq("multiple_option") - expect(questionnaire.questions[2].answer_options[0].free_text).to be(false) - expect(questionnaire.questions[2].max_choices).to be_nil - - expect(questionnaire.questions[3].question_type).to eq("multiple_option") - expect(questionnaire.questions[3].answer_options[0].free_text).to be(true) - expect(questionnaire.questions[3].max_choices).to eq(2) - - expect(questionnaire.questions[4].question_type).to eq("matrix_single") - expect(questionnaire.questions[4].answer_options[0].free_text).to be(true) - 2.times do |idx| - expect(questionnaire.questions[4].matrix_rows[idx].body["en"]).to eq(form_params["questions"]["4"]["matrix_rows"][idx.to_s]["body"]["en"]) - expect(questionnaire.questions[4].matrix_rows[idx].position).to eq(idx) - end - - expect(questionnaire.questions[5].question_type).to eq("matrix_multiple") - expect(questionnaire.questions[5].answer_options[0].free_text).to be(true) - expect(questionnaire.questions[5].matrix_rows[0].body["en"]).to eq(form_params["questions"]["5"]["matrix_rows"]["0"]["body"]["en"]) + expect(questionnaire.title["en"]).to eq("Title") + expect(questionnaire.tos["en"]).to eq("

TOS

") end it "traces the action", versioning: true do @@ -278,122 +84,6 @@ module Admin expect(action_log.version).to be_present end end - - describe "when the questionnaire has an existing question" do - let!(:question) { create(:questionnaire_question, questionnaire:) } - - context "and the question should be removed" do - let(:form_params) do - { - "title" => { - "en" => "Title", - "ca" => "Title", - "es" => "Title" - }, - "description" => { - "en" => "

Content

", - "ca" => "

Contingut

", - "es" => "

Contenido

" - }, - "tos" => { - "en" => "

TOS

", - "ca" => "

TOS

", - "es" => "

TOS

" - }, - "questions" => [ - { - "id" => question.id, - "body" => question.body, - "position" => 0, - "question_type" => "short_answer", - "deleted" => "true" - } - ] - } - end - - it "deletes the questionnaire question" do - command.call - questionnaire.reload - - expect(questionnaire.questions.length).to eq(0) - end - end - end - - describe "when the questionnaire has existing questions" do - let!(:questions) { 0.upto(3).to_a.map { |x| create(:questionnaire_question, questionnaire:, position: x) } } - let!(:question_2_answer_options) { create_list(:answer_option, 3, question: questions.second) } - - context "and display conditions are to be created" do - let(:form_params) do - { - "title" => { - "en" => "Title", - "ca" => "Títol", - "es" => "Título" - }, - "description" => { - "en" => "

Content

", - "ca" => "

Contingut

", - "es" => "

Contenido

" - }, - "tos" => { - "en" => "

TOS

", - "ca" => "

TOS

", - "es" => "

TOS

" - }, - "questions" => { - "1" => { - "id" => questions[0].id, - "body" => questions[0].body, - "position" => 0, - "question_type" => "short_answer" - }, - "2" => { - "id" => questions[1].id, - "body" => questions[1].body, - "position" => 1, - "question_type" => "single_option", - "answer_options" => question_2_answer_options.to_h do |answer_option| - [answer_option.id.to_s, { "id" => answer_option.id, "body" => answer_option.body, "free_text" => answer_option.free_text, "deleted" => false }] - end - }, - "3" => { - "id" => questions[2].id, - "body" => questions[2].body, - "position" => 2, - "question_type" => "short_answer", - "deleted" => "false", - "display_conditions" => { - "1" => { - "decidim_condition_question_id" => questions[0].id, - "decidim_question_id" => questions[2].id, - "condition_type" => "answered" - }, - "2" => { - "decidim_condition_question_id" => questions[1].id, - "decidim_question_id" => questions[2].id, - "condition_type" => "equal", - "decidim_answer_option_id" => question_2_answer_options.first.id - } - } - } - } - } - end - - it "saves the questionnaire" do - expect { command.call }.to broadcast(:ok) - questionnaire.reload - - expect(questionnaire.questions[2].display_conditions).not_to be_empty - expect(questionnaire.questions[2].display_conditions.first.condition_type).to eq("answered") - expect(questionnaire.questions[2].display_conditions.second.condition_type).to eq("equal") - expect(questionnaire.questions[2].display_conditions.second.decidim_answer_option_id).to eq(question_2_answer_options.first.id) - end - end - end end end end diff --git a/decidim-forms/spec/commands/decidim/forms/admin/update_questions_spec.rb b/decidim-forms/spec/commands/decidim/forms/admin/update_questions_spec.rb new file mode 100644 index 0000000000000..282207ff9a4a5 --- /dev/null +++ b/decidim-forms/spec/commands/decidim/forms/admin/update_questions_spec.rb @@ -0,0 +1,355 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Forms + module Admin + describe UpdateQuestions do + let(:current_organization) { create(:organization) } + let(:participatory_process) { create(:participatory_process, organization: current_organization) } + let(:questionnaire) { create(:questionnaire, questionnaire_for: participatory_process) } + let(:user) { create(:user, organization: current_organization) } + let(:published_at) { nil } + let(:form_params) do + { + "questions" => { + "0" => { + "body" => { + "en" => "First question", + "ca" => "Primera pregunta", + "es" => "Primera pregunta" + }, + "position" => "0", + "question_type" => "short_answer", + "max_characters" => "0", + "answer_options" => {} + }, + "1" => { + "body" => { + "en" => "Second question", + "ca" => "Segona pregunta", + "es" => "Segunda pregunta" + }, + "description" => { "en" => "Description" }, + "position" => "1", + "mandatory" => "1", + "question_type" => "long_answer", + "max_characters" => "100", + "answer_options" => {} + }, + "2" => { + "body" => { + "en" => "Third question", + "ca" => "Tercera pregunta", + "es" => "Tercera pregunta" + }, + "position" => "2", + "question_type" => "single_option", + "max_characters" => "0", + "answer_options" => { + "0" => { + "body" => { + "en" => "First answer", + "ca" => "Primera resposta", + "es" => "Primera respuesta" + }, + "free_text" => "0" + }, + "1" => { + "body" => { + "en" => "Second answer", + "ca" => "Segona resposta", + "es" => "Segunda respuesta" + } + } + } + }, + "3" => { + "body" => { + "en" => "Fourth question", + "ca" => "Quarta pregunta", + "es" => "Cuarta pregunta" + }, + "position" => "3", + "question_type" => "multiple_option", + "max_choices" => "2", + "answer_options" => { + "0" => { + "body" => { + "en" => "First answer", + "ca" => "Primera resposta", + "es" => "Primera respuesta" + }, + "free_text" => "1" + }, + "1" => { + "body" => { + "en" => "Second answer", + "ca" => "Segona resposta", + "es" => "Segunda respuesta" + } + } + } + }, + "4" => { + "body" => { + "en" => "Fifth question", + "ca" => "Cinquena pregunta", + "es" => "Quinta pregunta" + }, + "position" => "4", + "question_type" => "matrix_single", + "answer_options" => { + "0" => { + "body" => { + "en" => "First answer", + "ca" => "Primera resposta", + "es" => "Primera respuesta" + }, + "free_text" => "1" + }, + "1" => { + "body" => { + "en" => "Second answer", + "ca" => "Segona resposta", + "es" => "Segunda respuesta" + } + } + }, + "matrix_rows" => { + "0" => { + "body" => { + "en" => "First row", + "ca" => "Primera fila", + "es" => "Primera fila" + } + }, + "1" => { + "body" => { + "en" => "Second row", + "ca" => "Segona fila", + "es" => "Segunda fila" + } + } + } + }, + "5" => { + "body" => { + "en" => "Sixth question", + "ca" => "Sisena pregunta", + "es" => "Sexta pregunta" + }, + "position" => "5", + "question_type" => "matrix_multiple", + "max_choices" => "2", + "answer_options" => { + "0" => { + "body" => { + "en" => "First answer", + "ca" => "Primera resposta", + "es" => "Primera respuesta" + }, + "free_text" => "1" + }, + "1" => { + "body" => { + "en" => "Second answer", + "ca" => "Segona resposta", + "es" => "Segunda respuesta" + } + } + }, + "matrix_rows" => { + "0" => { + "body" => { + "en" => "First row", + "ca" => "Primera fila", + "es" => "Primera fila" + } + }, + "1" => { + "body" => { + "en" => "Second row", + "ca" => "Segona fila", + "es" => "Segunda fila" + } + } + } + } + }, + "published_at" => published_at + } + end + let(:form) do + QuestionsForm.from_params( + questions: form_params + ).with_context( + current_organization:, + current_user: user + ) + end + let(:command) { described_class.new(form, questionnaire) } + + describe "when the form is invalid" do + before do + allow(form).to receive(:invalid?).and_return(true) + end + + it "broadcasts invalid" do + expect { command.call }.to broadcast(:invalid) + end + + it "does not update the questionnaire" do + expect(questionnaire).not_to receive(:update!) + command.call + end + end + + describe "when the form is valid" do + it "broadcasts ok" do + expect { command.call }.to broadcast(:ok) + end + + it "updates the questions" do + command.call + questionnaire.reload + + expect(questionnaire.questions.length).to eq(6) + + questionnaire.questions.each_with_index do |question, idx| + expect(question.body["en"]).to eq(form_params["questions"][idx.to_s]["body"]["en"]) + end + + expect(questionnaire.questions[1]).to be_mandatory + expect(questionnaire.questions[1].description["en"]).to eq(form_params["questions"]["1"]["description"]["en"]) + expect(questionnaire.questions[1].question_type).to eq("long_answer") + expect(questionnaire.questions[1].max_characters).to eq(100) + expect(questionnaire.questions[2].answer_options[1]["body"]["en"]).to eq(form_params["questions"]["2"]["answer_options"]["1"]["body"]["en"]) + + expect(questionnaire.questions[2].question_type).to eq("single_option") + expect(questionnaire.questions[2].max_choices).to be_nil + expect(questionnaire.questions[2].max_characters).to eq(0) + + expect(questionnaire.questions[3].question_type).to eq("multiple_option") + expect(questionnaire.questions[2].answer_options[0].free_text).to be(false) + expect(questionnaire.questions[2].max_choices).to be_nil + + expect(questionnaire.questions[3].question_type).to eq("multiple_option") + expect(questionnaire.questions[3].answer_options[0].free_text).to be(true) + expect(questionnaire.questions[3].max_choices).to eq(2) + + expect(questionnaire.questions[4].question_type).to eq("matrix_single") + expect(questionnaire.questions[4].answer_options[0].free_text).to be(true) + 2.times do |idx| + expect(questionnaire.questions[4].matrix_rows[idx].body["en"]).to eq(form_params["questions"]["4"]["matrix_rows"][idx.to_s]["body"]["en"]) + expect(questionnaire.questions[4].matrix_rows[idx].position).to eq(idx) + end + + expect(questionnaire.questions[5].question_type).to eq("matrix_multiple") + expect(questionnaire.questions[5].answer_options[0].free_text).to be(true) + expect(questionnaire.questions[5].matrix_rows[0].body["en"]).to eq(form_params["questions"]["5"]["matrix_rows"]["0"]["body"]["en"]) + end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:perform_action!) + .with("update", questionnaire, user) + .and_call_original + + expect { command.call }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + expect(action_log.action).to eq("update") + expect(action_log.version).to be_present + end + end + + describe "when the questionnaire has an existing question" do + let!(:question) { create(:questionnaire_question, questionnaire:) } + + context "and the question should be removed" do + let(:form_params) do + { + "questions" => [ + { + "id" => question.id, + "body" => question.body, + "position" => 0, + "question_type" => "short_answer", + "deleted" => "true" + } + ] + } + end + + it "deletes the questionnaire question" do + command.call + questionnaire.reload + + expect(questionnaire.questions.length).to eq(0) + end + end + end + + describe "when the questionnaire has existing questions" do + let!(:questions) { 0.upto(3).to_a.map { |x| create(:questionnaire_question, questionnaire:, position: x) } } + let!(:question_2_answer_options) { create_list(:answer_option, 3, question: questions.second) } + + context "and display conditions are to be created" do + let(:form_params) do + { + "questions" => { + "1" => { + "id" => questions[0].id, + "body" => questions[0].body, + "position" => 0, + "question_type" => "short_answer" + }, + "2" => { + "id" => questions[1].id, + "body" => questions[1].body, + "position" => 1, + "question_type" => "single_option", + "answer_options" => question_2_answer_options.to_h do |answer_option| + [answer_option.id.to_s, { "id" => answer_option.id, "body" => answer_option.body, "free_text" => answer_option.free_text, "deleted" => false }] + end + }, + "3" => { + "id" => questions[2].id, + "body" => questions[2].body, + "position" => 2, + "question_type" => "short_answer", + "deleted" => "false", + "display_conditions" => { + "1" => { + "decidim_condition_question_id" => questions[0].id, + "decidim_question_id" => questions[2].id, + "condition_type" => "answered" + }, + "2" => { + "decidim_condition_question_id" => questions[1].id, + "decidim_question_id" => questions[2].id, + "condition_type" => "equal", + "decidim_answer_option_id" => question_2_answer_options.first.id + } + } + } + } + } + end + + it "saves the questionnaire" do + expect { command.call }.to broadcast(:ok) + questionnaire.reload + + expect(questionnaire.questions[2].display_conditions).not_to be_empty + expect(questionnaire.questions[2].display_conditions.first.condition_type).to eq("answered") + expect(questionnaire.questions[2].display_conditions.second.condition_type).to eq("equal") + expect(questionnaire.questions[2].display_conditions.second.decidim_answer_option_id).to eq(question_2_answer_options.first.id) + end + end + end + end + end + end +end diff --git a/decidim-forms/spec/forms/decidim/forms/admin/questionnaire_form_spec.rb b/decidim-forms/spec/forms/decidim/forms/admin/questionnaire_form_spec.rb index 23b4f1fccc777..61bbe99ed0c78 100644 --- a/decidim-forms/spec/forms/decidim/forms/admin/questionnaire_form_spec.rb +++ b/decidim-forms/spec/forms/decidim/forms/admin/questionnaire_form_spec.rb @@ -38,41 +38,14 @@ module Admin } end - let(:body_english) { "First question" } let(:tos_english) { "

TOS: content

" } - let(:questions) do - [ - { - body: { - "en" => body_english, - "ca" => "Primera pregunta", - "es" => "Primera pregunta" - }, - position: 0, - question_type: "short_answer" - }, - { - body: { - "en" => "Second question", - "ca" => "Segona pregunta", - "es" => "Segunda pregunta" - }, - position: 1, - mandatory: true, - question_type: "short_answer", - max_characters: 30 - } - ] - end - let(:attributes) do { "questionnaire" => { "tos" => tos, "title" => title, - "description" => description, - "questions" => questions + "description" => description } } end @@ -81,12 +54,6 @@ module Admin it { is_expected.to be_valid } end - context "when a question is not valid" do - let(:body_english) { "" } - - it { is_expected.not_to be_valid } - end - context "when tos is not valid" do let(:tos_english) { "" } diff --git a/decidim-forms/spec/forms/decidim/forms/admin/questions_form_spec.rb b/decidim-forms/spec/forms/decidim/forms/admin/questions_form_spec.rb new file mode 100644 index 0000000000000..240dc17165d73 --- /dev/null +++ b/decidim-forms/spec/forms/decidim/forms/admin/questions_form_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Forms + module Admin + describe QuestionsForm do + subject do + described_class.from_params(attributes).with_context( + current_organization: + ) + end + + let(:body_english) { "First question" } + let(:current_organization) { create(:organization) } + + let(:questions) do + [ + { + body: { + "en" => body_english, + "ca" => "Primera pregunta", + "es" => "Primera pregunta" + }, + position: 0, + question_type: "single_option", + answer_options: [ + { "body" => { "en" => "A" } }, + { "body" => { "en" => "B" } }, + { "body" => { "en" => "C" } } + ] + }, + { + body: { + "en" => "Second question", + "ca" => "Segona pregunta", + "es" => "Segunda pregunta" + }, + position: 1, + question_type: "multiple_option", + answer_options: [ + { "body" => { "en" => "A" } }, + { "body" => { "en" => "B" } }, + { "body" => { "en" => "C" } } + ] + } + ] + end + + let(:attributes) do + { + "questions" => questions + } + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when a question is not valid" do + let(:body_english) { "" } + + it { is_expected.not_to be_valid } + end + end + end + end +end diff --git a/decidim-forms/spec/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper_spec.rb b/decidim-forms/spec/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper_spec.rb new file mode 100644 index 0000000000000..5f5cd914649b0 --- /dev/null +++ b/decidim-forms/spec/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Forms + module Admin + module Concerns + describe HasQuestionnaireAnswersUrlHelper, type: :controller do + controller(ApplicationController) do + include Decidim::Forms::Admin::Concerns::HasQuestionnaireAnswersUrlHelper + + def index; end + end + + let(:questionnaire) { double("Questionnaire", questionnaire_for: "/test-path") } + + before do + allow(controller).to receive(:questionnaire).and_return(questionnaire) + routes.draw { get "index" => "anonymous#index" } + end + + describe "#questionnaire_url" do + it "returns the correct URL for the questionnaire" do + expect(controller.questionnaire_url).to eq("/test-path") + end + end + end + end + end + end +end diff --git a/decidim-meetings/app/controllers/decidim/meetings/admin/registration_form_controller.rb b/decidim-meetings/app/controllers/decidim/meetings/admin/registration_form_controller.rb index 33e69aef63990..af1c5367816ba 100644 --- a/decidim-meetings/app/controllers/decidim/meetings/admin/registration_form_controller.rb +++ b/decidim-meetings/app/controllers/decidim/meetings/admin/registration_form_controller.rb @@ -23,8 +23,16 @@ def public_url Decidim::EngineRouter.main_proxy(current_component).join_meeting_registration_path(meeting) end + def edit_questions_template + "decidim/meetings/admin/registration_form/edit_questions" + end + private + def i18n_questions_flashes_scope + "decidim.forms.admin.questionnaires" + end + def meeting @meeting ||= Meeting.where(component: current_component).find(params[:meeting_id]) end diff --git a/decidim-meetings/app/views/decidim/meetings/admin/registration_form/edit_questions.html.erb b/decidim-meetings/app/views/decidim/meetings/admin/registration_form/edit_questions.html.erb new file mode 100644 index 0000000000000..c6fc2f7666e41 --- /dev/null +++ b/decidim-meetings/app/views/decidim/meetings/admin/registration_form/edit_questions.html.erb @@ -0,0 +1,44 @@ +<% add_decidim_page_title(t("decidim.forms.admin.questionnaires.edit_questions.title")) %> + +<% if templates_defined? && choose_template? %> + <%= render partial: "decidim/templates/admin/questionnaire_templates/choose", locals: { target: questionnaire, form_title: t("decidim.forms.admin.questionnaires.edit.title") } %> +<% else %> + +
+
+

+ <%= t("decidim.forms.admin.questionnaires.edit_questions.title") %> + <% if questionnaire.questions_editable? %> + + + + <% end %> + <% if allowed_to? :preview, :questionnaire %> + <%= link_to t("preview", scope: "decidim.forms.admin.questionnaires.form"), public_url, class: "button button__sm button__transparent-secondary", target: :_blank, data: { "external-link": false } %> + <% end %> + <% if questionnaire.answers.any? %> + <%= export_dropdown(current_component, questionnaire.id) if allowed_to? :export_answers, :questionnaire %> + <% if allowed_to? :show, :questionnaire_answers %> + <%= link_to t("actions.show", scope: "decidim.forms.admin.questionnaires"), questionnaire_participants_url, class: "button button__sm button__secondary new whitespace-nowrap" %> + <% end %> + <% else %> + + <% end %> +

+
+
+ +
+
+ <%= decidim_form_for(@form, url: update_questions_meeting_registrations_form_path(meeting_id: meeting.id), method: :patch, html: { class: "form-defaults form edit_questions_survey" }) do |form| %> + <%= render partial: "decidim/forms/admin/questionnaires/questions_form", object: form %> +
+
+ <%= form.submit t("save", scope: "decidim.forms.admin.questionnaires.edit"), class: "button button__sm button__secondary" %> +
+
+ <% end %> +
+
+ +<% end %> diff --git a/decidim-meetings/app/views/decidim/meetings/admin/registrations/edit.html.erb b/decidim-meetings/app/views/decidim/meetings/admin/registrations/edit.html.erb index 8c3ce6f4b83af..5240bccce4db6 100644 --- a/decidim-meetings/app/views/decidim/meetings/admin/registrations/edit.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/admin/registrations/edit.html.erb @@ -20,6 +20,7 @@
<% end %> <%= link_to t("registration_form" , scope: "decidim.meetings.admin.registrations.form"), edit_meeting_registrations_form_path(meeting_id: meeting.id), class: "button button__sm button__secondary" %> + <%= link_to t("manage_questions" , scope: "decidim.meetings.admin.registrations.form"), edit_questions_meeting_registrations_form_path(meeting_id: meeting.id), class: "button button__sm button__secondary" %>
diff --git a/decidim-meetings/config/locales/en.yml b/decidim-meetings/config/locales/en.yml index 8a88232a5c5b3..48b4a6e133611 100644 --- a/decidim-meetings/config/locales/en.yml +++ b/decidim-meetings/config/locales/en.yml @@ -409,6 +409,7 @@ en: form: available_slots_help: Leave it to 0 if you have unlimited slots available. invites: Invitations + manage_questions: Manage questions recommendation_message: For privacy reasons we recommend that you delete this inscription form when you no longer need it. By default this is 3 months after the meeting has ended. registration_email_help: This text will appear in the middle of the registration confirmation email. Just after the registration code. registration_form: Registration form diff --git a/decidim-meetings/lib/decidim/meetings/admin_engine.rb b/decidim-meetings/lib/decidim/meetings/admin_engine.rb index 1d85dbbb6fcf4..4439048d450f2 100644 --- a/decidim-meetings/lib/decidim/meetings/admin_engine.rb +++ b/decidim-meetings/lib/decidim/meetings/admin_engine.rb @@ -26,7 +26,12 @@ class AdminEngine < ::Rails::Engine end resource :registrations, only: [:edit, :update] do resources :invites, only: [:index, :create] - resource :form, only: [:edit, :update], controller: "registration_form" + resource :form, only: [:edit, :update], controller: "registration_form" do + member do + get :edit_questions + patch :update_questions + end + end collection do get :export post :validate_registration_code diff --git a/decidim-meetings/spec/system/admin/admin_manages_meetings_registration_forms_spec.rb b/decidim-meetings/spec/system/admin/admin_manages_meetings_registration_forms_spec.rb index 440e56f795da6..18084dfe446fe 100644 --- a/decidim-meetings/spec/system/admin/admin_manages_meetings_registration_forms_spec.rb +++ b/decidim-meetings/spec/system/admin/admin_manages_meetings_registration_forms_spec.rb @@ -20,10 +20,26 @@ def questionnaire_edit_path Decidim::EngineRouter.admin_proxy(component).edit_meeting_registrations_form_path(meeting_id: meeting.id) end + def manage_questions_path + Decidim::EngineRouter.admin_proxy(component).edit_questions_meeting_registrations_form_path(meeting_id: meeting.id) + end + def questionnaire_public_path Decidim::EngineRouter.main_proxy(component).join_meeting_registration_path(meeting_id: meeting.id) end + def update_component_settings_or_attributes + component.update!( + step_settings: { + component.participatory_space.active_step.id => { + allow_answers: true + } + } + ) + end + + def see_questionnaire_questions; end + describe "manages registration form" do it "allows to change the custom content in registration email" do visit registrations_edit_path diff --git a/decidim-meetings/spec/system/meeting_registrations_spec.rb b/decidim-meetings/spec/system/meeting_registrations_spec.rb index 666553ad5b2b7..6366ffcf5837f 100644 --- a/decidim-meetings/spec/system/meeting_registrations_spec.rb +++ b/decidim-meetings/spec/system/meeting_registrations_spec.rb @@ -30,6 +30,8 @@ def questionnaire_public_path Decidim::EngineRouter.main_proxy(component).join_meeting_registration_path(meeting_id: meeting.id) end + def see_questionnaire_questions; end + before do stub_geocoding_coordinates([meeting.latitude, meeting.longitude]) meeting.update!( @@ -475,4 +477,6 @@ def questionnaire_public_path end end end + + def see_questionnaire_questions; end end diff --git a/decidim-surveys/app/cells/decidim/surveys/survey_card_metadata_cell.rb b/decidim-surveys/app/cells/decidim/surveys/survey_card_metadata_cell.rb new file mode 100644 index 0000000000000..07cc6d6fbbea7 --- /dev/null +++ b/decidim-surveys/app/cells/decidim/surveys/survey_card_metadata_cell.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + # This cell renders metadata for an instance of a Survey + class SurveyCardMetadataCell < Decidim::CardMetadataCell + include Decidim::LayoutHelper + include ActionView::Helpers::DateHelper + + alias survey model + + def initialize(*) + super + + @items.prepend(*survey_items) + end + + private + + def survey_items + [duration, questions_count_item] + end + + def survey_items_for_map + [duration, questions_count_item].compact_blank.map do |item| + { + text: item[:text].to_s.html_safe, + icon: item[:icon].present? ? icon(item[:icon]).html_safe : nil + } + end + end + + def duration + text = survey.open? ? t("open", scope: "decidim.surveys.surveys.show") : t("closed", scope: "decidim.surveys.surveys.show") + + { + text:, + icon: "time-line" + } + end + + def questions_count_item + text = "#{survey.questionnaire.questions.size} #{t("questions", scope: "decidim.surveys.surveys.show")}" + + { + text:, + icon: "survey-line" + } + end + end + end +end diff --git a/decidim-surveys/app/cells/decidim/surveys/survey_cell.rb b/decidim-surveys/app/cells/decidim/surveys/survey_cell.rb new file mode 100644 index 0000000000000..d71e8d49213d7 --- /dev/null +++ b/decidim-surveys/app/cells/decidim/surveys/survey_cell.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + # This cell renders the proposal card for an instance of a Survey + # the default size is the Medium Card (:m) + class SurveyCell < Decidim::ViewModel + include Cell::ViewModel::Partial + + def show + cell card_size, model, options + end + + private + + def card_size + case @options[:size] + when :s + "decidim/surveys/survey_s" + else + "decidim/surveys/survey_l" + end + end + end + end +end diff --git a/decidim-surveys/app/cells/decidim/surveys/survey_l_cell.rb b/decidim-surveys/app/cells/decidim/surveys/survey_l_cell.rb new file mode 100644 index 0000000000000..4a6041b894ae0 --- /dev/null +++ b/decidim-surveys/app/cells/decidim/surveys/survey_l_cell.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "cell/partial" + +module Decidim + module Surveys + # This cell renders the List (:l) survey card + # for a given instance of a Survey + class SurveyLCell < Decidim::CardLCell + include Decidim::SanitizeHelper + + def has_description? + true + end + + def title + decidim_sanitize_translated(model.title) + end + + def description + attribute = model.try(:short_description) || model.try(:body) || model.description + text = translated_attribute(attribute) + + decidim_sanitize(html_truncate(text, length: 240), strip_tags: true) + end + + private + + def metadata_cell + "decidim/surveys/survey_card_metadata" + end + end + end +end diff --git a/decidim-surveys/app/cells/decidim/surveys/survey_s_cell.rb b/decidim-surveys/app/cells/decidim/surveys/survey_s_cell.rb new file mode 100644 index 0000000000000..297f36df27067 --- /dev/null +++ b/decidim-surveys/app/cells/decidim/surveys/survey_s_cell.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "cell/partial" + +module Decidim + module Surveys + # This cell renders the Search (:s) survey card + # for a given instance of a Survey + class SurveySCell < Decidim::CardSCell + private + + def title + present(model).title(html_escape: true) + end + + def metadata_cell + "decidim/surveys/survey_card_metadata" + end + end + end +end diff --git a/decidim-surveys/app/commands/decidim/surveys/admin/publish_survey.rb b/decidim-surveys/app/commands/decidim/surveys/admin/publish_survey.rb new file mode 100644 index 0000000000000..d67d0e74efb44 --- /dev/null +++ b/decidim-surveys/app/commands/decidim/surveys/admin/publish_survey.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + module Admin + # A command with all the business logic that publishes an + # existing survey. + class PublishSurvey < Decidim::Command + # Public: Initializes the command. + # + # survey - Decidim::Surveys::Survey + # current_user - the user performing the action + def initialize(survey, current_user) + @survey = survey + @current_user = current_user + @questionnaire = survey.questionnaire + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid. + # - :invalid if the form was not valid and we could not proceed. + # + # Returns nothing. + def call + return broadcast(:invalid) if survey.published? + + transaction do + publish_survey + delete_answers if @survey.clean_after_publish? + end + + broadcast(:ok, survey) + end + + private + + attr_reader :survey, :current_user + + def publish_survey + @survey = Decidim.traceability.perform_action!( + :publish, + survey, + current_user, + visibility: "all" + ) do + survey.publish! + survey + end + end + + def delete_answers + @questionnaire.answers.destroy_all + end + end + end + end +end diff --git a/decidim-surveys/app/commands/decidim/surveys/admin/unpublish_survey.rb b/decidim-surveys/app/commands/decidim/surveys/admin/unpublish_survey.rb new file mode 100644 index 0000000000000..7c75cd9401011 --- /dev/null +++ b/decidim-surveys/app/commands/decidim/surveys/admin/unpublish_survey.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + module Admin + # A command with all the business logic that unpublishes an + # existing survey. + class UnpublishSurvey < Decidim::Command + # Public: Initializes the command. + # + # survey - Decidim::Surveys::Survey + # current_user - the user performing the action + def initialize(survey, current_user) + @survey = survey + @current_user = current_user + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid. + # - :invalid if the form was not valid and we could not proceed. + # + # Returns nothing. + def call + return broadcast(:invalid) unless survey.published? + + @survey = Decidim.traceability.perform_action!( + :unpublish, + survey, + current_user + ) do + survey.unpublish! + survey + end + broadcast(:ok, survey) + end + + private + + attr_reader :survey, :current_user + end + end + end +end diff --git a/decidim-surveys/app/commands/decidim/surveys/admin/update_survey.rb b/decidim-surveys/app/commands/decidim/surveys/admin/update_survey.rb new file mode 100644 index 0000000000000..87f2a4330556c --- /dev/null +++ b/decidim-surveys/app/commands/decidim/surveys/admin/update_survey.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + module Admin + # This command is executed when the user changes a Survey Questionnaire from the admin + # panel. + class UpdateSurvey < Decidim::Forms::Admin::UpdateQuestionnaire + # Initializes a UpdateSurvey Command. + # + # form - The form from which to get the data. + # survey questionnaire - The current instance of the questionnaire to be updated. + def initialize(form, survey, user) + @form = form + @survey = survey + @questionnaire = survey.questionnaire + @user = user + end + + # Updates the survey questionnaire if valid. + # + # Broadcasts :ok if successful, :invalid otherwise. + def call + return broadcast(:invalid) if @form.invalid? + + Decidim.traceability.perform_action!("update", @survey, @user) do + transaction do + update_survey_attributes + update_questionnaire_attributes + end + rescue ActiveRecord::RecordInvalid + broadcast(:invalid) + end + + broadcast(:ok) + end + + private + + def update_survey_attributes + @survey.update!( + allow_answers: @form.allow_answers, + allow_unregistered: @form.allow_unregistered, + starts_at: @form.starts_at, + ends_at: @form.ends_at, + clean_after_publish: @form.clean_after_publish, + announcement: @form.announcement + ) + end + + def update_questionnaire_attributes + @questionnaire.update!( + title: @form.title, + description: @form.description, + tos: @form.tos + ) + end + end + end + end +end diff --git a/decidim-surveys/app/commands/decidim/surveys/create_survey.rb b/decidim-surveys/app/commands/decidim/surveys/create_survey.rb index 21cd9c2c837ce..4d0fef45f6a5a 100644 --- a/decidim-surveys/app/commands/decidim/surveys/create_survey.rb +++ b/decidim-surveys/app/commands/decidim/surveys/create_survey.rb @@ -12,7 +12,7 @@ def initialize(component) def call @survey = Survey.new(component: @component, questionnaire: Decidim::Forms::Questionnaire.new) - @survey.save ? broadcast(:ok) : broadcast(:invalid) + @survey.save ? broadcast(:ok, @survey) : broadcast(:invalid) end end end diff --git a/decidim-surveys/app/controllers/concerns/decidim/surveys/admin/filterable.rb b/decidim-surveys/app/controllers/concerns/decidim/surveys/admin/filterable.rb new file mode 100644 index 0000000000000..3f626e953d4fc --- /dev/null +++ b/decidim-surveys/app/controllers/concerns/decidim/surveys/admin/filterable.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module Surveys + module Admin + module Filterable + extend ActiveSupport::Concern + + included do + include Decidim::Admin::Filterable + + private + + def base_query + collection + end + end + end + end + end +end diff --git a/decidim-surveys/app/controllers/decidim/surveys/admin/answers_controller.rb b/decidim-surveys/app/controllers/decidim/surveys/admin/answers_controller.rb new file mode 100644 index 0000000000000..fff0358f4bba2 --- /dev/null +++ b/decidim-surveys/app/controllers/decidim/surveys/admin/answers_controller.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + module Admin + # This controller allows the user to update a Page. + class AnswersController < Admin::ApplicationController + include Decidim::Forms::Admin::Concerns::HasQuestionnaireAnswers + + def index + enforce_permission_to :index, :questionnaire_answers + + @query = paginate(collection) + @participants = participants(@query) + @total = participants_query.count_participants + + render template: "decidim/surveys/admin/answers/index" + end + + def show + enforce_permission_to :show, :questionnaire_answers + + @participant = participant(participants_query.participant(params[:id])) + + render template: "decidim/surveys/admin/answers/show" + end + + def questionnaire_for + @questionnaire_for ||= Decidim::Surveys::Survey.where(component: current_component).find_by(id: params[:survey_id]) + end + + def questionnaire_export_response_url(id) + Decidim::EngineRouter.admin_proxy(questionnaire_for.component).export_response_survey_answer_path(questionnaire_for, id:) + end + + def questionnaire_url + Decidim::EngineRouter.admin_proxy(questionnaire_for.component).edit_questions_survey_path(questionnaire_for) + end + + # Specify where to redirect after exporting a user response + def questionnaire_participant_answers_url(id) + Decidim::EngineRouter.admin_proxy(questionnaire_for.component).survey_answer_path(questionnaire_for, id:) + end + + def questionnaire_participants_url + Decidim::EngineRouter.admin_proxy(questionnaire_for.component).survey_answers_path(questionnaire_for) + end + + private + + def questionnaire + @questionnaire ||= Decidim::Forms::Questionnaire.find_by(questionnaire_for:) + end + + def participants_query + Decidim::Forms::QuestionnaireParticipants.new(questionnaire) + end + + def collection + @collection ||= participants_query.participants + end + + def participant(answer) + Decidim::Forms::Admin::QuestionnaireParticipantPresenter.new(participant: answer) + end + + def participants(query) + query.map { |answer| participant(answer) } + end + end + end + end +end diff --git a/decidim-surveys/app/controllers/decidim/surveys/admin/surveys_controller.rb b/decidim-surveys/app/controllers/decidim/surveys/admin/surveys_controller.rb index 586fcb33020aa..12a912f4c83e9 100644 --- a/decidim-surveys/app/controllers/decidim/surveys/admin/surveys_controller.rb +++ b/decidim-surveys/app/controllers/decidim/surveys/admin/surveys_controller.rb @@ -6,40 +6,128 @@ module Admin # This controller allows the user to update a Page. class SurveysController < Admin::ApplicationController include Decidim::Forms::Admin::Concerns::HasQuestionnaire - include Decidim::Forms::Admin::Concerns::HasQuestionnaireAnswers + include Decidim::Forms::Admin::Concerns::HasQuestionnaireAnswersUrlHelper + include Decidim::Surveys::Admin::Filterable + + helper_method :surveys + + def index; end + + def create + enforce_permission_to(:create, :questionnaire) + Decidim::Surveys::CreateSurvey.call(current_component) do + on(:ok) do |survey| + flash[:notice] = I18n.t("create.success", scope: "decidim.surveys.admin.surveys") + redirect_to edit_survey_path(survey) + end + + on(:invalid) do + flash.now[:alert] = I18n.t("create.invalid", scope: "decidim.surveys.admin.surveys") + render action: "index" + end + end + end def edit enforce_permission_to(:update, :questionnaire, questionnaire:) + @form = form(Admin::SurveyForm).from_model(survey) + end + + def update + enforce_permission_to(:update, :questionnaire, questionnaire:) + @form = form(Admin::SurveyForm).from_params(params) + + Admin::UpdateSurvey.call(@form, survey, current_user) do + on(:ok) do + flash[:notice] = I18n.t("update.success", scope: "decidim.surveys.admin.surveys") + redirect_to(surveys_path) && return + end + + on(:invalid) do + flash.now[:alert] = I18n.t("update.invalid", scope: "decidim.surveys.admin.surveys") + render action: "edit" + end + end + end + + def publish + enforce_permission_to(:update, :questionnaire, questionnaire:) + Decidim::Surveys::Admin::PublishSurvey.call(survey, current_user) do + on(:ok) do + flash[:notice] = I18n.t("publish.success", scope: "decidim.surveys.admin.surveys") + redirect_to surveys_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("publish.invalid", scope: "decidim.surveys.admin.surveys") + render action: "index" + end + end + end + + def unpublish + enforce_permission_to(:update, :questionnaire, questionnaire:) + Decidim::Surveys::Admin::UnpublishSurvey.call(survey, current_user) do + on(:ok) do + flash[:notice] = I18n.t("unpublish.success", scope: "decidim.surveys.admin.surveys") + redirect_to surveys_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("unpublish.invalid", scope: "decidim.surveys.admin.surveys") + render action: "index" + end + end + end - @form = form(Decidim::Forms::Admin::QuestionnaireForm).from_model(questionnaire) + def destroy + enforce_permission_to(:destroy, :questionnaire, questionnaire:) + Decidim::Commands::DestroyResource.call(survey, current_user) do + on(:ok) do + flash[:notice] = I18n.t("destroy.success", scope: "decidim.surveys.admin.surveys") + + redirect_to surveys_path + end + end + end + + def edit_questions_template + "decidim/surveys/admin/surveys/edit_questions" end def questionnaire_for survey end + def after_update_url + surveys_path + end + + def questionnaire_participants_url + Decidim::EngineRouter.admin_proxy(survey.component).survey_answers_path(survey) + end + # Specify the public url from which the survey can be viewed and answered def public_url Decidim::EngineRouter.main_proxy(current_component).survey_path(survey) end - # Specify where to redirect after exporting a user response - def questionnaire_participant_answers_url(session_token) - Decidim::EngineRouter.admin_proxy(survey.component).show_survey_path(session_token:) - end - def edit_questionnaire_title t(:title, scope: "decidim.forms.admin.questionnaires.form", questionnaire_for: translated_attribute(current_component.name)) end private - def i18n_flashes_scope - "decidim.surveys.admin.surveys" + def surveys + @surveys ||= filtered_collection end def survey - @survey ||= Survey.find_by(component: current_component) + @survey ||= collection.find(params[:id]) + end + + def collection + @collection ||= Decidim::Surveys::Survey.where(component: current_component) end end end diff --git a/decidim-surveys/app/controllers/decidim/surveys/surveys_controller.rb b/decidim-surveys/app/controllers/decidim/surveys/surveys_controller.rb index b684da53fd301..14f7961e0d69e 100644 --- a/decidim-surveys/app/controllers/decidim/surveys/surveys_controller.rb +++ b/decidim-surveys/app/controllers/decidim/surveys/surveys_controller.rb @@ -7,12 +7,14 @@ class SurveysController < Decidim::Surveys::ApplicationController include Decidim::Forms::Concerns::HasQuestionnaire include Decidim::ComponentPathHelper include Decidim::Surveys::SurveyHelper + include FilterResource + include Paginable - helper_method :authorizations + helper_method :authorizations, :surveys - delegate :allow_unregistered?, to: :current_settings + before_action :check_permissions, except: [:index] - before_action :check_permissions + def index; end def check_permissions render :no_permission unless action_authorized_to(:answer, resource: survey).ok? @@ -25,7 +27,11 @@ def questionnaire_for protected def allow_answers? - !current_component.published? || (current_settings.allow_answers? && survey.open?) + !current_component.published? || @survey.open? + end + + def allow_unregistered? + @survey.allow_unregistered end def form_path @@ -38,8 +44,22 @@ def i18n_flashes_scope "decidim.surveys.surveys" end + def surveys + paginate(search.result).published + end + def survey - @survey ||= Survey.find_by(component: current_component) + @survey ||= search_collection.find_by(id: params[:id]) + end + + def search_collection + @search_collection ||= Decidim::Surveys::Survey.where(component: current_component) + end + + def default_filter_params + { + with_any_state: %w(open) + } end end end diff --git a/decidim-surveys/app/forms/decidim/surveys/admin/survey_form.rb b/decidim-surveys/app/forms/decidim/surveys/admin/survey_form.rb new file mode 100644 index 0000000000000..f65d7cc9f20ed --- /dev/null +++ b/decidim-surveys/app/forms/decidim/surveys/admin/survey_form.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + module Admin + class SurveyForm < Decidim::Forms::Admin::QuestionnaireForm + translatable_attribute :announcement, String + + attribute :allow_answers, Boolean + attribute :allow_unregistered, Boolean + attribute :clean_after_publish, Boolean + attribute :starts_at, Decidim::Attributes::TimeWithZone + attribute :ends_at, Decidim::Attributes::TimeWithZone + end + end + end +end diff --git a/decidim-surveys/app/helpers/decidim/surveys/application_helper.rb b/decidim-surveys/app/helpers/decidim/surveys/application_helper.rb new file mode 100644 index 0000000000000..d6b4da495fc4d --- /dev/null +++ b/decidim-surveys/app/helpers/decidim/surveys/application_helper.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + # Custom helpers, scoped to the surveys engine. + # + module ApplicationHelper + include PaginateHelper + include Decidim::SanitizeHelper + include Decidim::CheckBoxesTreeHelper + include Decidim::RichTextEditorHelper + + # Returns a TreeNode to be used in the list filters to filter surveys by + # its state. + def filter_surveys_date_values + [ + ["all", t("all", scope: "decidim.surveys.surveys.filters")], + ["open", { checked: true }, t("open", scope: "decidim.surveys.surveys.filters.state_values")], + ["closed", t("closed", scope: "decidim.surveys.surveys.filters.state_values")] + ] + end + + def filter_sections + @filter_sections ||= [{ + method: :with_any_state, + collection: filter_surveys_date_values, + label: t("decidim.proposals.proposals.filters.state"), + id: "state", + type: :radio_buttons + }] + end + end + end +end diff --git a/decidim-surveys/app/helpers/decidim/surveys/survey_helper.rb b/decidim-surveys/app/helpers/decidim/surveys/survey_helper.rb index 3dc86fe251b25..b72fe75497177 100644 --- a/decidim-surveys/app/helpers/decidim/surveys/survey_helper.rb +++ b/decidim-surveys/app/helpers/decidim/surveys/survey_helper.rb @@ -26,6 +26,10 @@ def authorize_action_path(handler_name) def authorizations @authorizations ||= action_authorized_to(:answer, resource: questionnaire_for) end + + def filter_date_values + flat_filter_values(:all, :open, :closed, scope: "decidim.surveys.surveys.filters.date_values") + end end end end diff --git a/decidim-surveys/app/models/decidim/surveys/survey.rb b/decidim-surveys/app/models/decidim/surveys/survey.rb index 18857b56c69e3..9cc3df4d7b7de 100644 --- a/decidim-surveys/app/models/decidim/surveys/survey.rb +++ b/decidim-surveys/app/models/decidim/surveys/survey.rb @@ -7,33 +7,84 @@ class Survey < Surveys::ApplicationRecord include Decidim::Resourceable include Decidim::Forms::HasQuestionnaire include Decidim::HasComponent + include Decidim::FilterableResource + include Decidim::Publicable component_manifest_name "surveys" delegate :title, to: :questionnaire + delegate :description, to: :questionnaire + delegate :tos, to: :questionnaire validates :questionnaire, presence: true - def clean_after_publish? - component.settings.clean_after_publish? + scope :open, lambda { + where(allow_answers: true) + .where(starts_at: nil, ends_at: nil).or( + where("starts_at <= ? AND (ends_at IS NULL OR ends_at > ?)", Time.current, Time.current) + ).or( + where("ends_at > ? AND (starts_at IS NULL OR starts_at <= ?)", Time.current, Time.current) + ) + } + scope :closed, lambda { + where(allow_answers: false).or( + where("starts_at > ?", Time.current).or( + where(ends_at: ...Time.current) + ) + ) + } + scope :published, -> { where.not(published_at: nil) } + + scope_search_multi :with_any_state, [:open, :closed] + + def open? + return false if allow_answers.blank? + return true if time_indefinite? + return true if started_but_no_end? + return true if no_start_but_ends_later? + + return within_time_range? if time_range_defined? + + false end - def starts_at - component.settings.starts_at + def self.ransackable_scopes(_auth_object = nil) + [:with_any_state] end - def ends_at - component.settings.ends_at + def self.ransackable_attributes(_auth_object = nil) + %w(ends_at starts_at allow_answers) end - def open? - return true if starts_at.blank? && ends_at.blank? - return true if ends_at.blank? && starts_at.past? - return true if starts_at.blank? && ends_at.future? + def self.log_presenter_class_for(_log) + Decidim::Surveys::AdminLog::SurveyPresenter + end - return Time.zone.now.between?(starts_at, ends_at) if starts_at.present? && ends_at.present? + # Public: Overrides the `allow_resource_permissions?` Resourceable concern method. + def allow_resource_permissions? + true + end - false + private + + def time_indefinite? + starts_at.blank? && ends_at.blank? + end + + def started_but_no_end? + ends_at.blank? && starts_at.past? + end + + def no_start_but_ends_later? + starts_at.blank? && ends_at.future? + end + + def time_range_defined? + starts_at.present? && ends_at.present? + end + + def within_time_range? + Time.zone.now.between?(starts_at, ends_at) end end end diff --git a/decidim-surveys/app/permissions/decidim/surveys/admin/permissions.rb b/decidim-surveys/app/permissions/decidim/surveys/admin/permissions.rb index 08a02cb27dacf..623ce8b40be28 100644 --- a/decidim-surveys/app/permissions/decidim/surveys/admin/permissions.rb +++ b/decidim-surveys/app/permissions/decidim/surveys/admin/permissions.rb @@ -12,7 +12,7 @@ def permissions case permission_action.subject when :questionnaire case permission_action.action - when :export_answers, :update + when :export_answers, :update, :create, :destroy permission_action.allow! end when :questionnaire_answers diff --git a/decidim-surveys/app/permissions/decidim/surveys/permissions.rb b/decidim-surveys/app/permissions/decidim/surveys/permissions.rb index 73c56597a49c8..d9e80454e34fc 100644 --- a/decidim-surveys/app/permissions/decidim/surveys/permissions.rb +++ b/decidim-surveys/app/permissions/decidim/surveys/permissions.rb @@ -4,8 +4,6 @@ module Decidim module Surveys class Permissions < Decidim::DefaultPermissions def permissions - return permission_action unless user || context[:current_settings].allow_unregistered? - return Decidim::Surveys::Admin::Permissions.new(user, permission_action, context).permissions if permission_action.scope == :admin return permission_action if permission_action.scope != :public diff --git a/decidim-surveys/app/presenters/decidim/surveys/admin_log/survey_presenter.rb b/decidim-surveys/app/presenters/decidim/surveys/admin_log/survey_presenter.rb new file mode 100644 index 0000000000000..c12b1414524d0 --- /dev/null +++ b/decidim-surveys/app/presenters/decidim/surveys/admin_log/survey_presenter.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Decidim + module Surveys + module AdminLog + # This class holds the logic to present a `Decidim::Surveys::Survey` + # for the `AdminLog` log. + # + # Usage should be automatic and you should not need to call this class + # directly, but here is an example: + # + # action_log = Decidim::ActionLog.last + # view_helpers # => this comes from the views + # SurveyPresenter.new(action_log, view_helpers).present + class SurveyPresenter < Decidim::Log::BasePresenter + private + + def action_string + case action + when "delete", "create", "update", "publish" + "decidim.surveys.admin_log.survey.#{action}" + else + super + end + end + end + end + end +end diff --git a/decidim-surveys/app/views/decidim/surveys/admin/answers/index.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/answers/index.html.erb new file mode 100644 index 0000000000000..5ea996aac0d8f --- /dev/null +++ b/decidim-surveys/app/views/decidim/surveys/admin/answers/index.html.erb @@ -0,0 +1,49 @@ +
+
+

+ <%= t ".title", total: @total %> + <%= link_to t("actions.back", scope: "decidim.forms.admin.questionnaires"), questionnaire_url, class: "button button__sm button__secondary new" %> +

+
+
+ + + + + + + + + + + + + + <% @participants.each_with_index do |participant, idx| %> + + + + <% end %> + + + + + + + <% end %> + +
#<%= first_table_th(@participants.first) %><%= t("user_status", scope: "decidim.forms.user_answers_serializer") %><%= t("ip_hash", scope: "decidim.forms.user_answers_serializer") %><%= t("completion", scope: "decidim.forms.user_answers_serializer") %><%= t("created_at", scope: "decidim.forms.user_answers_serializer") %>
<%= idx + 1 + page_offset %> + <% if allowed_to? :show, :questionnaire_answers %> + <%= link_to first_table_td(participant), questionnaire_participant_answers_url(participant.session_token) %> + <% else %> + <%= first_table_td(participant) %><%= participant.status %><%= participant.ip_hash %><%= display_percentage(participant.completion) %><%= l participant.answered_at, format: :short %> + <% if allowed_to? :show, :questionnaire_answers %> + <%= icon_link_to "eye-line", questionnaire_participant_answers_url(participant.session_token), t("actions.show", scope: "decidim.forms.admin.questionnaires.answers"), class: "action-icon--eye", target: "_blank", data: { "external-link": false } %> + <% end %> + <% if allowed_to? :export_response, :questionnaire_answers %> + <%= icon_link_to "download-line", questionnaire_export_response_url(participant.session_token), t("actions.export", scope: "decidim.forms.admin.questionnaires.answers"), class: "action-icon--data-transfer-download" %> + <% end %> +
+
+
+<%= decidim_paginate @query %> diff --git a/decidim-surveys/app/views/decidim/surveys/admin/answers/show.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/answers/show.html.erb new file mode 100644 index 0000000000000..e08c09334fc84 --- /dev/null +++ b/decidim-surveys/app/views/decidim/surveys/admin/answers/show.html.erb @@ -0,0 +1,43 @@ +
+
+

+ <%= t ".title", number: current_idx %> + + <%= link_to t("actions.next", scope: "decidim.forms.admin.questionnaires.answers").html_safe, next_url, rel: "next", class: "button button__sm button__secondary next" unless last? %> + <%= link_to t("actions.previous", scope: "decidim.forms.admin.questionnaires.answers").html_safe, prev_url, rel: "prev", class: "button button__sm button__secondary prev" unless first? %> + <%= link_to t("actions.export", scope: "decidim.forms.admin.questionnaires.answers"), questionnaire_export_response_url(@participant.session_token), class: "button button__sm button__secondary export" %> + <%= link_to t("actions.back", scope: "decidim.forms.admin.questionnaires.answers"), questionnaire_participants_url, class: "button button__sm button__secondary back" %> +

+
+
+
+ + + + + + + + + + + + + + + + + + + + +
<%= t("session_token", scope: "decidim.forms.user_answers_serializer") %><%= t("user_status", scope: "decidim.forms.user_answers_serializer") %><%= t("ip_hash", scope: "decidim.forms.user_answers_serializer") %><%= t("completion", scope: "decidim.forms.user_answers_serializer") %><%= t("created_at", scope: "decidim.forms.user_answers_serializer") %>
<%= @participant.session_token %><%= @participant.status %><%= @participant.ip_hash %><%= display_percentage(@participant.completion) %><%= l @participant.answered_at, format: :short %>
+
+
+ <% @participant.answers.each do |answer| %> +
<%= answer.question %>
+
<%= answer.body %>
+ <% end %> +
+
+
diff --git a/decidim-surveys/app/views/decidim/surveys/admin/component/_actions.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/component/_actions.html.erb deleted file mode 100644 index ad5f67b925559..0000000000000 --- a/decidim-surveys/app/views/decidim/surveys/admin/component/_actions.html.erb +++ /dev/null @@ -1,75 +0,0 @@ -<% if view == :deleted %> - <% if allowed_to? :restore, :component, trashable_deleted_resource: component %> - <%= icon_link_to "refresh-line", url_for(action: :restore, id: component, controller: "components"), t("decidim.admin.actions.restore"), method: :patch, class: "action-icon--restore" %> - <% end %> -<% else %> - <% if component.manifest.admin_engine %> - <%= icon_link_to "pencil-line", manage_component_path(component), t("actions.manage", scope: "decidim.admin"), class: "action-icon--manage" %> - <% else %> - - <% end %> - - <% if component.manifest.admin_engine && allowed_to?(:share, :component, component: component) %> - <%= icon_link_to "share-line", component_share_tokens_path(component_id: component), t("actions.share", scope: "decidim.admin"), class: "action-icon--share" %> - <% else %> - - <% end %> - - <% if allowed_to? :update, :component, component: component %> - <%= icon_link_to "settings-4-line", url_for(action: :edit, id: component, controller: "components"), t("actions.configure", scope: "decidim.admin"), class: "action-icon--configure" %> - <% else %> - - <% end %> - - <% if allowed_to?(:update, :component, component: component) %> - <% if component.published? %> - <% if component.visible? %> - <%= icon_link_to "eye-close", url_for(action: :hide, id: component, controller: "components"), t("actions.menu_hidden", scope: "decidim.admin"), class: "action-icon--unpublish", method: :put %> - <% else %> - <%= icon_link_to "close-circle-line", url_for(action: :unpublish, id: component, controller: "components"), t("actions.unpublish", scope: "decidim.admin"), class: "action-icon--menu-hidden", method: :put %> - <% end %> - <% else %> - - <% end %> - <% end %> - - <% if allowed_to? :share, :component, component: component %> - <%= icon_link_to "share-line", url_for(action: :share, id: component, controller: "components"), t("actions.share", scope: "decidim.admin"), target: :blank, class: "action-icon--share" %> - <% else %> - - <% end %> - <% if allowed_to? :update, :component, component: component %> - <%= icon_link_to "settings-4-line", url_for(action: :edit, id: component, controller: "components"), t("actions.configure", scope: "decidim.admin"), class: "action-icon--configure" %> - <% else %> - - <% end %> - - <% if allowed_to?(:update, :component, component: component) %> - <% if component.published? %> - <%= icon_link_to "close-circle-line", url_for(action: :unpublish, id: component, controller: "components"), t("actions.unpublish", scope: "decidim.admin"), class: "action-icon--unpublish", method: :put %> - <% else %> - <%= icon_link_to "check-line", url_for(action: :publish, id: component, controller: "components"), t("actions.publish", scope: "decidim.admin"), class: "action-icon--publish", method: :put, data: { confirm: t(".answers_alert") } %> - <% end %> - <% else %> - - <% end %> - - <%= icon_link_to "eye-line", main_component_path(component), t("actions.preview", scope: "decidim.admin"), class: "action-icon--preview", target: :blank, data: { "external-link": false } %> - - <% if allowed_to? :update, :component, component: component %> - <% if component.manifest.actions.empty? %> - <%= icon "key-2-line", class: "action-icon action-icon--disabled" %> - <% else %> - <%= icon_link_to "key-2-line", url_for(action: :edit, component_id: component, controller: "component_permissions"), t("actions.permissions", scope: "decidim.admin"), class: "action-icon--permissions" %> - <% end %> - <% else %> - - <% end %> - - <% if allowed_to? :soft_delete, :component, trashable_deleted_resource: component %> - <% resources_count = component.primary_stat || 0 %> - <%= icon_link_to "delete-bin-line", url_for(action: :soft_delete, id: component, controller: "components"), t("actions.soft_delete", scope: "decidim.admin", resources_count:), method: :patch, class: "action-icon--delete", data: { confirm: (t("actions.confirm_delete_component", scope: "decidim.admin", resources_count:)) } %> - <% else %> - <%= icon "delete-bin-line", class: "action-icon action-icon--disabled", role: "img", aria_label: t("actions.soft_delete", scope: "decidim.admin") %> - <% end %> -<% end %> diff --git a/decidim-surveys/app/views/decidim/surveys/admin/surveys/_form.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/surveys/_form.html.erb new file mode 100644 index 0000000000000..6f5d05087126b --- /dev/null +++ b/decidim-surveys/app/views/decidim/surveys/admin/surveys/_form.html.erb @@ -0,0 +1,25 @@ +<%= render "decidim/forms/admin/questionnaires/form", form: %> +
+
+
+
+ <%= form.check_box :allow_answers, label: t("models.components.allow_answers", scope: "decidim.forms.admin"), aria: { label: :allow_answers } %> +
+
+ <%= form.check_box :allow_unregistered, label: t("models.components.allow_unregistered", scope: "decidim.forms.admin"), aria: { label: :allow_unregistered }, help_text: t("models.components.allow_unregistered_help", scope: "decidim.forms.admin") %> +
+
+ <%= form.datetime_field :starts_at, label: t("models.components.starts_at", scope: "decidim.forms.admin"), aria: { label: :starts_at }, help_text: t("models.components.starts_at_help", scope: "decidim.forms.admin") %> +
+
+ <%= form.datetime_field :ends_at, label: t("models.components.ends_at", scope: "decidim.forms.admin"), aria: { label: :ends_at }, help_text: t("models.components.ends_at_help", scope: "decidim.forms.admin") %> +
+
+ <%= form.check_box :clean_after_publish, label: t("models.components.clean_after_publish", scope: "decidim.forms.admin"), aria: { label: :clean_after_publish } %> +
+
+ <%= form.translated :editor, :announcement, toolbar: :content, lines: 10, label: t("models.components.announcement", scope: "decidim.forms.admin"), aria: { label: :announcement } %> +
+
+
+
diff --git a/decidim-surveys/app/views/decidim/surveys/admin/surveys/edit.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/surveys/edit.html.erb index 8216aec6ee346..e1183df4ae3da 100644 --- a/decidim-surveys/app/views/decidim/surveys/admin/surveys/edit.html.erb +++ b/decidim-surveys/app/views/decidim/surveys/admin/surveys/edit.html.erb @@ -1,33 +1,17 @@ -<% add_decidim_page_title(t("decidim.forms.admin.questionnaires.edit.title")) %> +<% add_decidim_page_title(t(".title")) %> <% if templates_defined? && choose_template? %> <%= render partial: "decidim/templates/admin/questionnaire_templates/choose", locals: { target: questionnaire, form_title: t("decidim.forms.admin.questionnaires.edit.title") } %> <% else %> -

- <%= edit_questionnaire_title %> - - <% if allowed_to? :preview, :questionnaire %> - <%= link_to t("preview", scope: "decidim.forms.admin.questionnaires.form"), public_url, class: "button button__sm button__secondary", target: :_blank, data: { "external-link": false } %> - <% end %> - <% if questionnaire.answers.any? %> - <%= export_dropdown(current_component, questionnaire.id) if allowed_to? :export_answers, :questionnaire %> - <% if allowed_to? :show, :questionnaire_answers %> - <%= link_to t("actions.show", scope: "decidim.forms.admin.questionnaires"), questionnaire_participants_url, class: "button button__sm button__secondary new whitespace-nowrap" %> - <% end %> - <% else %> - - <% end %> - <%= render partial: "decidim/admin/components/resource_action" %> - + <%= t(".title") %>

-
- <%= decidim_form_for(@form, url: update_url, method: :put, html: { class: "form-defaults form edit_questionnaire" }) do |form| %> - <%= render partial: "decidim/forms/admin/questionnaires/form", object: form %> + <%= decidim_form_for(@form, url: survey_path(@survey), html: { class: "form-defaults form edit_questionnaire" }) do |form| %> + <%= render partial: "form", object: form %>
<%= form.submit t("save", scope: "decidim.forms.admin.questionnaires.edit"), class: "button button__sm button__secondary" %> @@ -36,5 +20,4 @@ <% end %>
- <% end %> diff --git a/decidim-surveys/app/views/decidim/surveys/admin/surveys/edit_questions.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/surveys/edit_questions.html.erb new file mode 100644 index 0000000000000..ed05ba65cb26a --- /dev/null +++ b/decidim-surveys/app/views/decidim/surveys/admin/surveys/edit_questions.html.erb @@ -0,0 +1,44 @@ +<% add_decidim_page_title(t("decidim.forms.admin.questionnaires.edit_questions.title")) %> + +<% if templates_defined? && choose_template? %> + <%= render partial: "decidim/templates/admin/questionnaire_templates/choose", locals: { target: questionnaire, form_title: t("decidim.forms.admin.questionnaires.edit.title") } %> +<% else %> + +
+
+

+ <%= t("decidim.forms.admin.questionnaires.edit_questions.title") %> + <% if questionnaire.questions_editable? %> + + + + <% end %> + <% if allowed_to? :preview, :questionnaire %> + <%= link_to t("preview", scope: "decidim.forms.admin.questionnaires.form"), public_url, class: "button button__sm button__transparent-secondary", target: :_blank, data: { "external-link": false } %> + <% end %> + <% if questionnaire.answers.any? %> + <%= export_dropdown(current_component, questionnaire.id) if allowed_to? :export_answers, :questionnaire %> + <% if allowed_to? :show, :questionnaire_answers %> + <%= link_to t("actions.show", scope: "decidim.forms.admin.questionnaires"), questionnaire_participants_url, class: "button button__sm button__secondary new whitespace-nowrap" %> + <% end %> + <% else %> + + <% end %> +

+
+
+ +
+
+ <%= decidim_form_for(@form, url: update_questions_survey_path(@survey), method: :patch, html: { class: "form-defaults form edit_questions_survey" }) do |form| %> + <%= render partial: "decidim/forms/admin/questionnaires/questions_form", object: form %> +
+
+ <%= form.submit t("save", scope: "decidim.forms.admin.questionnaires.edit"), class: "button button__sm button__secondary" %> +
+
+ <% end %> +
+
+ +<% end %> diff --git a/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb new file mode 100644 index 0000000000000..77e7b2c402a39 --- /dev/null +++ b/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb @@ -0,0 +1,51 @@ +<% add_decidim_page_title(t(".title")) %> +
+
+

+ <%= t(".title") %> + <%= link_to t("actions.new_survey", scope: "decidim.surveys"), surveys_path, method: :post, class: "button button__sm button__secondary" %> + <%= render partial: "decidim/admin/components/resource_action" %> +

+
+
+ + + + + + + + + + + + <% surveys.each do |survey| %> + + + + + + + + <% end %> + +
<%= t("models.survey.fields.title", scope: "decidim.surveys") %><%= t("models.survey.fields.questions", scope: "decidim.surveys") %><%= t("models.survey.fields.answers", scope: "decidim.surveys") %><%= t("models.survey.fields.status", scope: "decidim.surveys") %><%= t("actions.title", scope: "decidim.surveys") %>
<%= decidim_sanitize_translated(survey.title) %><%= survey.questionnaire.questions.size %><%= survey.questionnaire.answers.size %><%= survey.open? ? t("models.survey.status.open", scope: "decidim.surveys") : t("models.survey.status.closed", scope: "decidim.surveys") %> + <%= icon_link_to "pencil-line", edit_survey_path(survey), t("actions.edit", scope: "decidim.surveys"), class: "action-icon--edit" %> + <%= icon_link_to "survey-line", edit_questions_survey_path(survey), t("actions.manage_questions", scope: "decidim.surveys"), class: "action-icon--copy" %> + <% if allowed_to? :preview, :questionnaire %> + <%= icon_link_to "eye-line", resource_locator(survey).path, t("actions.preview", scope: "decidim.surveys"), class: "action-icon--preview", target: :blank, data: { "external-link": false } %> + <% end %> + <% if allowed_to?(:update, :questionnaire) %> + <% if survey.published? %> + <%= icon_link_to "close-circle-line", unpublish_survey_path(survey), t("actions.unpublish", scope: "decidim.admin"), method: :put, class: "action-icon--unpublish" %> + <% elsif survey.clean_after_publish? %> + <%= icon_link_to "check-line", publish_survey_path(survey), t("actions.publish", scope: "decidim.admin"), method: :put, class: "action-icon--publish", data: { confirm: t("actions.answers_alert", scope: "decidim.surveys", answers_count: survey.questionnaire.answers.size) } %> + <% else %> + <%= icon_link_to "check-line", publish_survey_path(survey), t("actions.publish", scope: "decidim.admin"), method: :put, class: "action-icon--publish" %> + <% end %> + <% end %> + <%= resource_permissions_link(survey) %> + <%= icon_link_to "delete-bin-line", survey_path(survey), t("actions.destroy", scope: "decidim.surveys"), method: :delete, class: "action-icon--remove", data: { confirm: t("actions.confirm_destroy", scope: "decidim.surveys") } %> +
+
+
diff --git a/decidim-surveys/app/views/decidim/surveys/surveys/_surveys.html.erb b/decidim-surveys/app/views/decidim/surveys/surveys/_surveys.html.erb new file mode 100644 index 0000000000000..3053a084b2632 --- /dev/null +++ b/decidim-surveys/app/views/decidim/surveys/surveys/_surveys.html.erb @@ -0,0 +1,13 @@ +<% if surveys.length.zero? %> + <%= cell("decidim/announcement", t("decidim.surveys.surveys.no_surveys_warning"), callout_class: "warning" ) %> +<% else %> +

<%= t("surveys_count", scope: "decidim.surveys.surveys.count", count: surveys.count) %>

+ +
+ <% surveys.each do |survey| %> + <%= card_for survey %> + <% end %> +
+ + <%= decidim_paginate surveys %> +<% end %> diff --git a/decidim-surveys/app/views/decidim/surveys/surveys/index.html.erb b/decidim-surveys/app/views/decidim/surveys/surveys/index.html.erb new file mode 100644 index 0000000000000..b354f59c729e5 --- /dev/null +++ b/decidim-surveys/app/views/decidim/surveys/surveys/index.html.erb @@ -0,0 +1,24 @@ +<% add_decidim_meta_tags( + description: translated_attribute(current_component.participatory_space.try(:description)), + title: translated_attribute(current_component.name), + url: surveys_url, + resource: current_component +) %> + +<% add_decidim_page_title(t("decidim.surveys.directory.surveys.index.surveys")) %> + +<% content_for :aside do %> + <% title = current_component.present? ? translated_attribute(current_component.name) : t("decidim.components.surveys.name") %> +

<%= title %>

+ + <%= render layout: "decidim/shared/filters", locals: { filter_sections:, skip_to_id: "surveys" } do %> + <% end %> +<% end %> + +<%= render layout: "layouts/decidim/shared/layout_two_col" do %> + <%= render partial: "decidim/shared/component_announcement" %> + +
+ <%= render partial: "surveys" %> +
+<% end %> diff --git a/decidim-surveys/app/views/decidim/surveys/surveys/index.js.erb b/decidim-surveys/app/views/decidim/surveys/surveys/index.js.erb new file mode 100644 index 0000000000000..e7fe71d624689 --- /dev/null +++ b/decidim-surveys/app/views/decidim/surveys/surveys/index.js.erb @@ -0,0 +1,4 @@ +var $surveys = $('#surveys'); +$surveys.html('<%= j(render partial: "surveys").strip.html_safe %>'); + +document.dispatchEvent(new CustomEvent("ajax:loaded", { detail: $surveys[0] })); diff --git a/decidim-surveys/config/locales/en.yml b/decidim-surveys/config/locales/en.yml index b60be3e0ca8a6..d198ed698d9d2 100644 --- a/decidim-surveys/config/locales/en.yml +++ b/decidim-surveys/config/locales/en.yml @@ -21,21 +21,14 @@ en: surveys: actions: answer: Answer - name: Survey + name: Surveys settings: + announcement: Announcement global: announcement: Announcement - clean_after_publish: Delete answers when publishing the survey - ends_at: Answers accepted until - ends_at_help: Leave blank for no specific date scope_id: Scope scopes_enabled: Scopes enabled - starts_at: Answers accepted from - starts_at_help: Leave blank for no specific date step: - allow_answers: Allow answers - allow_unregistered: Allow unregistered users to answer the survey - allow_unregistered_help: If active, no login will be required in order to answer the survey. This may lead to poor or unreliable data and it will be more vulnerable to automated attacks. Use with caution! Mind that a participant could answer the same survey multiple times, by using different browsers or the "private browsing" feature of her web browser. announcement: Announcement events: surveys: @@ -57,18 +50,64 @@ en: statistics: answers_count: Answers surveys: + actions: + answers_alert: Delete answers at publish is active for this survey. There are currently %{answers_count} answers that will be destroyed if you continue. + confirm_destroy: Are you sure you want to delete this? + destroy: Destroy + edit: Edit + manage_questions: Manage questions + new_survey: New survey + preview: Preview + title: Actions admin: - component: - actions: - answers_alert: If you publish the component, all results will be removed. + answers: + index: + title: "%{total} total responses" + show: + title: 'Answer #%{number}' exports: survey_user_answers: Survey participant answers surveys: + create: + invalid: There was a problem creating the survey. + success: Survey successfully created. + destroy: + success: Survey successfully deleted. + edit: + title: Edit survey + index: + title: Surveys + publish: + invalid: There was a problem publishing this survey. + success: Survey successfully published. + unpublish: + invalid: There was a problem unpublishing this survey. + success: Survey successfully unpublished. update: invalid: There was a problem saving the survey. success: Survey successfully saved. + admin_log: + survey: + create: "%{user_name} created the %{resource_name} survey on the %{space_name} space as an survey" + delete: "%{user_name} deleted the %{resource_name} survey on the %{space_name} space as an survey" + publish: "%{user_name} published the %{resource_name} survey on the %{space_name} space" + update: "%{user_name} updated the %{resource_name} survey on the %{space_name} space" + directory: + surveys: + index: + surveys: Surveys last_activity: new_survey: 'New survey:' + models: + survey: + fields: + answers: Answers + questions: Questions + status: Status + title: Title + status: + closed: Closed + open: Open survey_confirmation_mailer: confirmation: body: You have successfully answered the %{questionnaire_title} survey within %{participatory_space} @@ -79,3 +118,17 @@ en: invalid: There was a problem answering the survey. spam_detected: There was a problem answering the form. Maybe you have been too quick, can you try again? success: Survey successfully answered. + count: + surveys_count: + one: "%{count} survey" + other: "%{count} surveys" + filters: + all: All + state_values: + closed: Closed + open: Open + no_surveys_warning: No surveys match your search criteria or there is not any survey open. + show: + closed: Closed + open: Open + questions: questions diff --git a/decidim-surveys/db/migrate/20240925124312_add_settings_to_decidim_surveys_surveys.rb b/decidim-surveys/db/migrate/20240925124312_add_settings_to_decidim_surveys_surveys.rb new file mode 100644 index 0000000000000..53b03785b6138 --- /dev/null +++ b/decidim-surveys/db/migrate/20240925124312_add_settings_to_decidim_surveys_surveys.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class AddSettingsToDecidimSurveysSurveys < ActiveRecord::Migration[7.0] + class Survey < ApplicationRecord + include Decidim::HasComponent + + self.table_name = :decidim_surveys_surveys + end + + def change + reversible do |dir| + dir.up do + add_column :decidim_surveys_surveys, :starts_at, :datetime + add_column :decidim_surveys_surveys, :ends_at, :datetime + add_column :decidim_surveys_surveys, :announcement, :jsonb + add_column :decidim_surveys_surveys, :allow_answers, :boolean + add_column :decidim_surveys_surveys, :allow_unregistered, :boolean + add_column :decidim_surveys_surveys, :clean_after_publish, :boolean + add_column :decidim_surveys_surveys, :published_at, :datetime + add_index :decidim_surveys_surveys, :published_at + + Survey.where(published_at: nil).find_each do |survey| + published_at = survey.component.published_at + next if published_at.nil? + + survey.update(published_at:) + end + end + + dir.down do + remove_index :decidim_surveys_surveys, :published_at + remove_column :decidim_surveys_surveys, :starts_at + remove_column :decidim_surveys_surveys, :ends_at + remove_column :decidim_surveys_surveys, :announcement + remove_column :decidim_surveys_surveys, :allow_answers + remove_column :decidim_surveys_surveys, :allow_unregistered + remove_column :decidim_surveys_surveys, :clean_after_publish + remove_column :decidim_surveys_surveys, :published_at + end + end + end +end diff --git a/decidim-surveys/lib/decidim/surveys/admin_engine.rb b/decidim-surveys/lib/decidim/surveys/admin_engine.rb index faf39fde3a49a..622f6b792ce70 100644 --- a/decidim-surveys/lib/decidim/surveys/admin_engine.rb +++ b/decidim-surveys/lib/decidim/surveys/admin_engine.rb @@ -10,12 +10,21 @@ class AdminEngine < ::Rails::Engine paths["lib/tasks"] = nil routes do - get "/answer/:session_token", to: "surveys#show", as: :show_survey - get "/answer/:session_token/export", to: "surveys#export_response", as: :export_response_survey - get "/answers", to: "surveys#index", as: :index_survey get "/answer_options", to: "surveys#answer_options", as: :answer_options_survey - put "/", to: "surveys#update", as: :survey - root to: "surveys#edit" + resources :surveys, except: [:new] do + member do + get :edit_questions + patch :update_questions + put :publish + put :unpublish + end + resources :answers, only: [:index, :show] do + member do + get :export_response + end + end + end + root to: "surveys#index" end initializer "decidim_surveys_admin.notifications.components" do diff --git a/decidim-surveys/lib/decidim/surveys/component.rb b/decidim-surveys/lib/decidim/surveys/component.rb index c8841ed0b6289..1cc0db3054e30 100644 --- a/decidim-surveys/lib/decidim/surveys/component.rb +++ b/decidim-surveys/lib/decidim/surveys/component.rb @@ -12,18 +12,6 @@ component.specific_data_importer_class_name = "Decidim::Surveys::DataImporter" component.query_type = "Decidim::Surveys::SurveysType" - component.on(:copy) do |context| - Decidim::Surveys::CreateSurvey.call(context[:new_component]) do - on(:invalid) { raise "Cannot create survey" } - end - end - - component.on(:create) do |instance| - Decidim::Surveys::CreateSurvey.call(instance) do - on(:invalid) { raise "Cannot create survey" } - end - end - component.data_portable_entities = ["Decidim::Forms::Answer"] component.newsletter_participant_entities = ["Decidim::Forms::Answer"] @@ -37,9 +25,11 @@ component.register_resource(:survey) do |resource| resource.model_class_name = "Decidim::Surveys::Survey" + resource.card = "decidim/surveys/survey" + resource.actions = %w(answer) end - component.register_stat :surveys_count do |components, start_at, end_at| + component.register_stat :surveys_count, primary: true, priority: Decidim::StatsRegistry::HIGH_PRIORITY do |components, start_at, end_at| surveys = Decidim::Surveys::Survey.where(component: components) surveys = surveys.where(created_at: start_at..) if start_at.present? surveys = surveys.where(created_at: ..end_at) if end_at.present? @@ -58,17 +48,10 @@ component.actions = %w(answer) component.settings(:global) do |settings| - settings.attribute :scopes_enabled, type: :boolean, default: false - settings.attribute :scope_id, type: :scope - settings.attribute :starts_at, type: :time - settings.attribute :ends_at, type: :time settings.attribute :announcement, type: :text, translated: true, editor: true - settings.attribute :clean_after_publish, type: :boolean, default: true end component.settings(:step) do |settings| - settings.attribute :allow_answers, type: :boolean, default: false - settings.attribute :allow_unregistered, type: :boolean, default: false settings.attribute :announcement, type: :text, translated: true, editor: true end diff --git a/decidim-surveys/lib/decidim/surveys/engine.rb b/decidim-surveys/lib/decidim/surveys/engine.rb index 908abdf4ee519..85580d6e55626 100644 --- a/decidim-surveys/lib/decidim/surveys/engine.rb +++ b/decidim-surveys/lib/decidim/surveys/engine.rb @@ -9,12 +9,12 @@ class Engine < ::Rails::Engine isolate_namespace Decidim::Surveys routes do - resources :surveys, only: [:show] do + resources :surveys, only: [:index, :show] do member do post :answer end end - root to: "surveys#show" + root to: "surveys#index" end initializer "decidim_surveys.settings_changes" do diff --git a/decidim-surveys/lib/decidim/surveys/test/factories.rb b/decidim-surveys/lib/decidim/surveys/test/factories.rb index 4da0eeb572176..e69b9c596fcf3 100644 --- a/decidim-surveys/lib/decidim/surveys/test/factories.rb +++ b/decidim-surveys/lib/decidim/surveys/test/factories.rb @@ -21,5 +21,25 @@ end questionnaire { build(:questionnaire, :with_questions, skip_injection:) } component { build(:surveys_component, skip_injection:) } + + trait :published do + published_at { Time.current } + end + + trait :allow_answers do + allow_answers { true } + end + + trait :allow_unregistered do + allow_unregistered { true } + end + + trait :clean_after_publish do + clean_after_publish { true } + end + + trait :announcement do + announcement { "This is a custom announcement." } + end end end diff --git a/decidim-surveys/spec/cells/decidim/surveys/survey_card_metadata_cell_spec.rb b/decidim-surveys/spec/cells/decidim/surveys/survey_card_metadata_cell_spec.rb new file mode 100644 index 0000000000000..edf798db0f3f0 --- /dev/null +++ b/decidim-surveys/spec/cells/decidim/surveys/survey_card_metadata_cell_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Surveys + describe SurveyCardMetadataCell, type: :cell do + controller Decidim::Surveys::SurveysController + + subject { cell_html } + + let(:my_cell) { cell("decidim/surveys/survey_card_metadata", survey) } + let(:cell_html) { my_cell.call } + let(:survey) { create(:survey, published_at: Time.current, starts_at: 2.days.ago, ends_at: 1.week.from_now) } + let(:component) { survey.component } + let(:user) { create(:user, :confirmed, :admin, organization: component.organization) } + + describe "duration" do + context "when survey is open" do + before do + allow(survey).to receive(:open?).and_return(true) + end + + it "renders the 'open' text and the time-line icon" do + expect(subject.to_s).to include(I18n.t("open", scope: "decidim.surveys.surveys.show")) + expect(subject.to_s).to include("time-line") + end + end + + context "when survey is closed" do + before do + allow(survey).to receive(:open?).and_return(false) + end + + it "renders the 'closed' text and the time-line icon" do + expect(subject.to_s).to include(I18n.t("closed", scope: "decidim.surveys.surveys.show")) + expect(subject.to_s).to include("time-line") + end + end + end + + describe "questions_count_item" do + it "renders the correct number of questions and survey icon" do + questions_count = survey.questionnaire.questions.size + expect(subject.to_s).to include("#{questions_count} #{I18n.t("questions", scope: "decidim.surveys.surveys.show")}") + expect(subject.to_s).to include("survey-line") + end + end + end +end diff --git a/decidim-surveys/spec/cells/decidim/surveys/survey_l_cell_spec.rb b/decidim-surveys/spec/cells/decidim/surveys/survey_l_cell_spec.rb new file mode 100644 index 0000000000000..3ba4777fdef6a --- /dev/null +++ b/decidim-surveys/spec/cells/decidim/surveys/survey_l_cell_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Surveys + describe SurveyLCell, type: :cell do + controller Decidim::Surveys::SurveysController + + subject { cell_html } + + let(:my_cell) { cell("decidim/surveys/survey_l", survey, context: { show_space: }) } + let(:cell_html) { my_cell.call } + let(:component) { survey.component } + let(:survey) { create(:survey, starts_at: 2.days.ago, ends_at: 1.week.from_now) } + let(:model) { survey } + let(:user) { create(:user, :confirmed, :admin, organization: component.organization) } + + before do + allow(controller).to receive(:current_user).and_return(user) + end + + context "when rendering" do + let(:show_space) { false } + + it "renders the card" do + expect(subject).to have_css("[id^='surveys__survey']") + end + + it "renders the metadata" do + expect(subject).to have_css(".card__list-metadata") + end + + it "renders the title" do + expect(subject).to have_content(decidim_sanitize(translated_attribute(survey.title), strip_tags: true)) + expect(subject).to have_css(".card__list-title") + end + + it "renders the description" do + expect(subject).to have_content(decidim_sanitize(translated_attribute(survey.description), strip_tags: true)) + expect(subject).to have_css(".card__list-text") + end + end + end +end diff --git a/decidim-surveys/spec/commands/decidim/surveys/publish_survey_spec.rb b/decidim-surveys/spec/commands/decidim/surveys/publish_survey_spec.rb new file mode 100644 index 0000000000000..539a341e705ad --- /dev/null +++ b/decidim-surveys/spec/commands/decidim/surveys/publish_survey_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "spec_helper" +module Decidim + module Surveys + module Admin + describe PublishSurvey, type: :command do + let(:organization) { create(:organization, available_locales: [:en]) } + let(:survey) { create(:survey, published_at: nil) } + let(:current_user) { create(:user, :admin, :confirmed, organization:) } + let(:command) { described_class.new(survey, current_user) } + + describe "call" do + context "when the survey is not already published" do + it "publishes the survey" do + expect(Decidim.traceability).to receive(:perform_action!).with( + :publish, + survey, + current_user, + visibility: "all" + ).and_call_original + + expect(command).to broadcast(:ok, survey) + end + end + + context "when the survey is already published" do + before do + survey.update!(published_at: Time.current) + end + + it "does not publish the survey" do + expect { command.call }.not_to(change { survey.reload.published_at }) + + expect(command).to broadcast(:invalid) + end + end + end + end + end + end +end diff --git a/decidim-surveys/spec/commands/decidim/surveys/unpublish_survey_spec.rb b/decidim-surveys/spec/commands/decidim/surveys/unpublish_survey_spec.rb new file mode 100644 index 0000000000000..50ccd97427aa0 --- /dev/null +++ b/decidim-surveys/spec/commands/decidim/surveys/unpublish_survey_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Surveys + module Admin + describe UnpublishSurvey, type: :command do + let(:organization) { create(:organization, available_locales: [:en]) } + let(:survey) { create(:survey, published_at: Time.current) } + let(:current_user) { create(:user, :admin, :confirmed, organization:) } + let(:command) { described_class.new(survey, current_user) } + + describe "call" do + context "when the survey is published" do + it "unpublishes the survey" do + expect(Decidim.traceability).to receive(:perform_action!).with( + :unpublish, + survey, + current_user + ).and_call_original + + expect(command).to broadcast(:ok, survey) + end + end + + context "when the survey is not published" do + before do + survey.update!(published_at: nil) + end + + it "does not unpublish the survey" do + expect { command.call }.not_to(change { survey.reload.published_at }) + + expect(command).to broadcast(:invalid) + end + end + end + end + end + end +end diff --git a/decidim-surveys/spec/controllers/decidim/surveys/admin/answers_controller_spec.rb b/decidim-surveys/spec/controllers/decidim/surveys/admin/answers_controller_spec.rb new file mode 100644 index 0000000000000..c090066735c3f --- /dev/null +++ b/decidim-surveys/spec/controllers/decidim/surveys/admin/answers_controller_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Surveys + module Admin + describe AnswersController do + routes { Decidim::Surveys::AdminEngine.routes } + + let(:component) { survey.component } + let(:survey) { create(:survey, published_at: Time.current) } + let(:user) { create(:user, :confirmed, :admin, organization: component.organization) } + + let(:answers) do + survey.questionnaire.questions.map do |question| + create(:answer, questionnaire: survey.questionnaire, question:, user:) + end + end + + before do + request.env["decidim.current_organization"] = component.organization + request.env["decidim.current_component"] = component + sign_in user, scope: :user + end + + describe "index" do + let(:params) do + { + component_id: survey.component.id, + participatory_process_slug: survey.component.participatory_space.slug, + survey_id: survey.id + } + end + + it "renders the index template" do + get(:index, params:) + expect(response).to render_template(:index) + end + end + + describe "show" do + let(:answer) { create(:answer, questionnaire: survey.questionnaire, question: survey.questionnaire.questions.first, user:) } + let(:params) do + { + component_id: survey.component.id, + participatory_process_slug: survey.component.participatory_space.slug, + survey_id: survey.id, + id: answer.id + } + end + + it "renders the show template" do + get(:show, params:) + expect(response).to render_template(:show) + end + end + + describe "export_response" do + let(:answer) { create(:answer, questionnaire: survey.questionnaire, question: survey.questionnaire.questions.first, user:) } + let(:params) do + { + component_id: survey.component.id, + participatory_process_slug: survey.component.participatory_space.slug, + survey_id: survey.id, + id: answer.id + } + end + + it "redirects with a flash notice message" do + get(:export_response, params:) + + expect(response).to be_redirect + expect(flash[:notice]).to be_present + end + end + end + end + end +end diff --git a/decidim-surveys/spec/controllers/decidim/surveys/admin/surveys_controller_spec.rb b/decidim-surveys/spec/controllers/decidim/surveys/admin/surveys_controller_spec.rb index 1b1033c8c9fba..95408787c3a3e 100644 --- a/decidim-surveys/spec/controllers/decidim/surveys/admin/surveys_controller_spec.rb +++ b/decidim-surveys/spec/controllers/decidim/surveys/admin/surveys_controller_spec.rb @@ -11,15 +11,14 @@ module Admin let(:component) { survey.component } let(:survey) { create(:survey) } let(:user) { create(:user, :confirmed, :admin, organization: component.organization) } - - let(:answers) do - survey.questionnaire.questions.map do |question| - create(:answer, questionnaire: survey.questionnaire, question:, user:) - end + let(:params) do + { + component_id: survey.component.id, + participatory_process_slug: survey.component.participatory_space.slug, + id: survey.id + } end - let(:session_token) { answers.first.session_token } - before do request.env["decidim.current_organization"] = component.organization request.env["decidim.current_component"] = component @@ -27,53 +26,15 @@ module Admin end describe "index" do - let(:survey) { create(:survey) } - let(:params) do - { - component_id: survey.component.id, - participatory_process_slug: survey.component.participatory_space.slug, - id: survey.id - } - end - it "renders the index template" do get(:index, params:) expect(response).to render_template(:index) end - end - - describe "show" do - let(:params) do - { - component_id: survey.component.id, - participatory_process_slug: survey.component.participatory_space.slug, - id: survey.id, - session_token: - } - end - it "renders the show template" do - get(:show, params:) - expect(response).to render_template(:show) - end - end - - describe "export_response" do - let(:filename) { "Response for #{session_token}.pdf" } - let(:params) do - { - component_id: survey.component.id, - participatory_process_slug: survey.component.participatory_space.slug, - id: survey.id, - session_token: - } - end - - it "redirects with a flash notice message" do - get(:export_response, params:) - - expect(response).to be_redirect - expect(flash[:notice]).to be_present + it "renders the index template even when no surveys are present" do + allow(Decidim::Surveys::Survey).to receive(:where).and_return([]) + get(:index, params:) + expect(response).to render_template(:index) end end end diff --git a/decidim-surveys/spec/helpers/application_helper_spec.rb b/decidim-surveys/spec/helpers/application_helper_spec.rb new file mode 100644 index 0000000000000..3c7605e6c5200 --- /dev/null +++ b/decidim-surveys/spec/helpers/application_helper_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Surveys + describe ApplicationHelper do + describe "#filter_surveys_date_values" do + it "returns the correct filter values for survey dates" do + expected_values = [ + ["all", I18n.t("all", scope: "decidim.surveys.surveys.filters")], + ["open", { checked: true }, I18n.t("open", scope: "decidim.surveys.surveys.filters.state_values")], + ["closed", I18n.t("closed", scope: "decidim.surveys.surveys.filters.state_values")] + ] + + expect(helper.filter_surveys_date_values).to eq(expected_values) + end + end + + describe "#filter_sections" do + it "returns the correct filter sections structure" do + expected_sections = [{ + method: :with_any_state, + collection: helper.filter_surveys_date_values, + label: t("decidim.proposals.proposals.filters.state"), + id: "state", + type: :radio_buttons + }] + + expect(helper.filter_sections).to eq(expected_sections) + end + end + end + end +end diff --git a/decidim-surveys/spec/helpers/survey_helper_spec.rb b/decidim-surveys/spec/helpers/survey_helper_spec.rb new file mode 100644 index 0000000000000..17a5ce055c80a --- /dev/null +++ b/decidim-surveys/spec/helpers/survey_helper_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Surveys + describe SurveyHelper do + let(:organization) { create(:organization) } + let(:participatory_space) { create(:participatory_process, organization:) } + let(:surveys_component) { create(:surveys_component, :published, participatory_space:) } + let(:survey) { create(:survey, component: surveys_component) } + + describe "#no_permission" do + it "renders the authorization modal" do + authorizations = double("authorizations") + allow(helper).to receive(:authorizations).and_return(authorizations) + + expect(helper).to receive(:cell).with("decidim/authorization_modal", authorizations) + helper.no_permission + end + end + + describe "#resource" do + it "returns the questionnaire resource" do + allow(helper).to receive(:questionnaire_for).and_return(survey) + expect(helper.resource).to eq(survey) + end + end + + describe "#current_component" do + let(:params) { { component_id: surveys_component.id } } + + it "returns the current component based on params" do + allow(helper).to receive(:params).and_return(params) + expect(helper.current_component).to eq(surveys_component) + end + end + + describe "#authorization_action" do + let(:params) { { authorization_action: "some_action" } } + + it "returns the authorization action from params" do + allow(helper).to receive(:params).and_return(params) + expect(helper.authorization_action).to eq("some_action") + end + end + + describe "#authorize_action_path" do + it "returns the authorization path for a handler" do + status = double("status", current_path: "/path/to/authorization") + authorizations = double("authorizations") + allow(helper).to receive(:authorizations).and_return(authorizations) + allow(authorizations).to receive(:status_for).with("some_handler").and_return(status) + + expect(helper.authorize_action_path("some_handler")).to eq("/path/to/authorization") + end + end + + describe "#filter_date_values" do + it "returns the correct filter values" do + expected_values = [:all, :open, :closed] + allow(helper).to receive(:flat_filter_values).with(:all, :open, :closed, scope: "decidim.surveys.surveys.filters.date_values").and_return(expected_values) + + expect(helper.filter_date_values).to eq(expected_values) + end + end + end + end +end diff --git a/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb b/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb index 682f6be4385d7..2b440ff165b30 100644 --- a/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb +++ b/decidim-surveys/spec/lib/decidim/surveys/component_spec.rb @@ -38,9 +38,5 @@ it "does not raise any error" do expect { subject.manifest.run_hooks(:copy, old_component: component, new_component:) }.not_to raise_error end - - it "create a survey component" do - expect { subject.manifest.run_hooks(:copy, old_component: component, new_component:) }.to change(Decidim::Surveys::Survey, :count).by(1) - end end end diff --git a/decidim-surveys/spec/models/decidim/surveys/survey_spec.rb b/decidim-surveys/spec/models/decidim/surveys/survey_spec.rb index 04c5e431ba82b..3f427f499e22b 100644 --- a/decidim-surveys/spec/models/decidim/surveys/survey_spec.rb +++ b/decidim-surveys/spec/models/decidim/surveys/survey_spec.rb @@ -8,6 +8,11 @@ module Surveys subject { survey } let(:survey) { create(:survey) } + let!(:open_survey) { create(:survey, allow_answers: true, starts_at: 2.days.ago, ends_at: 1.day.from_now) } + let!(:closed_survey) { create(:survey, allow_answers: false, starts_at: nil, ends_at: nil) } + let!(:future_survey) { create(:survey, allow_answers: true, starts_at: 1.day.from_now, ends_at: nil) } + let!(:past_survey) { create(:survey, allow_answers: true, starts_at: 5.days.ago, ends_at: 2.days.ago) } + let!(:indefinite_survey) { create(:survey, allow_answers: true, starts_at: nil, ends_at: nil) } include_examples "has component" @@ -45,7 +50,7 @@ module Surveys let(:component) { survey.component } before do - component.settings = { starts_at:, ends_at: } + survey.update!(starts_at:, ends_at:, allow_answers: true) end context "when neither starts_at or ends_at are defined" do @@ -103,6 +108,37 @@ module Surveys end end end + + describe "scopes" do + describe ".open" do + it "returns surveys that are currently open" do + expect(Decidim::Surveys::Survey.open).to include(open_survey, indefinite_survey) + end + + it "does not return surveys that are closed, in the future, or already finished" do + expect(Decidim::Surveys::Survey.open).not_to include(closed_survey, future_survey, past_survey) + end + end + + describe ".closed" do + it "returns surveys that are closed or past their end date" do + expect(Decidim::Surveys::Survey.closed).to include(closed_survey, past_survey) + end + end + end + + describe ".ransackable_attributes" do + it "returns the correct ransackable attributes" do + expected_attributes = %w(ends_at starts_at allow_answers) + expect(described_class.ransackable_attributes).to eq(expected_attributes) + end + end + + describe ".log_presenter_class_for" do + it "returns the correct presenter class for logs" do + expect(described_class.log_presenter_class_for(nil)).to eq(Decidim::Surveys::AdminLog::SurveyPresenter) + end + end end end end diff --git a/decidim-surveys/spec/presenters/decidim/surveys/admin_log/survey_presenter_spec.rb b/decidim-surveys/spec/presenters/decidim/surveys/admin_log/survey_presenter_spec.rb new file mode 100644 index 0000000000000..e7b5e9c7d1696 --- /dev/null +++ b/decidim-surveys/spec/presenters/decidim/surveys/admin_log/survey_presenter_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Decidim::Surveys::AdminLog::SurveyPresenter, type: :helper do + include_examples "present admin log entry" do + let(:participatory_space) { create(:participatory_process, organization:) } + let(:component) { create(:surveys_component, participatory_space:) } + let(:admin_log_resource) { create(:survey, component:) } + let(:action) { "update" } + end +end diff --git a/decidim-surveys/spec/shared/export_survey_user_answers_examples.rb b/decidim-surveys/spec/shared/export_survey_user_answers_examples.rb index d30f680b69849..4e85e50f5988f 100644 --- a/decidim-surveys/spec/shared/export_survey_user_answers_examples.rb +++ b/decidim-surveys/spec/shared/export_survey_user_answers_examples.rb @@ -11,6 +11,7 @@ it "exports a CSV" do visit_component_admin + click_on "Manage questions" find(".exports").click expect(Decidim::PrivateExport.count).to eq(0) @@ -26,6 +27,7 @@ it "exports a JSON" do visit_component_admin + click_on "Manage questions" find(".exports").click expect(Decidim::PrivateExport.count).to eq(0) @@ -40,6 +42,7 @@ it "exports a PDF" do visit_component_admin + click_on "Manage questions" find(".exports").click expect(Decidim::PrivateExport.count).to eq(0) diff --git a/decidim-surveys/spec/system/admin_manages_surveys_spec.rb b/decidim-surveys/spec/system/admin_manages_surveys_spec.rb index e6060b5379048..66297d2f5e7b2 100644 --- a/decidim-surveys/spec/system/admin_manages_surveys_spec.rb +++ b/decidim-surveys/spec/system/admin_manages_surveys_spec.rb @@ -11,7 +11,7 @@ published_at: nil) end let!(:questionnaire) { create(:questionnaire) } - let!(:survey) { create(:survey, component:, questionnaire:) } + let!(:survey) { create(:survey, :published, :clean_after_publish, component:, questionnaire:) } include_context "when managing a component as an admin" @@ -28,53 +28,55 @@ let!(:question) { create(:questionnaire_question, questionnaire:) } it "allows to preview survey" do - visit questionnaire_edit_path + visit manage_questions_path expect(page).to have_link("Preview", href: [questionnaire_public_path, "surveys/#{survey.id}"].join) end it "shows a warning message" do visit questionnaire_public_path - expect(page).to have_content("This form is not published yet") + expect(page).to have_content("No surveys match your search criteria or there is not any survey open.") end it "allows to answer survey" do visit questionnaire_public_path - expect(page).to have_field(id: "questionnaire_responses_0") + expect(page).to have_no_field(id: "questionnaire_responses_0") end context "when the survey has answers" do let!(:answer) { create(:answer, question:, questionnaire:) } it "shows warning message" do - visit questionnaire_edit_path + click_on "Manage questions" expect(page).to have_content("The form is not published") end it "allows editing questions" do - visit questionnaire_edit_path + click_on "Manage questions" click_on "Expand all" - expect(page).to have_css("#questionnaire_questions_#{question.id}_body_en") - expect(page).to have_no_selector("#questionnaire_questions_#{question.id}_body_en[disabled]") + expect(page).to have_css("#questions_questions_#{question.id}_body_en") + expect(page).to have_no_selector("#questions_questions_#{question.id}_body_en[disabled]") end - it "deletes answers after editing" do - visit questionnaire_edit_path + it "deletes answers after published" do + click_on "Manage questions" click_on "Expand all" - within "form.edit_questionnaire" do - within "#accordion-questionnaire_question_#{question.id}-field" do - find_nested_form_field("body_en").fill_in with: "Have you been writing specs today?" - end - click_on "Save" + within "#accordion-questionnaire_question_#{question.id}-field" do + find_nested_form_field("body_en").fill_in with: "Have you been writing specs today?" end + click_on "Save" + expect(page).to have_admin_callout "Survey questions successfully saved" + + click_on "Unpublish" + expect(page).to have_admin_callout "Survey successfully unpublished" - expect(page).to have_admin_callout "Survey successfully saved" + accept_confirm { click_on("Publish") } + expect(page).to have_admin_callout "Survey successfully published" expect(questionnaire.answers).to be_empty end context "when publishing the survey" do - let(:clean_after_publish) { true } let!(:participatory_process) do create(:participatory_process, organization:) end @@ -84,24 +86,15 @@ let(:components_path) { participatory_space_path } before do - component.update!( - settings: { - clean_after_publish: - } - ) - visit components_path end context "when clean_after_publish is set to true" do - context "when deletes previous answers afer publishing" do - it "show popup with an alert" do - find(:css, ".action-icon--publish").click - expect(page).to have_content("Confirm") - end - + context "when deletes previous answers after publishing" do it "deletes previous answers" do - expect(survey.clean_after_publish?).to be true + click_on translated_attribute(component.name) + click_on "Edit" + expect(survey.clean_after_publish).to be true perform_enqueued_jobs do Decidim::Admin::PublishComponent.call(component, user) @@ -113,9 +106,9 @@ end context "when clean_after_publish is set to false" do - let(:clean_after_publish) { false } + let!(:survey) { create(:survey, :published, clean_after_publish: false, component:, questionnaire:) } - it "does not delete previous answers afer publishing" do + it "does not delete previous answers after publishing" do expect(survey.clean_after_publish?).to be false perform_enqueued_jobs do @@ -129,8 +122,40 @@ end end + context "when updates the questionnaire" do + let(:description) do + { + en: "

New description

", + ca: "

Nova descripció

", + es: "

Nueva descripción

" + } + end + let!(:questionnaire) { create(:questionnaire, description:) } + let!(:survey) { create(:survey, :published, component:, questionnaire:) } + + it "updates the questionnaire description" do + visit questionnaire_public_path + choose "All" + + expect(page).to have_content("New description") + end + end + + def manage_questions_path + Decidim::EngineRouter.admin_proxy(component).edit_questions_survey_path(survey) + end + + def update_component_settings_or_attributes + survey.update!(allow_answers: true) + end + + def see_questionnaire_questions + choose "All" + click_on decidim_sanitize_translated(questionnaire.title) + end + def questionnaire_edit_path - manage_component_path(component) + Decidim::EngineRouter.admin_proxy(component).edit_survey_path(survey) end def questionnaire_public_path diff --git a/decidim-surveys/spec/system/private_space_survey_spec.rb b/decidim-surveys/spec/system/private_space_survey_spec.rb index b0fdea87d5f32..3cc5c2679d8ef 100644 --- a/decidim-surveys/spec/system/private_space_survey_spec.rb +++ b/decidim-surveys/spec/system/private_space_survey_spec.rb @@ -28,7 +28,7 @@ let!(:participatory_space_private_user) { create(:participatory_space_private_user, user: another_user, privatable_to: participatory_space_private) } let!(:questionnaire) { create(:questionnaire, title:, description:) } - let!(:survey) { create(:survey, component:, questionnaire:) } + let!(:survey) { create(:survey, :published, :allow_answers, component:, questionnaire:) } let!(:question) { create(:questionnaire_question, questionnaire:, position: 0) } let!(:question_conditioned) { create(:questionnaire_question, :conditioned, questionnaire:, position: 1) } @@ -38,7 +38,6 @@ before do switch_to_host(organization.host) - component.update!(default_step_settings: { allow_answers: true }) end def visit_component @@ -51,6 +50,7 @@ def visit_component context "when the user is not logged in" do it "does not allow answering the survey" do visit_component + click_on translated_attribute(questionnaire.title) expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) @@ -72,6 +72,7 @@ def visit_component it "allows answering the survey" do visit_component + click_on translated_attribute(questionnaire.title) expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) @@ -98,6 +99,7 @@ def visit_component it "not allows answering the survey" do visit_component + click_on translated_attribute(questionnaire.title) expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) @@ -133,10 +135,13 @@ def visit_component it "allows answering the survey" do visit_component + choose "All" expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) + click_on translated_attribute(questionnaire.title) + fill_in question.body["en"], with: "My first answer" check "questionnaire_tos_agreement" diff --git a/decidim-surveys/spec/system/registered_user_survey_spec.rb b/decidim-surveys/spec/system/registered_user_survey_spec.rb index 56f05fe7706fc..6f4775bd5b9e7 100644 --- a/decidim-surveys/spec/system/registered_user_survey_spec.rb +++ b/decidim-surveys/spec/system/registered_user_survey_spec.rb @@ -23,7 +23,7 @@ } end let!(:questionnaire) { create(:questionnaire, title:, description:) } - let!(:survey) { create(:survey, component:, questionnaire:) } + let!(:survey) { create(:survey, :published, component:, questionnaire:) } let!(:question) { create(:questionnaire_question, questionnaire:, position: 0) } let(:mailer) { double(deliver_later: true) } @@ -32,6 +32,8 @@ context "when the survey does not allow answers" do it "does not allow answering the survey" do visit_component + choose "All" + click_on translated_attribute(questionnaire.title) expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) @@ -45,17 +47,9 @@ context "when the survey allow answers" do let(:organization) { create(:organization) } let!(:user) { create(:user, :confirmed, organization:) } + let!(:survey) { create(:survey, :published, :allow_answers, allow_unregistered: false, component:, questionnaire:) } before do - component.update!( - step_settings: { - component.participatory_space.active_step.id => { - allow_answers: true, - allow_unregistered: false - } - } - ) - login_as user, scope: :user end @@ -63,6 +57,7 @@ allow(Decidim::Surveys::SurveyConfirmationMailer).to receive(:confirmation).and_return(mailer) visit_component + click_on translated_attribute(questionnaire.title) expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) diff --git a/decidim-surveys/spec/system/survey_spec.rb b/decidim-surveys/spec/system/survey_spec.rb index d845b4051e030..94027f85ceca0 100644 --- a/decidim-surveys/spec/system/survey_spec.rb +++ b/decidim-surveys/spec/system/survey_spec.rb @@ -28,7 +28,7 @@ end let(:user) { create(:user, :confirmed, organization: component.organization) } let!(:questionnaire) { create(:questionnaire, title:, description:) } - let!(:survey) { create(:survey, component:, questionnaire:) } + let!(:survey) { create(:survey, :published, component:, questionnaire:) } let!(:question) { create(:questionnaire_question, questionnaire:, position: 0, description: question_description) } include_context "with a component" @@ -39,11 +39,15 @@ it "does not allow answering the survey" do visit_component + choose "All" + expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) expect(page).to have_no_i18n_content(question.body) + click_on translated_attribute(questionnaire.title) + expect(page).to have_content("The form is closed and cannot be answered.") end end @@ -60,6 +64,8 @@ component.update!(permissions:) visit_component + choose "All" + click_on translated_attribute(questionnaire.title) end it_behaves_like "accessible page" @@ -72,17 +78,20 @@ context "when the survey allow answers" do context "when the survey is closed by start and end dates" do before do - component.update!(settings: { starts_at: 1.week.ago, ends_at: 1.day.ago }) + survey.update!(starts_at: 1.week.ago, ends_at: 1.day.ago) end it "does not allow answering the survey" do visit_component + choose "All" expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) expect(page).to have_no_i18n_content(question.body) + click_on translated_attribute(questionnaire.title) + expect(page).to have_content("The form is closed and cannot be answered.") end end @@ -92,14 +101,7 @@ let(:callout_success) { "Survey successfully answered." } before do - component.update!( - step_settings: { - component.participatory_space.active_step.id => { - allow_answers: true - } - }, - settings: { starts_at: 1.week.ago, ends_at: 1.day.from_now } - ) + survey.update!(allow_answers: true, starts_at: 1.week.ago, ends_at: 1.day.from_now) end it_behaves_like "has questionnaire" @@ -107,16 +109,14 @@ context "when displaying questionnaire rich content" do before do - component.update!( - step_settings: { - component.participatory_space.active_step.id => { - allow_answers: true, - allow_unregistered: true - } - }, - settings: { starts_at: 1.week.ago, ends_at: 1.day.from_now } + survey.update!( + allow_answers: true, + allow_unregistered: true, + starts_at: 1.week.ago, + ends_at: 1.day.from_now ) visit_component + click_on translated_attribute(questionnaire.title) end context "when displaying questionnaire description" do @@ -129,8 +129,22 @@ end end + context "when survey has a custom announcement" do + let!(:survey) { create(:survey, :published, :announcement, :allow_answers, :allow_unregistered, component:, questionnaire:) } + + before do + visit_component + click_on translated_attribute(questionnaire.title) + end + + it "displays the announcement in the survey" do + expect(page).to have_content("This is a custom announcement.") + end + end + context "when survey has action log entry" do - let!(:action_log) { create(:action_log, user:, organization: component.organization, resource: survey, component:, participatory_space: component.participatory_space, visibility: "all") } + let!(:action_log) { create(:action_log, user:, action: "publish", organization: component.organization, resource: survey, component:, participatory_space: component.participatory_space, visibility: "all") } + let(:router) { Decidim::EngineRouter.main_proxy(component) } it "shows action log entry" do @@ -144,4 +158,8 @@ def questionnaire_public_path main_component_path(component) end + + def see_questionnaire_questions + click_on translated_attribute(questionnaire.title) + end end diff --git a/decidim-surveys/spec/system/unregistered_user_survey_spec.rb b/decidim-surveys/spec/system/unregistered_user_survey_spec.rb index b5e616b591e17..ac1c03f4ab336 100644 --- a/decidim-surveys/spec/system/unregistered_user_survey_spec.rb +++ b/decidim-surveys/spec/system/unregistered_user_survey_spec.rb @@ -23,7 +23,7 @@ } end let!(:questionnaire) { create(:questionnaire, title:, description:) } - let!(:survey) { create(:survey, component:, questionnaire:) } + let!(:survey) { create(:survey, :published, component:, questionnaire:) } let!(:question) { create(:questionnaire_question, questionnaire:, position: 0) } include_context "with a component" @@ -31,6 +31,8 @@ context "when the survey does not allow answers" do it "does not allow answering the survey" do visit_component + choose "All" + click_on translated_attribute(questionnaire.title) expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) @@ -45,18 +47,12 @@ let(:last_answer) { questionnaire.answers.last } before do - component.update!( - step_settings: { - component.participatory_space.active_step.id => { - allow_answers: true, - allow_unregistered: true - } - } - ) + survey.update!(allow_answers: true, allow_unregistered: true) end it "allows answering the questionnaire" do visit_component + click_on translated_attribute(questionnaire.title) expect(page).to have_i18n_content(questionnaire.title) expect(page).to have_i18n_content(questionnaire.description) @@ -82,6 +78,7 @@ context "and honeypot is filled" do it "fails with spam complain" do visit_component + click_on translated_attribute(questionnaire.title) fill_in question.body["en"], with: "My first answer" fill_in "honeypot_id", with: "I am a robot" diff --git a/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb b/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb index 7e9d4c7a7ad3a..a0eb02590d96e 100644 --- a/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb +++ b/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb @@ -7,8 +7,10 @@ module Admin # class QuestionnaireTemplatesController < Decidim::Templates::Admin::ApplicationController include Decidim::TranslatableAttributes + include Decidim::Forms::Admin::Concerns::HasQuestionnaire + helper Decidim::Forms::Admin::ApplicationHelper - helper_method :template + helper_method :template, :questionnaire add_breadcrumb_item_from_menu :admin_template_types_menu @@ -135,8 +137,24 @@ def skip redirect_to URI.parse(params[:url]).path end + def edit_questions_template + "decidim/templates/admin/questionnaire_templates/edit_questions" + end + + def after_update_url + edit_questionnaire_template_path(template) + end + private + def questionnaire + template.templatable + end + + def i18n_flashes_scope + "decidim.forms.admin.questionnaires" + end + def collection @collection ||= current_organization.templates.where(templatable_type: "Decidim::Forms::Questionnaire") end diff --git a/decidim-templates/app/views/decidim/templates/admin/questionnaire_templates/edit.html.erb b/decidim-templates/app/views/decidim/templates/admin/questionnaire_templates/edit.html.erb index 79e0fa10fd5d3..83a5f7ca1b529 100644 --- a/decidim-templates/app/views/decidim/templates/admin/questionnaire_templates/edit.html.erb +++ b/decidim-templates/app/views/decidim/templates/admin/questionnaire_templates/edit.html.erb @@ -17,8 +17,10 @@
-

<%= t(".questionnaire") %> +

+ <%= t(".questionnaire") %> <%= link_to t(".edit"), edit_questionnaire_path(template), class: "button button__sm button__secondary" %> + <%= link_to t(".manage_questions"), edit_questions_questionnaire_template_path(template), class: "button button__sm button__secondary" %>

diff --git a/decidim-templates/app/views/decidim/templates/admin/questionnaire_templates/edit_questions.erb b/decidim-templates/app/views/decidim/templates/admin/questionnaire_templates/edit_questions.erb new file mode 100644 index 0000000000000..27fc806ae05eb --- /dev/null +++ b/decidim-templates/app/views/decidim/templates/admin/questionnaire_templates/edit_questions.erb @@ -0,0 +1,27 @@ +<% add_decidim_page_title(t("decidim.forms.admin.questionnaires.edit.title")) %> + +
+
+

+ <%= t("decidim.forms.admin.questionnaires.edit_questions.title") %> + <% if questionnaire.questions_editable? %> + + + + <% end %> +

+
+
+ +
+
+ <%= decidim_form_for(@form, url: update_questions_questionnaire_template_path(template), method: :patch, html: { class: "form-defaults form edit_questions_survey" }) do |form| %> + <%= render partial: "decidim/forms/admin/questionnaires/questions_form", object: form %> +
+
+ <%= form.submit t("save", scope: "decidim.forms.admin.questionnaires.edit"), class: "button button__sm button__secondary" %> +
+
+ <% end %> +
+
diff --git a/decidim-templates/config/locales/en.yml b/decidim-templates/config/locales/en.yml index f76ef6f84c65b..d6cb407a6fc36 100644 --- a/decidim-templates/config/locales/en.yml +++ b/decidim-templates/config/locales/en.yml @@ -89,8 +89,13 @@ en: edit: edit: Edit empty: There are no questions yet. + manage_questions: Manage questions questionnaire: Questionnaire title: Edit questionnaire template + edit_questions: + add_question: Add question + add_separator: Add separator + add_title_and_description: Add title and description form: title: Questionnaire template %{questionnaire_for} index: diff --git a/decidim-templates/db/seeds.rb b/decidim-templates/db/seeds.rb index 17f5258100342..eebd1cf4479bd 100644 --- a/decidim-templates/db/seeds.rb +++ b/decidim-templates/db/seeds.rb @@ -62,11 +62,12 @@ ) end - %w(matrix_single matrix_multiple).each do |matrix_question_type| + %w(matrix_single matrix_multiple).each_with_index do |matrix_question_type, index| question = Decidim::Forms::Question.create!( questionnaire:, body: Decidim::Faker::Localized.paragraph, - question_type: matrix_question_type + question_type: matrix_question_type, + position: index + 4 ) 3.times do |idx| diff --git a/decidim-templates/lib/decidim/templates/admin_engine.rb b/decidim-templates/lib/decidim/templates/admin_engine.rb index 2d26b6869b7ca..aae16c6a9657c 100644 --- a/decidim-templates/lib/decidim/templates/admin_engine.rb +++ b/decidim-templates/lib/decidim/templates/admin_engine.rb @@ -25,7 +25,8 @@ class AdminEngine < ::Rails::Engine resources :questionnaire_templates do member do post :copy - + get :edit_questions + patch :update_questions resource :questionnaire, module: :questionnaire_templates # To manage the templatable resource end diff --git a/decidim-templates/lib/decidim/templates/test/shared_examples/uses_questionnaire_templates.rb b/decidim-templates/lib/decidim/templates/test/shared_examples/uses_questionnaire_templates.rb index d884520763932..3dd1963e77862 100644 --- a/decidim-templates/lib/decidim/templates/test/shared_examples/uses_questionnaire_templates.rb +++ b/decidim-templates/lib/decidim/templates/test/shared_examples/uses_questionnaire_templates.rb @@ -46,7 +46,7 @@ description: {}, tos: {} ) - visit questionnaire_edit_path + click_on "Edit" end it "shows the template choosing screen" do @@ -87,7 +87,7 @@ description: {}, tos: {} ) - visit questionnaire_edit_path + click_on "Edit" select(template.name["en"], from: "select-template") @@ -96,14 +96,13 @@ end click_on "Create from template" + click_on "Save" end it "copies the template data to the questionnaire on submit" do - within "form.edit_questionnaire" do - click_on "Expand all" - expect(page.find_by_id("questionnaire_title_en").value).to eq(template.templatable.title["en"]) - expect(page.find("#questionnaire_questions_#{questionnaire_question.id}_body_en").value).to eq(question.body["en"]) - end + click_on "Manage questions" + click_on "Expand all" + expect(page.find("#questions_questions_#{questionnaire_question.id}_body_en").value).to eq(question.body["en"]) end end end diff --git a/decidim-templates/spec/system/admin/admin_adds_display_condition_on_questionnaire_templates_spec.rb b/decidim-templates/spec/system/admin/admin_adds_display_condition_on_questionnaire_templates_spec.rb index 4bf6a1d8eda1d..91bca7387df05 100644 --- a/decidim-templates/spec/system/admin/admin_adds_display_condition_on_questionnaire_templates_spec.rb +++ b/decidim-templates/spec/system/admin/admin_adds_display_condition_on_questionnaire_templates_spec.rb @@ -19,17 +19,17 @@ question_one = create(:questionnaire_question, mandatory: true, question_type: "matrix_single", rows: matrix_rows, options: answer_options, questionnaire:) question_two = create(:questionnaire_question, :with_answer_options, questionnaire:) - visit decidim_admin_templates.edit_questionnaire_path(template.id) + visit decidim_admin_templates.edit_questions_questionnaire_template_path(template.id) # expand question two find("[data-controls=panel-questionnaire_question_#{question_two.id}-question-card]").click # add display condition find(".add-display-condition").click # select question - select translated(question_one.body), from: "questionnaire[questions][#{question_two.id}][display_conditions][questionnaire-display-condition-id][decidim_condition_question_id]" + select translated(question_one.body), from: "questions[questions][#{question_two.id}][display_conditions][questionnaire-display-condition-id][decidim_condition_question_id]" # select equal - select "Equal", from: "questionnaire[questions][#{question_two.id}][display_conditions][questionnaire-display-condition-id][condition_type]" + select "Equal", from: "questions[questions][#{question_two.id}][display_conditions][questionnaire-display-condition-id][condition_type]" # validate we have the 2 answer options from question one in the select - select = find("#questionnaire_questions_#{question_two.id}_display_conditions_questionnaire-display-condition-id_decidim_answer_option_id") + select = find("#questions_questions_#{question_two.id}_display_conditions_questionnaire-display-condition-id_decidim_answer_option_id") expect(select).to have_content(translated(question_one.answer_options.first.body)) expect(select).to have_content(translated(question_one.answer_options.last.body)) end diff --git a/decidim-templates/spec/system/admin/admin_applies_questionnaire_template_to_component_spec.rb b/decidim-templates/spec/system/admin/admin_applies_questionnaire_template_to_component_spec.rb index cbbc366a670aa..89ea77c0dbadb 100644 --- a/decidim-templates/spec/system/admin/admin_applies_questionnaire_template_to_component_spec.rb +++ b/decidim-templates/spec/system/admin/admin_applies_questionnaire_template_to_component_spec.rb @@ -29,7 +29,8 @@ end click_on "Add component" - click_on "Survey" + click_on "Surveys" + click_on "New survey" select(translated_attribute(questionnaire_template.name), from: "select-template") expect(page).to have_content("If you are human, ignore this field") diff --git a/decidim-templates/spec/system/admin/admin_manages_questionnaire_templates_spec.rb b/decidim-templates/spec/system/admin/admin_manages_questionnaire_templates_spec.rb index a757324a2309d..100c33704e9ff 100644 --- a/decidim-templates/spec/system/admin/admin_manages_questionnaire_templates_spec.rb +++ b/decidim-templates/spec/system/admin/admin_manages_questionnaire_templates_spec.rb @@ -2,6 +2,9 @@ require "spec_helper" +require "decidim/forms/test/shared_examples/manage_questionnaires/add_questions" +require "decidim/forms/test/shared_examples/manage_questionnaires/update_questions" + describe "Admin manages questionnaire templates" do let!(:organization) { create(:organization) } let!(:user) { create(:user, :admin, :confirmed, organization:) } @@ -88,6 +91,34 @@ click_on "Save" expect(page).to have_admin_callout("successfully") end + + context "when the questionnaire is not already answered" do + let!(:template) { create(:questionnaire_template, organization:) } + let(:questionnaire) { template.templatable } + + let(:body) do + { + en: "This is the first question", + ca: "Aquesta es la primera pregunta", + es: "Esta es la primera pregunta" + } + end + + let(:title_and_description_body) do + { + en: "Este es el primer separador de texto", + ca: "Aquest és el primer separador de text", + es: "Esta es la primera pregunta" + } + end + + before do + visit decidim_admin_templates.edit_questions_questionnaire_template_path(template) + end + + it_behaves_like "add questions" + it_behaves_like "update questions" + end end describe "trying to create a questionnaire_template with invalid data" do @@ -226,13 +257,6 @@ ca: "Els meus termes" ) - click_on "Add question" - find(".button.expand-all").click - - within ".questionnaire-question" do - find("[id$=body_en]").fill_in(with: "My question") - end - find("*[type=submit]").click end @@ -240,22 +264,7 @@ within "[data-content]" do expect(page).to have_current_path decidim_admin_templates.edit_questionnaire_template_path(template) - expect(page).to have_content("My question") - end - end - - it "does not show preview or answers buttons" do - within ".layout-content" do - click_on("Edit") - end - - within "[data-content]" do - click_on("Edit") - end - - within ".item_show__header" do - expect(page).to have_no_button("Preview") - expect(page).to have_no_button("No answers yet") + expect(page).to have_content("Edit questionnaire template") end end end @@ -295,4 +304,55 @@ end end end + + private + + def find_nested_form_field_locator(attribute, visible: :visible) + find_nested_form_field(attribute, visible:)["id"] + end + + def find_nested_form_field(attribute, visible: :visible) + current_scope.find(nested_form_field_selector(attribute), visible:) + end + + def have_nested_field(attribute, with:) + have_field find_nested_form_field_locator(attribute), with: + end + + def have_no_nested_field(attribute, with:) + have_no_field(find_nested_form_field_locator(attribute), with:) + end + + def nested_form_field_selector(attribute) + "[id$=#{attribute}]" + end + + def within_add_display_condition + within ".questionnaire-question:last-of-type" do + click_on "Add display condition" + + within ".questionnaire-question-display-condition:last-of-type" do + yield + end + end + end + + def expand_all_questions + click_on "Expand all" + end + + def visit_manage_questions_and_expand_all + click_on "Manage questions" + expand_all_questions + end + + def update_component_settings_or_attributes; end + + def questionnaire_public_path + decidim_admin_templates.edit_questions_questionnaire_template_path(template) + end + + def see_questionnaire_questions + click_on "Expand all" + end end