decidim-forms 0.31.8 → 0.32.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/app/cells/decidim/forms/step_navigation_cell.rb +1 -2
  4. data/app/commands/decidim/forms/response_questionnaire.rb +3 -3
  5. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +22 -39
  6. data/app/controllers/decidim/forms/admin/concerns/has_questionnaire_responses.rb +2 -4
  7. data/app/controllers/decidim/forms/concerns/has_questionnaire.rb +1 -5
  8. data/app/forms/decidim/forms/questionnaire_form.rb +1 -1
  9. data/app/forms/decidim/forms/response_form.rb +7 -7
  10. data/app/models/decidim/forms/question.rb +2 -2
  11. data/app/models/decidim/forms/response.rb +1 -1
  12. data/app/packs/src/decidim/forms/admin/collapsible_questions.js +10 -0
  13. data/app/packs/src/decidim/forms/display_conditions.component.js +0 -27
  14. data/app/views/decidim/forms/admin/questionnaires/_question.html.erb +1 -1
  15. data/app/views/decidim/forms/admin/questionnaires/edit.html.erb +1 -1
  16. data/app/views/decidim/forms/admin/questionnaires/edit_questions.html.erb +1 -1
  17. data/app/views/decidim/forms/admin/questionnaires/responses/index.html.erb +3 -1
  18. data/app/views/decidim/forms/admin/questionnaires/responses/show.html.erb +3 -1
  19. data/app/views/decidim/forms/questionnaires/_questionnaire.html.erb +2 -2
  20. data/app/views/decidim/forms/questionnaires/responses/_files.html.erb +1 -1
  21. data/app/views/decidim/forms/questionnaires/show.html.erb +1 -1
  22. data/config/locales/ar.yml +0 -4
  23. data/config/locales/bg.yml +0 -4
  24. data/config/locales/ca-IT.yml +5 -5
  25. data/config/locales/ca.yml +5 -5
  26. data/config/locales/cs.yml +3 -4
  27. data/config/locales/de.yml +2 -5
  28. data/config/locales/el.yml +0 -4
  29. data/config/locales/en.yml +3 -2
  30. data/config/locales/es-MX.yml +4 -4
  31. data/config/locales/es-PY.yml +4 -4
  32. data/config/locales/es.yml +3 -3
  33. data/config/locales/eu.yml +14 -14
  34. data/config/locales/fi-plain.yml +4 -4
  35. data/config/locales/fi.yml +3 -3
  36. data/config/locales/fr-CA.yml +4 -6
  37. data/config/locales/fr.yml +4 -6
  38. data/config/locales/ga-IE.yml +0 -1
  39. data/config/locales/gl.yml +0 -171
  40. data/config/locales/hu.yml +0 -4
  41. data/config/locales/id-ID.yml +0 -4
  42. data/config/locales/it.yml +0 -4
  43. data/config/locales/ja.yml +3 -3
  44. data/config/locales/kaa.yml +0 -1
  45. data/config/locales/lb.yml +0 -4
  46. data/config/locales/lt.yml +0 -4
  47. data/config/locales/lv.yml +0 -4
  48. data/config/locales/nl.yml +0 -4
  49. data/config/locales/no.yml +0 -4
  50. data/config/locales/pl.yml +0 -4
  51. data/config/locales/pt-BR.yml +3 -4
  52. data/config/locales/pt.yml +0 -4
  53. data/config/locales/ro-RO.yml +0 -4
  54. data/config/locales/ru.yml +0 -4
  55. data/config/locales/sk.yml +0 -186
  56. data/config/locales/sv.yml +3 -3
  57. data/config/locales/tr-TR.yml +0 -4
  58. data/config/locales/val-ES.yml +0 -1
  59. data/config/locales/zh-CN.yml +0 -4
  60. data/config/locales/zh-TW.yml +0 -4
  61. data/decidim-forms.gemspec +6 -8
  62. data/lib/decidim/api/questionnaire_entity_interface.rb +1 -0
  63. data/lib/decidim/forms/download_your_data_user_responses_serializer.rb +1 -0
  64. data/lib/decidim/forms/test/factories.rb +9 -5
  65. data/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +23 -23
  66. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_display_conditions.rb +1 -1
  67. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb +4 -9
  68. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_display_conditions.rb +1 -1
  69. data/lib/decidim/forms/test/shared_examples/manage_questionnaires/update_questions.rb +9 -11
  70. data/lib/decidim/forms/version.rb +1 -1
  71. metadata +10 -18
  72. data/app/packs/src/decidim/forms/display_conditions.component.test.js +0 -142
  73. data/app/packs/src/decidim/forms/display_conditions_hidden_responses.test.js +0 -224
  74. data/db/data/20260616144141_rename_answers_to_responses.rb +0 -31
  75. data/lib/decidim/forms/test/shared_examples/questionnaire_admin_access.rb +0 -95
  76. data/lib/decidim/forms/test/shared_examples/questionnaire_admin_controller_permissions.rb +0 -41
@@ -1,224 +0,0 @@
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
- });
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RenameAnswersToResponses < ActiveRecord::Migration[7.2]
4
- class Attachment < ApplicationRecord
5
- self.table_name = "decidim_attachments"
6
- end
7
-
8
- class ActionLog < ApplicationRecord
9
- self.table_name = "decidim_action_logs"
10
- end
11
-
12
- class Version < ApplicationRecord
13
- self.table_name = "versions"
14
- end
15
-
16
- def up
17
- # rubocop:disable Rails/SkipsModelValidations
18
- Attachment.where(attached_to_type: "Decidim::Forms::Answer").update_all(attached_to_type: "Decidim::Forms::Response")
19
- ActionLog.where(resource_type: "Decidim::Forms::Answer").update_all(resource_type: "Decidim::Forms::Response")
20
- Version.where(item_type: "Decidim::Forms::Answer").update_all(item_type: "Decidim::Forms::Response")
21
- # rubocop:enable Rails/SkipsModelValidations
22
- end
23
-
24
- def down
25
- # rubocop:disable Rails/SkipsModelValidations
26
- Attachment.where(attached_to_type: "Decidim::Forms::Response").update_all(attached_to_type: "Decidim::Forms::Answer")
27
- ActionLog.where(resource_type: "Decidim::Forms::Response").update_all(resource_type: "Decidim::Forms::Answer")
28
- Version.where(item_type: "Decidim::Forms::Response").update_all(item_type: "Decidim::Forms::Answer")
29
- # rubocop:enable Rails/SkipsModelValidations
30
- end
31
- end
@@ -1,95 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- shared_examples_for "questionnaire admin access" do |denied_error:, allow_process_admin: true|
6
- context "when the user is not an admin", driver: :rack_test do
7
- let(:regular_user) { create(:user, :confirmed, organization:) }
8
- let(:target_path) { manage_questions_path }
9
-
10
- before do
11
- login_as regular_user, scope: :user
12
- end
13
-
14
- before do
15
- allow(Rails.application).to \
16
- receive(:env_config).with(no_args).and_wrap_original do |m, *|
17
- m.call.merge(
18
- "action_dispatch.show_exceptions" => true,
19
- "action_dispatch.show_detailed_exceptions" => false
20
- )
21
- end
22
-
23
- visit target_path
24
- end
25
-
26
- it "leads to an error" do
27
- denied_response = case denied_error
28
- when 403
29
- page.status_code == 403 || page.has_content?("You are not authorized to perform this action")
30
- when 404
31
- page.status_code == 404 || page.has_content?("The page you are looking for cannot be found")
32
- else
33
- raise ArgumentError, "unsupported denied_error: #{denied_error.inspect}. Use 403 or 404"
34
- end
35
-
36
- expect(denied_response).to be(true)
37
- end
38
- end
39
-
40
- if allow_process_admin
41
- context "when the user is a process admin" do
42
- let(:process_admin) { create(:process_admin, :confirmed, participatory_process:) }
43
-
44
- it "allows access to the questionnaire" do
45
- login_as process_admin, scope: :user
46
- visit manage_questions_path
47
-
48
- expect(page).to have_current_path(manage_questions_path)
49
- end
50
- end
51
- else
52
- context "when the user is a process admin", driver: :rack_test do
53
- let(:participatory_process) { create(:participatory_process, organization:) }
54
- let(:process_admin) { create(:process_admin, :confirmed, participatory_process:) }
55
-
56
- before do
57
- login_as process_admin, scope: :user
58
-
59
- allow(Rails.application).to \
60
- receive(:env_config).with(no_args).and_wrap_original do |m, *|
61
- m.call.merge(
62
- "action_dispatch.show_exceptions" => true,
63
- "action_dispatch.show_detailed_exceptions" => false
64
- )
65
- end
66
-
67
- visit manage_questions_path
68
- end
69
-
70
- it "denies access to the questionnaire" do
71
- denied_response = case denied_error
72
- when 403
73
- page.status_code == 403 || page.has_content?("You are not authorized to perform this action")
74
- when 404
75
- page.status_code == 404 || page.has_content?("The page you are looking for cannot be found")
76
- else
77
- raise ArgumentError, "unsupported denied_error: #{denied_error.inspect}. Use 403 or 404"
78
- end
79
-
80
- expect(denied_response).to be(true)
81
- end
82
- end
83
- end
84
-
85
- context "when the user is an admin" do
86
- let(:admin) { create(:user, :admin, :confirmed, organization:) }
87
-
88
- it "allows access to the questionnaire" do
89
- login_as admin, scope: :user
90
- visit manage_questions_path
91
-
92
- expect(page).to have_current_path(manage_questions_path)
93
- end
94
- end
95
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- shared_examples_for "questionnaire admin controller permissions" do
6
- describe "GET #edit" do
7
- let(:action) { :edit }
8
-
9
- it "enforces permission to update the questionnaire" do
10
- expect(controller).to receive(:enforce_permission_to).with(:update, permission_subject, questionnaire:)
11
- get action
12
- end
13
- end
14
-
15
- describe "PATCH #update" do
16
- let(:action) { :update }
17
-
18
- it "enforces permission to update the questionnaire" do
19
- expect(controller).to receive(:enforce_permission_to).with(:update, permission_subject, questionnaire:)
20
- patch action, params: { questionnaire: {} }
21
- end
22
- end
23
-
24
- describe "GET #edit_questions" do
25
- let(:action) { :edit_questions }
26
-
27
- it "enforces permission to update the questionnaire" do
28
- expect(controller).to receive(:enforce_permission_to).with(:update, permission_subject, questionnaire:)
29
- get action
30
- end
31
- end
32
-
33
- describe "PATCH #update_questions" do
34
- let(:action) { :update_questions }
35
-
36
- it "enforces permission to update the questionnaire" do
37
- expect(controller).to receive(:enforce_permission_to).with(:update, permission_subject, questionnaire:)
38
- patch action, params: { questions: {} }
39
- end
40
- end
41
- end