decidim-forms 0.19.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/config/admin/decidim_forms_manifest.js +1 -0
  4. data/app/assets/images/decidim/surveys/icon.svg +1 -19
  5. data/app/assets/javascripts/decidim/forms/admin/collapsible_questions.js.es6 +13 -0
  6. data/app/assets/javascripts/decidim/forms/admin/forms.js.es6 +95 -7
  7. data/app/assets/javascripts/decidim/forms/admin/live_text_update.component.js.es6 +52 -0
  8. data/app/assets/javascripts/decidim/forms/forms.js.es6 +42 -1
  9. data/app/assets/javascripts/decidim/forms/max_choices_alert.component.js.es6 +44 -0
  10. data/app/assets/stylesheets/decidim/forms/forms.scss +39 -0
  11. data/app/cells/decidim/forms/matrix_readonly/show.erb +5 -0
  12. data/app/cells/decidim/forms/matrix_readonly_cell.rb +12 -0
  13. data/app/cells/decidim/forms/question_readonly/show.erb +5 -1
  14. data/app/cells/decidim/forms/question_readonly_cell.rb +5 -0
  15. data/app/cells/decidim/forms/step_navigation/show.erb +35 -0
  16. data/app/cells/decidim/forms/step_navigation_cell.rb +46 -0
  17. data/app/commands/decidim/forms/admin/update_questionnaire.rb +8 -0
  18. data/app/commands/decidim/forms/answer_questionnaire.rb +7 -2
  19. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +6 -2
  20. data/app/controllers/decidim/forms/concerns/has_questionnaire.rb +58 -2
  21. data/app/forms/decidim/forms/admin/question_form.rb +20 -1
  22. data/app/forms/decidim/forms/admin/question_matrix_row_form.rb +26 -0
  23. data/app/forms/decidim/forms/answer_choice_form.rb +1 -0
  24. data/app/forms/decidim/forms/answer_form.rb +16 -2
  25. data/app/forms/decidim/forms/questionnaire_form.rb +33 -3
  26. data/app/helpers/decidim/forms/admin/application_helper.rb +16 -0
  27. data/app/models/decidim/forms/answer.rb +2 -2
  28. data/app/models/decidim/forms/answer_choice.rb +7 -0
  29. data/app/models/decidim/forms/question.rb +18 -2
  30. data/app/models/decidim/forms/question_matrix_row.rb +12 -0
  31. data/app/models/decidim/forms/questionnaire.rb +2 -1
  32. data/app/queries/decidim/forms/questionnaire_user_answers.rb +1 -1
  33. data/app/types/decidim/forms/answer_option_type.rb +14 -0
  34. data/app/types/decidim/forms/question_type.rb +23 -0
  35. data/app/types/decidim/forms/questionnaire_type.rb +22 -0
  36. data/app/views/decidim/forms/admin/questionnaires/_answer_option_template.html.erb +1 -1
  37. data/app/views/decidim/forms/admin/questionnaires/_form.html.erb +37 -4
  38. data/app/views/decidim/forms/admin/questionnaires/_matrix_row.html.erb +34 -0
  39. data/app/views/decidim/forms/admin/questionnaires/_matrix_row_template.html.erb +7 -0
  40. data/app/views/decidim/forms/admin/questionnaires/_question.html.erb +29 -6
  41. data/app/views/decidim/forms/admin/questionnaires/_separator.html.erb +41 -0
  42. data/app/views/decidim/forms/questionnaires/_answer.html.erb +21 -92
  43. data/app/views/decidim/forms/questionnaires/answers/_long_answer.html.erb +1 -0
  44. data/app/views/decidim/forms/questionnaires/answers/_matrix_multiple.html.erb +43 -0
  45. data/app/views/decidim/forms/questionnaires/answers/_matrix_single.html.erb +43 -0
  46. data/app/views/decidim/forms/questionnaires/answers/_multiple_option.html.erb +23 -0
  47. data/app/views/decidim/forms/questionnaires/answers/_separator.html.erb +1 -0
  48. data/app/views/decidim/forms/questionnaires/answers/_short_answer.html.erb +1 -0
  49. data/app/views/decidim/forms/questionnaires/answers/_single_option.html.erb +30 -0
  50. data/app/views/decidim/forms/questionnaires/answers/_sorting.html.erb +23 -0
  51. data/app/views/decidim/forms/questionnaires/show.html.erb +60 -27
  52. data/config/locales/ar.yml +12 -3
  53. data/config/locales/bg-BG.yml +16 -0
  54. data/config/locales/ca.yml +45 -7
  55. data/config/locales/cs.yml +41 -3
  56. data/config/locales/da-DK.yml +1 -0
  57. data/config/locales/de.yml +41 -3
  58. data/config/locales/el-GR.yml +1 -0
  59. data/config/locales/el.yml +120 -0
  60. data/config/locales/en.yml +41 -3
  61. data/config/locales/es-MX.yml +41 -3
  62. data/config/locales/es-PY.yml +41 -3
  63. data/config/locales/es.yml +41 -3
  64. data/config/locales/et-EE.yml +1 -0
  65. data/config/locales/eu.yml +7 -3
  66. data/config/locales/fi-plain.yml +41 -3
  67. data/config/locales/fi.yml +42 -4
  68. data/config/locales/fr-CA.yml +120 -0
  69. data/config/locales/fr.yml +41 -3
  70. data/config/locales/ga-IE.yml +1 -0
  71. data/config/locales/gl.yml +7 -3
  72. data/config/locales/hr-HR.yml +1 -0
  73. data/config/locales/hu.yml +18 -4
  74. data/config/locales/id-ID.yml +7 -3
  75. data/config/locales/is-IS.yml +1 -0
  76. data/config/locales/it.yml +42 -4
  77. data/config/locales/ja-JP.yml +120 -0
  78. data/config/locales/lt-LT.yml +1 -0
  79. data/config/locales/lv-LV.yml +119 -0
  80. data/config/locales/mt-MT.yml +1 -0
  81. data/config/locales/nl.yml +41 -3
  82. data/config/locales/no.yml +86 -1
  83. data/config/locales/pl.yml +63 -25
  84. data/config/locales/pt-BR.yml +8 -4
  85. data/config/locales/pt.yml +62 -24
  86. data/config/locales/ro-RO.yml +118 -0
  87. data/config/locales/ru.yml +47 -0
  88. data/config/locales/sk-SK.yml +88 -0
  89. data/config/locales/sk.yml +92 -0
  90. data/config/locales/sl.yml +5 -0
  91. data/config/locales/sr-CS.yml +1 -0
  92. data/config/locales/sv.yml +41 -3
  93. data/config/locales/tr-TR.yml +7 -3
  94. data/db/migrate/20190315203056_add_session_token_to_decidim_forms_answers.rb +17 -0
  95. data/db/migrate/20190930094710_add_ip_hash_to_decidim_form_answers.rb +12 -0
  96. data/db/migrate/20200225123810_create_decidim_forms_question_matrix_rows.rb +11 -0
  97. data/db/migrate/20200304152939_add_matrix_row_id_to_decidim_forms_answer_choices.rb +11 -0
  98. data/lib/decidim/api/questionnaire_entity_interface.rb +18 -0
  99. data/lib/decidim/forms.rb +1 -0
  100. data/lib/decidim/forms/api.rb +7 -0
  101. data/lib/decidim/forms/test/factories.rb +55 -11
  102. data/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +320 -43
  103. data/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb +347 -16
  104. data/lib/decidim/forms/user_answers_serializer.rb +23 -4
  105. data/lib/decidim/forms/version.rb +1 -1
  106. metadata +56 -9
@@ -0,0 +1,5 @@
1
+ <li class="questionnaire-question_readonly-answer">
2
+ <%= translated_attribute(model.body) %>
3
+
4
+ (<%= answer_options %>)
5
+ </li>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # This cell renders a possible matrix answer of a question (readonly)
6
+ class MatrixReadonlyCell < Decidim::ViewModel
7
+ def answer_options
8
+ model.question.answer_options.map { |option| translated_attribute(option.body) }.join(" / ")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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
 
@@ -24,16 +24,20 @@ module Decidim
24
24
  broadcast(:ok)
25
25
  end
26
26
 
27
+ attr_reader :form
28
+
27
29
  private
28
30
 
29
31
  def answer_questionnaire
30
32
  Answer.transaction do
31
- @form.answers.each do |form_answer|
33
+ form.responses_by_step.flatten.each do |form_answer|
32
34
  answer = Answer.new(
33
35
  user: @current_user,
34
36
  questionnaire: @questionnaire,
35
37
  question: form_answer.question,
36
- body: form_answer.body
38
+ body: form_answer.body,
39
+ session_token: form.context.session_token,
40
+ ip_hash: form.context.ip_hash
37
41
  )
38
42
 
39
43
  form_answer.selected_choices.each do |choice|
@@ -41,6 +45,7 @@ module Decidim
41
45
  body: choice.body,
42
46
  custom_body: choice.custom_body,
43
47
  decidim_answer_option_id: choice.answer_option_id,
48
+ decidim_question_matrix_row_id: choice.matrix_row_id,
44
49
  position: choice.position
45
50
  )
46
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,9 @@ module Decidim
16
16
  helper Decidim::Forms::ApplicationHelper
17
17
  include FormFactory
18
18
 
19
- helper_method :questionnaire_for, :questionnaire, :allow_answers?, :update_url
19
+ helper_method :questionnaire_for, :questionnaire, :allow_answers?, :visitor_can_answer?, :visitor_already_answered?, :update_url, :form_path
20
+
21
+ invisible_captcha on_spam: :spam_detected
20
22
 
21
23
  def show
22
24
  @form = form(Decidim::Forms::QuestionnaireForm).from_model(questionnaire)
@@ -26,7 +28,7 @@ module Decidim
26
28
  def answer
27
29
  enforce_permission_to :answer, :questionnaire
28
30
 
29
- @form = form(Decidim::Forms::QuestionnaireForm).from_params(params)
31
+ @form = form(Decidim::Forms::QuestionnaireForm).from_params(params, session_token: session_token, ip_hash: ip_hash)
30
32
 
31
33
  Decidim::Forms::AnswerQuestionnaire.call(@form, current_user, questionnaire) do
32
34
  on(:ok) do
@@ -49,6 +51,22 @@ module Decidim
49
51
  raise "#{self.class.name} is expected to implement #allow_answers?"
50
52
  end
51
53
 
54
+ # Public: Method to be implemented at the controller if needed. You need to
55
+ # return true if the questionnaire can receive answers by unregistered users
56
+ def allow_unregistered?
57
+ false
58
+ end
59
+
60
+ # Public: return true if the current user (if logged) can answer the questionnaire
61
+ def visitor_can_answer?
62
+ current_user || allow_unregistered?
63
+ end
64
+
65
+ # Public: return true if the current user (or session visitor) can answer the questionnaire
66
+ def visitor_already_answered?
67
+ questionnaire.answered_by?(current_user || tokenize(session[:session_id]))
68
+ end
69
+
52
70
  # Public: Returns a String or Object that will be passed to `redirect_to` after
53
71
  # answering the questionnaire. By default it redirects to the questionnaire_for.
54
72
  #
@@ -63,6 +81,15 @@ module Decidim
63
81
  url_for([questionnaire_for, action: :answer])
64
82
  end
65
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
+
66
93
  # Public: Method to be implemented at the controller. You need to
67
94
  # return the object that will hold the questionnaire.
68
95
  def questionnaire_for
@@ -78,6 +105,35 @@ module Decidim
78
105
  def questionnaire
79
106
  @questionnaire ||= Questionnaire.includes(questions: :answer_options).find_by(questionnaire_for: questionnaire_for)
80
107
  end
108
+
109
+ def spam_detected
110
+ enforce_permission_to :answer, :questionnaire
111
+
112
+ @form = form(Decidim::Forms::QuestionnaireForm).from_params(params)
113
+
114
+ flash.now[:alert] = I18n.t("answer.spam_detected", scope: i18n_flashes_scope)
115
+ render template: "decidim/forms/questionnaires/show"
116
+ end
117
+
118
+ def ip_hash
119
+ return nil unless request&.remote_ip
120
+
121
+ @ip_hash ||= tokenize(request&.remote_ip)
122
+ end
123
+
124
+ # token is used as a substitute of user_id if unregistered
125
+ def session_token
126
+ id = current_user&.id
127
+ session_id = request.session[:session_id] if request&.session
128
+
129
+ return nil unless id || session_id
130
+
131
+ @session_token ||= tokenize(id || session_id)
132
+ end
133
+
134
+ def tokenize(id)
135
+ Digest::MD5.hexdigest("#{id}-#{Rails.application.secrets.secret_key_base}")
136
+ end
81
137
  end
82
138
  end
83
139
  end
@@ -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,20 +4,50 @@ 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
13
+
11
14
  validates :tos_agreement, allow_nil: false, acceptance: true
15
+ validate :session_token_in_context
12
16
 
13
- # Private: Create the answers from the questionnaire questions
17
+ # Private: Create the responses from the questionnaire questions
14
18
  #
15
19
  # Returns nothing.
16
20
  def map_model(model)
17
- self.answers = model.questions.map do |question|
21
+ self.responses = model.questions.map do |question|
18
22
  AnswerForm.from_model(Decidim::Forms::Answer.new(question: question))
19
23
  end
20
24
  end
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
+
46
+ def session_token_in_context
47
+ return if context&.session_token
48
+
49
+ errors.add(:tos_agreement, I18n.t("activemodel.errors.models.questionnaire.request_invalid"))
50
+ end
21
51
  end
22
52
  end
23
53
  end