diff --git a/app/helpers/decidim/proposals/external_proposal_helper.rb b/app/helpers/decidim/proposals/external_proposal_helper.rb index bb1abd0..602df84 100644 --- a/app/helpers/decidim/proposals/external_proposal_helper.rb +++ b/app/helpers/decidim/proposals/external_proposal_helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "uri" + module Decidim module Proposals module ExternalProposalHelper @@ -42,6 +44,11 @@ def external_css_style(state) "" end end + + def display_host(url) + uri = URI.parse(url) + uri.host + end end end end diff --git a/app/packs/src/decidim/dataspace/component_edit_form.js b/app/packs/src/decidim/dataspace/component_edit_form.js index c890da8..e4db964 100644 --- a/app/packs/src/decidim/dataspace/component_edit_form.js +++ b/app/packs/src/decidim/dataspace/component_edit_form.js @@ -3,24 +3,54 @@ document.addEventListener("DOMContentLoaded", function(){ const urlDiv = document.querySelector("div.integration_url_container") const localeDiv = document.querySelector("div.preferred_locale_container") const inputUrl = document.querySelector("input[name='component[settings][integration_url]']") - inputUrl.setAttribute("placeholder", "https://platform.com") + inputUrl.setAttribute("placeholder", "https://platform.com, https://example.com") if(integrationCheck){ if(integrationCheck.checked){ - urlDiv.style.display = "block" - localeDiv.style.display = "block" + urlDiv.style.display = "block"; + localeDiv.style.display = "block"; } else { - urlDiv.style.display = "none" - localeDiv.style.display = "none" + urlDiv.style.display = "none"; + localeDiv.style.display = "none"; } integrationCheck.addEventListener('change', function(){ if (this.checked) { - urlDiv.style.display = "block" - localeDiv.style.display = "block" + urlDiv.style.display = "block"; + localeDiv.style.display = "block"; } else { - urlDiv.style.display = "none" - localeDiv.style.display = "none" + urlDiv.style.display = "none"; + localeDiv.style.display = "none"; } }) } + // check validity of urls when input looses focus + inputUrl.addEventListener("blur", checkUrl) + function checkUrl(event){ + const values = event.target.value; + const errors = []; + values.split(",").forEach(function(value){ + try { + // if value is not valid, it will throw a TypeError + const url = new URL(value); + } catch(error){ + errors.push(error); + } + }) + if(errors.length !== 0 && inputUrl.parentNode.lastChild === inputUrl){ + // create p + const elem = document.createElement('p'); + // create content + const newContent = document.createTextNode("There is an invalid url"); + // add content to p + elem.appendChild(newContent); + // add style and class to p + elem.style.color = "red"; + elem.classList.add('url_input_error'); + // insert p after input + inputUrl.after(elem); + } else if(errors.length === 0 && inputUrl.parentNode.lastChild !== inputUrl){ + const elem = document.querySelector('p.url_input_error'); + inputUrl.parentNode.removeChild(elem); + } + } }) diff --git a/app/views/decidim/proposals/proposals/_external_proposal.html.erb b/app/views/decidim/proposals/proposals/_external_proposal.html.erb index 76cf14e..1273ecd 100644 --- a/app/views/decidim/proposals/proposals/_external_proposal.html.erb +++ b/app/views/decidim/proposals/proposals/_external_proposal.html.erb @@ -1,9 +1,9 @@ <% if card_size == :g %> - <%= link_to external_proposal_proposals_path(external_proposal["reference"]), class: "card__grid-external", id: external_proposal["reference"] do %> + <%= link_to external_proposal_proposals_path(external_proposal["reference"], url: external_proposal["source"]), class: "card__grid-external", id: external_proposal["reference"] do %>
<%= external_icon "media/images/placeholder-card-g.svg", class: "card__placeholder-g" %> -

<%= t('.view_from', platform: @platform) %>

+

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

diff --git a/config/locales/en.yml b/config/locales/en.yml index 006504b..8bb978c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,8 +7,8 @@ en: proposals: settings: global: - add_integration: Add an integration - integration_url: Url platform to integrate + add_integration: Add integrations + integration_url: Url platforms to integrate (separated by a comma) preferred_locale: Preferred language preferred_locale_options: ca: ca diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 87f8f3f..ea7aba8 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -7,8 +7,8 @@ fr: proposals: settings: global: - add_integration: Ajouter une integration - integration_url: Url de la plateforme à intégrer + add_integration: Ajouter des integrations + integration_url: Url des plateformes à intégrer (séparées par une virgule) preferred_locale: Langue préférée preferred_locale_options: ca: ca diff --git a/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb b/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb index 49ec062..292b783 100644 --- a/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb +++ b/lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/concern" +require "uri" module ProposalsControllerExtends extend ActiveSupport::Concern @@ -16,10 +17,9 @@ 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 + if component_settings.add_integration && component_settings.integration_url.present? && data.present? external_proposals = data["contributions"] - @platform = component_settings.integration_url.split("//")[1] @authors = data["authors"] proposals = search.result proposals = reorder(proposals.includes(:component, :coauthorships, :attachments)) @@ -42,12 +42,16 @@ def index end def external_proposal - @external_proposal = GetDataFromApi.contribution(component_settings.integration_url, params[:reference], component_settings.preferred_locale || "en", "true") + 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(component_settings.integration_url, component_settings.preferred_locale || "en") + @authors = GetDataFromApi.authors(url, component_settings.preferred_locale || "en") .select { |author| @external_proposal["authors"].include?(author["reference"]) } .map { |author| author["name"] }.join(", ") end @@ -66,7 +70,25 @@ def voted_proposals end def data - @data ||= GetDataFromApi.data(component_settings.integration_url, component_settings.preferred_locale || "en").presence + @data ||= compile_data + end + + def compile_data + data = {} + component_settings.integration_url.split(", ").each do |url| + url = url.strip + datum = GetDataFromApi.data(url, component_settings.preferred_locale || "en").presence + if datum + if data.has_key?("contributions") + data["contributions"].concat(datum["contributions"]) + data["authors"].concat(datum["authors"]) + else + data["contributions"] = datum["contributions"] + data["authors"] = datum["authors"] + end + end + end + data end def define_proposals_and_external_proposals(proposals, external_proposals, current_page, per_page) diff --git a/spec/controllers/decidim/proposals/proposals_controller_spec.rb b/spec/controllers/decidim/proposals/proposals_controller_spec.rb index 0349b86..4f6076e 100644 --- a/spec/controllers/decidim/proposals/proposals_controller_spec.rb +++ b/spec/controllers/decidim/proposals/proposals_controller_spec.rb @@ -111,28 +111,44 @@ module Proposals } end - before do - component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" }) - allow(GetDataFromApi).to receive(:data).and_return(json) + 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 + 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 - 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(:platform)).to eq "example.org" - 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" + 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 "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 @@ -264,7 +280,7 @@ module Proposals end it "displays external_proposal view and sets variables" do - get :external_proposal, params: { reference: "JD-PROP-2025-09-1", param: :reference } + 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 diff --git a/spec/helpers/external_proposal_helper_spec.rb b/spec/helpers/external_proposal_helper_spec.rb index 11e5eaf..c47404e 100644 --- a/spec/helpers/external_proposal_helper_spec.rb +++ b/spec/helpers/external_proposal_helper_spec.rb @@ -70,6 +70,14 @@ module Proposals end end end + + describe "display_host" do + let(:url) { "http://localhost:3000" } + + it "returns the host" do + expect(helper.display_host(url)).to eq("localhost") + end + 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 new file mode 100644 index 0000000..2ab14b4 --- /dev/null +++ b/spec/system/admin_adds_integrations_on_proposals_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "spec_helper" + +# rubocop:disable RSpec/DescribeClass +describe "Admin adds integrations on proposals component" do + # rubocop:enable RSpec/DescribeClass + include_context "with a component" + let!(:manifest_name) { "proposals" } + let(:participatory_process) { create(:participatory_process, organization:) } + let!(:component) { create(:proposal_component, participatory_space: participatory_process) } + let(:admin) { create(:user, :admin, :confirmed, organization:) } + + 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 b8a2966..fd3869a 100644 --- a/spec/system/external_proposal_spec.rb +++ b/spec/system/external_proposal_spec.rb @@ -36,7 +36,7 @@ end before do - component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" }) + 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 @@ -165,6 +165,6 @@ def decidim_proposals end def visit_external_proposal - visit decidim_proposals.external_proposal_proposals_path("JD-PROP-2025-09-1") + visit decidim_proposals.external_proposal_proposals_path("JD-PROP-2025-09-1", url: "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1") end end diff --git a/spec/system/proposals_index_spec.rb b/spec/system/proposals_index_spec.rb index 0553b76..7236c38 100644 --- a/spec/system/proposals_index_spec.rb +++ b/spec/system/proposals_index_spec.rb @@ -122,42 +122,64 @@ before do create(:proposal_component, manifest:, participatory_space: participatory_process) create_list(:proposal, 3, component:) - component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" }) allow(GetDataFromApi).to receive(:data).and_return(json) 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) - 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 ordered randomly by default and the 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 + # 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 + + 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 + + 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