Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 0 additions & 28 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,7 @@ GEM
logger
faraday-net_http (3.4.1)
net-http (>= 0.5.0)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl)
file_validators (3.0.0)
activemodel (>= 3.2)
mime-types (>= 1.0)
Expand Down Expand Up @@ -507,22 +500,8 @@ GEM
net-smtp (0.3.4)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-musl)
racc (~> 1.4)
oauth (1.1.2)
oauth-tty (~> 1.0, >= 1.0.6)
snaky_hash (~> 2.0)
Expand Down Expand Up @@ -821,14 +800,7 @@ GEM
zeitwerk (2.7.3)

PLATFORMS
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl

DEPENDENCIES
bootsnap
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

require "active_support/concern"

module Decidim
module StratifiedSortitions
# Shared logic for computing strata and candidates chart data.
# Used by both admin and public controllers to avoid duplication.
module StrataChartsData
extend ActiveSupport::Concern

private

def strata_data(stratified_sortition)
stratified_sortition.strata.map do |stratum|
chart_data = stratum.substrata.map do |substratum|
quota_value = substratum.max_quota_percentage.present? ? substratum.max_quota_percentage.to_f : 0.0
label_with_percentage = "#{translated_attribute(substratum.name)} (#{quota_value}%)"
[label_with_percentage, quota_value]
end
chart_data = chart_data.reject { |_name, value| value.zero? }
{
stratum:,
chart_data:,
}
end
end

def candidates_data(stratified_sortition)
participant_ids = stratified_sortition.sample_participants.pluck(:id)
participants_distribution_data(stratified_sortition, participant_ids)
end

def results_data(stratified_sortition)
return stratified_sortition.strata.map { |stratum| { stratum:, chart_data: [] } } unless stratified_sortition.panel_portfolio&.sampled?

selected_ids = stratified_sortition.panel_portfolio.selected_panel
participants_distribution_data(stratified_sortition, selected_ids)
end

def participants_distribution_data(stratified_sortition, participant_ids)
sample_candidates_stratum = fetch_sample_candidates_stratum(participant_ids)
by_stratum = group_by_stratum(sample_candidates_stratum)
by_stratum_and_substratum = group_by_stratum_and_substratum(sample_candidates_stratum)

stratified_sortition.strata.map do |stratum|
build_stratum_chart(stratum, by_stratum, by_stratum_and_substratum)
end
end

def fetch_sample_candidates_stratum(sample_candidates_ids)
Decidim::StratifiedSortitions::SampleParticipantStratum
.where(decidim_stratified_sortitions_sample_participant_id: sample_candidates_ids)
.select(:decidim_stratified_sortitions_sample_participant_id,
:decidim_stratified_sortitions_stratum_id,
:decidim_stratified_sortitions_substratum_id)
.distinct
.to_a
end

def group_by_stratum(sample_candidates_stratum)
sample_candidates_stratum.group_by(&:decidim_stratified_sortitions_stratum_id)
end

def group_by_stratum_and_substratum(sample_candidates_stratum)
sample_candidates_stratum.group_by do |s|
[s.decidim_stratified_sortitions_stratum_id, s.decidim_stratified_sortitions_substratum_id]
end
end

def build_stratum_chart(stratum, by_stratum, by_stratum_and_substratum)
substrata = stratum.substrata
total = by_stratum[stratum.id]&.map(&:decidim_stratified_sortitions_sample_participant_id)&.uniq&.count || 0
chart_data = substrata.map do |substratum|
build_substratum_chart_row(stratum, substratum, by_stratum_and_substratum, total)
end
chart_data = chart_data.reject { |_name, value| value.zero? }
{ stratum:, chart_data: }
end

def build_substratum_chart_row(stratum, substratum, by_stratum_and_substratum, total)
ids = (by_stratum_and_substratum[[stratum.id, substratum.id]] || [])
.map(&:decidim_stratified_sortitions_sample_participant_id).uniq
count = ids.count
percentage = total.positive? ? ((count.to_f / total) * 100).round(1) : 0.0
label = "#{translated_attribute(substratum.name)} (#{percentage}%)"
[label, count]
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Admin
#
class StratifiedSortitionsController < Decidim::StratifiedSortitions::Admin::ApplicationController
include Decidim::ApplicationHelper
include StrataChartsData

helper StratifiedSortitions::ApplicationHelper
helper Decidim::PaginateHelper
Expand Down Expand Up @@ -116,22 +117,75 @@ def execute
unless stratified_sortition.can_execute?
redirect_to edit_stratified_sortition_path(stratified_sortition),
flash: { warning: t("stratified_sortitions.execute.empty_sample_participants", scope: "decidim.stratified_sortitions.admin") }
return
end

@stratified_sortition = stratified_sortition
@portfolio = @stratified_sortition.panel_portfolio
@strata = @stratified_sortition.strata.order(:position)
@selected_participants = if @portfolio&.sampled?
SampleParticipant
.where(id: @portfolio.selected_panel)
.includes(sample_participant_strata: [:decidim_stratified_sortitions_stratum, :decidim_stratified_sortitions_substratum])
.order(:id)
.to_a
else
[]
end
@strata_data = strata_data(@stratified_sortition)
@candidates_data = candidates_data(@stratified_sortition)
@results_data = results_data(@stratified_sortition)
end

def execute_stratified_sortition
@result = FairSortitionService.new(stratified_sortition).call
if @result.success?
stratified_sortition.update!(status: "executed")
csv_data = generate_sortition_csv(@result)
send_data csv_data,
filename: "sortition_results_#{stratified_sortition.id}_#{Time.current.strftime("%Y%m%d_%H%M%S")}.csv",
type: "text/csv",
disposition: "attachment"
flash[:notice] = I18n.t("stratified_sortitions.execute.success", scope: "decidim.stratified_sortitions.admin")
else
flash[:error] = @result.error
end
redirect_to execute_stratified_sortition_path(stratified_sortition)
end

def export_charts_pdf
portfolio = stratified_sortition.panel_portfolio

unless portfolio&.sampled?
flash[:error] = I18n.t("stratified_sortitions.export_results.no_results", scope: "decidim.stratified_sortitions.admin")
redirect_to execute_stratified_sortition_path(stratified_sortition)
return
end

generator = ChartsPdfGenerator.new(
stratified_sortition,
strata_data(stratified_sortition),
candidates_data(stratified_sortition),
results_data(stratified_sortition),
locale: I18n.locale
)

filename = "sortition_charts_#{stratified_sortition.id}_#{Time.current.strftime("%Y%m%d_%H%M%S")}.pdf"
send_data generator.generate,
filename:,
type: "application/pdf",
disposition: "attachment"
end

def export_results
format = params[:format]&.downcase || "csv"
portfolio = stratified_sortition.panel_portfolio

unless portfolio&.sampled?
flash[:error] = I18n.t("stratified_sortitions.export_results.no_results", scope: "decidim.stratified_sortitions.admin")
redirect_to execute_stratified_sortition_path(stratified_sortition)
return
end

SortitionResultsExportJob.perform_later(current_user, stratified_sortition, format)

flash[:notice] = I18n.t("decidim.admin.exports.notice")
redirect_to execute_stratified_sortition_path(stratified_sortition)
end

private
Expand Down Expand Up @@ -159,115 +213,6 @@ def blank_stratum
def blank_substratum(stratum_form)
Decidim::StratifiedSortitions::Admin::SubstratumForm.new(stratum: stratum_form.model)
end

def strata_data(stratified_sortition)
stratified_sortition.strata.map do |stratum|
chart_data = stratum.substrata.map do |substratum|
quota_value = substratum.max_quota_percentage.present? ? substratum.max_quota_percentage.to_f : 0.0
label_with_percentage = "#{translated_attribute(substratum.name)} (#{quota_value}%)"
[label_with_percentage, quota_value]
end
chart_data = chart_data.reject { |_name, value| value.zero? }
{
stratum:,
chart_data:,
}
end
end

def candidates_data(stratified_sortition)
sample_candidates_ids = stratified_sortition.sample_participants.pluck(:id)
sample_candidates_stratum = fetch_sample_candidates_stratum(sample_candidates_ids)
by_stratum = group_by_stratum(sample_candidates_stratum)
by_stratum_and_substratum = group_by_stratum_and_substratum(sample_candidates_stratum)

stratified_sortition.strata.map do |stratum|
build_stratum_chart(stratum, by_stratum, by_stratum_and_substratum)
end
end

def fetch_sample_candidates_stratum(sample_candidates_ids)
Decidim::StratifiedSortitions::SampleParticipantStratum
.where(decidim_stratified_sortitions_sample_participant_id: sample_candidates_ids)
.select(:decidim_stratified_sortitions_sample_participant_id,
:decidim_stratified_sortitions_stratum_id,
:decidim_stratified_sortitions_substratum_id)
.distinct
.to_a
end

def group_by_stratum(sample_candidates_stratum)
sample_candidates_stratum.group_by(&:decidim_stratified_sortitions_stratum_id)
end

def group_by_stratum_and_substratum(sample_candidates_stratum)
sample_candidates_stratum.group_by do |s|
[s.decidim_stratified_sortitions_stratum_id, s.decidim_stratified_sortitions_substratum_id]
end
end

def build_stratum_chart(stratum, by_stratum, by_stratum_and_substratum)
substrata = stratum.substrata
total = by_stratum[stratum.id]&.map(&:decidim_stratified_sortitions_sample_participant_id)&.uniq&.count || 0
chart_data = substrata.map do |substratum|
build_substratum_chart_row(stratum, substratum, by_stratum_and_substratum, total)
end
chart_data = chart_data.reject { |_name, value| value.zero? }
{ stratum:, chart_data: }
end

def build_substratum_chart_row(stratum, substratum, by_stratum_and_substratum, total)
ids = (by_stratum_and_substratum[[stratum.id, substratum.id]] || [])
.map(&:decidim_stratified_sortitions_sample_participant_id).uniq
count = ids.count
percentage = total.positive? ? ((count.to_f / total) * 100).round(1) : 0.0
label = "#{translated_attribute(substratum.name)} (#{percentage}%)"
[label, count]
end

# NOTE: This is a temporary export for see sortition results
def generate_sortition_csv(result)
require "csv"

CSV.generate(headers: true, col_sep: ";") do |csv|
csv << ["# INFORMACIÓ DEL SORTEIG"]
csv << ["Algorisme", result.selection_log[:algorithm]]
csv << ["Versió", result.selection_log[:version]]
csv << ["ID Sorteig", result.selection_log[:stratified_sortition_id]]
csv << ["Generat el", result.selection_log[:generated_at]]
csv << ["Temps de generació (s)", result.selection_log[:generation_time_seconds]]
csv << ["Nombre de panels", result.selection_log[:num_panels]]
csv << ["Iteracions", result.selection_log[:num_iterations]]
csv << ["Convergència", result.selection_log[:convergence_achieved]]
csv << ["Seleccionat el", result.selection_log[:selected_at]]
csv << ["Índex panel seleccionat", result.selection_log[:selected_panel_index]]
csv << ["Seed de verificació", result.selection_log[:verification_seed]]
csv << ["Valor aleatori", result.selection_log[:random_value_used]]
csv << ["Probabilitat panel seleccionat", result.selection_log[:selected_panel_probability]]

if result.selection_log[:fairness_metrics].present?
csv << [""]
csv << ["# MÈTRIQUES D'EQUITAT"]
result.selection_log[:fairness_metrics].each do |key, value|
csv << [key.to_s.humanize, value]
end
end

csv << [""]
csv << ["# PARTICIPANTS SELECCIONATS"]
csv << %w[ID Dada_Personal_1(Identificador únic) Dada_Personal_2 Dada_Personal_3 Dada_Personal_4]

result.selected_participants.each do |participant|
csv << [
participant.id,
participant.personal_data_1,
participant.personal_data_2,
participant.personal_data_3,
participant.personal_data_4,
]
end
end
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Decidim
module StratifiedSortitions
# A dummy controller used to render PDF views outside the request cycle.
# Following the same pattern as Decidim core (decidim-forms).
# rubocop:disable Rails/ApplicationController
class ChartsPdfControllerHelper < ActionController::Base
# rubocop:enable Rails/ApplicationController
helper Decidim::TranslationsHelper
helper Decidim::StratifiedSortitions::ChartsPdfHelper

# Ensure the engine's views (templates + layouts) are found.
append_view_path Decidim::StratifiedSortitions::Engine.root.join("app", "views")
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class StratifiedSortitionsController < Decidim::StratifiedSortitions::Applicatio
include FilterResource
include Paginable
include OrderableStratifiedSortitions
include StrataChartsData

helper Decidim::CheckBoxesTreeHelper
helper Decidim::PaginateHelper
Expand All @@ -23,6 +24,12 @@ def index

def show
raise ActionController::RoutingError, "Not Found" unless stratified_sortition

if current_component.settings.publish_sortitions
@strata_data = strata_data(stratified_sortition) if stratified_sortition.strata_and_substrata_configured?
@candidates_data = candidates_data(stratified_sortition) if stratified_sortition.can_execute?
@results_data = results_data(stratified_sortition) if stratified_sortition.panel_portfolio&.sampled?
end
end

private
Expand Down
Loading
Loading