decidim-forms 0.19.1 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/config/admin/decidim_forms_manifest.css +3 -0
  4. data/app/assets/config/admin/decidim_forms_manifest.js +1 -0
  5. data/app/assets/config/decidim_forms_manifest.css +1 -0
  6. data/app/assets/images/decidim/surveys/icon.svg +1 -19
  7. data/app/assets/javascripts/decidim/forms/admin/auto_select_options_from_url.component.js.es6 +40 -0
  8. data/app/assets/javascripts/decidim/forms/admin/collapsible_questions.js.es6 +13 -0
  9. data/app/assets/javascripts/decidim/forms/admin/forms.js.es6 +260 -16
  10. data/app/assets/javascripts/decidim/forms/admin/live_text_update.component.js.es6 +52 -0
  11. data/app/assets/javascripts/decidim/forms/autosortable_checkboxes.component.js.es6 +54 -34
  12. data/app/assets/javascripts/decidim/forms/display_conditions.component.js.es6 +204 -0
  13. data/app/assets/javascripts/decidim/forms/forms.js.es6 +49 -1
  14. data/app/assets/javascripts/decidim/forms/max_choices_alert.component.js.es6 +44 -0
  15. data/app/assets/stylesheets/decidim/forms/forms.scss +39 -0
  16. data/app/assets/stylesheets/decidim/forms/questionnaire-answers-pdf.scss +69 -0
  17. data/app/cells/decidim/forms/matrix_readonly/show.erb +5 -0
  18. data/app/cells/decidim/forms/matrix_readonly_cell.rb +12 -0
  19. data/app/cells/decidim/forms/question_readonly/show.erb +5 -1
  20. data/app/cells/decidim/forms/question_readonly_cell.rb +5 -0
  21. data/app/cells/decidim/forms/step_navigation/show.erb +35 -0
  22. data/app/cells/decidim/forms/step_navigation_cell.rb +46 -0
  23. data/app/commands/decidim/forms/admin/update_questionnaire.rb +33 -1
  24. data/app/commands/decidim/forms/answer_questionnaire.rb +7 -2
  25. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +52 -2
  26. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire_answers.rb +97 -0
  27. data/app/controllers/decidim/forms/concerns/has_questionnaire.rb +58 -2
  28. data/app/forms/decidim/forms/admin/display_condition_form.rb +100 -0
  29. data/app/forms/decidim/forms/admin/question_form.rb +21 -1
  30. data/app/forms/decidim/forms/admin/question_matrix_row_form.rb +26 -0
  31. data/app/forms/decidim/forms/answer_choice_form.rb +1 -0
  32. data/app/forms/decidim/forms/answer_form.rb +31 -2
  33. data/app/forms/decidim/forms/questionnaire_form.rb +33 -3
  34. data/app/helpers/decidim/forms/admin/application_helper.rb +37 -0
  35. data/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_pagination_helper.rb +49 -0
  36. data/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper.rb +40 -0
  37. data/app/helpers/decidim/forms/admin/questionnaire_answers_helper.rb +27 -0
  38. data/app/jobs/decidim/forms/export_questionnaire_answers_job.rb +19 -0
  39. data/app/models/decidim/forms/answer.rb +2 -5
  40. data/app/models/decidim/forms/answer_choice.rb +7 -0
  41. data/app/models/decidim/forms/answer_option.rb +14 -0
  42. data/app/models/decidim/forms/display_condition.rb +51 -0
  43. data/app/models/decidim/forms/question.rb +52 -2
  44. data/app/models/decidim/forms/question_matrix_row.rb +15 -0
  45. data/app/models/decidim/forms/questionnaire.rb +13 -2
  46. data/app/presenters/decidim/forms/admin/questionnaire_answer_presenter.rb +43 -0
  47. data/app/presenters/decidim/forms/admin/questionnaire_participant_presenter.rb +60 -0
  48. data/app/presenters/decidim/forms/answer_option_presenter.rb +20 -0
  49. data/app/presenters/decidim/forms/question_presenter.rb +16 -0
  50. data/app/queries/decidim/forms/questionnaire_participant.rb +35 -0
  51. data/app/queries/decidim/forms/questionnaire_participants.rb +43 -0
  52. data/app/queries/decidim/forms/questionnaire_user_answers.rb +1 -1
  53. data/app/types/decidim/forms/answer_option_type.rb +14 -0
  54. data/app/types/decidim/forms/question_type.rb +23 -0
  55. data/app/types/decidim/forms/questionnaire_type.rb +22 -0
  56. data/app/views/decidim/forms/admin/questionnaires/_answer_option_template.html.erb +1 -1
  57. data/app/views/decidim/forms/admin/questionnaires/_display_condition.html.erb +88 -0
  58. data/app/views/decidim/forms/admin/questionnaires/_display_condition_template.html.erb +7 -0
  59. data/app/views/decidim/forms/admin/questionnaires/_form.html.erb +67 -8
  60. data/app/views/decidim/forms/admin/questionnaires/_matrix_row.html.erb +34 -0
  61. data/app/views/decidim/forms/admin/questionnaires/_matrix_row_template.html.erb +7 -0
  62. data/app/views/decidim/forms/admin/questionnaires/_question.html.erb +45 -6
  63. data/app/views/decidim/forms/admin/questionnaires/_separator.html.erb +41 -0
  64. data/app/views/decidim/forms/admin/questionnaires/answers/export/_answer.html.erb +31 -0
  65. data/app/views/decidim/forms/admin/questionnaires/answers/export/pdf.html.erb +13 -0
  66. data/app/views/decidim/forms/admin/questionnaires/answers/index.html.erb +53 -0
  67. data/app/views/decidim/forms/admin/questionnaires/answers/show.html.erb +48 -0
  68. data/app/views/decidim/forms/admin/questionnaires/edit.html.erb +9 -5
  69. data/app/views/decidim/forms/questionnaires/_answer.html.erb +28 -99
  70. data/app/views/decidim/forms/questionnaires/answers/_long_answer.html.erb +1 -0
  71. data/app/views/decidim/forms/questionnaires/answers/_matrix_multiple.html.erb +43 -0
  72. data/app/views/decidim/forms/questionnaires/answers/_matrix_single.html.erb +43 -0
  73. data/app/views/decidim/forms/questionnaires/answers/_multiple_option.html.erb +23 -0
  74. data/app/views/decidim/forms/questionnaires/answers/_separator.html.erb +1 -0
  75. data/app/views/decidim/forms/questionnaires/answers/_short_answer.html.erb +1 -0
  76. data/app/views/decidim/forms/questionnaires/answers/_single_option.html.erb +30 -0
  77. data/app/views/decidim/forms/questionnaires/answers/_sorting.html.erb +23 -0
  78. data/app/views/decidim/forms/questionnaires/show.html.erb +81 -28
  79. data/app/views/layouts/decidim/forms/admin/questionnaires/questionnaire_answers.html.erb +12 -0
  80. data/config/initializers/wicked_pdf.rb +25 -0
  81. data/config/locales/am-ET.yml +1 -0
  82. data/config/locales/ar.yml +12 -4
  83. data/config/locales/bg-BG.yml +14 -0
  84. data/config/locales/bg.yml +14 -0
  85. data/config/locales/ca.yml +98 -8
  86. data/config/locales/cs.yml +94 -4
  87. data/config/locales/da-DK.yml +1 -0
  88. data/config/locales/da.yml +1 -0
  89. data/config/locales/de.yml +92 -4
  90. data/config/locales/el-GR.yml +1 -0
  91. data/config/locales/el.yml +118 -0
  92. data/config/locales/en.yml +94 -4
  93. data/config/locales/eo.yml +1 -0
  94. data/config/locales/es-MX.yml +94 -4
  95. data/config/locales/es-PY.yml +94 -4
  96. data/config/locales/es.yml +94 -4
  97. data/config/locales/et-EE.yml +1 -0
  98. data/config/locales/et.yml +1 -0
  99. data/config/locales/eu.yml +7 -4
  100. data/config/locales/fi-plain.yml +94 -4
  101. data/config/locales/fi.yml +94 -4
  102. data/config/locales/fr-CA.yml +172 -0
  103. data/config/locales/fr.yml +94 -4
  104. data/config/locales/ga-IE.yml +1 -0
  105. data/config/locales/gl.yml +7 -4
  106. data/config/locales/hr-HR.yml +1 -0
  107. data/config/locales/hr.yml +1 -0
  108. data/config/locales/hu.yml +16 -4
  109. data/config/locales/id-ID.yml +7 -4
  110. data/config/locales/is-IS.yml +1 -0
  111. data/config/locales/is.yml +1 -0
  112. data/config/locales/it.yml +95 -5
  113. data/config/locales/ja-JP.yml +170 -0
  114. data/config/locales/ja.yml +170 -0
  115. data/config/locales/ko-KR.yml +1 -0
  116. data/config/locales/ko.yml +1 -0
  117. data/config/locales/lt-LT.yml +1 -0
  118. data/config/locales/lt.yml +1 -0
  119. data/config/locales/lv.yml +118 -0
  120. data/config/locales/mt-MT.yml +1 -0
  121. data/config/locales/mt.yml +1 -0
  122. data/config/locales/nl.yml +97 -7
  123. data/config/locales/no.yml +98 -1
  124. data/config/locales/om-ET.yml +1 -0
  125. data/config/locales/pl.yml +113 -26
  126. data/config/locales/pt-BR.yml +8 -5
  127. data/config/locales/pt.yml +111 -24
  128. data/config/locales/ro-RO.yml +167 -0
  129. data/config/locales/ru.yml +4 -2
  130. data/config/locales/sk-SK.yml +88 -0
  131. data/config/locales/sk.yml +90 -0
  132. data/config/locales/sl.yml +12 -0
  133. data/config/locales/so-SO.yml +1 -0
  134. data/config/locales/sr-CS.yml +1 -0
  135. data/config/locales/sv.yml +91 -4
  136. data/config/locales/ti-ER.yml +1 -0
  137. data/config/locales/tr-TR.yml +7 -4
  138. data/config/locales/vi-VN.yml +1 -0
  139. data/config/locales/vi.yml +1 -0
  140. data/config/locales/zh-CN.yml +172 -0
  141. data/config/locales/zh-TW.yml +1 -0
  142. data/db/migrate/20190315203056_add_session_token_to_decidim_forms_answers.rb +17 -0
  143. data/db/migrate/20190930094710_add_ip_hash_to_decidim_form_answers.rb +12 -0
  144. data/db/migrate/20200130194123_create_decidim_forms_display_conditions.rb +20 -0
  145. data/db/migrate/20200225123810_create_decidim_forms_question_matrix_rows.rb +11 -0
  146. data/db/migrate/20200304152939_add_matrix_row_id_to_decidim_forms_answer_choices.rb +11 -0
  147. data/lib/decidim/api/questionnaire_entity_interface.rb +18 -0
  148. data/lib/decidim/exporters/form_pdf.rb +33 -0
  149. data/lib/decidim/exporters/form_pdf_controller_helper.rb +11 -0
  150. data/lib/decidim/forms.rb +6 -0
  151. data/lib/decidim/forms/admin_engine.rb +1 -1
  152. data/lib/decidim/forms/api.rb +7 -0
  153. data/lib/decidim/forms/test.rb +6 -0
  154. data/lib/decidim/forms/test/factories.rb +80 -6
  155. data/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +924 -66
  156. data/lib/decidim/forms/test/shared_examples/manage_questionnaire_answers.rb +108 -0
  157. data/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb +33 -575
  158. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_display_conditions.rb +179 -0
  159. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb +463 -0
  160. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_display_conditions.rb +93 -0
  161. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_questions.rb +461 -0
  162. data/lib/decidim/forms/user_answers_serializer.rb +23 -4
  163. data/lib/decidim/forms/version.rb +1 -1
  164. metadata +136 -9
@@ -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
@@ -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,8 @@ 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]
15
+ attribute :matrix_rows, Array[QuestionMatrixRowForm]
14
16
  attribute :max_choices, Integer
15
17
  attribute :deleted, Boolean, default: false
16
18
 
@@ -20,7 +22,9 @@ module Decidim
20
22
  validates :position, numericality: { greater_than_or_equal_to: 0 }
21
23
  validates :question_type, inclusion: { in: Decidim::Forms::Question::TYPES }
22
24
  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
25
+ validates :body, translatable_presence: true, if: :requires_body?
26
+ validates :matrix_rows, presence: true, if: :matrix?
27
+ validates :answer_options, presence: true, if: :matrix?
24
28
 
25
29
  def to_param
26
30
  return id if id.present?
@@ -31,6 +35,22 @@ module Decidim
31
35
  def number_of_options
32
36
  answer_options.size
33
37
  end
38
+
39
+ def separator?
40
+ question_type == Decidim::Forms::Question::SEPARATOR_TYPE
41
+ end
42
+
43
+ private
44
+
45
+ def matrix?
46
+ question_type == "matrix_single" || question_type == "matrix_multiple"
47
+ end
48
+
49
+ def requires_body?
50
+ return false if separator?
51
+
52
+ !deleted
53
+ end
34
54
  end
35
55
  end
36
56
  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
 
@@ -47,10 +49,37 @@ module Decidim
47
49
  choices.select(&:body)
48
50
  end
49
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
+
50
59
  private
51
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
+
69
+ def grouped_choices
70
+ selected_choices.group_by(&:matrix_row_id).values
71
+ end
72
+
52
73
  def max_choices
53
- errors.add(:choices, :too_many) if selected_choices.size > question.max_choices
74
+ if matrix?
75
+ errors.add(:choices, :too_many) if grouped_choices.any? { |choices| choices.count > question.max_choices }
76
+ elsif selected_choices.size > question.max_choices
77
+ errors.add(:choices, :too_many)
78
+ end
79
+ end
80
+
81
+ def min_choices
82
+ errors.add(:choices, :missing) if grouped_choices.count != question.matrix_rows.count
54
83
  end
55
84
 
56
85
  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
@@ -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
@@ -13,6 +15,41 @@ module Decidim
13
15
  def tabs_id_for_question_answer_option(question, answer_option)
14
16
  "questionnaire_question_#{question.to_param}_answer_option_#{answer_option.to_param}"
15
17
  end
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
+
23
+ def tabs_id_for_question_matrix_row(question, matrix_row)
24
+ "questionnaire_question_#{question.to_param}_matrix_row_#{matrix_row.to_param}"
25
+ end
26
+
27
+ def dynamic_title(title, **options)
28
+ data = {
29
+ "max-length" => options[:max_length],
30
+ "omission" => options[:omission],
31
+ "placeholder" => options[:placeholder],
32
+ "locale" => I18n.locale
33
+ }
34
+ content_tag :span, class: options[:class], data: data do
35
+ truncate translated_attribute(title), length: options[:max_length], omission: options[:omission]
36
+ end
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
16
53
  end
17
54
  end
18
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