diff --git a/README.md b/README.md
index 850e2f19..f405e774 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ This is the open-source repository for "participa", based on [Decidim](https://g
- `Decidim::Regulations`, adds Regulations, a new type of Participatory process.
- `Decidim::Admin::Extended`, customize admin menu adding custom configurations.
- `Decidim::Recaptcha`, use recaptcha instead invisible captcha.
+- Proposals admin index: adds an "Attachments" column with ascending/descending sort support (see `app/overrides/decidim/admin/proposals_index_attachments_th.rb`, `app/overrides/decidim/admin/proposals_tr_attachments_td.rb`, and `app/decorators/decidim/proposals/proposal_decorator.rb`).
## Deploying the app
diff --git a/app/decorators/decidim/proposals/proposal_decorator.rb b/app/decorators/decidim/proposals/proposal_decorator.rb
new file mode 100644
index 00000000..9c1ebabf
--- /dev/null
+++ b/app/decorators/decidim/proposals/proposal_decorator.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Decidim::Proposals::ProposalDecorator
+ def self.decorate
+ Decidim::Proposals::Proposal.class_eval do
+ scope :sort_by_attachments_count_asc, lambda {
+ order(Arel.sql(
+ "(SELECT COUNT(*) FROM decidim_attachments " \
+ "WHERE decidim_attachments.attached_to_type = 'Decidim::Proposals::Proposal' " \
+ "AND decidim_attachments.attached_to_id = decidim_proposals_proposals.id) ASC"
+ ))
+ }
+
+ scope :sort_by_attachments_count_desc, lambda {
+ order(Arel.sql(
+ "(SELECT COUNT(*) FROM decidim_attachments " \
+ "WHERE decidim_attachments.attached_to_type = 'Decidim::Proposals::Proposal' " \
+ "AND decidim_attachments.attached_to_id = decidim_proposals_proposals.id) DESC"
+ ))
+ }
+ end
+ end
+end
+
+Decidim::Proposals::ProposalDecorator.decorate
diff --git a/app/overrides/decidim/admin/proposals_index_attachments_th.rb b/app/overrides/decidim/admin/proposals_index_attachments_th.rb
new file mode 100644
index 00000000..bc386aab
--- /dev/null
+++ b/app/overrides/decidim/admin/proposals_index_attachments_th.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# Inserts the "Attachments" column header after the "Valuators"
.
+Deface::Override.new(
+ virtual_path: "decidim/proposals/admin/proposals/index",
+ name: "proposals_admin_index_attachments_th",
+ insert_after: "thead th:nth-last-of-type(3)",
+ text: <<~EOHTML
+ <% if current_component.settings.attachments_allowed? %>
+ |
+ <%= sort_link(query, :attachments_count, t("models.proposal.fields.attachments", scope: "decidim.proposals")) %>
+ |
+ <% end %>
+ EOHTML
+)
diff --git a/app/overrides/decidim/admin/proposals_tr_attachments_td.rb b/app/overrides/decidim/admin/proposals_tr_attachments_td.rb
new file mode 100644
index 00000000..f37b233f
--- /dev/null
+++ b/app/overrides/decidim/admin/proposals_tr_attachments_td.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# Inserts the attachments count after the "Valuators" | .
+Deface::Override.new(
+ virtual_path: "decidim/proposals/admin/proposals/_proposal-tr",
+ name: "proposals_admin_tr_attachments_td",
+ insert_after: "td.valuators-count",
+ text: <<~EOHTML
+ <% if current_component.settings.attachments_allowed? %>
+ |
+ <%= proposal.attachments.size %>
+ |
+ <% end %>
+ EOHTML
+)
diff --git a/config/locales/ca_proposals.yml b/config/locales/ca_proposals.yml
index 224a3a2e..65310948 100644
--- a/config/locales/ca_proposals.yml
+++ b/config/locales/ca_proposals.yml
@@ -43,6 +43,10 @@ ca:
external_author/name: Nom de l'autor de la proposta extern a la plataforma Decidim
meeting_url: Url de la trobada a la plataforma Decidim
+ models:
+ proposal:
+ fields:
+ attachments: Adjunts
proposals:
filters:
amendment_type: Tipus
diff --git a/config/locales/en_proposals.yml b/config/locales/en_proposals.yml
index 55a07045..4be41cdd 100644
--- a/config/locales/en_proposals.yml
+++ b/config/locales/en_proposals.yml
@@ -27,6 +27,10 @@ en:
external_author/name: Proposal author name outside of the Decidim platform
meeting_url: Meeting url on the Decidim platform
+ models:
+ proposal:
+ fields:
+ attachments: Attachments
proposals:
show:
proposal_in_evaluation_reason: 'This proposal is under evaluation because:'
diff --git a/config/locales/es_proposals.yml b/config/locales/es_proposals.yml
index ec41477e..f65d7326 100644
--- a/config/locales/es_proposals.yml
+++ b/config/locales/es_proposals.yml
@@ -27,6 +27,10 @@ es:
external_author/name: Nombre del autor de la propuesta externo a la plataforma Decidim
meeting_url: Url del encuentro en la plataforma Decidim
+ models:
+ proposal:
+ fields:
+ attachments: Adjuntos
proposals:
show:
proposal_in_evaluation_reason: 'Esta propuesta está en evaluación porque:'
diff --git a/config/locales/oc_proposals.yml b/config/locales/oc_proposals.yml
index 508de74e..ae5b4533 100644
--- a/config/locales/oc_proposals.yml
+++ b/config/locales/oc_proposals.yml
@@ -379,6 +379,8 @@ oc:
models:
proposal:
+ fields:
+ attachments: Adjunts
name: Proposta
participatory_texts:
bulk-actions:
diff --git a/docs/HOW_TO_UPGRADE.md b/docs/HOW_TO_UPGRADE.md
index e8ec6feb..572c5e07 100644
--- a/docs/HOW_TO_UPGRADE.md
+++ b/docs/HOW_TO_UPGRADE.md
@@ -190,6 +190,10 @@ These are custom modules and this is what you have to keep in mind when updating
* Override to export proposal emails and names from authors
* probably removable from Decidim v0.28 (remember remove test too)
+ * `app/decorators/decidim/proposals/proposal_decorator.rb`
+ * Adds `sort_by_attachments_count_asc` and `sort_by_attachments_count_desc` scopes to `Decidim::Proposals::Proposal`
+ * Ransack picks up these scopes automatically via the `sort_by_X_asc/desc` naming convention when the field is not a DB column
+
* `lib/decidim/has_private_users.rb`
* Override to allow private space users to acces public view
* Could not use a decorator so the whole class has been copied
@@ -203,6 +207,10 @@ These are custom modules and this is what you have to keep in mind when updating
* `config/locales/`
* Overrides some translations keys
* Fixes some Decidim translations in `*_fix.yml` files
+ * `app/overrides/decidim/admin/proposals_index_attachments_th.rb (decidim-proposals)`
+ * Inserts an "Attachments" column header in the proposals admin index, with ascending/descending sort support via `sort_link`
+ * `app/overrides/decidim/admin/proposals_tr_attachments_td.rb (decidim-proposals)`
+ * Inserts the attachments count cell in each proposal row of the admin index
##### Custom files:
diff --git a/spec/decorators/decidim/proposals/proposal_decorator_spec.rb b/spec/decorators/decidim/proposals/proposal_decorator_spec.rb
new file mode 100644
index 00000000..d500c2d5
--- /dev/null
+++ b/spec/decorators/decidim/proposals/proposal_decorator_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe Decidim::Proposals::Proposal do
+ let(:organization) { create(:organization) }
+ let(:participatory_process) { create(:participatory_process, organization:) }
+ let(:component) { create(:component, manifest_name: :proposals, participatory_space: participatory_process) }
+
+ let!(:proposal_no_attachments) { create(:proposal, component:) }
+ let!(:proposal_one_attachment) { create(:proposal, component:) }
+ let!(:proposal_two_attachments) { create(:proposal, component:) }
+
+ before do
+ create(:attachment, attached_to: proposal_one_attachment)
+ create(:attachment, attached_to: proposal_two_attachments)
+ create(:attachment, attached_to: proposal_two_attachments)
+ end
+
+ let(:scoped_proposals) { described_class.where(component:) }
+
+ describe ".sort_by_attachments_count_asc" do
+ subject { scoped_proposals.sort_by_attachments_count_asc.to_a }
+
+ it "orders proposals by ascending attachment count" do
+ expect(subject.index(proposal_no_attachments)).to be < subject.index(proposal_one_attachment)
+ expect(subject.index(proposal_one_attachment)).to be < subject.index(proposal_two_attachments)
+ end
+ end
+
+ describe ".sort_by_attachments_count_desc" do
+ subject { scoped_proposals.sort_by_attachments_count_desc.to_a }
+
+ it "orders proposals by descending attachment count" do
+ expect(subject.index(proposal_two_attachments)).to be < subject.index(proposal_one_attachment)
+ expect(subject.index(proposal_one_attachment)).to be < subject.index(proposal_no_attachments)
+ end
+ end
+end
diff --git a/spec/system/proposals_admin_attachments_column_spec.rb b/spec/system/proposals_admin_attachments_column_spec.rb
new file mode 100644
index 00000000..ad72581c
--- /dev/null
+++ b/spec/system/proposals_admin_attachments_column_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe "Admin proposals attachments column", type: :system do
+ let(:manifest_name) { "proposals" }
+
+ include_context "when managing a component as an admin"
+
+ let!(:proposal) { create(:proposal, component:, users: [user]) }
+
+ context "when the component has attachments allowed" do
+ let!(:component) do
+ create(:component,
+ manifest_name: :proposals,
+ participatory_space: participatory_process,
+ settings: { "attachments_allowed" => true })
+ end
+
+ before { visit_component_admin }
+
+ it "shows the attachments column header" do
+ expect(page).to have_css("thead th", text: I18n.t("decidim.proposals.models.proposal.fields.attachments"))
+ end
+
+ it "shows the attachment count for each proposal row" do
+ within("tbody tr", match: :first) do
+ expect(page).to have_content(proposal.attachments.size.to_s)
+ end
+ end
+ end
+
+ context "when the component does not have attachments allowed" do
+ before { visit_component_admin }
+
+ it "does not show the attachments column header" do
+ expect(page).not_to have_css("thead th", text: I18n.t("decidim.proposals.models.proposal.fields.attachments"))
+ end
+ end
+end