diff --git a/Gemfile.lock b/Gemfile.lock index 5211874..e989858 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/README.md b/README.md index 1f0a3ec..497b297 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) diff --git a/app/controllers/decidim/dataspace/api/v1/contributions_controller.rb b/app/controllers/decidim/dataspace/api/v1/contributions_controller.rb index e299e51..35f976d 100644 --- a/app/controllers/decidim/dataspace/api/v1/contributions_controller.rb +++ b/app/controllers/decidim/dataspace/api/v1/contributions_controller.rb @@ -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 @@ -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 diff --git a/app/controllers/decidim/dataspace/api/v1/data_controller.rb b/app/controllers/decidim/dataspace/api/v1/data_controller.rb index 4fc8954..ba2922a 100644 --- a/app/controllers/decidim/dataspace/api/v1/data_controller.rb +++ b/app/controllers/decidim/dataspace/api/v1/data_controller.rb @@ -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 diff --git a/app/helpers/decidim/proposals/external_proposal_helper.rb b/app/helpers/decidim/proposals/external_proposal_helper.rb index 602df84..79fccbd 100644 --- a/app/helpers/decidim/proposals/external_proposal_helper.rb +++ b/app/helpers/decidim/proposals/external_proposal_helper.rb @@ -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 diff --git a/app/models/decidim/dataspace/author.rb b/app/models/decidim/dataspace/author.rb index 3390cda..0c3af52 100644 --- a/app/models/decidim/dataspace/author.rb +++ b/app/models/decidim/dataspace/author.rb @@ -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) diff --git a/app/models/decidim/dataspace/container.rb b/app/models/decidim/dataspace/container.rb index 90baeff..28a08c4 100644 --- a/app/models/decidim/dataspace/container.rb +++ b/app/models/decidim/dataspace/container.rb @@ -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 @@ -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 diff --git a/app/models/decidim/dataspace/contribution.rb b/app/models/decidim/dataspace/contribution.rb index cfd63f3..b6a92b7 100644 --- a/app/models/decidim/dataspace/contribution.rb +++ b/app/models/decidim/dataspace/contribution.rb @@ -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 @@ -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 diff --git a/app/presenters/decidim/dataspace/container_presenter.rb b/app/presenters/decidim/dataspace/container_presenter.rb new file mode 100644 index 0000000..3c88feb --- /dev/null +++ b/app/presenters/decidim/dataspace/container_presenter.rb @@ -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 diff --git a/app/presenters/decidim/dataspace/contribution_presenter.rb b/app/presenters/decidim/dataspace/contribution_presenter.rb new file mode 100644 index 0000000..1eda272 --- /dev/null +++ b/app/presenters/decidim/dataspace/contribution_presenter.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Decidim + module Dataspace + class ContributionPresenter + def initialize(contribution) + @contribution = contribution + end + + def comment(proposal, component, locale) + { + reference: @contribution.reference, + source: Decidim::ResourceLocatorPresenter.new(proposal).url, + container: component.participatory_space_type.constantize.find(component.participatory_space_id).reference, + locale:, + title: nil, + content: @contribution.body[locale] || @contribution.body["en"], + authors: @contribution.author.name, + parent: Decidim::Dataspace::Contribution.parent(@contribution, proposal), + children: Decidim::Dataspace::Contribution.children(@contribution), + metadata: { depth: @contribution.depth, alignment: @contribution.alignment }, + created_at: @contribution.created_at, + updated_at: @contribution.updated_at, + deleted_at: @contribution.deleted_at + } + end + + def proposal_with_comments(component, locale) + { + reference: @contribution.reference, + source: Decidim::ResourceLocatorPresenter.new(@contribution).url, + container: component.participatory_space_type.constantize.find(component.participatory_space_id).reference, + locale:, + title: @contribution.title[locale] || @contribution.title["en"], + content: @contribution.body[locale] || @contribution.body["en"], + authors: Decidim::Dataspace::Contribution.authors(@contribution), + parent: nil, + children: Decidim::Dataspace::Contribution.comments(@contribution, locale), + metadata: { state: { withdrawn: @contribution.withdrawn?, emendation: @contribution.emendation?, state: @contribution.state } }, + created_at: @contribution.created_at, + updated_at: @contribution.updated_at, + deleted_at: nil # does not exist in proposal + } + end + + def proposal_without_comment(component, locale) + { + reference: @contribution.reference, + source: Decidim::ResourceLocatorPresenter.new(@contribution).url, + container: component.participatory_space_type.constantize.find(component.participatory_space_id).reference, + locale:, + title: @contribution.title[locale] || @contribution.title["en"], + content: @contribution.body[locale] || @contribution.body["en"], + authors: Decidim::Dataspace::Contribution.authors(@contribution), + parent: nil, + children: @contribution.comments.map(&:reference), + metadata: { state: { withdrawn: @contribution.withdrawn?, emendation: @contribution.emendation?, state: @contribution.state } }, + created_at: @contribution.created_at, + updated_at: @contribution.updated_at, + deleted_at: nil + } + end + end + end +end diff --git a/app/services/get_data_from_api.rb b/app/services/get_data_from_api.rb index 8fb7965..d9076be 100644 --- a/app/services/get_data_from_api.rb +++ b/app/services/get_data_from_api.rb @@ -6,6 +6,13 @@ class GetDataFromApi def self.data(url, preferred_locale) uri = URI(url + "/api/v1/data?preferred_locale=#{preferred_locale}") + if url.include?("?container") + array = url.split("?") + url = array.first + container_ref = array.second.split("=").second + uri = URI(url + "/api/v1/data?preferred_locale=#{preferred_locale}&container=#{container_ref}") + end + begin result = Net::HTTP.get(uri) JSON.parse(result) @@ -16,6 +23,13 @@ def self.data(url, preferred_locale) def self.contributions(url, preferred_locale) uri = URI(url + "/api/v1/data/contributions?preferred_locale=#{preferred_locale}") + if url.include?("?container") + array = url.split("?") + url = array.first + container_ref = array.second.split("=").second + uri = URI(url + "/api/v1/data/contributions?preferred_locale=#{preferred_locale}&container=#{container_ref}") + end + begin result = Net::HTTP.get(uri) JSON.parse(result) diff --git a/app/views/decidim/proposals/proposals/_comment.html.erb b/app/views/decidim/proposals/proposals/_comment.html.erb index 70dea20..b663fe0 100644 --- a/app/views/decidim/proposals/proposals/_comment.html.erb +++ b/app/views/decidim/proposals/proposals/_comment.html.erb @@ -1,3 +1,4 @@ +<% alignment = comment["metadata"]["alignment"] %> <%= content_tag :div, id: "comment_#{comment_ref}", class: "comment relative", role: "comment", data: { comment_id: comment[:reference], parent: comment[:parent] }, root_depth: comment["metadata"]["depth"] do %>
<%= comment["content"] %>
+<%= comment["content"] %>
+
If you want to get only proposals from one participatory space, you can add a container param in the url with the reference of the participatory space (ex: https://example.org?container=JD-ASSE-2025-09-1)" preferred_locale: Preferred language preferred_locale_options: ca: ca diff --git a/config/locales/fr.yml b/config/locales/fr.yml index ea7aba8..ab2a842 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -8,7 +8,7 @@ fr: settings: global: add_integration: Ajouter des integrations - integration_url: Url des plateformes à intégrer (séparées par une virgule) + integration_url: "Url des plateformes à intégrer, séparées par une virgule.
Si vous souhaitez uniquement les propositions d'un espace participatif, vous pouvez ajouter un param container à l'url (ex: https://example.org?container=JD-ASSE-2025-09-1)" preferred_locale: Langue préférée preferred_locale_options: ca: ca diff --git a/db/migrate/20250805130830_create_interoperables.rb b/db/migrate/20250805130830_create_interoperables.rb deleted file mode 100644 index 75ce005..0000000 --- a/db/migrate/20250805130830_create_interoperables.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -class CreateInteroperables < ActiveRecord::Migration[7.0] - def change - create_table :dataspace_interoperables do |t| - t.string :reference, null: false, index: { unique: true } - t.string :source, null: false - t.timestamp :deleted_at - t.jsonb :metadata, default: {} - - t.timestamps - end - end -end diff --git a/db/migrate/20250805133740_create_containers.rb b/db/migrate/20250805133740_create_containers.rb deleted file mode 100644 index 170c9a7..0000000 --- a/db/migrate/20250805133740_create_containers.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -class CreateContainers < ActiveRecord::Migration[7.0] - def change - create_table :dataspace_containers do |t| - t.belongs_to :interoperable, null: false, foreign_key: { to_table: :dataspace_interoperables } - t.belongs_to :parent, optional: true, index: true - t.string :name - t.string :description - end - end -end diff --git a/db/migrate/20250805144148_create_authors.rb b/db/migrate/20250805144148_create_authors.rb deleted file mode 100644 index 3b16700..0000000 --- a/db/migrate/20250805144148_create_authors.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class CreateAuthors < ActiveRecord::Migration[7.0] - def change - create_table :dataspace_authors do |t| - t.belongs_to :interoperable, null: false, foreign_key: { to_table: :dataspace_interoperables } - t.string :name - end - end -end diff --git a/db/migrate/20250805150126_create_contributions.rb b/db/migrate/20250805150126_create_contributions.rb deleted file mode 100644 index 111b0f9..0000000 --- a/db/migrate/20250805150126_create_contributions.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -class CreateContributions < ActiveRecord::Migration[7.0] - def change - create_table :dataspace_contributions do |t| - t.belongs_to :interoperable, null: false, foreign_key: { to_table: :dataspace_interoperables } - t.string :title - t.string :content - t.string :locale - t.jsonb :translations, default: {} - t.belongs_to :parent, optional: true, index: true - t.belongs_to :container, null: false, foreign_key: { to_table: :dataspace_containers } - end - end -end diff --git a/db/migrate/20250806074048_create_contributions_authors_join_table.rb b/db/migrate/20250806074048_create_contributions_authors_join_table.rb deleted file mode 100644 index 1d0a9c0..0000000 --- a/db/migrate/20250806074048_create_contributions_authors_join_table.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class CreateContributionsAuthorsJoinTable < ActiveRecord::Migration[7.0] - def change - create_join_table :contributions, :authors, table_name: :decidim_contributions_authors do |t| - # index to retrieve faster all authors for a given contribution - t.index [:contribution_id, :author_id], name: "index_on_contribution_id_and_author_id", unique: true - end - end -end diff --git a/db/migrate/20251231081306_add_reference_column_to_comments.rb b/db/migrate/20251231081306_add_reference_column_to_comments.rb new file mode 100644 index 0000000..ebdb5b2 --- /dev/null +++ b/db/migrate/20251231081306_add_reference_column_to_comments.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddReferenceColumnToComments < ActiveRecord::Migration[7.0] + def up + add_column :decidim_comments_comments, :reference, :string + end + + def down + remove_column :decidim_comments_comments, :reference + end +end diff --git a/lib/decidim/dataspace/engine.rb b/lib/decidim/dataspace/engine.rb index 9691118..b0e0177 100644 --- a/lib/decidim/dataspace/engine.rb +++ b/lib/decidim/dataspace/engine.rb @@ -20,7 +20,7 @@ class Engine < ::Rails::Engine scope :data do get "/", to: "data#index" resources :containers, only: [:index, :show], param: :reference - resources :authors, only: [:index, :show, :create], param: :reference + resources :authors, only: [:index, :show], param: :reference resources :contributions, only: [:index, :show], param: :reference end end @@ -42,6 +42,8 @@ class Engine < ::Rails::Engine initializer "dataspace-extends" do config.after_initialize do require "extends/controllers/decidim/proposals/proposals_controller_extends" + require "extends/models/decidim/comments/comment_extends" + require "extends/lib/decidim/core_extends" end end diff --git a/lib/extends/lib/decidim/core_extends.rb b/lib/extends/lib/decidim/core_extends.rb new file mode 100644 index 0000000..c2d4319 --- /dev/null +++ b/lib/extends/lib/decidim/core_extends.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module CoreExtends + extend ActiveSupport::Concern + + included do + # we can't include HasComponent to comment because it needs a decidim_component_id column + # instead we override the reference_generator in order to add the proper reference + config_accessor :reference_generator do + lambda do |resource, component| + ref = "" + + if resource.is_a?(Decidim::HasComponent) && component.present? + # It is a component resource + ref = component.participatory_space.organization.reference_prefix + elsif resource.is_a?(Decidim::Comments::Comment) && component.present? + # It is a comment resource + ref = component.participatory_space.organization.reference_prefix + elsif resource.is_a?(Decidim::Participable) + # It is a participatory space + ref = resource.organization.reference_prefix + end + + class_identifier = resource.class.name.demodulize[0..3].upcase + year_month = (resource.created_at || Time.current).strftime("%Y-%m") + + [ref, class_identifier, year_month, resource.id].join("-") + end + end + end +end + +Decidim.include(CoreExtends) diff --git a/lib/extends/models/decidim/comments/comment_extends.rb b/lib/extends/models/decidim/comments/comment_extends.rb new file mode 100644 index 0000000..418f692 --- /dev/null +++ b/lib/extends/models/decidim/comments/comment_extends.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module CommentExtends + extend ActiveSupport::Concern + + included do + include Decidim::HasReference + # we can't include HasComponent because it needs a decidim_component_id column + # instead we override the reference_generator in order to add the proper reference + end +end + +Decidim::Comments::Comment.include(CommentExtends) diff --git a/lib/tasks/comments.rake b/lib/tasks/comments.rake new file mode 100644 index 0000000..302cee2 --- /dev/null +++ b/lib/tasks/comments.rake @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +namespace :comments do + desc "Set reference to comments" + task add_reference: :environment do + comments = Decidim::Comments::Comment.where(reference: nil) + p "begin to add reference to #{comments.size} comments" + count = 0 + comments.find_each(batch_size: 100) do |comment| + # rubocop:disable Rails/SkipsModelValidations + comment.update_column(:reference, Decidim.reference_generator.call(comment, comment.component)) + # rubocop:enable Rails/SkipsModelValidations + count += 1 if comment.reference.present? + end + p "Comments updated: #{count}" + end +end diff --git a/spec/controllers/decidim/dataspace/api/v1/contributions_controller_spec.rb b/spec/controllers/decidim/dataspace/api/v1/contributions_controller_spec.rb index fedfbe8..5eb2b01 100644 --- a/spec/controllers/decidim/dataspace/api/v1/contributions_controller_spec.rb +++ b/spec/controllers/decidim/dataspace/api/v1/contributions_controller_spec.rb @@ -45,6 +45,24 @@ expect(response.parsed_body).to eq({ "error" => "Contributions not found" }) end end + + context "when there is a container param" do + let(:component_two) { create(:proposal_component) } + let!(:proposal_four) { create(:proposal, :participant_author, component: component_two) } + + before do + get :index, params: { container: component_two.participatory_space.reference } + end + + it "is a success and returns json with filtered proposals as contributions" do + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + # only proposal_four is rendered + expect(response.parsed_body.size).to eq(1) + expect(response.parsed_body.first["reference"]).to eq(proposal_four.reference) + end + end end describe "show" do @@ -74,7 +92,7 @@ # 2 comments without details expect(response.parsed_body["children"].size).to eq(2) expect(response.parsed_body["children"].first.class).to eq(String) - expect(response.parsed_body["children"].first).to eq("#{proposal["reference"]}-#{comment_one.id}") + expect(response.parsed_body["children"].first).to eq(comment_one.reference) end end @@ -98,7 +116,7 @@ # 2 comments with details expect(response.parsed_body["children"].size).to eq(2) expect(response.parsed_body["children"].first.class).to eq(Hash) - expect(response.parsed_body["children"].first["reference"]).to eq("#{proposal["reference"]}-#{comment_one.id}") + expect(response.parsed_body["children"].first["reference"]).to eq(comment_one.reference) expect(response.parsed_body["children"].first["content"]).to eq(comment_one.body["en"]) end end diff --git a/spec/controllers/decidim/dataspace/api/v1/data_controller_spec.rb b/spec/controllers/decidim/dataspace/api/v1/data_controller_spec.rb index 255a10a..02bfadb 100644 --- a/spec/controllers/decidim/dataspace/api/v1/data_controller_spec.rb +++ b/spec/controllers/decidim/dataspace/api/v1/data_controller_spec.rb @@ -11,20 +11,44 @@ let!(:proposal_three) { create(:proposal, :official, component:) } describe "index" do - before do - get :index - end + context "when there is no container param" do + before do + get :index + end + + it "is successful and returns json" do + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + end - it "is successful and returns json" do - expect(response).to have_http_status(:ok) - expect(response.content_type).to include("application/json") - expect { response.parsed_body }.not_to raise_error + it "returns all data" do + expect(response.parsed_body["contributions"].size).to eq(3) + expect(response.parsed_body["authors"].size).to eq(3) + expect(response.parsed_body["containers"].size).to eq(1) + end end - it "returns all data" do - expect(response.parsed_body["contributions"].size).to eq(3) - expect(response.parsed_body["authors"].size).to eq(3) - expect(response.parsed_body["containers"].size).to eq(1) + context "when there is a container param" do + let(:component_two) { create(:proposal_component) } + let!(:proposal_four) { create(:proposal, :participant_author, component: component_two) } + + before do + get :index, params: { container: component_two.participatory_space.reference } + end + + it "is successful and returns json" do + expect(response).to have_http_status(:ok) + expect(response.content_type).to include("application/json") + expect { response.parsed_body }.not_to raise_error + end + + it "returns data with filtered contributions" do + expect(response.parsed_body["contributions"].size).to eq(1) + expect(response.parsed_body["contributions"].first["reference"]).to eq(proposal_four.reference) + expect(response.parsed_body["authors"].size).to eq(4) + expect(response.parsed_body["containers"].size).to eq(2) + end end end end diff --git a/spec/helpers/external_proposal_helper_spec.rb b/spec/helpers/external_proposal_helper_spec.rb index c47404e..e538bc1 100644 --- a/spec/helpers/external_proposal_helper_spec.rb +++ b/spec/helpers/external_proposal_helper_spec.rb @@ -78,6 +78,34 @@ module Proposals expect(helper.display_host(url)).to eq("localhost") end end + + describe "alignment_badge_classes" do + context "when alignment is 1" do + it "returns a string with success" do + expect(helper.alignment_badge_classes(1)).to eq("label alignment success") + end + end + + context "when alignment is -1" do + it "returns a string with alert" do + expect(helper.alignment_badge_classes(-1)).to eq("label alignment alert") + end + end + end + + describe "alignment_badge_label" do + context "when alignment is 1" do + it "returns in favor" do + expect(helper.alignment_badge_label(1)).to eq("In favor") + end + end + + context "when alignment is -1" do + it "returns against" do + expect(helper.alignment_badge_label(-1)).to eq("Against") + end + end + end end end end diff --git a/spec/models/author_spec.rb b/spec/models/author_spec.rb index fd04748..8c46703 100644 --- a/spec/models/author_spec.rb +++ b/spec/models/author_spec.rb @@ -5,47 +5,6 @@ module Decidim module Dataspace describe Author do - subject { author } - - context "when valid" do - context "when build" do - let(:author) { build(:author_one) } - - it { is_expected.to be_valid } - end - - context "when create" do - let(:author) { create(:author_two) } - - it "is expected to give the reference and source" do - expect(author.reference).to eq("A02") - expect(author.source).to eq("https://example.com/account/johnsmith") - end - end - end - - context "when invalid" do - context "without reference" do - let(:author) { build(:author_one, reference: "") } - - it { is_expected.not_to be_valid } - end - - context "without source" do - let(:author) { build(:author_one, source: "") } - - it { is_expected.not_to be_valid } - end - end - - context "when deleting author" do - let!(:author) { create(:author_one) } - - it "destroys associated interoperable" do - expect { author.destroy }.to change(Decidim::Dataspace::Interoperable, :count).by(-1) - end - end - context "when using from_proposals method" do let(:component) { create(:proposal_component) } let!(:proposal) { create(:proposal, :participant_author, component:) } diff --git a/spec/models/container_spec.rb b/spec/models/container_spec.rb index 64677c8..e4b217d 100644 --- a/spec/models/container_spec.rb +++ b/spec/models/container_spec.rb @@ -5,68 +5,6 @@ module Decidim module Dataspace describe Container do - subject { container } - - context "when valid" do - context "when build" do - let(:container) { build(:container) } - - it { is_expected.to be_valid } - end - - context "when create" do - let(:container) { create(:container) } - - it "is expected to give the name, reference, source and metadata" do - expect(container.name).to eq("Space 1") - expect(container.reference).to eq("B01") - expect(container.source).to eq("https://example.com/") - expect(container.metadata).to eq({ "type" => "participatory_process", "visibility" => "public", "status" => "published" }) - end - end - end - - context "when invalid" do - context "without reference" do - let(:container) { build(:container, reference: nil) } - - it { is_expected.not_to be_valid } - end - - context "without valid source url" do - let(:container) { build(:container, source: "test.com") } - - it { is_expected.not_to be_valid } - end - end - - context "with parent and children" do - let(:container) { create(:container) } - let(:parent_container) { create(:container, reference: "B02", container: container.container) } - - before do - # rubocop:disable Rails/SkipsModelValidations - container.update_column("parent_id", parent_container.id) - # rubocop:enable Rails/SkipsModelValidations - end - - it "has one parent" do - expect(subject.parent).not_to be_nil - end - - it "has one child" do - expect(subject.parent.children.size).to eq(1) - end - end - - context "when deleting container" do - let!(:container) { create(:container) } - - it "destroys associated interoperable" do - expect { container.destroy }.to change(Decidim::Dataspace::Interoperable, :count).by(-1) - end - end - context "when using from_proposals method" do let(:component) { create(:proposal_component) } let!(:proposal) { create(:proposal, component:) } diff --git a/spec/models/contribution_spec.rb b/spec/models/contribution_spec.rb index 9a71191..027fa6b 100644 --- a/spec/models/contribution_spec.rb +++ b/spec/models/contribution_spec.rb @@ -5,112 +5,6 @@ module Decidim module Dataspace describe Contribution do - subject { contribution } - - context "when valid" do - context "when build" do - let(:contribution) { build(:contribution) } - - it { is_expected.to be_valid } - - context "without title but with content" do - let(:contribution) { build(:contribution, title: nil) } - - it { is_expected.to be_valid } - end - - context "without content but with title" do - let(:contribution) { build(:contribution, content: nil) } - - it { is_expected.to be_valid } - end - end - - context "when create" do - let(:contribution) { create(:contribution) } - - it "is expected to give the different values" do - expect(contribution.title).to eq("Contribution 1") - expect(contribution.content).to eq("Contenu de la contribution 1") - expect(contribution.reference).to eq("C01") - expect(contribution.source).to eq("https://example.com/contribution/1") - expect(contribution.metadata).to eq({ "status" => "published", "type" => "proposal" }) - end - end - end - - context "when invalid" do - context "without reference" do - let(:contribution) { build(:contribution, reference: nil) } - - it { is_expected.not_to be_valid } - end - - context "without source" do - let(:contribution) { build(:contribution, source: nil) } - - it { is_expected.not_to be_valid } - end - - context "without title and content" do - let(:contribution) { build(:contribution, title: nil, content: nil) } - - it { is_expected.not_to be_valid } - end - - context "without container" do - let(:contribution) { build(:contribution, container_id: nil) } - - it { is_expected.not_to be_valid } - end - end - - context "with authors" do - context "when 1 author" do - let(:contribution) { create(:contribution, :contrib_one) } - - it "has 1 author" do - expect(contribution.authors.size).to eq(1) - expect(contribution.authors.first.name).to eq("Jane Doe") - end - end - - context "when 2 authors" do - let(:contribution) { create(:contribution, :with_two_authors) } - - it "has 2 authors" do - expect(contribution.authors.size).to eq(2) - end - end - end - - context "with parent and children" do - let(:contribution) { create(:contribution) } - let(:parent_contrib) { create(:parent_contrib, container: contribution.container) } - - before do - # rubocop:disable Rails/SkipsModelValidations - contribution.update_column("parent_id", parent_contrib.id) - # rubocop:enable Rails/SkipsModelValidations - end - - it "has one parent" do - expect(subject.parent).not_to be_nil - end - - it "has one child" do - expect(subject.parent.children.size).to eq(1) - end - end - - context "when deleting contribution" do - let!(:contribution) { create(:contribution) } - - it "destroys associated interoperable" do - expect { contribution.destroy }.to change(Decidim::Dataspace::Interoperable, :count).by(-1) - end - end - context "when using self.from_proposals method with 3 proposals and 2 comments" do let(:component) { create(:proposal_component) } let!(:proposal) { create(:proposal, component:) } @@ -138,50 +32,77 @@ module Dataspace # first is proposal and has 2 children expect(method_call.first[:children].size).to eq(2) # second will be the first comment of proposal - expect(method_call.second[:reference]).to eq("#{method_call.first[:reference]}-#{comment_one.id}") + expect(method_call.second[:reference]).to eq(comment_one.reference) # last is proposal_three and has no children expect(method_call.last[:children]).to eq([]) end end + + context "and there is a container" do + let(:component_two) { create(:proposal_component) } + let!(:proposal_four) { create(:proposal, :participant_author, component: component_two) } + + it "returns the proposals filtered by container" do + method_call = Contribution.from_proposals("en", "false", component_two.participatory_space.reference) + expect(method_call.class).to eq(Array) + expect(method_call.size).to eq(1) + expect(method_call.first.class).to eq(Hash) + expect(method_call.first[:reference]).to eq(proposal_four.reference) + end + end end - context "when using self.proposal method" do + context "when using self.get_one method" do let(:component) { create(:proposal_component) } let!(:proposal) { create(:proposal, :participant_author, component:) } let!(:comment_one) { create(:comment, commentable: proposal) } let!(:comment_two) { create(:comment, commentable: proposal) } - context "and with_comments is false" do - it "returns an array with 1 hash element and no detailed comments in children key" do - method_call = Contribution.proposal(proposal.reference, "en") - expect(method_call.class).to eq(Hash) - # we have 13 keys in the returned hash - expect(method_call.size).to eq(13) - expect(method_call[:reference]).to eq(proposal.reference) - # reference for user author is name - expect(method_call[:authors]).to eq([proposal.authors.first.name]) - # proposal has 2 comments - expect(method_call[:children].size).to eq(2) - # comments are not detailed - expect(method_call[:children].first.class).to eq(String) - expect(method_call[:children].first).to eq("#{method_call[:reference]}-#{comment_one.id}") + context "and getting a proposal" do + context "and with_comments is false" do + it "returns an array with 1 hash element and no detailed comments in children key" do + method_call = Contribution.get_one(proposal.reference, "en") + expect(method_call.class).to eq(Hash) + # we have 13 keys in the returned hash + expect(method_call.size).to eq(13) + expect(method_call[:reference]).to eq(proposal.reference) + # reference for user author is name + expect(method_call[:authors]).to eq([proposal.authors.first.name]) + # proposal has 2 comments + expect(method_call[:children].size).to eq(2) + # comments are not detailed + expect(method_call[:children].first.class).to eq(String) + expect(method_call[:children].first).to eq(comment_one.reference) + end + end + + context "and with_comments is true" do + it "returns an array with 1 hash element and detailed comments in children key" do + method_call = Contribution.get_one(proposal.reference, "en", "true") + expect(method_call.class).to eq(Hash) + # we have 13 keys in the returned hash + expect(method_call.size).to eq(13) + expect(method_call[:reference]).to eq(proposal.reference) + # reference for user author is name + expect(method_call[:authors]).to eq([proposal.authors.first.name]) + # proposal has 2 comments + expect(method_call[:children].size).to eq(2) + # comments are detailed + expect(method_call[:children].first.class).to eq(Hash) + expect(method_call[:children].first[:reference]).to eq(comment_one.reference) + end end end - context "and with_comments is true" do - it "returns an array with 1 hash element and detailed comments in children key" do - method_call = Contribution.proposal(proposal.reference, "en", "true") + context "and getting a comment" do + it "returns an array with 1 hash element" do + method_call = Contribution.get_one(comment_one.reference, "en") expect(method_call.class).to eq(Hash) # we have 13 keys in the returned hash expect(method_call.size).to eq(13) - expect(method_call[:reference]).to eq(proposal.reference) + expect(method_call[:reference]).to eq(comment_one.reference) # reference for user author is name - expect(method_call[:authors]).to eq([proposal.authors.first.name]) - # proposal has 2 comments - expect(method_call[:children].size).to eq(2) - # comments are detailed - expect(method_call[:children].first.class).to eq(Hash) - expect(method_call[:children].first[:reference]).to eq("#{method_call[:reference]}-#{comment_one.id}") + expect(method_call[:authors]).to eq(comment_one.author.name) end end end diff --git a/spec/models/interoperable_spec.rb b/spec/models/interoperable_spec.rb deleted file mode 100644 index b092c7d..0000000 --- a/spec/models/interoperable_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Dataspace - describe Interoperable do - subject { interoperable } - - context "when valid" do - let(:interoperable) { build(:interoperable) } - - it { is_expected.to be_valid } - end - - context "when invalid" do - context "without reference" do - let(:interoperable) { build(:interoperable, :reference_nil) } - - it { is_expected.not_to be_valid } - end - - context "with reference not unique" do - let(:interoperable_one) { create(:interoperable) } - let(:interoperable) { build(:interoperable, reference: interoperable_one.reference) } - - it { is_expected.not_to be_valid } - end - - context "without source" do - let(:interoperable) { build(:interoperable, :source_nil) } - - it { is_expected.not_to be_valid } - end - - context "with source not url" do - let(:interoperable) { build(:interoperable, source: "example.org") } - - it { is_expected.not_to be_valid } - end - end - end - end -end diff --git a/spec/presenters/container_presenter_spec.rb b/spec/presenters/container_presenter_spec.rb new file mode 100644 index 0000000..a3f90e5 --- /dev/null +++ b/spec/presenters/container_presenter_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Dataspace + describe ContainerPresenter do + let!(:process) { create(:participatory_process) } + let!(:locale) { "en" } + + describe "container method" do + let(:presenter) { ContainerPresenter.new(process) } + + it "returns a hash with container informations" do + method_call = presenter.container(locale) + expect(method_call.class).to eq(Hash) + expect(method_call.size).to eq(8) + expect(method_call[:reference]).to eq(process.reference) + expect(method_call[:description]).to eq(translated_attribute(process.description)) + end + end + end + end +end diff --git a/spec/presenters/contribution_presenter_spec.rb b/spec/presenters/contribution_presenter_spec.rb new file mode 100644 index 0000000..ad70892 --- /dev/null +++ b/spec/presenters/contribution_presenter_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Dataspace + describe ContributionPresenter do + let(:component) { create(:proposal_component) } + let(:proposal) { create(:proposal, component:) } + let!(:comment_one) { create(:comment, commentable: proposal) } + let!(:comment_two) { create(:comment, commentable: proposal) } + let!(:locale) { "en" } + + describe "comment method" do + let(:presenter) { ContributionPresenter.new(comment_one) } + + it "returns a hash with comment informations" do + method_call = presenter.comment(proposal, component, locale) + expect(method_call.class).to eq(Hash) + expect(method_call.size).to eq(13) + expect(method_call[:reference]).to eq(comment_one.reference) + expect(method_call[:content]).to eq(translated_attribute(comment_one.body)) + expect(method_call[:parent]).to eq(proposal.reference) + end + end + + describe "proposal_with_comments method" do + let(:presenter) { ContributionPresenter.new(proposal) } + + it "returns a hash with proposal informations and detailed comments" do + method_call = presenter.proposal_with_comments(component, locale) + expect(method_call.class).to eq(Hash) + expect(method_call.size).to eq(13) + expect(method_call[:reference]).to eq(proposal.reference) + expect(method_call[:title]).to eq(translated_attribute(proposal.title)) + expect(method_call[:children].map { |hash| hash[:reference] }).to include(comment_one.reference) + expect(method_call[:children].map { |hash| hash[:reference] }).to include(comment_two.reference) + end + end + + describe "proposal_without_comment method" do + let(:presenter) { ContributionPresenter.new(proposal) } + + it "returns a hash with proposal informations and no detailed comments" do + method_call = presenter.proposal_without_comment(component, locale) + expect(method_call.class).to eq(Hash) + expect(method_call.size).to eq(13) + expect(method_call[:reference]).to eq(proposal.reference) + expect(method_call[:title]).to eq(translated_attribute(proposal.title)) + expect(method_call[:children]).to eq([comment_one.reference, comment_two.reference]) + end + end + end + end +end diff --git a/spec/services/get_data_from_api_service_spec.rb b/spec/services/get_data_from_api_service_spec.rb index 0af159f..eeb9fbc 100644 --- a/spec/services/get_data_from_api_service_spec.rb +++ b/spec/services/get_data_from_api_service_spec.rb @@ -45,11 +45,29 @@ allow(Net::HTTP).to receive(:get).with(uri).and_return(json) end - it "returns json containing a list of contributions" do - response = GetDataFromApi.contributions(url, "en") - expect(response.class).to eq Array - expect(response.size).to eq(2) - expect(response[0]["title"]).to eq("Test one") + context "and there is no container param" do + it "returns json containing a list of contributions" do + response = GetDataFromApi.contributions(url, "en") + expect(response.class).to eq Array + expect(response.size).to eq(2) + expect(response[0]["title"]).to eq("Test one") + end + end + + context "and there is a container param" do + let(:url) { "http://example.com?container=JD-PROP-2025-09-1" } + let(:uri) { URI("http://example.com/api/v1/data/contributions?preferred_locale=en&container=JD-PROP-2025-09-1") } + + before do + allow(Net::HTTP).to receive(:get).with(uri).and_return([contrib_one].to_json) + end + + it "returns json containing filtered contributions" do + response = GetDataFromApi.contributions(url, "en") + expect(response.class).to eq Array + expect(response.size).to eq(1) + expect(response[0]["title"]).to eq("Test one") + end end end