decidim-forms 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +65 -0
  3. data/Rakefile +3 -0
  4. data/app/assets/config/admin/decidim_forms_manifest.js +1 -0
  5. data/app/assets/config/decidim_forms_manifest.js +1 -0
  6. data/app/assets/images/decidim/surveys/icon.svg +19 -0
  7. data/app/assets/javascripts/decidim/forms/admin/auto_buttons_by_min_items.component.js.es6 +25 -0
  8. data/app/assets/javascripts/decidim/forms/admin/auto_select_options_by_total_items.component.js.es6 +23 -0
  9. data/app/assets/javascripts/decidim/forms/admin/forms.js.es6 +188 -0
  10. data/app/assets/javascripts/decidim/forms/autosortable_checkboxes.component.js.es6 +65 -0
  11. data/app/assets/javascripts/decidim/forms/forms.js.es6 +20 -0
  12. data/app/assets/javascripts/decidim/forms/option_attached_inputs.component.js.es6 +32 -0
  13. data/app/commands/decidim/forms/admin/update_questionnaire.rb +86 -0
  14. data/app/commands/decidim/forms/answer_questionnaire.rb +54 -0
  15. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +95 -0
  16. data/app/controllers/decidim/forms/concerns/has_questionnaire.rb +84 -0
  17. data/app/forms/decidim/forms/admin/answer_option_form.rb +23 -0
  18. data/app/forms/decidim/forms/admin/question_form.rb +35 -0
  19. data/app/forms/decidim/forms/admin/questionnaire_form.rb +27 -0
  20. data/app/forms/decidim/forms/answer_choice_form.rb +15 -0
  21. data/app/forms/decidim/forms/answer_form.rb +69 -0
  22. data/app/forms/decidim/forms/questionnaire_form.rb +22 -0
  23. data/app/helpers/decidim/forms/admin/application_helper.rb +19 -0
  24. data/app/models/concerns/decidim/forms/has_questionnaire.rb +20 -0
  25. data/app/models/decidim/forms/answer.rb +45 -0
  26. data/app/models/decidim/forms/answer_choice.rb +15 -0
  27. data/app/models/decidim/forms/answer_option.rb +11 -0
  28. data/app/models/decidim/forms/application_record.rb +10 -0
  29. data/app/models/decidim/forms/question.rb +36 -0
  30. data/app/models/decidim/forms/questionnaire.rb +23 -0
  31. data/app/queries/decidim/forms/questionnaire_user_answers.rb +28 -0
  32. data/app/views/decidim/forms/admin/questionnaires/_answer_option.html.erb +44 -0
  33. data/app/views/decidim/forms/admin/questionnaires/_form.html.erb +51 -0
  34. data/app/views/decidim/forms/admin/questionnaires/_question.html.erb +115 -0
  35. data/app/views/decidim/forms/admin/questionnaires/edit.html.erb +7 -0
  36. data/app/views/decidim/forms/questionnaires/_answer.html.erb +94 -0
  37. data/app/views/decidim/forms/questionnaires/show.html.erb +84 -0
  38. data/config/locales/ca.yml +79 -0
  39. data/config/locales/de.yml +79 -0
  40. data/config/locales/en.yml +79 -0
  41. data/config/locales/es-PY.yml +79 -0
  42. data/config/locales/es.yml +79 -0
  43. data/config/locales/eu.yml +79 -0
  44. data/config/locales/fi-pl.yml +79 -0
  45. data/config/locales/fi.yml +79 -0
  46. data/config/locales/fr.yml +79 -0
  47. data/config/locales/gl.yml +79 -0
  48. data/config/locales/hu.yml +79 -0
  49. data/config/locales/id-ID.yml +1 -0
  50. data/config/locales/it.yml +79 -0
  51. data/config/locales/nl.yml +79 -0
  52. data/config/locales/pl.yml +79 -0
  53. data/config/locales/pt-BR.yml +79 -0
  54. data/config/locales/pt.yml +79 -0
  55. data/config/locales/ru.yml +1 -0
  56. data/config/locales/sv.yml +79 -0
  57. data/config/locales/tr-TR.yml +1 -0
  58. data/config/locales/uk.yml +1 -0
  59. data/db/migrate/20170511092231_create_decidim_forms_questionnaires.rb +15 -0
  60. data/db/migrate/20170515090916_create_decidim_forms_questions.rb +17 -0
  61. data/db/migrate/20170515144119_create_decidim_forms_answers.rb +15 -0
  62. data/db/migrate/20180405015012_create_decidim_forms_answer_options.rb +11 -0
  63. data/db/migrate/20180405015147_create_decidim_forms_answer_choices.rb +13 -0
  64. data/lib/decidim/forms.rb +13 -0
  65. data/lib/decidim/forms/admin.rb +9 -0
  66. data/lib/decidim/forms/admin_engine.rb +21 -0
  67. data/lib/decidim/forms/data_portability_user_answers_serializer.rb +37 -0
  68. data/lib/decidim/forms/engine.rb +14 -0
  69. data/lib/decidim/forms/test.rb +4 -0
  70. data/lib/decidim/forms/test/factories.rb +55 -0
  71. data/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +524 -0
  72. data/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb +626 -0
  73. data/lib/decidim/forms/user_answers_serializer.rb +29 -0
  74. data/lib/decidim/forms/version.rb +10 -0
  75. metadata +165 -0
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ # This command is executed when the user changes a Questionnaire from the admin
7
+ # panel.
8
+ class UpdateQuestionnaire < Rectify::Command
9
+ # Initializes a UpdateQuestionnaire Command.
10
+ #
11
+ # form - The form from which to get the data.
12
+ # questionnaire - The current instance of the questionnaire to be updated.
13
+ def initialize(form, questionnaire)
14
+ @form = form
15
+ @questionnaire = questionnaire
16
+ end
17
+
18
+ # Updates the questionnaire if valid.
19
+ #
20
+ # Broadcasts :ok if successful, :invalid otherwise.
21
+ def call
22
+ return broadcast(:invalid) if @form.invalid?
23
+
24
+ Decidim::Forms::Questionnaire.transaction do
25
+ update_questionnaire_questions if @questionnaire.questions_editable?
26
+ update_questionnaire
27
+ end
28
+
29
+ broadcast(:ok)
30
+ end
31
+
32
+ private
33
+
34
+ def update_questionnaire_questions
35
+ @form.questions.each do |form_question|
36
+ update_questionnaire_question(form_question)
37
+ end
38
+ end
39
+
40
+ def update_questionnaire_question(form_question)
41
+ question_attributes = {
42
+ body: form_question.body,
43
+ description: form_question.description,
44
+ position: form_question.position,
45
+ mandatory: form_question.mandatory,
46
+ question_type: form_question.question_type,
47
+ max_choices: form_question.max_choices
48
+ }
49
+
50
+ update_nested_model(form_question, question_attributes, @questionnaire.questions) do |question|
51
+ form_question.answer_options.each do |form_answer_option|
52
+ answer_option_attributes = {
53
+ body: form_answer_option.body,
54
+ free_text: form_answer_option.free_text
55
+ }
56
+
57
+ update_nested_model(form_answer_option, answer_option_attributes, question.answer_options)
58
+ end
59
+ end
60
+ end
61
+
62
+ def update_nested_model(form, attributes, parent_association)
63
+ record = parent_association.find_by(id: form.id) || parent_association.build(attributes)
64
+
65
+ yield record if block_given?
66
+
67
+ if record.persisted?
68
+ if form.deleted?
69
+ record.destroy!
70
+ else
71
+ record.update!(attributes)
72
+ end
73
+ else
74
+ record.save!
75
+ end
76
+ end
77
+
78
+ def update_questionnaire
79
+ @questionnaire.update!(title: @form.title,
80
+ description: @form.description,
81
+ tos: @form.tos)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # This command is executed when the user answers a Questionnaire.
6
+ class AnswerQuestionnaire < Rectify::Command
7
+ # Initializes a AnswerQuestionnaire Command.
8
+ #
9
+ # form - The form from which to get the data.
10
+ # questionnaire - The current instance of the questionnaire to be answered.
11
+ def initialize(form, current_user, questionnaire)
12
+ @form = form
13
+ @current_user = current_user
14
+ @questionnaire = questionnaire
15
+ end
16
+
17
+ # Answers a questionnaire if it is valid
18
+ #
19
+ # Broadcasts :ok if successful, :invalid otherwise.
20
+ def call
21
+ return broadcast(:invalid) if @form.invalid?
22
+
23
+ answer_questionnaire
24
+ broadcast(:ok)
25
+ end
26
+
27
+ private
28
+
29
+ def answer_questionnaire
30
+ Answer.transaction do
31
+ @form.answers.each do |form_answer|
32
+ answer = Answer.new(
33
+ user: @current_user,
34
+ questionnaire: @questionnaire,
35
+ question: form_answer.question,
36
+ body: form_answer.body
37
+ )
38
+
39
+ form_answer.selected_choices.each do |choice|
40
+ answer.choices.build(
41
+ body: choice.body,
42
+ custom_body: choice.custom_body,
43
+ decidim_answer_option_id: choice.answer_option_id,
44
+ position: choice.position
45
+ )
46
+ end
47
+
48
+ answer.save!
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ module Concerns
7
+ # Questionnaires can be related to any class in Decidim, in order to
8
+ # manage the questionnaires for a given type, you should create a new
9
+ # controller and include this concern.
10
+ #
11
+ # The only requirement is to define a `questionnaire_for` method that
12
+ # returns an instance of the model that questionnaire belongs to.
13
+ module HasQuestionnaire
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ helper Decidim::Forms::Admin::ApplicationHelper
18
+ helper_method :questionnaire_for, :questionnaire, :blank_question, :blank_answer_option, :question_types, :update_url
19
+
20
+ def edit
21
+ enforce_permission_to :update, :questionnaire, questionnaire: questionnaire
22
+
23
+ @form = form(Admin::QuestionnaireForm).from_model(questionnaire)
24
+
25
+ render template: "decidim/forms/admin/questionnaires/edit"
26
+ end
27
+
28
+ def update
29
+ enforce_permission_to :update, :questionnaire, questionnaire: questionnaire
30
+
31
+ params["published_at"] = Time.current if params.has_key? "save_and_publish"
32
+ @form = form(Admin::QuestionnaireForm).from_params(params)
33
+
34
+ Admin::UpdateQuestionnaire.call(@form, questionnaire) do
35
+ on(:ok) do
36
+ # i18n-tasks-use t("decidim.forms.admin.questionnaires.update.success")
37
+ flash[:notice] = I18n.t("update.success", scope: i18n_flashes_scope)
38
+ redirect_to after_update_url
39
+ end
40
+
41
+ on(:invalid) do
42
+ # i18n-tasks-use t("decidim.forms.admin.questionnaires.update.invalid")
43
+ flash.now[:alert] = I18n.t("update.invalid", scope: i18n_flashes_scope)
44
+ render template: "decidim/forms/admin/questionnaires/edit"
45
+ end
46
+ end
47
+ end
48
+
49
+ # Public: The only method to be implemented at the controller. You need to
50
+ # return the object that will hold the questionnaire.
51
+ def questionnaire_for
52
+ raise "#{self.class.name} is expected to implement #questionnaire_for"
53
+ end
54
+
55
+ # You can implement this method in your controller to change the URL
56
+ # where the questionnaire will be submitted.
57
+ def update_url
58
+ url_for(questionnaire.questionnaire_for)
59
+ end
60
+
61
+ # You can implement this method in your controller to change the URL
62
+ # where the user will be redirected after updating the questionnaire
63
+ def after_update_url
64
+ url_for(questionnaire.questionnaire_for)
65
+ end
66
+
67
+ private
68
+
69
+ def i18n_flashes_scope
70
+ "decidim.forms.admin.questionnaires"
71
+ end
72
+
73
+ def questionnaire
74
+ @questionnaire ||= Questionnaire.find_by(questionnaire_for: questionnaire_for)
75
+ end
76
+
77
+ def blank_question
78
+ @blank_question ||= Admin::QuestionForm.new
79
+ end
80
+
81
+ def blank_answer_option
82
+ @blank_answer_option ||= Admin::AnswerOptionForm.new
83
+ end
84
+
85
+ def question_types
86
+ @question_types ||= Question::TYPES.map do |question_type|
87
+ [question_type, I18n.t("decidim.forms.question_types.#{question_type}")]
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Concerns
6
+ # Questionnaires can be related to any class in Decidim, in order to
7
+ # manage the questionnaires for a given type, you should create a new
8
+ # controller and include this concern.
9
+ #
10
+ # The only requirement is to define a `questionnaire_for` method that
11
+ # returns an instance of the model that questionnaire belongs to.
12
+ module HasQuestionnaire
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ include FormFactory
17
+
18
+ helper_method :questionnaire_for, :questionnaire, :allow_answers?, :update_url
19
+
20
+ def show
21
+ @form = form(Decidim::Forms::QuestionnaireForm).from_model(questionnaire)
22
+ render template: "decidim/forms/questionnaires/show"
23
+ end
24
+
25
+ def answer
26
+ enforce_permission_to :answer, :questionnaire
27
+
28
+ @form = form(Decidim::Forms::QuestionnaireForm).from_params(params)
29
+
30
+ Decidim::Forms::AnswerQuestionnaire.call(@form, current_user, questionnaire) do
31
+ on(:ok) do
32
+ # i18n-tasks-use t("decidim.forms.questionnaires.answer.success")
33
+ flash[:notice] = I18n.t("answer.success", scope: i18n_flashes_scope)
34
+ redirect_to after_answer_path
35
+ end
36
+
37
+ on(:invalid) do
38
+ # i18n-tasks-use t("decidim.forms.questionnaires.answer.invalid")
39
+ flash.now[:alert] = I18n.t("answer.invalid", scope: i18n_flashes_scope)
40
+ render template: "decidim/forms/questionnaires/show"
41
+ end
42
+ end
43
+ end
44
+
45
+ # Public: Method to be implemented at the controller. You need to
46
+ # return true if the questionnaire can receive answers
47
+ def allow_answers?
48
+ raise "#{self.class.name} is expected to implement #allow_answers?"
49
+ end
50
+
51
+ # Public: Returns a String or Object that will be passed to `redirect_to` after
52
+ # answering the questionnaire. By default it redirects to the questionnaire_for.
53
+ #
54
+ # It can be redefined at controller level if you need to redirect elsewhere.
55
+ def after_answer_path
56
+ questionnaire_for
57
+ end
58
+
59
+ # You can implement this method in your controller to change the URL
60
+ # where the questionnaire will be submitted.
61
+ def update_url
62
+ url_for([questionnaire_for, action: :answer])
63
+ end
64
+
65
+ # Public: Method to be implemented at the controller. You need to
66
+ # return the object that will hold the questionnaire.
67
+ def questionnaire_for
68
+ raise "#{self.class.name} is expected to implement #questionnaire_for"
69
+ end
70
+
71
+ private
72
+
73
+ def i18n_flashes_scope
74
+ "decidim.forms.questionnaires"
75
+ end
76
+
77
+ def questionnaire
78
+ @questionnaire ||= Questionnaire.includes(questions: :answer_options).find_by(questionnaire_for: questionnaire_for)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ # This class holds a Form to update answer options
7
+ class AnswerOptionForm < Decidim::Form
8
+ include TranslatableAttributes
9
+
10
+ attribute :deleted, Boolean, default: false
11
+ attribute :free_text, Boolean
12
+
13
+ translatable_attribute :body, String
14
+
15
+ validates :body, translatable_presence: true, unless: :deleted
16
+
17
+ def to_param
18
+ id || "questionnaire-question-answer-option-id"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
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 QuestionForm < Decidim::Form
8
+ include TranslatableAttributes
9
+
10
+ attribute :position, Integer
11
+ attribute :mandatory, Boolean, default: false
12
+ attribute :question_type, String
13
+ attribute :answer_options, Array[AnswerOptionForm]
14
+ attribute :max_choices, Integer
15
+ attribute :deleted, Boolean, default: false
16
+
17
+ translatable_attribute :body, String
18
+ translatable_attribute :description, String
19
+
20
+ validates :position, numericality: { greater_than_or_equal_to: 0 }
21
+ validates :question_type, inclusion: { in: Decidim::Forms::Question::TYPES }
22
+ 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
+
25
+ def to_param
26
+ id || "questionnaire-question-id"
27
+ end
28
+
29
+ def number_of_options
30
+ answer_options.size
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ # This class holds a Form to update questionnaires from Decidim's admin panel.
7
+ class QuestionnaireForm < Decidim::Form
8
+ include TranslatableAttributes
9
+
10
+ translatable_attribute :title, String
11
+ translatable_attribute :description, String
12
+ translatable_attribute :tos, String
13
+
14
+ attribute :published_at, Decidim::Attributes::TimeWithZone
15
+ attribute :questions, Array[QuestionForm]
16
+
17
+ validates :title, :tos, translatable_presence: true
18
+
19
+ def map_model(model)
20
+ self.questions = model.questions.map do |question|
21
+ QuestionForm.from_model(question)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # This class holds a Form to save the chosen option for an answer
6
+ class AnswerChoiceForm < Decidim::Form
7
+ attribute :body, String
8
+ attribute :custom_body, String
9
+ attribute :position, Integer
10
+ attribute :answer_option_id, Integer
11
+
12
+ validates :answer_option_id, presence: true
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # This class holds a Form to save the questionnaire answers from Decidim's public page
6
+ class AnswerForm < Decidim::Form
7
+ include Decidim::TranslationsHelper
8
+
9
+ attribute :question_id, String
10
+ attribute :body, String
11
+ attribute :choices, Array[AnswerChoiceForm]
12
+
13
+ validates :body, presence: true, if: :mandatory_body?
14
+ validates :selected_choices, presence: true, if: :mandatory_choices?
15
+
16
+ validate :max_choices, if: -> { question.max_choices }
17
+ validate :all_choices, if: -> { question.question_type == "sorting" }
18
+
19
+ delegate :mandatory_body?, :mandatory_choices?, to: :question
20
+
21
+ attr_writer :question
22
+
23
+ def question
24
+ @question ||= Decidim::Forms::Question.find(question_id)
25
+ end
26
+
27
+ def label(idx)
28
+ base = "#{idx + 1}. #{translated_attribute(question.body)}"
29
+ base += " #{mandatory_label}" if question.mandatory?
30
+ base += " (#{max_choices_label})" if question.max_choices
31
+ base
32
+ end
33
+
34
+ # Public: Map the correct fields.
35
+ #
36
+ # Returns nothing.
37
+ def map_model(model)
38
+ self.question_id = model.decidim_question_id
39
+ self.question = model.question
40
+
41
+ self.choices = model.choices.map do |choice|
42
+ AnswerChoiceForm.from_model(choice)
43
+ end
44
+ end
45
+
46
+ def selected_choices
47
+ choices.select(&:body)
48
+ end
49
+
50
+ private
51
+
52
+ def max_choices
53
+ errors.add(:choices, :too_many) if selected_choices.size > question.max_choices
54
+ end
55
+
56
+ def all_choices
57
+ errors.add(:choices, :missing) if selected_choices.size != question.number_of_options
58
+ end
59
+
60
+ def mandatory_label
61
+ "*"
62
+ end
63
+
64
+ def max_choices_label
65
+ I18n.t("questionnaires.question.max_choices", scope: "decidim.forms", n: question.max_choices)
66
+ end
67
+ end
68
+ end
69
+ end