diff --git a/app/commands/decidim/superspaces/admin/create_superspace.rb b/app/commands/decidim/superspaces/admin/create_superspace.rb index 84d5c60..59b99cb 100644 --- a/app/commands/decidim/superspaces/admin/create_superspace.rb +++ b/app/commands/decidim/superspaces/admin/create_superspace.rb @@ -58,7 +58,8 @@ def create_superspace! title: form.title, description: form.description, locale: form.locale, - hero_image: form.hero_image + hero_image: form.hero_image, + show_statistics: form.show_statistics } @superspace = Decidim.traceability.create!( diff --git a/app/commands/decidim/superspaces/admin/update_superspace.rb b/app/commands/decidim/superspaces/admin/update_superspace.rb index d513ccf..a42bd6a 100644 --- a/app/commands/decidim/superspaces/admin/update_superspace.rb +++ b/app/commands/decidim/superspaces/admin/update_superspace.rb @@ -44,7 +44,8 @@ def update_superspace! title: form.title, description: form.description, hero_image: form.hero_image, - locale: form.locale + locale: form.locale, + show_statistics: form.show_statistics ) update_associations(assembly_ids, participatory_process_ids, conference_ids) end diff --git a/app/forms/decidim/superspaces/admin/superspace_form.rb b/app/forms/decidim/superspaces/admin/superspace_form.rb index 062334b..7390b35 100644 --- a/app/forms/decidim/superspaces/admin/superspace_form.rb +++ b/app/forms/decidim/superspaces/admin/superspace_form.rb @@ -14,6 +14,7 @@ class SuperspaceForm < Decidim::Form attribute :assembly_ids attribute :participatory_process_ids attribute :conference_ids + attribute :show_statistics, Boolean validates :title, translatable_presence: true validates :locale, presence: true diff --git a/app/models/decidim/superspaces/superspace.rb b/app/models/decidim/superspaces/superspace.rb index 1e53431..b101a3b 100644 --- a/app/models/decidim/superspaces/superspace.rb +++ b/app/models/decidim/superspaces/superspace.rb @@ -35,6 +35,10 @@ def conferences find_spaces_by_type("Decidim::Conference") end + def statistics + Decidim::Superspaces::SuperspaceStatsPresenter.new(self).collection + end + def self.log_presenter_class_for(_log) = Decidim::Superspaces::AdminLog::SuperspacePresenter private diff --git a/app/presenters/decidim/superspaces/superspace_stats_presenter.rb b/app/presenters/decidim/superspaces/superspace_stats_presenter.rb new file mode 100644 index 0000000..8b9e083 --- /dev/null +++ b/app/presenters/decidim/superspaces/superspace_stats_presenter.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Decidim + module Superspaces + # This class holds the logic to present superspace stats. + # It inherits from `Decidim::StatsPresenter` and overrides the methods + # needed to adapt the stats to the superspace context. + + class SuperspaceStatsPresenter < Decidim::StatsPresenter + include Decidim::IconHelper + + private + + def participatory_space = __getobj__ + + def participatory_processes + @participatory_processes ||= participatory_space.participatory_processes + participatory_space.assemblies + participatory_space.conferences + end + + def participatory_space_participants_stats + Decidim::Superspaces::StatsParticipantsCount.for(participatory_space) + end + + def participatory_space_followers_stats(_conditions) + Decidim::Superspaces::StatsFollowersCount.for(participatory_space) + end + + def published_components + @published_components ||= Component.where(participatory_space: participatory_processes).published + end + + def participatory_space_sym = :superspace + end + end +end diff --git a/app/queries/decidim/superspaces/stats_followers_count.rb b/app/queries/decidim/superspaces/stats_followers_count.rb new file mode 100644 index 0000000..cdd670b --- /dev/null +++ b/app/queries/decidim/superspaces/stats_followers_count.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Decidim + module Superspaces + class StatsFollowersCount < Decidim::Query + # This class is responsible for calculating the number of followers of a superspace. + # The number of followers in a superspace is equal to the sum of the number of followers in all the participatory spaces that belong to the superspace. + + def self.for(superspace) + return 0 unless superspace.is_a?(Decidim::Superspaces::Superspace) + + new(superspace).query + end + + def initialize(superspace) + @superspace = superspace + end + + def query + count = space_query + components_query + + data = [{ participatory_space: superspace.to_s, stat_title: "followers_count", stat_value: count }] + + data.map do |d| + [d[:participatory_space].to_sym, d[:stat_title].to_sym, d[:stat_value].to_i] + end + end + + private + + attr_reader :superspace + + def components_query + Decidim.component_manifests.sum do |component| + component.stats + .filter(tag: :followers) + .with_context(space_components) + .map { |_name, value| value } + .sum + end + end + + def space_query + Decidim.participatory_space_manifests.sum do |space| + space.stats + .filter(tag: :followers) + .with_context(participatory_space_items) + .map { |_name, value| value } + .sum + end + end + + def participatory_space_items + @participatory_space_items ||= superspace.participatory_spaces + end + + def space_components + @space_components ||= Decidim::Component.where(participatory_space: participatory_space_items).published + end + end + end +end diff --git a/app/queries/decidim/superspaces/stats_participants_count.rb b/app/queries/decidim/superspaces/stats_participants_count.rb new file mode 100644 index 0000000..081a15d --- /dev/null +++ b/app/queries/decidim/superspaces/stats_participants_count.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +module Decidim + module Superspaces + class StatsParticipantsCount < Decidim::Query + # This class is responsible for calculating the number of participants of a superspace. + # The number of participants in a superspace is equal to the sum of participants of the participatory spaces that belong to the superspace. + # If a user has participated in more than one participatory space, it will only be counted once. + + def self.for(superspace) + return 0 unless superspace.is_a?(Decidim::Superspaces::Superspace) + + new(superspace).query + end + + def initialize(superspace) + @superspace = superspace + end + + def query + participatory_process_class_name = Decidim::ParticipatoryProcess.class.name + assemblies_class_name = Decidim::Assembly.class.name + conferences_class_name = Decidim::Conference.class.name + + solution = [ + comments_query(participatory_process_class_name, participatory_space_ids), + comments_query(assemblies_class_name, assemblies_ids), + comments_query(conferences_class_name, conferences_ids), + debates_query(space_components), + debates_query(assemblies_components), + debates_query(conferences_components), + meetings_query(space_components), + meetings_query(assemblies_components), + meetings_query(conferences_components), + endorsements_query(space_components), + endorsements_query(assemblies_components), + endorsements_query(conferences_components), + project_votes_query(space_components), + project_votes_query(assemblies_components), + project_votes_query(conferences_components), + proposals_query(proposals_components), + proposals_query(assemblies_proposals_components), + proposals_query(conferences_proposals_components), + proposal_votes_query(proposals_components), + proposal_votes_query(assemblies_proposals_components), + proposal_votes_query(conferences_proposals_components), + survey_answer_query(space_components), + survey_answer_query(assemblies_components), + survey_answer_query(conferences_components) + ].flatten.uniq.count + + data = [{ participatory_space: @superspace.to_s, stat_title: "participants_count", stat_value: solution }] + + data.map do |d| + [d[:participatory_space].to_sym, d[:stat_title].to_sym, d[:stat_value].to_i] + end + end + + private + + def participatory_space_ids + @participatory_space_ids ||= @superspace.participatory_processes.map(&:id) + end + + def assemblies_ids + @assemblies_ids ||= @superspace.assemblies.map(&:id) + end + + def conferences_ids + @conferences_ids ||= @superspace.conferences.map(&:id) + end + + def comments_query(class_name, ids) + return [] unless Decidim.module_installed?(:comments) + + Decidim::Comments::Comment + .where(decidim_participatory_space_type: class_name) + .where(decidim_participatory_space_id: ids) + .pluck(:decidim_author_id) + .uniq + end + + def debates_query(components) + return [] unless Decidim.module_installed?(:debates) + + Decidim::Debates::Debate + .where( + component: components, + decidim_author_type: Decidim::UserBaseEntity.name + ) + .not_hidden + .pluck(:decidim_author_id) + .uniq + end + + def meetings_query(components) + return [] unless Decidim.module_installed?(:meetings) + + meetings = Decidim::Meetings::Meeting.where(component: components).not_hidden + registrations = Decidim::Meetings::Registration.where(decidim_meeting_id: meetings).distinct.pluck(:decidim_user_id) + organizers = meetings.where(decidim_author_type: Decidim::UserBaseEntity.name).distinct.pluck(:decidim_author_id) + + [registrations, organizers].flatten.uniq + end + + def endorsements_query(components) + Decidim::Endorsement + .where(resource: components) + .pluck(:decidim_author_id) + .uniq + end + + def proposals_query(proposals_components) + return [] unless Decidim.module_installed?(:proposals) + + Decidim::Coauthorship + .where( + coauthorable: proposals_components, + decidim_author_type: Decidim::UserBaseEntity.name + ) + .pluck(:decidim_author_id) + .uniq + end + + def proposal_votes_query(proposals_components) + return [] unless Decidim.module_installed?(:proposals) + + Decidim::Proposals::ProposalVote + .where( + proposal: proposals_components + ) + .final + .pluck(:decidim_author_id) + .uniq + end + + def project_votes_query(components) + return [] unless Decidim.module_installed?(:budgets) + + Decidim::Budgets::Order.joins(budget: [:component]) + .where(budget: { + decidim_components: { id: components.pluck(:id) } + }) + .pluck(:decidim_user_id) + .uniq + end + + def survey_answer_query(components) + Decidim::Forms::Answer.newsletter_participant_ids(components) + end + + def space_components + Decidim::Component.where(participatory_space: @superspace.participatory_processes) + end + + def assemblies_components + Decidim::Component.where(participatory_space: @superspace.assemblies) + end + + def conferences_components + Decidim::Component.where(participatory_space: @superspace.conferences) + end + + def proposals_components + @proposals_components ||= Decidim::Proposals::FilteredProposals.for(space_components).published.not_hidden + end + + def assemblies_proposals_components + @assemblies_proposals_components ||= Decidim::Proposals::FilteredProposals.for(assemblies_components).published.not_hidden + end + + def conferences_proposals_components + @conferences_proposals_components ||= Decidim::Proposals::FilteredProposals.for(conferences_components).published.not_hidden + end + end + end +end diff --git a/app/views/decidim/superspaces/admin/superspaces/_form.html.erb b/app/views/decidim/superspaces/admin/superspaces/_form.html.erb index fde4eae..d781c5f 100644 --- a/app/views/decidim/superspaces/admin/superspaces/_form.html.erb +++ b/app/views/decidim/superspaces/admin/superspaces/_form.html.erb @@ -76,6 +76,9 @@ +
+ <%= form.check_box :show_statistics %> +
diff --git a/app/views/decidim/superspaces/superspaces/show.html.erb b/app/views/decidim/superspaces/superspaces/show.html.erb index bd5c0bc..bedb4b0 100644 --- a/app/views/decidim/superspaces/superspaces/show.html.erb +++ b/app/views/decidim/superspaces/superspaces/show.html.erb @@ -41,6 +41,13 @@ <% end %> <% end %> + + <% if superspace.show_statistics %> +
+

<%= t("statistics", scope: "decidim.superspaces.superspaces.show") %>

+ <%= cell("decidim/statistics",superspace.statistics) %> +
+ <% end %> <% end %> <%= render partial: "language_selector" %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 38fefdc..a48eb7b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -58,3 +58,4 @@ en: show: assemblies: Assemblies participatory_processes: Participatory Processes + statistics: Statistics diff --git a/config/locales/es.yml b/config/locales/es.yml index 4279cfa..1ce7f24 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -57,3 +57,4 @@ es: show: assemblies: Asambleas participatory_processes: Procesos participativos + statistics: Estadísticas diff --git a/db/migrate/20250218123528_add_show_statistics_to_decidim_superspaces_superspaces.rb b/db/migrate/20250218123528_add_show_statistics_to_decidim_superspaces_superspaces.rb new file mode 100644 index 0000000..7e44a7e --- /dev/null +++ b/db/migrate/20250218123528_add_show_statistics_to_decidim_superspaces_superspaces.rb @@ -0,0 +1,5 @@ +class AddShowStatisticsToDecidimSuperspacesSuperspaces < ActiveRecord::Migration[6.1] + def change + add_column :decidim_superspaces_superspaces, :show_statistics, :boolean + end +end diff --git a/spec/commands/decidim/superspaces/admin/create_superspace_spec.rb b/spec/commands/decidim/superspaces/admin/create_superspace_spec.rb index 8cedd20..b97fd44 100644 --- a/spec/commands/decidim/superspaces/admin/create_superspace_spec.rb +++ b/spec/commands/decidim/superspaces/admin/create_superspace_spec.rb @@ -18,6 +18,7 @@ module Admin let(:participatory_process_ids) { nil } let(:conference_ids) { nil } let(:invalid) { false } + let(:show_statistics) { false } let(:form) do double( invalid?: invalid, @@ -28,7 +29,8 @@ module Admin locale:, assembly_ids:, participatory_process_ids:, - conference_ids: + conference_ids:, + show_statistics: ) end diff --git a/spec/commands/decidim/superspaces/admin/update_superspace_spec.rb b/spec/commands/decidim/superspaces/admin/update_superspace_spec.rb index e12eb35..8f9f4ac 100644 --- a/spec/commands/decidim/superspaces/admin/update_superspace_spec.rb +++ b/spec/commands/decidim/superspaces/admin/update_superspace_spec.rb @@ -19,6 +19,7 @@ module Admin let(:assembly_ids) { nil } let(:participatory_process_ids) { nil } let(:conference_ids) { nil } + let(:show_statistics) { false } let(:form) do double( invalid?: invalid, @@ -29,7 +30,8 @@ module Admin locale:, assembly_ids:, participatory_process_ids:, - conference_ids: + conference_ids:, + show_statistics: ) end @@ -67,7 +69,7 @@ module Admin it "traces the action", :versioning do expect(Decidim.traceability) .to receive(:update!) - .with(superspace, current_user, { title: { en: title }, description: { en: description }, locale:, hero_image: }) + .with(superspace, current_user, { title: { en: title }, description: { en: description }, locale:, hero_image:, show_statistics: false }) .and_call_original expect { subject.call }.to change(Decidim::ActionLog, :count) diff --git a/spec/system/superspaces_spec.rb b/spec/system/superspaces_spec.rb index 04ebef8..fd74648 100644 --- a/spec/system/superspaces_spec.rb +++ b/spec/system/superspaces_spec.rb @@ -5,7 +5,7 @@ describe "User sees superspaces" do let!(:organization) { create(:organization) } let!(:user) { create(:user, :admin, :confirmed, organization:) } - let!(:superspaces) { create_list(:superspace, 10, organization:) } + let!(:superspaces) { create_list(:superspace, 10, organization:, show_statistics: true) } let!(:assemblies) { create_list(:assembly, 10, organization:) } let!(:participatory_processes) { create_list(:participatory_process, 10, organization:) } let!(:assembly) { assemblies.first } @@ -42,6 +42,9 @@ within all("#assemblies-grid").last do expect(page).to have_content("Assemblies") end + within all("#statistics-grid").last do + expect(page).to have_content("Statistics") + end end end @@ -53,6 +56,14 @@ it "doesn't render grid if the superspace is empty" do expect(page).to have_no_selector("#processes-grid") expect(page).to have_no_selector("#assemblies-grid") + expect(page).to have_css("#statistics-grid") + end + + it "doesn't render statistics if the superspace show statistics is false" do + superspaces.last.update!(show_statistics: false) + visit decidim_superspaces.superspace_path(superspaces.last) + + expect(page).to have_no_selector("#statistics-grid") end end end