Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/helpers/decidim/proposals/external_proposal_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "uri"

module Decidim
module Proposals
module ExternalProposalHelper
Expand Down Expand Up @@ -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
48 changes: 39 additions & 9 deletions app/packs/src/decidim/dataspace/component_edit_form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
})
Original file line number Diff line number Diff line change
@@ -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 %>
<div class="card__grid-img">
<%= external_icon "media/images/placeholder-card-g.svg", class: "card__placeholder-g" %>
<p class="card__grid-img-text"><%= t('.view_from', platform: @platform) %></p>
<p class="card__grid-img-text"><%= t('.view_from', platform: display_host(external_proposal["source"])) %></p>
</div>
<div class="card__grid-text">
<div class="card__grid-text-title">
Expand Down
4 changes: 2 additions & 2 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "active_support/concern"
require "uri"

module ProposalsControllerExtends
extend ActiveSupport::Concern
Expand All @@ -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))
Expand All @@ -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
Expand All @@ -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)
Expand Down
58 changes: 37 additions & 21 deletions spec/controllers/decidim/proposals/proposals_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions spec/helpers/external_proposal_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
58 changes: 58 additions & 0 deletions spec/system/admin_adds_integrations_on_proposals_spec.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions spec/system/external_proposal_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading
Loading