decidim-initiatives 0.20.0 → 0.23.1.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -0
  3. data/app/assets/images/decidim/gamification/badges/initiatives.svg +1 -87
  4. data/app/assets/images/decidim/initiatives/icon.svg +1 -3
  5. data/app/assets/javascripts/decidim/initiatives/admin/initiatives_types.js.es6 +18 -0
  6. data/app/assets/stylesheet/decidim/initiatives/initiatives-votes.css.scss +0 -1
  7. data/app/assets/stylesheet/decidim/initiatives/initiatives.scss +6 -8
  8. data/app/assets/stylesheet/decidim/initiatives/popularity_item.css.scss +0 -1
  9. data/app/assets/stylesheet/decidim/initiatives/print-initiative.css.scss +0 -3
  10. data/app/cells/decidim/initiatives/content_blocks/highlighted_initiatives/show.erb +4 -3
  11. data/app/cells/decidim/initiatives/initiative_m_cell.rb +25 -0
  12. data/app/cells/decidim/initiatives_votes/vote/show.erb +12 -9
  13. data/app/cells/decidim/initiatives_votes/vote_cell.rb +7 -0
  14. data/app/commands/decidim/initiatives/admin/create_initiative_type.rb +6 -1
  15. data/app/commands/decidim/initiatives/admin/send_initiative_to_technical_validation.rb +17 -0
  16. data/app/commands/decidim/initiatives/admin/update_initiative.rb +29 -10
  17. data/app/commands/decidim/initiatives/admin/update_initiative_type.rb +6 -1
  18. data/app/commands/decidim/initiatives/attachment_methods.rb +37 -0
  19. data/app/commands/decidim/initiatives/create_initiative.rb +25 -3
  20. data/app/commands/decidim/initiatives/unvote_initiative.rb +4 -10
  21. data/app/commands/decidim/initiatives/vote_initiative.rb +47 -31
  22. data/app/controllers/concerns/decidim/initiatives/admin/filterable.rb +51 -0
  23. data/app/controllers/concerns/decidim/initiatives/orderable.rb +3 -1
  24. data/app/controllers/concerns/decidim/initiatives/single_initiative_type.rb +26 -0
  25. data/app/controllers/decidim/initiatives/admin/answers_controller.rb +2 -3
  26. data/app/controllers/decidim/initiatives/admin/initiatives_controller.rb +28 -15
  27. data/app/controllers/decidim/initiatives/create_initiative_controller.rb +36 -6
  28. data/app/controllers/decidim/initiatives/initiative_signatures_controller.rb +19 -23
  29. data/app/controllers/decidim/initiatives/initiative_votes_controller.rb +11 -5
  30. data/app/controllers/decidim/initiatives/initiatives_controller.rb +28 -13
  31. data/app/controllers/decidim/initiatives/initiatives_type_scopes_controller.rb +9 -1
  32. data/app/controllers/decidim/initiatives/versions_controller.rb +20 -0
  33. data/app/controllers/decidim/initiatives/{initiative_widgets_controller.rb → widgets_controller.rb} +2 -2
  34. data/app/events/decidim/initiatives/admin/initiative_sent_to_technical_validation_event.rb +21 -0
  35. data/app/events/decidim/initiatives/admin/support_threshold_reached_event.rb +13 -0
  36. data/app/forms/decidim/initiatives/admin/initiative_form.rb +49 -7
  37. data/app/forms/decidim/initiatives/admin/initiative_type_form.rb +8 -1
  38. data/app/forms/decidim/initiatives/initiative_form.rb +56 -1
  39. data/app/forms/decidim/initiatives/vote_form.rb +133 -76
  40. data/app/helpers/decidim/initiatives/application_helper.rb +104 -0
  41. data/app/helpers/decidim/initiatives/initiative_helper.rb +13 -0
  42. data/app/helpers/decidim/initiatives/initiatives_helper.rb +10 -0
  43. data/app/jobs/decidim/initiatives/export_initiatives_job.rb +25 -0
  44. data/app/mailers/decidim/initiatives/initiatives_mailer.rb +0 -21
  45. data/app/models/concerns/decidim/initiatives/has_area.rb +30 -0
  46. data/app/models/decidim/initiative.rb +184 -44
  47. data/app/models/decidim/initiatives_type.rb +5 -2
  48. data/app/models/decidim/initiatives_type_scope.rb +5 -1
  49. data/app/models/decidim/initiatives_vote.rb +19 -23
  50. data/app/permissions/decidim/initiatives/admin/permissions.rb +19 -8
  51. data/app/permissions/decidim/initiatives/permissions.rb +37 -14
  52. data/app/presenters/decidim/initiatives/initiative_stats_presenter.rb +1 -5
  53. data/app/queries/decidim/initiatives/admin/manageable_initiatives.rb +7 -39
  54. data/app/serializers/decidim/initiatives/initiative_serializer.rb +32 -0
  55. data/app/services/decidim/initiatives/diff_renderer.rb +18 -0
  56. data/app/services/decidim/initiatives/initiative_search.rb +59 -15
  57. data/app/services/decidim/initiatives/status_change_notifier.rb +4 -5
  58. data/app/types/decidim/initiatives/initiative_api_type.rb +26 -0
  59. data/app/types/decidim/initiatives/initiative_committee_member_type.rb +18 -0
  60. data/app/types/decidim/initiatives/initiative_type.rb +42 -0
  61. data/app/views/decidim/initiatives/admin/answers/_info_initiative.html.erb +1 -1
  62. data/app/views/decidim/initiatives/admin/exports/_dropdown.html.erb +8 -0
  63. data/app/views/decidim/initiatives/admin/initiatives/_form.html.erb +59 -14
  64. data/app/views/decidim/initiatives/admin/initiatives/_initiative_attachments.erb +43 -0
  65. data/app/views/decidim/initiatives/admin/initiatives/export_pdf_signatures.pdf.erb +12 -9
  66. data/app/views/decidim/initiatives/admin/initiatives/index.html.erb +10 -45
  67. data/app/views/decidim/initiatives/admin/initiatives_types/_form.html.erb +47 -10
  68. data/app/views/decidim/initiatives/admin/initiatives_types/_initiative_type_scopes.html.erb +28 -25
  69. data/app/views/decidim/initiatives/create_initiative/fill_data.html.erb +46 -10
  70. data/app/views/decidim/initiatives/create_initiative/finish.html.erb +17 -10
  71. data/app/views/decidim/initiatives/create_initiative/previous_form.html.erb +2 -1
  72. data/app/views/decidim/initiatives/create_initiative/promotal_committee.html.erb +1 -1
  73. data/app/views/decidim/initiatives/create_initiative/select_initiative_type.html.erb +1 -2
  74. data/app/views/decidim/initiatives/create_initiative/show_similar_initiatives.html.erb +1 -1
  75. data/app/views/decidim/initiatives/initiative_signatures/fill_personal_data.html.erb +1 -1
  76. data/app/views/decidim/initiatives/initiative_signatures/update_buttons_and_counters.js.erb +1 -1
  77. data/app/views/decidim/initiatives/initiative_votes/update_buttons_and_counters.js.erb +1 -1
  78. data/app/views/decidim/initiatives/initiatives/_author.html.erb +1 -1
  79. data/app/views/decidim/initiatives/initiatives/_filters.html.erb +16 -28
  80. data/app/views/decidim/initiatives/initiatives/_index_header.html.erb +39 -5
  81. data/app/views/decidim/initiatives/initiatives/_initiatives.html.erb +11 -1
  82. data/app/views/decidim/initiatives/initiatives/_interactions.html.erb +2 -3
  83. data/app/views/decidim/initiatives/initiatives/_progress_bar.html.erb +24 -9
  84. data/app/views/decidim/initiatives/initiatives/_tags.html.erb +3 -0
  85. data/app/views/decidim/initiatives/initiatives/_vote_cabin.html.erb +1 -13
  86. data/app/views/decidim/initiatives/initiatives/index.html.erb +1 -1
  87. data/app/views/decidim/initiatives/initiatives/show.html.erb +2 -3
  88. data/app/views/decidim/initiatives/versions/index.html.erb +8 -0
  89. data/app/views/decidim/initiatives/versions/show.html.erb +10 -0
  90. data/app/views/layouts/decidim/_initiative_creation_header.html.erb +2 -1
  91. data/app/views/layouts/decidim/_initiative_header.html.erb +2 -1
  92. data/app/views/layouts/decidim/_initiative_signature_creation_header.html.erb +1 -1
  93. data/app/views/layouts/decidim/initiative.html.erb +1 -0
  94. data/app/views/layouts/decidim/initiative_creation.html.erb +1 -2
  95. data/app/views/layouts/decidim/initiative_signature_creation.html.erb +2 -2
  96. data/config/locales/am-ET.yml +1 -0
  97. data/config/locales/ar.yml +17 -21
  98. data/config/locales/bg-BG.yml +13 -0
  99. data/config/locales/bg.yml +13 -0
  100. data/config/locales/ca.yml +100 -22
  101. data/config/locales/cs.yml +110 -32
  102. data/config/locales/da-DK.yml +1 -0
  103. data/config/locales/da.yml +1 -0
  104. data/config/locales/de.yml +75 -21
  105. data/config/locales/el-GR.yml +1 -0
  106. data/config/locales/el.yml +525 -0
  107. data/config/locales/en.yml +102 -24
  108. data/config/locales/eo.yml +1 -0
  109. data/config/locales/es-MX.yml +100 -21
  110. data/config/locales/es-PY.yml +100 -21
  111. data/config/locales/es.yml +104 -25
  112. data/config/locales/et-EE.yml +1 -0
  113. data/config/locales/et.yml +1 -0
  114. data/config/locales/eu.yml +4 -21
  115. data/config/locales/fi-plain.yml +99 -21
  116. data/config/locales/fi.yml +117 -39
  117. data/config/locales/fr-CA.yml +557 -0
  118. data/config/locales/fr.yml +101 -23
  119. data/config/locales/ga-IE.yml +1 -0
  120. data/config/locales/gl.yml +4 -21
  121. data/config/locales/hr-HR.yml +1 -0
  122. data/config/locales/hr.yml +1 -0
  123. data/config/locales/hu.yml +30 -24
  124. data/config/locales/id-ID.yml +4 -21
  125. data/config/locales/is-IS.yml +251 -0
  126. data/config/locales/is.yml +251 -0
  127. data/config/locales/it.yml +116 -61
  128. data/config/locales/ja-JP.yml +529 -0
  129. data/config/locales/ja.yml +549 -0
  130. data/config/locales/ko-KR.yml +1 -0
  131. data/config/locales/ko.yml +1 -0
  132. data/config/locales/lt-LT.yml +1 -0
  133. data/config/locales/lt.yml +1 -0
  134. data/config/locales/lv.yml +525 -0
  135. data/config/locales/mt-MT.yml +1 -0
  136. data/config/locales/mt.yml +1 -0
  137. data/config/locales/nl.yml +76 -21
  138. data/config/locales/no.yml +368 -9
  139. data/config/locales/om-ET.yml +1 -0
  140. data/config/locales/pl.yml +260 -189
  141. data/config/locales/pt-BR.yml +5 -22
  142. data/config/locales/pt.yml +231 -179
  143. data/config/locales/ro-RO.yml +533 -0
  144. data/config/locales/ru.yml +4 -21
  145. data/config/locales/sk-SK.yml +468 -0
  146. data/config/locales/sk.yml +458 -0
  147. data/config/locales/sl.yml +24 -0
  148. data/config/locales/so-SO.yml +1 -0
  149. data/config/locales/sr-CS.yml +8 -0
  150. data/config/locales/sv.yml +102 -31
  151. data/config/locales/ti-ER.yml +1 -0
  152. data/config/locales/tr-TR.yml +4 -21
  153. data/config/locales/uk.yml +4 -21
  154. data/config/locales/vi-VN.yml +1 -0
  155. data/config/locales/vi.yml +1 -0
  156. data/config/locales/zh-CN.yml +549 -0
  157. data/config/locales/zh-TW.yml +1 -0
  158. data/db/migrate/20191106144259_add_settings_to_initiatives_types.rb +8 -0
  159. data/db/migrate/20191107134847_add_scopes_to_initiatives_votes.rb +28 -0
  160. data/db/migrate/20191116170841_allow_multiple_initiative_votes_counter_caches.rb +32 -0
  161. data/db/migrate/20191118105634_allow_multiple_offline_votes.rb +34 -0
  162. data/db/migrate/20200320105920_index_foreign_keys_in_decidim_initiatives.rb +8 -0
  163. data/db/migrate/20200320105921_index_foreign_keys_in_decidim_initiatives_votes.rb +8 -0
  164. data/db/migrate/20200417120551_add_custom_signature_end_time_option.rb +7 -0
  165. data/db/migrate/20200424110930_add_attachments_enabled_option.rb +7 -0
  166. data/db/migrate/20200514085422_add_area_to_initiatives.rb +7 -0
  167. data/db/migrate/20200514102631_add_area_enabled_option_to_initiatives.rb +7 -0
  168. data/db/migrate/20200528151456_remove_user_groups_from_initiative_votes.rb +7 -0
  169. data/db/migrate/20200827154214_add_commentable_counter_cache_to_initiatives.rb +9 -0
  170. data/db/seeds/city.jpeg +0 -0
  171. data/db/seeds/city2.jpeg +0 -0
  172. data/lib/decidim/api/initiative_type_interface.rb +13 -0
  173. data/lib/decidim/initiatives/admin_engine.rb +5 -0
  174. data/lib/decidim/initiatives/api.rb +7 -0
  175. data/lib/decidim/initiatives/engine.rb +11 -2
  176. data/lib/decidim/initiatives/participatory_space.rb +18 -1
  177. data/lib/decidim/initiatives/query_extensions.rb +40 -0
  178. data/lib/decidim/initiatives/test/factories.rb +57 -10
  179. data/lib/decidim/initiatives/version.rb +1 -1
  180. data/lib/tasks/decidim_initiatives.rake +1 -3
  181. metadata +95 -22
  182. data/app/views/decidim/initiatives/initiative_widgets/show.html.erb +0 -4
  183. data/app/views/decidim/initiatives/initiatives/_supports.html.erb +0 -22
  184. data/app/views/decidim/initiatives/initiatives/signature_identities.html.erb +0 -42
  185. data/app/views/decidim/initiatives/initiatives_mailer/notify_validating_request.html.erb +0 -3
@@ -14,6 +14,11 @@ module Decidim
14
14
  attribute :banner_image, String
15
15
  attribute :signature_type, String
16
16
  attribute :undo_online_signatures_enabled, Boolean
17
+ attribute :attachments_enabled, Boolean
18
+ attribute :custom_signature_end_date_enabled, Boolean
19
+ attribute :area_enabled, Boolean
20
+ attribute :child_scope_threshold_enabled, Boolean
21
+ attribute :only_global_scope_enabled, Boolean
17
22
  attribute :promoting_committee_enabled, Boolean
18
23
  attribute :minimum_committee_members, Integer
19
24
  attribute :collect_user_extra_fields, Boolean
@@ -22,9 +27,11 @@ module Decidim
22
27
  attribute :document_number_authorization_handler, String
23
28
 
24
29
  validates :title, :description, translatable_presence: true
25
- validates :undo_online_signatures_enabled, :promoting_committee_enabled, inclusion: { in: [true, false] }
30
+ validates :attachments_enabled, :undo_online_signatures_enabled, :custom_signature_end_date_enabled,
31
+ :area_enabled, :promoting_committee_enabled, inclusion: { in: [true, false] }
26
32
  validates :minimum_committee_members, numericality: { only_integer: true }, allow_nil: true
27
33
  validates :banner_image, presence: true, if: ->(form) { form.context.initiative_type.nil? }
34
+ validates :document_number_authorization_handler, presence: true, if: ->(form) { form.collect_user_extra_fields? }
28
35
 
29
36
  def minimum_committee_members=(value)
30
37
  super(value.presence)
@@ -12,15 +12,24 @@ module Decidim
12
12
  attribute :description, String
13
13
  attribute :type_id, Integer
14
14
  attribute :scope_id, Integer
15
+ attribute :area_id, Integer
15
16
  attribute :decidim_user_group_id, Integer
16
17
  attribute :signature_type, String
18
+ attribute :signature_end_date, Date
17
19
  attribute :state, String
20
+ attribute :attachment, AttachmentForm
18
21
 
19
22
  validates :title, :description, presence: true
20
23
  validates :title, length: { maximum: 150 }
21
24
  validates :signature_type, presence: true
22
25
  validates :type_id, presence: true
26
+ validates :area, presence: true, if: ->(form) { form.area_id.present? }
23
27
  validate :scope_exists
28
+ validate :notify_missing_attachment_if_errored
29
+ validate :trigger_attachment_errors
30
+ validates :signature_end_date, date: { after: Date.current }, if: lambda { |form|
31
+ form.context.initiative_type.custom_signature_end_date_enabled? && form.signature_end_date.present?
32
+ }
24
33
 
25
34
  def map_model(model)
26
35
  self.type_id = model.type.id
@@ -32,15 +41,61 @@ module Decidim
32
41
  end
33
42
 
34
43
  def scope_id
44
+ return nil if initiative_type.only_global_scope_enabled?
45
+
35
46
  super.presence
36
47
  end
37
48
 
49
+ def area
50
+ @area ||= current_organization.areas.find_by(id: area_id)
51
+ end
52
+
53
+ def initiative_type
54
+ @initiative_type ||= InitiativesType.find(type_id)
55
+ end
56
+
57
+ def available_scopes
58
+ @available_scopes ||= if initiative_type.only_global_scope_enabled?
59
+ initiative_type.scopes.where(scope: nil)
60
+ else
61
+ initiative_type.scopes
62
+ end
63
+ end
64
+
65
+ def scope
66
+ @scope ||= Scope.find(scope_id) if scope_id.present?
67
+ end
68
+
38
69
  private
39
70
 
40
71
  def scope_exists
41
72
  return if scope_id.blank?
42
73
 
43
- errors.add(:scope_id, :invalid) unless InitiativesTypeScope.where(decidim_initiatives_types_id: type_id, decidim_scopes_id: scope_id).exists?
74
+ errors.add(:scope_id, :invalid) unless InitiativesTypeScope.where(type: initiative_type, scope: scope).exists?
75
+ end
76
+
77
+ # This method will add an error to the `attachment` field only if there's
78
+ # any error in any other field. This is needed because when the form has
79
+ # an error, the attachment is lost, so we need a way to inform the user of
80
+ # this problem.
81
+ def notify_missing_attachment_if_errored
82
+ return if attachment.blank?
83
+
84
+ errors.add(:attachment, :needs_to_be_reattached) if errors.any?
85
+ end
86
+
87
+ def trigger_attachment_errors
88
+ return if attachment.blank?
89
+ return if attachment.valid?
90
+
91
+ attachment.errors.each { |error| errors.add(:attachment, error) }
92
+
93
+ attachment = Attachment.new(
94
+ attached_to: attachment.try(:attached_to),
95
+ file: attachment.try(:file)
96
+ )
97
+
98
+ errors.add(:attachment, :file) if !attachment.save && attachment.errors.has_key?(:file)
44
99
  end
45
100
  end
46
101
  end
@@ -19,110 +19,163 @@ module Decidim
19
19
  attribute :encrypted_metadata, String
20
20
  attribute :hash_id, String
21
21
 
22
- attribute :initiative_id, Integer
23
- attribute :author_id, Integer
24
- attribute :group_id, Integer
22
+ attribute :initiative, Decidim::Initiative
23
+ attribute :signer, Decidim::User
25
24
 
26
- validates :name_and_surname, :document_number, :date_of_birth, :postal_code, presence: true, if: :required_personal_data?
27
- validates :encrypted_metadata, presence: true, if: :required_personal_data?
28
- validates :initiative_id, presence: true
29
- validates :author_id, presence: true
25
+ validates :initiative, :signer, presence: true
30
26
 
31
- validate :document_number_authorized, if: :required_personal_data?
32
- validate :document_number_uniqueness, if: :required_personal_data?
33
- validate :personal_data_consistent_with_metadata, if: :required_personal_data?
34
-
35
- def initiative
36
- @initiative ||= Decidim::Initiative.find_by(id: initiative_id)
27
+ with_options if: :required_personal_data? do
28
+ validates :name_and_surname, :document_number, :date_of_birth, :postal_code, :encrypted_metadata, :hash_id, presence: true
29
+ validate :document_number_authorized?
30
+ validate :already_voted?
37
31
  end
38
32
 
39
33
  delegate :scope, to: :initiative
40
34
 
41
- def metadata
42
- { name_and_surname: name_and_surname,
43
- document_number: document_number,
44
- date_of_birth: date_of_birth,
45
- postal_code: postal_code }
46
- end
47
-
48
35
  def encrypted_metadata
49
- @encrypted_metadata ||= encrypt_metadata
36
+ return unless required_personal_data?
37
+
38
+ @encrypted_metadata ||= encryptor.encrypt(metadata)
50
39
  end
51
40
 
41
+ # Public: The hash to uniquely identify an initiative vote. It uses the
42
+ # initiative scope as a default.
43
+ #
44
+ # Returns a String.
52
45
  def hash_id
53
- Digest::MD5.hexdigest(
54
- "#{initiative_id}-#{document_number || author_id}-#{Rails.application.secrets.secret_key_base}"
46
+ return unless initiative && (document_number || signer)
47
+
48
+ @hash_id ||= Digest::MD5.hexdigest(
49
+ [
50
+ initiative.id,
51
+ document_number || signer.id,
52
+ Rails.application.secrets.secret_key_base
53
+ ].compact.join("-")
55
54
  )
56
55
  end
57
56
 
58
- def decrypted_metadata
59
- return unless encrypted_metadata
57
+ # Public: Builds the list of scopes where the user is authorized to vote in. This is used when
58
+ # the initiative allows also voting on child scopes, not only the main scope.
59
+ #
60
+ # Instead of just listing the children of the main scope, we just want to select the ones that
61
+ # have been added to the InitiativeType with its voting settings.
62
+ #
63
+ def authorized_scopes
64
+ initiative.votable_initiative_type_scopes.select do |initiative_type_scope|
65
+ initiative_type_scope.global_scope? ||
66
+ initiative_type_scope.scope == user_authorized_scope ||
67
+ initiative_type_scope.scope.ancestor_of?(user_authorized_scope)
68
+ end.flat_map(&:scope)
69
+ end
70
+
71
+ # Public: Finds the scope the user has an authorization for, this way the user can vote
72
+ # on that scope and its parents.
73
+ #
74
+ # This is can be used to allow users that are authorized with a children
75
+ # scope to sign an initiative with a parent scope.
76
+ #
77
+ # As an example: A city (global scope) has many districts (scopes with
78
+ # parent nil), and each district has different neighbourhoods (with its
79
+ # parent as a district). If we setup the authorization handler to match
80
+ # a neighbourhood, the same authorization can be used to participate
81
+ # in district, neighbourhoods or city initiatives.
82
+ #
83
+ # Returns a Decidim::Scope.
84
+ def user_authorized_scope
85
+ return scope if handler_name.blank?
86
+ return unless authorized?
87
+
88
+ @user_authorized_scope ||= authorized_scope_candidates.find do |scope|
89
+ scope&.id == authorization.metadata.symbolize_keys[:scope_id]
90
+ end
91
+ end
92
+
93
+ # Public: Builds a list of Decidim::Scopes where the user could have a
94
+ # valid authorization.
95
+ #
96
+ # If the intiative is set with a global scope (meaning the scope is nil),
97
+ # all the scopes in the organizaton are valid.
98
+ #
99
+ # Returns an array of Decidim::Scopes.
100
+ def authorized_scope_candidates
101
+ authorized_scope_candidates = [initiative.scope]
102
+ authorized_scope_candidates += if initiative.scope.present?
103
+ initiative.scope.descendants
104
+ else
105
+ initiative.organization.scopes
106
+ end
107
+ authorized_scope_candidates.uniq
108
+ end
60
109
 
61
- encryptor.decrypt(encrypted_metadata)
110
+ def metadata
111
+ {
112
+ name_and_surname: name_and_surname,
113
+ document_number: document_number,
114
+ date_of_birth: date_of_birth,
115
+ postal_code: postal_code
116
+ }
62
117
  end
63
118
 
64
119
  protected
65
120
 
121
+ # Private: Whether the personal data given when signing the initiative should
122
+ # be stored together with the vote or not.
123
+ #
124
+ # Returns a Boolean.
66
125
  def required_personal_data?
67
- initiative_type&.collect_user_extra_fields?
126
+ @required_personal_data ||= initiative&.type&.collect_user_extra_fields?
68
127
  end
69
128
 
70
- def initiative_type
71
- @initiative_type ||= initiative&.scoped_type&.type
72
- end
73
-
74
- def encryptor
75
- @encryptor ||= DataEncryptor.new(secret: "personal user metadata")
76
- end
77
-
78
- def encrypt_metadata
79
- return unless required_personal_data?
80
-
81
- encryptor.encrypt(metadata)
82
- end
83
-
84
- def document_number_authorized
129
+ # Private: Checks that the unique hash computed from the authorization
130
+ # and the user provided data match.
131
+ #
132
+ # This prevents users that know partial data from another user to sign
133
+ # initiatives with someone elses identity.
134
+ def document_number_authorized?
85
135
  return if initiative.document_number_authorization_handler.blank?
86
136
 
87
137
  errors.add(:document_number, :invalid) unless authorized? && authorization_handler && authorization.unique_id == authorization_handler.unique_id
88
138
  end
89
139
 
90
- def document_number_uniqueness
91
- errors.add(:document_number, :taken) if initiative.votes.where(hash_id: hash_id).exists?
92
- end
93
-
94
- def personal_data_consistent_with_metadata
95
- return if initiative.document_number_authorization_handler.blank?
96
-
97
- errors.add(:base, :invalid) unless authorized? &&
98
- authorization_handler &&
99
- authorization_handler_metadata_variations.any? { |variation| authorization.metadata.symbolize_keys == variation.symbolize_keys }
140
+ # Private: Checks if there's any existing vote that matches the user's data.
141
+ def already_voted?
142
+ errors.add(:document_number, :taken) if initiative.votes.where(hash_id: hash_id, scope: scope).exists?
100
143
  end
101
144
 
102
145
  def author
103
146
  @author ||= current_organization.users.find_by(id: author_id)
104
147
  end
105
148
 
149
+ # Private: Finds an authorization for the user signing the initiative and
150
+ # the configured handler.
106
151
  def authorization
107
- return unless author && handler_name
108
-
109
- @authorization ||= Verifications::Authorizations.new(organization: author.organization, user: author, name: handler_name).first
110
- end
111
-
112
- def authorization_status
113
- return unless authorization
152
+ return unless signer && handler_name
114
153
 
115
- Decidim::Verifications::Adapter.from_element(handler_name).authorize(authorization, {}, nil, nil)
154
+ @authorization ||= Verifications::Authorizations.new(
155
+ organization: signer.organization,
156
+ user: signer,
157
+ name: handler_name
158
+ ).first
116
159
  end
117
160
 
161
+ # Private: Checks if the authorization hasn't expired or is invalid.
118
162
  def authorized?
119
163
  authorization_status&.first == :ok
120
164
  end
121
165
 
122
- def handler_name
123
- initiative.document_number_authorization_handler
124
- end
125
-
166
+ # Private: Builds an authorization handler with the data the user provided
167
+ # when signing the initiative.
168
+ #
169
+ # This is currently tied to authorization handlers that have, at least, these attributes:
170
+ # * document_number
171
+ # * name_and_surname
172
+ # * date_of_birth
173
+ # * postal_code
174
+ #
175
+ # Once we have the authorization handler we can use is to compute the
176
+ # unique_id and compare it to an existing authorization.
177
+ #
178
+ # Returns a Decidim::AuthorizationHandler.
126
179
  def authorization_handler
127
180
  return unless document_number && handler_name
128
181
 
@@ -130,21 +183,25 @@ module Decidim
130
183
  document_number: document_number,
131
184
  name_and_surname: name_and_surname,
132
185
  date_of_birth: date_of_birth,
133
- postal_code: postal_code,
134
- scope_id: scope&.id)
186
+ postal_code: postal_code)
135
187
  end
136
188
 
137
- def authorization_handler_metadata_variations
138
- return [] unless authorization_handler && scope.present?
189
+ # Private: The AuthorizationHandler name used to verify the user's
190
+ # document number.
191
+ #
192
+ # Returns a String.
193
+ def handler_name
194
+ initiative.document_number_authorization_handler
195
+ end
196
+
197
+ def authorization_status
198
+ return unless authorization
139
199
 
140
- scope.children.map do |child_scope|
141
- Decidim::AuthorizationHandler.handler_for(handler_name,
142
- document_number: document_number,
143
- name_and_surname: name_and_surname,
144
- date_of_birth: date_of_birth,
145
- postal_code: postal_code,
146
- scope_id: child_scope&.id)
147
- end.unshift(authorization_handler).map(&:metadata)
200
+ Decidim::Verifications::Adapter.from_element(handler_name).authorize(authorization, {}, nil, nil)
201
+ end
202
+
203
+ def encryptor
204
+ @encryptor ||= DataEncryptor.new(secret: "personal user metadata")
148
205
  end
149
206
  end
150
207
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Initiatives
5
+ # Custom helpers, scoped to the initiatives engine.
6
+ #
7
+ module ApplicationHelper
8
+ include Decidim::CheckBoxesTreeHelper
9
+
10
+ def filter_states_values
11
+ TreeNode.new(
12
+ TreePoint.new("", t("decidim.initiatives.application_helper.filter_state_values.all")),
13
+ [
14
+ TreePoint.new("open", t("decidim.initiatives.application_helper.filter_state_values.open")),
15
+ TreeNode.new(
16
+ TreePoint.new("closed", t("decidim.initiatives.application_helper.filter_state_values.closed")),
17
+ [
18
+ TreePoint.new("accepted", t("decidim.initiatives.application_helper.filter_state_values.accepted")),
19
+ TreePoint.new("rejected", t("decidim.initiatives.application_helper.filter_state_values.rejected"))
20
+ ]
21
+ ),
22
+ TreePoint.new("answered", t("decidim.initiatives.application_helper.filter_state_values.answered"))
23
+ ]
24
+ )
25
+ end
26
+
27
+ def filter_scopes_values
28
+ main_scopes = current_organization.scopes.top_level
29
+
30
+ scopes_values = main_scopes.includes(:scope_type, :children).flat_map do |scope|
31
+ TreeNode.new(
32
+ TreePoint.new(scope.id.to_s, translated_attribute(scope.name, current_organization)),
33
+ scope_children_to_tree(scope)
34
+ )
35
+ end
36
+
37
+ scopes_values.prepend(TreePoint.new("global", t("decidim.scopes.global")))
38
+
39
+ TreeNode.new(
40
+ TreePoint.new("", t("decidim.initiatives.application_helper.filter_scope_values.all")),
41
+ scopes_values
42
+ )
43
+ end
44
+
45
+ def scope_children_to_tree(scope)
46
+ return unless scope.children.any?
47
+
48
+ scope.children.includes(:scope_type, :children).flat_map do |child|
49
+ TreeNode.new(
50
+ TreePoint.new(child.id.to_s, translated_attribute(child.name, current_organization)),
51
+ scope_children_to_tree(child)
52
+ )
53
+ end
54
+ end
55
+
56
+ def filter_types_values
57
+ types_values = Decidim::InitiativesType.where(organization: current_organization).map do |type|
58
+ TreeNode.new(
59
+ TreePoint.new(type.id.to_s, type.title[I18n.locale.to_s])
60
+ )
61
+ end
62
+
63
+ TreeNode.new(
64
+ TreePoint.new("", t("decidim.initiatives.application_helper.filter_type_values.all")),
65
+ types_values
66
+ )
67
+ end
68
+
69
+ def filter_areas_values
70
+ areas_or_types = areas_for_select(current_organization)
71
+
72
+ areas_values = if areas_or_types.first.is_a?(Decidim::Area)
73
+ filter_areas(areas_or_types)
74
+ else
75
+ filter_areas_and_types(areas_or_types)
76
+ end
77
+
78
+ TreeNode.new(
79
+ TreePoint.new("", t("decidim.initiatives.application_helper.filter_area_values.all")),
80
+ areas_values
81
+ )
82
+ end
83
+
84
+ def filter_areas(areas)
85
+ areas.map do |area|
86
+ TreeNode.new(
87
+ TreePoint.new(area.id.to_s, area.name[I18n.locale.to_s])
88
+ )
89
+ end
90
+ end
91
+
92
+ def filter_areas_and_types(area_types)
93
+ area_types.map do |area_type|
94
+ TreeNode.new(
95
+ TreePoint.new(area_type.area_ids.join("_"), area_type.name[I18n.locale.to_s]),
96
+ area_type.areas.map do |area|
97
+ TreePoint.new(area.id.to_s, area.name[I18n.locale.to_s])
98
+ end
99
+ )
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end