decidim-forms 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/admin/decidim_forms_manifest.css +3 -0
  3. data/app/assets/config/decidim_forms_manifest.css +1 -0
  4. data/app/assets/javascripts/decidim/forms/admin/auto_select_options_from_url.component.js.es6 +40 -0
  5. data/app/assets/javascripts/decidim/forms/admin/forms.js.es6 +181 -25
  6. data/app/assets/javascripts/decidim/forms/autosortable_checkboxes.component.js.es6 +54 -34
  7. data/app/assets/javascripts/decidim/forms/display_conditions.component.js.es6 +204 -0
  8. data/app/assets/javascripts/decidim/forms/forms.js.es6 +8 -1
  9. data/app/assets/stylesheets/decidim/forms/questionnaire-answers-pdf.scss +69 -0
  10. data/app/commands/decidim/forms/admin/update_questionnaire.rb +25 -1
  11. data/app/commands/decidim/forms/answer_questionnaire.rb +1 -1
  12. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +47 -1
  13. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire_answers.rb +97 -0
  14. data/app/forms/decidim/forms/admin/display_condition_form.rb +100 -0
  15. data/app/forms/decidim/forms/admin/question_form.rb +1 -0
  16. data/app/forms/decidim/forms/answer_form.rb +15 -0
  17. data/app/helpers/decidim/forms/admin/application_helper.rb +21 -0
  18. data/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_pagination_helper.rb +49 -0
  19. data/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper.rb +40 -0
  20. data/app/helpers/decidim/forms/admin/questionnaire_answers_helper.rb +27 -0
  21. data/app/jobs/decidim/forms/export_questionnaire_answers_job.rb +19 -0
  22. data/app/models/decidim/forms/answer.rb +0 -3
  23. data/app/models/decidim/forms/answer_option.rb +14 -0
  24. data/app/models/decidim/forms/display_condition.rb +51 -0
  25. data/app/models/decidim/forms/question.rb +34 -0
  26. data/app/models/decidim/forms/question_matrix_row.rb +3 -0
  27. data/app/models/decidim/forms/questionnaire.rb +11 -1
  28. data/app/presenters/decidim/forms/admin/questionnaire_answer_presenter.rb +43 -0
  29. data/app/presenters/decidim/forms/admin/questionnaire_participant_presenter.rb +60 -0
  30. data/app/presenters/decidim/forms/answer_option_presenter.rb +20 -0
  31. data/app/presenters/decidim/forms/question_presenter.rb +16 -0
  32. data/app/queries/decidim/forms/questionnaire_participant.rb +35 -0
  33. data/app/queries/decidim/forms/questionnaire_participants.rb +43 -0
  34. data/app/views/decidim/forms/admin/questionnaires/_display_condition.html.erb +88 -0
  35. data/app/views/decidim/forms/admin/questionnaires/_display_condition_template.html.erb +7 -0
  36. data/app/views/decidim/forms/admin/questionnaires/_form.html.erb +30 -4
  37. data/app/views/decidim/forms/admin/questionnaires/_question.html.erb +19 -3
  38. data/app/views/decidim/forms/admin/questionnaires/answers/export/_answer.html.erb +31 -0
  39. data/app/views/decidim/forms/admin/questionnaires/answers/export/pdf.html.erb +13 -0
  40. data/app/views/decidim/forms/admin/questionnaires/answers/index.html.erb +53 -0
  41. data/app/views/decidim/forms/admin/questionnaires/answers/show.html.erb +48 -0
  42. data/app/views/decidim/forms/admin/questionnaires/edit.html.erb +9 -5
  43. data/app/views/decidim/forms/questionnaires/_answer.html.erb +1 -1
  44. data/app/views/decidim/forms/questionnaires/show.html.erb +22 -2
  45. data/app/views/layouts/decidim/forms/admin/questionnaires/questionnaire_answers.html.erb +12 -0
  46. data/config/initializers/wicked_pdf.rb +25 -0
  47. data/config/locales/am-ET.yml +1 -0
  48. data/config/locales/ar.yml +0 -1
  49. data/config/locales/bg-BG.yml +0 -2
  50. data/config/locales/bg.yml +14 -0
  51. data/config/locales/ca.yml +56 -4
  52. data/config/locales/cs.yml +54 -2
  53. data/config/locales/da.yml +1 -0
  54. data/config/locales/de.yml +51 -1
  55. data/config/locales/el.yml +0 -2
  56. data/config/locales/en.yml +54 -2
  57. data/config/locales/eo.yml +1 -0
  58. data/config/locales/es-MX.yml +53 -1
  59. data/config/locales/es-PY.yml +53 -1
  60. data/config/locales/es.yml +54 -2
  61. data/config/locales/et.yml +1 -0
  62. data/config/locales/eu.yml +0 -1
  63. data/config/locales/fi-plain.yml +53 -1
  64. data/config/locales/fi.yml +53 -1
  65. data/config/locales/fr-CA.yml +54 -2
  66. data/config/locales/fr.yml +54 -2
  67. data/config/locales/gl.yml +0 -1
  68. data/config/locales/hr.yml +1 -0
  69. data/config/locales/hu.yml +0 -2
  70. data/config/locales/id-ID.yml +0 -1
  71. data/config/locales/is.yml +1 -0
  72. data/config/locales/it.yml +53 -1
  73. data/config/locales/ja-JP.yml +51 -1
  74. data/config/locales/ja.yml +170 -0
  75. data/config/locales/ko-KR.yml +1 -0
  76. data/config/locales/ko.yml +1 -0
  77. data/config/locales/lt.yml +1 -0
  78. data/config/locales/{lv-LV.yml → lv.yml} +0 -1
  79. data/config/locales/mt.yml +1 -0
  80. data/config/locales/nl.yml +59 -7
  81. data/config/locales/no.yml +47 -2
  82. data/config/locales/om-ET.yml +1 -0
  83. data/config/locales/pl.yml +52 -3
  84. data/config/locales/pt-BR.yml +0 -1
  85. data/config/locales/pt.yml +51 -2
  86. data/config/locales/ro-RO.yml +51 -2
  87. data/config/locales/ru.yml +0 -1
  88. data/config/locales/sk.yml +0 -2
  89. data/config/locales/sl.yml +7 -0
  90. data/config/locales/so-SO.yml +1 -0
  91. data/config/locales/sv.yml +51 -2
  92. data/config/locales/ti-ER.yml +1 -0
  93. data/config/locales/tr-TR.yml +0 -1
  94. data/config/locales/vi-VN.yml +1 -0
  95. data/config/locales/vi.yml +1 -0
  96. data/config/locales/zh-CN.yml +172 -0
  97. data/config/locales/zh-TW.yml +1 -0
  98. data/db/migrate/20200130194123_create_decidim_forms_display_conditions.rb +20 -0
  99. data/lib/decidim/exporters/form_pdf.rb +33 -0
  100. data/lib/decidim/exporters/form_pdf_controller_helper.rb +11 -0
  101. data/lib/decidim/forms.rb +5 -0
  102. data/lib/decidim/forms/admin_engine.rb +1 -1
  103. data/lib/decidim/forms/test.rb +6 -0
  104. data/lib/decidim/forms/test/factories.rb +30 -0
  105. data/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +619 -38
  106. data/lib/decidim/forms/test/shared_examples/manage_questionnaire_answers.rb +108 -0
  107. data/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb +21 -894
  108. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_display_conditions.rb +179 -0
  109. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb +463 -0
  110. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_display_conditions.rb +93 -0
  111. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_questions.rb +461 -0
  112. data/lib/decidim/forms/version.rb +1 -1
  113. metadata +91 -11
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ # This class holds a Form to update questionnaire questions from Decidim's admin panel.
7
+ class DisplayConditionForm < Decidim::Form
8
+ include TranslatableAttributes
9
+
10
+ attribute :decidim_question_id, Integer
11
+ attribute :decidim_condition_question_id, Integer
12
+ attribute :decidim_answer_option_id, Integer
13
+ attribute :condition_type, String
14
+ attribute :mandatory, Boolean, default: false
15
+ attribute :deleted, Boolean, default: false
16
+
17
+ translatable_attribute :condition_value, String
18
+
19
+ validates :question, presence: true, unless: :deleted
20
+ validates :condition_question, presence: true, unless: :deleted
21
+ validates :answer_option, presence: true, if: :answer_option_mandatory?
22
+
23
+ validates :condition_value, translatable_presence: true, if: :condition_value_mandatory?
24
+ validates :condition_type, presence: true, unless: :deleted
25
+
26
+ validate :condition_question_position, unless: :deleted
27
+ validate :valid_answer_option?, unless: :deleted
28
+
29
+ def to_param
30
+ return id if id.present?
31
+
32
+ "questionnaire-display-condition-id"
33
+ end
34
+
35
+ def answer_options
36
+ return [] if condition_question.blank?
37
+
38
+ condition_question.answer_options
39
+ end
40
+
41
+ def questions_for_select(questionnaire, id)
42
+ questionnaire.questions.map do |question|
43
+ [
44
+ question.translated_body,
45
+ question.id,
46
+ {
47
+ "disabled" => (question.question_type == "sorting" || question.id == id),
48
+ "data-type" => question.question_type
49
+ }
50
+ ]
51
+ end
52
+ end
53
+
54
+ # Finds the Question from the given decidim_question_id
55
+ #
56
+ # Returns a Decidim::Forms::Question
57
+ def question
58
+ @question ||= Question.find_by(id: @decidim_question_id)
59
+ end
60
+
61
+ # Finds the Condition Question from the given decidim_condition_question_id
62
+ #
63
+ # Returns a Decidim::Forms::Question
64
+ def condition_question
65
+ @condition_question ||= Question.find_by(id: @decidim_condition_question_id)
66
+ end
67
+
68
+ # Finds the Answer Option from the given decidim_answer_option_id
69
+ #
70
+ # Returns a Decidim::Forms::AnswerOption
71
+ def answer_option
72
+ @answer_option ||= AnswerOption.find_by(id: @decidim_answer_option_id)
73
+ end
74
+
75
+ private
76
+
77
+ def condition_value_mandatory?
78
+ !deleted && condition_type == "match"
79
+ end
80
+
81
+ def answer_option_mandatory?
82
+ !deleted && %w(equal not_equal).include?(condition_type)
83
+ end
84
+
85
+ def valid_answer_option?
86
+ return unless answer_option_mandatory?
87
+ return if answer_option.blank?
88
+
89
+ errors.add(:decidim_answer_option_id, :invalid) if answer_option.question.id != decidim_condition_question_id
90
+ end
91
+
92
+ def condition_question_position
93
+ return if decidim_question_id.blank?
94
+
95
+ errors.add(:decidim_question_id, :invalid) unless question.position&.positive?
96
+ end
97
+ end
98
+ end
99
+ end
100
+ 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 :display_conditions, Array[DisplayConditionForm]
14
15
  attribute :matrix_rows, Array[QuestionMatrixRowForm]
15
16
  attribute :max_choices, Integer
16
17
  attribute :deleted, Boolean, default: false
@@ -49,8 +49,23 @@ module Decidim
49
49
  choices.select(&:body)
50
50
  end
51
51
 
52
+ def display_conditions_fulfilled?
53
+ question.display_conditions.all? do |condition|
54
+ answer = question.questionnaire.answers.find_by(question: condition.condition_question)
55
+ condition.fulfilled?(answer)
56
+ end
57
+ end
58
+
52
59
  private
53
60
 
61
+ def mandatory_body?
62
+ question.mandatory_body? if display_conditions_fulfilled?
63
+ end
64
+
65
+ def mandatory_choices?
66
+ question.mandatory_choices? if display_conditions_fulfilled?
67
+ end
68
+
54
69
  def grouped_choices
55
70
  selected_choices.group_by(&:matrix_row_id).values
56
71
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # i18n-tasks-use t('decidim.templates.admin.questionnaire_templates.form.title')
4
+
3
5
  module Decidim
4
6
  module Forms
5
7
  module Admin
@@ -14,6 +16,10 @@ module Decidim
14
16
  "questionnaire_question_#{question.to_param}_answer_option_#{answer_option.to_param}"
15
17
  end
16
18
 
19
+ def tabs_id_for_question_display_condition(question, display_condition)
20
+ "questionnaire_question_#{question.to_param}_display_condition_#{display_condition.to_param}"
21
+ end
22
+
17
23
  def tabs_id_for_question_matrix_row(question, matrix_row)
18
24
  "questionnaire_question_#{question.to_param}_matrix_row_#{matrix_row.to_param}"
19
25
  end
@@ -29,6 +35,21 @@ module Decidim
29
35
  truncate translated_attribute(title), length: options[:max_length], omission: options[:omission]
30
36
  end
31
37
  end
38
+
39
+ def template?(questionnaire_for)
40
+ return unless defined? Decidim::Templates::Template
41
+
42
+ questionnaire_for.is_a? Decidim::Templates::Template
43
+ end
44
+
45
+ def templates_defined?
46
+ defined? Decidim::Templates::Admin::Concerns::Templatable
47
+ end
48
+
49
+ def title_for_questionnaire
50
+ scope = templates_defined? ? "decidim.templates.admin.questionnaire_templates" : "decidim.forms.admin.questionnaires"
51
+ t("form.title", scope: scope)
52
+ end
32
53
  end
33
54
  end
34
55
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ module Concerns
7
+ # Pagination helper for HasQuestionnaireAnswers controller concern
8
+ #
9
+ module HasQuestionnaireAnswersPaginationHelper
10
+ def self.included(base)
11
+ base.helper_method :prev_url, :next_url, :first?, :last?, :current_idx
12
+ end
13
+
14
+ def prev_url
15
+ return if first?
16
+
17
+ token = participant_ids[current_idx - 1]
18
+ questionnaire_participant_answers_url(token)
19
+ end
20
+
21
+ def next_url
22
+ return if last?
23
+
24
+ token = participant_ids[current_idx + 1]
25
+ questionnaire_participant_answers_url(token)
26
+ end
27
+
28
+ def first?
29
+ current_idx.zero?
30
+ end
31
+
32
+ def last?
33
+ current_idx == participant_ids.count - 1
34
+ end
35
+
36
+ private
37
+
38
+ def participant_ids
39
+ @participant_ids ||= collection.pluck(:session_token)
40
+ end
41
+
42
+ def current_idx
43
+ participant_ids.index(params[:session_token])
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ module Concerns
7
+ # Url helper for HasQuestionnaireAnswers controller concern
8
+ #
9
+ module HasQuestionnaireAnswersUrlHelper
10
+ def self.included(base)
11
+ base.helper_method :questionnaire_url, :questionnaire_participants_url,
12
+ :questionnaire_participant_answers_url, :questionnaire_export_response_url
13
+ end
14
+
15
+ # You can implement this method in your controller to change the URL
16
+ # where the questionnaire can be edited.
17
+ def questionnaire_url
18
+ url_for(questionnaire.questionnaire_for)
19
+ end
20
+
21
+ # You can implement this method in your controller to change the URL
22
+ # where the questionnaire participants' info will be shown.
23
+ def questionnaire_participants_url
24
+ url_for([:index, questionnaire.questionnaire_for, format: nil])
25
+ end
26
+
27
+ # You can implement this method in your controller to change the URL
28
+ # where the user's questionnaire answers will be shown.
29
+ def questionnaire_participant_answers_url(session_token)
30
+ url_for([:show, questionnaire.questionnaire_for, session_token: session_token])
31
+ end
32
+
33
+ def questionnaire_export_response_url(session_token)
34
+ url_for([:export_response, questionnaire.questionnaire_for, session_token: session_token, format: "pdf"])
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ # Custom helpers for questionnaire answers
7
+ #
8
+ module QuestionnaireAnswersHelper
9
+ def first_table_th(answer)
10
+ return translated_attribute answer.first_short_answer.question.body if answer.first_short_answer
11
+
12
+ t("session_token", scope: "decidim.forms.user_answers_serializer")
13
+ end
14
+
15
+ def first_table_td(answer)
16
+ return answer.first_short_answer.body if answer.first_short_answer
17
+
18
+ answer.session_token
19
+ end
20
+
21
+ def display_percentage(number)
22
+ number_to_percentage(number, precision: 0, strip_insignificant_zeros: true, locale: I18n.locale)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ class ExportQuestionnaireAnswersJob < ApplicationJob
6
+ queue_as :default
7
+
8
+ def perform(user, title, answers)
9
+ return if user&.email.blank?
10
+ return if answers&.blank?
11
+
12
+ serializer = Decidim::Forms::UserAnswersSerializer
13
+ export_data = Decidim::Exporters::FormPDF.new(answers, serializer).export
14
+
15
+ ExportMailer.export(user, title, export_data).deliver_now
16
+ end
17
+ end
18
+ end
19
+ end
@@ -17,9 +17,6 @@ module Decidim
17
17
  dependent: :destroy,
18
18
  inverse_of: :answer
19
19
 
20
- validates :body, presence: true, if: -> { question.mandatory_body? }
21
- validates :choices, presence: true, if: -> { question.mandatory_choices? }
22
-
23
20
  validate :user_questionnaire_same_organization
24
21
  validate :question_belongs_to_questionnaire
25
22
 
@@ -3,9 +3,23 @@
3
3
  module Decidim
4
4
  module Forms
5
5
  class AnswerOption < Forms::ApplicationRecord
6
+ include Decidim::TranslatableResource
7
+
6
8
  default_scope { order(arel_table[:id].asc) }
7
9
 
10
+ translatable_fields :body
11
+
8
12
  belongs_to :question, class_name: "Question", foreign_key: "decidim_question_id"
13
+
14
+ has_many :display_conditions,
15
+ class_name: "DisplayCondition",
16
+ foreign_key: "decidim_answer_option_id",
17
+ dependent: :nullify,
18
+ inverse_of: :answer_option
19
+
20
+ def translated_body
21
+ Decidim::Forms::AnswerOptionPresenter.new(self).translated_body
22
+ end
9
23
  end
10
24
  end
11
25
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # The data store for a DisplayCondition in the Decidim::Forms component.
6
+ # A display condition is associated to two questions. :question is the question
7
+ # that we want to display or hide based on some conditions, and :condition_question
8
+ # is the question the answers of which are checked against the conditions.
9
+ # Conditions can be whether the question is answered ("answered") or it is not ("not_answered"),
10
+ # if the selected answer option is ("equal") or is not ("not_equal") a given one, or whethe
11
+ # the text value of the answer matches a string ("match").
12
+ class DisplayCondition < Forms::ApplicationRecord
13
+ enum condition_type: [:answered, :not_answered, :equal, :not_equal, :match], _prefix: true
14
+
15
+ # Question which will be displayed or hidden
16
+ belongs_to :question, class_name: "Question", foreign_key: "decidim_question_id", inverse_of: :display_conditions
17
+
18
+ # Question the answers of which are checked against conditions
19
+ belongs_to :condition_question, class_name: "Question", foreign_key: "decidim_condition_question_id", inverse_of: :display_conditions_for_other_questions
20
+
21
+ # Answer option provided to check for "equal" or "not_equal" (optional)
22
+ belongs_to :answer_option, class_name: "AnswerOption", foreign_key: "decidim_answer_option_id", optional: true
23
+
24
+ def fulfilled?(answer)
25
+ case condition_type
26
+ when "answered"
27
+ answer.present?
28
+ when "not_answered"
29
+ answer.blank?
30
+ when "equal"
31
+ answer.present? ? answer.choices.pluck(:decidim_answer_option_id).include?(answer_option.id) : false
32
+ when "not_equal"
33
+ answer.present? ? !answer.choices.pluck(:decidim_answer_option_id).include?(answer_option.id) : true
34
+ when "match"
35
+ answer.present? ? condition_value.values.any? { |value| answer.body.match?(Regexp.new(value, Regexp::IGNORECASE)) } : false
36
+ end
37
+ end
38
+
39
+ def to_html_data
40
+ {
41
+ id: id,
42
+ type: condition_type,
43
+ condition: decidim_condition_question_id,
44
+ option: decidim_answer_option_id,
45
+ mandatory: mandatory,
46
+ value: condition_value&.dig(I18n.locale.to_s)
47
+ }.compact
48
+ end
49
+ end
50
+ end
51
+ end
@@ -4,10 +4,14 @@ 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
+ include Decidim::TranslatableResource
8
+
7
9
  QUESTION_TYPES = %w(short_answer long_answer single_option multiple_option sorting matrix_single matrix_multiple).freeze
8
10
  SEPARATOR_TYPE = "separator"
9
11
  TYPES = (QUESTION_TYPES + [SEPARATOR_TYPE]).freeze
10
12
 
13
+ translatable_fields :body, :description
14
+
11
15
  belongs_to :questionnaire, class_name: "Questionnaire", foreign_key: "decidim_questionnaire_id"
12
16
 
13
17
  has_many :matrix_rows,
@@ -22,8 +26,34 @@ module Decidim
22
26
  dependent: :destroy,
23
27
  inverse_of: :question
24
28
 
29
+ # Conditions to display this question in questionnaire
30
+ has_many :display_conditions,
31
+ class_name: "DisplayCondition",
32
+ foreign_key: "decidim_question_id",
33
+ dependent: :destroy,
34
+ inverse_of: :question
35
+
36
+ # Conditions to display other questions based on the value of this question's answer
37
+ has_many :display_conditions_for_other_questions,
38
+ class_name: "DisplayCondition",
39
+ foreign_key: "decidim_condition_question_id",
40
+ dependent: :destroy,
41
+ inverse_of: :question
42
+
43
+ # Questions which have display conditions based on the value of this question's answer
44
+ has_many :conditioned_questions,
45
+ through: :display_conditions_for_other_questions,
46
+ foreign_key: "decidim_condition_question_id",
47
+ class_name: "Question"
48
+
25
49
  validates :question_type, inclusion: { in: TYPES }
26
50
 
51
+ scope :with_body, -> { where(question_type: %w(short_answer long_answer)) }
52
+ scope :with_choices, -> { where.not(question_type: %w(short_answer long_answer)) }
53
+
54
+ scope :conditioned, -> { includes(:display_conditions).where.not(decidim_forms_display_conditions: { id: nil }) }
55
+ scope :not_conditioned, -> { includes(:display_conditions).where(decidim_forms_display_conditions: { id: nil }) }
56
+
27
57
  def matrix?
28
58
  %w(matrix_single matrix_multiple).include?(question_type)
29
59
  end
@@ -44,6 +74,10 @@ module Decidim
44
74
  answer_options.size
45
75
  end
46
76
 
77
+ def translated_body
78
+ Decidim::Forms::QuestionPresenter.new(self).translated_body
79
+ end
80
+
47
81
  def separator?
48
82
  question_type.to_s == SEPARATOR_TYPE
49
83
  end