decidim-forms 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
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