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
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Decidim::StratifiedSortitions
2
+
3
+ ## Usage
4
+
5
+ Simply include it in your Decidim instance.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'decidim-stratified_sortitions'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ ```bash
18
+ bundle
19
+ ```
20
+
21
+ ### System Dependencies
22
+
23
+ This module uses the COIN-OR CBC solver for the LEXIMIN fair selection algorithm. You need to install CBC on your system:
24
+
25
+ **Ubuntu/Debian:**
26
+ ```bash
27
+ sudo apt install coinor-cbc coinor-libcbc-dev
28
+ ```
29
+
30
+ **macOS (Homebrew):**
31
+ ```bash
32
+ brew install cbc
33
+ ```
34
+
35
+ **Fedora/RHEL:**
36
+ ```bash
37
+ sudo dnf install coin-or-Cbc coin-or-Cbc-devel
38
+ ```
39
+
40
+ ### Import migrations
41
+
42
+ After installing the gem you must import and execute the migrations bundled with the gem:
43
+
44
+ ```bash
45
+ bundle exec rails decidim_stratified_sortitions:install:migrations
46
+ bundle exec rails db:migrate
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ Decidim::StratifiedSortitions allows fair sortitions in two ways.
52
+
53
+ 1. Generate all the panels and extract the samples at once.
54
+ 2. Execute the sortition in two phases
55
+
56
+ See `Decidim::StratifiedSortitions::FairSortitionService` for more details on how to each workflow works.
57
+
58
+ ## Running tests
59
+
60
+ Create a dummy app in your application (if not present):
61
+
62
+ ```bash
63
+ bundle exec rake test_app
64
+ cd spec/decidim_dummy_app/
65
+ bundle exec rails decidim_stratified_sortitions:install:migrations
66
+ RAILS_ENV=test bundle exec rails db:migrate
67
+ ```
68
+
69
+ And run tests:
70
+
71
+ ```bash
72
+ # Run all tests
73
+ bundle exec rspec spec
74
+
75
+ # Run only performance tests
76
+ bundle exec rspec --tag performance
77
+
78
+ # Run all tests except performance (default)
79
+ bundle exec rspec --tag ~performance
80
+
81
+ # Run also slow tests
82
+ bundle exec rspec --tag performance --tag slow
83
+ ```
84
+
85
+ ## Contributing
86
+
87
+ See [Decidim](https://github.com/decidim/decidim).
88
+
89
+ ## License
90
+
91
+ See [Decidim](https://github.com/decidim/decidim).
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/dev/common_rake"
4
+
5
+ def install_module(path)
6
+ Dir.chdir(path) do
7
+ system("bundle exec rake decidim_stratified_sortitions:install:migrations")
8
+ system("bundle exec rake db:migrate")
9
+ end
10
+ end
11
+
12
+ def seed_db(path)
13
+ Dir.chdir(path) do
14
+ system("bundle exec rake db:seed")
15
+ end
16
+ end
17
+
18
+ desc "Generates a dummy app for testing"
19
+ task test_app: "decidim:generate_external_test_app" do
20
+ ENV["RAILS_ENV"] = "test"
21
+ install_module("spec/decidim_dummy_app")
22
+ end
23
+
24
+ # rubocop disable:Rails/RakeEnvironment
25
+ desc "Generates a development app."
26
+ task development_app: :environment do
27
+ Bundler.with_original_env do
28
+ generate_decidim_app(
29
+ "development_app",
30
+ "--app_name",
31
+ "#{base_app_name}_development_app",
32
+ "--path",
33
+ "..",
34
+ "--recreate_db",
35
+ "--demo"
36
+ )
37
+ end
38
+
39
+ install_module("development_app")
40
+ seed_db("development_app")
41
+ end
42
+ # rubocop enable:Rails/RakeEnvironment
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cell/partial"
4
+
5
+ module Decidim
6
+ module StratifiedSortitions
7
+ # This cell renders the card for an instance of a StratifiedSortition
8
+ # the default size is the Large Card (:l)
9
+ class StratifiedSortitionCell < Decidim::ViewModel
10
+ def show
11
+ cell card_size, model, @options
12
+ end
13
+
14
+ private
15
+
16
+ def card_size
17
+ "decidim/stratified_sortitions/stratified_sortition_l"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cell/partial"
4
+
5
+ module Decidim
6
+ module StratifiedSortitions
7
+ # This cell renders a stratified sortition with its L-size card.
8
+ class StratifiedSortitionLCell < Decidim::CardLCell
9
+ include Decidim::StratifiedSortitions::Engine.routes.url_helpers
10
+
11
+ private
12
+
13
+ def has_state?
14
+ true
15
+ end
16
+
17
+ def metadata_cell
18
+ "decidim/stratified_sortitions/stratified_sortition_metadata"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ # This cell renders the stratified sortition metadata for l card
6
+ class StratifiedSortitionMetadataCell < Decidim::CardMetadataCell
7
+ include Decidim::StratifiedSortitions::ApplicationHelper
8
+
9
+ def initialize(*)
10
+ super
11
+
12
+ @items.prepend(*stratified_sortition_items)
13
+ end
14
+
15
+ def stratified_sortition_items
16
+ [candidates_item, status_item]
17
+ end
18
+
19
+ def candidates_item
20
+ {
21
+ text: data_with_text(model.num_candidates.to_s, t("decidim.stratified_sortitions.stratified_sortitions.stratified_sortition.num_candidates")),
22
+ icon: "group-line",
23
+ }
24
+ end
25
+
26
+ def status_item
27
+ {
28
+ text: content_tag(:span, class: "label #{status_classes}") { status_name },
29
+ }
30
+ end
31
+
32
+ def strata_item
33
+ {
34
+ text: data_with_text(model.strata.count.to_s, t("decidim.stratified_sortitions.stratified_sortitions.stratified_sortition.strata_count")),
35
+ icon: "bar-chart-box-line",
36
+ }
37
+ end
38
+
39
+ def has_badge?
40
+ false
41
+ end
42
+
43
+ def status_name
44
+ t(model.status, scope: "decidim.stratified_sortitions.stratified_sortitions.show.statuses", default: model.status)
45
+ end
46
+
47
+ def data_with_text(data, text)
48
+ "#{content_tag(:strong) { data }}#{content_tag(:span) { text }}".html_safe
49
+ end
50
+
51
+ def status_classes
52
+ case model.status
53
+ when "executed"
54
+ "success"
55
+ when "pending"
56
+ "warning"
57
+ else
58
+ "muted"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ module Admin
6
+ # Command that creates a stratified sortition
7
+ class CreateStratifiedSortition < Decidim::Command
8
+ # Public: Initializes the command.
9
+ #
10
+ # form - A form object with the params.
11
+ def initialize(form)
12
+ super()
13
+ @form = form
14
+ end
15
+
16
+ # Executes the command. Broadcasts these events:
17
+ #
18
+ # - :ok when everything is valid.
19
+ # - :invalid if the form was not valid and we could not proceed.
20
+ #
21
+ # Returns nothing.
22
+ def call
23
+ return broadcast(:invalid) if form.invalid?
24
+
25
+ transaction do
26
+ create_stratified_sortition!
27
+ create_strata(stratified_sortition)
28
+ end
29
+
30
+ broadcast(:ok, stratified_sortition)
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :form, :stratified_sortition
36
+
37
+ def create_stratified_sortition!
38
+ parsed_title = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.title, current_organization: form.current_organization).rewrite
39
+ parsed_description = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.description, current_organization: form.current_organization).rewrite
40
+ parsed_selection_criteria = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.selection_criteria, current_organization: form.current_organization).rewrite
41
+ parsed_selected_profiles_description = Decidim::ContentProcessor.parse_with_processor(:hashtag, form.selected_profiles_description,
42
+ current_organization: form.current_organization).rewrite
43
+ params = {
44
+ title: parsed_title,
45
+ description: parsed_description,
46
+ selection_criteria: parsed_selection_criteria,
47
+ selected_profiles_description: parsed_selected_profiles_description,
48
+ component: form.current_component,
49
+ num_candidates: form.num_candidates,
50
+ }
51
+
52
+ @stratified_sortition = Decidim.traceability.create!(
53
+ Decidim::StratifiedSortitions::StratifiedSortition,
54
+ form.current_user,
55
+ params,
56
+ visibility: "all"
57
+ )
58
+ end
59
+
60
+ def create_strata(stratified_sortition)
61
+ form.strata_to_persist.each do |stratum_form|
62
+ stratum = Decidim::StratifiedSortitions::Stratum.create!(
63
+ stratified_sortition:,
64
+ name: stratum_form.name,
65
+ kind: stratum_form.kind,
66
+ position: stratum_form.position
67
+ )
68
+
69
+ stratum_form.substrata_to_persist.each do |substratum_form|
70
+ Decidim::StratifiedSortitions::Substratum.create!(
71
+ stratum:,
72
+ name: substratum_form.name,
73
+ value: substratum_form.value,
74
+ range: substratum_form.range,
75
+ max_quota_percentage: substratum_form.max_quota_percentage,
76
+ position: substratum_form.position
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ module Admin
6
+ # This command deals with destroying a StratifiedSortition from the admin panel.
7
+ class DestroyStratifiedSortition < Decidim::Command
8
+ # Public: Initializes the command.
9
+ #
10
+ # page - The StratifiedSortition to be destroyed.
11
+ def initialize(stratified_sortition, current_user)
12
+ super()
13
+ @stratified_sortition = stratified_sortition
14
+ @current_user = current_user
15
+ end
16
+
17
+ # Public: Executes the command.
18
+ #
19
+ # Broadcasts :ok if it got destroyed
20
+ # Broadcasts :has_problems if not destroyed 'cause dependent
21
+ # Broadcasts :invalid if it not destroyed
22
+ def call
23
+ destroy_stratified_sortition
24
+ broadcast(:ok)
25
+ rescue ActiveRecord::DeleteRestrictionError
26
+ broadcast(:has_problems)
27
+ rescue ActiveRecord::RecordNotDestroyed
28
+ broadcast(:invalid)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :stratified_sortition, :current_user
34
+
35
+ def destroy_stratified_sortition
36
+ transaction do
37
+ Decidim.traceability.perform_action!(
38
+ "delete",
39
+ stratified_sortition,
40
+ current_user
41
+ ) do
42
+ stratified_sortition.destroy!
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,74 @@
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 duplicates a stratified sortition.
7
+ class DuplicateStratifiedSortition < Decidim::Command
8
+ include ::Decidim::AttachmentAttributesMethods
9
+
10
+ # Public: Initializes the command.
11
+ #
12
+ # stratified_sortition - the stratified sortition to update.
13
+ # current_user - the user performing the duplication.
14
+ def initialize(stratified_sortition, current_user)
15
+ super()
16
+ @stratified_sortition = stratified_sortition
17
+ @current_user = current_user
18
+ end
19
+
20
+ # Executes the command. Broadcasts these events:
21
+ #
22
+ # - :ok when everything is valid, together with the duplicated_stratified_sortition.
23
+ #
24
+ # Returns nothing.
25
+ def call
26
+ transaction do
27
+ duplicate_stratified_sortition
28
+ end
29
+
30
+ broadcast(:ok)
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :stratified_sortition, :current_user
36
+
37
+ def duplicate_stratified_sortition
38
+ @stratified_sortition = Decidim.traceability.perform_action!(
39
+ :duplicate,
40
+ stratified_sortition,
41
+ current_user,
42
+ ) do
43
+ @duplicated_stratified_sortition = stratified_sortition.dup
44
+ @duplicated_stratified_sortition.status = :pending
45
+ if @duplicated_stratified_sortition.save!
46
+ duplicate_strata
47
+ @duplicated_stratified_sortition
48
+ else
49
+ broadcast(:invalid)
50
+ end
51
+ end
52
+ end
53
+
54
+ def duplicate_strata
55
+ stratified_sortition.strata.order(:position).each do |stratum|
56
+ new_stratum = stratum.dup
57
+ new_stratum.decidim_stratified_sortition_id = @duplicated_stratified_sortition.id
58
+ new_stratum.save!
59
+
60
+ duplicate_substrata(stratum, new_stratum)
61
+ end
62
+ end
63
+
64
+ def duplicate_substrata(stratum, new_stratum)
65
+ stratum.substrata.order(:position).each do |substratum|
66
+ new_substratum = substratum.dup
67
+ new_substratum.decidim_stratified_sortitions_stratum_id = new_stratum.id
68
+ new_substratum.save!
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ module Admin
6
+ # Command that import samples of a stratified sortition
7
+ class ImportSample < Decidim::Command
8
+ # Public: Initializes the command.
9
+ #
10
+ # form - A SampleUploadForm with the file blob
11
+ # stratified_sortition - The stratified sortition to import samples to
12
+ # user - The user performing the import
13
+ def initialize(form, stratified_sortition, user)
14
+ super()
15
+ @form = form
16
+ @stratified_sortition = stratified_sortition
17
+ @user = user
18
+ end
19
+
20
+ # Executes the command. Broadcasts these events:
21
+ #
22
+ # - :ok when everything is valid.
23
+ # - :invalid if the form was not valid and we could not proceed.
24
+ #
25
+ # Returns nothing.
26
+ def call
27
+ return broadcast(:invalid) unless @form.valid?
28
+
29
+ blob = @form.file
30
+ file_content = blob.download
31
+ filename = blob.filename.to_s
32
+
33
+ Decidim::StratifiedSortitions::Admin::ImportSampleJob
34
+ .perform_later(file_content, filename, @stratified_sortition, @user)
35
+ broadcast(:ok)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ module Admin
6
+ # Command that removes uploaded samples of a stratified sortition
7
+ class RemoveUploadedSamples < Decidim::Command
8
+ # Public: Initializes the command.
9
+ #
10
+ # stratified_sortition - The stratified sortition to import samples to
11
+ # user - The user performing the import
12
+ def initialize(stratified_sortition)
13
+ super()
14
+ @stratified_sortition = stratified_sortition
15
+ end
16
+
17
+ # Executes the command. Broadcasts these events:
18
+ #
19
+ # - :ok when everything is valid.
20
+ # - :invalid if the form was not valid and we could not proceed.
21
+ #
22
+ # Returns nothing.
23
+ def call
24
+ remove_participants
25
+ remove_samples
26
+ broadcast(:ok)
27
+ rescue StandardError
28
+ broadcast(:invalid)
29
+ end
30
+
31
+ private
32
+
33
+ def remove_participants
34
+ participant_scope = Decidim::StratifiedSortitions::SampleParticipant.where(
35
+ decidim_stratified_sortition: @stratified_sortition
36
+ )
37
+
38
+ Decidim::StratifiedSortitions::SampleParticipantStratum.where(
39
+ decidim_stratified_sortitions_sample_participant_id: participant_scope.select(:id)
40
+ ).delete_all
41
+
42
+ participant_scope.delete_all
43
+ end
44
+
45
+ def remove_samples
46
+ Decidim::StratifiedSortitions::SampleImport.where(
47
+ stratified_sortition: @stratified_sortition
48
+ ).delete_all
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end