decidim-forms 0.20.1 → 0.23.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/admin/decidim_forms_manifest.css +3 -0
  3. data/app/assets/config/admin/decidim_forms_manifest.js +1 -0
  4. data/app/assets/config/decidim_forms_manifest.css +1 -0
  5. data/app/assets/images/decidim/surveys/icon.svg +1 -19
  6. data/app/assets/javascripts/decidim/forms/admin/auto_select_options_from_url.component.js.es6 +40 -0
  7. data/app/assets/javascripts/decidim/forms/admin/collapsible_questions.js.es6 +13 -0
  8. data/app/assets/javascripts/decidim/forms/admin/forms.js.es6 +260 -16
  9. data/app/assets/javascripts/decidim/forms/admin/live_text_update.component.js.es6 +52 -0
  10. data/app/assets/javascripts/decidim/forms/autosortable_checkboxes.component.js.es6 +54 -34
  11. data/app/assets/javascripts/decidim/forms/display_conditions.component.js.es6 +203 -0
  12. data/app/assets/javascripts/decidim/forms/forms.js.es6 +49 -1
  13. data/app/assets/javascripts/decidim/forms/max_choices_alert.component.js.es6 +44 -0
  14. data/app/assets/stylesheets/decidim/forms/forms.scss +39 -0
  15. data/app/assets/stylesheets/decidim/forms/questionnaire-answers-pdf.scss +69 -0
  16. data/app/cells/decidim/forms/matrix_readonly/show.erb +5 -0
  17. data/app/cells/decidim/forms/matrix_readonly_cell.rb +12 -0
  18. data/app/cells/decidim/forms/question_readonly/show.erb +5 -1
  19. data/app/cells/decidim/forms/question_readonly_cell.rb +5 -0
  20. data/app/cells/decidim/forms/step_navigation/show.erb +35 -0
  21. data/app/cells/decidim/forms/step_navigation_cell.rb +46 -0
  22. data/app/commands/decidim/forms/admin/update_questionnaire.rb +33 -1
  23. data/app/commands/decidim/forms/answer_questionnaire.rb +2 -1
  24. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +52 -2
  25. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire_answers.rb +97 -0
  26. data/app/controllers/decidim/forms/concerns/has_questionnaire.rb +11 -2
  27. data/app/forms/decidim/forms/admin/display_condition_form.rb +100 -0
  28. data/app/forms/decidim/forms/admin/question_form.rb +21 -1
  29. data/app/forms/decidim/forms/admin/question_matrix_row_form.rb +26 -0
  30. data/app/forms/decidim/forms/answer_choice_form.rb +1 -0
  31. data/app/forms/decidim/forms/answer_form.rb +31 -2
  32. data/app/forms/decidim/forms/questionnaire_form.rb +30 -3
  33. data/app/helpers/decidim/forms/admin/application_helper.rb +37 -0
  34. data/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_pagination_helper.rb +49 -0
  35. data/app/helpers/decidim/forms/admin/concerns/has_questionnaire_answers_url_helper.rb +40 -0
  36. data/app/helpers/decidim/forms/admin/questionnaire_answers_helper.rb +27 -0
  37. data/app/jobs/decidim/forms/export_questionnaire_answers_job.rb +19 -0
  38. data/app/models/decidim/forms/answer.rb +0 -3
  39. data/app/models/decidim/forms/answer_choice.rb +7 -0
  40. data/app/models/decidim/forms/answer_option.rb +14 -0
  41. data/app/models/decidim/forms/display_condition.rb +65 -0
  42. data/app/models/decidim/forms/question.rb +52 -2
  43. data/app/models/decidim/forms/question_matrix_row.rb +15 -0
  44. data/app/models/decidim/forms/questionnaire.rb +11 -1
  45. data/app/presenters/decidim/forms/admin/questionnaire_answer_presenter.rb +43 -0
  46. data/app/presenters/decidim/forms/admin/questionnaire_participant_presenter.rb +60 -0
  47. data/app/presenters/decidim/forms/answer_option_presenter.rb +20 -0
  48. data/app/presenters/decidim/forms/question_presenter.rb +16 -0
  49. data/app/queries/decidim/forms/questionnaire_participant.rb +35 -0
  50. data/app/queries/decidim/forms/questionnaire_participants.rb +43 -0
  51. data/app/types/decidim/forms/answer_option_type.rb +14 -0
  52. data/app/types/decidim/forms/question_type.rb +23 -0
  53. data/app/types/decidim/forms/questionnaire_type.rb +22 -0
  54. data/app/views/decidim/forms/admin/questionnaires/_answer_option_template.html.erb +1 -1
  55. data/app/views/decidim/forms/admin/questionnaires/_display_condition.html.erb +88 -0
  56. data/app/views/decidim/forms/admin/questionnaires/_display_condition_template.html.erb +7 -0
  57. data/app/views/decidim/forms/admin/questionnaires/_form.html.erb +67 -8
  58. data/app/views/decidim/forms/admin/questionnaires/_matrix_row.html.erb +34 -0
  59. data/app/views/decidim/forms/admin/questionnaires/_matrix_row_template.html.erb +7 -0
  60. data/app/views/decidim/forms/admin/questionnaires/_question.html.erb +45 -6
  61. data/app/views/decidim/forms/admin/questionnaires/_separator.html.erb +41 -0
  62. data/app/views/decidim/forms/admin/questionnaires/answers/export/_answer.html.erb +31 -0
  63. data/app/views/decidim/forms/admin/questionnaires/answers/export/pdf.html.erb +13 -0
  64. data/app/views/decidim/forms/admin/questionnaires/answers/index.html.erb +53 -0
  65. data/app/views/decidim/forms/admin/questionnaires/answers/show.html.erb +48 -0
  66. data/app/views/decidim/forms/admin/questionnaires/edit.html.erb +9 -5
  67. data/app/views/decidim/forms/questionnaires/_answer.html.erb +28 -99
  68. data/app/views/decidim/forms/questionnaires/answers/_long_answer.html.erb +1 -0
  69. data/app/views/decidim/forms/questionnaires/answers/_matrix_multiple.html.erb +43 -0
  70. data/app/views/decidim/forms/questionnaires/answers/_matrix_single.html.erb +43 -0
  71. data/app/views/decidim/forms/questionnaires/answers/_multiple_option.html.erb +23 -0
  72. data/app/views/decidim/forms/questionnaires/answers/_separator.html.erb +1 -0
  73. data/app/views/decidim/forms/questionnaires/answers/_short_answer.html.erb +1 -0
  74. data/app/views/decidim/forms/questionnaires/answers/_single_option.html.erb +30 -0
  75. data/app/views/decidim/forms/questionnaires/answers/_sorting.html.erb +23 -0
  76. data/app/views/decidim/forms/questionnaires/show.html.erb +78 -26
  77. data/app/views/layouts/decidim/forms/admin/questionnaires/questionnaire_answers.html.erb +12 -0
  78. data/config/initializers/wicked_pdf.rb +25 -0
  79. data/config/locales/am-ET.yml +1 -0
  80. data/config/locales/ar.yml +7 -4
  81. data/config/locales/bg-BG.yml +14 -0
  82. data/config/locales/bg.yml +14 -0
  83. data/config/locales/ca.yml +89 -5
  84. data/config/locales/cs.yml +89 -5
  85. data/config/locales/da-DK.yml +1 -0
  86. data/config/locales/da.yml +1 -0
  87. data/config/locales/de.yml +92 -4
  88. data/config/locales/el.yml +118 -0
  89. data/config/locales/en.yml +89 -5
  90. data/config/locales/eo.yml +1 -0
  91. data/config/locales/es-MX.yml +88 -4
  92. data/config/locales/es-PY.yml +88 -4
  93. data/config/locales/es.yml +88 -4
  94. data/config/locales/et-EE.yml +1 -0
  95. data/config/locales/et.yml +1 -0
  96. data/config/locales/eu.yml +7 -4
  97. data/config/locales/fi-plain.yml +88 -4
  98. data/config/locales/fi.yml +88 -4
  99. data/config/locales/fr-CA.yml +172 -0
  100. data/config/locales/fr.yml +89 -5
  101. data/config/locales/ga-IE.yml +1 -0
  102. data/config/locales/gl.yml +7 -4
  103. data/config/locales/hr-HR.yml +1 -0
  104. data/config/locales/hr.yml +1 -0
  105. data/config/locales/hu.yml +11 -5
  106. data/config/locales/id-ID.yml +7 -4
  107. data/config/locales/is.yml +1 -0
  108. data/config/locales/it.yml +89 -5
  109. data/config/locales/ja-JP.yml +170 -0
  110. data/config/locales/ja.yml +170 -0
  111. data/config/locales/ko-KR.yml +1 -0
  112. data/config/locales/ko.yml +1 -0
  113. data/config/locales/lt-LT.yml +1 -0
  114. data/config/locales/lt.yml +1 -0
  115. data/config/locales/lv.yml +118 -0
  116. data/config/locales/mt-MT.yml +1 -0
  117. data/config/locales/mt.yml +1 -0
  118. data/config/locales/nl.yml +92 -8
  119. data/config/locales/no.yml +54 -5
  120. data/config/locales/om-ET.yml +1 -0
  121. data/config/locales/pl.yml +113 -26
  122. data/config/locales/pt-BR.yml +8 -5
  123. data/config/locales/pt.yml +111 -24
  124. data/config/locales/ro-RO.yml +167 -0
  125. data/config/locales/ru.yml +4 -2
  126. data/config/locales/sk-SK.yml +88 -0
  127. data/config/locales/sk.yml +90 -0
  128. data/config/locales/sl.yml +12 -0
  129. data/config/locales/so-SO.yml +1 -0
  130. data/config/locales/sr-CS.yml +1 -0
  131. data/config/locales/sv.yml +88 -7
  132. data/config/locales/ti-ER.yml +1 -0
  133. data/config/locales/tr-TR.yml +7 -4
  134. data/config/locales/vi-VN.yml +1 -0
  135. data/config/locales/vi.yml +1 -0
  136. data/config/locales/zh-CN.yml +172 -0
  137. data/config/locales/zh-TW.yml +1 -0
  138. data/db/migrate/20200130194123_create_decidim_forms_display_conditions.rb +20 -0
  139. data/db/migrate/20200225123810_create_decidim_forms_question_matrix_rows.rb +11 -0
  140. data/db/migrate/20200304152939_add_matrix_row_id_to_decidim_forms_answer_choices.rb +11 -0
  141. data/lib/decidim/api/questionnaire_entity_interface.rb +18 -0
  142. data/lib/decidim/exporters/form_pdf.rb +33 -0
  143. data/lib/decidim/exporters/form_pdf_controller_helper.rb +11 -0
  144. data/lib/decidim/forms.rb +6 -0
  145. data/lib/decidim/forms/admin_engine.rb +1 -1
  146. data/lib/decidim/forms/api.rb +7 -0
  147. data/lib/decidim/forms/test.rb +6 -0
  148. data/lib/decidim/forms/test/factories.rb +55 -0
  149. data/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +918 -60
  150. data/lib/decidim/forms/test/shared_examples/manage_questionnaire_answers.rb +108 -0
  151. data/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb +33 -575
  152. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_display_conditions.rb +179 -0
  153. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb +463 -0
  154. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_display_conditions.rb +93 -0
  155. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_questions.rb +461 -0
  156. data/lib/decidim/forms/user_answers_serializer.rb +21 -4
  157. data/lib/decidim/forms/version.rb +1 -1
  158. metadata +133 -10
@@ -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
 
@@ -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
@@ -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,65 @@
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_form)
25
+ return answer_form.present? if condition_type == "answered"
26
+ return answer_form.blank? if condition_type == "not_answered"
27
+ # rest of options require presence
28
+ return if answer_form.blank?
29
+
30
+ case condition_type
31
+ when "equal"
32
+ answer_form.choices.pluck(:answer_option_id).include?(answer_option.id)
33
+ when "not_equal"
34
+ answer_form.choices.pluck(:answer_option_id).exclude?(answer_option.id)
35
+ when "match"
36
+ condition_value.values.reject(&:blank?).any? { |value| answer_form_matches?(answer_form, value) }
37
+ end
38
+ end
39
+
40
+ def to_html_data
41
+ {
42
+ id: id,
43
+ type: condition_type,
44
+ condition: decidim_condition_question_id,
45
+ option: decidim_answer_option_id,
46
+ mandatory: mandatory,
47
+ value: condition_value&.dig(I18n.locale.to_s)
48
+ }.compact
49
+ end
50
+
51
+ private
52
+
53
+ def answer_form_matches?(answer_form, value)
54
+ search = Regexp.new(value, Regexp::IGNORECASE)
55
+ if answer_form.body
56
+ answer_form.body.match?(search)
57
+ else
58
+ answer_form.choices.any? do |choice_value|
59
+ choice_value.body&.match?(search) || choice_value.custom_body&.match?(search)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -4,20 +4,62 @@ 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
+ include Decidim::TranslatableResource
8
+
9
+ QUESTION_TYPES = %w(short_answer long_answer single_option multiple_option sorting matrix_single matrix_multiple).freeze
10
+ SEPARATOR_TYPE = "separator"
11
+ TYPES = (QUESTION_TYPES + [SEPARATOR_TYPE]).freeze
12
+
13
+ translatable_fields :body, :description
8
14
 
9
15
  belongs_to :questionnaire, class_name: "Questionnaire", foreign_key: "decidim_questionnaire_id"
10
16
 
17
+ has_many :matrix_rows,
18
+ class_name: "QuestionMatrixRow",
19
+ foreign_key: "decidim_question_id",
20
+ dependent: :destroy,
21
+ inverse_of: :question
22
+
11
23
  has_many :answer_options,
12
24
  class_name: "AnswerOption",
13
25
  foreign_key: "decidim_question_id",
14
26
  dependent: :destroy,
15
27
  inverse_of: :question
16
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
+
17
49
  validates :question_type, inclusion: { in: TYPES }
18
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
+
57
+ def matrix?
58
+ %w(matrix_single matrix_multiple).include?(question_type)
59
+ end
60
+
19
61
  def multiple_choice?
20
- %w(single_option multiple_option sorting).include?(question_type)
62
+ %w(single_option multiple_option sorting matrix_single matrix_multiple).include?(question_type)
21
63
  end
22
64
 
23
65
  def mandatory_body?
@@ -31,6 +73,14 @@ module Decidim
31
73
  def number_of_options
32
74
  answer_options.size
33
75
  end
76
+
77
+ def translated_body
78
+ Decidim::Forms::QuestionPresenter.new(self).translated_body
79
+ end
80
+
81
+ def separator?
82
+ question_type.to_s == SEPARATOR_TYPE
83
+ end
34
84
  end
35
85
  end
36
86
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # The data store for a QuestionMatrix in the Decidim::Forms component.
6
+ class QuestionMatrixRow < Forms::ApplicationRecord
7
+ include Decidim::TranslatableResource
8
+
9
+ translatable_fields :body
10
+ belongs_to :question, class_name: "Question", foreign_key: "decidim_question_id"
11
+
12
+ delegate :answer_options, :mandatory, :max_choices, to: :question
13
+ end
14
+ end
15
+ end
@@ -4,6 +4,11 @@ module Decidim
4
4
  module Forms
5
5
  # The data store for a Questionnaire in the Decidim::Forms component.
6
6
  class Questionnaire < Forms::ApplicationRecord
7
+ include Decidim::Templates::Templatable if defined? Decidim::Templates::Templatable
8
+ include Decidim::Publicable
9
+ include Decidim::TranslatableResource
10
+
11
+ translatable_fields :title, :description, :tos
7
12
  belongs_to :questionnaire_for, polymorphic: true
8
13
 
9
14
  has_many :questions, -> { order(:position) }, class_name: "Question", foreign_key: "decidim_questionnaire_id", dependent: :destroy
@@ -11,7 +16,8 @@ module Decidim
11
16
 
12
17
  # Public: returns whether the questionnaire questions can be modified or not.
13
18
  def questions_editable?
14
- answers.empty?
19
+ has_component = questionnaire_for.respond_to? :component
20
+ (has_component && !questionnaire_for.component.published?) || answers.empty?
15
21
  end
16
22
 
17
23
  # Public: returns whether the questionnaire is answered by the user or not.
@@ -19,6 +25,10 @@ module Decidim
19
25
  query = user.is_a?(String) ? { session_token: user } : { user: user }
20
26
  answers.where(query).any? if questions.present?
21
27
  end
28
+
29
+ def pristine?
30
+ created_at.to_i == updated_at.to_i && questions.empty?
31
+ end
22
32
  end
23
33
  end
24
34
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ #
7
+ # Presenter for questionnaire answer
8
+ #
9
+ class QuestionnaireAnswerPresenter < Rectify::Presenter
10
+ include Decidim::TranslatableAttributes
11
+
12
+ attribute :answer, Decidim::Forms::Answer
13
+
14
+ def question
15
+ translated_attribute(answer.question.body)
16
+ end
17
+
18
+ def body
19
+ return answer.body if answer.body.present?
20
+ return "-" if answer.choices.empty?
21
+
22
+ choices = answer.choices.map do |choice|
23
+ choice.try(:custom_body) || choice.try(:body)
24
+ end
25
+
26
+ return choices.first if answer.question.question_type == "single_option"
27
+
28
+ content_tag(:ul) do
29
+ safe_join(choices.map { |c| choice(c) })
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def choice(choice_body)
36
+ content_tag :li do
37
+ choice_body
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ #
7
+ # Presenter for questionnaire response
8
+ #
9
+ class QuestionnaireParticipantPresenter < Rectify::Presenter
10
+ attribute :participant, Decidim::Forms::Answer
11
+
12
+ def session_token
13
+ participant.session_token || "-"
14
+ end
15
+
16
+ def ip_hash
17
+ participant.ip_hash || "-"
18
+ end
19
+
20
+ def answered_at
21
+ participant.created_at
22
+ end
23
+
24
+ delegate :questionnaire, to: :participant
25
+
26
+ def registered?
27
+ participant.decidim_user_id.present?
28
+ end
29
+
30
+ def status
31
+ t(registered? ? "registered" : "unregistered", scope: "decidim.forms.user_answers_serializer")
32
+ end
33
+
34
+ def answers
35
+ sibilings.map { |answer| QuestionnaireAnswerPresenter.new(answer: answer) }
36
+ end
37
+
38
+ def first_short_answer
39
+ short = sibilings.where("decidim_forms_questions.question_type in (?)", %w(short_answer))
40
+ short.first
41
+ end
42
+
43
+ def completion
44
+ with_body = sibilings.where("decidim_forms_questions.question_type in (?)", %w(short_answer long_answer))
45
+ .where.not(body: "").count
46
+ with_choices = sibilings.where.not("decidim_forms_questions.question_type in (?)", %w(short_answer long_answer))
47
+ .where("decidim_forms_answers.id IN (SELECT decidim_answer_id FROM decidim_forms_answer_choices)").count
48
+
49
+ (with_body + with_choices).to_f / questionnaire.questions.count * 100
50
+ end
51
+
52
+ private
53
+
54
+ def sibilings
55
+ Answer.where(session_token: participant.session_token).joins(:question).order("decidim_forms_questions.position ASC")
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ #
6
+ # Decorator for answer_options
7
+ #
8
+ class AnswerOptionPresenter < SimpleDelegator
9
+ include Decidim::TranslationsHelper
10
+
11
+ def translated_body
12
+ @translated_body ||= translated_attribute body
13
+ end
14
+
15
+ def as_json(*_args)
16
+ { id: id, body: translated_body }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ #
6
+ # Decorator for questions
7
+ #
8
+ class QuestionPresenter < SimpleDelegator
9
+ include Decidim::TranslationsHelper
10
+
11
+ def translated_body
12
+ @translated_body ||= translated_attribute body
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # A class used to collect user answers for a questionnaire
6
+ class QuestionnaireParticipant < Rectify::Query
7
+ # Syntactic sugar to initialize the class and return the queried objects.
8
+ #
9
+ # questionnaire - a Questionnaire object
10
+ # session_token = the session_token used by the questionnaire participant
11
+ def self.for(questionnaire, session_token)
12
+ new(questionnaire, session_token).query
13
+ end
14
+
15
+ # Initializes the class.
16
+ #
17
+ # questionnaire = a Questionnaire object
18
+ # session_token = the session_token used by the questionnaire participant
19
+ def initialize(questionnaire, session_token)
20
+ @questionnaire = questionnaire
21
+ @token = session_token
22
+ end
23
+
24
+ # Returns query with participant info
25
+ def query
26
+ answers.select(:session_token, :decidim_user_id, :ip_hash).first
27
+ end
28
+
29
+ # Finds the participant's answers.
30
+ def answers
31
+ Answer.where(questionnaire: @questionnaire, session_token: @token)
32
+ end
33
+ end
34
+ end
35
+ end