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,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
module Admin
|
|
6
|
+
# Controller that allows managing admin stratified sortitions.
|
|
7
|
+
#
|
|
8
|
+
class StratifiedSortitionsController < Decidim::StratifiedSortitions::Admin::ApplicationController
|
|
9
|
+
include Decidim::ApplicationHelper
|
|
10
|
+
include StrataChartsData
|
|
11
|
+
|
|
12
|
+
helper StratifiedSortitions::ApplicationHelper
|
|
13
|
+
helper Decidim::PaginateHelper
|
|
14
|
+
|
|
15
|
+
helper_method :stratified_sortitions, :stratified_sortition, :form_presenter, :blank_stratum
|
|
16
|
+
|
|
17
|
+
def index
|
|
18
|
+
enforce_permission_to :read, :stratified_sortitions
|
|
19
|
+
@stratified_sortitions = stratified_sortitions
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def new
|
|
23
|
+
enforce_permission_to :create, :stratified_sortition
|
|
24
|
+
@form = form(Decidim::StratifiedSortitions::Admin::StratifiedSortitionsForm).instance
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def edit
|
|
28
|
+
enforce_permission_to(:edit, :stratified_sortition, stratified_sortition:)
|
|
29
|
+
@form = form(Decidim::StratifiedSortitions::Admin::StratifiedSortitionsForm).from_model(stratified_sortition)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create
|
|
33
|
+
enforce_permission_to :create, :stratified_sortition
|
|
34
|
+
@form = form(Decidim::StratifiedSortitions::Admin::StratifiedSortitionsForm).from_params(params)
|
|
35
|
+
|
|
36
|
+
Decidim::StratifiedSortitions::Admin::CreateStratifiedSortition.call(@form) do
|
|
37
|
+
on(:ok) do |created_sortition|
|
|
38
|
+
flash[:notice] = I18n.t("stratified_sortitions.create.success", scope: "decidim.stratified_sortitions.admin")
|
|
39
|
+
redirect_to edit_stratified_sortition_path(created_sortition)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
on(:invalid) do
|
|
43
|
+
flash.now[:alert] = I18n.t("stratified_sortitions.create.error", scope: "decidim.stratified_sortitions.admin")
|
|
44
|
+
render action: "new"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def update
|
|
50
|
+
enforce_permission_to(:edit, :stratified_sortition, stratified_sortition:)
|
|
51
|
+
@form = form(Decidim::StratifiedSortitions::Admin::StratifiedSortitionsForm).from_params(params)
|
|
52
|
+
|
|
53
|
+
Decidim::StratifiedSortitions::Admin::UpdateStratifiedSortition.call(@form, stratified_sortition) do
|
|
54
|
+
on(:ok) do |_stratified_sortition|
|
|
55
|
+
flash[:notice] = t("stratified_sortitions.update.success", scope: "decidim.stratified_sortitions.admin")
|
|
56
|
+
redirect_to edit_stratified_sortition_path(stratified_sortition)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
on(:invalid) do
|
|
60
|
+
flash.now[:alert] = t("stratified_sortitions.update.error", scope: "decidim.stratified_sortitions.admin")
|
|
61
|
+
render :edit
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def duplicate
|
|
67
|
+
enforce_permission_to(:duplicate, :stratified_sortition, stratified_sortition:)
|
|
68
|
+
|
|
69
|
+
Decidim::StratifiedSortitions::Admin::DuplicateStratifiedSortition.call(stratified_sortition, current_user) do
|
|
70
|
+
on(:ok) do |_new_stratified_sortition|
|
|
71
|
+
flash[:notice] = I18n.t("stratified_sortitions.duplicate.success", scope: "decidim.stratified_sortitions.admin")
|
|
72
|
+
redirect_to stratified_sortitions_path(assembly_slug: -1, component_id: -1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
on(:invalid) do
|
|
76
|
+
flash[:alert] = I18n.t("stratified_sortitions.duplicate.error", scope: "decidim.stratified_sortitions.admin")
|
|
77
|
+
redirect_to stratified_sortitions_path(assembly_slug: -1, component_id: -1)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def destroy
|
|
83
|
+
enforce_permission_to(:destroy, :stratified_sortition, stratified_sortition:)
|
|
84
|
+
|
|
85
|
+
Decidim::StratifiedSortitions::Admin::DestroyStratifiedSortition.call(stratified_sortition, current_user) do
|
|
86
|
+
on(:ok) do
|
|
87
|
+
flash[:notice] = I18n.t("stratified_sortitions.destroy.success", scope: "decidim.stratified_sortitions.admin")
|
|
88
|
+
redirect_to stratified_sortitions_path(assembly_slug: -1, component_id: -1)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
on(:has_problems) do
|
|
92
|
+
redirect_to stratified_sortitions_path, flash: { error: t("stratified_sortitions.destroy.has_problems", scope: "decidim.stratified_sortitions.admin") }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
on(:invalid) do
|
|
96
|
+
redirect_to stratified_sortitions_path, flash: { error: t("stratified_sortitions.destroy.error", scope: "decidim.stratified_sortitions.admin") }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def upload_sample
|
|
102
|
+
enforce_permission_to :upload_sample, :stratified_sortition
|
|
103
|
+
if stratified_sortition.strata_and_substrata_configured?
|
|
104
|
+
@stratified_sortition = stratified_sortition
|
|
105
|
+
@form = SampleUploadForm.new
|
|
106
|
+
@sample_participants_count = @stratified_sortition.sample_participants.count
|
|
107
|
+
@last_sample = SampleImport.where(stratified_sortition: @stratified_sortition).order(created_at: :desc).first
|
|
108
|
+
@samples = SampleImport.where(stratified_sortition: @stratified_sortition).order(created_at: :asc)
|
|
109
|
+
@strata_data = strata_data(@stratified_sortition)
|
|
110
|
+
@candidates_data = candidates_data(@stratified_sortition)
|
|
111
|
+
else
|
|
112
|
+
redirect_to edit_stratified_sortition_path(stratified_sortition),
|
|
113
|
+
flash: { warning: t("stratified_sortitions.upload_sample.strata_not_configured", scope: "decidim.stratified_sortitions.admin") }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def execute
|
|
118
|
+
unless stratified_sortition.can_execute? || stratified_sortition.executing? || stratified_sortition.executed? || stratified_sortition.failed?
|
|
119
|
+
redirect_to edit_stratified_sortition_path(stratified_sortition),
|
|
120
|
+
flash: { warning: t("stratified_sortitions.execute.empty_sample_participants", scope: "decidim.stratified_sortitions.admin") }
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
@stratified_sortition = stratified_sortition
|
|
125
|
+
@portfolio = @stratified_sortition.panel_portfolio
|
|
126
|
+
@strata = @stratified_sortition.strata.order(:position)
|
|
127
|
+
@selected_participants = if @portfolio&.sampled?
|
|
128
|
+
SampleParticipant
|
|
129
|
+
.where(id: @portfolio.selected_panel)
|
|
130
|
+
.includes(sample_participant_strata: [:decidim_stratified_sortitions_stratum, :decidim_stratified_sortitions_substratum])
|
|
131
|
+
.order(:id)
|
|
132
|
+
.to_a
|
|
133
|
+
else
|
|
134
|
+
[]
|
|
135
|
+
end
|
|
136
|
+
@strata_data = strata_data(@stratified_sortition)
|
|
137
|
+
@candidates_data = candidates_data(@stratified_sortition)
|
|
138
|
+
@results_data = results_data(@stratified_sortition)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def execute_stratified_sortition
|
|
142
|
+
unless stratified_sortition.can_execute?
|
|
143
|
+
redirect_to execute_stratified_sortition_path(stratified_sortition)
|
|
144
|
+
return
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
stratified_sortition.update!(status: "executing")
|
|
148
|
+
ExecuteSortitionJob.perform_later(stratified_sortition, current_user)
|
|
149
|
+
flash[:notice] = I18n.t("stratified_sortitions.execute.executing", scope: "decidim.stratified_sortitions.admin")
|
|
150
|
+
redirect_to execute_stratified_sortition_path(stratified_sortition)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def export_charts_pdf
|
|
154
|
+
portfolio = stratified_sortition.panel_portfolio
|
|
155
|
+
|
|
156
|
+
unless portfolio&.sampled?
|
|
157
|
+
flash[:error] = I18n.t("stratified_sortitions.export_results.no_results", scope: "decidim.stratified_sortitions.admin")
|
|
158
|
+
redirect_to execute_stratified_sortition_path(stratified_sortition)
|
|
159
|
+
return
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
generator = ChartsPdfGenerator.new(
|
|
163
|
+
stratified_sortition,
|
|
164
|
+
strata_data(stratified_sortition),
|
|
165
|
+
candidates_data(stratified_sortition),
|
|
166
|
+
results_data(stratified_sortition),
|
|
167
|
+
locale: I18n.locale
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
filename = "sortition_charts_#{stratified_sortition.id}_#{Time.current.strftime("%Y%m%d_%H%M%S")}.pdf"
|
|
171
|
+
send_data generator.generate,
|
|
172
|
+
filename:,
|
|
173
|
+
type: "application/pdf",
|
|
174
|
+
disposition: "attachment"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def export_results
|
|
178
|
+
format = params[:format]&.downcase || "csv"
|
|
179
|
+
portfolio = stratified_sortition.panel_portfolio
|
|
180
|
+
|
|
181
|
+
unless portfolio&.sampled?
|
|
182
|
+
flash[:error] = I18n.t("stratified_sortitions.export_results.no_results", scope: "decidim.stratified_sortitions.admin")
|
|
183
|
+
redirect_to execute_stratified_sortition_path(stratified_sortition)
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
SortitionResultsExportJob.perform_later(current_user, stratified_sortition, format)
|
|
188
|
+
|
|
189
|
+
Decidim.traceability.perform_action!("export_results", stratified_sortition, current_user, visibility: "all")
|
|
190
|
+
flash[:notice] = I18n.t("decidim.admin.exports.notice")
|
|
191
|
+
redirect_to execute_stratified_sortition_path(stratified_sortition)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def log_view_participants
|
|
195
|
+
portfolio = stratified_sortition.panel_portfolio
|
|
196
|
+
|
|
197
|
+
unless portfolio&.sampled?
|
|
198
|
+
head :unprocessable_entity
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
Decidim.traceability.perform_action!("view_participants", stratified_sortition, current_user, visibility: "all")
|
|
203
|
+
|
|
204
|
+
head :ok
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def collection
|
|
210
|
+
@collection ||= StratifiedSortition.where(component: current_component)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def stratified_sortitions
|
|
214
|
+
@stratified_sortitions ||= collection.page(params[:page]).per(10)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def stratified_sortition
|
|
218
|
+
@stratified_sortition ||= collection.find_by(id: params[:id])
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def form_presenter
|
|
222
|
+
@form_presenter ||= present(@form, presenter_class: Decidim::StratifiedSortitions::StratifiedSortitionPresenter)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def blank_stratum
|
|
226
|
+
@blank_stratum ||= Decidim::StratifiedSortitions::Admin::StratumForm.new
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def blank_substratum(stratum_form)
|
|
230
|
+
Decidim::StratifiedSortitions::Admin::SubstratumForm.new(stratum: stratum_form.model)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
# This controller is the abstract class from which all other controllers of
|
|
6
|
+
# this engine inherit.
|
|
7
|
+
#
|
|
8
|
+
# Note that it inherits from `Decidim::Components::BaseController`, which
|
|
9
|
+
# override its layout and provide all kinds of useful methods.
|
|
10
|
+
class ApplicationController < Decidim::Components::BaseController
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
# A dummy controller used to render PDF views outside the request cycle.
|
|
6
|
+
# Following the same pattern as Decidim core (decidim-forms).
|
|
7
|
+
# rubocop:disable Rails/ApplicationController
|
|
8
|
+
class ChartsPdfControllerHelper < ActionController::Base
|
|
9
|
+
# rubocop:enable Rails/ApplicationController
|
|
10
|
+
helper Decidim::TranslationsHelper
|
|
11
|
+
helper Decidim::StratifiedSortitions::ChartsPdfHelper
|
|
12
|
+
|
|
13
|
+
# Ensure the engine's views (templates + layouts) are found.
|
|
14
|
+
append_view_path Decidim::StratifiedSortitions::Engine.root.join("app", "views")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
# Controller that allows browsing stratified sortitions.
|
|
6
|
+
#
|
|
7
|
+
class StratifiedSortitionsController < Decidim::StratifiedSortitions::ApplicationController
|
|
8
|
+
include Decidim::ApplicationHelper
|
|
9
|
+
include FilterResource
|
|
10
|
+
include Paginable
|
|
11
|
+
include OrderableStratifiedSortitions
|
|
12
|
+
include StrataChartsData
|
|
13
|
+
|
|
14
|
+
helper Decidim::CheckBoxesTreeHelper
|
|
15
|
+
helper Decidim::PaginateHelper
|
|
16
|
+
|
|
17
|
+
helper_method :stratified_sortitions, :stratified_sortition
|
|
18
|
+
|
|
19
|
+
def index
|
|
20
|
+
@stratified_sortitions = search.result
|
|
21
|
+
@stratified_sortitions = reorder(@stratified_sortitions)
|
|
22
|
+
@stratified_sortitions = paginate(@stratified_sortitions)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def show
|
|
26
|
+
raise ActionController::RoutingError, "Not Found" unless stratified_sortition
|
|
27
|
+
|
|
28
|
+
if current_component.settings.publish_sortitions
|
|
29
|
+
@strata_data = strata_data(stratified_sortition) if stratified_sortition.strata_and_substrata_configured?
|
|
30
|
+
@candidates_data = candidates_data(stratified_sortition) if stratified_sortition.can_execute?
|
|
31
|
+
@results_data = results_data(stratified_sortition) if stratified_sortition.panel_portfolio&.sampled?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def stratified_sortition
|
|
38
|
+
@stratified_sortition ||= search_collection.find_by(id: params[:id])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stratified_sortition_scope
|
|
42
|
+
@stratified_sortition_scope ||= current_organization.scopes.find_by(id: stratified_sortition&.decidim_scope_id)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stratified_sortitions
|
|
46
|
+
@stratified_sortitions ||= reorder(paginate(search.result))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def search_collection
|
|
50
|
+
::Decidim::StratifiedSortitions::StratifiedSortition.where(component: current_component)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def default_filter_params
|
|
54
|
+
{
|
|
55
|
+
search_text_cont: "",
|
|
56
|
+
with_any_state: "all",
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
module Admin
|
|
6
|
+
class SampleUploadForm < Decidim::Form
|
|
7
|
+
include Decidim::HasUploadValidations
|
|
8
|
+
|
|
9
|
+
attribute :file, Decidim::Attributes::Blob
|
|
10
|
+
|
|
11
|
+
validates :file, presence: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
module Admin
|
|
6
|
+
# A form object used to create stratified sortitions from the admin dashboard.
|
|
7
|
+
#
|
|
8
|
+
class StratifiedSortitionsForm < Decidim::Form
|
|
9
|
+
include TranslatableAttributes
|
|
10
|
+
include TranslationsHelper
|
|
11
|
+
include Decidim::HasUploadValidations
|
|
12
|
+
include ApplicationHelper
|
|
13
|
+
|
|
14
|
+
mimic :stratified_sortition
|
|
15
|
+
|
|
16
|
+
translatable_attribute :title, String do |field, _locale|
|
|
17
|
+
validates field, length: { in: 5..150 }, if: proc { |resource| resource.send(field).present? }
|
|
18
|
+
end
|
|
19
|
+
translatable_attribute :description, String
|
|
20
|
+
translatable_attribute :selection_criteria, String
|
|
21
|
+
translatable_attribute :selected_profiles_description, String
|
|
22
|
+
|
|
23
|
+
attribute :id, Integer
|
|
24
|
+
attribute :decidim_component_id, Integer
|
|
25
|
+
attribute :num_candidates, Integer
|
|
26
|
+
attribute :strata, [StratumForm]
|
|
27
|
+
|
|
28
|
+
validates :title, :description, translatable_presence: true
|
|
29
|
+
validates :num_candidates,
|
|
30
|
+
presence: true,
|
|
31
|
+
numericality: {
|
|
32
|
+
only_integer: true,
|
|
33
|
+
greater_than_or_equal_to: 1,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
alias organization current_organization
|
|
37
|
+
|
|
38
|
+
def map_model(model)
|
|
39
|
+
super
|
|
40
|
+
self.strata = model.strata.order(:position).map do |stratum|
|
|
41
|
+
Decidim::StratifiedSortitions::Admin::StratumForm.from_model(stratum)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def strata_to_persist
|
|
46
|
+
strata.reject(&:deleted)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
validate :strata_immutable_when_sample_participants_exist
|
|
50
|
+
validate :num_candidates_immutable_when_executed
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def num_candidates_immutable_when_executed
|
|
55
|
+
return unless stratified_sortition&.executed?
|
|
56
|
+
return if stratified_sortition.num_candidates == num_candidates
|
|
57
|
+
|
|
58
|
+
errors.add(:num_candidates, :cannot_change_num_candidates_when_executed)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def strata_immutable_when_sample_participants_exist
|
|
62
|
+
return unless stratified_sortition&.sample_participants&.any?
|
|
63
|
+
return if stratified_sortition.executed?
|
|
64
|
+
|
|
65
|
+
validate_strata_count
|
|
66
|
+
validate_strata_attributes
|
|
67
|
+
validate_substrata_changes
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_strata_count
|
|
71
|
+
existing_strata_ids = stratified_sortition.strata.pluck(:id)
|
|
72
|
+
form_strata_ids = strata_to_persist.map { |s| s.id.to_i }.reject(&:zero?)
|
|
73
|
+
|
|
74
|
+
new_strata = strata_to_persist.select { |s| s.id.blank? }
|
|
75
|
+
errors.add(:strata, :cannot_add_strata_with_sample_participants) if new_strata.any?
|
|
76
|
+
|
|
77
|
+
errors.add(:strata, :cannot_delete_strata_with_sample_participants) if (existing_strata_ids - form_strata_ids).any?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_strata_attributes
|
|
81
|
+
strata_to_persist.each do |stratum_form|
|
|
82
|
+
next if stratum_form.id.blank?
|
|
83
|
+
|
|
84
|
+
existing_stratum = stratified_sortition.strata.find_by(id: stratum_form.id)
|
|
85
|
+
next unless existing_stratum
|
|
86
|
+
|
|
87
|
+
errors.add(:strata, :cannot_change_stratum_name_with_sample_participants) unless same_translated_attribute?(existing_stratum.name, stratum_form.name)
|
|
88
|
+
|
|
89
|
+
errors.add(:strata, :cannot_change_stratum_kind_with_sample_participants) if existing_stratum.kind != stratum_form.kind
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_substrata_changes
|
|
94
|
+
strata_to_persist.each do |stratum_form|
|
|
95
|
+
next if stratum_form.id.blank?
|
|
96
|
+
|
|
97
|
+
existing_stratum = stratified_sortition.strata.find_by(id: stratum_form.id)
|
|
98
|
+
next unless existing_stratum
|
|
99
|
+
|
|
100
|
+
existing_substrata_ids = existing_stratum.substrata.pluck(:id)
|
|
101
|
+
form_substrata_ids = stratum_form.substrata_to_persist.map { |s| s.id.to_i }.reject(&:zero?)
|
|
102
|
+
|
|
103
|
+
new_substrata = stratum_form.substrata_to_persist.select { |s| s.id.blank? }
|
|
104
|
+
errors.add(:strata, :cannot_add_substrata_with_sample_participants) if new_substrata.any?
|
|
105
|
+
|
|
106
|
+
errors.add(:strata, :cannot_delete_substrata_with_sample_participants) if (existing_substrata_ids - form_substrata_ids).any?
|
|
107
|
+
|
|
108
|
+
validate_substrata_attributes(existing_stratum, stratum_form)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_substrata_attributes(existing_stratum, stratum_form)
|
|
113
|
+
stratum_form.substrata_to_persist.each do |substratum_form|
|
|
114
|
+
next if substratum_form.id.blank?
|
|
115
|
+
|
|
116
|
+
existing_substratum = existing_stratum.substrata.find_by(id: substratum_form.id)
|
|
117
|
+
next unless existing_substratum
|
|
118
|
+
|
|
119
|
+
errors.add(:strata, :cannot_change_substratum_name_with_sample_participants) unless same_translated_attribute?(existing_substratum.name, substratum_form.name)
|
|
120
|
+
|
|
121
|
+
unless normalize_blank(existing_substratum.value) == normalize_blank(substratum_form.value)
|
|
122
|
+
errors.add(:strata, :cannot_change_substratum_value_with_sample_participants)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
unless normalize_blank(existing_substratum.range) == normalize_blank(substratum_form.range)
|
|
126
|
+
errors.add(:strata, :cannot_change_substratum_range_with_sample_participants)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def normalize_blank(value)
|
|
132
|
+
value.presence
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def same_translated_attribute?(existing_value, form_value)
|
|
136
|
+
existing_hash = normalize_translated_hash(existing_value)
|
|
137
|
+
form_hash = normalize_translated_hash(form_value)
|
|
138
|
+
|
|
139
|
+
locales = (existing_hash.keys + form_hash.keys).uniq
|
|
140
|
+
locales.all? { |locale| existing_hash[locale] == form_hash[locale] }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def normalize_translated_hash(value)
|
|
144
|
+
hash = value.is_a?(Hash) ? value.stringify_keys : {}
|
|
145
|
+
hash = hash.except("machine_translations")
|
|
146
|
+
hash.transform_values { |v| v.to_s.strip.presence }.compact
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def stratified_sortition
|
|
150
|
+
@stratified_sortition ||= Decidim::StratifiedSortitions::StratifiedSortition.find_by(id:)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
4
|
+
# rubocop:disable Lint/ReturnInVoidContext
|
|
5
|
+
module Decidim
|
|
6
|
+
module StratifiedSortitions
|
|
7
|
+
module Admin
|
|
8
|
+
class StratumForm < Decidim::Form
|
|
9
|
+
include TranslatableAttributes
|
|
10
|
+
|
|
11
|
+
translatable_attribute :name, String
|
|
12
|
+
attribute :kind, String
|
|
13
|
+
attribute :deleted, Boolean, default: false
|
|
14
|
+
attribute :substrata, [SubstratumForm]
|
|
15
|
+
attribute :position, Integer
|
|
16
|
+
|
|
17
|
+
validates :name, translatable_presence: true, unless: :deleted
|
|
18
|
+
validates :position, numericality: { greater_than_or_equal_to: 0 }
|
|
19
|
+
|
|
20
|
+
def substrata
|
|
21
|
+
@substrata ||= []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def substrata=(value)
|
|
25
|
+
return @substrata = [] if value.blank?
|
|
26
|
+
|
|
27
|
+
if value.is_a?(Hash)
|
|
28
|
+
value = if value.values.first.is_a?(String)
|
|
29
|
+
[value]
|
|
30
|
+
else
|
|
31
|
+
value.values
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@substrata = value.map do |substratum_data|
|
|
36
|
+
if substratum_data.is_a?(SubstratumForm)
|
|
37
|
+
substratum_data
|
|
38
|
+
elsif substratum_data.is_a?(Hash)
|
|
39
|
+
SubstratumForm.from_params(substratum_data)
|
|
40
|
+
elsif substratum_data.is_a?(Array) && substratum_data[1].is_a?(Hash)
|
|
41
|
+
SubstratumForm.from_params(substratum_data[1])
|
|
42
|
+
else
|
|
43
|
+
next
|
|
44
|
+
end
|
|
45
|
+
end.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def map_model(model)
|
|
49
|
+
super
|
|
50
|
+
self.substrata = model.substrata.order(:position).map do |substratum|
|
|
51
|
+
Decidim::StratifiedSortitions::Admin::SubstratumForm.from_model(substratum)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def substrata_to_persist
|
|
56
|
+
substrata.select { |s| s.is_a?(SubstratumForm) && !s.deleted }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_param
|
|
60
|
+
return id if id.present?
|
|
61
|
+
|
|
62
|
+
"stratified-sortition-stratum-id"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
69
|
+
# rubocop:enable Lint/ReturnInVoidContext
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
module Admin
|
|
6
|
+
class SubstratumForm < Decidim::Form
|
|
7
|
+
include TranslatableAttributes
|
|
8
|
+
|
|
9
|
+
translatable_attribute :name, String
|
|
10
|
+
attribute :value, String
|
|
11
|
+
attribute :range, String
|
|
12
|
+
attribute :max_quota_percentage, String
|
|
13
|
+
attribute :deleted, Boolean, default: false
|
|
14
|
+
attribute :position, Integer
|
|
15
|
+
|
|
16
|
+
validates :position, numericality: { greater_than_or_equal_to: 0 }
|
|
17
|
+
|
|
18
|
+
def to_param
|
|
19
|
+
return id if id.present?
|
|
20
|
+
|
|
21
|
+
"stratified-sortition-strata-substratum-id"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module StratifiedSortitions
|
|
5
|
+
module Admin
|
|
6
|
+
module StratifiedSortitionsHelper
|
|
7
|
+
def stratified_sortition_state_css_class(stratified_sortition)
|
|
8
|
+
case stratified_sortition.status
|
|
9
|
+
when "executed" then "success"
|
|
10
|
+
when "executing", "failed" then "alert"
|
|
11
|
+
when "pending" then "warning"
|
|
12
|
+
else "info"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|