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