decidim-forms 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/admin/decidim_forms_manifest.js +1 -0
  3. data/app/assets/images/decidim/surveys/icon.svg +1 -19
  4. data/app/assets/javascripts/decidim/forms/admin/collapsible_questions.js.es6 +13 -0
  5. data/app/assets/javascripts/decidim/forms/admin/forms.js.es6 +95 -7
  6. data/app/assets/javascripts/decidim/forms/admin/live_text_update.component.js.es6 +52 -0
  7. data/app/assets/javascripts/decidim/forms/forms.js.es6 +42 -1
  8. data/app/assets/javascripts/decidim/forms/max_choices_alert.component.js.es6 +44 -0
  9. data/app/assets/stylesheets/decidim/forms/forms.scss +39 -0
  10. data/app/cells/decidim/forms/matrix_readonly/show.erb +5 -0
  11. data/app/cells/decidim/forms/matrix_readonly_cell.rb +12 -0
  12. data/app/cells/decidim/forms/question_readonly/show.erb +5 -1
  13. data/app/cells/decidim/forms/question_readonly_cell.rb +5 -0
  14. data/app/cells/decidim/forms/step_navigation/show.erb +35 -0
  15. data/app/cells/decidim/forms/step_navigation_cell.rb +46 -0
  16. data/app/commands/decidim/forms/admin/update_questionnaire.rb +8 -0
  17. data/app/commands/decidim/forms/answer_questionnaire.rb +2 -1
  18. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +6 -2
  19. data/app/controllers/decidim/forms/concerns/has_questionnaire.rb +11 -2
  20. data/app/forms/decidim/forms/admin/question_form.rb +20 -1
  21. data/app/forms/decidim/forms/admin/question_matrix_row_form.rb +26 -0
  22. data/app/forms/decidim/forms/answer_choice_form.rb +1 -0
  23. data/app/forms/decidim/forms/answer_form.rb +16 -2
  24. data/app/forms/decidim/forms/questionnaire_form.rb +25 -3
  25. data/app/helpers/decidim/forms/admin/application_helper.rb +16 -0
  26. data/app/models/decidim/forms/answer_choice.rb +7 -0
  27. data/app/models/decidim/forms/question.rb +18 -2
  28. data/app/models/decidim/forms/question_matrix_row.rb +12 -0
  29. data/app/views/decidim/forms/admin/questionnaires/_answer_option_template.html.erb +1 -1
  30. data/app/views/decidim/forms/admin/questionnaires/_form.html.erb +37 -4
  31. data/app/views/decidim/forms/admin/questionnaires/_matrix_row.html.erb +34 -0
  32. data/app/views/decidim/forms/admin/questionnaires/_matrix_row_template.html.erb +7 -0
  33. data/app/views/decidim/forms/admin/questionnaires/_question.html.erb +29 -6
  34. data/app/views/decidim/forms/admin/questionnaires/_separator.html.erb +41 -0
  35. data/app/views/decidim/forms/questionnaires/_answer.html.erb +21 -92
  36. data/app/views/decidim/forms/questionnaires/answers/_long_answer.html.erb +1 -0
  37. data/app/views/decidim/forms/questionnaires/answers/_matrix_multiple.html.erb +43 -0
  38. data/app/views/decidim/forms/questionnaires/answers/_matrix_single.html.erb +43 -0
  39. data/app/views/decidim/forms/questionnaires/answers/_multiple_option.html.erb +23 -0
  40. data/app/views/decidim/forms/questionnaires/answers/_separator.html.erb +1 -0
  41. data/app/views/decidim/forms/questionnaires/answers/_short_answer.html.erb +1 -0
  42. data/app/views/decidim/forms/questionnaires/answers/_single_option.html.erb +30 -0
  43. data/app/views/decidim/forms/questionnaires/answers/_sorting.html.erb +23 -0
  44. data/app/views/decidim/forms/questionnaires/show.html.erb +57 -25
  45. data/config/locales/ar.yml +7 -3
  46. data/config/locales/bg-BG.yml +16 -0
  47. data/config/locales/ca.yml +35 -3
  48. data/config/locales/cs.yml +35 -3
  49. data/config/locales/da-DK.yml +1 -0
  50. data/config/locales/de.yml +41 -3
  51. data/config/locales/el.yml +119 -0
  52. data/config/locales/en.yml +35 -3
  53. data/config/locales/es-MX.yml +35 -3
  54. data/config/locales/es-PY.yml +35 -3
  55. data/config/locales/es.yml +35 -3
  56. data/config/locales/et-EE.yml +1 -0
  57. data/config/locales/eu.yml +7 -3
  58. data/config/locales/fi-plain.yml +35 -3
  59. data/config/locales/fi.yml +36 -4
  60. data/config/locales/fr-CA.yml +120 -0
  61. data/config/locales/fr.yml +36 -4
  62. data/config/locales/ga-IE.yml +1 -0
  63. data/config/locales/gl.yml +7 -3
  64. data/config/locales/hr-HR.yml +1 -0
  65. data/config/locales/hu.yml +11 -3
  66. data/config/locales/id-ID.yml +7 -3
  67. data/config/locales/it.yml +36 -4
  68. data/config/locales/ja-JP.yml +120 -0
  69. data/config/locales/lt-LT.yml +1 -0
  70. data/config/locales/lv-LV.yml +119 -0
  71. data/config/locales/mt-MT.yml +1 -0
  72. data/config/locales/nl.yml +35 -3
  73. data/config/locales/no.yml +7 -3
  74. data/config/locales/pl.yml +63 -25
  75. data/config/locales/pt-BR.yml +8 -4
  76. data/config/locales/pt.yml +62 -24
  77. data/config/locales/ro-RO.yml +118 -0
  78. data/config/locales/ru.yml +4 -1
  79. data/config/locales/sk-SK.yml +88 -0
  80. data/config/locales/sk.yml +92 -0
  81. data/config/locales/sl.yml +5 -0
  82. data/config/locales/sr-CS.yml +1 -0
  83. data/config/locales/sv.yml +38 -6
  84. data/config/locales/tr-TR.yml +7 -3
  85. data/db/migrate/20200225123810_create_decidim_forms_question_matrix_rows.rb +11 -0
  86. data/db/migrate/20200304152939_add_matrix_row_id_to_decidim_forms_answer_choices.rb +11 -0
  87. data/lib/decidim/forms/test/factories.rb +20 -0
  88. data/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +313 -36
  89. data/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb +346 -15
  90. data/lib/decidim/forms/user_answers_serializer.rb +21 -4
  91. data/lib/decidim/forms/version.rb +1 -1
  92. metadata +45 -8
@@ -9,7 +9,11 @@
9
9
 
10
10
  <% if model.multiple_choice? %>
11
11
  <ul class="questionnaire-question_readonly-answers <%= model.question_type %>">
12
- <%= cell("decidim/forms/answer_readonly", collection: model.answer_options) %>
12
+ <% if model.matrix? %>
13
+ <%= cell("decidim/forms/matrix_readonly", collection: model.matrix_rows) %>
14
+ <% else %>
15
+ <%= cell("decidim/forms/answer_readonly", collection: model.answer_options) %>
16
+ <% end %>
13
17
  </ul>
14
18
  <% end %>
15
19
  </li>
@@ -4,6 +4,11 @@ module Decidim
4
4
  module Forms
5
5
  # This cell renders a question (readonly) of a questionnaire
6
6
  class QuestionReadonlyCell < Decidim::ViewModel
7
+ def show
8
+ return if model.separator?
9
+
10
+ render :show
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -0,0 +1,35 @@
1
+ <div class="button--double form-general-submit answer-questionnaire__submit">
2
+ <% if first_step? %>
3
+ <a></a>
4
+ <% else %>
5
+ <%= link_to(
6
+ icon("caret-left", class: "icon--small", role: "img") + " " + t("decidim.forms.step_navigation.show.back"),
7
+ "#",
8
+ class: "hollow secondary",
9
+ data: {
10
+ toggle: [previous_step_dom_id, current_step_dom_id].join(" ")
11
+ }
12
+ ) %>
13
+ <% end %>
14
+
15
+ <% if last_step? %>
16
+ <%= form.submit(
17
+ t("decidim.forms.step_navigation.show.submit"),
18
+ class: "button button--sc",
19
+ disabled: button_disabled?,
20
+ data: {
21
+ confirm: t("decidim.forms.step_navigation.show.are_you_sure"),
22
+ disable: true
23
+ }
24
+ ) %>
25
+ <% else %>
26
+ <%= link_to(
27
+ t("decidim.forms.step_navigation.show.continue"),
28
+ "#",
29
+ class: "button button--sc",
30
+ data: {
31
+ toggle: [next_step_dom_id, current_step_dom_id].join(" ")
32
+ }
33
+ ) %>
34
+ <% end %>
35
+ </div>
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # This cell renders the navigation of a questionnaire step.
6
+ class StepNavigationCell < Decidim::ViewModel
7
+ include Decidim::LayoutHelper
8
+
9
+ def current_step_index
10
+ model
11
+ end
12
+
13
+ def first_step?
14
+ current_step_index.zero?
15
+ end
16
+
17
+ def last_step?
18
+ current_step_index + 1 == total_steps
19
+ end
20
+
21
+ def total_steps
22
+ options[:total_steps]
23
+ end
24
+
25
+ def form
26
+ options[:form]
27
+ end
28
+
29
+ def button_disabled?
30
+ options[:button_disabled]
31
+ end
32
+
33
+ def previous_step_dom_id
34
+ "step-#{current_step_index - 1}"
35
+ end
36
+
37
+ def next_step_dom_id
38
+ "step-#{current_step_index + 1}"
39
+ end
40
+
41
+ def current_step_dom_id
42
+ "step-#{current_step_index}"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -56,6 +56,14 @@ module Decidim
56
56
 
57
57
  update_nested_model(form_answer_option, answer_option_attributes, question.answer_options)
58
58
  end
59
+
60
+ form_question.matrix_rows.each do |form_matrix_row|
61
+ matrix_row_attributes = {
62
+ body: form_matrix_row.body
63
+ }
64
+
65
+ update_nested_model(form_matrix_row, matrix_row_attributes, question.matrix_rows)
66
+ end
59
67
  end
60
68
  end
61
69
 
@@ -30,7 +30,7 @@ module Decidim
30
30
 
31
31
  def answer_questionnaire
32
32
  Answer.transaction do
33
- form.answers.each do |form_answer|
33
+ form.responses_by_step.flatten.each do |form_answer|
34
34
  answer = Answer.new(
35
35
  user: @current_user,
36
36
  questionnaire: @questionnaire,
@@ -45,6 +45,7 @@ module Decidim
45
45
  body: choice.body,
46
46
  custom_body: choice.custom_body,
47
47
  decidim_answer_option_id: choice.answer_option_id,
48
+ decidim_question_matrix_row_id: choice.matrix_row_id,
48
49
  position: choice.position
49
50
  )
50
51
  end
@@ -15,7 +15,7 @@ module Decidim
15
15
 
16
16
  included do
17
17
  helper Decidim::Forms::Admin::ApplicationHelper
18
- helper_method :questionnaire_for, :questionnaire, :blank_question, :blank_answer_option, :question_types, :update_url
18
+ helper_method :questionnaire_for, :questionnaire, :blank_question, :blank_answer_option, :blank_matrix_row, :question_types, :update_url
19
19
 
20
20
  def edit
21
21
  enforce_permission_to :update, :questionnaire, questionnaire: questionnaire
@@ -82,8 +82,12 @@ module Decidim
82
82
  @blank_answer_option ||= Admin::AnswerOptionForm.new
83
83
  end
84
84
 
85
+ def blank_matrix_row
86
+ @blank_matrix_row ||= Admin::QuestionMatrixRowForm.new
87
+ end
88
+
85
89
  def question_types
86
- @question_types ||= Question::TYPES.map do |question_type|
90
+ @question_types ||= Question::QUESTION_TYPES.map do |question_type|
87
91
  [question_type, I18n.t("decidim.forms.question_types.#{question_type}")]
88
92
  end
89
93
  end
@@ -16,7 +16,7 @@ module Decidim
16
16
  helper Decidim::Forms::ApplicationHelper
17
17
  include FormFactory
18
18
 
19
- helper_method :questionnaire_for, :questionnaire, :allow_answers?, :visitor_can_answer?, :visitor_already_answered?, :update_url
19
+ helper_method :questionnaire_for, :questionnaire, :allow_answers?, :visitor_can_answer?, :visitor_already_answered?, :update_url, :form_path
20
20
 
21
21
  invisible_captcha on_spam: :spam_detected
22
22
 
@@ -28,7 +28,7 @@ module Decidim
28
28
  def answer
29
29
  enforce_permission_to :answer, :questionnaire
30
30
 
31
- @form = form(Decidim::Forms::QuestionnaireForm).from_params(params, session_token: session_token)
31
+ @form = form(Decidim::Forms::QuestionnaireForm).from_params(params, session_token: session_token, ip_hash: ip_hash)
32
32
 
33
33
  Decidim::Forms::AnswerQuestionnaire.call(@form, current_user, questionnaire) do
34
34
  on(:ok) do
@@ -81,6 +81,15 @@ module Decidim
81
81
  url_for([questionnaire_for, action: :answer])
82
82
  end
83
83
 
84
+ # Points to the shortest path accessing the current form. This will be
85
+ # used to detect whether a user is leaving the form with some partial
86
+ # answers, so that we can warn them.
87
+ #
88
+ # Overwrite this method at the controller.
89
+ def form_path
90
+ url_for([questionnaire_for, only_path: true])
91
+ end
92
+
84
93
  # Public: Method to be implemented at the controller. You need to
85
94
  # return the object that will hold the questionnaire.
86
95
  def questionnaire_for
@@ -11,6 +11,7 @@ module Decidim
11
11
  attribute :mandatory, Boolean, default: false
12
12
  attribute :question_type, String
13
13
  attribute :answer_options, Array[AnswerOptionForm]
14
+ attribute :matrix_rows, Array[QuestionMatrixRowForm]
14
15
  attribute :max_choices, Integer
15
16
  attribute :deleted, Boolean, default: false
16
17
 
@@ -20,7 +21,9 @@ module Decidim
20
21
  validates :position, numericality: { greater_than_or_equal_to: 0 }
21
22
  validates :question_type, inclusion: { in: Decidim::Forms::Question::TYPES }
22
23
  validates :max_choices, numericality: { only_integer: true, greater_than: 1, less_than_or_equal_to: ->(form) { form.number_of_options } }, allow_blank: true
23
- validates :body, translatable_presence: true, unless: :deleted
24
+ validates :body, translatable_presence: true, if: :requires_body?
25
+ validates :matrix_rows, presence: true, if: :matrix?
26
+ validates :answer_options, presence: true, if: :matrix?
24
27
 
25
28
  def to_param
26
29
  return id if id.present?
@@ -31,6 +34,22 @@ module Decidim
31
34
  def number_of_options
32
35
  answer_options.size
33
36
  end
37
+
38
+ def separator?
39
+ question_type == Decidim::Forms::Question::SEPARATOR_TYPE
40
+ end
41
+
42
+ private
43
+
44
+ def matrix?
45
+ question_type == "matrix_single" || question_type == "matrix_multiple"
46
+ end
47
+
48
+ def requires_body?
49
+ return false if separator?
50
+
51
+ !deleted
52
+ end
34
53
  end
35
54
  end
36
55
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ # This class holds a Form to update questionnaire question matrixes from Decidim's admin panel.
7
+ class QuestionMatrixRowForm < Decidim::Form
8
+ include TranslatableAttributes
9
+
10
+ attribute :position, Integer, default: 0
11
+ attribute :deleted, Boolean, default: false
12
+
13
+ translatable_attribute :body, String
14
+
15
+ validates :position, numericality: { greater_than_or_equal_to: 0 }
16
+ validates :body, translatable_presence: true, unless: :deleted
17
+
18
+ def to_param
19
+ return id if id.present?
20
+
21
+ "questionnaire-question-matrix-row-id"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -8,6 +8,7 @@ module Decidim
8
8
  attribute :custom_body, String
9
9
  attribute :position, Integer
10
10
  attribute :answer_option_id, Integer
11
+ attribute :matrix_row_id, Integer
11
12
 
12
13
  validates :answer_option_id, presence: true
13
14
  end
@@ -9,14 +9,16 @@ module Decidim
9
9
  attribute :question_id, String
10
10
  attribute :body, String
11
11
  attribute :choices, Array[AnswerChoiceForm]
12
+ attribute :matrix_choices, Array[AnswerChoiceForm]
12
13
 
13
14
  validates :body, presence: true, if: :mandatory_body?
14
15
  validates :selected_choices, presence: true, if: :mandatory_choices?
15
16
 
16
17
  validate :max_choices, if: -> { question.max_choices }
17
18
  validate :all_choices, if: -> { question.question_type == "sorting" }
19
+ validate :min_choices, if: -> { question.matrix? && question.mandatory? }
18
20
 
19
- delegate :mandatory_body?, :mandatory_choices?, to: :question
21
+ delegate :mandatory_body?, :mandatory_choices?, :matrix?, to: :question
20
22
 
21
23
  attr_writer :question
22
24
 
@@ -49,8 +51,20 @@ module Decidim
49
51
 
50
52
  private
51
53
 
54
+ def grouped_choices
55
+ selected_choices.group_by(&:matrix_row_id).values
56
+ end
57
+
52
58
  def max_choices
53
- errors.add(:choices, :too_many) if selected_choices.size > question.max_choices
59
+ if matrix?
60
+ errors.add(:choices, :too_many) if grouped_choices.any? { |choices| choices.count > question.max_choices }
61
+ elsif selected_choices.size > question.max_choices
62
+ errors.add(:choices, :too_many)
63
+ end
64
+ end
65
+
66
+ def min_choices
67
+ errors.add(:choices, :missing) if grouped_choices.count != question.matrix_rows.count
54
68
  end
55
69
 
56
70
  def all_choices
@@ -4,7 +4,9 @@ module Decidim
4
4
  module Forms
5
5
  # This class holds a Form to answer a questionnaire from Decidim's public page.
6
6
  class QuestionnaireForm < Decidim::Form
7
- attribute :answers, Array[AnswerForm]
7
+ # as questionnaire uses "answers" for the database relationships is
8
+ # important not to use the same word here to avoid querying all the entries, resulting in a high performance penalty
9
+ attribute :responses, Array[AnswerForm]
8
10
  attribute :user_group_id, Integer
9
11
 
10
12
  attribute :tos_agreement, Boolean
@@ -12,15 +14,35 @@ module Decidim
12
14
  validates :tos_agreement, allow_nil: false, acceptance: true
13
15
  validate :session_token_in_context
14
16
 
15
- # Private: Create the answers from the questionnaire questions
17
+ # Private: Create the responses from the questionnaire questions
16
18
  #
17
19
  # Returns nothing.
18
20
  def map_model(model)
19
- self.answers = model.questions.map do |question|
21
+ self.responses = model.questions.map do |question|
20
22
  AnswerForm.from_model(Decidim::Forms::Answer.new(question: question))
21
23
  end
22
24
  end
23
25
 
26
+ # Public: Splits reponses by step, keeping the separator.
27
+ #
28
+ # Returns an array of steps. Each step is a list of the questions in that
29
+ # step, including the separator.
30
+ def responses_by_step
31
+ @responses_by_step ||=
32
+ begin
33
+ steps = responses.chunk_while do |a, b|
34
+ !a.question.separator? || b.question.separator?
35
+ end.to_a
36
+
37
+ steps = [[]] if steps == []
38
+ steps
39
+ end
40
+ end
41
+
42
+ def total_steps
43
+ responses_by_step.count
44
+ end
45
+
24
46
  def session_token_in_context
25
47
  return if context&.session_token
26
48
 
@@ -13,6 +13,22 @@ module Decidim
13
13
  def tabs_id_for_question_answer_option(question, answer_option)
14
14
  "questionnaire_question_#{question.to_param}_answer_option_#{answer_option.to_param}"
15
15
  end
16
+
17
+ def tabs_id_for_question_matrix_row(question, matrix_row)
18
+ "questionnaire_question_#{question.to_param}_matrix_row_#{matrix_row.to_param}"
19
+ end
20
+
21
+ def dynamic_title(title, **options)
22
+ data = {
23
+ "max-length" => options[:max_length],
24
+ "omission" => options[:omission],
25
+ "placeholder" => options[:placeholder],
26
+ "locale" => I18n.locale
27
+ }
28
+ content_tag :span, class: options[:class], data: data do
29
+ truncate translated_attribute(title), length: options[:max_length], omission: options[:omission]
30
+ end
31
+ end
16
32
  end
17
33
  end
18
34
  end
@@ -10,6 +10,13 @@ module Decidim
10
10
  belongs_to :answer_option,
11
11
  class_name: "AnswerOption",
12
12
  foreign_key: "decidim_answer_option_id"
13
+
14
+ belongs_to :matrix_row,
15
+ class_name: "QuestionMatrixRow",
16
+ foreign_key: "decidim_question_matrix_row_id",
17
+ optional: true
18
+
19
+ validates :matrix_row, presence: true, if: -> { answer.question.matrix? }
13
20
  end
14
21
  end
15
22
  end
@@ -4,10 +4,18 @@ module Decidim
4
4
  module Forms
5
5
  # The data store for a Question in the Decidim::Forms component.
6
6
  class Question < Forms::ApplicationRecord
7
- TYPES = %w(short_answer long_answer single_option multiple_option sorting).freeze
7
+ QUESTION_TYPES = %w(short_answer long_answer single_option multiple_option sorting matrix_single matrix_multiple).freeze
8
+ SEPARATOR_TYPE = "separator"
9
+ TYPES = (QUESTION_TYPES + [SEPARATOR_TYPE]).freeze
8
10
 
9
11
  belongs_to :questionnaire, class_name: "Questionnaire", foreign_key: "decidim_questionnaire_id"
10
12
 
13
+ has_many :matrix_rows,
14
+ class_name: "QuestionMatrixRow",
15
+ foreign_key: "decidim_question_id",
16
+ dependent: :destroy,
17
+ inverse_of: :question
18
+
11
19
  has_many :answer_options,
12
20
  class_name: "AnswerOption",
13
21
  foreign_key: "decidim_question_id",
@@ -16,8 +24,12 @@ module Decidim
16
24
 
17
25
  validates :question_type, inclusion: { in: TYPES }
18
26
 
27
+ def matrix?
28
+ %w(matrix_single matrix_multiple).include?(question_type)
29
+ end
30
+
19
31
  def multiple_choice?
20
- %w(single_option multiple_option sorting).include?(question_type)
32
+ %w(single_option multiple_option sorting matrix_single matrix_multiple).include?(question_type)
21
33
  end
22
34
 
23
35
  def mandatory_body?
@@ -31,6 +43,10 @@ module Decidim
31
43
  def number_of_options
32
44
  answer_options.size
33
45
  end
46
+
47
+ def separator?
48
+ question_type.to_s == SEPARATOR_TYPE
49
+ end
34
50
  end
35
51
  end
36
52
  end