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,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # This class holds a Form to answer a questionnaire from Decidim's public page.
6
+ class QuestionnaireForm < Decidim::Form
7
+ attribute :answers, Array[AnswerForm]
8
+
9
+ attribute :tos_agreement, Boolean
10
+ validates :tos_agreement, allow_nil: false, acceptance: true
11
+
12
+ # Private: Create the answers from the questionnaire questions
13
+ #
14
+ # Returns nothing.
15
+ def map_model(model)
16
+ self.answers = model.questions.map do |question|
17
+ AnswerForm.from_model(Decidim::Forms::Answer.new(question: question))
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ module Admin
6
+ # Custom helpers, scoped to the forms engine.
7
+ #
8
+ module ApplicationHelper
9
+ def tabs_id_for_question(question)
10
+ "questionnaire_question_#{question.to_param}"
11
+ end
12
+
13
+ def tabs_id_for_question_answer_option(question, answer_option)
14
+ "questionnaire_question_#{question.to_param}_answer_option_#{answer_option.to_param}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Decidim
6
+ module Forms
7
+ # A concern with the components needed when you want a model to be have a questionnaire attached
8
+ module HasQuestionnaire
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ has_one :questionnaire,
13
+ class_name: "Decidim::Forms::Questionnaire",
14
+ dependent: :destroy,
15
+ inverse_of: :questionnaire_for,
16
+ as: :questionnaire_for
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # The data store for an Answer in the Decidim::Forms
6
+ class Answer < Forms::ApplicationRecord
7
+ include Decidim::DataPortability
8
+
9
+ belongs_to :user, class_name: "Decidim::User", foreign_key: "decidim_user_id"
10
+ belongs_to :questionnaire, class_name: "Questionnaire", foreign_key: "decidim_questionnaire_id"
11
+ belongs_to :question, class_name: "Question", foreign_key: "decidim_question_id"
12
+
13
+ has_many :choices,
14
+ class_name: "AnswerChoice",
15
+ foreign_key: "decidim_answer_id",
16
+ dependent: :destroy,
17
+ inverse_of: :answer
18
+
19
+ validates :body, presence: true, if: -> { question.mandatory_body? }
20
+ validates :choices, presence: true, if: -> { question.mandatory_choices? }
21
+
22
+ validate :user_questionnaire_same_organization
23
+ validate :question_belongs_to_questionnaire
24
+
25
+ def self.user_collection(user)
26
+ where(decidim_user_id: user.id)
27
+ end
28
+
29
+ def self.export_serializer
30
+ Decidim::Forms::DataPortabilityUserAnswersSerializer
31
+ end
32
+
33
+ private
34
+
35
+ def user_questionnaire_same_organization
36
+ return if user&.organization == questionnaire.questionnaire_for&.organization
37
+ errors.add(:user, :invalid)
38
+ end
39
+
40
+ def question_belongs_to_questionnaire
41
+ errors.add(:questionnaire, :invalid) if question&.questionnaire != questionnaire
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ class AnswerChoice < Forms::ApplicationRecord
6
+ belongs_to :answer,
7
+ class_name: "Answer",
8
+ foreign_key: "decidim_answer_id"
9
+
10
+ belongs_to :answer_option,
11
+ class_name: "AnswerOption",
12
+ foreign_key: "decidim_answer_option_id"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ class AnswerOption < Forms::ApplicationRecord
6
+ default_scope { order(arel_table[:id].asc) }
7
+
8
+ belongs_to :question, class_name: "Question", foreign_key: "decidim_question_id"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # Abstract class from which all models in this engine inherit.
6
+ class ApplicationRecord < ActiveRecord::Base
7
+ self.abstract_class = true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # The data store for a Question in the Decidim::Forms component.
6
+ class Question < Forms::ApplicationRecord
7
+ TYPES = %w(short_answer long_answer single_option multiple_option sorting).freeze
8
+
9
+ belongs_to :questionnaire, class_name: "Questionnaire", foreign_key: "decidim_questionnaire_id"
10
+
11
+ has_many :answer_options,
12
+ class_name: "AnswerOption",
13
+ foreign_key: "decidim_question_id",
14
+ dependent: :destroy,
15
+ inverse_of: :question
16
+
17
+ validates :question_type, inclusion: { in: TYPES }
18
+
19
+ def multiple_choice?
20
+ %w(single_option multiple_option sorting).include?(question_type)
21
+ end
22
+
23
+ def mandatory_body?
24
+ mandatory? && !multiple_choice?
25
+ end
26
+
27
+ def mandatory_choices?
28
+ mandatory? && multiple_choice?
29
+ end
30
+
31
+ def number_of_options
32
+ answer_options.size
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Forms
5
+ # The data store for a Questionnaire in the Decidim::Forms component.
6
+ class Questionnaire < Forms::ApplicationRecord
7
+ belongs_to :questionnaire_for, polymorphic: true
8
+
9
+ has_many :questions, -> { order(:position) }, class_name: "Question", foreign_key: "decidim_questionnaire_id", dependent: :destroy
10
+ has_many :answers, class_name: "Answer", foreign_key: "decidim_questionnaire_id", dependent: :destroy
11
+
12
+ # Public: returns whether the questionnaire questions can be modified or not.
13
+ def questions_editable?
14
+ answers.empty?
15
+ end
16
+
17
+ # Public: returns whether the questionnaire is answered by the user or not.
18
+ def answered_by?(user)
19
+ answers.where(user: user).any? if questions.present?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
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 QuestionnaireUserAnswers < Rectify::Query
7
+ # Syntactic sugar to initialize the class and return the queried objects.
8
+ #
9
+ # questionnaire - a Questionnaire object
10
+ def self.for(questionnaire)
11
+ new(questionnaire).query
12
+ end
13
+
14
+ # Initializes the class.
15
+ #
16
+ # questionnaire = a Questionnaire object
17
+ def initialize(questionnaire)
18
+ @questionnaire = questionnaire
19
+ end
20
+
21
+ # Finds and group answers by user for each questionnaire's question.
22
+ def query
23
+ answers = Answer.where(questionnaire: @questionnaire)
24
+ answers.sort_by { |answer| answer.question.position }.group_by(&:user).values
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ <% answer_option = form.object %>
2
+
3
+ <div class="card questionnaire-question-answer-option">
4
+ <div class="card-divider">
5
+ <h2 class="card-title">
6
+ <span><%= t(".answer_option") %></span>
7
+ <% if editable %>
8
+ <button class="button small alert hollow remove-answer-option button--title">
9
+ <%= t(".remove") %>
10
+ </button>
11
+ <% end %>
12
+ </h2>
13
+ </div>
14
+
15
+ <div class="card-section">
16
+ <div class="row column">
17
+ <%=
18
+ form.translated(
19
+ :text_field,
20
+ :body,
21
+ tabs_id: tabs_id_for_question_answer_option(question, answer_option),
22
+ label: t(".statement"),
23
+ disabled: !editable
24
+ )
25
+ %>
26
+ </div>
27
+
28
+ <div class="row column">
29
+ <%=
30
+ form.check_box(
31
+ :free_text,
32
+ label: t(".free_text"),
33
+ disabled: !questionnaire.questions_editable?
34
+ )
35
+ %>
36
+ </div>
37
+ </div>
38
+
39
+ <% if answer_option.persisted? %>
40
+ <%= form.hidden_field :id, disabled: !editable %>
41
+ <% end %>
42
+
43
+ <%= form.hidden_field :deleted, disabled: !editable %>
44
+ </div>
@@ -0,0 +1,51 @@
1
+ <div class="card">
2
+ <div class="card-divider">
3
+ <h2 class="card-title">
4
+ <%= title %>
5
+ <div class="button--title">
6
+ <%= export_dropdown if allowed_to? :export_answers, :questionnaire %>
7
+ </div>
8
+ </h2>
9
+ </div>
10
+ <div class="card-section">
11
+ <div class="row column">
12
+ <%= form.translated :text_field, :title, autofocus: true %>
13
+ </div>
14
+ <div class="row column">
15
+ <%= form.translated :editor, :description, toolbar: :full, lines: 30, label: t("models.components.description", scope: "decidim.forms.admin") %>
16
+ </div>
17
+ <div class="row column">
18
+ <%= form.translated :editor, :tos, toolbar: :full, lines: 10, label: t("models.components.tos", scope: "decidim.forms.admin") %>
19
+ </div>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="questionnaire-questions">
24
+ <% if questionnaire.questions_editable? %>
25
+ <template>
26
+ <%= fields_for "questionnaire[questions][#{blank_question.to_param}]", blank_question do |question_form| %>
27
+ <%= render "decidim/forms/admin/questionnaires/question", form: question_form, id: tabs_id_for_question(blank_question), editable: questionnaire.questions_editable? %>
28
+ <% end %>
29
+ </template>
30
+ <% else %>
31
+ <div class="callout warning">
32
+ <%= t(".already_answered_warning") %>
33
+ </div>
34
+ <% end %>
35
+
36
+ <div class="questionnaire-questions-list">
37
+ <% @form.questions.each do |question| %>
38
+ <%= fields_for "questionnaire[questions][]", question do |question_form| %>
39
+ <%= render "decidim/forms/admin/questionnaires/question", form: question_form, id: tabs_id_for_question(question), editable: questionnaire.questions_editable? %>
40
+ <% end %>
41
+ <% end %>
42
+ </div>
43
+
44
+ <% if questionnaire.questions_editable? %>
45
+ <button class="button add-question"><%= t(".add_question") %></button>
46
+ <% end %>
47
+ </div>
48
+
49
+ <% if questionnaire.questions_editable? %>
50
+ <%= javascript_include_tag "decidim/forms/admin/forms" %>
51
+ <% end %>
@@ -0,0 +1,115 @@
1
+ <% question = form.object %>
2
+
3
+ <div class="card questionnaire-question" id="<%= id %>-field">
4
+ <div class="card-divider question-divider">
5
+ <h2 class="card-title">
6
+ <span>
7
+ <% if editable %>
8
+ <%== "#{icon("move")} #{t(".question")}" %>
9
+ <% else %>
10
+ <%= t(".question") %>
11
+ <% end %>
12
+ </span>
13
+
14
+ <% if editable %>
15
+ <button class="button small alert hollow move-up-question button--title">
16
+ <%== "#{icon("arrow-top")} #{t(".up")}" %>
17
+ </button>
18
+
19
+ <button class="button small alert hollow move-down-question button--title">
20
+ <%== "#{icon("arrow-bottom")} #{t(".down")}" %>
21
+ </button>
22
+
23
+ <button class="button small alert hollow remove-question button--title">
24
+ <%= t(".remove") %>
25
+ </button>
26
+ <% end %>
27
+ </h2>
28
+ </div>
29
+
30
+ <div class="card-section">
31
+ <div class="row column">
32
+ <%=
33
+ form.translated(
34
+ :text_field,
35
+ :body,
36
+ tabs_id: id,
37
+ label: t(".statement"),
38
+ disabled: !editable
39
+ )
40
+ %>
41
+ </div>
42
+
43
+ <div class="row column">
44
+ <%=
45
+ form.translated(
46
+ :editor,
47
+ :description,
48
+ toolbar: :full,
49
+ tabs_id: id,
50
+ label: t(".description"),
51
+ disabled: !editable
52
+ )
53
+ %>
54
+ </div>
55
+
56
+ <div class="row column">
57
+ <%=
58
+ form.check_box(
59
+ :mandatory,
60
+ disabled: !editable,
61
+ label: t("activemodel.attributes.questionnaire_question.mandatory")
62
+ )
63
+ %>
64
+ </div>
65
+
66
+ <div class="row column">
67
+ <%=
68
+ form.select(
69
+ :question_type,
70
+ options_from_collection_for_select(question_types, :first, :last, question.question_type),
71
+ {},
72
+ disabled: !editable
73
+ )
74
+ %>
75
+ </div>
76
+
77
+ <% if question.persisted? %>
78
+ <%= form.hidden_field :id, disabled: !editable %>
79
+ <% end %>
80
+
81
+ <%= form.hidden_field :position, value: question.position || 0, disabled: !editable %>
82
+ <%= form.hidden_field :deleted, disabled: !editable %>
83
+
84
+ <div class="questionnaire-question-answer-options">
85
+ <template>
86
+ <%= fields_for "questionnaire[questions][#{question.to_param}][answer_options][]", blank_answer_option do |answer_option_form| %>
87
+ <%= render "decidim/forms/admin/questionnaires/answer_option", form: answer_option_form, question: question, editable: editable %>
88
+ <% end %>
89
+ </template>
90
+
91
+ <div class="questionnaire-question-answer-options-list">
92
+ <% question.answer_options.each do |answer_option| %>
93
+ <%= fields_for "questionnaire[questions][#{question.to_param}][answer_options][]", answer_option do |answer_option_form| %>
94
+ <%= render "decidim/forms/admin/questionnaires/answer_option", form: answer_option_form, question: question, editable: editable %>
95
+ <% end %>
96
+ <% end %>
97
+ </div>
98
+
99
+ <% if editable %>
100
+ <button class="button add-answer-option"><%= t(".add_answer_option") %></button>
101
+ <% end %>
102
+ </div>
103
+
104
+ <div class="row column questionnaire-question-max-choices">
105
+ <%=
106
+ form.select(
107
+ :max_choices,
108
+ (2..question.number_of_options),
109
+ { prompt: t(".any") },
110
+ disabled: !editable
111
+ )
112
+ %>
113
+ </div>
114
+ </div>
115
+ </div>