Skip to content

Commit f6604ca

Browse files
authored
Merge pull request #2 from OpenSourcePolitics/feat/add_multiple_platforms
Feat/add multiple platforms
2 parents 32c5abd + 378762b commit f6604ca

File tree

11 files changed

+230
-67
lines changed

11 files changed

+230
-67
lines changed

app/helpers/decidim/proposals/external_proposal_helper.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require "uri"
4+
35
module Decidim
46
module Proposals
57
module ExternalProposalHelper
@@ -42,6 +44,11 @@ def external_css_style(state)
4244
""
4345
end
4446
end
47+
48+
def display_host(url)
49+
uri = URI.parse(url)
50+
uri.host
51+
end
4552
end
4653
end
4754
end

app/packs/src/decidim/dataspace/component_edit_form.js

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,54 @@ document.addEventListener("DOMContentLoaded", function(){
33
const urlDiv = document.querySelector("div.integration_url_container")
44
const localeDiv = document.querySelector("div.preferred_locale_container")
55
const inputUrl = document.querySelector("input[name='component[settings][integration_url]']")
6-
inputUrl.setAttribute("placeholder", "https://platform.com")
6+
inputUrl.setAttribute("placeholder", "https://platform.com, https://example.com")
77

88
if(integrationCheck){
99
if(integrationCheck.checked){
10-
urlDiv.style.display = "block"
11-
localeDiv.style.display = "block"
10+
urlDiv.style.display = "block";
11+
localeDiv.style.display = "block";
1212
} else {
13-
urlDiv.style.display = "none"
14-
localeDiv.style.display = "none"
13+
urlDiv.style.display = "none";
14+
localeDiv.style.display = "none";
1515
}
1616
integrationCheck.addEventListener('change', function(){
1717
if (this.checked) {
18-
urlDiv.style.display = "block"
19-
localeDiv.style.display = "block"
18+
urlDiv.style.display = "block";
19+
localeDiv.style.display = "block";
2020
} else {
21-
urlDiv.style.display = "none"
22-
localeDiv.style.display = "none"
21+
urlDiv.style.display = "none";
22+
localeDiv.style.display = "none";
2323
}
2424
})
2525
}
26+
// check validity of urls when input looses focus
27+
inputUrl.addEventListener("blur", checkUrl)
28+
function checkUrl(event){
29+
const values = event.target.value;
30+
const errors = [];
31+
values.split(",").forEach(function(value){
32+
try {
33+
// if value is not valid, it will throw a TypeError
34+
const url = new URL(value);
35+
} catch(error){
36+
errors.push(error);
37+
}
38+
})
39+
if(errors.length !== 0 && inputUrl.parentNode.lastChild === inputUrl){
40+
// create p
41+
const elem = document.createElement('p');
42+
// create content
43+
const newContent = document.createTextNode("There is an invalid url");
44+
// add content to p
45+
elem.appendChild(newContent);
46+
// add style and class to p
47+
elem.style.color = "red";
48+
elem.classList.add('url_input_error');
49+
// insert p after input
50+
inputUrl.after(elem);
51+
} else if(errors.length === 0 && inputUrl.parentNode.lastChild !== inputUrl){
52+
const elem = document.querySelector('p.url_input_error');
53+
inputUrl.parentNode.removeChild(elem);
54+
}
55+
}
2656
})

app/views/decidim/proposals/proposals/_external_proposal.html.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<% if card_size == :g %>
22

3-
<%= link_to external_proposal_proposals_path(external_proposal["reference"]), class: "card__grid-external", id: external_proposal["reference"] do %>
3+
<%= link_to external_proposal_proposals_path(external_proposal["reference"], url: external_proposal["source"]), class: "card__grid-external", id: external_proposal["reference"] do %>
44
<div class="card__grid-img">
55
<%= external_icon "media/images/placeholder-card-g.svg", class: "card__placeholder-g" %>
6-
<p class="card__grid-img-text"><%= t('.view_from', platform: @platform) %></p>
6+
<p class="card__grid-img-text"><%= t('.view_from', platform: display_host(external_proposal["source"])) %></p>
77
</div>
88
<div class="card__grid-text">
99
<div class="card__grid-text-title">

config/locales/en.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ en:
77
proposals:
88
settings:
99
global:
10-
add_integration: Add an integration
11-
integration_url: Url platform to integrate
10+
add_integration: Add integrations
11+
integration_url: Url platforms to integrate (separated by a comma)
1212
preferred_locale: Preferred language
1313
preferred_locale_options:
1414
ca: ca

config/locales/fr.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ fr:
77
proposals:
88
settings:
99
global:
10-
add_integration: Ajouter une integration
11-
integration_url: Url de la plateforme à intégrer
10+
add_integration: Ajouter des integrations
11+
integration_url: Url des plateformes à intégrer (séparées par une virgule)
1212
preferred_locale: Langue préférée
1313
preferred_locale_options:
1414
ca: ca

lib/extends/controllers/decidim/proposals/proposals_controller_extends.rb

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "active_support/concern"
4+
require "uri"
45

56
module ProposalsControllerExtends
67
extend ActiveSupport::Concern
@@ -16,10 +17,9 @@ def index
1617
.order(position: :asc)
1718
render "decidim/proposals/proposals/participatory_texts/participatory_text"
1819
else
19-
if component_settings.add_integration && component_settings.integration_url.present? && data
20+
if component_settings.add_integration && component_settings.integration_url.present? && data.present?
2021

2122
external_proposals = data["contributions"]
22-
@platform = component_settings.integration_url.split("//")[1]
2323
@authors = data["authors"]
2424
proposals = search.result
2525
proposals = reorder(proposals.includes(:component, :coauthorships, :attachments))
@@ -42,12 +42,16 @@ def index
4242
end
4343

4444
def external_proposal
45-
@external_proposal = GetDataFromApi.contribution(component_settings.integration_url, params[:reference], component_settings.preferred_locale || "en", "true")
45+
uri = URI.parse(params[:url])
46+
url = "#{uri.scheme}://#{uri.host}"
47+
url += ":3000" if uri.host == "localhost"
48+
49+
@external_proposal = GetDataFromApi.contribution(url, params[:reference], component_settings.preferred_locale || "en", "true")
4650
return if @external_proposal.nil?
4751

4852
@comments = @external_proposal["children"]
4953
@parent_comments = @comments.select { |comment| comment["parent"] == @external_proposal["reference"] } if @comments
50-
@authors = GetDataFromApi.authors(component_settings.integration_url, component_settings.preferred_locale || "en")
54+
@authors = GetDataFromApi.authors(url, component_settings.preferred_locale || "en")
5155
.select { |author| @external_proposal["authors"].include?(author["reference"]) }
5256
.map { |author| author["name"] }.join(", ")
5357
end
@@ -66,7 +70,25 @@ def voted_proposals
6670
end
6771

6872
def data
69-
@data ||= GetDataFromApi.data(component_settings.integration_url, component_settings.preferred_locale || "en").presence
73+
@data ||= compile_data
74+
end
75+
76+
def compile_data
77+
data = {}
78+
component_settings.integration_url.split(", ").each do |url|
79+
url = url.strip
80+
datum = GetDataFromApi.data(url, component_settings.preferred_locale || "en").presence
81+
if datum
82+
if data.has_key?("contributions")
83+
data["contributions"].concat(datum["contributions"])
84+
data["authors"].concat(datum["authors"])
85+
else
86+
data["contributions"] = datum["contributions"]
87+
data["authors"] = datum["authors"]
88+
end
89+
end
90+
end
91+
data
7092
end
7193

7294
def define_proposals_and_external_proposals(proposals, external_proposals, current_page, per_page)

spec/controllers/decidim/proposals/proposals_controller_spec.rb

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -111,28 +111,44 @@ module Proposals
111111
}
112112
end
113113

114-
before do
115-
component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" })
116-
allow(GetDataFromApi).to receive(:data).and_return(json)
114+
context "and there is one url in integration url" do
115+
before do
116+
component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" })
117+
allow(GetDataFromApi).to receive(:data).and_return(json)
118+
end
119+
120+
it "sorts proposals by search defaults and define external_proposals and other variables" do
121+
get :index
122+
expect(response).to have_http_status(:ok)
123+
expect(subject).to render_template(:index)
124+
expect(assigns(:proposals).order_values).to eq [Decidim::Proposals::Proposal.arel_table[Decidim::Proposals::Proposal.primary_key] * Arel.sql("RANDOM()")]
125+
expect(assigns(:proposals).order_values.map(&:to_sql)).to eq ["\"decidim_proposals_proposals\".\"id\" * RANDOM()"]
126+
expect(assigns(:authors).count).to eq 2
127+
expect(assigns(:authors).first[:reference]).to eq "JD-MEET-2025-09-6"
128+
expect(assigns(:authors).last[:reference]).to eq "JD-MEET-2025-09-23"
129+
expect(assigns(:total_count)).to eq 4
130+
expect(assigns(:current_page)).to eq 1
131+
expect(assigns(:total_pages)).to eq 1
132+
expect(assigns(:proposals).count).to eq 2
133+
expect(assigns(:external_proposals).count).to eq 2
134+
expect(assigns(:external_proposals).first[:reference]).to eq "JD-PROP-2025-09-1"
135+
expect(assigns(:external_proposals).last[:reference]).to eq "JD-PROP-2025-09-20"
136+
end
117137
end
118138

119-
it "sorts proposals by search defaults and define external_proposals and other variables" do
120-
get :index
121-
expect(response).to have_http_status(:ok)
122-
expect(subject).to render_template(:index)
123-
expect(assigns(:proposals).order_values).to eq [Decidim::Proposals::Proposal.arel_table[Decidim::Proposals::Proposal.primary_key] * Arel.sql("RANDOM()")]
124-
expect(assigns(:proposals).order_values.map(&:to_sql)).to eq ["\"decidim_proposals_proposals\".\"id\" * RANDOM()"]
125-
expect(assigns(:platform)).to eq "example.org"
126-
expect(assigns(:authors).count).to eq 2
127-
expect(assigns(:authors).first[:reference]).to eq "JD-MEET-2025-09-6"
128-
expect(assigns(:authors).last[:reference]).to eq "JD-MEET-2025-09-23"
129-
expect(assigns(:total_count)).to eq 4
130-
expect(assigns(:current_page)).to eq 1
131-
expect(assigns(:total_pages)).to eq 1
132-
expect(assigns(:proposals).count).to eq 2
133-
expect(assigns(:external_proposals).count).to eq 2
134-
expect(assigns(:external_proposals).first[:reference]).to eq "JD-PROP-2025-09-1"
135-
expect(assigns(:external_proposals).last[:reference]).to eq "JD-PROP-2025-09-20"
139+
context "and there are 2 urls in integration_url" do
140+
before do
141+
component.update!(settings: { add_integration: true, integration_url: "http://example.org, http://example.org,", preferred_locale: "en" })
142+
allow(GetDataFromApi).to receive(:data).and_return(json)
143+
end
144+
145+
it "returns 4 external proposals and 4 authors" do
146+
get :index
147+
expect(response).to have_http_status(:ok)
148+
expect(subject).to render_template(:index)
149+
expect(assigns(:external_proposals).count).to eq 4
150+
expect(assigns(:authors).count).to eq 4
151+
end
136152
end
137153
end
138154

@@ -264,7 +280,7 @@ module Proposals
264280
end
265281

266282
it "displays external_proposal view and sets variables" do
267-
get :external_proposal, params: { reference: "JD-PROP-2025-09-1", param: :reference }
283+
get :external_proposal, params: { reference: "JD-PROP-2025-09-1", param: :reference, url: "http://example.org" }
268284
expect(response).to have_http_status(:ok)
269285
expect(subject).to render_template(:external_proposal)
270286
expect(assigns(:external_proposal)).to eq json_contrib

spec/helpers/external_proposal_helper_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ module Proposals
7070
end
7171
end
7272
end
73+
74+
describe "display_host" do
75+
let(:url) { "http://localhost:3000" }
76+
77+
it "returns the host" do
78+
expect(helper.display_host(url)).to eq("localhost")
79+
end
80+
end
7381
end
7482
end
7583
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
# rubocop:disable RSpec/DescribeClass
6+
describe "Admin adds integrations on proposals component" do
7+
# rubocop:enable RSpec/DescribeClass
8+
include_context "with a component"
9+
let!(:manifest_name) { "proposals" }
10+
let(:participatory_process) { create(:participatory_process, organization:) }
11+
let!(:component) { create(:proposal_component, participatory_space: participatory_process) }
12+
let(:admin) { create(:user, :admin, :confirmed, organization:) }
13+
14+
context "when editing the proposals component" do
15+
before do
16+
switch_to_host(organization.host)
17+
login_as admin, scope: :user
18+
visit decidim_admin_participatory_processes.components_path(participatory_process)
19+
within ".component-#{component.id}" do
20+
find("a[title='Configure']").click
21+
end
22+
end
23+
24+
context "and adding valid urls" do
25+
it "can adds multiple integrations and updates component" do
26+
# check add integration displays 2 divs
27+
check I18n.t("decidim.components.proposals.settings.global.add_integration")
28+
expect(page).to have_css("div.integration_url_container")
29+
expect(page).to have_css("div.preferred_locale_container")
30+
# provide valid urls, no error message displayed
31+
fill_in "component[settings][integration_url]", with: "http://example.com, http://localhost:3000"
32+
expect(page).to have_no_css("p.url_input_error")
33+
select("fr", from: "component[settings][preferred_locale]")
34+
# update component succesfully
35+
click_link_or_button "Update"
36+
expect(page).to have_content("The component was updated successfully.")
37+
end
38+
end
39+
40+
context "and adding invalid url" do
41+
it "gets an error message" do
42+
check I18n.t("decidim.components.proposals.settings.global.add_integration")
43+
# provide invalid url
44+
fill_in "component[settings][integration_url]", with: "http://localhost:3000, example.com"
45+
# error message displayed when input looses focus
46+
find_by_id("component_settings_preferred_locale").click
47+
sleep(1)
48+
expect(page).to have_css("p.url_input_error")
49+
# providing a good url removes the error
50+
fill_in "component[settings][integration_url]", with: "http://localhost:3000, http://example.com"
51+
# error message removed when input looses focus
52+
find_by_id("component_settings_preferred_locale").click
53+
sleep(1)
54+
expect(page).to have_no_css("p.url_input_error")
55+
end
56+
end
57+
end
58+
end

spec/system/external_proposal_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
end
3737

3838
before do
39-
component.update!(settings: { add_integration: true, integration_url: "http://example.org", preferred_locale: "en" })
39+
component.update!(settings: { add_integration: true, integration_url: "http://localhost:3000", preferred_locale: "en" })
4040
allow(GetDataFromApi).to receive(:contribution).and_return(json_contrib)
4141
allow(GetDataFromApi).to receive(:authors).and_return(authors)
4242
visit_external_proposal
@@ -165,6 +165,6 @@ def decidim_proposals
165165
end
166166

167167
def visit_external_proposal
168-
visit decidim_proposals.external_proposal_proposals_path("JD-PROP-2025-09-1")
168+
visit decidim_proposals.external_proposal_proposals_path("JD-PROP-2025-09-1", url: "http://localhost:3000/processes/satisfaction-hope/f/7/proposals/1")
169169
end
170170
end

0 commit comments

Comments
 (0)