VYPR
High severity7.5NVD Advisory· Published Apr 21, 2026· Updated Apr 23, 2026

CVE-2026-40869

CVE-2026-40869

Description

Decidim is a participatory democracy framework. Starting in version 0.19.0 and prior to versions 0.30.5 and 0.31.1, a vulnerability allows any registered and authenticated user to accept or reject any amendments. The impact is on any users who have created proposals where the amendments feature is enabled. This also elevates the user accepting the amendment as the author of the original proposal as people amending proposals are provided coauthorship on the coauthorable resources. Versions 0.30.5 and 0.31.1 fix the issue. As a workaround, disable amendment reactions for the amendable component (e.g. proposals).

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
decidim-coreRubyGems
>= 0.31.0.rc1, < 0.31.10.31.1
decidim-coreRubyGems
>= 0.19.0, < 0.30.50.30.5

Affected products

1
  • cpe:2.3:a:decidim:decidim:*:*:*:*:*:ruby:*:*
    Range: >=0.19.0,<0.30.5

Patches

1
1b99136a1c7a

Admin configuration of amendments by step (#5178)

https://github.com/decidim/decidimAgusti B.RJun 17, 2019via ghsa
25 files changed · +504 222
  • decidim-admin/app/assets/javascripts/decidim/admin/form.js.es6+20 8 modified
    @@ -1,17 +1,29 @@
    -// Checks if the form contains a field with a special CSS class added in
    -// Decidim::Admin::SettingsHelper. If so, prevents the checkbox from being clicked,
    -// extracts the stored text and adds a new paragraph after the field.
    +// Checks if the form contains fields with special CSS classes added in
    +// Decidim::Admin::SettingsHelper and acts accordingly.
     $(() => {
    -  const $checkbox = $(".participatory_texts_disabled");
    +  // Prevents checkbox with ".participatory_texts_disabled" class from being clicked.
    +  const $participatoryTexts = $(".participatory_texts_disabled");
     
    -  $checkbox.click((event) => {
    +  $participatoryTexts.click((event) => {
         event.preventDefault();
         return false;
       });
     
    -  if ($checkbox.length > 0) {
    -    const $text = $checkbox[0].dataset.text
    +  // (1) Hides fields with ".amendments_step_settings" class if amendments_enabled
    +  // component setting is NOT checked.
    +  // (2) Toggles visibilty of fields with ".amendments_step_settings" class when
    +  // amendments_enabled component setting is clicked.
    +  const $amendmentsEnabled = $("input#component_settings_amendments_enabled");
     
    -    $checkbox.parent().after(`<p class="help-text">${$text}</p>`)
    +  if ($amendmentsEnabled.length > 0) {
    +    const $amendmentStepSettings = $(".amendments_step_settings").parent();
    +
    +    if ($amendmentsEnabled.is(":not(:checked)")) {
    +      $amendmentStepSettings.hide().siblings(".help-text").hide();
    +    }
    +
    +    $amendmentsEnabled.click(() => {
    +      $amendmentStepSettings.toggle().siblings(".help-text").toggle();
    +    });
       }
     });
    
  • decidim-admin/app/assets/stylesheets/decidim/admin/modules/_forms.scss+8 0 modified
    @@ -134,3 +134,11 @@ textarea{
       cursor: not-allowed;
       opacity: .5;
     }
    +
    +.edit_component .step-settings{
    +  .card-section fieldset legend{
    +    font-size: 24px;
    +    font-weight: bold;
    +    border-bottom: 1px solid grey;
    +  }
    +}
    
  • decidim-admin/app/helpers/decidim/admin/settings_helper.rb+31 9 modified
    @@ -30,29 +30,51 @@ def settings_attribute_input(form, attribute, name, options = {})
             end
           end
     
    +      # Returns a translation or nil. If nil, ZURB Foundation won't add the help_text.
    +      def help_text_for_component_setting(field_name, settings_name, component_name)
    +        key = "decidim.components.#{component_name}.settings.#{settings_name}.#{field_name}_help"
    +        return t(key) if I18n.exists?(key)
    +      end
    +
           private
     
           def form_method_for_attribute(attribute)
             return :editor if attribute.type.to_sym == :text && attribute.editor?
             TYPES[attribute.type.to_sym]
           end
     
    -      # Marks :participatory_texts_enabled checkbox with a unique class if
    -      # the Proposals component has existing proposals, and stores the help text
    -      # that will be added in a new div via JavaScript in "decidim/admin/form".
    -      #
    -      # field_name - The name of the field to disable.
    -      #
    +      # Handles special cases.
           # Returns an empty Hash or a Hash with extra HTML options.
           def extra_options_for(field_name)
    -        return {} unless field_name == :participatory_texts_enabled &&
    -                         Decidim::Proposals::Proposal.where(component: @component).any?
    +        case field_name
    +        when :participatory_texts_enabled
    +          participatory_texts_extra_options
    +        when :amendment_creation_enabled,
    +            :amendment_reaction_enabled,
    +            :amendment_promotion_enabled
    +          amendments_extra_options
    +        else
    +          {}
    +        end
    +      end
    +
    +      # Marks :participatory_texts_enabled setting with a CSS class if the
    +      # Proposals component has existing proposals, so it can be identified
    +      # in "decidim/admin/form.js". Also, adds a help_text.
    +      def participatory_texts_extra_options
    +        return {} unless Decidim::Proposals::Proposal.where(component: @component).any?
     
             {
               class: "participatory_texts_disabled",
    -          data: { text: t("decidim.admin.components.form.participatory_texts_enabled_help") }
    +          help_text: help_text_for_component_setting(:participatory_texts_enabled, :global, :proposals)
             }
           end
    +
    +      # Marks component_step_settings related to amendments with a CSS class,
    +      # so they can be identified in "decidim/admin/form.js".
    +      def amendments_extra_options
    +        { class: "amendments_step_settings" }
    +      end
         end
       end
     end
    
  • decidim-admin/app/views/decidim/admin/components/_settings_fields.html.erb+2 1 modified
    @@ -4,6 +4,7 @@
         attribute,
         name,
         label: t("decidim.components.#{component.manifest.name}.settings.#{settings_name}.#{name}"),
    -    tabs_prefix: tabs_prefix
    +    tabs_prefix: tabs_prefix,
    +    help_text: help_text_for_component_setting(name, settings_name, component.manifest.name)
       ) %>
     <% end %>
    
  • decidim-admin/config/locales/en.yml+0 1 modified
    @@ -247,7 +247,6 @@ en:
             form:
               default_step_settings: Default step settings
               global_settings: Global settings
    -          participatory_texts_enabled_help: Cannot interact with this setting if there are existing proposals. Please, create a new `Proposals component` if you want to enable this feature or discard all imported proposals in the `Participatory Texts` menu if you want to disable it.
               step_settings: Step settings
             index:
               add: Add component
    
  • decidim-core/app/cells/decidim/amendable/amenders_list/show.erb+1 1 modified
    @@ -1,4 +1,4 @@
    -<div class="card extra">
    +<div class="card extra amender-list">
       <div class="definition-data__item">
         <span class="definition-data__title">
           <%= t("amended_by", scope: "decidim.amendments.amendable") %>
    
  • decidim-core/app/cells/decidim/amendable/announcement_cell.rb+20 8 modified
    @@ -13,22 +13,34 @@ def show
     
         def announcement
           {
    -        announcement: emendation_message,
    +        announcement: emendation_message + promoted_message,
             callout_class: state_classes
           }
         end
     
         def emendation_message
    -      t(model.emendation_state,
    +      message(model.emendation_state, amendable_type, proposal_link, announcement_date)
    +    end
    +
    +    def promoted_message
    +      return "" unless model.amendment.promoted?
    +      proposal = model.linked_promoted_resource
    +      text = message(:promoted, amendable_type)
    +      %(<br><strong>#{proposal_link(proposal, text)}</strong>)
    +    end
    +
    +    def message(state, type, link = nil, date = nil)
    +      t(state,
             scope: "decidim.amendments.emendation.announcement",
    -        amendable_type: amendable_type,
    -        amendable_link: amendable_link,
    -        announcement_date: announcement_date)
    +        amendable_type: type,
    +        proposal_link: link,
    +        date: date)
         end
     
    -    def amendable_link
    -      link_to resource_locator(model.amendable).path do
    -        %(<strong>#{present(model.amendable).title}</strong>)
    +    def proposal_link(resource = model.amendable, text = nil)
    +      text ||= %(<strong>#{present(model.amendable).title}</strong>)
    +      link_to resource_locator(resource).path do
    +        text
           end
         end
     
    
  • decidim-core/app/commands/decidim/amendable/promote.rb+7 4 modified
    @@ -26,6 +26,7 @@ def call
             transaction do
               promote_emendation!
               notify_amendable_and_emendation_authors_and_followers
    +          link_promoted_emendation_and_proposal
             end
     
             broadcast(:ok, @promoted_emendation)
    @@ -35,10 +36,8 @@ def call
     
           attr_reader :form
     
    -      # The log of this action contains unique information
    -      # that is used to show/hide the promote button
    -      #
    -      # extra_log_info = { promoted_form: emendation.id }
    +      # The log of this action contains unique information:
    +      # extra_log_info = { promoted_from: emendation.id }
           def promote_emendation!
             @promoted_emendation = Decidim.traceability.perform_action!(
               "promote",
    @@ -72,6 +71,10 @@ def notify_amendable_and_emendation_authors_and_followers
           def proposal?
             @amendable.amendable_type == "Decidim::Proposals::Proposal"
           end
    +
    +      def link_promoted_emendation_and_proposal
    +        @promoted_emendation.link_resources(@emendation, "created_from_rejected_emendation")
    +      end
         end
       end
     end
    
  • decidim-core/app/controllers/concerns/decidim/amendments_controller.rb+4 5 modified
    @@ -14,8 +14,7 @@ def new
     
         def create
           @form = form(Decidim::Amendable::CreateForm).from_params(params)
    -      enforce_permission_to :create, :amendment
    -
    +      enforce_permission_to :create, :amendment, current_component: @form.component
           Decidim::Amendable::Create.call(@form) do
             on(:ok) do
               flash[:notice] = t("created.success", scope: "decidim.amendments")
    @@ -31,7 +30,7 @@ def create
     
         def reject
           @form = form(Decidim::Amendable::RejectForm).from_params(params)
    -      enforce_permission_to :reject, :amendment, amendment: @form.amendable
    +      enforce_permission_to :reject, :amendment, amendment: @form.amendable, current_component: @form.component
     
           Decidim::Amendable::Reject.call(@form) do
             on(:ok) do
    @@ -47,7 +46,7 @@ def reject
     
         def promote
           @form = Decidim::Amendable::PromoteForm.from_params(params)
    -      enforce_permission_to :promote, :amendment, amendment: @form.emendation
    +      enforce_permission_to :promote, :amendment, amendment: @form.emendation, current_component: @form.component
     
           Decidim::Amendable::Promote.call(@form) do
             on(:ok) do |proposal|
    @@ -68,7 +67,7 @@ def review
     
         def accept
           @form = Decidim::Amendable::ReviewForm.from_params(params)
    -      enforce_permission_to :accept, :amendment, amendment: @form.amendable
    +      enforce_permission_to :accept, :amendment, amendment: @form.amendable, current_component: @form.component
     
           Decidim::Amendable::Accept.call(@form) do
             on(:ok) do
    
  • decidim-core/app/forms/decidim/amendable/form.rb+4 0 modified
    @@ -53,6 +53,10 @@ def run_validations
     
             amendable_form.validate
           end
    +
    +      def component
    +        @component ||= amendable&.component || emendation&.component
    +      end
         end
       end
     end
    
  • decidim-core/app/helpers/decidim/amendments_helper.rb+5 13 modified
    @@ -16,7 +16,7 @@ def amendments_for(amendable)
           content << cell("decidim/collapsible_list",
                           amendable.emendations,
                           cell_options: { context: { current_user: current_user } },
    -                      list_class: "row small-up-1 medium-up-2 card-grid",
    +                      list_class: "row small-up-1 medium-up-2 card-grid amendment-list",
                           size: 4).to_s
     
           content_tag :div, content.html_safe, class: "section"
    @@ -41,6 +41,7 @@ def emendation_announcement_for(emendation)
         # Returns Html action button card to AMEND an amendable resource
         def amend_button_for(amendable)
           return unless amendments_enabled? && amendable.amendable?
    +      return unless current_component.current_settings.amendment_creation_enabled
     
           cell("decidim/amendable/amend_button_card", amendable)
         end
    @@ -76,7 +77,7 @@ def amendments_enabled?
         def can_react_to_emendation?(emendation)
           return unless current_user && emendation.emendation?
     
    -      true
    +      current_component.current_settings.amendment_reaction_enabled
         end
     
         # Checks if the user can accept and reject the emendation
    @@ -89,18 +90,9 @@ def allowed_to_accept_and_reject?(emendation)
         # Checks if the user can promote the emendation
         def allowed_to_promote?(emendation)
           return unless emendation.amendment.rejected? && emendation.created_by?(current_user)
    -      return if promoted?(emendation)
    +      return if emendation.amendment.promoted?
     
    -      true
    -    end
    -
    -    # Checks if the unique ActionLog created in the promote command exists.
    -    def promoted?(emendation)
    -      logs = Decidim::ActionLog.where(decidim_component_id: emendation.component)
    -                               .where(decidim_user_id: emendation.creator_author)
    -                               .where(action: "promote")
    -
    -      logs.select { |log| log.extra["promoted_from"] == emendation.id }.present?
    +      current_component.current_settings.amendment_promotion_enabled
         end
     
         # Renders a UserGroup select field in a form.
    
  • decidim-core/app/models/decidim/amendment.rb+8 2 modified
    @@ -2,11 +2,13 @@
     
     module Decidim
       class Amendment < ApplicationRecord
    +    STATES = %w(evaluating accepted rejected withdrawn).freeze
    +
         belongs_to :amendable, foreign_key: "decidim_amendable_id", foreign_type: "decidim_amendable_type", polymorphic: true
         belongs_to :amender, foreign_key: "decidim_user_id", class_name: "Decidim::User"
         belongs_to :emendation, foreign_key: "decidim_emendation_id", foreign_type: "decidim_emendation_type", polymorphic: true
     
    -    STATES = %w(evaluating accepted rejected withdrawn).freeze
    +    validates :amendable, :amender, :emendation, presence: true
     
         def evaluating?
           state == "evaluating"
    @@ -16,6 +18,10 @@ def rejected?
           state == "rejected"
         end
     
    -    validates :amendable, :amender, :emendation, presence: true
    +    def promoted?
    +      return false unless rejected?
    +
    +      emendation.linked_promoted_resource.present?
    +    end
       end
     end
    
  • decidim-core/app/permissions/decidim/permissions.rb+11 4 modified
    @@ -82,10 +82,17 @@ def follow_action?
     
         def amend_action?
           return unless permission_action.subject == :amendment
    -      return allow! if permission_action.action == :create
    -      return allow! if permission_action.action == :reject
    -      return allow! if permission_action.action == :promote
    -      return allow! if permission_action.action == :accept
    +      return disallow! unless component.settings.amendments_enabled
    +
    +      case permission_action.action
    +      when :create
    +        return allow! if component.current_settings.amendment_creation_enabled
    +      when :accept,
    +          :reject
    +        return allow! if component.current_settings.amendment_reaction_enabled
    +      when :promote
    +        return allow! if component.current_settings.amendment_promotion_enabled
    +      end
     
           amendment = context.fetch(:amendment, nil)
           toggle_allow(amendment&.amender == user)
    
  • decidim-core/config/locales/en.yml+6 5 modified
    @@ -153,14 +153,15 @@ en:
               help_text: Review the changes and accept or reject this amendment. A notification will be sent to its author(s).
             announcement:
               accepted: |-
    -            This amendment for the %{amendable_type} %{amendable_link} has been
    -            accepted on <strong>%{announcement_date}</strong>.
    +            This amendment for the %{amendable_type} %{proposal_link} has been
    +            accepted on <strong>%{date}</strong>.
               evaluating: |-
    -            This amendment for the %{amendable_type} %{amendable_link}
    +            This amendment for the %{amendable_type} %{proposal_link}
                 is being evaluated.
    -          rejected: This amendment for the %{amendable_type} %{amendable_link} has been rejected on <strong>%{announcement_date}</strong>.
    +          promoted: Promoted to a %{amendable_type}.
    +          rejected: This amendment for the %{amendable_type} %{proposal_link} was rejected on <strong>%{date}</strong>.
               withdrawn: |-
    -            This amendment for the %{amendable_type} %{amendable_link}
    +            This amendment for the %{amendable_type} %{proposal_link}
                 has been withdrawn by the author.
           new:
             amendment_author: Amendment author
    
  • decidim-core/lib/decidim/amendable.rb+8 1 modified
    @@ -84,7 +84,7 @@ def resource_state
         end
     
         def emendation_state
    -      return resource_state if resource_state == "withdrawn"
    +      return resource_state if resource_state == "withdrawn" # Special case for Proposals
     
           amendment.state
         end
    @@ -94,5 +94,12 @@ def state
     
           resource_state
         end
    +
    +    # Returns the linked resource to or from this model
    +    # for the given resource name and link name.
    +    # See Decidim::Resourceable#link_resources
    +    def linked_promoted_resource
    +      linked_resources(amendable_type, "created_from_rejected_emendation").first
    +    end
       end
     end
    
  • decidim-proposals/app/cells/decidim/proposals/proposal_linked_resources_cell.rb+14 0 added
    @@ -0,0 +1,14 @@
    +# frozen_string_literal: true
    +
    +require "cell/partial"
    +
    +module Decidim
    +  module Proposals
    +    # This cell renders the linked resource of a proposal.
    +    class ProposalLinkedResourcesCell < Decidim::ViewModel
    +      def show
    +        render if linked_resource
    +      end
    +    end
    +  end
    +end
    
  • decidim-proposals/app/cells/decidim/proposals/proposal_linked_resources/show.erb+9 0 added
    @@ -0,0 +1,9 @@
    +<div class="text-center mt-sm">
    +  <%= link_to_resource %>
    +
    +  <div class="text-center">
    +    <small>
    +      <%= link_help_text %>
    +    </small>
    +  </div>
    +</div>
    
  • decidim-proposals/app/cells/decidim/proposals/proposal_link_to_collaborative_draft_cell.rb+4 8 modified
    @@ -5,19 +5,15 @@
     module Decidim
       module Proposals
         # This cell renders the link to the source collaborative draft of a proposal.
    -    class ProposalLinkToCollaborativeDraftCell < Decidim::ViewModel
    -      def show
    -        render if collaborative_draft
    -      end
    -
    +    class ProposalLinkToCollaborativeDraftCell < ProposalLinkedResourcesCell
           private
     
    -      def collaborative_draft
    -        @collaborative_draft ||= model.linked_resources(:collaborative_draft, "created_from_collaborative_draft").first
    +      def linked_resource
    +        @linked_resource ||= model.linked_resources(:collaborative_draft, "created_from_collaborative_draft").first
           end
     
           def link_to_resource
    -        link_to resource_locator(collaborative_draft).path, class: "link" do
    +        link_to resource_locator(linked_resource).path, class: "link" do
               t("link_to_collaborative_draft_text", scope: "decidim.proposals.proposals.show")
             end
           end
    
  • decidim-proposals/app/cells/decidim/proposals/proposal_link_to_rejected_emendation_cell.rb+34 0 added
    @@ -0,0 +1,34 @@
    +# frozen_string_literal: true
    +
    +require "cell/partial"
    +
    +module Decidim
    +  module Proposals
    +    # This cell renders the link to the rejected emendation promoted to proposal.
    +    class ProposalLinkToRejectedEmendationCell < ProposalLinkedResourcesCell
    +      private
    +
    +      def linked_resource
    +        @linked_resource ||= model.linked_promoted_resource
    +      end
    +
    +      def link_to_resource
    +        link_to resource_locator(linked_resource).path, class: "link" do
    +          if model.emendation?
    +            t("link_to_proposal_from_emendation_text", scope: "decidim.proposals.proposals.show")
    +          else
    +            t("link_to_promoted_emendation_text", scope: "decidim.proposals.proposals.show")
    +          end
    +        end
    +      end
    +
    +      def link_help_text
    +        if model.emendation?
    +          t("link_to_proposal_from_emendation_help_text", scope: "decidim.proposals.proposals.show")
    +        else
    +          t("link_to_promoted_emendation_help_text", scope: "decidim.proposals.proposals.show")
    +        end
    +      end
    +    end
    +  end
    +end
    
  • decidim-proposals/app/cells/decidim/proposals/proposal_link_to_rejected_emendation/show.erb+0 0 renamed
  • decidim-proposals/app/views/decidim/proposals/proposals/show.html.erb+1 0 modified
    @@ -93,6 +93,7 @@ edit_link(
         <%= render partial: "decidim/shared/share_modal" %>
         <%= embed_modal_for proposal_proposal_widget_url(@proposal, format: :js) %>
         <%= cell "decidim/proposals/proposal_link_to_collaborative_draft", @proposal %>
    +    <%= cell "decidim/proposals/proposal_link_to_rejected_emendation", @proposal %>
       </div>
       <div class="columns mediumlarge-8 mediumlarge-pull-4">
         <div class="section">
    
  • decidim-proposals/config/locales/en.yml+12 0 modified
    @@ -87,6 +87,7 @@ en:
             settings:
               global:
                 amendments_enabled: Amendments enabled
    +            amendments_enabled_help: If active, configure Amendment features for each step.
                 announcement: Announcement
                 attachments_allowed: Allow attachments
                 can_accumulate_supports_beyond_threshold: Can accumulate supports beyond threshold
    @@ -97,6 +98,7 @@ en:
                 new_proposal_help_text: New proposal help text
                 official_proposals_enabled: Official proposals enabled
                 participatory_texts_enabled: Participatory texts enabled
    +            participatory_texts_enabled_help: Cannot interact with this setting if there are existing proposals. Please, create a new `Proposals component` if you want to enable this feature or discard all imported proposals in the `Participatory Texts` menu if you want to disable it.
                 proposal_answering_enabled: Proposal answering enabled
                 proposal_edit_before_minutes: Proposals can be edited by authors before this many minutes passes
                 proposal_length: Maximum proposal body length
    @@ -109,6 +111,12 @@ en:
                 threshold_per_proposal: Threshold per proposal
                 vote_limit: Support limit per participant
               step:
    +            amendment_creation_enabled: Amendment creation enabled
    +            amendment_creation_enabled_help: Participant can amend proposals.
    +            amendment_promotion_enabled: Amendment promotion enabled
    +            amendment_promotion_enabled_help: Emandation authors will be able to promote to Proposal the rejected emendation.
    +            amendment_reaction_enabled: Amendment reaction enabled
    +            amendment_reaction_enabled_help: Proposal's authors will be able to accept or reject Participant's emendations.
                 announcement: Announcement
                 automatic_hashtags: Hashtags added to all proposals
                 comments_blocked: Comments blocked
    @@ -687,6 +695,10 @@ en:
                 other: and %{count} more people
               link_to_collaborative_draft_help_text: This proposal is the result of a collaborative draft. Review the history
               link_to_collaborative_draft_text: See the collaborative draft
    +          link_to_promoted_emendation_help_text: This proposal is a promoted emendation
    +          link_to_promoted_emendation_text: See the rejected emendation.
    +          link_to_proposal_from_emendation_help_text: This is a rejected emendation
    +          link_to_proposal_from_emendation_text: See the Proposal
               proposal_accepted_reason: 'This proposal has been accepted because:'
               proposal_in_evaluation_reason: This proposal is being evaluated
               proposal_rejected_reason: 'This proposal has been rejected because:'
    
  • decidim-proposals/lib/decidim/proposals/component.rb+3 0 modified
    @@ -55,6 +55,9 @@
         settings.attribute :comments_blocked, type: :boolean, default: false
         settings.attribute :creation_enabled, type: :boolean
         settings.attribute :proposal_answering_enabled, type: :boolean, default: true
    +    settings.attribute :amendment_creation_enabled, type: :boolean, default: true
    +    settings.attribute :amendment_reaction_enabled, type: :boolean, default: true
    +    settings.attribute :amendment_promotion_enabled, type: :boolean, default: true
         settings.attribute :announcement, type: :text, translated: true, editor: true
         settings.attribute :automatic_hashtags, type: :text, editor: false, required: false
         settings.attribute :suggested_hashtags, type: :text, editor: false, required: false
    
  • decidim-proposals/spec/lib/decidim/proposals/component_spec.rb+50 17 modified
    @@ -33,7 +33,7 @@
       end
     
       describe "on update" do
    -    context "when trying to change the `participatory_texts_enabled` setting" do
    +    describe "participatory_texts_enabled" do
           let(:form) do
             instance_double(
               Decidim::Admin::ComponentForm,
    @@ -54,17 +54,15 @@
             it "updates the component" do
               expect do
                 Decidim::Admin::UpdateComponent.call(form, component)
    -          end.to change {
    -            component.settings.participatory_texts_enabled
    -          }.from(false).to(true)
    +          end.to broadcast(:ok)
             end
           end
     
           context "when there are proposals for the component" do
             let(:proposal) { create(:proposal, component: component) }
             let(:valid) { false }
     
    -        it "does not update the component" do
    +        it "does NOT update the component" do
               expect do
                 Decidim::Admin::UpdateComponent.call(form, component)
               end.to broadcast(:invalid)
    @@ -151,36 +149,71 @@
         let(:edit_component_path) do
           Decidim::EngineRouter.admin_proxy(component.participatory_space).edit_component_path(component.id)
         end
    -    let(:participatory_texts_enabled_checkbox) do
    -      page.find("input[name='component[settings][participatory_texts_enabled]']")
    -    end
     
         before do
           switch_to_host(component.organization.host)
           login_as current_user, scope: :user
         end
     
    -    context "when there are no proposals for the component" do
    +    describe "participatory_texts_enabled" do
    +      let(:participatory_texts_enabled) { page.find("input#component_settings_participatory_texts_enabled") }
    +
           before do
             visit edit_component_path
           end
     
    -      it "ALLOWS the admin the enable the Participatory texts feature" do
    -        expect(participatory_texts_enabled_checkbox[:class]).not_to include("disabled")
    +      context "when there are no proposals for the component" do
    +        it "allows to check the setting" do
    +          expect(participatory_texts_enabled[:class]).not_to include("disabled")
    +        end
    +
    +        it "changes the setting value after updating" do
    +          expect do # rubocop:disable Lint/AmbiguousBlockAssociation
    +            check "Participatory texts enabled"
    +            click_button "Update"
    +          end.to change { component.reload.settings.participatory_texts_enabled }
    +        end
    +      end
    +
    +      context "when there are proposals for the component" do
    +        before do
    +          component.update(settings: { participatory_texts_enabled: true }) # Testing from true to false
    +          create(:proposal, component: component)
    +          visit edit_component_path
    +        end
    +
    +        it "does NOT allow to check the setting" do
    +          expect(participatory_texts_enabled[:class]).to include("disabled")
    +
    +          expect(page).to have_content("Cannot interact with this setting if there are existing proposals. Please, create a new `Proposals component` if you want to enable this feature or discard all imported proposals in the `Participatory Texts` menu if you want to disable it.")
    +        end
    +
    +        it "does NOT change the setting value after updating" do
    +          expect do # rubocop:disable Lint/AmbiguousBlockAssociation
    +            click_button "Update"
    +          end.not_to change { component.reload.settings.participatory_texts_enabled }
    +        end
           end
         end
     
    -    context "when there are proposals for the component" do
    +    describe "amendments settings" do
           before do
    -        create(:proposal, component: component)
             visit edit_component_path
           end
     
    -      it "DOES NOT ALLOW the admin the enable the Participatory texts feature" do
    -        expect(participatory_texts_enabled_checkbox[:class]).to include("disabled")
    +      context "when amendments_enabled global setting is checked" do
    +        before do
    +          check "Amendments enabled"
    +        end
     
    -        within ".help-text" do
    -          expect(page).to have_content("Cannot interact with this setting if there are existing proposals. Please, create a new `Proposals component` if you want to enable this feature or discard all imported proposals in the `Participatory Texts` menu if you want to disable it.")
    +        it "is shown the amendments step settings" do
    +          expect(page).to have_css(".amendments_step_settings", visible: true)
    +        end
    +      end
    +
    +      context "when amendments_enabled global setting is NOT checked" do
    +        it "is NOT shown the amendments step settings" do
    +          expect(page).to have_css(".amendments_step_settings", visible: false)
             end
           end
         end
    
  • decidim-proposals/spec/system/amend_proposal_spec.rb+242 135 modified
    @@ -3,182 +3,256 @@
     require "spec_helper"
     
     describe "Amend Proposal", versioning: true, type: :system do
    -  include_context "with a component"
    -  let(:manifest_name) { "proposals" }
    -
    -  let!(:proposal) { create(:proposal, title: "Title", body: "One liner body", component: component) }
    -  let!(:emendation) { create(:proposal, body: "Amended One liner body", component: component) }
    -  let!(:amendment) { create :amendment, amendable: proposal, emendation: emendation }
    -  let!(:user) { create :user, :confirmed, organization: organization }
    -  let!(:user_group) { create(:user_group, :verified, organization: organization, users: [user]) }
    +  let!(:component) { create(:proposal_component) }
    +  let!(:proposal) { create(:proposal, title: "Long enough title", body: "One liner body", component: component) }
    +  let!(:emendation) { create(:proposal, title: "Amended Long enough title", body: "Amended One liner body", component: component) }
    +  let!(:amendment) { create :amendment, amendable: proposal, emendation: emendation, amender: emendation.creator_author }
       let(:emendation_path) { Decidim::ResourceLocatorPresenter.new(emendation).path }
    +  let(:proposal_path) { Decidim::ResourceLocatorPresenter.new(proposal).path }
    +
    +  def update_component_step_setting(component, step_setting_name, value)
    +    component.update!(
    +      step_settings: {
    +        component.participatory_space.active_step.id => {
    +          step_setting_name => value
    +        }
    +      }
    +    )
    +  end
     
    -  context "when amendments are not enabled" do
    -    it "doesn't show the amend proposal button" do
    -      visit_component
    -
    -      click_link proposal.title
    -      expect(page).to have_no_link("Amend Proposal")
    -    end
    +  before do
    +    switch_to_host(component.organization.host)
       end
     
    -  context "when amendments are enabled" do
    -    let!(:component) do
    -      create(:proposal_component,
    -             :with_amendments_enabled,
    -             manifest: manifest,
    -             participatory_space: participatory_process)
    +  context "with existing amendments" do
    +    context "when visiting an amended proposal" do
    +      before do
    +        visit proposal_path
    +      end
    +
    +      it "is shown the amendments list" do
    +        expect(page).to have_css("#amendments", text: "AMENDMENTS")
    +        within ".amendment-list" do
    +          expect(page).to have_content(emendation.title)
    +        end
    +      end
    +
    +      it "is shown the amenders list" do
    +        expect(page).to have_content("AMENDED BY")
    +        within ".amender-list" do
    +          expect(page).to have_content(emendation.creator_author.name)
    +        end
    +      end
         end
     
    -    context "and visits an amendable proposal" do
    +    context "when visiting an amendment to a proposal" do
           before do
    -        visit_component
    -        click_link proposal.title
    +        visit emendation_path
           end
     
    -      it "renders a link to Amend it" do
    -        expect(page).to have_link("Amend Proposal")
    -      end
    +      it "shows the changed attributes" do
    +        expect(page).to have_content("Amendment to \"#{proposal.title}\"")
     
    -      context "when the user is not logged in and clicks" do
    -        it "is shown the login modal" do
    -          within ".card__amend-button", match: :first do
    -            click_link "Amend Proposal"
    +        within ".diff-for-title" do
    +          expect(page).to have_content("TITLE")
    +
    +          within ".diff > ul > .del" do
    +            expect(page).to have_content(proposal.title)
               end
     
    -          expect(page).to have_css("#loginModal", visible: true)
    +          within ".diff > ul > .ins" do
    +            expect(page).to have_content(emendation.title)
    +          end
             end
    -      end
     
    -      context "when the user is logged in and clicks" do
    -        before do
    -          login_as user, scope: :user
    -          visit_component
    -          click_link proposal.title
    -        end
    +        within ".diff-for-body" do
    +          expect(page).to have_content("BODY")
     
    -        it "is shown the amend form" do
    -          within ".card__amend-button", match: :first do
    -            click_link "Amend Proposal"
    +          within ".diff > ul > .del" do
    +            expect(page).to have_content(proposal.body)
               end
     
    -          expect(page).to have_css(".new_amendment", visible: true)
    -          expect(page).to have_content("CREATE YOUR AMENDMENT")
    +          within ".diff > ul > .ins" do
    +            expect(page).to have_content(emendation.body)
    +          end
             end
           end
    +    end
    +  end
     
    -      context "when creating an amendment" do
    +  context "with amendments NOT enabled" do
    +    before do
    +      component.update!(settings: { amendments_enabled: false })
    +    end
    +
    +    context "when amendment CREATION is enabled" do
    +      before do
    +        update_component_step_setting(component, :amendment_creation_enabled, true)
    +      end
    +
    +      context "and visits an amendable proposal" do
             before do
    -          login_as user, scope: :user
    -          visit decidim.new_amend_path(amendable_gid: proposal.to_sgid.to_s)
    +          visit proposal_path
             end
     
    -        it "is shown the amend title field" do
    -          expect(page).to have_css(".field", text: "Title", visible: true)
    -        end
    -        it "is shown the amend body field" do
    -          expect(page).to have_css(".field", text: "Body", visible: true)
    -        end
    -        it "is shown the amend user group as field" do
    -          expect(page).to have_css(".field", text: "Amendment author", visible: true)
    -        end
    -        it "is shown the submit button" do
    -          expect(page).to have_button("Send amendment")
    +        it "is NOT shown a link to Amend it" do
    +          expect(page).not_to have_link("Amend Proposal")
             end
           end
    +    end
    +
    +    context "when amendment REACTION is enabled" do
    +      before do
    +        update_component_step_setting(component, :amendment_reaction_enabled, true)
    +      end
    +
    +      context "and the proposal author visits an emendation to their proposal" do
    +        let(:user) { proposal.creator_author }
     
    -      context "when the form is filled correctly" do
             before do
               login_as user, scope: :user
    -          visit decidim.new_amend_path(amendable_gid: proposal.to_sgid.to_s)
    -          within ".new_amendment" do
    -            fill_in "amendment[emendation_params][title]", with: "More sidewalks and less roads"
    -            fill_in "amendment[emendation_params][body]", with: "Cities need more people, not more cars"
    -            select user_group.name, from: :amendment_user_group_id
    -          end
    -          click_button "Send amendment"
    +          visit emendation_path
             end
     
    -        it "is shown the Success Callout" do
    -          expect(page).to have_css(".callout.success", text: "The amendment has been created successfully")
    +        it "is NOT shown the accept and reject button" do
    +          expect(page).not_to have_css(".success", text: "ACCEPT")
    +          expect(page).not_to have_css(".alert", text: "REJECT")
             end
    +      end
    +    end
     
    -        it "is shown the emendation in the amendments list" do
    -          emendation = Decidim::Proposals::Proposal.last
    -
    -          expect(page).to have_content(emendation.title)
    -          expect(page).to have_content(emendation.body)
    -          expect(page).to have_css(".card__text--status", text: "EVALUATING")
    -        end
    +    context "when amendment PROMOTION is enabled" do
    +      before do
    +        update_component_step_setting(component, :amendment_promotion_enabled, true)
           end
     
    -      context "when the form is filled incorrectly" do
    +      context "and the author of a rejected emendation visits their emendation" do
    +        let(:user) { emendation.creator_author }
    +
             before do
    +          amendment.update(state: "rejected")
               login_as user, scope: :user
    -          visit decidim.new_amend_path(amendable_gid: proposal.to_sgid.to_s)
    -          within ".new_amendment" do
    -            fill_in "amendment[emendation_params][title]", with: "INVALID TITLE"
    -          end
    -          click_button "Send amendment"
    -        end
    -
    -        it "is shown the Error Callout" do
    -          expect(page).to have_css(".callout.alert", text: "An error ocurred while creating the amendment")
    +          visit emendation_path
             end
     
    -        it "is shown the error message" do
    -          expect(page).to have_css(".form-error.is-visible", text: "Title is using too many capital letters")
    +        it "is NOT shown the promote button" do
    +          expect(page).not_to have_content("PROMOTE TO PROPOSAL")
    +          expect(page).not_to have_content("You can promote this emendation and publish it as an independent proposal")
             end
           end
         end
    +  end
    +
    +  context "with amendments enabled" do
    +    before do
    +      component.update!(settings: { amendments_enabled: true })
    +    end
     
    -    context "when viewing an amendment" do
    +    context "when amendment CREATION is enabled" do
           before do
    -        visit_component
    -        login_as user, scope: :user
    -        click_link emendation.title
    +        update_component_step_setting(component, :amendment_creation_enabled, true)
           end
     
    -      it "shows the changed attributes" do
    -        expect(page).to have_content("Amendment to \"#{proposal.title}\"")
    +      context "and visits an amendable proposal" do
    +        before do
    +          visit proposal_path
    +        end
     
    -        within ".diff-for-title" do
    -          expect(page).to have_content("TITLE")
    +        it "is shown a link to Amend it" do
    +          expect(page).to have_link("Amend Proposal")
    +        end
     
    -          within ".diff > ul > .del" do
    -            expect(page).to have_content(proposal.title)
    -          end
    +        context "when the user is not logged in and clicks" do
    +          it "is shown the login modal" do
    +            within ".card__amend-button", match: :first do
    +              click_link "Amend Proposal"
    +            end
     
    -          within ".diff > ul > .ins" do
    -            expect(page).to have_content(emendation.title)
    +            expect(page).to have_css("#loginModal", visible: true)
               end
             end
     
    -        within ".diff-for-body" do
    -          expect(page).to have_content("BODY")
    +        context "when the user is logged in and clicks" do
    +          let!(:user) { create :user, :confirmed, organization: component.organization }
    +          let!(:user_group) { create(:user_group, :verified, organization: user.organization, users: [user]) }
     
    -          within ".diff > ul > .del" do
    -            expect(page).to have_content(proposal.body)
    +          before do
    +            login_as user, scope: :user
    +            visit proposal_path
    +            click_link "Amend Proposal"
               end
     
    -          within ".diff > ul > .ins" do
    -            expect(page).to have_content(emendation.body)
    +          it "is shown the amendment create form" do
    +            expect(page).to have_css(".new_amendment", visible: true)
    +            expect(page).to have_content("CREATE YOUR AMENDMENT")
    +            expect(page).to have_css(".field", text: "Title", visible: true)
    +            expect(page).to have_css(".field", text: "Body", visible: true)
    +            expect(page).to have_css(".field", text: "Amendment author", visible: true)
    +            expect(page).to have_button("Send amendment")
    +          end
    +
    +          context "when the form is filled correctly" do
    +            before do
    +              within ".new_amendment" do
    +                fill_in "amendment[emendation_params][title]", with: "More sidewalks and less roads"
    +                fill_in "amendment[emendation_params][body]", with: "Cities need more people, not more cars"
    +                select user_group.name, from: :amendment_user_group_id # Optional
    +              end
    +              click_button "Send amendment"
    +            end
    +
    +            it "is shown the Success Callout" do
    +              expect(page).to have_css(".callout.success", text: "The amendment has been created successfully")
    +            end
    +          end
    +
    +          context "when the form is filled incorrectly" do
    +            before do
    +              within ".new_amendment" do
    +                fill_in "amendment[emendation_params][title]", with: "INVALID TITLE"
    +              end
    +              click_button "Send amendment"
    +            end
    +
    +            it "is shown the Error Callout" do
    +              expect(page).to have_css(".callout.alert", text: "An error ocurred while creating the amendment")
    +            end
    +
    +            it "is shown the field error message" do
    +              expect(page).to have_css(".form-error.is-visible", text: "Title is using too many capital letters")
    +            end
               end
             end
           end
         end
     
    -    context "when the user is the author of the amendable proposal" do
    -      let(:user) { proposal.creator_author }
    +    context "when amendment CREATION is NOT enabled" do
    +      before do
    +        update_component_step_setting(component, :amendment_creation_enabled, false)
    +      end
    +
    +      context "and visits an amendable proposal" do
    +        before do
    +          visit proposal_path
    +        end
    +
    +        it "is NOT shown a link to Amend it" do
    +          expect(page).not_to have_link("Amend Proposal")
    +        end
    +      end
    +    end
     
    +    context "when amendment REACTION is enabled" do
           before do
    -        visit_component
    -        login_as user, scope: :user
    +        update_component_step_setting(component, :amendment_reaction_enabled, true)
           end
     
    -      context "and visits an emendation to their proposal" do
    +      context "and the proposal author visits an emendation to their proposal" do
    +        let(:user) { proposal.creator_author }
    +
             before do
    -          click_link emendation.title
    +          login_as user, scope: :user
    +          visit emendation_path
             end
     
             it "is shown the accept and reject button" do
    @@ -188,7 +262,7 @@
     
             context "when the user clicks on the accept button" do
               before do
    -            visit decidim.review_amend_path(amendment)
    +            click_link "Accept"
               end
     
               it "shows the changed attributes" do
    @@ -217,11 +291,11 @@
                 end
               end
     
    -          it "is shown the review the amendment form" do
    +          it "is shown the amendment review form" do
                 expect(page).to have_css(".edit_amendment")
                 expect(page).to have_content("REVIEW THE AMENDMENT")
    -            expect(page).to have_field("Title", with: emendation.title.to_s)
    -            expect(page).to have_field("Body", with: emendation.body.to_s)
    +            expect(page).to have_field("Title", with: emendation.title)
    +            expect(page).to have_field("Body", with: emendation.body)
                 expect(page).to have_button("Accept amendment")
               end
     
    @@ -237,47 +311,59 @@
                 end
     
                 it "is changed the state of the emendation" do
    -              visit_component
    -
    -              within "#proposal_#{emendation.id}" do
    -                expect(page).to have_css(".success", text: "ACCEPTED")
    -              end
    +              expect(page).to have_css(".success", text: "This amendment for the proposal #{emendation.title} has been accepted")
                 end
               end
             end
     
             context "when the user clicks on the reject button" do
               before do
    -            find("a[href='#{decidim.reject_amend_path(amendment)}']").click
    +            click_link "Reject"
               end
     
               it "is shown the Success Callout" do
                 expect(page).to have_css(".callout.success", text: "The amendment has been successfully rejected")
               end
     
               it "is changed the state of the emendation" do
    -            visit_component
    -
    -            within "#proposal_#{emendation.id}" do
    -              expect(page).to have_css(".alert", text: "REJECTED")
    -            end
    +            expect(page).to have_css(".alert", text: "This amendment for the proposal #{proposal.title} was rejected")
               end
             end
           end
         end
     
    -    context "when the user is the author of the emendation" do
    -      let(:user) { emendation.creator_author }
    -      let!(:amendment) { create :amendment, amendable: proposal, emendation: emendation, state: "rejected" }
    +    context "when amendment REACTION is NOT enabled" do
    +      before do
    +        update_component_step_setting(component, :amendment_reaction_enabled, false)
    +      end
    +
    +      context "and the proposal author visits an emendation to their proposal" do
    +        let(:user) { proposal.creator_author }
    +
    +        before do
    +          login_as user, scope: :user
    +          visit emendation_path
    +        end
    +
    +        it "is NOT shown the accept and reject button" do
    +          expect(page).not_to have_css(".success", text: "ACCEPT")
    +          expect(page).not_to have_css(".alert", text: "REJECT")
    +        end
    +      end
    +    end
     
    +    context "when amendment PROMOTION is enabled" do
           before do
    -        visit_component
    -        login_as user, scope: :user
    +        update_component_step_setting(component, :amendment_promotion_enabled, true)
           end
     
    -      context "and visits a rejected emendation" do
    +      context "and the author of a rejected emendation visits their emendation" do
    +        let(:user) { emendation.creator_author }
    +
             before do
    -          click_link emendation.title
    +          amendment.update(state: "rejected")
    +          login_as user, scope: :user
    +          visit emendation_path
             end
     
             it "is shown the promote button" do
    @@ -287,7 +373,7 @@
     
             context "when the user clicks on the promote button" do
               before do
    -            find("a[href='#{decidim.promote_amend_path(amendment)}']").click
    +            click_link "Promote"
               end
     
               it "is shown the alert text" do
    @@ -313,5 +399,26 @@
             end
           end
         end
    +
    +    context "when amendment PROMOTION is NOT enabled" do
    +      before do
    +        update_component_step_setting(component, :amendment_promotion_enabled, false)
    +      end
    +
    +      context "and the author of a rejected emendation visits their emendation" do
    +        let(:user) { emendation.creator_author }
    +
    +        before do
    +          amendment.update(state: "rejected")
    +          login_as user, scope: :user
    +          visit emendation_path
    +        end
    +
    +        it "is NOT shown the promote button" do
    +          expect(page).not_to have_content("PROMOTE TO PROPOSAL")
    +          expect(page).not_to have_content("You can promote this emendation and publish it as an independent proposal")
    +        end
    +      end
    +    end
       end
     end
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.