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,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>