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.
- checksums.yaml +7 -0
- data/README.md +65 -0
- data/Rakefile +3 -0
- data/app/assets/config/admin/decidim_forms_manifest.js +1 -0
- data/app/assets/config/decidim_forms_manifest.js +1 -0
- data/app/assets/images/decidim/surveys/icon.svg +19 -0
- data/app/assets/javascripts/decidim/forms/admin/auto_buttons_by_min_items.component.js.es6 +25 -0
- data/app/assets/javascripts/decidim/forms/admin/auto_select_options_by_total_items.component.js.es6 +23 -0
- data/app/assets/javascripts/decidim/forms/admin/forms.js.es6 +188 -0
- data/app/assets/javascripts/decidim/forms/autosortable_checkboxes.component.js.es6 +65 -0
- data/app/assets/javascripts/decidim/forms/forms.js.es6 +20 -0
- data/app/assets/javascripts/decidim/forms/option_attached_inputs.component.js.es6 +32 -0
- data/app/commands/decidim/forms/admin/update_questionnaire.rb +86 -0
- data/app/commands/decidim/forms/answer_questionnaire.rb +54 -0
- data/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +95 -0
- data/app/controllers/decidim/forms/concerns/has_questionnaire.rb +84 -0
- data/app/forms/decidim/forms/admin/answer_option_form.rb +23 -0
- data/app/forms/decidim/forms/admin/question_form.rb +35 -0
- data/app/forms/decidim/forms/admin/questionnaire_form.rb +27 -0
- data/app/forms/decidim/forms/answer_choice_form.rb +15 -0
- data/app/forms/decidim/forms/answer_form.rb +69 -0
- data/app/forms/decidim/forms/questionnaire_form.rb +22 -0
- data/app/helpers/decidim/forms/admin/application_helper.rb +19 -0
- data/app/models/concerns/decidim/forms/has_questionnaire.rb +20 -0
- data/app/models/decidim/forms/answer.rb +45 -0
- data/app/models/decidim/forms/answer_choice.rb +15 -0
- data/app/models/decidim/forms/answer_option.rb +11 -0
- data/app/models/decidim/forms/application_record.rb +10 -0
- data/app/models/decidim/forms/question.rb +36 -0
- data/app/models/decidim/forms/questionnaire.rb +23 -0
- data/app/queries/decidim/forms/questionnaire_user_answers.rb +28 -0
- data/app/views/decidim/forms/admin/questionnaires/_answer_option.html.erb +44 -0
- data/app/views/decidim/forms/admin/questionnaires/_form.html.erb +51 -0
- data/app/views/decidim/forms/admin/questionnaires/_question.html.erb +115 -0
- data/app/views/decidim/forms/admin/questionnaires/edit.html.erb +7 -0
- data/app/views/decidim/forms/questionnaires/_answer.html.erb +94 -0
- data/app/views/decidim/forms/questionnaires/show.html.erb +84 -0
- data/config/locales/ca.yml +79 -0
- data/config/locales/de.yml +79 -0
- data/config/locales/en.yml +79 -0
- data/config/locales/es-PY.yml +79 -0
- data/config/locales/es.yml +79 -0
- data/config/locales/eu.yml +79 -0
- data/config/locales/fi-pl.yml +79 -0
- data/config/locales/fi.yml +79 -0
- data/config/locales/fr.yml +79 -0
- data/config/locales/gl.yml +79 -0
- data/config/locales/hu.yml +79 -0
- data/config/locales/id-ID.yml +1 -0
- data/config/locales/it.yml +79 -0
- data/config/locales/nl.yml +79 -0
- data/config/locales/pl.yml +79 -0
- data/config/locales/pt-BR.yml +79 -0
- data/config/locales/pt.yml +79 -0
- data/config/locales/ru.yml +1 -0
- data/config/locales/sv.yml +79 -0
- data/config/locales/tr-TR.yml +1 -0
- data/config/locales/uk.yml +1 -0
- data/db/migrate/20170511092231_create_decidim_forms_questionnaires.rb +15 -0
- data/db/migrate/20170515090916_create_decidim_forms_questions.rb +17 -0
- data/db/migrate/20170515144119_create_decidim_forms_answers.rb +15 -0
- data/db/migrate/20180405015012_create_decidim_forms_answer_options.rb +11 -0
- data/db/migrate/20180405015147_create_decidim_forms_answer_choices.rb +13 -0
- data/lib/decidim/forms.rb +13 -0
- data/lib/decidim/forms/admin.rb +9 -0
- data/lib/decidim/forms/admin_engine.rb +21 -0
- data/lib/decidim/forms/data_portability_user_answers_serializer.rb +37 -0
- data/lib/decidim/forms/engine.rb +14 -0
- data/lib/decidim/forms/test.rb +4 -0
- data/lib/decidim/forms/test/factories.rb +55 -0
- data/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +524 -0
- data/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb +626 -0
- data/lib/decidim/forms/user_answers_serializer.rb +29 -0
- data/lib/decidim/forms/version.rb +10 -0
- 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,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>
|