diff --git a/app/packs/stylesheets/decidim/dataspace/dataspace.scss b/app/packs/stylesheets/decidim/dataspace/dataspace.scss index 18438b5..b90b200 100644 --- a/app/packs/stylesheets/decidim/dataspace/dataspace.scss +++ b/app/packs/stylesheets/decidim/dataspace/dataspace.scss @@ -42,3 +42,6 @@ p.author_name{ margin-left: 0.75rem; } +.card__list-external { + border-color: rgb(var(--tertiary-rgb) / var(--tw-ring-opacity, 1)); +} diff --git a/app/views/decidim/proposals/proposals/_external_proposal.html.erb b/app/views/decidim/proposals/proposals/_external_proposal.html.erb index 1273ecd..cc67e81 100644 --- a/app/views/decidim/proposals/proposals/_external_proposal.html.erb +++ b/app/views/decidim/proposals/proposals/_external_proposal.html.erb @@ -8,7 +8,7 @@
<%= content_tag :h4, external_proposal["title"], class: "h4 text-secondary" %> - <% if external_proposal["metadata"]["state"]["state"].present? %> + <% if external_proposal.dig("metadata", "state", "state").present? %> <%= external_state_item(external_proposal["metadata"]["state"]) %> <% end %>
@@ -22,17 +22,31 @@
<%= icon("wechat-line") %> -

<%= external_proposal["children"].count %>

+

<%= external_proposal["children"].present? ? external_proposal["children"].count : 0 %>

<% end %> <% else %> - <%= link_to external_proposal["source"], class: "card__list", id: external_proposal["reference"] do %> + <%= link_to external_proposal_proposals_path(external_proposal["reference"], url: external_proposal["source"]), class: "card__list card__list-external", id: external_proposal["reference"] do %> <%= content_tag(:div, class: "card__list-content") do %> <%= content_tag :h4, class: "h4 card__list-title" do %> - <%= external_proposal["title"] %> + <%= external_proposal["title"] %> ( <%= t('.view_from', platform: display_host(external_proposal["source"])) %>) <% end %> -

Proposal from external platform

+
+
+
+ default avatar"> +
+

<%= @authors.select{|author| external_proposal["authors"].include?(author["reference"])}.map{|author| author["name"]}.join(', ') %>

+
+
+ <%= icon("wechat-line") %> +

<%= external_proposal["children"].present? ? external_proposal["children"].count : 0 %>

+
+ <% if external_proposal.dig("metadata", "state", "state").present? %> +

<%= external_state_item(external_proposal["metadata"]["state"]) %>

+ <% end %> +
<% end %> <% end %> <% end %> diff --git a/app/views/decidim/proposals/proposals/external_proposal.html.erb b/app/views/decidim/proposals/proposals/external_proposal.html.erb index f4b2727..9cda7e7 100644 --- a/app/views/decidim/proposals/proposals/external_proposal.html.erb +++ b/app/views/decidim/proposals/proposals/external_proposal.html.erb @@ -11,7 +11,7 @@
<%= render partial: "voting_rules" %> -

<%= @external_proposal["title"] %>

+

<%= @external_proposal["title"] %> (<%= t('.view_from', platform: display_host(@external_proposal["source"])) %>)

default avatar"> diff --git a/app/views/decidim/proposals/proposals/index.js.erb b/app/views/decidim/proposals/proposals/index.js.erb new file mode 100644 index 0000000..7960009 --- /dev/null +++ b/app/views/decidim/proposals/proposals/index.js.erb @@ -0,0 +1,17 @@ +var $proposals = $('#proposals'); +var $orderFilterInput = $('.order_filter'); + +$proposals.html('<%= j(render partial: "proposals").strip.html_safe %>'); +$orderFilterInput.val('<%= order %>'); + +<% if Decidim::Map.available?(:geocoding, :dynamic) && component_settings.geocoding_enabled? %> +var $map = $("#map"); +var controller = $map.data("map-controller"); +if (controller) { + var markerData = JSON.parse('<%= escape_javascript proposals_data_for_map(@proposals).to_json.html_safe %>'); + controller.clearMarkers(); + if (markerData.length > 0 ) { + controller.addMarkers(markerData); + } +} +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index fd86324..52715c8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,3 +29,8 @@ en: proposals: external_proposal: view_from: View from platform %{platform} + layouts: + decidim: + shared: + layout_item: + view_from: view from plateform %{platform} diff --git a/config/locales/fr.yml b/config/locales/fr.yml index ab2a842..46377e4 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -29,3 +29,8 @@ fr: proposals: external_proposal: view_from: Vue de la plateforme %{platform} + layouts: + decidim: + shared: + layout_item: + view_from: vue de la plateforme %{platform} diff --git a/lib/decidim/dataspace/engine.rb b/lib/decidim/dataspace/engine.rb index 221ad09..3777699 100644 --- a/lib/decidim/dataspace/engine.rb +++ b/lib/decidim/dataspace/engine.rb @@ -41,12 +41,13 @@ class Engine < ::Rails::Engine initializer "dataspace-extends" do config.after_initialize do - require "extends/controllers/decidim/proposals/proposals_controller_extends" + require "extends/controllers/decidim/proposals/proposals_controller_dataspace_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" + require "extends/forms/decidim/system/register_organization_form_extends" + require "extends/forms/decidim/system/update_organization_form_extends" end end diff --git a/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb b/lib/extends/controllers/decidim/proposals/proposals_controller_dataspace_extends.rb similarity index 98% rename from lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb rename to lib/extends/controllers/decidim/proposals/proposals_controller_dataspace_extends.rb index c8bc70e..1fa2ce9 100644 --- a/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb +++ b/lib/extends/controllers/decidim/proposals/proposals_controller_dataspace_extends.rb @@ -3,7 +3,7 @@ require "active_support/concern" require "uri" -module ProposalsControllerExtends +module ProposalsControllerDataspaceExtends extend ActiveSupport::Concern included do @@ -128,4 +128,4 @@ def create_pagination_object(total_count, current_page, per_page) end end -Decidim::Proposals::ProposalsController.include(ProposalsControllerExtends) +Decidim::Proposals::ProposalsController.include(ProposalsControllerDataspaceExtends) diff --git a/lib/extends/forms/decidim/system/register_organization_form_extends.rb b/lib/extends/forms/decidim/system/register_organization_form_extends.rb new file mode 100644 index 0000000..472475d --- /dev/null +++ b/lib/extends/forms/decidim/system/register_organization_form_extends.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module RegisterOrganizationFormExtends + extend ActiveSupport::Concern + + included do + attribute :enable_dataspace, Decidim::AttributeObject::TypeMap::Boolean + end +end + +Decidim::System::RegisterOrganizationForm.include(RegisterOrganizationFormExtends) diff --git a/lib/extends/forms/decidim/system/base_organization_form_extends.rb b/lib/extends/forms/decidim/system/update_organization_form_extends.rb similarity index 58% rename from lib/extends/forms/decidim/system/base_organization_form_extends.rb rename to lib/extends/forms/decidim/system/update_organization_form_extends.rb index 85a451e..6b850a4 100644 --- a/lib/extends/forms/decidim/system/base_organization_form_extends.rb +++ b/lib/extends/forms/decidim/system/update_organization_form_extends.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module BaseOrganizationFormExtends +module UpdateOrganizationFormExtends extend ActiveSupport::Concern included do @@ -8,4 +8,4 @@ module BaseOrganizationFormExtends end end -Decidim::System::BaseOrganizationForm.include(BaseOrganizationFormExtends) +Decidim::System::UpdateOrganizationForm.include(UpdateOrganizationFormExtends) diff --git a/spec/forms/register_organization_form_spec.rb b/spec/forms/register_organization_form_spec.rb new file mode 100644 index 0000000..acbc62b --- /dev/null +++ b/spec/forms/register_organization_form_spec.rb @@ -0,0 +1,379 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::System + describe RegisterOrganizationForm do + subject do + described_class.new( + name: "Gotham City", + host: "decide.example.org", + secondary_hosts: "foo.example.org\r\n\r\nbar.example.org", + reference_prefix: "JKR", + organization_admin_name: "Fiorello Henry La Guardia", + organization_admin_email: "f.laguardia@example.org", + available_locales: ["en"], + default_locale: "en", + users_registration_mode: "enabled", + force_users_to_authenticate_before_access_organization: "false", + enable_dataspace:, + **smtp_settings, + **omniauth_settings + ) + end + + let(:enable_dataspace) { false } + let(:omniauth_settings) do + { + "omniauth_settings_facebook_enabled" => true, + "omniauth_settings_facebook_app_id" => facebook_app_id, + "omniauth_settings_facebook_app_secret" => facebook_app_secret + } + end + let(:smtp_settings) do + { + "address" => "mail.example.org", + "port" => 25, + "user_name" => "f.laguardia", + "password" => password, + "from_email" => "decide@example.org", + "from_label" => from_label + } + end + let(:password) { "secret_password" } + let(:from_label) { "Decide Gotham" } + let(:facebook_app_id) { "plain-text-facebook-app-id" } + let(:facebook_app_secret) { "plain-text-facebook-app-secret" } + + context "when everything is OK" do + it { is_expected.to be_valid } + + describe "enable_dataspace" do + context "when enable_dataspace is true" do + let(:enable_dataspace) { true } + + it { is_expected.to be_valid } + end + end + + describe "omniauth_settings" do + it "contains attributes as plain text" do + expect(subject.omniauth_settings_facebook_enabled).to be(true) + expect(subject.omniauth_settings_facebook_app_id).to eq(facebook_app_id) + expect(subject.omniauth_settings_facebook_app_secret).to eq(facebook_app_secret) + end + + context "when all values are blank" do + let(:omniauth_settings) do + { + "omniauth_settings_facebook_enabled" => nil, + "omniauth_settings_facebook_app_id" => nil, + "omniauth_settings_facebook_app_secret" => nil + } + end + + it "returns nil" do + expect(subject.encrypted_omniauth_settings).to be_nil + end + end + end + + describe "encrypted_omniauth_settings" do + it "encrypts sensible attributes" do + encrypted_settings = subject.encrypted_omniauth_settings + + expect(encrypted_settings["omniauth_settings_facebook_enabled"]).to be(true) + expect( + Decidim::AttributeEncryptor.decrypt(encrypted_settings["omniauth_settings_facebook_app_id"]) + ).to eq(facebook_app_id) + expect( + Decidim::AttributeEncryptor.decrypt(encrypted_settings["omniauth_settings_facebook_app_secret"]) + ).to eq(facebook_app_secret) + end + end + + describe "#set_from" do + it "concatenates from_label and from_email" do + from = subject.set_from + + expect(from).to eq("Decide Gotham ") + end + + context "when from_label is empty" do + let(:from_label) { "" } + + it "returns the email" do + from = subject.set_from + + expect(from).to eq("decide@example.org") + end + end + end + + describe "smtp_settings" do + it "handles SMTP password properly" do + expect(subject.smtp_settings).to eq(smtp_settings.except("password")) + expect(Decidim::AttributeEncryptor.decrypt(subject.encrypted_smtp_settings[:encrypted_password])).to eq(password) + end + + context "when all values are blank" do + let(:smtp_settings) do + { + "address" => "", + "port" => "", + "user_name" => "", + "password" => "", + "from_email" => "", + "from_label" => "" + } + end + + it "returns nil" do + expect(subject.encrypted_smtp_settings).to be_nil + end + end + end + end + + describe "validations" do + describe "organization_admin_email" do + context "when organization_admin_email is blank" do + before { subject.organization_admin_email = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:organization_admin_email]).to include("cannot be blank") + end + end + + context "when organization_admin_email is nil" do + before { subject.organization_admin_email = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "organization_admin_name" do + context "when organization_admin_name is blank" do + before { subject.organization_admin_name = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:organization_admin_name]).to include("cannot be blank") + end + end + + context "when organization_admin_name is nil" do + before { subject.organization_admin_name = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "name" do + context "when name is blank" do + before { subject.name = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name]).to include("cannot be blank") + end + end + + context "when name is nil" do + before { subject.name = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "reference_prefix" do + context "when reference_prefix is blank" do + before { subject.reference_prefix = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:reference_prefix]).to include("cannot be blank") + end + end + + context "when reference_prefix is nil" do + before { subject.reference_prefix = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "available_locales" do + context "when available_locales is blank" do + before { subject.available_locales = [] } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:available_locales]).to include("cannot be blank") + end + end + + context "when available_locales is nil" do + before { subject.available_locales = nil } + + it { is_expected.not_to be_valid } + end + end + + describe "default_locale" do + context "when default_locale is blank" do + before { subject.default_locale = "" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:default_locale]).to include("cannot be blank") + end + end + + context "when default_locale is nil" do + before { subject.default_locale = nil } + + it { is_expected.not_to be_valid } + end + + context "when default_locale is not included in available_locales" do + before do + subject.available_locales = %w(en es) + subject.default_locale = "fr" + end + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:default_locale]).to include("is not included in the list") + end + end + + context "when default_locale is included in available_locales" do + before do + subject.available_locales = %w(en es fr) + subject.default_locale = "fr" + end + + it { is_expected.to be_valid } + end + end + + describe "organization uniqueness" do + let!(:existing_organization) do + create( + :organization, + name: { en: "Existing City", es: "Ciudad Existente" }, + host: "existing.example.org" + ) + end + + context "when organization name already exists (case-insensitive)" do + before { subject.name = "EXISTING CITY" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name]).to include("has already been taken") + end + end + + context "when organization name already exists in different locale" do + before { subject.name = "Ciudad Existente" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name]).to include("has already been taken") + end + end + + context "when host already exists" do + before { subject.host = "existing.example.org" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:host]).to include("has already been taken") + end + end + + context "when organization name is unique" do + before { subject.name = "Unique City" } + + it { is_expected.to be_valid } + end + + context "when host is unique" do + before { subject.host = "unique.example.org" } + + it { is_expected.to be_valid } + end + end + end + + describe "#map_model" do + subject { described_class.from_model(organization) } + + let(:organization) do + create( + :organization, + secondary_hosts: ["foobar.example.org", "foobaz.example.org"], + omniauth_settings: { + omniauth_settings_facebook_enabled: Decidim::AttributeEncryptor.encrypt(true), + omniauth_settings_facebook_app_id: Decidim::AttributeEncryptor.encrypt("foo") + }, + file_upload_settings: { + allowed_file_extensions: { + default: %w(jpg jpeg), + admin: %w(jpg jpeg png), + image: %w(jpg jpeg png) + }, + allowed_content_types: { + default: %w(image/*), + admin: %w(image/*) + }, + maximum_file_size: { + default: 7.2, + avatar: 2.4 + } + } + ) + end + + it "maps the organization attributes correctly" do + expect(subject.secondary_hosts).to eq(organization.secondary_hosts.join("\n")) + expect(subject.omniauth_settings).to eq( + { + "omniauth_settings_facebook_app_id" => "foo", + "omniauth_settings_facebook_enabled" => true + } + ) + expect(subject.file_upload_settings.final).to eq( + { + allowed_content_types: { "admin" => %w(image/*), "default" => %w(image/*) }, + allowed_file_extensions: { "admin" => %w(jpg jpeg png), "default" => %w(jpg jpeg), "image" => %w(jpg jpeg png) }, + maximum_file_size: { "avatar" => 2.4, "default" => 7.2 } + } + ) + end + end + end +end diff --git a/spec/forms/update_organization_form_spec.rb b/spec/forms/update_organization_form_spec.rb new file mode 100644 index 0000000..e41c94a --- /dev/null +++ b/spec/forms/update_organization_form_spec.rb @@ -0,0 +1,408 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::System + describe UpdateOrganizationForm do + subject do + described_class.new( + name: { ca: "", en: "Gotham City", es: "" }, + host: "decide.example.org", + secondary_hosts: "foo.example.org\r\n\r\nbar.example.org", + reference_prefix: "JKR", + organization_admin_name: "Fiorello Henry La Guardia", + organization_admin_email: "f.laguardia@example.org", + available_locales: ["en"], + default_locale: "en", + users_registration_mode: "enabled", + force_users_to_authenticate_before_access_organization: "false", + enable_dataspace:, + **smtp_settings, + **omniauth_settings + ) + end + + let(:enable_dataspace) { false } + let(:omniauth_settings) do + { + "omniauth_settings_facebook_enabled" => true, + "omniauth_settings_facebook_app_id" => facebook_app_id, + "omniauth_settings_facebook_app_secret" => facebook_app_secret + } + end + let(:smtp_settings) do + { + "address" => "mail.example.org", + "port" => 25, + "user_name" => "f.laguardia", + "password" => password, + "from_email" => "decide@example.org", + "from_label" => from_label + } + end + let(:password) { "secret_password" } + let(:from_label) { "Decide Gotham" } + let(:facebook_app_id) { "plain-text-facebook-app-id" } + let(:facebook_app_secret) { "plain-text-facebook-app-secret" } + + context "when everything is OK" do + it { is_expected.to be_valid } + + describe "enable_dataspace" do + context "when enable_dataspace is true" do + let(:enable_dataspace) { true } + + it { is_expected.to be_valid } + end + end + + describe "omniauth_settings" do + it "contains attributes as plain text" do + expect(subject.omniauth_settings_facebook_enabled).to be(true) + expect(subject.omniauth_settings_facebook_app_id).to eq(facebook_app_id) + expect(subject.omniauth_settings_facebook_app_secret).to eq(facebook_app_secret) + end + + context "when all values are blank" do + let(:omniauth_settings) do + { + "omniauth_settings_facebook_enabled" => nil, + "omniauth_settings_facebook_app_id" => nil, + "omniauth_settings_facebook_app_secret" => nil + } + end + + it "returns nil" do + expect(subject.encrypted_omniauth_settings).to be_nil + end + end + end + + describe "encrypted_omniauth_settings" do + it "encrypts sensible attributes" do + encrypted_settings = subject.encrypted_omniauth_settings + + expect(encrypted_settings["omniauth_settings_facebook_enabled"]).to be(true) + expect( + Decidim::AttributeEncryptor.decrypt(encrypted_settings["omniauth_settings_facebook_app_id"]) + ).to eq(facebook_app_id) + expect( + Decidim::AttributeEncryptor.decrypt(encrypted_settings["omniauth_settings_facebook_app_secret"]) + ).to eq(facebook_app_secret) + end + end + + describe "#set_from" do + it "concatenates from_label and from_email" do + from = subject.set_from + + expect(from).to eq("Decide Gotham ") + end + + context "when from_label is empty" do + let(:from_label) { "" } + + it "returns the email" do + from = subject.set_from + + expect(from).to eq("decide@example.org") + end + end + end + + describe "smtp_settings" do + it "handles SMTP password properly" do + expect(subject.smtp_settings).to eq(smtp_settings.except("password")) + expect(Decidim::AttributeEncryptor.decrypt(subject.encrypted_smtp_settings[:encrypted_password])).to eq(password) + end + + context "when all values are blank" do + let(:smtp_settings) do + { + "address" => "", + "port" => "", + "user_name" => "", + "password" => "", + "from_email" => "", + "from_label" => "" + } + end + + it "returns nil" do + expect(subject.encrypted_smtp_settings).to be_nil + end + end + end + end + + describe "validations" do + describe "organization name presence" do + let(:organization) { create(:organization, default_locale: "en") } + + before do + subject.id = organization.id + allow(subject).to receive(:current_organization).and_return(organization) + end + + context "when name in default locale is present" do + before { subject.name = { en: "Gotham City" } } + + it { is_expected.to be_valid } + end + + context "when name in default locale is blank" do + before { subject.name = { en: "" } } + + it { is_expected.not_to be_valid } + + it "adds an error to the default locale name attribute" do + subject.valid? + expect(subject.errors[:name_en]).to include("cannot be blank") + end + end + + context "when organization has different default locale" do + let(:organization) { create(:organization, default_locale: "es") } + + before do + subject.default_locale = "es" + subject.name = { es: "" } + end + + it { is_expected.not_to be_valid } + + it "adds an error to the correct locale name attribute" do + subject.valid? + expect(subject.errors[:name_es]).to include("cannot be blank") + end + end + + context "when current_organization is not set" do + before do + allow(subject).to receive(:current_organization).and_return(nil) + subject.send(:"name_#{Decidim.default_locale}=", "") + end + + it { is_expected.not_to be_valid } + + it "uses Decidim default locale" do + subject.valid? + expect(subject.errors[:"name_#{Decidim.default_locale}"]).to include("cannot be blank") + end + end + end + + describe "organization uniqueness" do + let!(:existing_organization) do + create( + :organization, + name: { en: "Existing City", es: "Ciudad Existente" }, + host: "existing.example.org" + ) + end + + context "when creating a new organization" do + context "when organization name already exists (case-insensitive)" do + before { subject.name_en = "EXISTING CITY" } + + it { is_expected.not_to be_valid } + + it "adds an error to the name attribute" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + end + end + + context "when organization name already exists in different locale" do + before { subject.name_en = "Ciudad Existente" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + end + end + + context "when multiple locale names conflict" do + before do + subject.name_en = "Existing City" + subject.name_es = "Ciudad Existente" + end + + it { is_expected.not_to be_valid } + + it "adds errors to both locale attributes" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + expect(subject.errors[:name_es]).to include("has already been taken") + end + end + + context "when host already exists" do + before { subject.host = "existing.example.org" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:host]).to include("has already been taken") + end + end + + context "when organization name is unique" do + before { subject.name_en = "Unique City" } + + it { is_expected.to be_valid } + end + + context "when host is unique" do + before { subject.host = "unique.example.org" } + + it { is_expected.to be_valid } + end + end + + context "when updating an existing organization" do + let(:organization_to_update) do + create( + :organization, + name: { en: "My City", es: "Mi Ciudad" }, + host: "mycity.example.org" + ) + end + + before do + subject.id = organization_to_update.id + end + + context "when keeping the same name" do + before { subject.name_en = "My City" } + + it { is_expected.to be_valid } + end + + context "when keeping the same host" do + before { subject.host = "mycity.example.org" } + + it { is_expected.to be_valid } + end + + context "when changing name to an existing one" do + before { subject.name_en = "Existing City" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + end + end + + context "when changing host to an existing one" do + before { subject.host = "existing.example.org" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:host]).to include("has already been taken") + end + end + + context "when changing name to a unique one" do + before { subject.name_en = "Brand New City" } + + it { is_expected.to be_valid } + end + + context "when changing host to a unique one" do + before { subject.host = "other.example.org" } + + it { is_expected.to be_valid } + end + end + + context "when name contains machine_translations" do + let!(:org_with_translations) do + create( + :organization, + name: { + :en => "City", + "machine_translations" => { fr: "Ville" } + } + ) + end + + context "when new name conflicts with machine translation" do + before { subject.name_en = "Ville" } + + it { is_expected.not_to be_valid } + + it "adds an error" do + subject.valid? + expect(subject.errors[:name_en]).to include("has already been taken") + end + end + end + + context "when name value is a Hash (nested structure)" do + before do + allow(subject).to receive(:name).and_return({ en: { nested: "value" }, es: "Valid Name" }) + end + + it "skips Hash values during validation" do + expect { subject.valid? }.not_to raise_error + end + end + end + end + + describe "#map_model" do + subject { described_class.from_model(organization) } + + let(:organization) do + create( + :organization, + secondary_hosts: ["foobar.example.org", "foobaz.example.org"], + omniauth_settings: { + omniauth_settings_facebook_enabled: Decidim::AttributeEncryptor.encrypt(true), + omniauth_settings_facebook_app_id: Decidim::AttributeEncryptor.encrypt("foo") + }, + file_upload_settings: { + allowed_file_extensions: { + default: %w(jpg jpeg), + admin: %w(jpg jpeg png), + image: %w(jpg jpeg png) + }, + allowed_content_types: { + default: %w(image/*), + admin: %w(image/*) + }, + maximum_file_size: { + default: 7.2, + avatar: 2.4 + } + } + ) + end + + it "maps the organization attributes correctly" do + expect(subject.secondary_hosts).to eq(organization.secondary_hosts.join("\n")) + expect(subject.omniauth_settings).to eq( + { + "omniauth_settings_facebook_app_id" => "foo", + "omniauth_settings_facebook_enabled" => true + } + ) + expect(subject.file_upload_settings.final).to eq( + { + allowed_content_types: { "admin" => %w(image/*), "default" => %w(image/*) }, + allowed_file_extensions: { "admin" => %w(jpg jpeg png), "default" => %w(jpg jpeg), "image" => %w(jpg jpeg png) }, + maximum_file_size: { "avatar" => 2.4, "default" => 7.2 } + } + ) + end + end + end +end diff --git a/spec/system/proposals_index_spec.rb b/spec/system/proposals_index_spec.rb index 40aa825..458057a 100644 --- a/spec/system/proposals_index_spec.rb +++ b/spec/system/proposals_index_spec.rb @@ -157,11 +157,10 @@ it "lists the proposals and the external proposals" 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) + expect(page).to have_css("a[class='card__list']", count: 3) # 2 external proposals + expect(page).to have_css("a[class='card__list card__list-external']", count: 2) 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 @@ -181,9 +180,9 @@ 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("a[class='card__list']", count: 3) + expect(page).to have_css("a[class='card__list card__list-external']", count: 2) 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 @@ -197,11 +196,10 @@ 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) + expect(page).to have_css("a[class='card__list']", count: 3) # 4 external proposals + expect(page).to have_css("a[class='card__list card__list-external']", count: 4) 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