Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0c2c510
fix: remove unused author route
Stef-Rousset Dec 30, 2025
ae9807c
feat: add alignment in metadata
Stef-Rousset Dec 30, 2025
2f8f9e7
feat: add alignment in view
Stef-Rousset Dec 30, 2025
a1723bd
test: add test for new helper methods
Stef-Rousset Dec 30, 2025
fe86912
chore: update uri version
Stef-Rousset Dec 30, 2025
0fcb8d8
feat: update data and contributions methods to include container param
Stef-Rousset Dec 30, 2025
9131667
test: update service test with container param
Stef-Rousset Dec 30, 2025
6eb5760
feat: update model to handle container param
Stef-Rousset Dec 30, 2025
e47e973
test: update model test to handle container param
Stef-Rousset Dec 30, 2025
1d43c0c
feat: update data and contributions controllers to handle container p…
Stef-Rousset Dec 30, 2025
509cbe6
test: updata data and contributions controller to handle container param
Stef-Rousset Dec 30, 2025
7b13369
feat: update integration_url key
Stef-Rousset Dec 30, 2025
58a7598
chore: update readme file with container param
Stef-Rousset Dec 30, 2025
f6ec6c2
feat: add reference to comments
Stef-Rousset Jan 2, 2026
2ee2dbb
feat: add presenters to clean models
Stef-Rousset Jan 2, 2026
eff074b
feat: update models
Stef-Rousset Jan 2, 2026
33af131
update controller
Stef-Rousset Jan 2, 2026
c622b14
test: add presenters tests and update existing tests
Stef-Rousset Jan 2, 2026
ddead70
feat: update readme
Stef-Rousset Jan 2, 2026
bf10fcd
fix: update contributions method in service
Stef-Rousset Jan 2, 2026
8e5b7b5
test: update presenter test
Stef-Rousset Jan 2, 2026
475ee46
test: fix test
Stef-Rousset Jan 2, 2026
ec7c4f9
test: try fixing test again
Stef-Rousset Jan 2, 2026
4fad8b2
test: try fixing test again
Stef-Rousset Jan 2, 2026
cc9ab70
chore: remove migration files
Stef-Rousset Jan 5, 2026
1c4da72
test: update tests after deleting migration files
Stef-Rousset Jan 5, 2026
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
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ GEM
uber (0.1.0)
unicode-display_width (2.6.0)
uniform_notifier (1.17.0)
uri (1.0.4)
uri (1.1.1)
valid_email2 (4.0.6)
activemodel (>= 3.2)
mail (~> 2.5)
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ bundle install
# For versions >= 0.27
bundle exec rake railties:install:migrations
bundle exec rake db:migrate
bundle exec rake comments:add_reference
```

## API endpoints
Expand All @@ -39,7 +40,10 @@ GET "api/v1/data/authors"
+ Retrieve an author using its reference\
GET "api/v1/data/authors/:reference"

Please note that for the 2 endpoints related to contribution, you can add 2 query params
Please note that for the data and the /contributions endpoints, you can add a container query params
+ "container=JD-PROP-2025-09-1" to get only the contributions from the specified container

Please note that for the 2 endpoints related to contribution, you can also add 2 query params
+ "preferred_locale=fr" to get the data with your favorite language (default is "en")
+ "with_comments=true" (default is false)
+ for contributions endpoint, it will give you proposals and comments (the default is only proposals)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ class Api::V1::ContributionsController < Api::V1::BaseController

def index
preferred_locale = params[:preferred_locale].presence || "en"
container = params[:container].presence
with_comments = params[:with_comments].presence
contributions = Contribution.from_proposals(preferred_locale, with_comments)
contributions = Contribution.from_proposals(preferred_locale, with_comments, container)
return resource_not_found("Contributions") if contributions.blank?

render json: contributions, status: :ok
Expand All @@ -26,7 +27,7 @@ def set_contribution
ref = CGI.unescape(params[:reference])
preferred_locale = params[:preferred_locale].presence || "en"
with_comments = params[:with_comments].presence
@contribution = Contribution.proposal(ref, preferred_locale, with_comments)
@contribution = Contribution.get_one(ref, preferred_locale, with_comments)
return resource_not_found("Contribution") unless @contribution

@contribution
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/decidim/dataspace/api/v1/data_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ module Dataspace
class Api::V1::DataController < Api::V1::BaseController
def index
locale = params[:preferred_locale].presence || "en"
container = params[:container]

render json:
{
containers: Container.from_proposals(locale),
contributions: Contribution.from_proposals(locale),
contributions: Contribution.from_proposals(locale, nil, container),
authors: Author.from_proposals(locale)
}, status: :ok
end
Expand Down
19 changes: 19 additions & 0 deletions app/helpers/decidim/proposals/external_proposal_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ def display_host(url)
uri = URI.parse(url)
uri.host
end

def alignment_badge_classes(alignment)
classes = %w(label alignment)
case alignment
when 1
classes << "success"
when -1
classes << "alert"
end
classes.join(" ")
end

def alignment_badge_label(alignment)
if alignment == 1
I18n.t("decidim.components.comment.alignment.in_favor")
else
I18n.t("decidim.components.comment.alignment.against")
end
end
end
end
end
1 change: 0 additions & 1 deletion app/models/decidim/dataspace/author.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ class Author < Decidim::Dataspace::Interoperable
def self.from_proposals(preferred_locale)
proposals = Decidim::Proposals::Proposal.published
.not_hidden
.only_amendables
locale = "en"
available_locales = proposals.first&.organization&.available_locales
locale = preferred_locale if available_locales.present? && available_locales.include?(preferred_locale)
Expand Down
24 changes: 2 additions & 22 deletions app/models/decidim/dataspace/container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ class Container < Decidim::Dataspace::Interoperable
def self.from_proposals(preferred_locale)
proposals = Decidim::Proposals::Proposal.published
.not_hidden
.only_amendables
.includes([:component])
locale = "en"
available_locales = proposals.first&.organization&.available_locales
Expand All @@ -32,32 +31,13 @@ def self.from_params(ref, preferred_locale)
return nil unless container

locale = container.organization.available_locales.include?(preferred_locale) ? preferred_locale : "en"
{
"reference": container.reference,
"source": Decidim::ResourceLocatorPresenter.new(container).url,
"name": container.title[locale],
"description": container.description[locale],
"metadata": {},
"created_at": container.created_at,
"updated_at": container.updated_at,
"deleted_at": nil
}
Decidim::Dataspace::ContainerPresenter.new(container).container(locale)
end

def self.from_proposal(proposal, locale)
component = proposal.component
container = component.participatory_space_type.constantize.find(component.participatory_space_id)

{
"reference": container.reference,
"source": Decidim::ResourceLocatorPresenter.new(container).url,
"name": container.title[locale],
"description": container.description[locale],
"metadata": {},
"created_at": container.created_at,
"updated_at": container.updated_at,
"deleted_at": nil
}
Decidim::Dataspace::ContainerPresenter.new(container).container(locale)
end
end
end
Expand Down
132 changes: 50 additions & 82 deletions app/models/decidim/dataspace/contribution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,133 +18,92 @@ class Contribution < Decidim::Dataspace::Interoperable
delegate :reference, :source, :metadata, :created_at, :updated_at, :deleted_at, to: :interoperable

# get all the proposals with or without comments
def self.from_proposals(preferred_locale, with_comments = "false")
def self.from_proposals(preferred_locale, with_comments = "false", container = nil)
proposals = Decidim::Proposals::Proposal.published
.not_hidden
.only_amendables
.includes(:component)

proposals = Contribution.filter_proposals_by_container(container, proposals) if container

locale = "en"
available_locales = proposals.first&.organization&.available_locales
locale = preferred_locale if available_locales.present? && available_locales.include?(preferred_locale)

return Contribution.proposals_with_comments(proposals, locale) if with_comments == "true"
return Contribution.proposals_and_comments(proposals, locale) if with_comments == "true"

proposals.all.map do |proposal|
proposals.map do |proposal|
Contribution.proposal_without_comment(proposal, locale)
end
end

# get one proposal with or without detailed comments
def self.proposal(params_reference, preferred_locale, with_comments = "false")
proposal = Decidim::Proposals::Proposal.find_by(reference: params_reference)
return nil unless proposal
# get one contribution (proposal or comment)
def self.get_one(params_reference, preferred_locale, with_comments = "false")
contribution = Decidim::Proposals::Proposal.find_by(reference: params_reference) || Decidim::Comments::Comment.find_by(reference: params_reference)
return nil unless contribution

available_locales = proposal.organization.available_locales
available_locales = contribution.organization.available_locales
locale = available_locales.include?(preferred_locale) ? preferred_locale : "en"

return Contribution.proposal_with_comments(proposal, locale) if with_comments == "true"
if contribution.is_a?(Decidim::Proposals::Proposal)
# proposal with detailed comments
return Contribution.proposal_with_comments(contribution, locale) if with_comments == "true"

Contribution.proposal_without_comment(proposal, locale)
# proposal without detailed comments
Contribution.proposal_without_comment(contribution, locale)
else
Contribution.comment(contribution, locale)
end
end

# get proposals with comments
def self.proposals_with_comments(proposals, locale)
# get proposals and their comments
def self.proposals_and_comments(proposals, locale)
proposals.all.map do |proposal|
component = proposal.component
comments = Contribution.comments(proposal, locale)
[
{
reference: proposal.reference,
source: Decidim::ResourceLocatorPresenter.new(proposal).url,
container: component.participatory_space_type.constantize.find(component.participatory_space_id).reference,
locale:,
title: proposal.title[locale] || proposal.title["en"],
content: proposal.body[locale] || proposal.body["en"],
authors: Contribution.authors(proposal),
parent: nil,
children: comments.map { |comment| comment[:reference] },
metadata: { state: { withdrawn: proposal.withdrawn?, emendation: proposal.emendation?, state: proposal.state } },
created_at: proposal.created_at,
updated_at: proposal.updated_at,
deleted_at: nil # does not exist in proposal
}, comments.flatten
Decidim::Dataspace::ContributionPresenter.new(proposal).proposal_without_comment(component, locale), comments.flatten
]
end.flatten
end

# get proposal without detailed comments
# get one proposal without detailed comments
def self.proposal_without_comment(proposal, locale)
component = proposal.component
{
reference: proposal.reference,
source: Decidim::ResourceLocatorPresenter.new(proposal).url,
container: component.participatory_space_type.constantize.find(component.participatory_space_id).reference,
locale:,
title: proposal.title[locale] || proposal.title["en"],
content: proposal.body[locale] || proposal.body["en"],
authors: Contribution.authors(proposal),
parent: nil,
children: proposal.comments.map { |comment| "#{proposal.reference}-#{comment.id}" },
metadata: { state: { withdrawn: proposal.withdrawn?, emendation: proposal.emendation?, state: proposal.state } },
created_at: proposal.created_at,
updated_at: proposal.updated_at,
deleted_at: nil # does not exist in proposal
}
Decidim::Dataspace::ContributionPresenter.new(proposal).proposal_without_comment(component, locale)
end

# get one proposal with detailed comments
def self.proposal_with_comments(proposal, locale)
component = proposal.component
Decidim::Dataspace::ContributionPresenter.new(proposal).proposal_with_comments(component, locale)
end

# get detailed comments of a proposal
def self.comments(proposal, locale)
proposal.comments.map do |comment|
component = comment.component
{
reference: "#{proposal.reference}-#{comment.id}",
source: Decidim::ResourceLocatorPresenter.new(proposal).url,
container: component.participatory_space_type.constantize.find(component.participatory_space_id).reference,
locale:,
title: nil,
content: comment.body[locale] || comment.body["en"],
authors: comment.author.name,
parent: Contribution.parent(comment, proposal),
children: Contribution.children(comment, proposal),
metadata: { depth: comment.depth },
created_at: comment.created_at,
updated_at: comment.updated_at,
deleted_at: comment.deleted_at
}
Decidim::Dataspace::ContributionPresenter.new(comment).comment(proposal, component, locale)
end
end

# get one comment
def self.comment(comment, locale)
component = comment.component
proposal = Decidim::Proposals::Proposal.find(comment.decidim_root_commentable_id)
Decidim::Dataspace::ContributionPresenter.new(comment).comment(proposal, component, locale)
end

# get parent of a comment
def self.parent(comment, proposal)
comment.decidim_commentable_type == "Decidim::Comments::Comment" ? "#{proposal.reference}-#{comment.decidim_commentable_id}" : proposal.reference
comment.decidim_commentable_type == "Decidim::Comments::Comment" ? Decidim::Comments::Comment.find(comment.decidim_commentable_id).reference : proposal.reference
end

# get children of a comment
def self.children(comment, proposal)
def self.children(comment)
children = Decidim::Comments::Comment.where(decidim_commentable_type: "Decidim::Comments::Comment", decidim_commentable_id: comment.id)
return nil if children.blank?

children.map { |child| "#{proposal.reference}-#{child.id}" }
end

# get one proposal with detailed comments
def self.proposal_with_comments(proposal, locale)
component = proposal.component
{
reference: proposal.reference,
source: Decidim::ResourceLocatorPresenter.new(proposal).url,
container: component.participatory_space_type.constantize.find(component.participatory_space_id).reference,
locale:,
title: proposal.title[locale] || proposal.title["en"],
content: proposal.body[locale] || proposal.body["en"],
authors: Contribution.authors(proposal),
parent: nil,
children: Contribution.comments(proposal, locale),
metadata: { state: { withdrawn: proposal.withdrawn?, emendation: proposal.emendation?, state: proposal.state } },
created_at: proposal.created_at,
updated_at: proposal.updated_at,
deleted_at: nil
}
children.map(&:reference)
end

# get authors of a proposal
Expand All @@ -160,6 +119,15 @@ def self.authors(proposal)
end
end

def self.filter_proposals_by_container(container, proposals)
participatory_space = Decidim::ParticipatoryProcess.find_by(reference: container) || Decidim::Assembly.find_by(reference: container)
if participatory_space
component_ids = participatory_space.components.where(manifest_name: "proposals").ids
proposals = proposals.where(decidim_component_id: component_ids)
end
proposals
end

private

def title_or_content
Expand Down
24 changes: 24 additions & 0 deletions app/presenters/decidim/dataspace/container_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Decidim
module Dataspace
class ContainerPresenter
def initialize(container)
@container = container
end

def container(locale)
{
"reference": @container.reference,
"source": Decidim::ResourceLocatorPresenter.new(@container).url,
"name": @container.title[locale],
"description": @container.description[locale],
"metadata": {},
"created_at": @container.created_at,
"updated_at": @container.updated_at,
"deleted_at": nil
}
end
end
end
end
Loading
Loading