decidim-forms 0.31.7 → 0.31.8
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 +4 -4
- data/app/cells/decidim/forms/step_navigation_cell.rb +2 -1
- data/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +27 -17
- data/app/controllers/decidim/forms/admin/concerns/has_questionnaire_responses.rb +3 -1
- data/app/controllers/decidim/forms/concerns/has_questionnaire.rb +4 -0
- data/app/models/decidim/forms/question.rb +2 -2
- data/app/packs/src/decidim/forms/admin/collapsible_questions.js +0 -10
- data/app/packs/src/decidim/forms/display_conditions.component.js +27 -0
- data/app/packs/src/decidim/forms/display_conditions.component.test.js +142 -0
- data/app/packs/src/decidim/forms/display_conditions_hidden_responses.test.js +224 -0
- data/app/views/decidim/forms/admin/questionnaires/_question.html.erb +1 -1
- data/config/locales/de.yml +1 -0
- data/config/locales/eu.yml +10 -10
- data/config/locales/gl.yml +167 -0
- data/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb +5 -0
- data/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_questions.rb +2 -0
- data/lib/decidim/forms/version.rb +1 -1
- metadata +10 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b84a65887877bd8ffdbb7c872bdd1f2a885cceacbd270d6ecc09eb979d4c99c4
|
|
4
|
+
data.tar.gz: 831bd2c87a75931b4c23177bbfc7885d1fb194aeaa6615f8292632d3f9ece517
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71987f2643d3d28978ef8c6b73a9ae03b92c2ffe180e572d21618e21664a7ffbb498da2fc729e14b5b4ec7c618c790319f2e89eb9d75853cdbd4f56c317d9ab9
|
|
7
|
+
data.tar.gz: 74ae0de2474b4be8e1c504ccd6a1dd678d1b9904ba8f6f10c3569c417c85f6dc7ddf4a3281f27164df455e9b4af66f942f54b33325fe94c7ebdac20fa4182cde
|
|
@@ -20,19 +20,6 @@ module Decidim
|
|
|
20
20
|
helper_method :questionnaire_for, :questionnaire, :blank_question, :blank_response_option, :blank_matrix_row,
|
|
21
21
|
:blank_display_condition, :question_types, :display_condition_types, :update_url, :public_url, :response_options_url, :edit_questionnaire_title
|
|
22
22
|
|
|
23
|
-
if defined?(Decidim::Templates::Admin::Concerns::Templatable)
|
|
24
|
-
include Decidim::Templates::Admin::Concerns::Templatable
|
|
25
|
-
helper Decidim::DatalistSelectHelper
|
|
26
|
-
|
|
27
|
-
def templatable_type
|
|
28
|
-
"Decidim::Forms::Questionnaire"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def templatable
|
|
32
|
-
questionnaire
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
23
|
def edit
|
|
37
24
|
enforce_permission_to(:update, permission_subject, questionnaire:)
|
|
38
25
|
|
|
@@ -88,18 +75,41 @@ module Decidim
|
|
|
88
75
|
end
|
|
89
76
|
end
|
|
90
77
|
|
|
78
|
+
# This endpoint is only used by the questions form rendered by the
|
|
79
|
+
# already authorized "edit_questions" action. The host controllers
|
|
80
|
+
# resolve their resource from params (e.g. "params[:id]") that have
|
|
81
|
+
# a different meaning in this request, so the authorization cannot
|
|
82
|
+
# be delegated to "questionnaire_for" here. Access is already
|
|
83
|
+
# restricted by the admin dashboard routing constraint.
|
|
84
|
+
# rubocop:disable Decidim/EnforcePermissionTo
|
|
91
85
|
def response_options
|
|
92
86
|
respond_to do |format|
|
|
93
87
|
format.json do
|
|
94
88
|
question_id = params["id"]
|
|
95
|
-
question =
|
|
89
|
+
question = questionnaire.questions.find_by(id: question_id)
|
|
96
90
|
render json: question.response_options.map { |response_option| ResponseOptionPresenter.new(response_option).as_json } if question.present?
|
|
97
91
|
end
|
|
98
92
|
end
|
|
99
93
|
end
|
|
94
|
+
# rubocop:enable Decidim/EnforcePermissionTo
|
|
95
|
+
|
|
96
|
+
protected
|
|
97
|
+
|
|
98
|
+
if defined?(Decidim::Templates::Admin::Concerns::Templatable)
|
|
99
|
+
include Decidim::Templates::Admin::Concerns::Templatable
|
|
100
|
+
|
|
101
|
+
helper Decidim::DatalistSelectHelper
|
|
102
|
+
|
|
103
|
+
def templatable_type
|
|
104
|
+
"Decidim::Forms::Questionnaire"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def templatable
|
|
108
|
+
questionnaire
|
|
109
|
+
end
|
|
110
|
+
end
|
|
100
111
|
|
|
101
|
-
#
|
|
102
|
-
# return the object that will hold the questionnaire.
|
|
112
|
+
# You need to return the object that will hold the questionnaire.
|
|
103
113
|
def questionnaire_for
|
|
104
114
|
raise "#{self.class.name} is expected to implement #questionnaire_for"
|
|
105
115
|
end
|
|
@@ -155,7 +165,7 @@ module Decidim
|
|
|
155
165
|
end
|
|
156
166
|
|
|
157
167
|
def questionnaire
|
|
158
|
-
@questionnaire ||= Questionnaire.find_by(questionnaire_for:)
|
|
168
|
+
@questionnaire ||= Questionnaire.find_by(questionnaire_for:) # rubocop:disable Decidim/OrganizationScopedFinder -- questionnaire_for is scoped by the host controller
|
|
159
169
|
end
|
|
160
170
|
|
|
161
171
|
def blank_question
|
|
@@ -60,6 +60,8 @@ module Decidim
|
|
|
60
60
|
redirect_back(fallback_location: questionnaire_participant_responses_url(session_token))
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
protected
|
|
64
|
+
|
|
63
65
|
# Public: The only method to be implemented at the controller. You need to
|
|
64
66
|
# return the object that will hold the questionnaire.
|
|
65
67
|
def questionnaire_for
|
|
@@ -77,7 +79,7 @@ module Decidim
|
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
def questionnaire
|
|
80
|
-
@questionnaire ||= Questionnaire.find_by(questionnaire_for:)
|
|
82
|
+
@questionnaire ||= Questionnaire.find_by(questionnaire_for:) # rubocop:disable Decidim/OrganizationScopedFinder -- questionnaire_for is scoped by the host controller
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
def participants_query
|
|
@@ -22,6 +22,8 @@ module Decidim
|
|
|
22
22
|
invisible_captcha on_spam: :spam_detected
|
|
23
23
|
|
|
24
24
|
def show
|
|
25
|
+
enforce_permission_to :read, :component
|
|
26
|
+
|
|
25
27
|
@form = form(Decidim::Forms::QuestionnaireForm).from_model(questionnaire)
|
|
26
28
|
render template:
|
|
27
29
|
end
|
|
@@ -46,6 +48,8 @@ module Decidim
|
|
|
46
48
|
end
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
private
|
|
52
|
+
|
|
49
53
|
def template
|
|
50
54
|
"decidim/forms/questionnaires/show"
|
|
51
55
|
end
|
|
@@ -39,13 +39,13 @@ module Decidim
|
|
|
39
39
|
class_name: "DisplayCondition",
|
|
40
40
|
foreign_key: "decidim_condition_question_id",
|
|
41
41
|
dependent: :destroy,
|
|
42
|
-
inverse_of: :
|
|
42
|
+
inverse_of: :condition_question,
|
|
43
43
|
counter_cache: :display_conditions_for_other_questions_count
|
|
44
44
|
|
|
45
45
|
# Questions which have display conditions based on the value of this question's response
|
|
46
46
|
has_many :conditioned_questions,
|
|
47
47
|
through: :display_conditions_for_other_questions,
|
|
48
|
-
|
|
48
|
+
source: :question,
|
|
49
49
|
class_name: "Question"
|
|
50
50
|
|
|
51
51
|
has_many :responses,
|
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
document.addEventListener("turbo:load", () => {
|
|
2
|
-
const getButtons = document.querySelectorAll("button.question--collapse");
|
|
3
|
-
|
|
4
|
-
setTimeout(() => {
|
|
5
|
-
getButtons.forEach((button) => {
|
|
6
|
-
if (button.classList.contains("question-error")) {
|
|
7
|
-
button.click();
|
|
8
|
-
}
|
|
9
|
-
});
|
|
10
|
-
}, 100);
|
|
11
|
-
|
|
12
2
|
document.querySelector("button.collapse-all")?.addEventListener("click", () => {
|
|
13
3
|
document.querySelectorAll("[id$=field] button.question--collapse[aria-expanded='true']").
|
|
14
4
|
forEach((button) => button.click());
|
|
@@ -15,12 +15,23 @@ class DisplayCondition {
|
|
|
15
15
|
bindEvent() {
|
|
16
16
|
this.checkCondition();
|
|
17
17
|
this.getInputsToListen().on("change", this.checkCondition.bind(this));
|
|
18
|
+
|
|
19
|
+
const conditionWrapper = document.querySelector(`.question[data-question-id='${this.conditionQuestion}']`);
|
|
20
|
+
conditionWrapper?.addEventListener("display-conditions:visibility", this.checkCondition.bind(this));
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
getInputValue() {
|
|
21
24
|
const $conditionWrapperField = $(`.question[data-question-id='${this.conditionQuestion}']`);
|
|
22
25
|
const $textInput = $conditionWrapperField.find("textarea, input[type='text']:not([name$=\\[custom_body\\]])");
|
|
23
26
|
|
|
27
|
+
// A hidden question counts as unanswered, like the server sees it: its inputs are
|
|
28
|
+
// disabled, so it submits nothing. The response itself is left alone.
|
|
29
|
+
if ($conditionWrapperField.is("[data-condition-hidden]")) {
|
|
30
|
+
return $textInput.length
|
|
31
|
+
? ""
|
|
32
|
+
: [];
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
if ($textInput.length) {
|
|
25
36
|
return $textInput.val();
|
|
26
37
|
}
|
|
@@ -181,6 +192,7 @@ class DisplayConditionsComponent {
|
|
|
181
192
|
this.wrapperField.fadeIn();
|
|
182
193
|
this.wrapperField.find("input, textarea").prop("disabled", null);
|
|
183
194
|
this.showCount++;
|
|
195
|
+
this.setHidden(false);
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
hideQuestion() {
|
|
@@ -192,6 +204,21 @@ class DisplayConditionsComponent {
|
|
|
192
204
|
}
|
|
193
205
|
|
|
194
206
|
this.wrapperField.find("input, textarea").prop("disabled", "disabled");
|
|
207
|
+
this.setHidden(true);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// A hidden question stops counting as answered, so questions conditioned on this
|
|
211
|
+
// one have to re-evaluate, which the visibility event asks them to do. Only a real
|
|
212
|
+
// change is propagated, so the cascade settles after one pass and cannot loop.
|
|
213
|
+
setHidden(hidden) {
|
|
214
|
+
const [wrapper] = this.wrapperField;
|
|
215
|
+
|
|
216
|
+
if (wrapper.hasAttribute("data-condition-hidden") === hidden) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
wrapper.toggleAttribute("data-condition-hidden", hidden);
|
|
221
|
+
wrapper.dispatchEvent(new CustomEvent("display-conditions:visibility"));
|
|
195
222
|
}
|
|
196
223
|
}
|
|
197
224
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import $ from "jquery"; // eslint-disable-line id-length
|
|
2
|
+
|
|
3
|
+
// Relative import: the forms pack is not in jest's moduleDirectories, so it
|
|
4
|
+
// cannot be resolved through the absolute "src/..." path used elsewhere.
|
|
5
|
+
import createDisplayConditions from "./display_conditions.component"; // eslint-disable-line no-relative-import-paths/no-relative-import-paths
|
|
6
|
+
|
|
7
|
+
describe("DisplayConditionsComponent", () => {
|
|
8
|
+
// Renders a single-choice (radio) question with the DOM structure the
|
|
9
|
+
// component expects (js-collection-input wrapping body/custom_body/option_id).
|
|
10
|
+
const radioCollection = (qid, options) => `
|
|
11
|
+
<div class="js-radio-button-collection">
|
|
12
|
+
${options.map((opt) => `
|
|
13
|
+
<div class="js-collection-input">
|
|
14
|
+
<input name="resp[${qid}][body]" type="radio" value="${opt.value}" />
|
|
15
|
+
<input name="resp[${qid}][custom_body]" type="text" />
|
|
16
|
+
<input name="resp[${qid}][response_option_id]" type="hidden" value="${opt.optionId}" />
|
|
17
|
+
</div>
|
|
18
|
+
`).join("")}
|
|
19
|
+
</div>
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const conditionTag = (data) => {
|
|
23
|
+
const attrs = Object.entries(data).map(([key, val]) => `data-${key}='${val}'`).join(" ");
|
|
24
|
+
return `<div class="display-condition" ${attrs}></div>`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Q0 (A/B/C) --C--> Q3 (0/1) --0--> FINAL0
|
|
28
|
+
// \--1--> FINAL1
|
|
29
|
+
const content = `
|
|
30
|
+
<div class="answer-questionnaire">
|
|
31
|
+
<div class="question" data-question-id="Q0" data-conditioned="false">
|
|
32
|
+
${radioCollection("Q0", [{ optionId: "optA", value: "A" }, { optionId: "optB", value: "B" }, { optionId: "optC", value: "C" }])}
|
|
33
|
+
</div>
|
|
34
|
+
<div class="question" data-question-id="Q3" data-conditioned="true">
|
|
35
|
+
${conditionTag({ id: "dcQ3", type: "equal", condition: "Q0", option: "optC", mandatory: false })}
|
|
36
|
+
${radioCollection("Q3", [{ optionId: "opt0", value: "0" }, { optionId: "opt1", value: "1" }])}
|
|
37
|
+
</div>
|
|
38
|
+
<div class="question" data-question-id="FINAL0" data-conditioned="true">
|
|
39
|
+
${conditionTag({ id: "dcF0", type: "equal", condition: "Q3", option: "opt0", mandatory: false })}
|
|
40
|
+
${radioCollection("FINAL0", [{ optionId: "f0opt", value: "yes" }])}
|
|
41
|
+
</div>
|
|
42
|
+
<div class="question" data-question-id="FINAL1" data-conditioned="true">
|
|
43
|
+
${conditionTag({ id: "dcF1", type: "equal", condition: "Q3", option: "opt1", mandatory: false })}
|
|
44
|
+
${radioCollection("FINAL1", [{ optionId: "f1opt", value: "yes" }])}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const wrapper = (qid) => $(`.question[data-question-id='${qid}']`);
|
|
50
|
+
// The component enables inputs when a question is shown and disables them when
|
|
51
|
+
// hidden, so the disabled state is a reliable proxy for visibility in jsdom.
|
|
52
|
+
const isVisible = (qid) => !wrapper(qid).find("input[name$='[body]']").first().prop("disabled");
|
|
53
|
+
const selectOption = (qid, value) => {
|
|
54
|
+
const $input = wrapper(qid).find(`input[name$='[body]'][value='${value}']`);
|
|
55
|
+
$input.prop("checked", true);
|
|
56
|
+
$input.trigger("change");
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
document.body.innerHTML = content;
|
|
61
|
+
$(".answer-questionnaire .question[data-conditioned='true']").each((idx, el) => {
|
|
62
|
+
createDisplayConditions({ wrapperField: $(el) });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("keeps conditioned questions hidden until their trigger is fulfilled", () => {
|
|
67
|
+
expect(isVisible("Q3")).toBe(false);
|
|
68
|
+
expect(isVisible("FINAL0")).toBe(false);
|
|
69
|
+
expect(isVisible("FINAL1")).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("shows only the directly conditioned question when its trigger is met", () => {
|
|
73
|
+
selectOption("Q0", "C");
|
|
74
|
+
|
|
75
|
+
expect(isVisible("Q3")).toBe(true);
|
|
76
|
+
// Q3 is not answered yet, so neither final must appear
|
|
77
|
+
expect(isVisible("FINAL0")).toBe(false);
|
|
78
|
+
expect(isVisible("FINAL1")).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("shows the matching final once the intermediate question is answered", () => {
|
|
82
|
+
selectOption("Q0", "C");
|
|
83
|
+
selectOption("Q3", "1");
|
|
84
|
+
|
|
85
|
+
expect(isVisible("FINAL1")).toBe(true);
|
|
86
|
+
expect(isVisible("FINAL0")).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("cascades hiding down the chain when an ancestor trigger stops being fulfilled", () => {
|
|
90
|
+
selectOption("Q0", "C");
|
|
91
|
+
selectOption("Q3", "1");
|
|
92
|
+
expect(isVisible("FINAL1")).toBe(true);
|
|
93
|
+
|
|
94
|
+
// Move Q0 away from C: Q3 hides and stops counting as answered, and the
|
|
95
|
+
// visibility change propagates so FINAL1 hides too instead of lingering.
|
|
96
|
+
selectOption("Q0", "A");
|
|
97
|
+
|
|
98
|
+
expect(isVisible("Q3")).toBe(false);
|
|
99
|
+
expect(isVisible("FINAL1")).toBe(false);
|
|
100
|
+
// Q3's response is kept, so showing it again restores the chain as it was
|
|
101
|
+
expect(wrapper("Q3").find("input[value='1']").prop("checked")).toBe(true);
|
|
102
|
+
|
|
103
|
+
selectOption("Q0", "C");
|
|
104
|
+
|
|
105
|
+
expect(isVisible("Q3")).toBe(true);
|
|
106
|
+
expect(isVisible("FINAL1")).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("keeps file upload responses of a hidden question out of the submission without destroying them", () => {
|
|
110
|
+
document.body.innerHTML = `
|
|
111
|
+
<div class="answer-questionnaire">
|
|
112
|
+
<div class="question" data-question-id="Q0" data-conditioned="false">
|
|
113
|
+
${radioCollection("Q0", [{ optionId: "optA", value: "A" }, { optionId: "optC", value: "C" }])}
|
|
114
|
+
</div>
|
|
115
|
+
<div class="question" data-question-id="QFILE" data-conditioned="true">
|
|
116
|
+
${conditionTag({ id: "dcFile", type: "equal", condition: "Q0", option: "optC", mandatory: false })}
|
|
117
|
+
<div class="upload-modal__files" data-active-uploads="QFILE">
|
|
118
|
+
<div class="attachment-details" data-filename="doc.pdf" data-state="uploaded">
|
|
119
|
+
<input name="resp[QFILE][add_attachments]" type="hidden" value="signed-id-123" />
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
`;
|
|
125
|
+
$(".answer-questionnaire .question[data-conditioned='true']").each((idx, el) => {
|
|
126
|
+
createDisplayConditions({ wrapperField: $(el) });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Q0 is not "C", so QFILE is hidden. The attachment stays in the DOM, but it is
|
|
130
|
+
// disabled, so nothing stale is submitted and nothing already uploaded is lost.
|
|
131
|
+
const $hiddenField = wrapper("QFILE").find("input[name='resp[QFILE][add_attachments]']");
|
|
132
|
+
|
|
133
|
+
expect(wrapper("QFILE").find(".attachment-details").length).toBe(1);
|
|
134
|
+
expect($hiddenField.length).toBe(1);
|
|
135
|
+
expect($hiddenField.prop("disabled")).toBe(true);
|
|
136
|
+
|
|
137
|
+
// and it is submitted again once the question is shown
|
|
138
|
+
selectOption("Q0", "C");
|
|
139
|
+
|
|
140
|
+
expect($hiddenField.prop("disabled")).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/* eslint-disable no-relative-import-paths/no-relative-import-paths, id-length */
|
|
2
|
+
//
|
|
3
|
+
// Covers what happens to a hidden question's response, for the two question types
|
|
4
|
+
// the display-conditions component cannot clear by hand: `files` (its response
|
|
5
|
+
// lives in UploadModal's markup) and `sorting` (hidden fields only).
|
|
6
|
+
//
|
|
7
|
+
// On the clearAnswers() implementation, the three cases about an already-saved
|
|
8
|
+
// attachment fail: its preview and hidden add_attachments field are removed, and
|
|
9
|
+
// showQuestion() never puts them back. The other two pass there as well, and show
|
|
10
|
+
// why the selector was inconsistent: a file uploaded in the current session, and a
|
|
11
|
+
// sorting question, were never cleared at all.
|
|
12
|
+
//
|
|
13
|
+
// The fixture reproduces the markup actually rendered to participants:
|
|
14
|
+
// decidim-core/app/cells/decidim/upload_modal/files.erb
|
|
15
|
+
// decidim-core/app/cells/decidim/upload_modal/modal.erb
|
|
16
|
+
// decidim-forms/app/views/decidim/forms/questionnaires/responses/_files.html.erb
|
|
17
|
+
// decidim-forms/app/views/decidim/forms/questionnaires/responses/_sorting.html.erb
|
|
18
|
+
// and drives it through the real initializeUploadFields() and
|
|
19
|
+
// createDisplayConditions(), so no behaviour is stubbed.
|
|
20
|
+
//
|
|
21
|
+
// Run with: npx jest decidim-forms/app/packs/src/decidim/forms/display_conditions_hidden_responses.test.js
|
|
22
|
+
//
|
|
23
|
+
|
|
24
|
+
import $ from "jquery";
|
|
25
|
+
import createDisplayConditions from "./display_conditions.component";
|
|
26
|
+
import { initializeUploadFields } from "src/decidim/direct_uploads/upload_field";
|
|
27
|
+
|
|
28
|
+
const MODAL_ID = "modal-qfile";
|
|
29
|
+
// Raw JSON because the keys are the snake_case ones upload_modal/modal.erb emits.
|
|
30
|
+
const LOCALES = `{
|
|
31
|
+
"error": "error", "title_required": "title required", "filename": "filename",
|
|
32
|
+
"file_size_too_large": "too large", "remove": "remove", "title": "title",
|
|
33
|
+
"uploaded": "uploaded", "validating": "validating", "validation_error": "validation error"
|
|
34
|
+
}`;
|
|
35
|
+
const UPLOAD_OPTS = JSON.stringify({
|
|
36
|
+
addAttribute: "add_attachments",
|
|
37
|
+
resourceName: "questionnaire",
|
|
38
|
+
resourceClass: "Decidim::Forms::Questionnaire",
|
|
39
|
+
required: false,
|
|
40
|
+
maxFileSize: 10485760,
|
|
41
|
+
multiple: true,
|
|
42
|
+
titled: false,
|
|
43
|
+
formObjectClass: "Decidim::Forms::QuestionnaireForm"
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const radioCollection = (qid, options) => `
|
|
47
|
+
<div class="js-radio-button-collection">
|
|
48
|
+
${options.map((opt) => `
|
|
49
|
+
<div class="js-collection-input">
|
|
50
|
+
<input name="questionnaire[responses][${qid}][body]" type="radio" value="${opt.value}" />
|
|
51
|
+
<input name="questionnaire[responses][${qid}][custom_body]" type="text" />
|
|
52
|
+
<input name="questionnaire[responses][${qid}][response_option_id]" type="hidden" value="${opt.optionId}" />
|
|
53
|
+
</div>
|
|
54
|
+
`).join("")}
|
|
55
|
+
</div>
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const conditionTag = (data) =>
|
|
59
|
+
`<div class="display-condition" ${Object.entries(data).map(([key, val]) => `data-${key}='${val}'`).join(" ")}></div>`;
|
|
60
|
+
|
|
61
|
+
// Mirrors upload_modal/files.erb for one already-persisted, non-image attachment.
|
|
62
|
+
const uploadMarkup = () => `
|
|
63
|
+
<div class="upload-modal__files-container upload-container-for-add_attachments">
|
|
64
|
+
<div>
|
|
65
|
+
<div class="upload-modal__files" data-active-uploads="${MODAL_ID}">
|
|
66
|
+
<div class="attachment-details" data-attachment-id="99" data-title="doc.pdf" data-filename="doc.pdf" data-state="uploaded" data-hidden-field="signed-id-abc">
|
|
67
|
+
<a href="/rails/active_storage/blobs/doc.pdf">doc.pdf</a>
|
|
68
|
+
<input type="hidden" name="questionnaire[responses][QFILE][add_attachments]" value="99" id="hidden_add_attachments_99" />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<button id="button-${MODAL_ID}" name="add_attachments" type="button" data-dialog-open="${MODAL_ID}" data-upload='${UPLOAD_OPTS}'>Add file</button>
|
|
73
|
+
</div>
|
|
74
|
+
<div id="${MODAL_ID}" data-dialog="${MODAL_ID}">
|
|
75
|
+
<div id="${MODAL_ID}-content" class="upload-modal">
|
|
76
|
+
<div data-dialog-container>
|
|
77
|
+
<h2 data-dialog-title data-addlabel="Add file" data-editlabel="Edit file">Add file</h2>
|
|
78
|
+
<div data-name="add_attachments" data-dropzone>
|
|
79
|
+
<input id="files-${MODAL_ID}" type="file" multiple hidden />
|
|
80
|
+
<ul hidden data-dropzone-items="" data-locales='${LOCALES}'></ul>
|
|
81
|
+
<div data-dropzone-no-items="">
|
|
82
|
+
<label for="files-${MODAL_ID}"><span>Select file</span></label>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div data-dialog-actions>
|
|
87
|
+
<button type="button" data-dropzone-cancel data-dialog-close="${MODAL_ID}">Cancel</button>
|
|
88
|
+
<button type="button" data-dropzone-save data-dialog-close="${MODAL_ID}" disabled>Save</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
// Mirrors responses/_sorting.html.erb
|
|
95
|
+
const sortingMarkup = (qid) => `
|
|
96
|
+
<div class="response-questionnaire__sorting-container js-sortable-check-box-collection">
|
|
97
|
+
${[["10", "1"], ["11", "2"], ["12", "3"]].map(([optId, pos], idx) => `
|
|
98
|
+
<div class="response-questionnaire__sorting js-collection-input" role="button">
|
|
99
|
+
<input type="hidden" name="questionnaire[responses][${qid}][choices][${idx}][position]" value="${pos}" />
|
|
100
|
+
<input type="hidden" name="questionnaire[responses][${qid}][choices][${idx}][body]" value="opt${optId}" />
|
|
101
|
+
<input type="hidden" name="questionnaire[responses][${qid}][choices][${idx}][response_option_id]" value="${optId}" />
|
|
102
|
+
</div>
|
|
103
|
+
`).join("")}
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const page = () => `
|
|
108
|
+
<form class="response-questionnaire" data-safe-path="/f">
|
|
109
|
+
<div class="question" data-question-id="Q0" data-conditioned="false">
|
|
110
|
+
${radioCollection("Q0", [{ optionId: "optA", value: "A" }, { optionId: "optC", value: "C" }])}
|
|
111
|
+
</div>
|
|
112
|
+
<div class="question" data-question-id="QFILE" data-conditioned="true">
|
|
113
|
+
${conditionTag({ id: "dcFile", type: "equal", condition: "Q0", option: "optC", mandatory: false })}
|
|
114
|
+
${uploadMarkup()}
|
|
115
|
+
<input type="hidden" name="questionnaire[responses][QFILE][question_id]" value="QFILE" />
|
|
116
|
+
</div>
|
|
117
|
+
<div class="question" data-question-id="QSORT" data-conditioned="true">
|
|
118
|
+
${conditionTag({ id: "dcSort", type: "equal", condition: "Q0", option: "optC", mandatory: false })}
|
|
119
|
+
${sortingMarkup("QSORT")}
|
|
120
|
+
<input type="hidden" name="questionnaire[responses][QSORT][question_id]" value="QSORT" />
|
|
121
|
+
</div>
|
|
122
|
+
</form>
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
const initUploads = () => initializeUploadFields(document.querySelectorAll("button[data-upload]"));
|
|
126
|
+
const initConditions = () =>
|
|
127
|
+
$(".response-questionnaire .question[data-conditioned='true']").each((idx, el) =>
|
|
128
|
+
createDisplayConditions({ wrapperField: $(el) })
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const wrapper = (qid) => $(`.question[data-question-id='${qid}']`);
|
|
132
|
+
const selectOption = (qid, value) => {
|
|
133
|
+
const $input = wrapper(qid).find(`input[name$='[body]'][value='${value}']`);
|
|
134
|
+
$input.prop("checked", true);
|
|
135
|
+
$input.trigger("change");
|
|
136
|
+
};
|
|
137
|
+
// What the browser would actually submit: enabled fields only.
|
|
138
|
+
const submitted = () =>
|
|
139
|
+
Array.from(document.querySelectorAll("form input:not([disabled])")).
|
|
140
|
+
filter((el) => el.name && (!["radio", "checkbox"].includes(el.type) || el.checked)).
|
|
141
|
+
map((el) => `${el.name}=${el.value}`);
|
|
142
|
+
// Any preview node holding the hidden add_attachments field, whether server
|
|
143
|
+
// rendered (.attachment-details) or re-rendered by updateActiveUploads (no class).
|
|
144
|
+
const previews = () => document.querySelectorAll(`[data-active-uploads='${MODAL_ID}'] > *`).length;
|
|
145
|
+
const serverRenderedPreviews = () => document.querySelectorAll(`[data-active-uploads='${MODAL_ID}'] .attachment-details`).length;
|
|
146
|
+
const modalItems = () => document.querySelectorAll(`#${MODAL_ID} [data-dropzone-items] > *`).length;
|
|
147
|
+
const buttonLabel = () => document.getElementById(`button-${MODAL_ID}`).innerHTML;
|
|
148
|
+
const positions = () =>
|
|
149
|
+
Array.from(document.querySelectorAll("[data-question-id='QSORT'] input[name$='[position]']")).map((el) => el.value);
|
|
150
|
+
|
|
151
|
+
describe("hidden questions and their responses", () => {
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
// UploadModal renders icons through window.Decidim.config
|
|
154
|
+
window.Decidim = { config: { get: () => "/icons.svg" } };
|
|
155
|
+
document.body.innerHTML = page();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("a file question hidden on page load", () => {
|
|
159
|
+
it("keeps the attachment and UploadModal in sync, whichever initializes first", () => {
|
|
160
|
+
initUploads();
|
|
161
|
+
initConditions();
|
|
162
|
+
|
|
163
|
+
expect(previews()).toBe(1);
|
|
164
|
+
expect(serverRenderedPreviews()).toBe(1);
|
|
165
|
+
expect(modalItems()).toBe(1);
|
|
166
|
+
expect(buttonLabel()).toBe("Add file");
|
|
167
|
+
// kept, but disabled, so it is not submitted while the question is hidden
|
|
168
|
+
expect(submitted().filter((field) => field.includes("add_attachments"))).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("behaves the same when the display conditions initialize first", () => {
|
|
172
|
+
initConditions();
|
|
173
|
+
initUploads();
|
|
174
|
+
|
|
175
|
+
expect(previews()).toBe(1);
|
|
176
|
+
expect(modalItems()).toBe(1);
|
|
177
|
+
expect(submitted().filter((field) => field.includes("add_attachments"))).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("a file question hidden and shown again", () => {
|
|
182
|
+
it("submits the attachment it already had", () => {
|
|
183
|
+
initUploads();
|
|
184
|
+
initConditions();
|
|
185
|
+
|
|
186
|
+
selectOption("Q0", "C");
|
|
187
|
+
|
|
188
|
+
expect(previews()).toBe(1);
|
|
189
|
+
expect(submitted()).toContain("questionnaire[responses][QFILE][add_attachments]=99");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("submits a file uploaded in this session, whose preview carries no attachment-details class", () => {
|
|
193
|
+
initUploads();
|
|
194
|
+
initConditions();
|
|
195
|
+
selectOption("Q0", "C");
|
|
196
|
+
|
|
197
|
+
// saving the modal makes updateActiveUploads() re-render the preview
|
|
198
|
+
document.getElementById(`button-${MODAL_ID}`).click();
|
|
199
|
+
document.querySelector(`#${MODAL_ID} [data-dropzone-save]`).click();
|
|
200
|
+
expect(serverRenderedPreviews()).toBe(0);
|
|
201
|
+
|
|
202
|
+
selectOption("Q0", "A");
|
|
203
|
+
selectOption("Q0", "C");
|
|
204
|
+
|
|
205
|
+
expect(previews()).toBe(1);
|
|
206
|
+
expect(submitted().filter((field) => field.includes("add_attachments"))).not.toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("a sorting question", () => {
|
|
211
|
+
it("keeps its order out of the submission while hidden, and restores it when shown", () => {
|
|
212
|
+
initUploads();
|
|
213
|
+
initConditions();
|
|
214
|
+
|
|
215
|
+
expect(positions()).toEqual(["1", "2", "3"]);
|
|
216
|
+
expect(submitted().filter((field) => field.includes("QSORT"))).toEqual([]);
|
|
217
|
+
|
|
218
|
+
selectOption("Q0", "C");
|
|
219
|
+
|
|
220
|
+
expect(positions()).toEqual(["1", "2", "3"]);
|
|
221
|
+
expect(submitted()).toContain("questionnaire[responses][QSORT][choices][0][position]=1");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</span>
|
|
13
13
|
|
|
14
14
|
<span class="ml-auto flex flex-row-reverse items-center gap-2">
|
|
15
|
-
<button type="button" class="question--collapse
|
|
15
|
+
<button type="button" class="question--collapse" data-controls="panel-<%= id %>-question-card" data-open="<%= is_expanded %>">
|
|
16
16
|
<span class="icon-collapse">
|
|
17
17
|
<%== icon("arrow-up-s-line", aria_label: t(".collapse"), role: "img") %>
|
|
18
18
|
</span>
|
data/config/locales/de.yml
CHANGED
|
@@ -44,6 +44,7 @@ de:
|
|
|
44
44
|
allow_editing_responses: Registrierten Benutzern die Bearbeitung eigener Umfrageantworten ermöglichen
|
|
45
45
|
allow_responses: Antworten erlauben
|
|
46
46
|
allow_unregistered: Teilnahme an der Umfrage ohne Registrierung erlauben
|
|
47
|
+
allow_unregistered_help: Wenn aktiviert, ist keine Anmeldung erforderlich, um die Umfrage zu beantworten. Dies kann zu ungenauen oder unzuverlässigen Daten führen und macht die Umfrage anfälliger für automatisierte Angriffe. Mit Vorsicht verwenden! Beachten Sie, dass dieselbe Umfrage von einer teilnehmenden Person mehrfach beantwortet werden kann, etwa durch die Verwendung unterschiedlicher Browser oder der Funktion "Privates Surfen" des Webbrowsers.
|
|
47
48
|
announcement: Ankündigung
|
|
48
49
|
clean_after_publish: Antworten beim Veröffentlichen der Umfrage löschen
|
|
49
50
|
description: Beschreibung
|
data/config/locales/eu.yml
CHANGED
|
@@ -31,7 +31,7 @@ eu:
|
|
|
31
31
|
responses:
|
|
32
32
|
id: Erantzunaren identifikatzaile bakarra
|
|
33
33
|
question: Erantzun den galdera
|
|
34
|
-
questionnaire:
|
|
34
|
+
questionnaire: Galdetegia erantzuna izan da
|
|
35
35
|
response: Galderaren erantzuna
|
|
36
36
|
user: Inkesta erantzun duen parte-hartzailea
|
|
37
37
|
show:
|
|
@@ -48,9 +48,9 @@ eu:
|
|
|
48
48
|
announcement: Oharra
|
|
49
49
|
clean_after_publish: Ezabatu erantzunak inkesta argitaratzean
|
|
50
50
|
description: Deskribapena
|
|
51
|
-
ends_at: Erantzunak noiz arte
|
|
51
|
+
ends_at: Erantzunak noiz arte onartua
|
|
52
52
|
ends_at_help: Utzi zuriz data zehatzik ez badago
|
|
53
|
-
starts_at: Erantzunak noiztik
|
|
53
|
+
starts_at: Erantzunak noiztik onartua
|
|
54
54
|
starts_at_help: Zuriz utzi data zehatzik ez badago
|
|
55
55
|
tos: Zerbitzu-baldintzak
|
|
56
56
|
questionnaires:
|
|
@@ -70,7 +70,7 @@ eu:
|
|
|
70
70
|
condition_value: Testua barne
|
|
71
71
|
display_condition: Bistaratzeko baldintza
|
|
72
72
|
mandatory: Baldintza hau beti bete behar da, beste baldintza batzuen egoera edozein dela ere
|
|
73
|
-
remove:
|
|
73
|
+
remove: Ezabatu
|
|
74
74
|
response_option: Erantzuteko aukera
|
|
75
75
|
save_warning: Gogoratu formularioa gorde behar duzula bistaratzeko baldintzak konfiguratu aurretik
|
|
76
76
|
select_condition_question: Hautatu galdera bat
|
|
@@ -78,7 +78,7 @@ eu:
|
|
|
78
78
|
select_response_option: Hautatu erantzuteko aukera
|
|
79
79
|
edit:
|
|
80
80
|
save: Gorde
|
|
81
|
-
title: Galdetegia
|
|
81
|
+
title: Galdetegia editatu
|
|
82
82
|
edit_questions:
|
|
83
83
|
add_question: Gehitu galdera
|
|
84
84
|
add_separator: Gehitu banatzailea
|
|
@@ -89,11 +89,11 @@ eu:
|
|
|
89
89
|
add_question: Gehitu galdera
|
|
90
90
|
collapse: Bildu galdera guztiak
|
|
91
91
|
expand: Zabaldu galdera guztiak
|
|
92
|
-
preview:
|
|
92
|
+
preview: Aurrebistaratu
|
|
93
93
|
title: Editatu galdetegia honetarako %{questionnaire_for}
|
|
94
94
|
matrix_row:
|
|
95
95
|
matrix_row: Errenkada
|
|
96
|
-
remove:
|
|
96
|
+
remove: Ezabatu
|
|
97
97
|
statement: Adierazpena
|
|
98
98
|
question:
|
|
99
99
|
add_display_condition: Bistaratzeko baldintza gehitu
|
|
@@ -105,7 +105,7 @@ eu:
|
|
|
105
105
|
description: Deskribapena
|
|
106
106
|
expand: Zabaldu
|
|
107
107
|
question: Galdera
|
|
108
|
-
remove:
|
|
108
|
+
remove: Ezabatu
|
|
109
109
|
statement: Adierazpena
|
|
110
110
|
questions_form:
|
|
111
111
|
already_responded_warning: Formularioa parte-hartzaile batzuek erantzuten dute dagoeneko; beraz, ezin dituzu galderak aldatu.
|
|
@@ -116,7 +116,7 @@ eu:
|
|
|
116
116
|
success: Inkestaren galderak zuzen gordeta.
|
|
117
117
|
response_option:
|
|
118
118
|
free_text: Testu librea
|
|
119
|
-
remove:
|
|
119
|
+
remove: Ezabatu
|
|
120
120
|
response_option: Erantzuteko aukera
|
|
121
121
|
statement: Adierazpena
|
|
122
122
|
responses:
|
|
@@ -137,7 +137,7 @@ eu:
|
|
|
137
137
|
show:
|
|
138
138
|
title: '#%{number} erantzun'
|
|
139
139
|
separator:
|
|
140
|
-
remove:
|
|
140
|
+
remove: Ezabatu
|
|
141
141
|
separator: Bereizlea
|
|
142
142
|
title_and_description:
|
|
143
143
|
collapse: Kolapsoa
|
data/config/locales/gl.yml
CHANGED
|
@@ -7,27 +7,136 @@ gl:
|
|
|
7
7
|
question_type: Tipo
|
|
8
8
|
questionnaire_question:
|
|
9
9
|
mandatory: Obrigatorio
|
|
10
|
+
max_characters: Límite de caracteres (deixa 0 se non hai límite)
|
|
11
|
+
response:
|
|
12
|
+
body: Resposta
|
|
13
|
+
choices: Opcións
|
|
14
|
+
selected_choices: Opcións seleccionadas
|
|
15
|
+
errors:
|
|
16
|
+
models:
|
|
17
|
+
questionnaire:
|
|
18
|
+
request_invalid: Produciuse un problema ao procesar a solicitude. Téntao de novo.
|
|
19
|
+
response:
|
|
20
|
+
attributes:
|
|
21
|
+
add_documents:
|
|
22
|
+
needs_to_be_reattached: Debe anexarse de novo
|
|
23
|
+
body:
|
|
24
|
+
too_long: é demasiado longo
|
|
25
|
+
choices:
|
|
26
|
+
missing: non están completas
|
|
27
|
+
too_many: Podes escoller un máximo de %{count}.
|
|
10
28
|
decidim:
|
|
29
|
+
download_your_data:
|
|
30
|
+
help:
|
|
31
|
+
responses:
|
|
32
|
+
id: O identificador único da resposta
|
|
33
|
+
question: A pregunta que se respondeu
|
|
34
|
+
questionnaire: O cuestionario que se respondeu
|
|
35
|
+
response: A resposta á pregunta
|
|
36
|
+
user: A persoa participante que respondeu ao cuestionario
|
|
37
|
+
show:
|
|
38
|
+
responses: Exportación de respostas
|
|
39
|
+
survey_user_responses: Respostas das persoas participantes á enquisa
|
|
11
40
|
forms:
|
|
12
41
|
admin:
|
|
13
42
|
models:
|
|
14
43
|
components:
|
|
44
|
+
allow_editing_responses: Permitir que as persoas participantes rexistradas editen as súas propias respostas á enquisa
|
|
45
|
+
allow_responses: Permitir respostas
|
|
46
|
+
allow_unregistered: Permitir que persoas participantes non rexistradas respondan á enquisa
|
|
47
|
+
allow_unregistered_help: Se está activado, non será necesario iniciar sesión para responder á enquisa. Isto pode producir datos de baixa calidade ou pouco fiables e aumentar a vulnerabilidade fronte a ataques automatizados. Úsao con precaución. Ten en conta que unha persoa participante podería responder á mesma enquisa varias veces usando navegadores distintos ou a función de «navegación privada» do navegador.
|
|
48
|
+
announcement: Aviso
|
|
15
49
|
description: Descrición
|
|
50
|
+
ends_at: Respostas aceptadas ata
|
|
51
|
+
ends_at_help: Déixao en branco se non hai unha data específica
|
|
52
|
+
starts_at: Respostas aceptadas desde
|
|
53
|
+
starts_at_help: Déixao en branco se non hai unha data específica
|
|
16
54
|
tos: Condicións de servicio
|
|
17
55
|
questionnaires:
|
|
56
|
+
actions:
|
|
57
|
+
back: Volver ás respostas
|
|
58
|
+
publish_responses: Publicar respostas
|
|
59
|
+
show: Respostas
|
|
60
|
+
display_condition:
|
|
61
|
+
condition_question: Pregunta
|
|
62
|
+
condition_type: Condición
|
|
63
|
+
condition_types:
|
|
64
|
+
equal: Igual
|
|
65
|
+
match: Inclúe texto
|
|
66
|
+
not_equal: Distinto
|
|
67
|
+
not_responded: Sen responder
|
|
68
|
+
responded: Respondida
|
|
69
|
+
condition_value: Texto incluído
|
|
70
|
+
display_condition: Condición de visualización
|
|
71
|
+
mandatory: Esta condición debe cumprirse sempre, independentemente do estado das demais condicións
|
|
72
|
+
remove: Eliminar
|
|
73
|
+
response_option: Opción de resposta
|
|
74
|
+
save_warning: Lembra gardar o formulario antes de configurar as condicións de visualización
|
|
75
|
+
select_condition_question: Seleccionar unha pregunta
|
|
76
|
+
select_condition_type: Seleccionar un tipo de condición
|
|
77
|
+
select_response_option: Seleccionar unha opción de resposta
|
|
18
78
|
edit:
|
|
19
79
|
save: Gardar
|
|
80
|
+
title: Editar cuestionario
|
|
81
|
+
edit_questions:
|
|
82
|
+
add_question: Engadir pregunta
|
|
83
|
+
add_separator: Engadir separador
|
|
84
|
+
add_title_and_description: Engadir título e descrición
|
|
85
|
+
save: Gardar
|
|
86
|
+
title: Preguntas
|
|
20
87
|
form:
|
|
21
88
|
add_question: Engadir pregunta
|
|
89
|
+
collapse: Contraer todas as preguntas
|
|
90
|
+
expand: Expandir todas as preguntas
|
|
91
|
+
preview: Vista previa
|
|
92
|
+
title: Editar o formulario de %{questionnaire_for}
|
|
22
93
|
matrix_row:
|
|
94
|
+
matrix_row: Fila
|
|
23
95
|
remove: Quitar
|
|
24
96
|
statement: Declaración
|
|
25
97
|
question:
|
|
98
|
+
add_display_condition: Engadir condición de visualización
|
|
99
|
+
add_display_condition_info: Garda o formulario para configurar as condicións de visualización
|
|
100
|
+
add_matrix_row: Engadir fila
|
|
101
|
+
add_response_option: Engadir opción de resposta
|
|
26
102
|
any: Calquera
|
|
103
|
+
collapse: Contraer
|
|
27
104
|
description: Descrición
|
|
105
|
+
expand: Expandir
|
|
28
106
|
question: Pregunta
|
|
29
107
|
remove: Quitar
|
|
30
108
|
statement: Declaración
|
|
109
|
+
questions_form:
|
|
110
|
+
already_responded_warning: O formulario xa foi respondido por algunhas persoas participantes, polo que non podes modificar as súas preguntas.
|
|
111
|
+
collapse: Contraer todas as preguntas
|
|
112
|
+
expand: Expandir todas as preguntas
|
|
113
|
+
unpublished_warning: O formulario non está publicado. Podes modificar as súas preguntas, pero ao facelo eliminaranse as respostas actuais.
|
|
114
|
+
update:
|
|
115
|
+
success: As preguntas da enquisa gardáronse correctamente.
|
|
116
|
+
response_option:
|
|
117
|
+
free_text: Texto libre
|
|
118
|
+
remove: Eliminar
|
|
119
|
+
response_option: Opción de resposta
|
|
120
|
+
statement: Declaración
|
|
121
|
+
responses:
|
|
122
|
+
actions:
|
|
123
|
+
back: Volver ás respostas
|
|
124
|
+
export: Exportar
|
|
125
|
+
next: Seguinte ›
|
|
126
|
+
previous: "‹ Anterior"
|
|
127
|
+
empty: Aínda non hai respostas
|
|
128
|
+
export:
|
|
129
|
+
response:
|
|
130
|
+
title: 'Resposta n.º %{number}'
|
|
131
|
+
export_response:
|
|
132
|
+
title: survey_participant_responses_%{token}
|
|
133
|
+
index:
|
|
134
|
+
title: "%{total} respostas en total"
|
|
135
|
+
show:
|
|
136
|
+
title: 'Resposta n.º %{number}'
|
|
137
|
+
separator:
|
|
138
|
+
remove: Eliminar
|
|
139
|
+
separator: Separador
|
|
31
140
|
title_and_description:
|
|
32
141
|
collapse: Contraer
|
|
33
142
|
description: Descrición
|
|
@@ -37,25 +146,83 @@ gl:
|
|
|
37
146
|
title_and_description: Título e descrición
|
|
38
147
|
update:
|
|
39
148
|
invalid: Produciuse un erro ao gardar o cuestionario.
|
|
149
|
+
success: O formulario gardouse correctamente.
|
|
150
|
+
admin_log:
|
|
151
|
+
question:
|
|
152
|
+
publish_responses: "%{user_name} publicou as respostas da pregunta %{resource_name} no espazo %{space_name}"
|
|
153
|
+
unpublish_responses: "%{user_name} retirou a publicación das respostas da pregunta %{resource_name} no espazo %{space_name}"
|
|
154
|
+
questionnaire:
|
|
155
|
+
update: "%{user_name} actualizou o cuestionario %{resource_name}"
|
|
156
|
+
errors:
|
|
157
|
+
response:
|
|
158
|
+
body: O corpo non pode estar baleiro
|
|
40
159
|
images:
|
|
41
160
|
processors:
|
|
42
161
|
resize_and_pad: A imaxe axustarase para se encadrar a %{dimensions}.
|
|
43
162
|
resize_to_fit: A imaxe axustarase para se adaptar a %{dimensions}.
|
|
44
163
|
question_types:
|
|
164
|
+
files: Arquivos
|
|
165
|
+
long_response: Resposta longa
|
|
166
|
+
matrix_multiple: Matriz (opción múltiple)
|
|
167
|
+
matrix_single: Matriz (opción única)
|
|
45
168
|
multiple_option: Múltipla opción
|
|
169
|
+
short_response: Resposta curta
|
|
46
170
|
single_option: Opción única
|
|
47
171
|
sorting: Clasificación
|
|
48
172
|
title_and_description: Título e descrición
|
|
173
|
+
questionnaire_response_presenter:
|
|
174
|
+
download_attachment: Descargar anexo
|
|
49
175
|
questionnaires:
|
|
50
176
|
question:
|
|
51
177
|
max_choices: 'Opcións máximas: %{n}'
|
|
178
|
+
response:
|
|
179
|
+
invalid: Produciuse un problema ao responder ao formulario.
|
|
180
|
+
max_choices_alert: Seleccionáronse demasiadas opcións
|
|
181
|
+
success: O formulario respondeuse correctamente.
|
|
52
182
|
show:
|
|
183
|
+
current_step: Paso %{step}
|
|
184
|
+
empty: Aínda non hai preguntas configuradas para este formulario.
|
|
185
|
+
of_total_steps: de %{total_steps}
|
|
53
186
|
questionnaire_closed:
|
|
187
|
+
body: O formulario está pechado e non se pode responder.
|
|
54
188
|
title: Cuestionario pechado
|
|
55
189
|
questionnaire_for_private_users:
|
|
56
190
|
body: O cuestionario só está dispoñible para usuarios privados
|
|
57
191
|
title: Cuestionario pechado
|
|
192
|
+
questionnaire_js_disabled:
|
|
193
|
+
body: Algunhas funcións deste formulario estarán desactivadas. Para mellorar a experiencia, activa JavaScript no navegador.
|
|
194
|
+
title: JavaScript está desactivado
|
|
195
|
+
questionnaire_not_published:
|
|
196
|
+
body: Este formulario aínda non está publicado.
|
|
197
|
+
questionnaire_responded:
|
|
198
|
+
body: Xa respondiches a este formulario.
|
|
199
|
+
title: Xa respondido
|
|
200
|
+
questionnaire_responded_edit:
|
|
201
|
+
body: Xa respondiches a este formulario. %{link}
|
|
202
|
+
edit: Editar as túas respostas
|
|
203
|
+
response_questionnaire:
|
|
204
|
+
already_have_an_account?: Xa ten unha conta?
|
|
205
|
+
are_you_new?: Nova persoa participante?
|
|
206
|
+
sign_in_description: Inicia sesión para responder á enquisa
|
|
207
|
+
sign_up_description: Crea unha conta de participante para responder á enquisa
|
|
208
|
+
title: Responder ao formulario
|
|
58
209
|
tos_agreement: Ao participar aceptas as Condicións de servizo
|
|
59
210
|
step_navigation:
|
|
60
211
|
show:
|
|
212
|
+
are_you_sure_edit_guest: Se queres poder editar as túas respostas máis adiante, debes iniciar sesión ou crear unha conta.
|
|
213
|
+
are_you_sure_no_edit: Esta acción non se pode desfacer e non poderás editar as túas respostas. Seguro que queres continuar?
|
|
214
|
+
back: Volver
|
|
215
|
+
continue: Continuar
|
|
216
|
+
disallowed: Non tes permiso para editar as túas respostas.
|
|
61
217
|
submit: Enviar
|
|
218
|
+
user_responses_serializer:
|
|
219
|
+
body: Resposta
|
|
220
|
+
completion: Completado
|
|
221
|
+
created_at: Respondida o
|
|
222
|
+
id: ID da resposta
|
|
223
|
+
ip_hash: Hash da IP
|
|
224
|
+
question: Pregunta
|
|
225
|
+
registered: Rexistrada
|
|
226
|
+
session_token: Identificador da persoa participante
|
|
227
|
+
unregistered: Non rexistrada
|
|
228
|
+
user_status: Estado da persoa participante
|
|
@@ -263,6 +263,7 @@ shared_examples_for "add questions" do
|
|
|
263
263
|
select "Long response", from: "Type"
|
|
264
264
|
click_on "Save"
|
|
265
265
|
|
|
266
|
+
expect(page).to have_css(".panel-error", visible: :visible)
|
|
266
267
|
expand_all_questions
|
|
267
268
|
expect(page).to have_select("Type", selected: "Long response", wait: 10)
|
|
268
269
|
end
|
|
@@ -280,6 +281,7 @@ shared_examples_for "add questions" do
|
|
|
280
281
|
select "Long response", from: "Type"
|
|
281
282
|
|
|
282
283
|
click_on "Save"
|
|
284
|
+
expect(page).to have_css(".panel-error", visible: :visible)
|
|
283
285
|
expand_all_questions
|
|
284
286
|
|
|
285
287
|
expect(page).to have_text("Type")
|
|
@@ -303,6 +305,7 @@ shared_examples_for "add questions" do
|
|
|
303
305
|
select "Long response", from: "Type"
|
|
304
306
|
|
|
305
307
|
click_on "Save"
|
|
308
|
+
expect(page).to have_css(".panel-error", visible: :visible)
|
|
306
309
|
expand_all_questions
|
|
307
310
|
|
|
308
311
|
expect(page).to have_text("Type")
|
|
@@ -332,6 +335,7 @@ shared_examples_for "add questions" do
|
|
|
332
335
|
select "3", from: "Maximum number of choices"
|
|
333
336
|
|
|
334
337
|
click_on "Save"
|
|
338
|
+
expect(page).to have_css(".panel-error", visible: :visible)
|
|
335
339
|
expand_all_questions
|
|
336
340
|
|
|
337
341
|
expect(page).to have_css(".questionnaire-question-response-option")
|
|
@@ -359,6 +363,7 @@ shared_examples_for "add questions" do
|
|
|
359
363
|
click_on "Add row"
|
|
360
364
|
|
|
361
365
|
click_on "Save"
|
|
366
|
+
expect(page).to have_css(".panel-error", visible: :visible)
|
|
362
367
|
expand_all_questions
|
|
363
368
|
|
|
364
369
|
within ".questionnaire-question-matrix-row:first-of-type" do
|
|
@@ -45,6 +45,7 @@ shared_examples_for "update questions" do
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
click_on "Save"
|
|
48
|
+
expect(page).to have_css(".panel-error", visible: :visible)
|
|
48
49
|
click_on "Expand all questions"
|
|
49
50
|
|
|
50
51
|
expect(page).to have_admin_callout("There was a problem saving")
|
|
@@ -512,6 +513,7 @@ shared_examples_for "update questions" do
|
|
|
512
513
|
end
|
|
513
514
|
|
|
514
515
|
click_on "Save"
|
|
516
|
+
expect(page).to have_css(".panel-error", visible: :visible)
|
|
515
517
|
expand_all_questions
|
|
516
518
|
|
|
517
519
|
within ".questionnaire-question:last-of-type" do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: decidim-forms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.31.
|
|
4
|
+
version: 0.31.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josep Jaume Rey Peroy
|
|
@@ -11,7 +11,7 @@ authors:
|
|
|
11
11
|
autorequire:
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date: 2026-
|
|
14
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: decidim-core
|
|
@@ -19,42 +19,42 @@ dependencies:
|
|
|
19
19
|
requirements:
|
|
20
20
|
- - '='
|
|
21
21
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: 0.31.
|
|
22
|
+
version: 0.31.8
|
|
23
23
|
type: :runtime
|
|
24
24
|
prerelease: false
|
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
26
26
|
requirements:
|
|
27
27
|
- - '='
|
|
28
28
|
- !ruby/object:Gem::Version
|
|
29
|
-
version: 0.31.
|
|
29
|
+
version: 0.31.8
|
|
30
30
|
- !ruby/object:Gem::Dependency
|
|
31
31
|
name: decidim-admin
|
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
|
33
33
|
requirements:
|
|
34
34
|
- - '='
|
|
35
35
|
- !ruby/object:Gem::Version
|
|
36
|
-
version: 0.31.
|
|
36
|
+
version: 0.31.8
|
|
37
37
|
type: :development
|
|
38
38
|
prerelease: false
|
|
39
39
|
version_requirements: !ruby/object:Gem::Requirement
|
|
40
40
|
requirements:
|
|
41
41
|
- - '='
|
|
42
42
|
- !ruby/object:Gem::Version
|
|
43
|
-
version: 0.31.
|
|
43
|
+
version: 0.31.8
|
|
44
44
|
- !ruby/object:Gem::Dependency
|
|
45
45
|
name: decidim-dev
|
|
46
46
|
requirement: !ruby/object:Gem::Requirement
|
|
47
47
|
requirements:
|
|
48
48
|
- - '='
|
|
49
49
|
- !ruby/object:Gem::Version
|
|
50
|
-
version: 0.31.
|
|
50
|
+
version: 0.31.8
|
|
51
51
|
type: :development
|
|
52
52
|
prerelease: false
|
|
53
53
|
version_requirements: !ruby/object:Gem::Requirement
|
|
54
54
|
requirements:
|
|
55
55
|
- - '='
|
|
56
56
|
- !ruby/object:Gem::Version
|
|
57
|
-
version: 0.31.
|
|
57
|
+
version: 0.31.8
|
|
58
58
|
description: A forms gem for decidim.
|
|
59
59
|
email:
|
|
60
60
|
- josepjaume@gmail.com
|
|
@@ -117,6 +117,8 @@ files:
|
|
|
117
117
|
- app/packs/src/decidim/forms/admin/live_text_update.component.js
|
|
118
118
|
- app/packs/src/decidim/forms/admin/publish_responses_buttons.js
|
|
119
119
|
- app/packs/src/decidim/forms/display_conditions.component.js
|
|
120
|
+
- app/packs/src/decidim/forms/display_conditions.component.test.js
|
|
121
|
+
- app/packs/src/decidim/forms/display_conditions_hidden_responses.test.js
|
|
120
122
|
- app/packs/src/decidim/forms/forms.js
|
|
121
123
|
- app/packs/src/decidim/forms/max_choices_alert.component.js
|
|
122
124
|
- app/packs/src/decidim/forms/option_attached_inputs.component.js
|