decidim-action_delegator 0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +225 -0
  4. data/Rakefile +41 -0
  5. data/app/assets/config/admin/decidim_action_delegator_manifest.css +0 -0
  6. data/app/assets/config/admin/decidim_action_delegator_manifest.js +1 -0
  7. data/app/assets/config/decidim_action_delegator_manifest.css +3 -0
  8. data/app/assets/config/decidim_action_delegator_manifest.js +2 -0
  9. data/app/assets/images/decidim/action_delegator/icon.svg +1 -0
  10. data/app/assets/javascripts/decidim/action_delegator/admin/action_delegator.js.es6 +3 -0
  11. data/app/assets/javascripts/decidim/action_delegator/questions.js.es6 +26 -0
  12. data/app/assets/stylesheets/decidim/action_delegator/questions.scss +25 -0
  13. data/app/commands/decidim/action_delegator/admin/create_delegation.rb +62 -0
  14. data/app/commands/decidim/action_delegator/consultations/multiple_vote_question_override.rb +31 -0
  15. data/app/commands/decidim/action_delegator/consultations/vote_question_override.rb +36 -0
  16. data/app/commands/decidim/action_delegator/vote_delegation.rb +28 -0
  17. data/app/controllers/concerns/decidim/action_delegator/admin/filterable.rb +23 -0
  18. data/app/controllers/decidim/action_delegator/admin/application_controller.rb +19 -0
  19. data/app/controllers/decidim/action_delegator/admin/consultations/exports_controller.rb +27 -0
  20. data/app/controllers/decidim/action_delegator/admin/consultations_controller.rb +33 -0
  21. data/app/controllers/decidim/action_delegator/admin/delegations_controller.rb +79 -0
  22. data/app/controllers/decidim/action_delegator/admin/exports/sum_of_weights_controller.rb +15 -0
  23. data/app/controllers/decidim/action_delegator/admin/results/sum_of_weights_controller.rb +37 -0
  24. data/app/controllers/decidim/action_delegator/admin/settings_controller.rb +71 -0
  25. data/app/controllers/decidim/action_delegator/application_controller.rb +14 -0
  26. data/app/controllers/decidim/action_delegator/consultations/question_multiple_votes_controller_override.rb +28 -0
  27. data/app/controllers/decidim/action_delegator/consultations/question_votes_controller_override.rb +55 -0
  28. data/app/controllers/decidim/action_delegator/user_delegations_controller.rb +22 -0
  29. data/app/controllers/decidim/action_delegator/verifications/sms/authorizations_controller_override.rb +38 -0
  30. data/app/forms/decidim/action_delegator/admin/delegation_form.rb +25 -0
  31. data/app/forms/decidim/action_delegator/consultations/vote_form_override.rb +15 -0
  32. data/app/helpers/decidim/action_delegator/admin/delegation_helper.rb +27 -0
  33. data/app/helpers/decidim/action_delegator/application_helper.rb +10 -0
  34. data/app/jobs/decidim/action_delegator/export_consultation_results_job.rb +51 -0
  35. data/app/jobs/decidim/action_delegator/send_sms_job.rb +60 -0
  36. data/app/jobs/decidim/action_delegator/twilio_send_sms_job.rb +43 -0
  37. data/app/models/decidim/action_delegator/application_record.rb +10 -0
  38. data/app/models/decidim/action_delegator/consultations/vote_override.rb +15 -0
  39. data/app/models/decidim/action_delegator/delegation.rb +26 -0
  40. data/app/models/decidim/action_delegator/setting.rb +22 -0
  41. data/app/models/decidim/action_delegator/unversioned_vote.rb +19 -0
  42. data/app/models/decidim/action_delegator/whodunnit_vote.rb +28 -0
  43. data/app/permissions/decidim/action_delegator/consultations_permissions_extension.rb +27 -0
  44. data/app/permissions/decidim/action_delegator/permissions.rb +74 -0
  45. data/app/presenters/decidim/action_delegator/admin/consultation_presenter.rb +15 -0
  46. data/app/presenters/decidim/action_delegator/admin/setting_presenter.rb +13 -0
  47. data/app/presenters/decidim/action_delegator/question_with_totals.rb +24 -0
  48. data/app/queries/decidim/action_delegator/consultation_delegations.rb +25 -0
  49. data/app/queries/decidim/action_delegator/decrypted_authorizations.rb +112 -0
  50. data/app/queries/decidim/action_delegator/delegated_votes_versions.rb +31 -0
  51. data/app/queries/decidim/action_delegator/delegates_votes_by_consultation.rb +24 -0
  52. data/app/queries/decidim/action_delegator/delegates_votes_by_question.rb +26 -0
  53. data/app/queries/decidim/action_delegator/delegation_votes.rb +30 -0
  54. data/app/queries/decidim/action_delegator/grantee_delegations.rb +24 -0
  55. data/app/queries/decidim/action_delegator/json_build_object_query.rb +45 -0
  56. data/app/queries/decidim/action_delegator/organization_delegations.rb +26 -0
  57. data/app/queries/decidim/action_delegator/organization_settings.rb +25 -0
  58. data/app/queries/decidim/action_delegator/published_responses.rb +25 -0
  59. data/app/queries/decidim/action_delegator/responses_by_membership.rb +75 -0
  60. data/app/queries/decidim/action_delegator/scrutiny.rb +87 -0
  61. data/app/queries/decidim/action_delegator/setting_delegations.rb +19 -0
  62. data/app/queries/decidim/action_delegator/sum_of_membership_weight.rb +48 -0
  63. data/app/queries/decidim/action_delegator/sum_of_weights.rb +25 -0
  64. data/app/queries/decidim/action_delegator/type_and_weight.rb +26 -0
  65. data/app/queries/decidim/action_delegator/voted_with_direct_verification.rb +53 -0
  66. data/app/queries/decidim/action_delegator/votes_count_aggregation.rb +34 -0
  67. data/app/serializers/decidim/action_delegator/consultation_results_serializer.rb +19 -0
  68. data/app/serializers/decidim/action_delegator/sum_of_weights_serializer.rb +17 -0
  69. data/app/services/decidim/action_delegator/sms_gateway.rb +51 -0
  70. data/app/views/decidim/action_delegator/_callout.html.erb +5 -0
  71. data/app/views/decidim/action_delegator/_delegations_modal.html.erb +93 -0
  72. data/app/views/decidim/action_delegator/_link_to_delegations.html.erb +5 -0
  73. data/app/views/decidim/action_delegator/_link_to_question.html.erb +5 -0
  74. data/app/views/decidim/action_delegator/admin/consultations/results.html.erb +62 -0
  75. data/app/views/decidim/action_delegator/admin/delegations/index.html.erb +36 -0
  76. data/app/views/decidim/action_delegator/admin/delegations/new.html.erb +30 -0
  77. data/app/views/decidim/action_delegator/admin/results/sum_of_weights/index.html.erb +63 -0
  78. data/app/views/decidim/action_delegator/admin/settings/index.html.erb +39 -0
  79. data/app/views/decidim/action_delegator/admin/settings/new.html.erb +30 -0
  80. data/app/views/decidim/action_delegator/user_delegations/index.html.erb +19 -0
  81. data/app/views/decidim/consultations/consultations/_question.html.erb +41 -0
  82. data/app/views/decidim/consultations/consultations/show.html.erb +14 -0
  83. data/app/views/decidim/consultations/question_multiple_votes/_form.html.erb +25 -0
  84. data/app/views/decidim/consultations/question_votes/update_vote_button.js.erb +32 -0
  85. data/app/views/decidim/consultations/questions/_vote_button.html.erb +107 -0
  86. data/app/views/decidim/consultations/questions/_vote_modal.html.erb +30 -0
  87. data/app/views/decidim/consultations/questions/_vote_modal_confirm.html.erb +38 -0
  88. data/app/views/decidim/verifications/sms/authorizations/new.html.erb +33 -0
  89. data/app/views/layouts/decidim/action_delegator/admin/_users_sidebar.html.erb +56 -0
  90. data/app/views/layouts/decidim/action_delegator/admin/delegations.html.erb +13 -0
  91. data/app/views/layouts/decidim/admin/consultation.html.erb +56 -0
  92. data/app/views/layouts/decidim/admin/question.html.erb +98 -0
  93. data/app/views/layouts/decidim/admin/users.html.erb +7 -0
  94. data/config/i18n-tasks.yml +10 -0
  95. data/config/locales/ca.yml +101 -0
  96. data/config/locales/cs.yml +101 -0
  97. data/config/locales/en.yml +109 -0
  98. data/config/locales/es.yml +101 -0
  99. data/db/migrate/20200729194540_create_decidim_action_delegator_delegations.rb +12 -0
  100. data/db/migrate/20200824113801_create_settings.rb +13 -0
  101. data/db/migrate/20200828113755_add_setting_id_to_delegations.rb +11 -0
  102. data/db/migrate/20200831141540_make_granter_id_and_grantee_id_non_nullable.rb +8 -0
  103. data/db/migrate/20201001172345_remove_expires_at_from_delegations.rb +7 -0
  104. data/db/migrate/20201005203554_add_setting_granter_unique_index_to_delegations.rb +10 -0
  105. data/db/migrate/20201006084522_remove_setting_id_index_from_delegations.rb +7 -0
  106. data/db/migrate/20201030164808_add_delegation_id_to_versions.rb +8 -0
  107. data/db/seeds.rb +7 -0
  108. data/lib/decidim/action_delegator/admin.rb +10 -0
  109. data/lib/decidim/action_delegator/admin_engine.rb +42 -0
  110. data/lib/decidim/action_delegator/engine.rb +53 -0
  111. data/lib/decidim/action_delegator/test/factories.rb +25 -0
  112. data/lib/decidim/action_delegator/version.rb +10 -0
  113. data/lib/decidim/action_delegator.rb +29 -0
  114. data/lib/json_key.rb +10 -0
  115. metadata +267 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class Permissions < Decidim::DefaultPermissions
6
+ SUBJECTS_WHITELIST = [:delegation, :setting, :consultation].freeze
7
+
8
+ def permissions
9
+ allowed_delegation_action?
10
+
11
+ return permission_action unless user.admin?
12
+ return permission_action unless permission_action.scope == :admin
13
+ return permission_action unless action_delegator_subject?
14
+
15
+ allow! if can_perform_action?
16
+
17
+ permission_action
18
+ end
19
+
20
+ private
21
+
22
+ def allowed_delegation_action?
23
+ return unless delegation
24
+ # Check that the required question verifications are fulfilled
25
+ return unless authorized?(:vote, delegation.grantee)
26
+
27
+ case permission_action.action
28
+ when :vote_delegation
29
+ toggle_allow(question.can_be_voted_by?(delegation.granter) && delegation.grantee == user)
30
+ when :unvote_delegation
31
+ toggle_allow(question.can_be_unvoted_by?(delegation.granter) && delegation.grantee == user)
32
+ end
33
+ end
34
+
35
+ def authorized?(permission_action, user, resource: nil)
36
+ return unless resource || question
37
+
38
+ ActionAuthorizer.new(user, permission_action, question, resource).authorize.ok?
39
+ end
40
+
41
+ def question
42
+ @question ||= context.fetch(:question, nil)
43
+ end
44
+
45
+ def delegation
46
+ @delegation ||= context.fetch(:delegation, nil)
47
+ end
48
+
49
+ def consultation_results_exports_action?
50
+ permission_action.subject == :consultation && permission_action.action == :export_results
51
+ end
52
+
53
+ def consultation
54
+ @consultation ||= context.fetch(:consultation)
55
+ end
56
+
57
+ def action_delegator_subject?
58
+ SUBJECTS_WHITELIST.include?(permission_action.subject)
59
+ end
60
+
61
+ def can_perform_action?
62
+ if permission_action.action == :destroy
63
+ resource.present?
64
+ else
65
+ true
66
+ end
67
+ end
68
+
69
+ def resource
70
+ @resource ||= context.fetch(:resource, nil)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ module Admin
6
+ class ConsultationPresenter < SimpleDelegator
7
+ include Decidim::TranslationsHelper
8
+
9
+ def translated_title
10
+ @translated_title ||= translated_attribute(title)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ module Admin
6
+ class SettingPresenter < SimpleDelegator
7
+ def consultation
8
+ Admin::ConsultationPresenter.new(__getobj__.consultation)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class QuestionWithTotals < SimpleDelegator
6
+ def initialize(question, questions_by_id)
7
+ super(question)
8
+ @questions_by_id = questions_by_id
9
+ end
10
+
11
+ def total_delegates
12
+ questions_by_id[id].total_delegates
13
+ end
14
+
15
+ def total_participants
16
+ questions_by_id[id].total_participants
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :questions_by_id
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class ConsultationDelegations < Rectify::Query
6
+ def self.for(consultation)
7
+ new(consultation).query
8
+ end
9
+
10
+ def initialize(consultation)
11
+ @consultation = consultation
12
+ end
13
+
14
+ def query
15
+ Delegation
16
+ .joins(setting: :consultation)
17
+ .where(decidim_consultations: { id: consultation.id })
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :consultation
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json_key"
4
+
5
+ module Decidim
6
+ module ActionDelegator
7
+ class DecryptedAuthorizations < Rectify::Query
8
+ METADATA_FIELDS = %w(
9
+ membership_type
10
+ membership_weight
11
+ ).freeze
12
+
13
+ def initialize(relation)
14
+ @relation = relation
15
+ end
16
+
17
+ def query
18
+ authorizations
19
+ .project(
20
+ authorizations[:id],
21
+ authorizations[:name],
22
+ authorizations[:decidim_user_id],
23
+ *decrypted_authorizations_metadata_fields
24
+ )
25
+ .where(authorizations[:id].in(authorization_ids))
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :relation
31
+
32
+ def authorizations
33
+ Decidim::Authorization.arel_table
34
+ end
35
+
36
+ def decrypted_authorizations_metadata_fields
37
+ METADATA_FIELDS.map do |field|
38
+ decrypted_authorizations_metadata_field(field)
39
+ end
40
+ end
41
+
42
+ def decrypted_authorizations_metadata_field(field)
43
+ JsonBuildObjectQuery.new(
44
+ metadata_field_by_authorization_id(field).flatten,
45
+ authorizations[:id],
46
+ field
47
+ ).to_sql
48
+ end
49
+
50
+ def metadata_field_by_authorization_id(field)
51
+ decrypted_authorizations.map do |hash|
52
+ [
53
+ hash.fetch("id"),
54
+ hash.fetch(field)
55
+ ]
56
+ end
57
+ end
58
+
59
+ def decrypted_authorizations
60
+ @decrypted_authorizations ||=
61
+ sql_query_results(encrypted_authorizations).map do |hash|
62
+ hash.transform_values! do |value|
63
+ if value.nil?
64
+ Arel.sql("NULL")
65
+ else
66
+ value.is_a?(String) ? decrypt_and_parse(value) : value
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def sql_query_results(sql)
73
+ ActiveRecord::Base.connection.exec_query(sql).to_a
74
+ end
75
+
76
+ def encrypted_authorizations
77
+ Decidim::Authorization
78
+ .select(
79
+ :id,
80
+ *authorizations_metadata_fields
81
+ )
82
+ .where(id: authorization_ids)
83
+ .to_sql
84
+ end
85
+
86
+ def authorizations_metadata_fields
87
+ METADATA_FIELDS.map do |field|
88
+ authorizations_metadata_field(field).as(field)
89
+ end
90
+ end
91
+
92
+ def authorizations_metadata_field(field)
93
+ JSONKey.new(authorizations[:metadata], field)
94
+ end
95
+
96
+ def authorization_ids
97
+ @authorization_ids ||= relation.pluck("decidim_authorizations.id").compact
98
+ end
99
+
100
+ def decrypt_and_parse(value)
101
+ Arel.sql("'#{JSON.parse(decrypt_value(value))}'")
102
+ end
103
+
104
+ def decrypt_value(value)
105
+ Decidim::AttributeEncryptor.decrypt(value)
106
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
107
+ # Support for legacy unencrypted values.
108
+ value
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ # Returns all PaperTrail versions of a consultation's delegated votes for auditing purposes.
6
+ # It is intended to be used to easily fetch this data when a judge ask us so.
7
+ class DelegatedVotesVersions
8
+ def initialize(consultation)
9
+ @consultation = consultation
10
+ end
11
+
12
+ def query
13
+ statement = <<-SQL.squish
14
+ SELECT *
15
+ FROM versions
16
+ INNER JOIN decidim_action_delegator_delegations
17
+ ON decidim_action_delegator_delegations.id = versions.decidim_action_delegator_delegation_id
18
+ INNER JOIN decidim_action_delegator_settings
19
+ ON decidim_action_delegator_settings.id = decidim_action_delegator_delegations.decidim_action_delegator_setting_id
20
+ WHERE decidim_action_delegator_settings.decidim_consultation_id = #{consultation.id}
21
+ SQL
22
+
23
+ ActiveRecord::Base.connection.execute(statement).to_a
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :consultation
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class DelegatesVotesByConsultation < Rectify::Query
6
+ def initialize(consultation, relation = DelegationVotes)
7
+ @consultation = consultation
8
+ @relation = relation
9
+ end
10
+
11
+ def query
12
+ relation.new.query.merge(consultation_delegations).count
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :consultation, :relation
18
+
19
+ def consultation_delegations
20
+ ConsultationDelegations.for(consultation)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class DelegatesVotesByQuestion < Rectify::Query
6
+ def initialize(question)
7
+ @question = question
8
+ end
9
+
10
+ def query
11
+ DelegationVotes.new.query
12
+ .merge(question.votes)
13
+ .merge(consultation_delegations)
14
+ .distinct.count(:granter_id)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :question
20
+
21
+ def consultation_delegations
22
+ ConsultationDelegations.for(question.consultation)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ # This query object replaces the ActiveRecord association we would have between the Vote and
6
+ # Delegation models. Unfortunately we can't use custom foreign keys on both ends of the
7
+ # association so this aims to replace `delegation.votes`.
8
+ class DelegationVotes < Rectify::Query
9
+ def query
10
+ Delegation.joins(
11
+ delegations.join(votes).on(vote_author_eq_granter).join_sources
12
+ )
13
+ end
14
+
15
+ private
16
+
17
+ def votes
18
+ Decidim::Consultations::Vote.arel_table
19
+ end
20
+
21
+ def delegations
22
+ Delegation.arel_table
23
+ end
24
+
25
+ def vote_author_eq_granter
26
+ votes[:decidim_author_id].eq(delegations[:granter_id])
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class GranteeDelegations
6
+ def self.for(consultation, user)
7
+ new(consultation, user).query
8
+ end
9
+
10
+ def initialize(consultation, user)
11
+ @consultation = consultation
12
+ @user = user
13
+ end
14
+
15
+ def query
16
+ ConsultationDelegations.for(consultation).where(grantee_id: user.id)
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :consultation, :user
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class JsonBuildObjectQuery
6
+ def initialize(json_args, query_field, aliaz)
7
+ @json_args = json_args
8
+ @query_field = query_field
9
+ @aliaz = aliaz
10
+ end
11
+
12
+ def to_sql
13
+ Arel::Nodes::InfixOperation.new(
14
+ "->>",
15
+ json_build_object(json_args),
16
+ cast(query_field, :text)
17
+ ).as(aliaz).to_sql
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :json_args, :query_field, :aliaz
23
+
24
+ # Returns the equivalent of `JSON_BUILD_OBJECT (ARRAY)` in Arel
25
+ def json_build_object(array)
26
+ Arel::Nodes::NamedFunction.new(
27
+ "JSON_BUILD_OBJECT",
28
+ [array]
29
+ )
30
+ end
31
+
32
+ # Returns the equivalent of `CAST ((<exprs>) AS <type>)` in Arel
33
+ def cast(*exprs, type)
34
+ Arel::Nodes::NamedFunction.new(
35
+ "CAST",
36
+ [Arel::Nodes::As.new(Arel::Nodes::Grouping.new(exprs), Arel.sql(type.to_s.upcase))]
37
+ )
38
+ end
39
+
40
+ def coalesce(*exprs)
41
+ Arel::Nodes::NamedFunction.new("COALESCE", exprs)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class OrganizationDelegations < Rectify::Query
6
+ def initialize(organization)
7
+ @organization = organization
8
+ end
9
+
10
+ def query
11
+ Delegation
12
+ .joins(setting: :consultation)
13
+ .merge(organization_consultations)
14
+ .includes(:grantee, :granter)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :organization
20
+
21
+ def organization_consultations
22
+ Decidim::Consultations::OrganizationConsultations.new(organization).query
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class OrganizationSettings < Rectify::Query
6
+ def initialize(organization)
7
+ @organization = organization
8
+ end
9
+
10
+ def query
11
+ Setting
12
+ .joins(:consultation)
13
+ .merge(organization_consultations)
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :organization
19
+
20
+ def organization_consultations
21
+ Decidim::Consultations::OrganizationConsultations.new(organization).query
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class PublishedResponses < Rectify::Query
6
+ def initialize(consultation)
7
+ @consultation = consultation
8
+ end
9
+
10
+ # The Question's default_scope, `order(order: :asc)`, messes up the ordering in our queries so
11
+ # we have to explicitly remove the ORDER BY close using `#reorder`.
12
+ def query
13
+ Decidim::Consultations::Response
14
+ .joins(question: :consultation)
15
+ .merge(Consultation.finished)
16
+ .where(decidim_consultations_questions: { decidim_consultation_id: consultation.id })
17
+ .where.not(decidim_consultations_questions: { published_at: nil })
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :consultation
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ # Returns total votes of each response by memberships' type and weight.
6
+ #
7
+ # This query completely relies on the schema of the `metadata` of the relevant
8
+ # `decidim_authorizations` records, which is expected to be like:
9
+ #
10
+ # "{ membership_type: '', membership_weight: '' }"
11
+ #
12
+ # Note that although we assume `membership_type` to be a string and `membership_weight` to be an
13
+ # integer, there are no implications in the code for their actual data types.
14
+ class ResponsesByMembership < Rectify::Query
15
+ DEFAULT_METADATA = I18n.t("decidim.admin.consultations.results.default_metadata")
16
+
17
+ def initialize(relation)
18
+ @relation = relation
19
+ end
20
+
21
+ def query
22
+ relation
23
+ .select(
24
+ responses[:decidim_consultations_questions_id],
25
+ responses[:title],
26
+ membership(:type),
27
+ membership(:weight),
28
+ votes_count
29
+ )
30
+ .group(
31
+ responses[:decidim_consultations_questions_id],
32
+ responses[:title],
33
+ sql(:membership_type),
34
+ sql(:membership_weight)
35
+ )
36
+ .order(:title, :membership_type, { membership_weight: :desc }, "votes_count DESC")
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :relation
42
+
43
+ def membership(field)
44
+ full_field = "membership_#{field}"
45
+ coalesce(sql(full_field), default_metadata).as(full_field)
46
+ end
47
+
48
+ def memberships
49
+ Arel::Table.new(:memberships)
50
+ end
51
+
52
+ def default_metadata
53
+ sql("'#{DEFAULT_METADATA}'")
54
+ end
55
+
56
+ def votes_count
57
+ sql("COUNT(*)").as(sql(:votes_count))
58
+ end
59
+
60
+ def responses
61
+ Decidim::Consultations::Response.arel_table
62
+ end
63
+
64
+ def sql(name)
65
+ Arel.sql(name.to_s)
66
+ end
67
+
68
+ # This method comes with Rails 6. See:
69
+ # https://github.com/rails/rails/commit/e5190acacd1088211cfe6f128b782af216aa6570
70
+ def coalesce(*exprs)
71
+ Arel::Nodes::NamedFunction.new("COALESCE", exprs)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ActionDelegator
5
+ class QuestionStats
6
+ attr_reader :total_delegates, :total_participants
7
+
8
+ def initialize(total_delegates, total_participants)
9
+ @total_delegates = total_delegates
10
+ @total_participants = total_participants
11
+ end
12
+ end
13
+
14
+ class Scrutiny
15
+ def initialize(consultation)
16
+ @consultation = consultation
17
+ @question_votes_by_id = questions_query.group_by(&:id)
18
+ end
19
+
20
+ def questions
21
+ questions_cache = build_questions_cache
22
+
23
+ question_votes_by_id.map do |_id, question_votes|
24
+ # They are all the same question so we can pick any
25
+ question = question_votes.first
26
+ QuestionWithTotals.new(question, questions_cache)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :consultation, :questions_cache, :question_votes_by_id
33
+
34
+ # Returns a hash where the key is the question id and the value is an object that wraps some
35
+ # question's counts displayed in the view.
36
+ def build_questions_cache
37
+ question_votes_by_id.each_with_object({}) do |(id, question_votes), memo|
38
+ total_delegations = delegations_count(question_votes)
39
+ total_participants = participants_count(question_votes)
40
+
41
+ memo[id] = QuestionStats.new(total_delegations, total_participants)
42
+ end
43
+ end
44
+
45
+ # Computes the count of delegated votes out of rows returned by `#questions_query`, named
46
+ # `question_votes`.
47
+ def delegations_count(question_votes)
48
+ question_votes.count { |question| question.granter_id.present? }
49
+ end
50
+
51
+ # Computes the count of participants out of rows returned by `#questions_query`, named
52
+ # `question_votes`.
53
+ def participants_count(question_votes)
54
+ question_votes.map(&:decidim_author_id).uniq.compact.size
55
+ end
56
+
57
+ # Returns questions joined with their votes and delegations so that we can aggregate the data
58
+ # in Ruby in different ways but reaching out to DB just once.
59
+ def questions_query
60
+ @questions_query ||= Decidim::Consultations::Question
61
+ .includes(:responses)
62
+ .select(
63
+ '"decidim_consultations_questions".*',
64
+ '"decidim_consultations_votes"."decidim_author_id"',
65
+ '"decidim_action_delegator_delegations"."granter_id"'
66
+ )
67
+ .from(questions_joined_votes_and_delegations)
68
+ .where(decidim_consultation_id: consultation.id)
69
+ .merge(Decidim::Consultations::Question.published)
70
+ end
71
+
72
+ def questions_joined_votes_and_delegations
73
+ <<-SQL.squish
74
+ "decidim_consultations_questions"
75
+ LEFT OUTER JOIN "decidim_consultations_votes"
76
+ ON "decidim_consultations_votes"."decidim_consultation_question_id" = "decidim_consultations_questions"."id"
77
+ LEFT JOIN "decidim_action_delegator_delegations"
78
+ ON "decidim_consultations_votes"."decidim_author_id" = "decidim_action_delegator_delegations"."granter_id"
79
+ LEFT JOIN "decidim_action_delegator_settings"
80
+ ON "decidim_action_delegator_settings"."id" = "decidim_action_delegator_delegations"."decidim_action_delegator_setting_id"
81
+ LEFT JOIN "decidim_consultations"
82
+ ON "decidim_consultations"."id" = "decidim_action_delegator_settings"."decidim_consultation_id"
83
+ SQL
84
+ end
85
+ end
86
+ end
87
+ end