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.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -9
  3. data/app/cells/decidim/initiatives/content_blocks/highlighted_initiatives_settings_form/show.erb +7 -2
  4. data/app/cells/decidim/initiatives/initiative_g_cell.rb +5 -1
  5. data/app/commands/decidim/initiatives/admin/publish_initiative.rb +1 -5
  6. data/app/commands/decidim/initiatives/admin/update_initiative.rb +1 -2
  7. data/app/commands/decidim/initiatives/create_initiative.rb +0 -1
  8. data/app/commands/decidim/initiatives/update_initiative.rb +1 -3
  9. data/app/commands/decidim/initiatives/vote_initiative.rb +1 -11
  10. data/app/controllers/concerns/decidim/initiatives/has_signature_workflow.rb +36 -0
  11. data/app/controllers/concerns/decidim/initiatives/needs_initiative.rb +1 -12
  12. data/app/controllers/decidim/initiatives/admin/initiatives_controller.rb +2 -2
  13. data/app/controllers/decidim/initiatives/admin/initiatives_settings_controller.rb +1 -1
  14. data/app/controllers/decidim/initiatives/admin/initiatives_type_scopes_controller.rb +2 -2
  15. data/app/controllers/decidim/initiatives/admin/initiatives_types_controller.rb +2 -2
  16. data/app/controllers/decidim/initiatives/committee_requests_controller.rb +10 -2
  17. data/app/controllers/decidim/initiatives/create_initiative_controller.rb +84 -18
  18. data/app/controllers/decidim/initiatives/initiative_signatures_controller.rb +133 -42
  19. data/app/controllers/decidim/initiatives/initiative_votes_controller.rb +3 -2
  20. data/app/controllers/decidim/initiatives/initiatives_controller.rb +21 -2
  21. data/app/forms/decidim/initiatives/admin/initiative_form.rb +0 -1
  22. data/app/forms/decidim/initiatives/initiative_form.rb +0 -3
  23. data/app/helpers/decidim/initiatives/application_helper.rb +2 -0
  24. data/app/helpers/decidim/initiatives/initiatives_helper.rb +0 -1
  25. data/app/models/decidim/initiative.rb +7 -31
  26. data/app/models/decidim/initiatives_committee_member.rb +1 -1
  27. data/app/models/decidim/initiatives_type.rb +5 -2
  28. data/app/models/decidim/initiatives_vote.rb +2 -2
  29. data/app/packs/entrypoints/decidim_initiatives.js +1 -1
  30. data/app/packs/entrypoints/decidim_initiatives_admin.scss +1 -1
  31. data/app/packs/src/decidim/initiatives/admin/initiatives_types.js +2 -11
  32. data/app/packs/src/decidim/initiatives/admin/invite_users.js +1 -1
  33. data/app/packs/src/decidim/initiatives/application.js +1 -1
  34. data/app/packs/src/decidim/initiatives/check_code.js +114 -0
  35. data/app/packs/src/decidim/initiatives/initiative_creation_wizard.js +16 -0
  36. data/app/packs/src/decidim/initiatives/scoped_type.js +1 -1
  37. data/app/packs/stylesheets/initiatives.scss +16 -2
  38. data/app/permissions/decidim/initiatives/admin/permissions.rb +1 -4
  39. data/app/permissions/decidim/initiatives/permissions.rb +26 -16
  40. data/app/presenters/decidim/initiative_presenter.rb +12 -6
  41. data/app/presenters/decidim/initiatives/admin_log/initiative_presenter.rb +1 -2
  42. data/app/queries/decidim/initiatives/initiatives_stats_followers_count.rb +14 -0
  43. data/app/queries/decidim/initiatives/initiatives_stats_participants_count.rb +14 -0
  44. data/app/serializers/decidim/initiatives/open_data_initiative_serializer.rb +0 -1
  45. data/app/services/decidim/initiatives/data_encryptor.rb +1 -1
  46. data/app/services/decidim/initiatives/legacy_signature_handler.rb +25 -0
  47. data/app/services/decidim/initiatives/progress_notifier.rb +1 -7
  48. data/app/services/decidim/initiatives/signature_handler.rb +248 -0
  49. data/app/services/decidim/initiatives/status_change_notifier.rb +1 -7
  50. data/app/views/decidim/initiatives/admin/committee_requests/index.html.erb +29 -11
  51. data/app/views/decidim/initiatives/admin/exports/_dropdown.html.erb +17 -20
  52. data/app/views/decidim/initiatives/admin/initiatives/_form.html.erb +7 -13
  53. data/app/views/decidim/initiatives/admin/initiatives/_initiative_attachments.erb +2 -2
  54. data/app/views/decidim/initiatives/admin/initiatives/index.html.erb +76 -47
  55. data/app/views/decidim/initiatives/admin/initiatives_types/_form.html.erb +13 -21
  56. data/app/views/decidim/initiatives/admin/initiatives_types/_initiative_type_scopes.html.erb +28 -12
  57. data/app/views/decidim/initiatives/admin/initiatives_types/index.html.erb +33 -15
  58. data/app/views/decidim/initiatives/create_initiative/_committee_member.html.erb +27 -0
  59. data/app/views/decidim/initiatives/create_initiative/_return_to_initiatives_button.html.erb +3 -0
  60. data/app/views/decidim/initiatives/create_initiative/_send_to_technical_validation_button.html.erb +10 -0
  61. data/app/views/decidim/initiatives/create_initiative/_share_committee_link.html.erb +5 -1
  62. data/app/views/decidim/initiatives/create_initiative/fill_data.html.erb +7 -11
  63. data/app/views/decidim/initiatives/create_initiative/finish.html.erb +16 -13
  64. data/app/views/decidim/initiatives/create_initiative/promotal_committee.html.erb +33 -6
  65. data/app/views/decidim/initiatives/create_initiative/select_initiative_type.html.erb +40 -26
  66. data/app/views/decidim/initiatives/initiative_signatures/_sms_code_form.html.erb +22 -0
  67. data/app/views/decidim/initiatives/initiative_signatures/_sms_phone_number_form.html.erb +13 -0
  68. data/app/views/decidim/initiatives/initiative_signatures/fill_personal_data.html.erb +23 -22
  69. data/app/views/decidim/initiatives/initiative_signatures/finish.html.erb +17 -5
  70. data/app/views/decidim/initiatives/initiative_signatures/sms_code.html.erb +6 -8
  71. data/app/views/decidim/initiatives/initiative_signatures/sms_phone_number.html.erb +3 -8
  72. data/app/views/decidim/initiatives/initiative_signatures/update_buttons_and_counters.js.erb +3 -14
  73. data/app/views/decidim/initiatives/initiative_votes/update_buttons_and_counters.js.erb +3 -14
  74. data/app/views/decidim/initiatives/initiatives/_committee_members.html.erb +1 -1
  75. data/app/views/decidim/initiatives/initiatives/_form.html.erb +1 -3
  76. data/app/views/decidim/initiatives/initiatives/_new_initiative_button.html.erb +10 -3
  77. data/app/views/decidim/initiatives/initiatives/_pending_initiatives.html.erb +5 -0
  78. data/app/views/decidim/initiatives/initiatives/index.html.erb +8 -0
  79. data/app/views/decidim/initiatives/initiatives/show.html.erb +2 -2
  80. data/app/views/layouts/decidim/_initiative_signature_creation_header.html.erb +20 -2
  81. data/app/views/layouts/decidim/admin/_manage_initiatives.html.erb +1 -1
  82. data/app/views/layouts/decidim/initiative_signature_creation.html.erb +3 -1
  83. data/config/assets.rb +2 -2
  84. data/config/locales/ar.yml +0 -45
  85. data/config/locales/bg.yml +0 -54
  86. data/config/locales/ca-IT.yml +99 -51
  87. data/config/locales/ca.yml +99 -51
  88. data/config/locales/cs.yml +93 -54
  89. data/config/locales/de.yml +99 -51
  90. data/config/locales/el.yml +0 -45
  91. data/config/locales/en.yml +99 -51
  92. data/config/locales/es-MX.yml +99 -51
  93. data/config/locales/es-PY.yml +99 -51
  94. data/config/locales/es.yml +99 -51
  95. data/config/locales/eu.yml +99 -51
  96. data/config/locales/fi-plain.yml +99 -51
  97. data/config/locales/fi.yml +99 -51
  98. data/config/locales/fr-CA.yml +44 -51
  99. data/config/locales/fr.yml +44 -51
  100. data/config/locales/ga-IE.yml +0 -17
  101. data/config/locales/gl.yml +0 -41
  102. data/config/locales/hu.yml +0 -54
  103. data/config/locales/id-ID.yml +0 -40
  104. data/config/locales/is-IS.yml +0 -22
  105. data/config/locales/it.yml +0 -53
  106. data/config/locales/ja.yml +98 -49
  107. data/config/locales/lb.yml +0 -50
  108. data/config/locales/lt.yml +0 -56
  109. data/config/locales/lv.yml +0 -46
  110. data/config/locales/nl.yml +0 -47
  111. data/config/locales/no.yml +0 -53
  112. data/config/locales/pl.yml +0 -56
  113. data/config/locales/pt-BR.yml +0 -53
  114. data/config/locales/pt.yml +0 -53
  115. data/config/locales/ro-RO.yml +92 -50
  116. data/config/locales/ru.yml +0 -25
  117. data/config/locales/sk.yml +0 -43
  118. data/config/locales/sl.yml +0 -1
  119. data/config/locales/sv.yml +10 -53
  120. data/config/locales/tr-TR.yml +0 -53
  121. data/config/locales/uk.yml +0 -25
  122. data/config/locales/zh-CN.yml +0 -45
  123. data/config/locales/zh-TW.yml +0 -53
  124. data/db/migrate/20250605104500_remove_hashtag_column_initiatives.rb +7 -0
  125. data/lib/decidim/api/initiative_api_type.rb +3 -0
  126. data/lib/decidim/api/initiative_type.rb +23 -4
  127. data/lib/decidim/exporters/initiative_votes_pdf.rb +1 -1
  128. data/lib/decidim/initiatives/default_signature_authorizer.rb +17 -0
  129. data/lib/decidim/initiatives/engine.rb +17 -14
  130. data/lib/decidim/initiatives/participatory_space.rb +15 -1
  131. data/lib/decidim/initiatives/seeds.rb +1 -2
  132. data/lib/decidim/initiatives/signature_workflow_manifest.rb +176 -0
  133. data/lib/decidim/initiatives/signatures.rb +12 -0
  134. data/lib/decidim/initiatives/test/factories.rb +7 -7
  135. data/lib/decidim/initiatives/test/initiatives_signatures_test_helpers.rb +19 -0
  136. data/lib/decidim/initiatives/validatable_authorizations.rb +83 -0
  137. data/lib/decidim/initiatives/version.rb +1 -1
  138. data/lib/decidim/initiatives.rb +23 -12
  139. metadata +33 -21
  140. data/app/events/decidim/initiatives/endorse_initiative_event.rb +0 -13
  141. data/app/forms/decidim/initiatives/vote_form.rb +0 -208
  142. data/app/packs/src/decidim/initiatives/identity_selector_dialog.js +0 -14
  143. data/app/services/decidim/initiatives/pdf_signature_example.rb +0 -110
  144. data/app/views/decidim/initiatives/initiative_signatures/_wizard_steps.html.erb +0 -15
  145. data/app/views/decidim/initiatives/initiatives/_interactions.html.erb +0 -10
  146. 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-10;
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 < SimpleDelegator
7
+ class InitiativePresenter < Decidim::ResourcePresenter
8
8
  def author
9
- @author ||= if user_group
10
- Decidim::UserGroupPresenter.new(user_group)
11
- else
12
- Decidim::UserPresenter.new(super)
13
- end
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
@@ -31,8 +31,7 @@ module Decidim
31
31
  signature_start_date: :date,
32
32
  signature_end_date: :date,
33
33
  description: :i18n,
34
- title: :i18n,
35
- hashtag: :string
34
+ title: :i18n
36
35
  }
37
36
  end
38
37
 
@@ -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 and followers about the
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