decidim-stratified_sortitions 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE-AGPLv3.txt +661 -0
- data/README.md +91 -0
- data/Rakefile +42 -0
- data/app/cells/decidim/stratified_sortitions/stratified_sortition_cell.rb +21 -0
- data/app/cells/decidim/stratified_sortitions/stratified_sortition_l_cell.rb +22 -0
- data/app/cells/decidim/stratified_sortitions/stratified_sortition_metadata_cell.rb +63 -0
- data/app/commands/decidim/stratified_sortitions/admin/create_stratified_sortition.rb +84 -0
- data/app/commands/decidim/stratified_sortitions/admin/destroy_stratified_sortition.rb +49 -0
- data/app/commands/decidim/stratified_sortitions/admin/duplicate_stratified_sortition.rb +74 -0
- data/app/commands/decidim/stratified_sortitions/admin/import_sample.rb +40 -0
- data/app/commands/decidim/stratified_sortitions/admin/remove_uploaded_samples.rb +53 -0
- data/app/commands/decidim/stratified_sortitions/admin/update_stratified_sortition.rb +174 -0
- data/app/controllers/concerns/decidim/stratified_sortitions/orderable_stratified_sortitions.rb +37 -0
- data/app/controllers/concerns/decidim/stratified_sortitions/strata_charts_data.rb +91 -0
- data/app/controllers/decidim/stratified_sortitions/admin/application_controller.rb +19 -0
- data/app/controllers/decidim/stratified_sortitions/admin/samples_controller.rb +143 -0
- data/app/controllers/decidim/stratified_sortitions/admin/stratified_sortitions_controller.rb +235 -0
- data/app/controllers/decidim/stratified_sortitions/application_controller.rb +13 -0
- data/app/controllers/decidim/stratified_sortitions/charts_pdf_controller_helper.rb +17 -0
- data/app/controllers/decidim/stratified_sortitions/stratified_sortitions_controller.rb +61 -0
- data/app/forms/decidim/stratified_sortitions/admin/sample_upload_form.rb +15 -0
- data/app/forms/decidim/stratified_sortitions/admin/stratified_sortitions_form.rb +155 -0
- data/app/forms/decidim/stratified_sortitions/admin/stratum_form.rb +69 -0
- data/app/forms/decidim/stratified_sortitions/admin/substratum_form.rb +26 -0
- data/app/helpers/decidim/stratified_sortitions/admin/stratified_sortitions_helper.rb +18 -0
- data/app/helpers/decidim/stratified_sortitions/application_helper.rb +85 -0
- data/app/helpers/decidim/stratified_sortitions/charts_pdf_helper.rb +73 -0
- data/app/helpers/decidim/stratified_sortitions/stratified_sortition_cells_helper.rb +56 -0
- data/app/javascript/channels/sample_import_progress.js +12 -0
- data/app/jobs/decidim/stratified_sortitions/admin/execute_sortition_job.rb +21 -0
- data/app/jobs/decidim/stratified_sortitions/admin/import_sample_job.rb +170 -0
- data/app/jobs/decidim/stratified_sortitions/admin/remove_samples_job.rb +19 -0
- data/app/jobs/decidim/stratified_sortitions/admin/sortition_results_export_job.rb +28 -0
- data/app/mailers/decidim/stratified_sortitions/admin/import_mailer.rb +26 -0
- data/app/models/decidim/stratified_sortitions/application_record.rb +9 -0
- data/app/models/decidim/stratified_sortitions/panel_portfolio.rb +165 -0
- data/app/models/decidim/stratified_sortitions/sample_import.rb +14 -0
- data/app/models/decidim/stratified_sortitions/sample_participant.rb +12 -0
- data/app/models/decidim/stratified_sortitions/sample_participant_stratum.rb +11 -0
- data/app/models/decidim/stratified_sortitions/stratified_sortition.rb +74 -0
- data/app/models/decidim/stratified_sortitions/stratum.rb +22 -0
- data/app/models/decidim/stratified_sortitions/substratum.rb +19 -0
- data/app/packs/entrypoints/decidim_stratified_sortitions.js +10 -0
- data/app/packs/entrypoints/decidim_stratified_sortitions_admin.js +9 -0
- data/app/packs/entrypoints/decidim_stratified_sortitions_admin.scss +1 -0
- data/app/packs/images/decidim/stratified_sortitions/icon.svg +1 -0
- data/app/packs/src/decidim/stratified_sortitions/application.js +3 -0
- data/app/packs/src/decidim/stratified_sortitions/results_tabs.js +59 -0
- data/app/packs/src/decidim/stratified_sortitions/stratum_fields.js +302 -0
- data/app/packs/src/decidim/stratified_sortitions/substratum_fields.js +165 -0
- data/app/packs/src/decidim/stratified_sortitions/upload_sample.js +16 -0
- data/app/packs/stylesheets/decidim/stratified_sortitions/admin/stratified_sortitions.scss +210 -0
- data/app/packs/stylesheets/decidim/stratified_sortitions/charts_pdf.scss +148 -0
- data/app/packs/stylesheets/stratified_sortitions.scss +56 -0
- data/app/permissions/decidim/stratified_sortitions/admin/permissions.rb +74 -0
- data/app/permissions/decidim/stratified_sortitions/permissions.rb +23 -0
- data/app/presenters/decidim/stratified_sortitions/admin_log/stratified_sortition_presenter.rb +35 -0
- data/app/queries/decidim/stratified_sortitions/filtered_stratified_sortitions.rb +39 -0
- data/app/serializers/decidim/stratified_sortitions/sortition_result_serializer.rb +116 -0
- data/app/services/decidim/stratified_sortitions/charts_pdf_generator.rb +96 -0
- data/app/services/decidim/stratified_sortitions/fair_sortition_service.rb +247 -0
- data/app/services/decidim/stratified_sortitions/leximin/constraint_builder.rb +180 -0
- data/app/services/decidim/stratified_sortitions/leximin/distribution_solver.rb +217 -0
- data/app/services/decidim/stratified_sortitions/leximin/feasibility_checker.rb +222 -0
- data/app/services/decidim/stratified_sortitions/leximin/panel_generator.rb +149 -0
- data/app/services/decidim/stratified_sortitions/leximin/panel_sampler.rb +125 -0
- data/app/services/decidim/stratified_sortitions/leximin_selector.rb +136 -0
- data/app/services/decidim/stratified_sortitions/sortition_results_exporter.rb +161 -0
- data/app/views/decidim/admin/components/index.html.erb +54 -0
- data/app/views/decidim/stratified_sortitions/admin/import_mailer/import.html.erb +22 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_confirm_execute_modal.html.erb +19 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_confirm_participants_modal.html.erb +20 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_form.html.erb +55 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_navigation_menu.html.erb +10 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_strata.html.erb +22 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_stratum.html.erb +75 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_substrata.html.erb +34 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_substratum.html.erb +54 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/edit.html.erb +21 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/execute.html.erb +155 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/export_charts_pdf.html.erb +38 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/index.html.erb +75 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/new.html.erb +21 -0
- data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/upload_sample.html.erb +117 -0
- data/app/views/decidim/stratified_sortitions/shared/_strata_charts.html.erb +21 -0
- data/app/views/decidim/stratified_sortitions/stratified_sortitions/_results_count.html.erb +15 -0
- data/app/views/decidim/stratified_sortitions/stratified_sortitions/_stratified_sortition.html.erb +1 -0
- data/app/views/decidim/stratified_sortitions/stratified_sortitions/_stratified_sortitions.html.erb +13 -0
- data/app/views/decidim/stratified_sortitions/stratified_sortitions/_tags.html.erb +9 -0
- data/app/views/decidim/stratified_sortitions/stratified_sortitions/index.html.erb +22 -0
- data/app/views/decidim/stratified_sortitions/stratified_sortitions/index.js.erb +5 -0
- data/app/views/decidim/stratified_sortitions/stratified_sortitions/show.html.erb +142 -0
- data/app/views/layouts/decidim/admin/_sidebar_menu.html.erb +45 -0
- data/app/views/layouts/decidim/stratified_sortitions/charts_pdf.html.erb +14 -0
- data/config/assets.rb +24 -0
- data/config/locales/ca.yml +361 -0
- data/config/locales/en.yml +360 -0
- data/config/locales/es.yml +360 -0
- data/config/locales/oc.yml +2 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20251023103900_create_decidim_stratified_sortitions_stratified_sortitions.rb +18 -0
- data/db/migrate/20251104120000_create_stratified_sortitions_strata_and_substrata.decidim_stratified_sortitions.rb +27 -0
- data/db/migrate/20251219120000_create_sample_imports.rb +17 -0
- data/db/migrate/20251219120100_create_sample_participants.rb +19 -0
- data/db/migrate/20251219120200_create_sample_participant_strata.rb +16 -0
- data/db/migrate/20251230130000_change_value_to_text_in_substrata.rb +11 -0
- data/db/migrate/20260102114744_add_status_to_stratified_sortitions.rb +11 -0
- data/db/migrate/20260109105643_allow_null_value_in_substrata.rb +10 -0
- data/db/migrate/20260113121726_add_position_to_strata_and_substrata.rb +8 -0
- data/db/migrate/20260127100000_rename_weighing_to_max_quota_percentage.rb +12 -0
- data/db/migrate/20260127120000_create_panel_portfolios.rb +46 -0
- data/db/migrate/20260518000000_add_foreign_key_to_stratified_sortitions_component.rb +10 -0
- data/db/migrate/20260521000000_add_cascade_to_stratified_sortitions_foreign_keys.rb +64 -0
- data/db/migrate/20260616000000_add_execution_error_to_stratified_sortitions.rb +11 -0
- data/lib/decidim/stratified_sortitions/admin.rb +10 -0
- data/lib/decidim/stratified_sortitions/admin_engine.rb +49 -0
- data/lib/decidim/stratified_sortitions/component.rb +41 -0
- data/lib/decidim/stratified_sortitions/engine.rb +34 -0
- data/lib/decidim/stratified_sortitions/seeds.rb +24 -0
- data/lib/decidim/stratified_sortitions/test/factories.rb +80 -0
- data/lib/decidim/stratified_sortitions/version.rb +13 -0
- data/lib/decidim/stratified_sortitions.rb +29 -0
- metadata +222 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
module Admin
|
|
6
|
+
# A command with all the business logic when a user updates a stratified sortition.
|
|
7
|
+
class UpdateStratifiedSortition < Decidim::Command
|
|
8
|
+
include ::Decidim::AttachmentAttributesMethods
|
|
9
|
+
|
|
10
|
+
# Public: Initializes the command.
|
|
11
|
+
#
|
|
12
|
+
# form - A form object with the params.
|
|
13
|
+
# stratified_sortition - the stratified sortition to update.
|
|
14
|
+
def initialize(form, stratified_sortition)
|
|
15
|
+
super()
|
|
16
|
+
@form = form
|
|
17
|
+
@stratified_sortition = stratified_sortition
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Executes the command. Broadcasts these events:
|
|
21
|
+
#
|
|
22
|
+
# - :ok when everything is valid, together with the stratified_sortition.
|
|
23
|
+
# - :invalid if the form wasn't valid and we couldn't proceed.
|
|
24
|
+
#
|
|
25
|
+
# Returns nothing.
|
|
26
|
+
def call
|
|
27
|
+
return broadcast(:invalid) if form.invalid?
|
|
28
|
+
|
|
29
|
+
transaction do
|
|
30
|
+
update_stratified_sortition
|
|
31
|
+
update_strata(stratified_sortition) unless stratified_sortition.executed?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
broadcast(:ok, stratified_sortition)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :form, :stratified_sortition
|
|
40
|
+
|
|
41
|
+
def update_stratified_sortition
|
|
42
|
+
Decidim.traceability.update!(
|
|
43
|
+
stratified_sortition,
|
|
44
|
+
form.current_user,
|
|
45
|
+
attributes:
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def attributes
|
|
50
|
+
parsed_title = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.title, current_organization: form.current_organization).rewrite
|
|
51
|
+
parsed_description = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.description, current_organization: form.current_organization).rewrite
|
|
52
|
+
parsed_selection_criteria = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.selection_criteria, current_organization: form.current_organization).rewrite
|
|
53
|
+
parsed_selected_profiles_description = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.selected_profiles_description,
|
|
54
|
+
current_organization: form.current_organization).rewrite
|
|
55
|
+
{
|
|
56
|
+
title: parsed_title,
|
|
57
|
+
description: parsed_description,
|
|
58
|
+
selection_criteria: parsed_selection_criteria,
|
|
59
|
+
selected_profiles_description: parsed_selected_profiles_description,
|
|
60
|
+
component: form.current_component,
|
|
61
|
+
num_candidates: form.num_candidates,
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def update_strata(stratified_sortition)
|
|
66
|
+
strata_ids_to_keep = form.strata_to_persist.pluck(:id).compact.map(&:to_i)
|
|
67
|
+
|
|
68
|
+
stratified_sortition.strata.each do |existing_stratum|
|
|
69
|
+
next if strata_ids_to_keep.include?(existing_stratum.id)
|
|
70
|
+
|
|
71
|
+
existing_stratum.substrata.each do |substratum|
|
|
72
|
+
Decidim::StratifiedSortitions::SampleParticipantStratum.where(
|
|
73
|
+
decidim_stratified_sortitions_substratum_id: substratum.id
|
|
74
|
+
).destroy_all
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
existing_stratum.destroy!
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
form.strata_to_persist.each do |stratum_form|
|
|
81
|
+
if stratum_form.id.present?
|
|
82
|
+
stratum_object = Decidim::StratifiedSortitions::Stratum.find_by(
|
|
83
|
+
id: stratum_form.id,
|
|
84
|
+
decidim_stratified_sortition_id: stratified_sortition.id
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if stratum_object
|
|
88
|
+
stratum_object.update!(
|
|
89
|
+
name: stratum_form.name,
|
|
90
|
+
kind: stratum_form.kind,
|
|
91
|
+
position: stratum_form.position
|
|
92
|
+
)
|
|
93
|
+
else
|
|
94
|
+
stratum_object = create_stratum(stratified_sortition, stratum_form)
|
|
95
|
+
end
|
|
96
|
+
update_substrata(stratum_object, stratum_form)
|
|
97
|
+
else
|
|
98
|
+
stratum_object = create_stratum(stratified_sortition, stratum_form)
|
|
99
|
+
create_substrata(stratum_object, stratum_form)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def update_substrata(stratum, stratum_form)
|
|
105
|
+
updated_or_created_ids = []
|
|
106
|
+
|
|
107
|
+
stratum_form.substrata_to_persist.each do |substratum_form|
|
|
108
|
+
next if substratum_form.deleted
|
|
109
|
+
|
|
110
|
+
if substratum_form.id.present?
|
|
111
|
+
substratum = Decidim::StratifiedSortitions::Substratum.find_by(
|
|
112
|
+
id: substratum_form.id,
|
|
113
|
+
decidim_stratified_sortitions_stratum_id: stratum.id
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if substratum
|
|
117
|
+
substratum.update!(
|
|
118
|
+
name: substratum_form.name,
|
|
119
|
+
value: substratum_form.value,
|
|
120
|
+
range: substratum_form.range,
|
|
121
|
+
max_quota_percentage: substratum_form.max_quota_percentage,
|
|
122
|
+
position: substratum_form.position
|
|
123
|
+
)
|
|
124
|
+
updated_or_created_ids << substratum.id
|
|
125
|
+
else
|
|
126
|
+
new_substratum = create_substratum(stratum, substratum_form)
|
|
127
|
+
updated_or_created_ids << new_substratum.id
|
|
128
|
+
end
|
|
129
|
+
else
|
|
130
|
+
new_substratum = create_substratum(stratum, substratum_form)
|
|
131
|
+
updated_or_created_ids << new_substratum.id
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
stratum.substrata.reload.each do |existing_substratum|
|
|
136
|
+
next if updated_or_created_ids.include?(existing_substratum.id)
|
|
137
|
+
|
|
138
|
+
Decidim::StratifiedSortitions::SampleParticipantStratum.where(
|
|
139
|
+
decidim_stratified_sortitions_substratum_id: existing_substratum.id
|
|
140
|
+
).destroy_all
|
|
141
|
+
|
|
142
|
+
existing_substratum.destroy!
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def create_stratum(stratified_sortition, stratum_form)
|
|
147
|
+
Decidim::StratifiedSortitions::Stratum.create!(
|
|
148
|
+
stratified_sortition:,
|
|
149
|
+
name: stratum_form.name,
|
|
150
|
+
kind: stratum_form.kind,
|
|
151
|
+
position: stratum_form.position
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def create_substrata(stratum, stratum_form)
|
|
156
|
+
stratum_form.substrata_to_persist.each do |substratum_form|
|
|
157
|
+
create_substratum(stratum, substratum_form)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def create_substratum(stratum, substratum_form)
|
|
162
|
+
Decidim::StratifiedSortitions::Substratum.create!(
|
|
163
|
+
stratum:,
|
|
164
|
+
name: substratum_form.name,
|
|
165
|
+
value: substratum_form.value,
|
|
166
|
+
range: substratum_form.range,
|
|
167
|
+
max_quota_percentage: substratum_form.max_quota_percentage,
|
|
168
|
+
position: substratum_form.position
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
data/app/controllers/concerns/decidim/stratified_sortitions/orderable_stratified_sortitions.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Decidim
|
|
6
|
+
module StratifiedSortitions
|
|
7
|
+
# Common logic to sorting resources
|
|
8
|
+
module OrderableStratifiedSortitions
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
include Decidim::Orderable
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def available_orders
|
|
17
|
+
@available_orders ||= %w(random recent)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def default_order
|
|
21
|
+
"random"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reorder(stratified_sortitions)
|
|
25
|
+
case order
|
|
26
|
+
when "recent"
|
|
27
|
+
stratified_sortitions.order("created_at DESC")
|
|
28
|
+
when "random"
|
|
29
|
+
stratified_sortitions.order_randomly(random_seed)
|
|
30
|
+
else
|
|
31
|
+
stratified_sortitions
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Decidim
|
|
6
|
+
module StratifiedSortitions
|
|
7
|
+
# Shared logic for computing strata and candidates chart data.
|
|
8
|
+
# Used by both admin and public controllers to avoid duplication.
|
|
9
|
+
module StrataChartsData
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def strata_data(stratified_sortition)
|
|
15
|
+
stratified_sortition.strata.order(:position).map do |stratum|
|
|
16
|
+
chart_data = stratum.substrata.order(:position).map do |substratum|
|
|
17
|
+
quota_value = substratum.max_quota_percentage.present? ? substratum.max_quota_percentage.to_f : 0.0
|
|
18
|
+
label_with_percentage = "#{translated_attribute(substratum.name)} (#{quota_value}%)"
|
|
19
|
+
[label_with_percentage, quota_value]
|
|
20
|
+
end
|
|
21
|
+
chart_data = chart_data.reject { |_name, value| value.zero? }
|
|
22
|
+
{
|
|
23
|
+
stratum:,
|
|
24
|
+
chart_data:,
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def candidates_data(stratified_sortition)
|
|
30
|
+
participant_ids = stratified_sortition.sample_participants.pluck(:id)
|
|
31
|
+
participants_distribution_data(stratified_sortition, participant_ids)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def results_data(stratified_sortition)
|
|
35
|
+
return stratified_sortition.strata.order(:position).map { |stratum| { stratum:, chart_data: [] } } unless stratified_sortition.panel_portfolio&.sampled?
|
|
36
|
+
|
|
37
|
+
selected_ids = stratified_sortition.panel_portfolio.selected_panel
|
|
38
|
+
participants_distribution_data(stratified_sortition, selected_ids)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def participants_distribution_data(stratified_sortition, participant_ids)
|
|
42
|
+
sample_candidates_stratum = fetch_sample_candidates_stratum(participant_ids)
|
|
43
|
+
by_stratum = group_by_stratum(sample_candidates_stratum)
|
|
44
|
+
by_stratum_and_substratum = group_by_stratum_and_substratum(sample_candidates_stratum)
|
|
45
|
+
|
|
46
|
+
stratified_sortition.strata.order(:position).map do |stratum|
|
|
47
|
+
build_stratum_chart(stratum, by_stratum, by_stratum_and_substratum)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fetch_sample_candidates_stratum(sample_candidates_ids)
|
|
52
|
+
Decidim::StratifiedSortitions::SampleParticipantStratum
|
|
53
|
+
.where(decidim_stratified_sortitions_sample_participant_id: sample_candidates_ids)
|
|
54
|
+
.select(:decidim_stratified_sortitions_sample_participant_id,
|
|
55
|
+
:decidim_stratified_sortitions_stratum_id,
|
|
56
|
+
:decidim_stratified_sortitions_substratum_id)
|
|
57
|
+
.distinct
|
|
58
|
+
.to_a
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def group_by_stratum(sample_candidates_stratum)
|
|
62
|
+
sample_candidates_stratum.group_by(&:decidim_stratified_sortitions_stratum_id)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def group_by_stratum_and_substratum(sample_candidates_stratum)
|
|
66
|
+
sample_candidates_stratum.group_by do |s|
|
|
67
|
+
[s.decidim_stratified_sortitions_stratum_id, s.decidim_stratified_sortitions_substratum_id]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_stratum_chart(stratum, by_stratum, by_stratum_and_substratum)
|
|
72
|
+
substrata = stratum.substrata.order(:position)
|
|
73
|
+
total = by_stratum[stratum.id]&.map(&:decidim_stratified_sortitions_sample_participant_id)&.uniq&.count || 0
|
|
74
|
+
chart_data = substrata.map do |substratum|
|
|
75
|
+
build_substratum_chart_row(stratum, substratum, by_stratum_and_substratum, total)
|
|
76
|
+
end
|
|
77
|
+
chart_data = chart_data.reject { |_name, value| value.zero? }
|
|
78
|
+
{ stratum:, chart_data: }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_substratum_chart_row(stratum, substratum, by_stratum_and_substratum, total)
|
|
82
|
+
ids = (by_stratum_and_substratum[[stratum.id, substratum.id]] || [])
|
|
83
|
+
.map(&:decidim_stratified_sortitions_sample_participant_id).uniq
|
|
84
|
+
count = ids.count
|
|
85
|
+
percentage = total.positive? ? ((count.to_f / total) * 100).round(1) : 0.0
|
|
86
|
+
label = "#{translated_attribute(substratum.name)} (#{percentage}%)"
|
|
87
|
+
[label, count]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
module Admin
|
|
6
|
+
# This controller is the abstract class from which all other controllers of
|
|
7
|
+
# this engine inherit.
|
|
8
|
+
#
|
|
9
|
+
# Note that it inherits from `Decidim::Admin::Components::BaseController`, which
|
|
10
|
+
# override its layout and provide all kinds of useful methods.
|
|
11
|
+
class ApplicationController < Decidim::Admin::Components::BaseController
|
|
12
|
+
def index
|
|
13
|
+
enforce_permission_to :read, :stratified_sortitions
|
|
14
|
+
@stratified_sortitions = stratified_sortitions
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubyXL"
|
|
4
|
+
require "rubyXL/convenience_methods/cell"
|
|
5
|
+
require "rubyXL/convenience_methods/font"
|
|
6
|
+
require "rubyXL/convenience_methods/workbook"
|
|
7
|
+
require "rubyXL/convenience_methods/worksheet"
|
|
8
|
+
|
|
9
|
+
module Decidim
|
|
10
|
+
module StratifiedSortitions
|
|
11
|
+
module Admin
|
|
12
|
+
# Controller that allows managing samples for stratified sortitions.
|
|
13
|
+
#
|
|
14
|
+
class SamplesController < Decidim::StratifiedSortitions::Admin::ApplicationController
|
|
15
|
+
include Decidim::TranslatableAttributes
|
|
16
|
+
|
|
17
|
+
before_action :ensure_strata_configured, only: [:create, :remove_multiple]
|
|
18
|
+
before_action :ensure_stratified_sortition_is_pending, only: [:create, :remove_multiple]
|
|
19
|
+
|
|
20
|
+
include Decidim::ApplicationHelper
|
|
21
|
+
|
|
22
|
+
helper StratifiedSortitions::ApplicationHelper
|
|
23
|
+
helper Decidim::PaginateHelper
|
|
24
|
+
|
|
25
|
+
helper_method :stratified_sortitions, :stratified_sortition, :form_presenter, :blank_stratum, :blank_substratum
|
|
26
|
+
|
|
27
|
+
def download_template
|
|
28
|
+
if params[:format] == "xlsx"
|
|
29
|
+
send_data generate_template_sample_xlsx,
|
|
30
|
+
filename: "sample_importation_template.xlsx",
|
|
31
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
32
|
+
else
|
|
33
|
+
send_data generate_template_sample_csv,
|
|
34
|
+
filename: "sample_importation_template.csv",
|
|
35
|
+
type: "text/csv"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create
|
|
40
|
+
enforce_permission_to :upload_sample, :stratified_sortition
|
|
41
|
+
|
|
42
|
+
@form = form(SampleUploadForm).from_params(params)
|
|
43
|
+
|
|
44
|
+
Decidim::StratifiedSortitions::Admin::ImportSample.call(@form, stratified_sortition, current_user) do
|
|
45
|
+
on(:ok) do
|
|
46
|
+
Decidim.traceability.perform_action!("import_sample", stratified_sortition, current_user, visibility: "all")
|
|
47
|
+
flash[:notice] = I18n.t("sample_imports.create.success", scope: "decidim.stratified_sortitions.admin")
|
|
48
|
+
redirect_to upload_sample_stratified_sortition_path(stratified_sortition)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
on(:invalid) do
|
|
52
|
+
flash.now[:alert] = I18n.t("sample_imports.create.invalid", scope: "decidim.stratified_sortitions.admin")
|
|
53
|
+
redirect_to upload_sample_stratified_sortition_path(stratified_sortition)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def remove_multiple
|
|
59
|
+
enforce_permission_to :upload_sample, :stratified_sortition
|
|
60
|
+
|
|
61
|
+
Decidim::StratifiedSortitions::Admin::RemoveSamplesJob.perform_later(stratified_sortition, current_user)
|
|
62
|
+
flash[:notice] = I18n.t("sample_imports.remove_uploaded_samples.enqueued", scope: "decidim.stratified_sortitions.admin")
|
|
63
|
+
redirect_to upload_sample_stratified_sortition_path(stratified_sortition)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def ensure_strata_configured
|
|
69
|
+
unless stratified_sortition.strata_and_substrata_configured?
|
|
70
|
+
flash[:alert] = t("decidim.stratified_sortitions.admin.samples.errors.no_strata")
|
|
71
|
+
redirect_to upload_sample_stratified_sortition_path(stratified_sortition)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def ensure_stratified_sortition_is_pending
|
|
76
|
+
unless stratified_sortition.status == "pending"
|
|
77
|
+
flash[:alert] = t("decidim.stratified_sortitions.admin.samples.errors.pending")
|
|
78
|
+
redirect_to upload_sample_stratified_sortition_path(stratified_sortition)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def collection
|
|
83
|
+
@collection ||= StratifiedSortition.where(component: current_component)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def stratified_sortitions
|
|
87
|
+
@stratified_sortitions ||= collection.page(params[:page]).per(10)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stratified_sortition
|
|
91
|
+
@stratified_sortition ||= collection.find(params[:id])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def form_presenter
|
|
95
|
+
@form_presenter ||= present(@form, presenter_class: Decidim::StratifiedSortitions::StratifiedSortitionPresenter)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def blank_stratum
|
|
99
|
+
@blank_stratum ||= Decidim::StratifiedSortitions::Admin::StratumForm.new
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def blank_substratum
|
|
103
|
+
@blank_substratum ||= Decidim::StratifiedSortitions::Admin::SubstratumForm.new
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def template_sample_headers
|
|
107
|
+
personal_headers = [
|
|
108
|
+
I18n.t("decidim.stratified_sortitions.admin.samples.template.personal_data_1"),
|
|
109
|
+
I18n.t("decidim.stratified_sortitions.admin.samples.template.personal_data_2"),
|
|
110
|
+
I18n.t("decidim.stratified_sortitions.admin.samples.template.personal_data_3"),
|
|
111
|
+
I18n.t("decidim.stratified_sortitions.admin.samples.template.personal_data_4"),
|
|
112
|
+
]
|
|
113
|
+
strata = stratified_sortition.strata.order(:position)
|
|
114
|
+
strata_headers = strata.map do |stratum|
|
|
115
|
+
translated_attribute(stratum.name)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
personal_headers + strata_headers
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def generate_template_sample_csv
|
|
122
|
+
headers = template_sample_headers
|
|
123
|
+
CSV.generate(col_sep: ",") do |csv|
|
|
124
|
+
csv << headers
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def generate_template_sample_xlsx
|
|
129
|
+
headers = template_sample_headers
|
|
130
|
+
workbook = RubyXL::Workbook.new
|
|
131
|
+
worksheet = workbook[0]
|
|
132
|
+
|
|
133
|
+
headers.each_with_index do |header, col|
|
|
134
|
+
worksheet.add_cell(0, col, header)
|
|
135
|
+
worksheet.change_column_width(col, 20)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
workbook.stream.string
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|