From 290fbd9db515c3fcb448341e801fa4aba1364da3 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 2 Feb 2026 14:32:08 +0100 Subject: [PATCH 1/8] feat: add boolean column enable_dataspace to organization --- ...129143027_add_enable_dataspace_to_organizations.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 db/migrate/20260129143027_add_enable_dataspace_to_organizations.rb diff --git a/db/migrate/20260129143027_add_enable_dataspace_to_organizations.rb b/db/migrate/20260129143027_add_enable_dataspace_to_organizations.rb new file mode 100644 index 0000000..f3ba260 --- /dev/null +++ b/db/migrate/20260129143027_add_enable_dataspace_to_organizations.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddEnableDataspaceToOrganizations < ActiveRecord::Migration[7.0] + def up + add_column :decidim_organizations, :enable_dataspace, :boolean, default: false + end + + def down + remove_column :decidim_organizations, :enable_dataspace + end +end From 59028c9b6d0bb60860ebf575a6883266902c1b41 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 2 Feb 2026 14:34:11 +0100 Subject: [PATCH 2/8] feat: addenable dataspace in /system for organization --- .../system/organizations/edit.html.erb | 53 ++++++++++++++ .../decidim/system/organizations/new.html.erb | 72 +++++++++++++++++++ lib/decidim/dataspace/engine.rb | 3 + .../system/create_organization_extends.rb | 34 +++++++++ .../system/update_organization_extends.rb | 26 +++++++ .../system/base_organization_form_extends.rb | 11 +++ 6 files changed, 199 insertions(+) create mode 100644 app/views/decidim/system/organizations/edit.html.erb create mode 100644 app/views/decidim/system/organizations/new.html.erb create mode 100644 lib/extends/commands/decidim/system/create_organization_extends.rb create mode 100644 lib/extends/commands/decidim/system/update_organization_extends.rb create mode 100644 lib/extends/forms/decidim/system/base_organization_form_extends.rb diff --git a/app/views/decidim/system/organizations/edit.html.erb b/app/views/decidim/system/organizations/edit.html.erb new file mode 100644 index 0000000..cc699d2 --- /dev/null +++ b/app/views/decidim/system/organizations/edit.html.erb @@ -0,0 +1,53 @@ +<% add_decidim_page_title(t(".title")) %> + +<% provide :title do %> +

<%= t ".title" %>

+<% end %> + +<%= decidim_form_for(@form, url: organization_path(@organization)) do |f| %> +
+
+ <%= f.translated :text_field, :name, autofocus: true %> +
+ + <%= f.text_field :host %> + + <%= f.text_area :secondary_hosts, help_text: t(".secondary_hosts_hint") %> + + <%= field_set_tag f.label(:force_authentication, nil, for: nil) do %> + <%= f.check_box :force_users_to_authenticate_before_access_organization, label_options: { class: "form__wrapper-checkbox-label" } %> + <% end %> + + <%= field_set_tag f.label(:enable_dataspace, nil, for: nil) do %> + <%= f.check_box :enable_dataspace, label_options: { class: "form__wrapper-checkbox-label" } %> + <% end %> + + <%= field_set_tag f.label(:users_registration_mode, nil, for: nil) do %> + <%= f.collection_radio_buttons :users_registration_mode, + Decidim::Organization.users_registration_modes, + :first, + ->(mode) { t("decidim.system.organizations.users_registration_mode.#{mode.first}") } do |builder| + builder.label(for: nil, class: "form__wrapper-checkbox-label") { builder.radio_button(id: nil) + builder.text } + end %> + <% end %> + + <%= field_set_tag f.label(:available_authorizations, nil, for: nil) do %> + <%= f.collection_check_boxes :available_authorizations, Decidim.authorization_workflows, :name, :description do |builder| + builder.label(for: nil, class: "form__wrapper-checkbox-label") { builder.check_box(id: nil) + builder.text } + end %> + <% end %> + + <%= render partial: "advanced_settings", locals: { f: } %> +
+ +
+ <% if @organization&.users&.first&.invitation_pending? %> + <%= link_to t(".resend_invitation"), + resend_invitation_organization_path(@organization), + method: :post, + class: "button button__sm md:button__lg button__transparent-secondary", + data: { confirm: t(".confirm_resend_invitation") } %> + <% end %> + <%= f.submit t("decidim.system.actions.save"), class: "button button__sm md:button__lg button__primary" %> +
+<% end %> diff --git a/app/views/decidim/system/organizations/new.html.erb b/app/views/decidim/system/organizations/new.html.erb new file mode 100644 index 0000000..3fbaecf --- /dev/null +++ b/app/views/decidim/system/organizations/new.html.erb @@ -0,0 +1,72 @@ +<% add_decidim_page_title(t(".title")) %> + +<% provide :title do %> +

<%= t ".title" %>

+<% end %> + +<%= decidim_form_for(@form) do |f| %> +
+ <%= f.text_field :name, autofocus: true %> + + <%= f.text_field :reference_prefix, help_text: t(".reference_prefix_hint") %> + + <%= f.text_field :host %> + + <%= f.text_area :secondary_hosts, help_text: t(".secondary_hosts_hint") %> + + <%= f.text_field :organization_admin_name %> + + <%= f.email_field :organization_admin_email, help_text: t(".organization_admin_email_hint") %> + + <%= f.fields_for :locales do |fields| %> + <%= f.label :organization_locales, "", class: @form.respond_to?(:errors) && @form.errors[:default_locale].present? ? "is-invalid-label" : "" %> + + + + + + + + + + <% localized_locales.each do |locale| %> + + + + + + <% end %> + +
<%= t(".locale") %><%= t(".enabled") %> <%= f.error_for(:available_locales) %><%= t(".default") %><%= f.error_for(:default_locale) %>
<%= locale.name %><%= check_box_tag "organization[available_locales][#{locale.id}]", locale.id, @form.available_locales.include?(locale.id), class: "!m-0" %><%= radio_button_tag "organization[default_locale]", locale.id, @form.default_locale == locale.id, class: "!m-0" %>
+ <% end %> + + <%= field_set_tag f.label(:force_authentication, nil, for: nil) do %> + <%= f.check_box :force_users_to_authenticate_before_access_organization, label_options: { class: "form__wrapper-checkbox-label" } %> + <% end %> + + <%= field_set_tag f.label(:enable_dataspace, nil, for: nil) do %> + <%= f.check_box :enable_dataspace, label_options: { class: "form__wrapper-checkbox-label" } %> + <% end %> + + <%= field_set_tag f.label(:users_registration_mode, nil, for: nil) do %> + <%= f.collection_radio_buttons :users_registration_mode, + Decidim::Organization.users_registration_modes, + :first, + ->(mode) { t("decidim.system.organizations.users_registration_mode.#{mode.first}") } do |builder| + builder.label(for: nil, class: "form__wrapper-checkbox-label") { builder.radio_button(id: nil) + builder.text } + end %> + <% end %> + + <%= field_set_tag f.label(:available_authorizations, nil, for: nil) do %> + <%= f.collection_check_boxes :available_authorizations, Decidim.authorization_workflows, :name, :description do |builder| + builder.label(for: nil, class: "form__wrapper-checkbox-label") { builder.check_box(id: nil) + builder.text } + end %> + <% end %> + + <%= render partial: "advanced_settings", locals: { f: } %> +
+ +
+ <%= f.submit t("decidim.system.models.organization.actions.save_and_invite"), class: "button button__sm md:button__lg button__primary" %> +
+<% end %> diff --git a/lib/decidim/dataspace/engine.rb b/lib/decidim/dataspace/engine.rb index b0e0177..221ad09 100644 --- a/lib/decidim/dataspace/engine.rb +++ b/lib/decidim/dataspace/engine.rb @@ -44,6 +44,9 @@ class Engine < ::Rails::Engine require "extends/controllers/decidim/proposals/proposals_controller_extends" require "extends/models/decidim/comments/comment_extends" require "extends/lib/decidim/core_extends" + require "extends/commands/decidim/system/create_organization_extends" + require "extends/commands/decidim/system/update_organization_extends" + require "extends/forms/decidim/system/base_organization_form_extends" end end diff --git a/lib/extends/commands/decidim/system/create_organization_extends.rb b/lib/extends/commands/decidim/system/create_organization_extends.rb new file mode 100644 index 0000000..b0a2d51 --- /dev/null +++ b/lib/extends/commands/decidim/system/create_organization_extends.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + + +module CreateOrganizationExtends + extend ActiveSupport::Concern + + included do + def create_organization + Decidim::Organization.create!( + name: { form.default_locale => form.name }, + host: form.host, + secondary_hosts: form.clean_secondary_hosts, + reference_prefix: form.reference_prefix, + available_locales: form.available_locales, + available_authorizations: form.clean_available_authorizations, + external_domain_allowlist: ["decidim.org", "github.com"], + users_registration_mode: form.users_registration_mode, + force_users_to_authenticate_before_access_organization: form.force_users_to_authenticate_before_access_organization, + badges_enabled: true, + user_groups_enabled: true, + default_locale: form.default_locale, + omniauth_settings: form.encrypted_omniauth_settings, + smtp_settings: form.encrypted_smtp_settings, + send_welcome_notification: true, + file_upload_settings: form.file_upload_settings.final, + colors: default_colors, + content_security_policy: form.content_security_policy, + enable_dataspace: form.enable_dataspace + ) + end + end +end + +Decidim::System::CreateOrganization.include(CreateOrganizationExtends) diff --git a/lib/extends/commands/decidim/system/update_organization_extends.rb b/lib/extends/commands/decidim/system/update_organization_extends.rb new file mode 100644 index 0000000..0bff972 --- /dev/null +++ b/lib/extends/commands/decidim/system/update_organization_extends.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + + +module UpdateOrganizationExtends + extend ActiveSupport::Concern + + included do + def save_organization + organization.name = form.name + organization.host = form.host + organization.secondary_hosts = form.clean_secondary_hosts + organization.force_users_to_authenticate_before_access_organization = form.force_users_to_authenticate_before_access_organization + organization.available_authorizations = form.clean_available_authorizations + organization.users_registration_mode = form.users_registration_mode + organization.omniauth_settings = form.encrypted_omniauth_settings + organization.smtp_settings = form.encrypted_smtp_settings + organization.file_upload_settings = form.file_upload_settings.final + organization.content_security_policy = form.content_security_policy + organization.enable_dataspace = form.enable_dataspace + + organization.save! + end + end +end + +Decidim::System::UpdateOrganization.include(UpdateOrganizationExtends) diff --git a/lib/extends/forms/decidim/system/base_organization_form_extends.rb b/lib/extends/forms/decidim/system/base_organization_form_extends.rb new file mode 100644 index 0000000..85a451e --- /dev/null +++ b/lib/extends/forms/decidim/system/base_organization_form_extends.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module BaseOrganizationFormExtends + extend ActiveSupport::Concern + + included do + attribute :enable_dataspace, Decidim::AttributeObject::TypeMap::Boolean + end +end + +Decidim::System::BaseOrganizationForm.include(BaseOrganizationFormExtends) From 11760cee8cf66f3fb4331bbceb713e8192ce418e Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 2 Feb 2026 14:35:22 +0100 Subject: [PATCH 3/8] feat: update controllers depending on enable dataspace --- .../dataspace/api/v1/base_controller.rb | 11 +++++- .../proposals/proposals_controller_extends.rb | 35 +++++++++++-------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/controllers/decidim/dataspace/api/v1/base_controller.rb b/app/controllers/decidim/dataspace/api/v1/base_controller.rb index ea97216..ca941ba 100644 --- a/app/controllers/decidim/dataspace/api/v1/base_controller.rb +++ b/app/controllers/decidim/dataspace/api/v1/base_controller.rb @@ -2,8 +2,9 @@ module Decidim module Dataspace - class Api::V1::BaseController < ActionController::API + class Api::V1::BaseController < Decidim::Api::ApplicationController # skip_before_action :verify_authenticity_token + before_action :verify_dataspace_enabled rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :validation_error @@ -21,6 +22,14 @@ def validation_error(exception) def resource_not_found(resource) render json: { error: "#{resource} not found" }, status: :not_found end + + def verify_dataspace_enabled + render json: { error: "Dataspace is not enabled for this organization" }, status: :forbidden unless dataspace_enabled? + end + + def dataspace_enabled? + current_organization.enable_dataspace == true + end end end end diff --git a/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb b/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb index 292b783..de3014a 100644 --- a/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb +++ b/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb @@ -17,8 +17,7 @@ def index .order(position: :asc) render "decidim/proposals/proposals/participatory_texts/participatory_text" else - if component_settings.add_integration && component_settings.integration_url.present? && data.present? - + if verify_dataspace? && data.present? external_proposals = data["contributions"] @authors = data["authors"] proposals = search.result @@ -42,22 +41,30 @@ def index end def external_proposal - uri = URI.parse(params[:url]) - url = "#{uri.scheme}://#{uri.host}" - url += ":3000" if uri.host == "localhost" - - @external_proposal = GetDataFromApi.contribution(url, params[:reference], component_settings.preferred_locale || "en", "true") - return if @external_proposal.nil? - - @comments = @external_proposal["children"] - @parent_comments = @comments.select { |comment| comment["parent"] == @external_proposal["reference"] } if @comments - @authors = GetDataFromApi.authors(url, component_settings.preferred_locale || "en") - .select { |author| @external_proposal["authors"].include?(author["reference"]) } - .map { |author| author["name"] }.join(", ") + if verify_dataspace? + uri = URI.parse(params[:url]) + url = "#{uri.scheme}://#{uri.host}" + url += ":3000" if uri.host == "localhost" + + @external_proposal = GetDataFromApi.contribution(url, params[:reference], component_settings.preferred_locale || "en", "true") + return if @external_proposal.nil? + + @comments = @external_proposal["children"] + @parent_comments = @comments.select { |comment| comment["parent"] == @external_proposal["reference"] } if @comments + @authors = GetDataFromApi.authors(url, component_settings.preferred_locale || "en") + .select { |author| @external_proposal["authors"].include?(author["reference"]) } + .map { |author| author["name"] }.join(", ") + else + redirect_to root_url + end end private + def verify_dataspace? + current_organization.enable_dataspace == true && component_settings.add_integration && component_settings.integration_url.present? + end + def voted_proposals if current_user Decidim::Proposals::ProposalVote.where( From d1fd4679a908169de888794b89049ce0672daae6 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 2 Feb 2026 14:35:57 +0100 Subject: [PATCH 4/8] feat: update view dependint on enable dataspace --- app/views/decidim/admin/components/_settings_fields.html.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/decidim/admin/components/_settings_fields.html.erb b/app/views/decidim/admin/components/_settings_fields.html.erb index 2fc5009..b55db5c 100644 --- a/app/views/decidim/admin/components/_settings_fields.html.erb +++ b/app/views/decidim/admin/components/_settings_fields.html.erb @@ -1,4 +1,7 @@ <% manifest.settings(settings_name).attributes.each do |field_name, settings_attribute| %> + <% dataspace_related = [:add_integration, :integration_url, :preferred_locale] %> + <% next if dataspace_related.include?(field_name) && current_organization.enable_dataspace == false %> +
<%= settings_attribute_input( form, From de8469631c196bd7c149e6e42ebe28fcfc4a91d6 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 2 Feb 2026 14:37:57 +0100 Subject: [PATCH 5/8] test: update tests depending on enable dataspace --- .../api/v1/authors_controller_spec.rb | 151 ++++++--- .../api/v1/containers_controller_spec.rb | 121 ++++--- .../api/v1/contributions_controller_spec.rb | 229 ++++++++----- .../dataspace/api/v1/data_controller_spec.rb | 69 ++-- .../proposals/proposals_controller_spec.rb | 314 ++++++++++-------- spec/factories.rb | 82 +++++ ...min_adds_integrations_on_proposals_spec.rb | 84 +++-- spec/system/external_proposal_spec.rb | 237 +++++++------ spec/system/proposals_index_spec.rb | 104 +++--- 9 files changed, 861 insertions(+), 530 deletions(-) diff --git a/spec/controllers/decidim/dataspace/api/v1/authors_controller_spec.rb b/spec/controllers/decidim/dataspace/api/v1/authors_controller_spec.rb index b74311b..7e455c0 100644 --- a/spec/controllers/decidim/dataspace/api/v1/authors_controller_spec.rb +++ b/spec/controllers/decidim/dataspace/api/v1/authors_controller_spec.rb @@ -6,88 +6,129 @@ routes { Decidim::Dataspace::Engine.routes } describe "index" do - context "when there are proposals and authors" do - let(:component) { create(:proposal_component) } - let!(:proposal) { create(:proposal, :participant_author, component:) } - let!(:proposal_two) { create(:proposal, :official_meeting, component:) } - let!(:proposal_three) { create(:proposal, :official, component:) } + context "when dataspace is disabled" do + let!(:component) { create(:proposal_component) } - it "is a success and returns json" do + before do + request.env["decidim.current_organization"] = component.organization get :index - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error end - it "returns all authors" do - get :index - expect(response.parsed_body.size).to eq(3) + it "returns a forbidden status" do + expect(response).to have_http_status(:forbidden) end end - context "when there are no proposals and so no authors" do - it "is a not_found and returns json without authors" do - get :index - expect(response).to have_http_status(:not_found) - expect(response.content_type).to include("application/json") - expect(response.parsed_body).to eq({ "error" => "Authors not found" }) + context "when dataspace is enabled" do + let!(:component) { create(:proposal_component) } + + before do + request.env["decidim.current_organization"] = component.organization + component.organization.enable_dataspace = true + component.organization.save! + end + + context "and there are proposals and authors" do + let!(:proposal) { create(:proposal, :participant_author, component:) } + let!(:proposal_two) { create(:proposal, :official_meeting, component:) } + let!(:proposal_three) { create(:proposal, :official, component:) } + + it "is a success, and it returns json with 3 authors" do + get :index + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + # we have 3 different authors + expect(response.parsed_body.size).to eq(3) + end + end + + context "and there are no proposals and so no authors" do + it "is a not_found and returns json without authors" do + get :index + expect(response).to have_http_status(:not_found) + expect(response.content_type).to include("application/json") + expect(response.parsed_body).to eq({ "error" => "Authors not found" }) + end end end end describe "show" do - context "when author exists" do + context "when dataspace is disabled" do let(:component) { create(:proposal_component) } let!(:proposal) { create(:proposal, :participant_author, component:) } - it "is a success and returns json" do + before do + request.env["decidim.current_organization"] = component.organization get :show, params: { reference: proposal.authors.first.name } - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error end - context "and author is a user" do - it "returns the author" do - author = proposal.authors.first - get :show, params: { reference: author.name } - expect(response.parsed_body).to eq({ "reference" => author.name, - "name" => author.name, - "source" => author.personal_url }) - end + it "returns a forbidden status" do + expect(response).to have_http_status(:forbidden) end + end - context "and author is a official" do - let!(:proposal) { create(:proposal, :official, component:) } + context "when dataspace is enabled" do + let(:component) { create(:proposal_component) } + let!(:proposal) { create(:proposal, :participant_author, component:) } - it "returns the author" do - author = proposal.authors.first - get :show, params: { reference: author.reference_prefix } - expect(response.parsed_body).to eq({ "reference" => author.reference_prefix, - "name" => author.name["en"], - "source" => author.official_url }) - end + before do + request.env["decidim.current_organization"] = component.organization + component.organization.enable_dataspace = true + component.organization.save! end - context "and author is a meeting" do - let!(:proposal) { create(:proposal, :official_meeting, component:) } + context "and author exists" do + it "is a success and returns json" do + get :show, params: { reference: proposal.authors.first.name } + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + end - it "returns the author" do - author = proposal.authors.first - get :show, params: { reference: author.reference } - expect(response.parsed_body).to eq({ "reference" => author.reference, - "name" => author.title["en"], - "source" => Decidim::ResourceLocatorPresenter.new(author).url }) + context "and author is a user" do + it "returns the author" do + author = proposal.authors.first + get :show, params: { reference: author.name } + expect(response.parsed_body).to eq({ "reference" => author.name, + "name" => author.name, + "source" => author.personal_url }) + end + end + + context "and author is a official" do + let!(:proposal) { create(:proposal, :official, component:) } + + it "returns the author" do + author = proposal.authors.first + get :show, params: { reference: author.reference_prefix } + expect(response.parsed_body).to eq({ "reference" => author.reference_prefix, + "name" => author.name["en"], + "source" => author.official_url }) + end + end + + context "and author is a meeting" do + let!(:proposal) { create(:proposal, :official_meeting, component:) } + + it "returns the author" do + author = proposal.authors.first + get :show, params: { reference: author.reference } + expect(response.parsed_body).to eq({ "reference" => author.reference, + "name" => author.title["en"], + "source" => Decidim::ResourceLocatorPresenter.new(author).url }) + end end end - end - context "when author does not exist" do - it "is a not_found and returns json message" do - get :show, params: { reference: "XXX" } - expect(response).to have_http_status(:not_found) - expect(response.content_type).to include("application/json") - expect(response.parsed_body).to eq({ "error" => "Author not found" }) + context "when author does not exist" do + it "is a not_found and returns json message" do + get :show, params: { reference: "XXX" } + expect(response).to have_http_status(:not_found) + expect(response.content_type).to include("application/json") + expect(response.parsed_body).to eq({ "error" => "Author not found" }) + end end end end diff --git a/spec/controllers/decidim/dataspace/api/v1/containers_controller_spec.rb b/spec/controllers/decidim/dataspace/api/v1/containers_controller_spec.rb index fb4f4dd..1d76233 100644 --- a/spec/controllers/decidim/dataspace/api/v1/containers_controller_spec.rb +++ b/spec/controllers/decidim/dataspace/api/v1/containers_controller_spec.rb @@ -6,67 +6,112 @@ routes { Decidim::Dataspace::Engine.routes } describe "index" do - context "when there are proposals and containers" do - let(:component) { create(:proposal_component) } - let!(:proposal) { create(:proposal, component:) } - let!(:proposal_two) { create(:proposal, component:) } + context "when dataspace is disabled" do + let!(:component) { create(:proposal_component) } before do + request.env["decidim.current_organization"] = component.organization get :index end - it "is a success and returns json" do - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error + it "returns a forbidden status" do + expect(response).to have_http_status(:forbidden) end + end + + context "when dataspace is enabled" do + let!(:component) { create(:proposal_component) } - it "returns all containers" do - # proposals are created from participatory_process - expect(response.parsed_body.size).to eq(1) + before do + request.env["decidim.current_organization"] = component.organization + component.organization.enable_dataspace = true + component.organization.save! end - end - context "when there are no proposals and so no containers" do - it "is a not_found and returns json without authors" do - get :index - expect(response).to have_http_status(:not_found) - expect(response.content_type).to include("application/json") - expect(response.parsed_body).to eq({ "error" => "Containers not found" }) + context "and there are proposals and containers" do + let!(:proposal) { create(:proposal, component:) } + let!(:proposal_two) { create(:proposal, component:) } + + before do + get :index + end + + it "is a success and returns json" do + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + end + + it "returns all containers" do + # proposals are created from participatory_process + expect(response.parsed_body.size).to eq(1) + end + end + + context "and there are no proposals and so no containers" do + it "is a not_found and returns json without authors" do + get :index + expect(response).to have_http_status(:not_found) + expect(response.content_type).to include("application/json") + expect(response.parsed_body).to eq({ "error" => "Containers not found" }) + end end end end describe "show" do - context "when container exists" do + context "when dataspace is disabled" do let(:component) { create(:proposal_component) } - let(:proposal) { create(:proposal, component:) } - let!(:container) { proposal.component.participatory_space_type.constantize.find(proposal.component.participatory_space_id) } + let!(:proposal) { create(:proposal, :participant_author, component:) } before do - get :show, params: { reference: container.reference } + request.env["decidim.current_organization"] = component.organization + get :show, params: { reference: proposal.authors.first.name } end - it "is a success and returns json" do - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error + it "returns a forbidden status" do + expect(response).to have_http_status(:forbidden) end + end + + context "when dataspace is enabled" do + let!(:component) { create(:proposal_component) } - it "returns the container" do - expect(response.parsed_body["reference"]).to eq(container.reference) - expect(response.parsed_body["source"]).to eq(Decidim::ResourceLocatorPresenter.new(container).url) - expect(response.parsed_body["name"]).to eq(container.title["en"]) - expect(response.parsed_body["description"]).to eq(container.description["en"]) + before do + request.env["decidim.current_organization"] = component.organization + component.organization.enable_dataspace = true + component.organization.save! + end + + context "and container exists" do + let(:proposal) { create(:proposal, component:) } + let!(:container) { proposal.component.participatory_space_type.constantize.find(proposal.component.participatory_space_id) } + + before do + get :show, params: { reference: container.reference } + end + + it "is a success and returns json" do + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + end + + it "returns the container" do + expect(response.parsed_body["reference"]).to eq(container.reference) + expect(response.parsed_body["source"]).to eq(Decidim::ResourceLocatorPresenter.new(container).url) + expect(response.parsed_body["name"]).to eq(container.title["en"]) + expect(response.parsed_body["description"]).to eq(container.description["en"]) + end end - end - context "when container does not exist" do - it "is a not_found and returns json message" do - get :show, params: { reference: "XXX" } - expect(response).to have_http_status(:not_found) - expect(response.content_type).to include("application/json") - expect(response.parsed_body).to eq({ "error" => "Container not found" }) + context "when container does not exist" do + it "is a not_found and returns json message" do + get :show, params: { reference: "XXX" } + expect(response).to have_http_status(:not_found) + expect(response.content_type).to include("application/json") + expect(response.parsed_body).to eq({ "error" => "Container not found" }) + end end end end diff --git a/spec/controllers/decidim/dataspace/api/v1/contributions_controller_spec.rb b/spec/controllers/decidim/dataspace/api/v1/contributions_controller_spec.rb index 5eb2b01..fbeffc6 100644 --- a/spec/controllers/decidim/dataspace/api/v1/contributions_controller_spec.rb +++ b/spec/controllers/decidim/dataspace/api/v1/contributions_controller_spec.rb @@ -6,128 +6,173 @@ routes { Decidim::Dataspace::Engine.routes } describe "index" do - context "when there are contributions with comments" do - let(:component) { create(:proposal_component) } - let!(:proposal) { create(:proposal, component:) } - let!(:comment_one) { create(:comment, commentable: proposal) } - let!(:comment_two) { create(:comment, commentable: proposal) } - let!(:proposal_two) { create(:proposal, component:) } - let!(:proposal_three) { create(:proposal, component:) } + context "when dataspace is disabled" do + let!(:component) { create(:proposal_component) } - context "and with_comments is false" do - it "is a success and returns json with only proposals as contributions" do - get :index - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error - # only proposals are rendered - expect(response.parsed_body.size).to eq(3) - end + before do + request.env["decidim.current_organization"] = component.organization + get :index end - context "and with_comments is true" do - it "is a success and returns json with proposals and comments as contributions" do - get :index, params: { with_comments: "true" } - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error - # proposals + comments are rendered - expect(response.parsed_body.size).to eq(5) - end + it "returns a forbidden status" do + expect(response).to have_http_status(:forbidden) end end - context "when there are no contributions" do - it "is a not_found and returns json without authors" do - get :index - expect(response).to have_http_status(:not_found) - expect(response.content_type).to include("application/json") - expect(response.parsed_body).to eq({ "error" => "Contributions not found" }) + context "when dataspace is enabled" do + let!(:component) { create(:proposal_component) } + + before do + request.env["decidim.current_organization"] = component.organization + component.organization.enable_dataspace = true + component.organization.save! end - end - context "when there is a container param" do - let(:component_two) { create(:proposal_component) } - let!(:proposal_four) { create(:proposal, :participant_author, component: component_two) } + context "and there are contributions with comments" do + let!(:proposal) { create(:proposal, component:) } + let!(:comment_one) { create(:comment, commentable: proposal) } + let!(:comment_two) { create(:comment, commentable: proposal) } + let!(:proposal_two) { create(:proposal, component:) } + let!(:proposal_three) { create(:proposal, component:) } + + context "and with_comments is false" do + it "is a success and returns json with only proposals as contributions" do + get :index + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + # only proposals are rendered + expect(response.parsed_body.size).to eq(3) + end + end - before do - get :index, params: { container: component_two.participatory_space.reference } + context "and with_comments is true" do + it "is a success and returns json with proposals and comments as contributions" do + get :index, params: { with_comments: "true" } + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + # proposals + comments are rendered + expect(response.parsed_body.size).to eq(5) + end + end end - it "is a success and returns json with filtered proposals as contributions" do - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error - # only proposal_four is rendered - expect(response.parsed_body.size).to eq(1) - expect(response.parsed_body.first["reference"]).to eq(proposal_four.reference) + context "when there are no contributions" do + it "is a not_found and returns json without authors" do + get :index + expect(response).to have_http_status(:not_found) + expect(response.content_type).to include("application/json") + expect(response.parsed_body).to eq({ "error" => "Contributions not found" }) + end end - end - end - describe "show" do - let(:component) { create(:proposal_component) } - let!(:proposal) { create(:proposal, component:) } - let!(:comment_one) { create(:comment, commentable: proposal) } - let!(:comment_two) { create(:comment, commentable: proposal) } + context "when there is a container param" do + let(:component_two) { create(:proposal_component) } + let!(:proposal_four) { create(:proposal, :participant_author, component: component_two) } - context "when contribution exists" do - context "and with comments is false" do before do - get :show, params: { reference: proposal.reference } + get :index, params: { container: component_two.participatory_space.reference } end - it "is a success and returns json" do + it "is a success and returns json with filtered proposals as contributions" do expect(response).to have_http_status(:ok) expect(response.content_type).to include("application/json") expect { response.parsed_body }.not_to raise_error + # only proposal_four is rendered + expect(response.parsed_body.size).to eq(1) + expect(response.parsed_body.first["reference"]).to eq(proposal_four.reference) end + end + end + end - it "returns the contribution with no detailed comments" do - expect(response.parsed_body["reference"]).to eq(proposal.reference) - expect(response.parsed_body["source"]).to eq(Decidim::ResourceLocatorPresenter.new(proposal).url) - expect(response.parsed_body["container"]).to eq(proposal.component.participatory_space_type.constantize.find(proposal.component.participatory_space_id).reference) - expect(response.parsed_body["title"]).to eq(proposal.title["en"]) - expect(response.parsed_body["content"]).to eq(proposal.body["en"]) - # 2 comments without details - expect(response.parsed_body["children"].size).to eq(2) - expect(response.parsed_body["children"].first.class).to eq(String) - expect(response.parsed_body["children"].first).to eq(comment_one.reference) - end + describe "show" do + context "when dataspace is disabled" do + let(:component) { create(:proposal_component) } + let!(:proposal) { create(:proposal, component:) } + + before do + request.env["decidim.current_organization"] = component.organization + get :show, params: { reference: proposal.authors.first.name } end - context "and with comments is true" do - before do - get :show, params: { reference: proposal.reference, with_comments: "true" } - end + it "returns a forbidden status" do + expect(response).to have_http_status(:forbidden) + end + end - it "is a success and returns json" do - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error + context "when dataspace is enabled" do + let!(:component) { create(:proposal_component) } + + before do + request.env["decidim.current_organization"] = component.organization + component.organization.enable_dataspace = true + component.organization.save! + end + + context "and contribution exists" do + let!(:proposal) { create(:proposal, component:) } + let!(:comment_one) { create(:comment, commentable: proposal) } + let!(:comment_two) { create(:comment, commentable: proposal) } + + context "and with comments is false" do + before do + get :show, params: { reference: proposal.reference } + end + + it "is a success and returns json" do + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + end + + it "returns the contribution with no detailed comments" do + expect(response.parsed_body["reference"]).to eq(proposal.reference) + expect(response.parsed_body["source"]).to eq(Decidim::ResourceLocatorPresenter.new(proposal).url) + expect(response.parsed_body["container"]).to eq(proposal.component.participatory_space_type.constantize.find(proposal.component.participatory_space_id).reference) + expect(response.parsed_body["title"]).to eq(proposal.title["en"]) + expect(response.parsed_body["content"]).to eq(proposal.body["en"]) + # 2 comments without details + expect(response.parsed_body["children"].size).to eq(2) + expect(response.parsed_body["children"].first.class).to eq(String) + expect(response.parsed_body["children"].first).to eq(comment_one.reference) + end end - it "returns the contribution with detailed comments" do - expect(response.parsed_body["reference"]).to eq(proposal.reference) - expect(response.parsed_body["source"]).to eq(Decidim::ResourceLocatorPresenter.new(proposal).url) - expect(response.parsed_body["container"]).to eq(proposal.component.participatory_space_type.constantize.find(proposal.component.participatory_space_id).reference) - expect(response.parsed_body["title"]).to eq(proposal.title["en"]) - expect(response.parsed_body["content"]).to eq(proposal.body["en"]) - # 2 comments with details - expect(response.parsed_body["children"].size).to eq(2) - expect(response.parsed_body["children"].first.class).to eq(Hash) - expect(response.parsed_body["children"].first["reference"]).to eq(comment_one.reference) - expect(response.parsed_body["children"].first["content"]).to eq(comment_one.body["en"]) + context "and with comments is true" do + before do + get :show, params: { reference: proposal.reference, with_comments: "true" } + end + + it "is a success and returns json" do + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + end + + it "returns the contribution with detailed comments" do + expect(response.parsed_body["reference"]).to eq(proposal.reference) + expect(response.parsed_body["source"]).to eq(Decidim::ResourceLocatorPresenter.new(proposal).url) + expect(response.parsed_body["container"]).to eq(proposal.component.participatory_space_type.constantize.find(proposal.component.participatory_space_id).reference) + expect(response.parsed_body["title"]).to eq(proposal.title["en"]) + expect(response.parsed_body["content"]).to eq(proposal.body["en"]) + # 2 comments with details + expect(response.parsed_body["children"].size).to eq(2) + expect(response.parsed_body["children"].first.class).to eq(Hash) + expect(response.parsed_body["children"].first["reference"]).to eq(comment_one.reference) + expect(response.parsed_body["children"].first["content"]).to eq(comment_one.body["en"]) + end end end - end - context "when contribution does not exist" do - it "is a not_found and returns json message" do - get :show, params: { reference: "XXX" } - expect(response).to have_http_status(:not_found) - expect(response.content_type).to include("application/json") - expect(response.parsed_body).to eq({ "error" => "Contribution not found" }) + context "and contribution does not exist" do + it "is a not_found and returns json message" do + get :show, params: { reference: "XXX" } + expect(response).to have_http_status(:not_found) + expect(response.content_type).to include("application/json") + expect(response.parsed_body).to eq({ "error" => "Contribution not found" }) + end end end end diff --git a/spec/controllers/decidim/dataspace/api/v1/data_controller_spec.rb b/spec/controllers/decidim/dataspace/api/v1/data_controller_spec.rb index 02bfadb..d855168 100644 --- a/spec/controllers/decidim/dataspace/api/v1/data_controller_spec.rb +++ b/spec/controllers/decidim/dataspace/api/v1/data_controller_spec.rb @@ -11,43 +11,62 @@ let!(:proposal_three) { create(:proposal, :official, component:) } describe "index" do - context "when there is no container param" do + context "when dataspace is disabled" do before do + request.env["decidim.current_organization"] = component.organization get :index end - it "is successful and returns json" do - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error - end - - it "returns all data" do - expect(response.parsed_body["contributions"].size).to eq(3) - expect(response.parsed_body["authors"].size).to eq(3) - expect(response.parsed_body["containers"].size).to eq(1) + it "returns a forbidden status" do + expect(response).to have_http_status(:forbidden) end end - context "when there is a container param" do - let(:component_two) { create(:proposal_component) } - let!(:proposal_four) { create(:proposal, :participant_author, component: component_two) } - + context "when dataspace is enabled" do before do - get :index, params: { container: component_two.participatory_space.reference } + request.env["decidim.current_organization"] = component.organization + component.organization.enable_dataspace = true + component.organization.save! end - it "is successful and returns json" do - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error + context "and there is no container param" do + before do + get :index + end + + it "is successful and returns json" do + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + end + + it "returns all data" do + expect(response.parsed_body["contributions"].size).to eq(3) + expect(response.parsed_body["authors"].size).to eq(3) + expect(response.parsed_body["containers"].size).to eq(1) + end end - it "returns data with filtered contributions" do - expect(response.parsed_body["contributions"].size).to eq(1) - expect(response.parsed_body["contributions"].first["reference"]).to eq(proposal_four.reference) - expect(response.parsed_body["authors"].size).to eq(4) - expect(response.parsed_body["containers"].size).to eq(2) + context "and there is a container param" do + let(:component_two) { create(:proposal_component) } + let!(:proposal_four) { create(:proposal, :participant_author, component: component_two) } + + before do + get :index, params: { container: component_two.participatory_space.reference } + end + + it "is successful and returns json" do + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + end + + it "returns data with filtered contributions" do + expect(response.parsed_body["contributions"].size).to eq(1) + expect(response.parsed_body["contributions"].first["reference"]).to eq(proposal_four.reference) + expect(response.parsed_body["authors"].size).to eq(4) + expect(response.parsed_body["containers"].size).to eq(2) + end end end end diff --git a/spec/controllers/decidim/proposals/proposals_controller_spec.rb b/spec/controllers/decidim/proposals/proposals_controller_spec.rb index 4f6076e..608b4f3 100644 --- a/spec/controllers/decidim/proposals/proposals_controller_spec.rb +++ b/spec/controllers/decidim/proposals/proposals_controller_spec.rb @@ -7,8 +7,8 @@ module Proposals describe ProposalsController do routes { Decidim::Proposals::Engine.routes } + let(:component) { create(:proposal_component, :with_geocoding_enabled) } let(:user) { create(:user, :confirmed, organization: component.organization) } - let(:proposal_params) do { component_id: component.id @@ -24,167 +24,184 @@ module Proposals end describe "GET index" do + context "when participatory texts are enabled" do + let(:component) { create(:proposal_component, :with_participatory_texts_enabled) } + + it "sorts proposals by position" do + get :index + expect(response).to have_http_status(:ok) + expect(subject).to render_template(:participatory_text) + expect(assigns(:proposals).order_values.first.expr.name).to eq("position") + end + + context "when emendations exist" do + let!(:amendable) { create(:proposal, component:) } + let!(:emendation) { create(:proposal, component:) } + let!(:amendment) { create(:amendment, amendable:, emendation:, state: "accepted") } + + it "does not include emendations" do + get :index + expect(response).to have_http_status(:ok) + emendations = assigns(:proposals).select(&:emendation?) + expect(emendations).to be_empty + end + end + end + context "when participatory texts are disabled" do - let(:component) { create(:proposal_component, :with_geocoding_enabled) } + context "and dataspace is disabled" do + let!(:geocoded_proposals) { create_list(:proposal, 10, component:, latitude: 1.1, longitude: 2.2) } + let!(:proposals) { create_list(:proposal, 2, component:, latitude: nil, longitude: nil) } - context "and there are no externals proposals" do - it "sorts proposals by search defaults" do + before do get :index + end + + it "sorts proposals by search defaults" do expect(response).to have_http_status(:ok) expect(subject).to render_template(:index) + expect(assigns(:proposals).size).to eq(12) expect(assigns(:proposals).order_values).to eq [Decidim::Proposals::Proposal.arel_table[Decidim::Proposals::Proposal.primary_key] * Arel.sql("RANDOM()")] expect(assigns(:proposals).order_values.map(&:to_sql)).to eq ["\"decidim_proposals_proposals\".\"id\" * RANDOM()"] end + + it "sets two different collections" do + expect(assigns(:proposals)).to match_array(geocoded_proposals + proposals) + end end - context "and there are externals proposals" do - let(:component) { create(:proposal_component) } + context "and dataspace is enabled" do let!(:proposals) { create_list(:proposal, 2, component:) } - let(:contrib_one) do - { "reference": "JD-PROP-2025-09-1", - "source": "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", - "container": "JD-PART-2025-09-1", - "locale": "en", - "title": "Test one", - "content": "Debitis repellat provident", - "authors": ["JD-MEET-2025-09-6"], - "created_at": "2025-09-11T10:20:21.222Z", - "updated_at": "2025-09-11T10:21:56.604Z", - "deleted_at": nil } - end - let(:contrib_two) do - { "reference": "JD-PROP-2025-09-20", - "source": "http://localhost:3000/assemblies/smile-trivial/f/25/proposals/20", - "container": "JD-ASSE-2025-09-1", - "locale": "en", - "title": "Test two", - "content": "Non et vel", - "authors": ["JD-MEET-2025-09-23"], - "created_at": "2025-09-11T10:43:23.743Z", - "updated_at": "2025-09-11T10:43:27.147Z", - "deleted_at": nil } - end - let(:container_one) do - { - "reference": "JD-PART-2025-09-1", - "source": "http://localhost:3000/processes/satisfaction-hope", - "name": "Cupiditate natus dignissimos saepe ut.", - "description": "

Voluptas recusandae est. Nesciunt excepturi corrupti. Qui natus eligendi.

", - "metadata": {}, - "created_at": "2025-09-11T10:14:58.111Z", - "updated_at": "2025-09-11T10:14:58.126Z", - "deleted_at": nil - } - end - let(:container_two) do - { - "reference": "JD-ASSE-2025-09-1", - "source": "http://localhost:3000/assemblies/smile-trivial", - "name": "Molestiae aut corporis quas et.", - "description": "

Ratione autem repellendus. Error voluptatem ipsam. Ut dicta velit.

", - "metadata": {}, - "created_at": "2025-09-11T10:38:07.682Z", - "updated_at": "2025-09-11T10:38:07.682Z", - "deleted_at": nil - } - end - let(:author_one) do - { - "reference": "JD-MEET-2025-09-6", - "name": "Animi voluptatum.", - "source": "http://localhost:3000/processes/satisfaction-hope/f/5/meetings/6" - } - end - let(:author_two) do - { - "reference": "JD-MEET-2025-09-23", - "name": "Et natus.", - "source": "http://localhost:3000/assemblies/smile-trivial/f/23/meetings/23" - } - end - let(:json) do - { - "containers" => [container_one, container_two], - "contributions" => [contrib_one, contrib_two], - "authors" => [author_one, author_two] - } + before do + component.organization.enable_dataspace = true + component.organization.save! end - context "and there is one url in integration url" do - before do - component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" }) - allow(GetDataFromApi).to receive(:data).and_return(json) - end - - it "sorts proposals by search defaults and define external_proposals and other variables" do + context "and there are no external proposals" do + it "returns proposals" do get :index expect(response).to have_http_status(:ok) expect(subject).to render_template(:index) + expect(assigns(:proposals).size).to eq(2) expect(assigns(:proposals).order_values).to eq [Decidim::Proposals::Proposal.arel_table[Decidim::Proposals::Proposal.primary_key] * Arel.sql("RANDOM()")] expect(assigns(:proposals).order_values.map(&:to_sql)).to eq ["\"decidim_proposals_proposals\".\"id\" * RANDOM()"] - expect(assigns(:authors).count).to eq 2 - expect(assigns(:authors).first[:reference]).to eq "JD-MEET-2025-09-6" - expect(assigns(:authors).last[:reference]).to eq "JD-MEET-2025-09-23" - expect(assigns(:total_count)).to eq 4 - expect(assigns(:current_page)).to eq 1 - expect(assigns(:total_pages)).to eq 1 - expect(assigns(:proposals).count).to eq 2 - expect(assigns(:external_proposals).count).to eq 2 - expect(assigns(:external_proposals).first[:reference]).to eq "JD-PROP-2025-09-1" - expect(assigns(:external_proposals).last[:reference]).to eq "JD-PROP-2025-09-20" end end - context "and there are 2 urls in integration_url" do - before do - component.update!(settings: { add_integration: true, integration_url: "http://example.org, http://example.org,", preferred_locale: "en" }) - allow(GetDataFromApi).to receive(:data).and_return(json) + context "and there are externals proposals" do + let(:component) { create(:proposal_component) } + let!(:proposals) { create_list(:proposal, 2, component:) } + let(:contrib_one) do + { "reference": "JD-PROP-2025-09-1", + "source": "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", + "container": "JD-PART-2025-09-1", + "locale": "en", + "title": "Test one", + "content": "Debitis repellat provident", + "authors": ["JD-MEET-2025-09-6"], + "created_at": "2025-09-11T10:20:21.222Z", + "updated_at": "2025-09-11T10:21:56.604Z", + "deleted_at": nil } end - - it "returns 4 external proposals and 4 authors" do - get :index - expect(response).to have_http_status(:ok) - expect(subject).to render_template(:index) - expect(assigns(:external_proposals).count).to eq 4 - expect(assigns(:authors).count).to eq 4 + let(:contrib_two) do + { "reference": "JD-PROP-2025-09-20", + "source": "http://localhost:3000/assemblies/smile-trivial/f/25/proposals/20", + "container": "JD-ASSE-2025-09-1", + "locale": "en", + "title": "Test two", + "content": "Non et vel", + "authors": ["JD-MEET-2025-09-23"], + "created_at": "2025-09-11T10:43:23.743Z", + "updated_at": "2025-09-11T10:43:27.147Z", + "deleted_at": nil } + end + let(:container_one) do + { + "reference": "JD-PART-2025-09-1", + "source": "http://localhost:3000/processes/satisfaction-hope", + "name": "Cupiditate natus dignissimos saepe ut.", + "description": "

Voluptas recusandae est. Nesciunt excepturi corrupti. Qui natus eligendi.

", + "metadata": {}, + "created_at": "2025-09-11T10:14:58.111Z", + "updated_at": "2025-09-11T10:14:58.126Z", + "deleted_at": nil + } + end + let(:container_two) do + { + "reference": "JD-ASSE-2025-09-1", + "source": "http://localhost:3000/assemblies/smile-trivial", + "name": "Molestiae aut corporis quas et.", + "description": "

Ratione autem repellendus. Error voluptatem ipsam. Ut dicta velit.

", + "metadata": {}, + "created_at": "2025-09-11T10:38:07.682Z", + "updated_at": "2025-09-11T10:38:07.682Z", + "deleted_at": nil + } + end + let(:author_one) do + { + "reference": "JD-MEET-2025-09-6", + "name": "Animi voluptatum.", + "source": "http://localhost:3000/processes/satisfaction-hope/f/5/meetings/6" + } + end + let(:author_two) do + { + "reference": "JD-MEET-2025-09-23", + "name": "Et natus.", + "source": "http://localhost:3000/assemblies/smile-trivial/f/23/meetings/23" + } end - end - end - - it "sets two different collections" do - geocoded_proposals = create_list(:proposal, 10, component:, latitude: 1.1, longitude: 2.2) - non_geocoded_proposals = create_list(:proposal, 2, component:, latitude: nil, longitude: nil) - - get :index - expect(response).to have_http_status(:ok) - expect(subject).to render_template(:index) - expect(assigns(:proposals).count).to eq 12 - expect(assigns(:proposals)).to match_array(geocoded_proposals + non_geocoded_proposals) - end - end + let(:json) do + { + "containers" => [container_one, container_two], + "contributions" => [contrib_one, contrib_two], + "authors" => [author_one, author_two] + } + end - context "when participatory texts are enabled" do - let(:component) { create(:proposal_component, :with_participatory_texts_enabled) } + context "and there is one url in integration url" do + before do + component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" }) + allow(GetDataFromApi).to receive(:data).and_return(json) + end - it "sorts proposals by position" do - get :index - expect(response).to have_http_status(:ok) - expect(subject).to render_template(:participatory_text) - expect(assigns(:proposals).order_values.first.expr.name).to eq("position") - end + it "sorts proposals by search defaults and define external_proposals and other variables" do + get :index + expect(response).to have_http_status(:ok) + expect(subject).to render_template(:index) + expect(assigns(:proposals).order_values).to eq [Decidim::Proposals::Proposal.arel_table[Decidim::Proposals::Proposal.primary_key] * Arel.sql("RANDOM()")] + expect(assigns(:proposals).order_values.map(&:to_sql)).to eq ["\"decidim_proposals_proposals\".\"id\" * RANDOM()"] + expect(assigns(:authors).count).to eq 2 + expect(assigns(:authors).first[:reference]).to eq "JD-MEET-2025-09-6" + expect(assigns(:authors).last[:reference]).to eq "JD-MEET-2025-09-23" + expect(assigns(:total_count)).to eq 4 + expect(assigns(:current_page)).to eq 1 + expect(assigns(:total_pages)).to eq 1 + expect(assigns(:proposals).count).to eq 2 + expect(assigns(:external_proposals).count).to eq 2 + expect(assigns(:external_proposals).first[:reference]).to eq "JD-PROP-2025-09-1" + expect(assigns(:external_proposals).last[:reference]).to eq "JD-PROP-2025-09-20" + end + end - context "when emendations exist" do - let!(:amendable) { create(:proposal, component:) } - let!(:emendation) { create(:proposal, component:) } - let!(:amendment) { create(:amendment, amendable:, emendation:, state: "accepted") } + context "and there are 2 urls in integration_url" do + before do + component.update!(settings: { add_integration: true, integration_url: "http://example.org, http://example.org,", preferred_locale: "en" }) + allow(GetDataFromApi).to receive(:data).and_return(json) + end - it "does not include emendations" do - get :index - expect(response).to have_http_status(:ok) - emendations = assigns(:proposals).select(&:emendation?) - expect(emendations).to be_empty + it "returns 4 external proposals and 4 authors" do + get :index + expect(response).to have_http_status(:ok) + expect(subject).to render_template(:index) + expect(assigns(:external_proposals).count).to eq 4 + expect(assigns(:authors).count).to eq 4 + end + end end end end @@ -279,14 +296,29 @@ module Proposals allow(GetDataFromApi).to receive(:authors).and_return(authors) end - it "displays external_proposal view and sets variables" do - get :external_proposal, params: { reference: "JD-PROP-2025-09-1", param: :reference, url: "http://example.org" } - expect(response).to have_http_status(:ok) - expect(subject).to render_template(:external_proposal) - expect(assigns(:external_proposal)).to eq json_contrib - expect(assigns(:comments)).to eq json_contrib["children"] - expect(assigns(:parent_comments)).to eq(json_contrib["children"].select { |comment| comment["parent"] == json_contrib["reference"] }) - expect(assigns(:authors)).to eq "Et natus." + context "when dataspace is disabled" do + it "redirects to proposals index" do + get :external_proposal, params: { reference: "JD-PROP-2025-09-1", param: :reference, url: "http://example.org" } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to("/proposals") + end + end + + context "when dataspace is enabled" do + before do + component.organization.enable_dataspace = true + component.organization.save! + end + + it "displays external_proposal view and sets variables" do + get :external_proposal, params: { reference: "JD-PROP-2025-09-1", param: :reference, url: "http://example.org" } + expect(response).to have_http_status(:ok) + expect(subject).to render_template(:external_proposal) + expect(assigns(:external_proposal)).to eq json_contrib + expect(assigns(:comments)).to eq json_contrib["children"] + expect(assigns(:parent_comments)).to eq(json_contrib["children"].select { |comment| comment["parent"] == json_contrib["reference"] }) + expect(assigns(:authors)).to eq "Et natus." + end end end diff --git a/spec/factories.rb b/spec/factories.rb index b607669..933a026 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -2,3 +2,85 @@ require "decidim/dataspace/test/factories" require "decidim/proposals/test/factories" + +FactoryBot.modify do + factory :organization, class: "Decidim::Organization" do + transient do + skip_injection { false } + create_static_pages { true } + end + + # we do not want machine translation here + name do + Decidim.available_locales.index_with { |_locale| Faker::Company.unique.name } + end + + reference_prefix { Faker::Name.suffix } + time_zone { "UTC" } + twitter_handler { Faker::Hipster.word } + facebook_handler { Faker::Hipster.word } + instagram_handler { Faker::Hipster.word } + youtube_handler { Faker::Hipster.word } + github_handler { Faker::Hipster.word } + sequence(:host) { |n| "#{n}.lvh.me" } + description { generate_localized_description(:organization_description, skip_injection:) } + favicon { Decidim::Dev.test_file("icon.png", "image/png") } + default_locale { Decidim.default_locale } + available_locales { Decidim.available_locales } + users_registration_mode { :enabled } + official_img_footer { Decidim::Dev.test_file("avatar.jpg", "image/jpeg") } + official_url { Faker::Internet.url } + highlighted_content_banner_enabled { false } + enable_omnipresent_banner { false } + badges_enabled { true } + user_groups_enabled { true } + enable_dataspace { false } + send_welcome_notification { true } + comments_max_length { 1000 } + admin_terms_of_service_body { generate_localized_description(:admin_terms_of_service_body, skip_injection:) } + force_users_to_authenticate_before_access_organization { false } + machine_translation_display_priority { "original" } + external_domain_allowlist { ["example.org", "twitter.com", "facebook.com", "youtube.com", "github.com", "mytesturl.me"] } + smtp_settings do + { + "from" => "test@example.org", + "user_name" => "test", + "encrypted_password" => Decidim::AttributeEncryptor.encrypt("demo"), + "port" => "25", + "address" => "smtp.example.org" + } + end + file_upload_settings { Decidim::OrganizationSettings.default(:upload) } + enable_participatory_space_filters { true } + content_security_policy do + { + "default-src" => "localhost:* #{host}:*", + "script-src" => "localhost:* #{host}:*", + "style-src" => "localhost:* #{host}:*", + "img-src" => "localhost:* #{host}:*", + "font-src" => "localhost:* #{host}:*", + "connect-src" => "localhost:* #{host}:*", + "frame-src" => "localhost:* #{host}:* www.example.org", + "media-src" => "localhost:* #{host}:*" + } + end + colors do + { + primary: "#e02d2d", + secondary: "#155abf", + tertiary: "#ebc34b" + } + end + + trait :secure_context do + host { "localhost" } + end + + after(:create) do |organization, evaluator| + if evaluator.create_static_pages + tos_page = Decidim::StaticPage.find_by(slug: "terms-of-service", organization:) + create(:static_page, :tos, organization:, skip_injection: evaluator.skip_injection) if tos_page.nil? + end + end + end +end diff --git a/spec/system/admin_adds_integrations_on_proposals_spec.rb b/spec/system/admin_adds_integrations_on_proposals_spec.rb index 2ab14b4..970fee3 100644 --- a/spec/system/admin_adds_integrations_on_proposals_spec.rb +++ b/spec/system/admin_adds_integrations_on_proposals_spec.rb @@ -11,7 +11,7 @@ let!(:component) { create(:proposal_component, participatory_space: participatory_process) } let(:admin) { create(:user, :admin, :confirmed, organization:) } - context "when editing the proposals component" do + context "when dataspace is disabled" do before do switch_to_host(organization.host) login_as admin, scope: :user @@ -21,37 +21,61 @@ end end - context "and adding valid urls" do - it "can adds multiple integrations and updates component" do - # check add integration displays 2 divs - check I18n.t("decidim.components.proposals.settings.global.add_integration") - expect(page).to have_css("div.integration_url_container") - expect(page).to have_css("div.preferred_locale_container") - # provide valid urls, no error message displayed - fill_in "component[settings][integration_url]", with: "http://example.com, http://localhost:3000" - expect(page).to have_no_css("p.url_input_error") - select("fr", from: "component[settings][preferred_locale]") - # update component succesfully - click_link_or_button "Update" - expect(page).to have_content("The component was updated successfully.") - end + it "doesn't display the fields related to it" do + expect(page).not_to have_css("div.add_integration_container") + expect(page).not_to have_css("div.integration_url_container") + expect(page).not_to have_css("div.preferred_locale_container") + end + end + + context "when dataspace is enabled" do + before do + component.organization.enable_dataspace = true + component.organization.save! end - context "and adding invalid url" do - it "gets an error message" do - check I18n.t("decidim.components.proposals.settings.global.add_integration") - # provide invalid url - fill_in "component[settings][integration_url]", with: "http://localhost:3000, example.com" - # error message displayed when input looses focus - find_by_id("component_settings_preferred_locale").click - sleep(1) - expect(page).to have_css("p.url_input_error") - # providing a good url removes the error - fill_in "component[settings][integration_url]", with: "http://localhost:3000, http://example.com" - # error message removed when input looses focus - find_by_id("component_settings_preferred_locale").click - sleep(1) - expect(page).to have_no_css("p.url_input_error") + context "when editing the proposals component" do + before do + switch_to_host(organization.host) + login_as admin, scope: :user + visit decidim_admin_participatory_processes.components_path(participatory_process) + within ".component-#{component.id}" do + find("a[title='Configure']").click + end + end + + context "and adding valid urls" do + it "can adds multiple integrations and updates component" do + # check add integration displays 2 divs + check I18n.t("decidim.components.proposals.settings.global.add_integration") + expect(page).to have_css("div.integration_url_container") + expect(page).to have_css("div.preferred_locale_container") + # provide valid urls, no error message displayed + fill_in "component[settings][integration_url]", with: "http://example.com, http://localhost:3000" + expect(page).to have_no_css("p.url_input_error") + select("fr", from: "component[settings][preferred_locale]") + # update component succesfully + click_link_or_button "Update" + expect(page).to have_content("The component was updated successfully.") + end + end + + context "and adding invalid url" do + it "gets an error message" do + check I18n.t("decidim.components.proposals.settings.global.add_integration") + # provide invalid url + fill_in "component[settings][integration_url]", with: "http://localhost:3000, example.com" + # error message displayed when input looses focus + find_by_id("component_settings_preferred_locale").click + sleep(1) + expect(page).to have_css("p.url_input_error") + # providing a good url removes the error + fill_in "component[settings][integration_url]", with: "http://localhost:3000, http://example.com" + # error message removed when input looses focus + find_by_id("component_settings_preferred_locale").click + sleep(1) + expect(page).to have_no_css("p.url_input_error") + end end end end diff --git a/spec/system/external_proposal_spec.rb b/spec/system/external_proposal_spec.rb index fd3869a..3ad60dc 100644 --- a/spec/system/external_proposal_spec.rb +++ b/spec/system/external_proposal_spec.rb @@ -9,6 +9,7 @@ let!(:manifest_name) { "proposals" } let(:participatory_process) { create(:participatory_process, organization:) } let!(:component) { create(:proposal_component, participatory_space: participatory_process) } + let!(:proposals) { create_list(:proposal, 3, component:) } let(:authors) do [ @@ -35,125 +36,141 @@ ] end - before do - component.update!(settings: { add_integration: true, integration_url: "http://localhost:3000", preferred_locale: "en" }) - allow(GetDataFromApi).to receive(:contribution).and_return(json_contrib) - allow(GetDataFromApi).to receive(:authors).and_return(authors) - visit_external_proposal + context "when dataspace is disabled" do + before do + component.update!(settings: { add_integration: true, integration_url: "http://localhost:3000", preferred_locale: "en" }) + visit_external_proposal + end + + it "redirects to proposals index page" do + expect(page).to have_css("h1#proposals-count", text: "Proposals") + expect(page).to have_css("h2", text: "3 proposals") + end end - context "when the external proposal has no comments" do - let(:json_contrib) do - { - "reference" => "JD-PROP-2025-09-1", - "source" => "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", - "container" => "JD-PART-2025-09-1", - "locale" => "en", - "title" => "Quia sapiente.", - "content" => "Debitis repellat provident. Earum dolorem eaque. Aut quia officiis.", - "authors" => [ - "Aldo Davis" - ], - "parent" => nil, - "children" => nil, - "metadata" => { - "state" => { - "withdrawn" => false, - "emendation" => false, - "state" => "accepted" - } - }, - "created_at" => "2025-09-11T10:20:21.222Z", - "updated_at" => "2025-09-11T10:21:56.604Z", - "deleted_at" => nil - } + context "when dataspace is enabled" do + before do + component.organization.enable_dataspace = true + component.organization.save! + component.update!(settings: { add_integration: true, integration_url: "http://localhost:3000", preferred_locale: "en" }) + allow(GetDataFromApi).to receive(:contribution).and_return(json_contrib) + allow(GetDataFromApi).to receive(:authors).and_return(authors) + visit_external_proposal end - it "displays only external proposal" do - expect(page).to have_css("h1", text: "Quia sapiente.") - expect(page).to have_css("p.author_name", text: "Aldo Davis") - expect(page).to have_css("div.rich-text-display", text: json_contrib["content"]) - # check status is displayed - expect(page).to have_content("Accepted") - # check there is no comments - expect(page).to have_no_css("div#comments") + context "and the external proposal has no comments" do + let(:json_contrib) do + { + "reference" => "JD-PROP-2025-09-1", + "source" => "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", + "container" => "JD-PART-2025-09-1", + "locale" => "en", + "title" => "Quia sapiente.", + "content" => "Debitis repellat provident. Earum dolorem eaque. Aut quia officiis.", + "authors" => [ + "Aldo Davis" + ], + "parent" => nil, + "children" => nil, + "metadata" => { + "state" => { + "withdrawn" => false, + "emendation" => false, + "state" => "accepted" + } + }, + "created_at" => "2025-09-11T10:20:21.222Z", + "updated_at" => "2025-09-11T10:21:56.604Z", + "deleted_at" => nil + } + end + + it "displays only external proposal" do + expect(page).to have_css("h1", text: "Quia sapiente.") + expect(page).to have_css("p.author_name", text: "Aldo Davis") + expect(page).to have_css("div.rich-text-display", text: json_contrib["content"]) + # check status is displayed + expect(page).to have_content("Accepted") + # check there is no comments + expect(page).to have_no_css("div#comments") + end end - end - context "when the external proposal has comments" do - let(:json_contrib) do - { - "reference" => "JD-PROP-2025-09-1", - "source" => "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", - "container" => "JD-PART-2025-09-1", - "locale" => "en", - "title" => "Quia sapiente.", - "content" => "Debitis repellat provident. Earum dolorem eaque. Aut quia officiis.", - "authors" => [ - "Aldo Davis" - ], - "parent" => nil, - "children" => [ - { - "reference" => "JD-PROP-2025-09-1-249", - "source" => "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", - "container" => "JD-PART-2025-09-1", - "locale" => "en", - "title" => nil, - "content" => "Cumque hic quia veniam et dolores aliquam commodi laudantium omnis expedita enim natus et beatae quidem dolores architecto repudiandae rem a corporis impedit rerum fugit neque eos dicta deserunt consequatur numquam magnam voluptate inventore omnis aut porro nemo voluptas sit quia saepe aut provident accusantium voluptatem illum nam quaerat molestiae.", - "authors" => "Kautzer-Mayer", - "parent" => "JD-PROP-2025-09-1", - "children" => [ - "JD-PROP-2025-09-1-250" - ], - "metadata" => { - "depth" => 0 + context "and the external proposal has comments" do + let(:json_contrib) do + { + "reference" => "JD-PROP-2025-09-1", + "source" => "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", + "container" => "JD-PART-2025-09-1", + "locale" => "en", + "title" => "Quia sapiente.", + "content" => "Debitis repellat provident. Earum dolorem eaque. Aut quia officiis.", + "authors" => [ + "Aldo Davis" + ], + "parent" => nil, + "children" => [ + { + "reference" => "JD-PROP-2025-09-1-249", + "source" => "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", + "container" => "JD-PART-2025-09-1", + "locale" => "en", + "title" => nil, + "content" => "Cumque hic quia veniam et dolores aliquam commodi laudantium omnis expedita enim natus et beatae quidem dolores architecto repudiandae rem a corporis impedit rerum fugit neque eos dicta deserunt consequatur numquam magnam voluptate inventore omnis aut porro nemo voluptas sit quia saepe aut provident accusantium voluptatem illum nam quaerat molestiae.", + "authors" => "Kautzer-Mayer", + "parent" => "JD-PROP-2025-09-1", + "children" => [ + "JD-PROP-2025-09-1-250" + ], + "metadata" => { + "depth" => 0 + }, + "created_at" => "2025-09-11T10:20:23.609Z", + "updated_at" => "2025-09-11T10:20:23.609Z", + "deleted_at" => nil }, - "created_at" => "2025-09-11T10:20:23.609Z", - "updated_at" => "2025-09-11T10:20:23.609Z", - "deleted_at" => nil + { + "reference" => "JD-PROP-2025-09-1-250", + "source" => "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", + "container" => "JD-PART-2025-09-1", + "locale" => "en", + "title" => nil, + "content" => "Voluptatem illum sit eius eligendi omnis dolore qui alias et occaecati eos ipsum blanditiis unde fugit minus est quia excepturi eos ut nam iste molestias cupiditate et vel repellat quidem qui non est porro commodi quia mollitia reiciendis odit rem voluptas tempora autem et sequi quos provident accusantium fugiat accusamus.", + "authors" => "Aldo Davis", + "parent" => "JD-PROP-2025-09-1-249", + "children" => nil, + "metadata" => { + "depth" => 1 + }, + "created_at" => "2025-09-11T10:20:24.655Z", + "updated_at" => "2025-09-11T10:20:24.655Z", + "deleted_at" => nil + } + ], + "metadata" => { + "state" => { + "withdrawn" => false, + "emendation" => false, + "state" => nil + } }, - { - "reference" => "JD-PROP-2025-09-1-250", - "source" => "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1", - "container" => "JD-PART-2025-09-1", - "locale" => "en", - "title" => nil, - "content" => "Voluptatem illum sit eius eligendi omnis dolore qui alias et occaecati eos ipsum blanditiis unde fugit minus est quia excepturi eos ut nam iste molestias cupiditate et vel repellat quidem qui non est porro commodi quia mollitia reiciendis odit rem voluptas tempora autem et sequi quos provident accusantium fugiat accusamus.", - "authors" => "Aldo Davis", - "parent" => "JD-PROP-2025-09-1-249", - "children" => nil, - "metadata" => { - "depth" => 1 - }, - "created_at" => "2025-09-11T10:20:24.655Z", - "updated_at" => "2025-09-11T10:20:24.655Z", - "deleted_at" => nil - } - ], - "metadata" => { - "state" => { - "withdrawn" => false, - "emendation" => false, - "state" => nil - } - }, - "created_at" => "2025-09-11T10:20:21.222Z", - "updated_at" => "2025-09-11T10:21:56.604Z", - "deleted_at" => nil - } - end + "created_at" => "2025-09-11T10:20:21.222Z", + "updated_at" => "2025-09-11T10:21:56.604Z", + "deleted_at" => nil + } + end - it "displays external proposal with its comments" do - expect(page).to have_css("h1", text: "Quia sapiente.") - expect(page).to have_css("p.author_name", text: "Aldo Davis") - expect(page).to have_css("div.rich-text-display", text: json_contrib["content"]) - # check comments are displayed - expect(page).to have_css("div#comments") - expect(page).to have_css("span.comments-count", text: "2 comments") - within "div.comment-thread" do - expect(page).to have_css("div#comment_JD-PROP-2025-09-1-249") - expect(page).to have_css("div#accordion-JD-PROP-2025-09-1-249") + it "displays external proposal with its comments" do + expect(page).to have_css("h1", text: "Quia sapiente.") + expect(page).to have_css("p.author_name", text: "Aldo Davis") + expect(page).to have_css("div.rich-text-display", text: json_contrib["content"]) + # check comments are displayed + expect(page).to have_css("div#comments") + expect(page).to have_css("span.comments-count", text: "2 comments") + within "div.comment-thread" do + expect(page).to have_css("div#comment_JD-PROP-2025-09-1-249") + expect(page).to have_css("div#accordion-JD-PROP-2025-09-1-249") + end end end end diff --git a/spec/system/proposals_index_spec.rb b/spec/system/proposals_index_spec.rb index 7236c38..843e592 100644 --- a/spec/system/proposals_index_spec.rb +++ b/spec/system/proposals_index_spec.rb @@ -32,7 +32,7 @@ end end - context "when there is no external proposals" do + context "and there is no external proposals" do it "lists all the proposals" do create(:proposal_component, manifest:, @@ -125,60 +125,86 @@ allow(GetDataFromApi).to receive(:data).and_return(json) end - context "and there is one url in integration_url" do - before do - component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" }) + context "and dataspace is disabled" do + context "and there is one url in integration_url" do + before do + component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" }) + end + + it "lists only the proposals" do + visit_component + # 3 cards + expect(page).to have_css("a[class='card__list']", count: 3) + # 3 proposals + expect(page).to have_css("[id^='proposals__proposal']", count: 3) + # no external proposals + expect(page).not_to have_css("[id='JD-PROP-2025-09-1']") + expect(page).not_to have_css("[id='JD-PROP-2025-09-20']") + end end + end - it "lists the proposals ordered randomly by default and the external proposals at the end" do - visit_component - # 5 cards - expect(page).to have_css("a[class='card__list']", count: 5) - # 3 proposals - expect(page).to have_css("[id^='proposals__proposal']", count: 3) - # 2 external proposals - expect(page).to have_css("[id='JD-PROP-2025-09-1']", count: 1) - expect(page).to have_css("[id='JD-PROP-2025-09-20']", count: 1) + context "and dataspace is enabled" do + before do + component.organization.enable_dataspace = true + component.organization.save! end - context "and there are a lot of proposals" do + context "and there is one url in integration_url" do before do - # Decidim::Paginable::OPTIONS.first is 25 - create_list(:proposal, Decidim::Paginable::OPTIONS.first, component:) - # we have already create 3 proposals, so we will have a total of 28 proposals + component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" }) end - it "paginates them with proposals first and external proposals at the end" do + it "lists the proposals and the external proposals" do visit_component - # only proposals on first page - expect(page).to have_css("a[class='card__list']", count: Decidim::Paginable::OPTIONS.first) - expect(page).to have_css("[id^='proposals__proposal']", count: Decidim::Paginable::OPTIONS.first) - - click_on "Next" - # proposals and external proposals on second page + # 5 cards expect(page).to have_css("a[class='card__list']", count: 5) - expect(page).to have_css("[data-pages] [data-page][aria-current='page']", text: "2") + # 3 proposals expect(page).to have_css("[id^='proposals__proposal']", count: 3) + # 2 external proposals expect(page).to have_css("[id='JD-PROP-2025-09-1']", count: 1) expect(page).to have_css("[id='JD-PROP-2025-09-20']", count: 1) end - end - end - context "and there are 2 urls in integration_url" do - before do - component.update!(settings: { add_integration: true, integration_url: "http://example.org, http://example.org", preferred_locale: "en" }) + context "and there are a lot of proposals" do + before do + # Decidim::Paginable::OPTIONS.first is 25 + create_list(:proposal, Decidim::Paginable::OPTIONS.first, component:) + # we have already create 3 proposals, so we will have a total of 28 proposals + end + + it "paginates them with proposals first and external proposals at the end" do + visit_component + # only proposals on first page + expect(page).to have_css("a[class='card__list']", count: Decidim::Paginable::OPTIONS.first) + expect(page).to have_css("[id^='proposals__proposal']", count: Decidim::Paginable::OPTIONS.first) + + click_on "Next" + # proposals and external proposals on second page + expect(page).to have_css("a[class='card__list']", count: 5) + expect(page).to have_css("[data-pages] [data-page][aria-current='page']", text: "2") + expect(page).to have_css("[id^='proposals__proposal']", count: 3) + expect(page).to have_css("[id='JD-PROP-2025-09-1']", count: 1) + expect(page).to have_css("[id='JD-PROP-2025-09-20']", count: 1) + end + end end - it "returns the double amount of external proposals" do - visit_component - # 7 cards - expect(page).to have_css("a[class='card__list']", count: 7) - # 3 proposals - expect(page).to have_css("[id^='proposals__proposal']", count: 3) - # 4 external proposals - expect(page).to have_css("[id='JD-PROP-2025-09-1']", count: 2) - expect(page).to have_css("[id='JD-PROP-2025-09-20']", count: 2) + context "and there are 2 urls in integration_url" do + before do + component.update!(settings: { add_integration: true, integration_url: "http://example.org, http://example.org", preferred_locale: "en" }) + end + + it "returns the double amount of external proposals" do + visit_component + # 7 cards + expect(page).to have_css("a[class='card__list']", count: 7) + # 3 proposals + expect(page).to have_css("[id^='proposals__proposal']", count: 3) + # 4 external proposals + expect(page).to have_css("[id='JD-PROP-2025-09-1']", count: 2) + expect(page).to have_css("[id='JD-PROP-2025-09-20']", count: 2) + end end end end From 144f8cf4226c17db275bd66e547401e33a5c7f9a Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 2 Feb 2026 14:38:24 +0100 Subject: [PATCH 6/8] feat: update gemfile --- Gemfile | 4 ++-- Gemfile.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 7b75364..344652e 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source "https://rubygems.org" ruby RUBY_VERSION -gem "decidim", "~> 0.29.1" +gem "decidim", "~> 0.29.3" gem "decidim-dataspace", path: "." gem "bootsnap", "~> 1.4" @@ -14,7 +14,7 @@ gem "uri", ">= 1.0.4" group :development, :test do gem "byebug", "~> 11.0", platform: :mri - gem "decidim-dev", "~> 0.29.1" + gem "decidim-dev", "~> 0.29.3" end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index e989858..2372c81 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -819,9 +819,9 @@ PLATFORMS DEPENDENCIES bootsnap (~> 1.4) byebug (~> 11.0) - decidim (~> 0.29.1) + decidim (~> 0.29.3) decidim-dataspace! - decidim-dev (~> 0.29.1) + decidim-dev (~> 0.29.3) faker (~> 3.2) letter_opener_web (~> 2.0) listen (~> 3.1) From 432f88f99ea856368fd76c71320be479af4f370d Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 2 Feb 2026 17:24:07 +0100 Subject: [PATCH 7/8] style: refacto with rubocop --- ...7_add_enable_dataspace_to_organizations.rb | 2 +- .../system/create_organization_extends.rb | 1 - .../system/update_organization_extends.rb | 1 - .../proposals/proposals_controller_extends.rb | 34 ++++++++++--------- lib/extends/lib/decidim/core_extends.rb | 7 ++-- ...min_adds_integrations_on_proposals_spec.rb | 6 ++-- spec/system/proposals_index_spec.rb | 4 +-- 7 files changed, 26 insertions(+), 29 deletions(-) diff --git a/db/migrate/20260129143027_add_enable_dataspace_to_organizations.rb b/db/migrate/20260129143027_add_enable_dataspace_to_organizations.rb index f3ba260..97ece8e 100644 --- a/db/migrate/20260129143027_add_enable_dataspace_to_organizations.rb +++ b/db/migrate/20260129143027_add_enable_dataspace_to_organizations.rb @@ -2,7 +2,7 @@ class AddEnableDataspaceToOrganizations < ActiveRecord::Migration[7.0] def up - add_column :decidim_organizations, :enable_dataspace, :boolean, default: false + add_column :decidim_organizations, :enable_dataspace, :boolean, default: false, null: false end def down diff --git a/lib/extends/commands/decidim/system/create_organization_extends.rb b/lib/extends/commands/decidim/system/create_organization_extends.rb index b0a2d51..93d169d 100644 --- a/lib/extends/commands/decidim/system/create_organization_extends.rb +++ b/lib/extends/commands/decidim/system/create_organization_extends.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true - module CreateOrganizationExtends extend ActiveSupport::Concern diff --git a/lib/extends/commands/decidim/system/update_organization_extends.rb b/lib/extends/commands/decidim/system/update_organization_extends.rb index 0bff972..a46f1de 100644 --- a/lib/extends/commands/decidim/system/update_organization_extends.rb +++ b/lib/extends/commands/decidim/system/update_organization_extends.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true - module UpdateOrganizationExtends extend ActiveSupport::Concern diff --git a/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb b/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb index de3014a..c8bc70e 100644 --- a/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb +++ b/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb @@ -7,6 +7,8 @@ module ProposalsControllerExtends extend ActiveSupport::Concern included do + before_action :dataspace_enabled, only: :external_proposal + def index if component_settings.participatory_texts_enabled? @proposals = Decidim::Proposals::Proposal.where(component: current_component) @@ -41,22 +43,18 @@ def index end def external_proposal - if verify_dataspace? - uri = URI.parse(params[:url]) - url = "#{uri.scheme}://#{uri.host}" - url += ":3000" if uri.host == "localhost" - - @external_proposal = GetDataFromApi.contribution(url, params[:reference], component_settings.preferred_locale || "en", "true") - return if @external_proposal.nil? - - @comments = @external_proposal["children"] - @parent_comments = @comments.select { |comment| comment["parent"] == @external_proposal["reference"] } if @comments - @authors = GetDataFromApi.authors(url, component_settings.preferred_locale || "en") - .select { |author| @external_proposal["authors"].include?(author["reference"]) } - .map { |author| author["name"] }.join(", ") - else - redirect_to root_url - end + uri = URI.parse(params[:url]) + url = "#{uri.scheme}://#{uri.host}" + url += ":3000" if uri.host == "localhost" + + @external_proposal = GetDataFromApi.contribution(url, params[:reference], component_settings.preferred_locale || "en", "true") + return if @external_proposal.nil? + + @comments = @external_proposal["children"] + @parent_comments = @comments.select { |comment| comment["parent"] == @external_proposal["reference"] } if @comments + @authors = GetDataFromApi.authors(url, component_settings.preferred_locale || "en") + .select { |author| @external_proposal["authors"].include?(author["reference"]) } + .map { |author| author["name"] }.join(", ") end private @@ -65,6 +63,10 @@ def verify_dataspace? current_organization.enable_dataspace == true && component_settings.add_integration && component_settings.integration_url.present? end + def dataspace_enabled + redirect_to(root_url) && return unless verify_dataspace? + end + def voted_proposals if current_user Decidim::Proposals::ProposalVote.where( diff --git a/lib/extends/lib/decidim/core_extends.rb b/lib/extends/lib/decidim/core_extends.rb index c2d4319..039ca11 100644 --- a/lib/extends/lib/decidim/core_extends.rb +++ b/lib/extends/lib/decidim/core_extends.rb @@ -12,11 +12,8 @@ module CoreExtends lambda do |resource, component| ref = "" - if resource.is_a?(Decidim::HasComponent) && component.present? - # It is a component resource - ref = component.participatory_space.organization.reference_prefix - elsif resource.is_a?(Decidim::Comments::Comment) && component.present? - # It is a comment resource + if (resource.is_a?(Decidim::HasComponent) || resource.is_a?(Decidim::Comments::Comment)) && component.present? + # It is a component resource or a comment resource ref = component.participatory_space.organization.reference_prefix elsif resource.is_a?(Decidim::Participable) # It is a participatory space diff --git a/spec/system/admin_adds_integrations_on_proposals_spec.rb b/spec/system/admin_adds_integrations_on_proposals_spec.rb index 970fee3..359d230 100644 --- a/spec/system/admin_adds_integrations_on_proposals_spec.rb +++ b/spec/system/admin_adds_integrations_on_proposals_spec.rb @@ -22,9 +22,9 @@ end it "doesn't display the fields related to it" do - expect(page).not_to have_css("div.add_integration_container") - expect(page).not_to have_css("div.integration_url_container") - expect(page).not_to have_css("div.preferred_locale_container") + expect(page).to have_no_css("div.add_integration_container") + expect(page).to have_no_css("div.integration_url_container") + expect(page).to have_no_css("div.preferred_locale_container") end end diff --git a/spec/system/proposals_index_spec.rb b/spec/system/proposals_index_spec.rb index 843e592..40aa825 100644 --- a/spec/system/proposals_index_spec.rb +++ b/spec/system/proposals_index_spec.rb @@ -138,8 +138,8 @@ # 3 proposals expect(page).to have_css("[id^='proposals__proposal']", count: 3) # no external proposals - expect(page).not_to have_css("[id='JD-PROP-2025-09-1']") - expect(page).not_to have_css("[id='JD-PROP-2025-09-20']") + expect(page).to have_no_css("[id='JD-PROP-2025-09-1']") + expect(page).to have_no_css("[id='JD-PROP-2025-09-20']") end end end From d0ba4232a6c98f9ded010119bf3587f20b590944 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Mon, 2 Feb 2026 17:26:37 +0100 Subject: [PATCH 8/8] ci: add lint --- .github/workflows/ci_dataspace.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_dataspace.yml b/.github/workflows/ci_dataspace.yml index ba53310..1fe974b 100644 --- a/.github/workflows/ci_dataspace.yml +++ b/.github/workflows/ci_dataspace.yml @@ -20,7 +20,17 @@ concurrency: cancel-in-progress: true jobs: - main: + lint: + name: Lint code + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: rokroskar/workflow-run-cleanup-action@v0.3.0 + if: "github.ref != 'refs/heads/develop'" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + - uses: OpenSourcePolitics/lint-action@master + tests: name: Tests runs-on: ubuntu-latest timeout-minutes: 30