decidim-action_delegator 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE-AGPLv3.txt +661 -0
- data/README.md +225 -0
- data/Rakefile +41 -0
- data/app/assets/config/admin/decidim_action_delegator_manifest.css +0 -0
- data/app/assets/config/admin/decidim_action_delegator_manifest.js +1 -0
- data/app/assets/config/decidim_action_delegator_manifest.css +3 -0
- data/app/assets/config/decidim_action_delegator_manifest.js +2 -0
- data/app/assets/images/decidim/action_delegator/icon.svg +1 -0
- data/app/assets/javascripts/decidim/action_delegator/admin/action_delegator.js.es6 +3 -0
- data/app/assets/javascripts/decidim/action_delegator/questions.js.es6 +26 -0
- data/app/assets/stylesheets/decidim/action_delegator/questions.scss +25 -0
- data/app/commands/decidim/action_delegator/admin/create_delegation.rb +62 -0
- data/app/commands/decidim/action_delegator/consultations/multiple_vote_question_override.rb +31 -0
- data/app/commands/decidim/action_delegator/consultations/vote_question_override.rb +36 -0
- data/app/commands/decidim/action_delegator/vote_delegation.rb +28 -0
- data/app/controllers/concerns/decidim/action_delegator/admin/filterable.rb +23 -0
- data/app/controllers/decidim/action_delegator/admin/application_controller.rb +19 -0
- data/app/controllers/decidim/action_delegator/admin/consultations/exports_controller.rb +27 -0
- data/app/controllers/decidim/action_delegator/admin/consultations_controller.rb +33 -0
- data/app/controllers/decidim/action_delegator/admin/delegations_controller.rb +79 -0
- data/app/controllers/decidim/action_delegator/admin/exports/sum_of_weights_controller.rb +15 -0
- data/app/controllers/decidim/action_delegator/admin/results/sum_of_weights_controller.rb +37 -0
- data/app/controllers/decidim/action_delegator/admin/settings_controller.rb +71 -0
- data/app/controllers/decidim/action_delegator/application_controller.rb +14 -0
- data/app/controllers/decidim/action_delegator/consultations/question_multiple_votes_controller_override.rb +28 -0
- data/app/controllers/decidim/action_delegator/consultations/question_votes_controller_override.rb +55 -0
- data/app/controllers/decidim/action_delegator/user_delegations_controller.rb +22 -0
- data/app/controllers/decidim/action_delegator/verifications/sms/authorizations_controller_override.rb +38 -0
- data/app/forms/decidim/action_delegator/admin/delegation_form.rb +25 -0
- data/app/forms/decidim/action_delegator/consultations/vote_form_override.rb +15 -0
- data/app/helpers/decidim/action_delegator/admin/delegation_helper.rb +27 -0
- data/app/helpers/decidim/action_delegator/application_helper.rb +10 -0
- data/app/jobs/decidim/action_delegator/export_consultation_results_job.rb +51 -0
- data/app/jobs/decidim/action_delegator/send_sms_job.rb +60 -0
- data/app/jobs/decidim/action_delegator/twilio_send_sms_job.rb +43 -0
- data/app/models/decidim/action_delegator/application_record.rb +10 -0
- data/app/models/decidim/action_delegator/consultations/vote_override.rb +15 -0
- data/app/models/decidim/action_delegator/delegation.rb +26 -0
- data/app/models/decidim/action_delegator/setting.rb +22 -0
- data/app/models/decidim/action_delegator/unversioned_vote.rb +19 -0
- data/app/models/decidim/action_delegator/whodunnit_vote.rb +28 -0
- data/app/permissions/decidim/action_delegator/consultations_permissions_extension.rb +27 -0
- data/app/permissions/decidim/action_delegator/permissions.rb +74 -0
- data/app/presenters/decidim/action_delegator/admin/consultation_presenter.rb +15 -0
- data/app/presenters/decidim/action_delegator/admin/setting_presenter.rb +13 -0
- data/app/presenters/decidim/action_delegator/question_with_totals.rb +24 -0
- data/app/queries/decidim/action_delegator/consultation_delegations.rb +25 -0
- data/app/queries/decidim/action_delegator/decrypted_authorizations.rb +112 -0
- data/app/queries/decidim/action_delegator/delegated_votes_versions.rb +31 -0
- data/app/queries/decidim/action_delegator/delegates_votes_by_consultation.rb +24 -0
- data/app/queries/decidim/action_delegator/delegates_votes_by_question.rb +26 -0
- data/app/queries/decidim/action_delegator/delegation_votes.rb +30 -0
- data/app/queries/decidim/action_delegator/grantee_delegations.rb +24 -0
- data/app/queries/decidim/action_delegator/json_build_object_query.rb +45 -0
- data/app/queries/decidim/action_delegator/organization_delegations.rb +26 -0
- data/app/queries/decidim/action_delegator/organization_settings.rb +25 -0
- data/app/queries/decidim/action_delegator/published_responses.rb +25 -0
- data/app/queries/decidim/action_delegator/responses_by_membership.rb +75 -0
- data/app/queries/decidim/action_delegator/scrutiny.rb +87 -0
- data/app/queries/decidim/action_delegator/setting_delegations.rb +19 -0
- data/app/queries/decidim/action_delegator/sum_of_membership_weight.rb +48 -0
- data/app/queries/decidim/action_delegator/sum_of_weights.rb +25 -0
- data/app/queries/decidim/action_delegator/type_and_weight.rb +26 -0
- data/app/queries/decidim/action_delegator/voted_with_direct_verification.rb +53 -0
- data/app/queries/decidim/action_delegator/votes_count_aggregation.rb +34 -0
- data/app/serializers/decidim/action_delegator/consultation_results_serializer.rb +19 -0
- data/app/serializers/decidim/action_delegator/sum_of_weights_serializer.rb +17 -0
- data/app/services/decidim/action_delegator/sms_gateway.rb +51 -0
- data/app/views/decidim/action_delegator/_callout.html.erb +5 -0
- data/app/views/decidim/action_delegator/_delegations_modal.html.erb +93 -0
- data/app/views/decidim/action_delegator/_link_to_delegations.html.erb +5 -0
- data/app/views/decidim/action_delegator/_link_to_question.html.erb +5 -0
- data/app/views/decidim/action_delegator/admin/consultations/results.html.erb +62 -0
- data/app/views/decidim/action_delegator/admin/delegations/index.html.erb +36 -0
- data/app/views/decidim/action_delegator/admin/delegations/new.html.erb +30 -0
- data/app/views/decidim/action_delegator/admin/results/sum_of_weights/index.html.erb +63 -0
- data/app/views/decidim/action_delegator/admin/settings/index.html.erb +39 -0
- data/app/views/decidim/action_delegator/admin/settings/new.html.erb +30 -0
- data/app/views/decidim/action_delegator/user_delegations/index.html.erb +19 -0
- data/app/views/decidim/consultations/consultations/_question.html.erb +41 -0
- data/app/views/decidim/consultations/consultations/show.html.erb +14 -0
- data/app/views/decidim/consultations/question_multiple_votes/_form.html.erb +25 -0
- data/app/views/decidim/consultations/question_votes/update_vote_button.js.erb +32 -0
- data/app/views/decidim/consultations/questions/_vote_button.html.erb +107 -0
- data/app/views/decidim/consultations/questions/_vote_modal.html.erb +30 -0
- data/app/views/decidim/consultations/questions/_vote_modal_confirm.html.erb +38 -0
- data/app/views/decidim/verifications/sms/authorizations/new.html.erb +33 -0
- data/app/views/layouts/decidim/action_delegator/admin/_users_sidebar.html.erb +56 -0
- data/app/views/layouts/decidim/action_delegator/admin/delegations.html.erb +13 -0
- data/app/views/layouts/decidim/admin/consultation.html.erb +56 -0
- data/app/views/layouts/decidim/admin/question.html.erb +98 -0
- data/app/views/layouts/decidim/admin/users.html.erb +7 -0
- data/config/i18n-tasks.yml +10 -0
- data/config/locales/ca.yml +101 -0
- data/config/locales/cs.yml +101 -0
- data/config/locales/en.yml +109 -0
- data/config/locales/es.yml +101 -0
- data/db/migrate/20200729194540_create_decidim_action_delegator_delegations.rb +12 -0
- data/db/migrate/20200824113801_create_settings.rb +13 -0
- data/db/migrate/20200828113755_add_setting_id_to_delegations.rb +11 -0
- data/db/migrate/20200831141540_make_granter_id_and_grantee_id_non_nullable.rb +8 -0
- data/db/migrate/20201001172345_remove_expires_at_from_delegations.rb +7 -0
- data/db/migrate/20201005203554_add_setting_granter_unique_index_to_delegations.rb +10 -0
- data/db/migrate/20201006084522_remove_setting_id_index_from_delegations.rb +7 -0
- data/db/migrate/20201030164808_add_delegation_id_to_versions.rb +8 -0
- data/db/seeds.rb +7 -0
- data/lib/decidim/action_delegator/admin.rb +10 -0
- data/lib/decidim/action_delegator/admin_engine.rb +42 -0
- data/lib/decidim/action_delegator/engine.rb +53 -0
- data/lib/decidim/action_delegator/test/factories.rb +25 -0
- data/lib/decidim/action_delegator/version.rb +10 -0
- data/lib/decidim/action_delegator.rb +29 -0
- data/lib/json_key.rb +10 -0
- 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,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
|