Skip to content

Commit

Permalink
Add multiple statuses on proposals (#12052)
Browse files Browse the repository at this point in the history
* Add initial db structure

* Add build method

* Add proposal state model + admin presenter

* Add proposal_state factory

* Add admin interface for proposal states

* Misc changes

* Fix proposal status

* Running linters

* Fix failing Specs (#285)

* Implement custom answers

* Fix proposal fields

* Fix the admin status badges

* Fix migration script

* Add notifiable and anwerable to the proposal states

* Deprecating proposals old classes

* Migrate frontend classes

* Refactor seeds

* Fix frontend

* Fix more tests

* Remove obsolete specs

* Running linters

* More linting

* Fix specs

* Fixing the proposals specs

* Implement templates

* Fixing decidim-templates specs

* Remove unused i18n

* Fixing more specs

* Normalize I18n

* reverting helper methods

* Add proposal states tests

* Run linter

* Add release notes

* Fix the frontend filters

* Lint

* Fixing test

* Adjust labels

* Revert proposal related changes

* Fix small issues

* Running linters

* Update decidim-core/app/cells/decidim/amendable/announcement_cell.rb

* Add token validation

* Lint

* Fix tests

* Fix uniquess validator

* Refactor commands

* fix the specs on refactor

* Removed description field

* Remove boolean fields

* Fix description related errors

* Fix some tests

* Fix more specs

* Fix answerable in proposal answer

* Fix migration

* Rename migrations

* Fix failing specs

* Fix failing specs

* Fix more specs

* Fixing specs

* Remove token input

* Autogenerate the token

* removing default state of not_answered

* Remove bangs

* Fixing the specs

* Fix failing specs ...

* Fix the state vs Status

* Apply latest changes requested

* Normalize locales

* Apply suggestions from code review

Co-authored-by: Andrés Pereira de Lucena <andreslucena@users.noreply.github.com>

* Apply suggestions from code review

* Running linters, apply latest suggestions

* Update index.html.erb

* Fix failing specs

* Running linters

* Running linters

* Patch factory

* Apply Code Review Recommendations

---------

Co-authored-by: Andrés Pereira de Lucena <andreslucena@users.noreply.github.com>
  • Loading branch information
alecslupu and andreslucena committed Feb 6, 2024
1 parent c958a77 commit 93ec316
Show file tree
Hide file tree
Showing 97 changed files with 1,610 additions and 719 deletions.
1 change: 1 addition & 0 deletions Gemfile.lock
Expand Up @@ -182,6 +182,7 @@ PATH
decidim-templates (0.29.0.dev)
decidim-core (= 0.29.0.dev)
decidim-forms (= 0.29.0.dev)
decidim-proposals (= 0.29.0.dev)
decidim-verifications (0.29.0.dev)
decidim-core (= 0.29.0.dev)

Expand Down
16 changes: 16 additions & 0 deletions RELEASE_NOTES.md
Expand Up @@ -79,3 +79,19 @@ You need to change it to:
# Explain the usage of the API as it is in the new version
result = 1 + 1 if after
```

### 5.8 Migration of Proposal states in own table

As of [\#12052](https://github.com/decidim/decidim/pull/12052) all the proposals states are kept in a separate database table, enabling end users to customize the states of the proposals. By default we will create for any proposal component that is being installed in the project 5 default states that cannot be disabled nor deleted. These states are:

- Not Answered ( default state for any new created proposal )
- Evaluating
- Accepted
- Rejected
- Withdrawn ( special states for proposals that have been withdrawn by the author )

For any of the above states you can customize the name, description, css class used by labels. You can also decide which states the user can receive a notification or an answer.

You do not need to run any task to migrate the existing states, as we will automatically migrate the existing states to the new table.

You can see more details about this change on PR [\#12052](https://github.com/decidim/decidim/pull/12052)
Expand Up @@ -75,8 +75,7 @@ def proposals
end

def all_proposals
Decidim::Proposals::Proposal.where(component: origin_component)
.where(state: :accepted)
Decidim::Proposals::Proposal.where(component: origin_component).accepted
end

def origin_component
Expand Down
14 changes: 4 additions & 10 deletions decidim-core/app/cells/decidim/amendable/announcement_cell.rb
Expand Up @@ -51,16 +51,10 @@ def announcement_date
end

def state_classes
case model.state
when "accepted"
"success"
when "rejected", "withdrawn"
"alert"
when "evaluating"
"warning"
else
"muted"
end
return "muted" if model.state.blank?
return "alert" if model.withdrawn?

model.proposal_state&.css_class
end
end
end
1 change: 1 addition & 0 deletions decidim-generators/Gemfile.lock
Expand Up @@ -182,6 +182,7 @@ PATH
decidim-templates (0.29.0.dev)
decidim-core (= 0.29.0.dev)
decidim-forms (= 0.29.0.dev)
decidim-proposals (= 0.29.0.dev)
decidim-verifications (0.29.0.dev)
decidim-core (= 0.29.0.dev)

Expand Down
Expand Up @@ -19,8 +19,10 @@ def state_item

if model.withdrawn?
{ text: content_tag(:span, humanize_proposal_state(:withdrawn), class: "label alert") }
else
elsif model.emendation?
{ text: content_tag(:span, humanize_proposal_state(state), class: "label #{state_class}") }
else
{ text: content_tag(:span, translated_attribute(model.proposal_state&.title), class: "label #{model.proposal_state.css_class}") }
end
end

Expand Down
Expand Up @@ -44,7 +44,6 @@ def answer_proposal
form.current_user
) do
attributes = {
state: form.state,
answer: form.answer,
cost: form.cost,
cost_report: form.cost_report,
Expand All @@ -54,7 +53,9 @@ def answer_proposal
if form.state == "not_answered"
attributes[:answered_at] = nil
attributes[:state_published_at] = nil
proposal.proposal_state = nil
else
proposal.assign_state(form.state)
attributes[:answered_at] = Time.current
attributes[:state_published_at] = Time.current if !initial_has_state_published && form.publish_answer?
end
Expand Down
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Decidim
module Proposals
module Admin
class CreateProposalState < Decidim::Commands::CreateResource
fetch_form_attributes :title, :css_class, :announcement_title, :component

def resource_class
Decidim::Proposals::ProposalState
end
end
end
end
end
@@ -0,0 +1,10 @@
# frozen_string_literal: true

module Decidim
module Proposals
module Admin
class DestroyProposalState < Decidim::Commands::DestroyResource
end
end
end
end
Expand Up @@ -47,8 +47,14 @@ def import_proposals
def proposals
@proposals = Decidim::Proposals::Proposal
.where(component: origin_component)
.where(state: @form.states)
@proposals = @proposals.where(scope: proposal_scopes) unless proposal_scopes.empty?

@proposals = if @form.states.include?("not_answered")
@proposals.not_answered.or(@proposals.where(id: @proposals.only_status(@form.states).pluck(:id)))
else
@proposals.only_status(@form.states)
end

@proposals
end

Expand Down Expand Up @@ -80,10 +86,12 @@ def proposal_author
def proposal_answer_attributes(original_proposal)
return {} unless form.keep_answers

state = Decidim::Proposals::ProposalState.where(component: target_component, token: original_proposal.state).first

{
answer: original_proposal.answer,
answered_at: original_proposal.answered_at,
state: original_proposal.state,
proposal_state: state,
state_published_at: original_proposal.state_published_at
}
end
Expand Down
Expand Up @@ -11,7 +11,7 @@ class NotifyProposalAnswer < Decidim::Command
# initial_state - The proposal state before the current process.
def initialize(proposal, initial_state)
@proposal = proposal
@initial_state = initial_state.to_s
@initial_state = initial_state
end

# Executes the command. Broadcasts these events:
Expand Down Expand Up @@ -42,28 +42,11 @@ def state_changed?
end

def notify_followers
if proposal.accepted?
publish_event(
"decidim.events.proposals.proposal_accepted",
Decidim::Proposals::AcceptedProposalEvent
)
elsif proposal.rejected?
publish_event(
"decidim.events.proposals.proposal_rejected",
Decidim::Proposals::RejectedProposalEvent
)
elsif proposal.evaluating?
publish_event(
"decidim.events.proposals.proposal_evaluating",
Decidim::Proposals::EvaluatingProposalEvent
)
end
end
return if proposal.state == "not_answered"

def publish_event(event, event_class)
Decidim::EventsManager.publish(
event:,
event_class:,
event: "decidim.events.proposals.proposal_state_changed",
event_class: Decidim::Proposals::ProposalStateChangedEvent,
resource: proposal,
affected_users: proposal.notifiable_identities,
followers: proposal.followers - proposal.notifiable_identities
Expand Down
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Decidim
module Proposals
module Admin
class UpdateProposalState < Decidim::Commands::UpdateResource
include TranslatableAttributes

fetch_form_attributes :title, :css_class, :announcement_title, :component
end
end
end
end
Expand Up @@ -23,7 +23,7 @@ def call
return broadcast(:has_supports) if @proposal.votes.any?

transaction do
change_proposal_state_to_withdrawn
@proposal.withdraw!
reject_emendations_if_any
end

Expand All @@ -32,10 +32,6 @@ def call

private

def change_proposal_state_to_withdrawn
@proposal.withdraw!
end

def reject_emendations_if_any
return if @proposal.emendations.empty?

Expand Down
Expand Up @@ -33,8 +33,8 @@ def search_field_predicate
def filters
[
:is_emendation_true,
:with_any_state,
:state_eq,
:with_any_state,
:scope_id_eq,
:category_id_eq,
:valuator_role_ids_has
Expand All @@ -44,7 +44,7 @@ def filters
def filters_with_values
{
is_emendation_true: %w(true false),
state_eq: Proposal::STATES,
state_eq: state_eq_values,
with_any_state: %w(state_published state_not_published),
scope_id_eq: scope_ids_hash(scopes.top_level),
category_id_eq: category_ids_hash(categories.first_class),
Expand All @@ -55,7 +55,17 @@ def filters_with_values
# Cannot user `super` here, because it does not belong to a superclass
# but to a concern.
def dynamically_translated_filters
[:scope_id_eq, :category_id_eq, :valuator_role_ids_has]
[:scope_id_eq, :category_id_eq, :valuator_role_ids_has, :proposal_state_id_eq, :state_eq]
end

def translated_state_eq(state)
return t("decidim.admin.filters.proposals.state_eq.values.withdrawn") if state == "withdrawn"

translated_attribute(ProposalState.where(component: current_component, token: state).first&.title)
end

def state_eq_values
ProposalState.where(component: current_component).pluck(:token) + ["withdrawn"]
end

def valuator_role_ids
Expand Down
@@ -0,0 +1,86 @@
# frozen_string_literal: true

module Decidim
module Proposals
module Admin
class ProposalStatesController < Admin::ApplicationController
include Decidim::Admin::Paginable

helper_method :proposal_states, :proposal_state
def index
enforce_permission_to :read, :proposal_state
end

def new
enforce_permission_to :create, :proposal_state
@form = form(Decidim::Proposals::Admin::ProposalStateForm).instance
end

def create
enforce_permission_to :create, :proposal_state

@form = form(ProposalStateForm).from_params(params)

CreateProposalState.call(@form) do
on(:ok) do
flash[:notice] = I18n.t("proposal_states.create.success", scope: "decidim.proposals.admin")
redirect_to proposal_states_path
end

on(:invalid) do
flash.keep[:alert] = I18n.t("proposal_states.create.error", scope: "decidim.proposals.admin")

render action: :new
end
end
end

def edit
enforce_permission_to(:update, :proposal_state, proposal_state:)
@form = form(Decidim::Proposals::Admin::ProposalStateForm).from_model(proposal_state)
end

def update
enforce_permission_to(:update, :proposal_state, proposal_state:)
@form = form(ProposalStateForm).from_params(params)

UpdateProposalState.call(@form, proposal_state) do
on(:ok) do
flash[:notice] = I18n.t("proposal_states.update.success", scope: "decidim.proposals.admin")

redirect_to proposal_states_path
end

on(:invalid) do
flash.now[:alert] = I18n.t("proposal_states.update.error", scope: "decidim.proposals.admin")

render action: :edit
end
end
end

def destroy
enforce_permission_to(:destroy, :proposal_state, proposal_state:)

DestroyProposalState.call(proposal_state, current_user) do
on(:ok) do
flash[:notice] = I18n.t("proposal_states.destroy.success", scope: "decidim.proposals.admin")

redirect_to proposal_states_path
end
end
end

private

def proposal_state
@proposal_state ||= proposal_states.find(params[:id])
end

def proposal_states
@proposal_states ||= paginate(ProposalState.where(component: current_component))
end
end
end
end
end
Expand Up @@ -228,13 +228,20 @@ def default_filter_params
with_any_origin: nil,
activity: "all",
with_any_category: nil,
with_any_state: %w(accepted evaluating state_not_published),
with_any_state: default_states,
with_any_scope: nil,
related_to: "",
type: "all"
}
end

def default_states
[
Decidim::Proposals::ProposalState.where(component: current_component).pluck(:token).map(&:to_s),
%w(state_not_published)
].flatten - ["rejected"]
end

def proposal_draft
Proposal.from_all_author_identities(current_user).not_hidden.only_amendables
.where(component: current_component).find_by(published_at: nil)
Expand Down

This file was deleted.

0 comments on commit 93ec316

Please sign in to comment.