decidim-initiatives 0.30.2 → 0.31.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.
- checksums.yaml +4 -4
- data/README.md +46 -9
- data/app/cells/decidim/initiatives/content_blocks/highlighted_initiatives_settings_form/show.erb +7 -2
- data/app/cells/decidim/initiatives/initiative_g_cell.rb +5 -1
- data/app/commands/decidim/initiatives/admin/publish_initiative.rb +1 -5
- data/app/commands/decidim/initiatives/admin/update_initiative.rb +1 -2
- data/app/commands/decidim/initiatives/create_initiative.rb +0 -1
- data/app/commands/decidim/initiatives/update_initiative.rb +1 -3
- data/app/commands/decidim/initiatives/vote_initiative.rb +1 -11
- data/app/controllers/concerns/decidim/initiatives/has_signature_workflow.rb +36 -0
- data/app/controllers/concerns/decidim/initiatives/needs_initiative.rb +1 -12
- data/app/controllers/decidim/initiatives/admin/initiatives_controller.rb +2 -2
- data/app/controllers/decidim/initiatives/admin/initiatives_settings_controller.rb +1 -1
- data/app/controllers/decidim/initiatives/admin/initiatives_type_scopes_controller.rb +2 -2
- data/app/controllers/decidim/initiatives/admin/initiatives_types_controller.rb +2 -2
- data/app/controllers/decidim/initiatives/committee_requests_controller.rb +10 -2
- data/app/controllers/decidim/initiatives/create_initiative_controller.rb +84 -18
- data/app/controllers/decidim/initiatives/initiative_signatures_controller.rb +133 -42
- data/app/controllers/decidim/initiatives/initiative_votes_controller.rb +3 -2
- data/app/controllers/decidim/initiatives/initiatives_controller.rb +21 -2
- data/app/forms/decidim/initiatives/admin/initiative_form.rb +0 -1
- data/app/forms/decidim/initiatives/initiative_form.rb +0 -3
- data/app/helpers/decidim/initiatives/application_helper.rb +2 -0
- data/app/helpers/decidim/initiatives/initiatives_helper.rb +0 -1
- data/app/models/decidim/initiative.rb +7 -31
- data/app/models/decidim/initiatives_committee_member.rb +1 -1
- data/app/models/decidim/initiatives_type.rb +5 -2
- data/app/models/decidim/initiatives_vote.rb +2 -2
- data/app/packs/entrypoints/decidim_initiatives.js +1 -1
- data/app/packs/entrypoints/decidim_initiatives_admin.scss +1 -1
- data/app/packs/src/decidim/initiatives/admin/initiatives_types.js +2 -11
- data/app/packs/src/decidim/initiatives/admin/invite_users.js +1 -1
- data/app/packs/src/decidim/initiatives/application.js +1 -1
- data/app/packs/src/decidim/initiatives/check_code.js +114 -0
- data/app/packs/src/decidim/initiatives/initiative_creation_wizard.js +16 -0
- data/app/packs/src/decidim/initiatives/scoped_type.js +1 -1
- data/app/packs/stylesheets/initiatives.scss +16 -2
- data/app/permissions/decidim/initiatives/admin/permissions.rb +1 -4
- data/app/permissions/decidim/initiatives/permissions.rb +26 -16
- data/app/presenters/decidim/initiative_presenter.rb +12 -6
- data/app/presenters/decidim/initiatives/admin_log/initiative_presenter.rb +1 -2
- data/app/queries/decidim/initiatives/initiatives_stats_followers_count.rb +14 -0
- data/app/queries/decidim/initiatives/initiatives_stats_participants_count.rb +14 -0
- data/app/serializers/decidim/initiatives/open_data_initiative_serializer.rb +0 -1
- data/app/services/decidim/initiatives/data_encryptor.rb +1 -1
- data/app/services/decidim/initiatives/legacy_signature_handler.rb +25 -0
- data/app/services/decidim/initiatives/progress_notifier.rb +1 -7
- data/app/services/decidim/initiatives/signature_handler.rb +248 -0
- data/app/services/decidim/initiatives/status_change_notifier.rb +1 -7
- data/app/views/decidim/initiatives/admin/committee_requests/index.html.erb +29 -11
- data/app/views/decidim/initiatives/admin/exports/_dropdown.html.erb +17 -20
- data/app/views/decidim/initiatives/admin/initiatives/_form.html.erb +7 -13
- data/app/views/decidim/initiatives/admin/initiatives/_initiative_attachments.erb +2 -2
- data/app/views/decidim/initiatives/admin/initiatives/index.html.erb +76 -47
- data/app/views/decidim/initiatives/admin/initiatives_types/_form.html.erb +13 -21
- data/app/views/decidim/initiatives/admin/initiatives_types/_initiative_type_scopes.html.erb +28 -12
- data/app/views/decidim/initiatives/admin/initiatives_types/index.html.erb +33 -15
- data/app/views/decidim/initiatives/create_initiative/_committee_member.html.erb +27 -0
- data/app/views/decidim/initiatives/create_initiative/_return_to_initiatives_button.html.erb +3 -0
- data/app/views/decidim/initiatives/create_initiative/_send_to_technical_validation_button.html.erb +10 -0
- data/app/views/decidim/initiatives/create_initiative/_share_committee_link.html.erb +5 -1
- data/app/views/decidim/initiatives/create_initiative/fill_data.html.erb +7 -11
- data/app/views/decidim/initiatives/create_initiative/finish.html.erb +16 -13
- data/app/views/decidim/initiatives/create_initiative/promotal_committee.html.erb +33 -6
- data/app/views/decidim/initiatives/create_initiative/select_initiative_type.html.erb +40 -26
- data/app/views/decidim/initiatives/initiative_signatures/_sms_code_form.html.erb +22 -0
- data/app/views/decidim/initiatives/initiative_signatures/_sms_phone_number_form.html.erb +13 -0
- data/app/views/decidim/initiatives/initiative_signatures/fill_personal_data.html.erb +23 -22
- data/app/views/decidim/initiatives/initiative_signatures/finish.html.erb +17 -5
- data/app/views/decidim/initiatives/initiative_signatures/sms_code.html.erb +6 -8
- data/app/views/decidim/initiatives/initiative_signatures/sms_phone_number.html.erb +3 -8
- data/app/views/decidim/initiatives/initiative_signatures/update_buttons_and_counters.js.erb +3 -14
- data/app/views/decidim/initiatives/initiative_votes/update_buttons_and_counters.js.erb +3 -14
- data/app/views/decidim/initiatives/initiatives/_committee_members.html.erb +1 -1
- data/app/views/decidim/initiatives/initiatives/_form.html.erb +1 -3
- data/app/views/decidim/initiatives/initiatives/_new_initiative_button.html.erb +10 -3
- data/app/views/decidim/initiatives/initiatives/_pending_initiatives.html.erb +5 -0
- data/app/views/decidim/initiatives/initiatives/index.html.erb +8 -0
- data/app/views/decidim/initiatives/initiatives/show.html.erb +2 -2
- data/app/views/layouts/decidim/_initiative_signature_creation_header.html.erb +20 -2
- data/app/views/layouts/decidim/admin/_manage_initiatives.html.erb +1 -1
- data/app/views/layouts/decidim/initiative_signature_creation.html.erb +3 -1
- data/config/assets.rb +2 -2
- data/config/locales/ar.yml +0 -45
- data/config/locales/bg.yml +0 -54
- data/config/locales/ca-IT.yml +99 -51
- data/config/locales/ca.yml +99 -51
- data/config/locales/cs.yml +93 -54
- data/config/locales/de.yml +99 -51
- data/config/locales/el.yml +0 -45
- data/config/locales/en.yml +99 -51
- data/config/locales/es-MX.yml +99 -51
- data/config/locales/es-PY.yml +99 -51
- data/config/locales/es.yml +99 -51
- data/config/locales/eu.yml +99 -51
- data/config/locales/fi-plain.yml +99 -51
- data/config/locales/fi.yml +99 -51
- data/config/locales/fr-CA.yml +44 -51
- data/config/locales/fr.yml +44 -51
- data/config/locales/ga-IE.yml +0 -17
- data/config/locales/gl.yml +0 -41
- data/config/locales/hu.yml +0 -54
- data/config/locales/id-ID.yml +0 -40
- data/config/locales/is-IS.yml +0 -22
- data/config/locales/it.yml +0 -53
- data/config/locales/ja.yml +98 -49
- data/config/locales/lb.yml +0 -50
- data/config/locales/lt.yml +0 -56
- data/config/locales/lv.yml +0 -46
- data/config/locales/nl.yml +0 -47
- data/config/locales/no.yml +0 -53
- data/config/locales/pl.yml +0 -56
- data/config/locales/pt-BR.yml +0 -53
- data/config/locales/pt.yml +0 -53
- data/config/locales/ro-RO.yml +92 -50
- data/config/locales/ru.yml +0 -25
- data/config/locales/sk.yml +0 -43
- data/config/locales/sl.yml +0 -1
- data/config/locales/sv.yml +10 -53
- data/config/locales/tr-TR.yml +0 -53
- data/config/locales/uk.yml +0 -25
- data/config/locales/zh-CN.yml +0 -45
- data/config/locales/zh-TW.yml +0 -53
- data/db/migrate/20250605104500_remove_hashtag_column_initiatives.rb +7 -0
- data/lib/decidim/api/initiative_api_type.rb +3 -0
- data/lib/decidim/api/initiative_type.rb +23 -4
- data/lib/decidim/exporters/initiative_votes_pdf.rb +1 -1
- data/lib/decidim/initiatives/default_signature_authorizer.rb +17 -0
- data/lib/decidim/initiatives/engine.rb +17 -14
- data/lib/decidim/initiatives/participatory_space.rb +15 -1
- data/lib/decidim/initiatives/seeds.rb +1 -2
- data/lib/decidim/initiatives/signature_workflow_manifest.rb +176 -0
- data/lib/decidim/initiatives/signatures.rb +12 -0
- data/lib/decidim/initiatives/test/factories.rb +7 -7
- data/lib/decidim/initiatives/test/initiatives_signatures_test_helpers.rb +19 -0
- data/lib/decidim/initiatives/validatable_authorizations.rb +83 -0
- data/lib/decidim/initiatives/version.rb +1 -1
- data/lib/decidim/initiatives.rb +23 -12
- metadata +33 -21
- data/app/events/decidim/initiatives/endorse_initiative_event.rb +0 -13
- data/app/forms/decidim/initiatives/vote_form.rb +0 -208
- data/app/packs/src/decidim/initiatives/identity_selector_dialog.js +0 -14
- data/app/services/decidim/initiatives/pdf_signature_example.rb +0 -110
- data/app/views/decidim/initiatives/initiative_signatures/_wizard_steps.html.erb +0 -15
- data/app/views/decidim/initiatives/initiatives/_interactions.html.erb +0 -10
- data/app/views/layouts/decidim/_initiative_header.html.erb +0 -27
@@ -0,0 +1,114 @@
|
|
1
|
+
const focusDigit = (digit) => {
|
2
|
+
const length = digit.value.length;
|
3
|
+
digit.focus();
|
4
|
+
setTimeout(() => digit.setSelectionRange(length, length), 0);
|
5
|
+
};
|
6
|
+
|
7
|
+
const validateCode = (path, code) => {
|
8
|
+
return fetch(path, {
|
9
|
+
method: "PUT",
|
10
|
+
cache: "no-cache",
|
11
|
+
headers: {
|
12
|
+
"Content-Type": "application/json",
|
13
|
+
"X-CSRF-Token": document.querySelector("meta[name=csrf-token]")?.content
|
14
|
+
},
|
15
|
+
body: JSON.stringify({ confirmation: { "verification_code": code } })
|
16
|
+
}).
|
17
|
+
then((response) => response.json()).
|
18
|
+
then((json) => { return json.sms_code === "OK" });
|
19
|
+
};
|
20
|
+
|
21
|
+
const updateSubmit = (enable, includeAnnouncement) => {
|
22
|
+
const correctAnnouncement = document.querySelector(".code-correct-announcement");
|
23
|
+
const incorrectAnnouncement = document.querySelector(".code-incorrect-announcement");
|
24
|
+
const submitButton = document.querySelector("[data-submit-verification-code]");
|
25
|
+
const resendCodeMessage = document.querySelector("[data-resend-code]");
|
26
|
+
|
27
|
+
if (enable) {
|
28
|
+
if (includeAnnouncement) {
|
29
|
+
correctAnnouncement.classList.remove("hidden");
|
30
|
+
resendCodeMessage.classList.add("hidden");
|
31
|
+
} else {
|
32
|
+
correctAnnouncement.classList.add("hidden");
|
33
|
+
resendCodeMessage.classList.remove("hidden");
|
34
|
+
}
|
35
|
+
incorrectAnnouncement.classList.add("hidden");
|
36
|
+
submitButton.classList.remove("hidden");
|
37
|
+
submitButton.disabled = false;
|
38
|
+
} else {
|
39
|
+
if (includeAnnouncement) {
|
40
|
+
incorrectAnnouncement.classList.remove("hidden");
|
41
|
+
resendCodeMessage.classList.add("hidden");
|
42
|
+
} else {
|
43
|
+
incorrectAnnouncement.classList.add("hidden");
|
44
|
+
resendCodeMessage.classList.remove("hidden");
|
45
|
+
}
|
46
|
+
correctAnnouncement.classList.add("hidden");
|
47
|
+
submitButton.classList.add("hidden");
|
48
|
+
submitButton.disabled = true;
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
const updateValue = (codeInput, event, digitsInputs) => {
|
53
|
+
const checkCodePath = codeInput.dataset.checkCodePath;
|
54
|
+
const index = Number(event.target.dataset.verificationCode);
|
55
|
+
const prevDigit = digitsInputs[index - 1];
|
56
|
+
const nextDigit = digitsInputs[index + 1];
|
57
|
+
let digits = codeInput.value.split("");
|
58
|
+
const newDigit = event.target.value || "-";
|
59
|
+
if (newDigit.length > 0) {
|
60
|
+
const position = event.target.dataset.verificationCode;
|
61
|
+
digits[position] = newDigit;
|
62
|
+
const newCode = digits.join("")
|
63
|
+
if (codeInput.value !== newCode) {
|
64
|
+
codeInput.value = newCode;
|
65
|
+
if ((/^\d{6}$/).test(newCode)) {
|
66
|
+
validateCode(checkCodePath, newCode).then((validCode) => updateSubmit(validCode, true));
|
67
|
+
} else {
|
68
|
+
updateSubmit(false, false);
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
if (prevDigit && newDigit === "-") {
|
73
|
+
focusDigit(prevDigit);
|
74
|
+
} else if (nextDigit && newDigit !== "-") {
|
75
|
+
focusDigit(nextDigit);
|
76
|
+
}
|
77
|
+
}
|
78
|
+
};
|
79
|
+
|
80
|
+
const updatePosition = (codeInput, event, digitsInputs) => {
|
81
|
+
const index = Number(event.target.dataset.verificationCode);
|
82
|
+
const nextDigit = (() => {
|
83
|
+
if (event.key === "ArrowLeft" || ["Delete", "Backspace"].includes(event.key) && event.target.value === "") {
|
84
|
+
return digitsInputs[index - 1];
|
85
|
+
} else if (event.key === "ArrowRight" || (/^\d$/).test(event.key) && event.target.value.length > 0) {
|
86
|
+
return digitsInputs[index + 1];
|
87
|
+
}
|
88
|
+
return false;
|
89
|
+
})();
|
90
|
+
|
91
|
+
if (nextDigit) {
|
92
|
+
focusDigit(nextDigit);
|
93
|
+
}
|
94
|
+
return true;
|
95
|
+
};
|
96
|
+
|
97
|
+
|
98
|
+
const initializeCodeVerificator = (codeElement) => {
|
99
|
+
const codeInput = codeElement.querySelector("[data-check-code-path]");
|
100
|
+
const digitsInputs = codeElement.querySelectorAll("[data-verification-code]");
|
101
|
+
|
102
|
+
digitsInputs.forEach((digitInput) => {
|
103
|
+
digitInput.addEventListener("input", (event) => updateValue(codeInput, event, digitsInputs));
|
104
|
+
digitInput.addEventListener("keydown", (event) => updatePosition(codeInput, event, digitsInputs));
|
105
|
+
});
|
106
|
+
codeInput.value = "------";
|
107
|
+
};
|
108
|
+
|
109
|
+
document.addEventListener("turbo:load", () => {
|
110
|
+
const codeElement = document.querySelector("[data-check-code]");
|
111
|
+
if (codeElement) {
|
112
|
+
initializeCodeVerificator(codeElement);
|
113
|
+
}
|
114
|
+
});
|
@@ -0,0 +1,16 @@
|
|
1
|
+
document.addEventListener("turbo:load", () => {
|
2
|
+
const selectInitiativeType = document.getElementById("select-initiative-type");
|
3
|
+
|
4
|
+
if (selectInitiativeType) {
|
5
|
+
const submitButton = selectInitiativeType.querySelector('button[type="submit"]');
|
6
|
+
const radioButtons = selectInitiativeType.querySelectorAll('input[type="radio"][name="initiative[type_id]"]');
|
7
|
+
|
8
|
+
submitButton.disabled = true;
|
9
|
+
|
10
|
+
for (const radioButton of radioButtons) {
|
11
|
+
radioButton.addEventListener("click", () => {
|
12
|
+
submitButton.disabled = false;
|
13
|
+
});
|
14
|
+
}
|
15
|
+
}
|
16
|
+
});
|
@@ -27,7 +27,7 @@ const controlSelector = function (source, prefix, currentValueKey) {
|
|
27
27
|
}
|
28
28
|
};
|
29
29
|
|
30
|
-
|
30
|
+
document.addEventListener("turbo:load", () => {
|
31
31
|
let typeSelector = $("[data-scope-selector]");
|
32
32
|
controlSelector(typeSelector, "scope", "scope-id");
|
33
33
|
controlSelector(typeSelector, "signature-types", "signature-type");
|
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
&__selection {
|
5
5
|
.form-defaults {
|
6
|
-
@apply my-
|
6
|
+
@apply my-8;
|
7
7
|
}
|
8
8
|
|
9
9
|
.card__highlight {
|
@@ -90,7 +90,7 @@
|
|
90
90
|
}
|
91
91
|
|
92
92
|
&__form__committee {
|
93
|
-
@apply flex items-center px-4 py-2 border border-gray outline outline-1 outline-transparent rounded bg-background-2 focus:outline-2 focus:outline-secondary w-full;
|
93
|
+
@apply flex items-center px-4 py-2 border border-gray outline outline-1 outline-transparent rounded bg-background-2 focus:outline-2 focus:outline-secondary w-full mt-4;
|
94
94
|
|
95
95
|
span {
|
96
96
|
@apply text-black font-normal;
|
@@ -169,3 +169,17 @@
|
|
169
169
|
.edit_initiative .profile__group__list {
|
170
170
|
@apply w-full;
|
171
171
|
}
|
172
|
+
|
173
|
+
.pending-initiatives {
|
174
|
+
@apply bg-background rounded-lg px-4 py-8 mb-12;
|
175
|
+
|
176
|
+
.card__grid {
|
177
|
+
@apply bg-white;
|
178
|
+
|
179
|
+
--tw-ring-color: #ddd;
|
180
|
+
|
181
|
+
&:hover {
|
182
|
+
--tw-ring-color: var(--tertiary);
|
183
|
+
}
|
184
|
+
}
|
185
|
+
}
|
@@ -214,10 +214,7 @@ module Decidim
|
|
214
214
|
|
215
215
|
def allowed_to_send_to_technical_validation?
|
216
216
|
initiative.discarded? ||
|
217
|
-
(initiative.created? &&
|
218
|
-
!initiative.created_by_individual? ||
|
219
|
-
initiative.enough_committee_members?
|
220
|
-
))
|
217
|
+
(initiative.created? && initiative.enough_committee_members?)
|
221
218
|
end
|
222
219
|
end
|
223
220
|
end
|
@@ -15,12 +15,14 @@ module Decidim
|
|
15
15
|
read_public_initiative?
|
16
16
|
search_initiative_types_and_scopes?
|
17
17
|
request_membership?
|
18
|
+
ephemeral_vote_initiative?
|
18
19
|
|
19
20
|
return permission_action unless user
|
20
21
|
|
21
22
|
create_initiative?
|
22
23
|
edit_public_initiative?
|
23
24
|
update_public_initiative?
|
25
|
+
discard_initiative?
|
24
26
|
print_initiative?
|
25
27
|
|
26
28
|
vote_initiative?
|
@@ -89,11 +91,17 @@ module Decidim
|
|
89
91
|
toggle_allow(initiative&.created? && authorship_or_admin?)
|
90
92
|
end
|
91
93
|
|
94
|
+
def discard_initiative?
|
95
|
+
return unless permission_action.subject == :initiative &&
|
96
|
+
permission_action.action == :discard
|
97
|
+
|
98
|
+
toggle_allow(initiative&.created? && authorship_or_admin?)
|
99
|
+
end
|
100
|
+
|
92
101
|
def creation_enabled?
|
93
102
|
Decidim::Initiatives.creation_enabled && (
|
94
103
|
Decidim::Initiatives.do_not_require_authorization ||
|
95
|
-
UserAuthorizations.for(user).any?
|
96
|
-
Decidim::UserGroups::ManageableUserGroups.for(user).verified.any?) &&
|
104
|
+
UserAuthorizations.for(user).any?) &&
|
97
105
|
authorized?(:create, permissions_holder: initiative_type)
|
98
106
|
end
|
99
107
|
|
@@ -120,8 +128,7 @@ module Decidim
|
|
120
128
|
!initiative.has_authorship?(user) &&
|
121
129
|
(
|
122
130
|
Decidim::Initiatives.do_not_require_authorization ||
|
123
|
-
UserAuthorizations.for(user).any?
|
124
|
-
Decidim::UserGroups::ManageableUserGroups.for(user).verified.any?
|
131
|
+
UserAuthorizations.for(user).any?
|
125
132
|
)
|
126
133
|
end
|
127
134
|
|
@@ -143,6 +150,14 @@ module Decidim
|
|
143
150
|
toggle_allow(can_vote?)
|
144
151
|
end
|
145
152
|
|
153
|
+
def ephemeral_vote_initiative?
|
154
|
+
return unless permission_action.action == :vote &&
|
155
|
+
permission_action.subject == :initiative &&
|
156
|
+
user.blank?
|
157
|
+
|
158
|
+
toggle_allow(ephemeral_signature_workflow?)
|
159
|
+
end
|
160
|
+
|
146
161
|
def authorized?(permission_action, resource: nil, permissions_holder: nil)
|
147
162
|
return unless resource || permissions_holder
|
148
163
|
|
@@ -155,8 +170,7 @@ module Decidim
|
|
155
170
|
|
156
171
|
can_unvote = initiative.accepts_online_unvotes? &&
|
157
172
|
initiative.organization&.id == user.organization&.id &&
|
158
|
-
initiative.votes.where(author: user).any?
|
159
|
-
authorized?(:vote, resource: initiative, permissions_holder: initiative.type)
|
173
|
+
initiative.votes.where(author: user).any?
|
160
174
|
|
161
175
|
toggle_allow(can_unvote)
|
162
176
|
end
|
@@ -178,15 +192,10 @@ module Decidim
|
|
178
192
|
toggle_allow(can_sign)
|
179
193
|
end
|
180
194
|
|
181
|
-
def decidim_user_group_id
|
182
|
-
context.fetch(:group_id, nil)
|
183
|
-
end
|
184
|
-
|
185
195
|
def can_vote?
|
186
196
|
initiative.votes_enabled? &&
|
187
197
|
initiative.organization&.id == user.organization&.id &&
|
188
|
-
initiative.votes.where(author: user).empty?
|
189
|
-
authorized?(:vote, resource: initiative, permissions_holder: initiative.type)
|
198
|
+
initiative.votes.where(author: user).empty?
|
190
199
|
end
|
191
200
|
|
192
201
|
def can_user_support?(initiative)
|
@@ -226,15 +235,16 @@ module Decidim
|
|
226
235
|
end
|
227
236
|
|
228
237
|
def allowed_to_send_to_technical_validation?
|
229
|
-
initiative.created? &&
|
230
|
-
!initiative.created_by_individual? ||
|
231
|
-
initiative.enough_committee_members?
|
232
|
-
)
|
238
|
+
initiative.created? && initiative.enough_committee_members?
|
233
239
|
end
|
234
240
|
|
235
241
|
def authorship_or_admin?
|
236
242
|
initiative&.has_authorship?(user) || user.admin?
|
237
243
|
end
|
244
|
+
|
245
|
+
def ephemeral_signature_workflow?
|
246
|
+
initiative.type.signature_workflow_manifest&.ephemeral?
|
247
|
+
end
|
238
248
|
end
|
239
249
|
end
|
240
250
|
end
|
@@ -4,13 +4,19 @@ module Decidim
|
|
4
4
|
#
|
5
5
|
# Decorator for initiatives
|
6
6
|
#
|
7
|
-
class InitiativePresenter <
|
7
|
+
class InitiativePresenter < Decidim::ResourcePresenter
|
8
8
|
def author
|
9
|
-
@author ||=
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
@author ||= super.presenter
|
10
|
+
end
|
11
|
+
|
12
|
+
def initiative
|
13
|
+
__getobj__
|
14
|
+
end
|
15
|
+
|
16
|
+
def title(html_escape: false, all_locales: false)
|
17
|
+
return unless initiative
|
18
|
+
|
19
|
+
super(initiative.title, html_escape, all_locales)
|
14
20
|
end
|
15
21
|
end
|
16
22
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Initiatives
|
5
|
+
# This class counts all Followers of a initiative
|
6
|
+
class InitiativesStatsFollowersCount < Decidim::StatsFollowersCount
|
7
|
+
def self.for(participatory_space)
|
8
|
+
return 0 unless participatory_space.is_a?(Decidim::Initiatives)
|
9
|
+
|
10
|
+
new(participatory_space).query
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Initiatives
|
5
|
+
# This class counts unique Participants on a initiative
|
6
|
+
class InitiativesStatsParticipantsCount < Decidim::StatsParticipantsCount
|
7
|
+
def self.for(participatory_space)
|
8
|
+
return 0 unless participatory_space.is_a?(Decidim::Initiatives)
|
9
|
+
|
10
|
+
new(participatory_space).query
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -24,7 +24,6 @@ module Decidim
|
|
24
24
|
answer: resource.answer,
|
25
25
|
answered_at: resource.answered_at,
|
26
26
|
answer_url: resource.answer_url,
|
27
|
-
hashtag: resource.hashtag,
|
28
27
|
first_progress_notification_at: resource.first_progress_notification_at,
|
29
28
|
second_progress_notification_at: resource.second_progress_notification_at,
|
30
29
|
online_votes: resource.online_votes,
|
@@ -8,7 +8,7 @@ module Decidim
|
|
8
8
|
|
9
9
|
def initialize(args = {})
|
10
10
|
@secret = args.fetch(:secret) || "default"
|
11
|
-
@key = ActiveSupport::KeyGenerator.new(secret).generate_key(
|
11
|
+
@key = ActiveSupport::KeyGenerator.new(secret, hash_digest_class: OpenSSL::Digest::SHA1).generate_key(
|
12
12
|
Rails.application.secret_key_base, ActiveSupport::MessageEncryptor.key_len
|
13
13
|
)
|
14
14
|
@encryptor = ActiveSupport::MessageEncryptor.new(@key)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Initiatives
|
5
|
+
# Signature handler which reproduces the old feature of
|
6
|
+
# `collect_personal_data` of initiative types. The handler will collect the
|
7
|
+
# name and surname, the document number and the date of birth
|
8
|
+
class LegacySignatureHandler < SignatureHandler
|
9
|
+
attribute :name_and_surname, String
|
10
|
+
attribute :document_number, String
|
11
|
+
attribute :date_of_birth, Date
|
12
|
+
attribute :postal_code, String
|
13
|
+
|
14
|
+
validates :name_and_surname, :document_number, :date_of_birth, :postal_code, presence: true
|
15
|
+
|
16
|
+
def unique_id
|
17
|
+
document_number
|
18
|
+
end
|
19
|
+
|
20
|
+
def metadata
|
21
|
+
super.merge(name_and_surname:, document_number:, date_of_birth:, postal_code:)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -12,15 +12,9 @@ module Decidim
|
|
12
12
|
|
13
13
|
# PUBLIC: Notifies the support progress of the initiative.
|
14
14
|
#
|
15
|
-
# Notifies to Initiative's authors
|
15
|
+
# Notifies to Initiative's authors about the
|
16
16
|
# number of supports received by the initiative.
|
17
17
|
def notify
|
18
|
-
initiative.followers.each do |follower|
|
19
|
-
Decidim::Initiatives::InitiativesMailer
|
20
|
-
.notify_progress(initiative, follower)
|
21
|
-
.deliver_later
|
22
|
-
end
|
23
|
-
|
24
18
|
initiative.committee_members.approved.each do |committee_member|
|
25
19
|
Decidim::Initiatives::InitiativesMailer
|
26
20
|
.notify_progress(initiative, committee_member.user)
|
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Initiatives
|
5
|
+
# This is the base class for signature handlers, all implementations
|
6
|
+
# should inherit from it.
|
7
|
+
# Each SignatureHandler is a form that will be used to check if the
|
8
|
+
# signature is valid or not. When it is valid the initiatives votes
|
9
|
+
# defined by the initiative type will be created for the user.
|
10
|
+
#
|
11
|
+
# Feel free to use validations to assert fields against a remote API,
|
12
|
+
# local database, or whatever.
|
13
|
+
#
|
14
|
+
# It also sets two default attributes, `user` and `initiative`.
|
15
|
+
class SignatureHandler < Form
|
16
|
+
include ValidatableAuthorizations
|
17
|
+
|
18
|
+
mimic :initiatives_vote
|
19
|
+
|
20
|
+
# The user that is trying to sign, it is initialized with the
|
21
|
+
# `current_user` from the controller.
|
22
|
+
attribute :user, Decidim::User
|
23
|
+
|
24
|
+
# The initiative to be signed
|
25
|
+
attribute :initiative, Decidim::Initiative
|
26
|
+
|
27
|
+
attribute :tos_agreement, if: :ephemeral_tos_pending?
|
28
|
+
validates :tos_agreement, presence: true, if: :ephemeral_tos_pending?
|
29
|
+
validate :tos_agreement_acceptance, if: :ephemeral_tos_pending?
|
30
|
+
|
31
|
+
attribute :transfer_status
|
32
|
+
|
33
|
+
validates :initiative, :user, presence: true
|
34
|
+
validate :uniqueness
|
35
|
+
validate :valid_metadata
|
36
|
+
validate :valid_authorized_scopes
|
37
|
+
|
38
|
+
delegate :promote_authorization_validation_errors, :authorization_handler_form_class, :ephemeral?, to: :workflow_manifest
|
39
|
+
delegate :scope, to: :initiative
|
40
|
+
|
41
|
+
# A unique ID to be implemented by the signature handler that ensures
|
42
|
+
# no duplicates are created.
|
43
|
+
def unique_id
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def encrypted_metadata
|
48
|
+
return if metadata.blank?
|
49
|
+
|
50
|
+
@encrypted_metadata ||= encryptor.encrypt(metadata)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Public: Builds the list of scopes where the user is authorized to vote in. This is used when
|
54
|
+
# the initiative allows also voting on child scopes, not only the main scope.
|
55
|
+
#
|
56
|
+
# Instead of just listing the children of the main scope, we just want to select the ones that
|
57
|
+
# have been added to the InitiativeType with its voting settings.
|
58
|
+
#
|
59
|
+
def authorized_scopes
|
60
|
+
initiative.votable_initiative_type_scopes.select do |initiative_type_scope|
|
61
|
+
initiative_type_scope.global_scope? ||
|
62
|
+
initiative_type_scope.scope == user_signature_scope ||
|
63
|
+
initiative_type_scope.scope.ancestor_of?(user_signature_scope)
|
64
|
+
end.flat_map(&:scope)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Public: Finds the scope the user has an authorization for, this way the user can vote
|
68
|
+
# on that scope and its parents.
|
69
|
+
#
|
70
|
+
# This is can be used to allow users that are authorized with a children
|
71
|
+
# scope to sign an initiative with a parent scope.
|
72
|
+
#
|
73
|
+
# As an example: A city (global scope) has many districts (scopes with
|
74
|
+
# parent nil), and each district has different neighbourhoods (with its
|
75
|
+
# parent as a district). If we setup the authorization handler to match
|
76
|
+
# a neighbourhood, the same authorization can be used to participate
|
77
|
+
# in district, neighbourhoods or city initiatives.
|
78
|
+
#
|
79
|
+
# Returns a Decidim::Scope.
|
80
|
+
def user_signature_scope
|
81
|
+
return if signature_scope_id.blank?
|
82
|
+
|
83
|
+
@user_signature_scope ||= signature_scope_candidates.find do |scope_candidate|
|
84
|
+
scope_candidate&.id == signature_scope_id
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Public: Builds a list of Decidim::Scopes where the user could have a
|
89
|
+
# valid authorization.
|
90
|
+
#
|
91
|
+
# If the initiative is set with a global scope (meaning the scope is nil),
|
92
|
+
# all the scopes in the organization are valid.
|
93
|
+
#
|
94
|
+
# Returns an array of Decidim::Scopes.
|
95
|
+
def signature_scope_candidates
|
96
|
+
signature_scope_candidates = [initiative.scope]
|
97
|
+
signature_scope_candidates += if initiative.scope.present?
|
98
|
+
initiative.scope.descendants
|
99
|
+
else
|
100
|
+
initiative.organization.scopes
|
101
|
+
end
|
102
|
+
signature_scope_candidates.uniq
|
103
|
+
end
|
104
|
+
|
105
|
+
# Any data that the developer would like to inject to the `metadata` field
|
106
|
+
# of a vote when it is created. Can be useful if some of the params the
|
107
|
+
# user sent with the signature form want to be persisted for future use.
|
108
|
+
#
|
109
|
+
# Returns a Hash.
|
110
|
+
def metadata
|
111
|
+
{}
|
112
|
+
end
|
113
|
+
|
114
|
+
# Params to be sent to the authorization handler. By default consists on
|
115
|
+
# the metadata hash including the signer user
|
116
|
+
def authorization_handler_params
|
117
|
+
params = metadata.merge(user:)
|
118
|
+
params = params.merge(tos_agreement:) if ephemeral_tos_pending?
|
119
|
+
params
|
120
|
+
end
|
121
|
+
|
122
|
+
# The signature_scope_id can be defined in the signature workflow to be
|
123
|
+
# used by the author scope feature
|
124
|
+
def signature_scope_id
|
125
|
+
scope.id
|
126
|
+
end
|
127
|
+
|
128
|
+
def authorization_handler
|
129
|
+
return if authorization_handler_form_class.blank?
|
130
|
+
|
131
|
+
@authorization_handler ||= authorization_handler_form_class.from_params(authorization_handler_params)
|
132
|
+
end
|
133
|
+
|
134
|
+
def signature_workflow_name
|
135
|
+
@signature_workflow_name ||= initiative&.type&.document_number_authorization_handler
|
136
|
+
end
|
137
|
+
|
138
|
+
def hash_id
|
139
|
+
return unless initiative && (unique_id || user)
|
140
|
+
|
141
|
+
@hash_id ||= Digest::SHA256.hexdigest(
|
142
|
+
[
|
143
|
+
initiative.id,
|
144
|
+
unique_id || user.id,
|
145
|
+
Rails.application.secret_key_base
|
146
|
+
].compact.join("-")
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
# The attributes of the handler that should be exposed as form input when
|
151
|
+
# rendering the handler in a form.
|
152
|
+
#
|
153
|
+
# Returns an Array of Strings.
|
154
|
+
def form_attributes
|
155
|
+
attributes.except("id", "user", "initiative", "tos_agreement", "transfer_status").keys
|
156
|
+
end
|
157
|
+
|
158
|
+
# The String partial path so Rails can render the handler as a form. This
|
159
|
+
# is useful if you want to have a custom view to render the form instead of
|
160
|
+
# the default view.
|
161
|
+
#
|
162
|
+
# Example:
|
163
|
+
#
|
164
|
+
# A handler named Decidim::CensusHandler would look for its partial in:
|
165
|
+
# decidim/census/form
|
166
|
+
#
|
167
|
+
# Returns a String.
|
168
|
+
def to_partial_path
|
169
|
+
"decidim/initiatives/initiative_signatures/#{signature_workflow_name.sub(/_handler$/, "")}/form"
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.requires_extra_attributes?
|
173
|
+
new.form_attributes.present?
|
174
|
+
end
|
175
|
+
|
176
|
+
def already_voted?
|
177
|
+
Decidim::InitiativesVote.exists?(author: user, initiative:)
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
# It is expected to validate that no other user has voted with the same
|
183
|
+
# unique_id and scope. The unique_id should be defined by the classes
|
184
|
+
# inherited from this taking a personal data attribute like a document
|
185
|
+
# number. If not defined the user id is used
|
186
|
+
def uniqueness
|
187
|
+
add_invalid_base_error if Decidim::InitiativesVote.exists?(scope:, hash_id:)
|
188
|
+
end
|
189
|
+
|
190
|
+
def valid_metadata
|
191
|
+
return if authorization_handler_errors.blank?
|
192
|
+
|
193
|
+
keys = attributes.except("tos_agreement").keys.map(&:to_sym) & authorization_handler_errors.attribute_names
|
194
|
+
|
195
|
+
return if keys.blank? && authorization_handler_errors[:base].blank?
|
196
|
+
|
197
|
+
# Promote errors
|
198
|
+
if promote_authorization_validation_errors
|
199
|
+
keys.each do |attribute|
|
200
|
+
errors.add(attribute, authorization_handler_errors[attribute])
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
add_invalid_base_error
|
205
|
+
end
|
206
|
+
|
207
|
+
def tos_agreement_acceptance
|
208
|
+
return if (error_message = authorization_handler_errors[:tos_agreement]).blank?
|
209
|
+
|
210
|
+
errors.add(:tos_agreement, error_message)
|
211
|
+
end
|
212
|
+
|
213
|
+
def valid_authorized_scopes
|
214
|
+
return if authorized_scopes.present?
|
215
|
+
|
216
|
+
add_invalid_base_error
|
217
|
+
end
|
218
|
+
|
219
|
+
def add_invalid_base_error
|
220
|
+
errors.delete(:base)
|
221
|
+
errors.add(:base, I18n.t("invalid_data", scope: "decidim.initiatives.initiative_signatures.fill_personal_data"))
|
222
|
+
end
|
223
|
+
|
224
|
+
def encryptor
|
225
|
+
@encryptor ||= DataEncryptor.new(secret: Decidim::Initiatives.signature_handler_encryption_secret)
|
226
|
+
end
|
227
|
+
|
228
|
+
def workflow_manifest
|
229
|
+
@workflow_manifest ||= Decidim::Initiatives::Signatures.find_workflow_manifest(signature_workflow_name) || Decidim::Initiatives::SignatureWorkflowManifest.new
|
230
|
+
end
|
231
|
+
|
232
|
+
def ephemeral_tos_pending?
|
233
|
+
return unless ephemeral? && user.ephemeral?
|
234
|
+
|
235
|
+
!user.tos_accepted?
|
236
|
+
end
|
237
|
+
|
238
|
+
def authorization_handler_errors
|
239
|
+
@authorization_handler_errors ||= if authorization_handler.blank?
|
240
|
+
ActiveModel::Errors.new(nil)
|
241
|
+
else
|
242
|
+
authorization_handler.validate
|
243
|
+
authorization_handler.errors
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|