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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +91 -0
  4. data/Rakefile +42 -0
  5. data/app/cells/decidim/stratified_sortitions/stratified_sortition_cell.rb +21 -0
  6. data/app/cells/decidim/stratified_sortitions/stratified_sortition_l_cell.rb +22 -0
  7. data/app/cells/decidim/stratified_sortitions/stratified_sortition_metadata_cell.rb +63 -0
  8. data/app/commands/decidim/stratified_sortitions/admin/create_stratified_sortition.rb +84 -0
  9. data/app/commands/decidim/stratified_sortitions/admin/destroy_stratified_sortition.rb +49 -0
  10. data/app/commands/decidim/stratified_sortitions/admin/duplicate_stratified_sortition.rb +74 -0
  11. data/app/commands/decidim/stratified_sortitions/admin/import_sample.rb +40 -0
  12. data/app/commands/decidim/stratified_sortitions/admin/remove_uploaded_samples.rb +53 -0
  13. data/app/commands/decidim/stratified_sortitions/admin/update_stratified_sortition.rb +174 -0
  14. data/app/controllers/concerns/decidim/stratified_sortitions/orderable_stratified_sortitions.rb +37 -0
  15. data/app/controllers/concerns/decidim/stratified_sortitions/strata_charts_data.rb +91 -0
  16. data/app/controllers/decidim/stratified_sortitions/admin/application_controller.rb +19 -0
  17. data/app/controllers/decidim/stratified_sortitions/admin/samples_controller.rb +143 -0
  18. data/app/controllers/decidim/stratified_sortitions/admin/stratified_sortitions_controller.rb +235 -0
  19. data/app/controllers/decidim/stratified_sortitions/application_controller.rb +13 -0
  20. data/app/controllers/decidim/stratified_sortitions/charts_pdf_controller_helper.rb +17 -0
  21. data/app/controllers/decidim/stratified_sortitions/stratified_sortitions_controller.rb +61 -0
  22. data/app/forms/decidim/stratified_sortitions/admin/sample_upload_form.rb +15 -0
  23. data/app/forms/decidim/stratified_sortitions/admin/stratified_sortitions_form.rb +155 -0
  24. data/app/forms/decidim/stratified_sortitions/admin/stratum_form.rb +69 -0
  25. data/app/forms/decidim/stratified_sortitions/admin/substratum_form.rb +26 -0
  26. data/app/helpers/decidim/stratified_sortitions/admin/stratified_sortitions_helper.rb +18 -0
  27. data/app/helpers/decidim/stratified_sortitions/application_helper.rb +85 -0
  28. data/app/helpers/decidim/stratified_sortitions/charts_pdf_helper.rb +73 -0
  29. data/app/helpers/decidim/stratified_sortitions/stratified_sortition_cells_helper.rb +56 -0
  30. data/app/javascript/channels/sample_import_progress.js +12 -0
  31. data/app/jobs/decidim/stratified_sortitions/admin/execute_sortition_job.rb +21 -0
  32. data/app/jobs/decidim/stratified_sortitions/admin/import_sample_job.rb +170 -0
  33. data/app/jobs/decidim/stratified_sortitions/admin/remove_samples_job.rb +19 -0
  34. data/app/jobs/decidim/stratified_sortitions/admin/sortition_results_export_job.rb +28 -0
  35. data/app/mailers/decidim/stratified_sortitions/admin/import_mailer.rb +26 -0
  36. data/app/models/decidim/stratified_sortitions/application_record.rb +9 -0
  37. data/app/models/decidim/stratified_sortitions/panel_portfolio.rb +165 -0
  38. data/app/models/decidim/stratified_sortitions/sample_import.rb +14 -0
  39. data/app/models/decidim/stratified_sortitions/sample_participant.rb +12 -0
  40. data/app/models/decidim/stratified_sortitions/sample_participant_stratum.rb +11 -0
  41. data/app/models/decidim/stratified_sortitions/stratified_sortition.rb +74 -0
  42. data/app/models/decidim/stratified_sortitions/stratum.rb +22 -0
  43. data/app/models/decidim/stratified_sortitions/substratum.rb +19 -0
  44. data/app/packs/entrypoints/decidim_stratified_sortitions.js +10 -0
  45. data/app/packs/entrypoints/decidim_stratified_sortitions_admin.js +9 -0
  46. data/app/packs/entrypoints/decidim_stratified_sortitions_admin.scss +1 -0
  47. data/app/packs/images/decidim/stratified_sortitions/icon.svg +1 -0
  48. data/app/packs/src/decidim/stratified_sortitions/application.js +3 -0
  49. data/app/packs/src/decidim/stratified_sortitions/results_tabs.js +59 -0
  50. data/app/packs/src/decidim/stratified_sortitions/stratum_fields.js +302 -0
  51. data/app/packs/src/decidim/stratified_sortitions/substratum_fields.js +165 -0
  52. data/app/packs/src/decidim/stratified_sortitions/upload_sample.js +16 -0
  53. data/app/packs/stylesheets/decidim/stratified_sortitions/admin/stratified_sortitions.scss +210 -0
  54. data/app/packs/stylesheets/decidim/stratified_sortitions/charts_pdf.scss +148 -0
  55. data/app/packs/stylesheets/stratified_sortitions.scss +56 -0
  56. data/app/permissions/decidim/stratified_sortitions/admin/permissions.rb +74 -0
  57. data/app/permissions/decidim/stratified_sortitions/permissions.rb +23 -0
  58. data/app/presenters/decidim/stratified_sortitions/admin_log/stratified_sortition_presenter.rb +35 -0
  59. data/app/queries/decidim/stratified_sortitions/filtered_stratified_sortitions.rb +39 -0
  60. data/app/serializers/decidim/stratified_sortitions/sortition_result_serializer.rb +116 -0
  61. data/app/services/decidim/stratified_sortitions/charts_pdf_generator.rb +96 -0
  62. data/app/services/decidim/stratified_sortitions/fair_sortition_service.rb +247 -0
  63. data/app/services/decidim/stratified_sortitions/leximin/constraint_builder.rb +180 -0
  64. data/app/services/decidim/stratified_sortitions/leximin/distribution_solver.rb +217 -0
  65. data/app/services/decidim/stratified_sortitions/leximin/feasibility_checker.rb +222 -0
  66. data/app/services/decidim/stratified_sortitions/leximin/panel_generator.rb +149 -0
  67. data/app/services/decidim/stratified_sortitions/leximin/panel_sampler.rb +125 -0
  68. data/app/services/decidim/stratified_sortitions/leximin_selector.rb +136 -0
  69. data/app/services/decidim/stratified_sortitions/sortition_results_exporter.rb +161 -0
  70. data/app/views/decidim/admin/components/index.html.erb +54 -0
  71. data/app/views/decidim/stratified_sortitions/admin/import_mailer/import.html.erb +22 -0
  72. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_confirm_execute_modal.html.erb +19 -0
  73. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_confirm_participants_modal.html.erb +20 -0
  74. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_form.html.erb +55 -0
  75. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_navigation_menu.html.erb +10 -0
  76. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_strata.html.erb +22 -0
  77. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_stratum.html.erb +75 -0
  78. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_substrata.html.erb +34 -0
  79. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_substratum.html.erb +54 -0
  80. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/edit.html.erb +21 -0
  81. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/execute.html.erb +155 -0
  82. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/export_charts_pdf.html.erb +38 -0
  83. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/index.html.erb +75 -0
  84. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/new.html.erb +21 -0
  85. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/upload_sample.html.erb +117 -0
  86. data/app/views/decidim/stratified_sortitions/shared/_strata_charts.html.erb +21 -0
  87. data/app/views/decidim/stratified_sortitions/stratified_sortitions/_results_count.html.erb +15 -0
  88. data/app/views/decidim/stratified_sortitions/stratified_sortitions/_stratified_sortition.html.erb +1 -0
  89. data/app/views/decidim/stratified_sortitions/stratified_sortitions/_stratified_sortitions.html.erb +13 -0
  90. data/app/views/decidim/stratified_sortitions/stratified_sortitions/_tags.html.erb +9 -0
  91. data/app/views/decidim/stratified_sortitions/stratified_sortitions/index.html.erb +22 -0
  92. data/app/views/decidim/stratified_sortitions/stratified_sortitions/index.js.erb +5 -0
  93. data/app/views/decidim/stratified_sortitions/stratified_sortitions/show.html.erb +142 -0
  94. data/app/views/layouts/decidim/admin/_sidebar_menu.html.erb +45 -0
  95. data/app/views/layouts/decidim/stratified_sortitions/charts_pdf.html.erb +14 -0
  96. data/config/assets.rb +24 -0
  97. data/config/locales/ca.yml +361 -0
  98. data/config/locales/en.yml +360 -0
  99. data/config/locales/es.yml +360 -0
  100. data/config/locales/oc.yml +2 -0
  101. data/config/routes.rb +1 -0
  102. data/db/migrate/20251023103900_create_decidim_stratified_sortitions_stratified_sortitions.rb +18 -0
  103. data/db/migrate/20251104120000_create_stratified_sortitions_strata_and_substrata.decidim_stratified_sortitions.rb +27 -0
  104. data/db/migrate/20251219120000_create_sample_imports.rb +17 -0
  105. data/db/migrate/20251219120100_create_sample_participants.rb +19 -0
  106. data/db/migrate/20251219120200_create_sample_participant_strata.rb +16 -0
  107. data/db/migrate/20251230130000_change_value_to_text_in_substrata.rb +11 -0
  108. data/db/migrate/20260102114744_add_status_to_stratified_sortitions.rb +11 -0
  109. data/db/migrate/20260109105643_allow_null_value_in_substrata.rb +10 -0
  110. data/db/migrate/20260113121726_add_position_to_strata_and_substrata.rb +8 -0
  111. data/db/migrate/20260127100000_rename_weighing_to_max_quota_percentage.rb +12 -0
  112. data/db/migrate/20260127120000_create_panel_portfolios.rb +46 -0
  113. data/db/migrate/20260518000000_add_foreign_key_to_stratified_sortitions_component.rb +10 -0
  114. data/db/migrate/20260521000000_add_cascade_to_stratified_sortitions_foreign_keys.rb +64 -0
  115. data/db/migrate/20260616000000_add_execution_error_to_stratified_sortitions.rb +11 -0
  116. data/lib/decidim/stratified_sortitions/admin.rb +10 -0
  117. data/lib/decidim/stratified_sortitions/admin_engine.rb +49 -0
  118. data/lib/decidim/stratified_sortitions/component.rb +41 -0
  119. data/lib/decidim/stratified_sortitions/engine.rb +34 -0
  120. data/lib/decidim/stratified_sortitions/seeds.rb +24 -0
  121. data/lib/decidim/stratified_sortitions/test/factories.rb +80 -0
  122. data/lib/decidim/stratified_sortitions/version.rb +13 -0
  123. data/lib/decidim/stratified_sortitions.rb +29 -0
  124. 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
@@ -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