decidim-proposals 0.28.3 → 0.29.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -18
  3. data/app/cells/decidim/proposals/cost_report_cell.rb +0 -3
  4. data/app/cells/decidim/proposals/highlighted_proposals_for_component_cell.rb +1 -1
  5. data/app/cells/decidim/proposals/participatory_text_proposal/buttons.erb +1 -1
  6. data/app/cells/decidim/proposals/participatory_text_proposal_cell.rb +2 -3
  7. data/app/cells/decidim/proposals/proposal_cell.rb +2 -0
  8. data/app/cells/decidim/proposals/proposal_g/show.erb +23 -0
  9. data/app/cells/decidim/proposals/proposal_g_cell.rb +48 -0
  10. data/app/cells/decidim/proposals/proposal_l_cell.rb +1 -3
  11. data/app/cells/decidim/proposals/proposal_metadata_cell.rb +23 -15
  12. data/app/commands/decidim/proposals/admin/answer_proposal.rb +2 -1
  13. data/app/commands/decidim/proposals/admin/assign_proposals_to_valuator.rb +7 -5
  14. data/app/commands/decidim/proposals/admin/create_proposal.rb +2 -2
  15. data/app/commands/decidim/proposals/admin/create_proposal_state.rb +15 -0
  16. data/app/commands/decidim/proposals/admin/destroy_proposal_state.rb +10 -0
  17. data/app/commands/decidim/proposals/admin/import_proposals.rb +10 -2
  18. data/app/commands/decidim/proposals/admin/notify_proposal_answer.rb +4 -21
  19. data/app/commands/decidim/proposals/admin/unassign_proposals_from_valuator.rb +6 -4
  20. data/app/commands/decidim/proposals/admin/update_proposal_state.rb +13 -0
  21. data/app/commands/decidim/proposals/create_proposal.rb +21 -2
  22. data/app/commands/decidim/proposals/update_proposal.rb +2 -2
  23. data/app/commands/decidim/proposals/vote_proposal.rb +1 -1
  24. data/app/commands/decidim/proposals/withdraw_proposal.rb +3 -7
  25. data/app/controllers/concerns/decidim/proposals/admin/filterable.rb +10 -22
  26. data/app/controllers/decidim/proposals/admin/proposal_states_controller.rb +86 -0
  27. data/app/controllers/decidim/proposals/admin/proposals_controller.rb +4 -0
  28. data/app/controllers/decidim/proposals/admin/valuation_assignments_controller.rb +8 -11
  29. data/app/controllers/decidim/proposals/proposals_controller.rb +30 -35
  30. data/app/events/decidim/proposals/proposal_state_changed_event.rb +37 -0
  31. data/app/forms/decidim/proposals/admin/proposal_answer_form.rb +5 -1
  32. data/app/forms/decidim/proposals/admin/proposal_state_form.rb +22 -0
  33. data/app/forms/decidim/proposals/admin/proposals_fork_form.rb +1 -1
  34. data/app/forms/decidim/proposals/admin/proposals_import_form.rb +1 -1
  35. data/app/forms/decidim/proposals/admin/valuation_assignment_form.rb +12 -14
  36. data/app/forms/decidim/proposals/proposal_form.rb +25 -4
  37. data/app/forms/decidim/proposals/reject_access_to_collaborative_draft_form.rb +1 -1
  38. data/app/forms/decidim/proposals/request_access_to_collaborative_draft_form.rb +1 -1
  39. data/app/helpers/decidim/proposals/admin/proposal_bulk_actions_helper.rb +7 -17
  40. data/app/helpers/decidim/proposals/admin/proposals_helper.rb +13 -89
  41. data/app/helpers/decidim/proposals/application_helper.rb +16 -10
  42. data/app/helpers/decidim/proposals/proposal_cells_helper.rb +6 -2
  43. data/app/helpers/decidim/proposals/proposal_votes_helper.rb +3 -3
  44. data/app/helpers/decidim/proposals/proposal_wizard_helper.rb +5 -8
  45. data/app/helpers/decidim/proposals/proposals_helper.rb +18 -24
  46. data/app/models/decidim/proposals/proposal.rb +78 -28
  47. data/app/models/decidim/proposals/proposal_state.rb +58 -0
  48. data/app/packs/documents/decidim/proposals/participatory_texts/participatory_text.md +1 -3
  49. data/app/packs/images/decidim/proposals/proposal-placeholder-card-g.svg +15 -0
  50. data/app/packs/src/decidim/proposals/add_proposal.js +2 -0
  51. data/app/packs/src/decidim/proposals/admin/proposals.js +43 -8
  52. data/app/packs/stylesheets/decidim/proposals/proposals.scss +39 -1
  53. data/app/permissions/decidim/proposals/admin/permissions.rb +16 -4
  54. data/app/presenters/decidim/proposals/admin_log/proposal_state_presenter.rb +21 -0
  55. data/app/presenters/decidim/proposals/proposal_presenter.rb +12 -3
  56. data/app/queries/decidim/proposals/metrics/endorsements_metric_manage.rb +1 -1
  57. data/app/queries/decidim/proposals/metrics/proposal_followers_metric_measure.rb +1 -1
  58. data/app/queries/decidim/proposals/metrics/proposal_participants_metric_measure.rb +4 -4
  59. data/app/queries/decidim/proposals/metrics/proposals_metric_manage.rb +1 -1
  60. data/app/queries/decidim/proposals/metrics/votes_metric_manage.rb +1 -1
  61. data/app/services/decidim/proposals/diff_renderer.rb +1 -1
  62. data/app/views/decidim/proposals/admin/imports/_proposals_fields.html.erb +1 -1
  63. data/app/views/decidim/proposals/admin/participatory_texts/index.html.erb +3 -2
  64. data/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb +2 -2
  65. data/app/views/decidim/proposals/admin/proposal_notes/_proposal_notes.html.erb +3 -3
  66. data/app/views/decidim/proposals/admin/proposal_states/_form.html.erb +67 -0
  67. data/app/views/decidim/proposals/admin/proposal_states/edit.html.erb +18 -0
  68. data/app/views/decidim/proposals/admin/proposal_states/index.html.erb +50 -0
  69. data/app/views/decidim/proposals/admin/proposal_states/new.html.erb +18 -0
  70. data/app/views/decidim/proposals/admin/proposals/_bulk-actions.html.erb +6 -12
  71. data/app/views/decidim/proposals/admin/proposals/_form.html.erb +6 -6
  72. data/app/views/decidim/proposals/admin/proposals/_proposal-tr.html.erb +2 -2
  73. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_assign_to_valuator.html.erb +11 -7
  74. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb +7 -5
  75. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_merge.html.erb +2 -2
  76. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_publish_answers.html.erb +2 -2
  77. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_recategorize.html.erb +2 -2
  78. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_scope-change.html.erb +2 -2
  79. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_split.html.erb +2 -2
  80. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_unassign_from_valuator.html.erb +11 -7
  81. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_valuators_picker.html.erb +12 -0
  82. data/app/views/decidim/proposals/admin/proposals/index.html.erb +9 -5
  83. data/app/views/decidim/proposals/admin/proposals/publish_answers.js.erb +1 -1
  84. data/app/views/decidim/proposals/admin/proposals/show.html.erb +3 -2
  85. data/app/views/decidim/proposals/admin/proposals/update_attribute.js.erb +3 -3
  86. data/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb +12 -5
  87. data/app/views/decidim/proposals/proposals/_proposal.html.erb +1 -1
  88. data/app/views/decidim/proposals/proposals/_proposal_aside.html.erb +1 -1
  89. data/app/views/decidim/proposals/proposals/_proposals.html.erb +9 -3
  90. data/app/views/decidim/proposals/proposals/_vote_button.html.erb +1 -1
  91. data/app/views/decidim/proposals/proposals/_voting_rules.html.erb +3 -3
  92. data/app/views/decidim/proposals/proposals/_wizard_header.html.erb +0 -1
  93. data/app/views/decidim/proposals/proposals/new.html.erb +2 -7
  94. data/app/views/decidim/proposals/proposals/participatory_texts/_proposal_vote_button.html.erb +4 -4
  95. data/app/views/decidim/proposals/proposals/participatory_texts/_proposal_votes_count.html.erb +8 -8
  96. data/app/views/decidim/proposals/proposals/preview.html.erb +1 -1
  97. data/app/views/decidim/proposals/proposals/show.html.erb +1 -1
  98. data/config/locales/ar.yml +6 -114
  99. data/config/locales/bg.yml +110 -106
  100. data/config/locales/ca.yml +81 -78
  101. data/config/locales/cs.yml +60 -122
  102. data/config/locales/de.yml +122 -119
  103. data/config/locales/el.yml +3 -118
  104. data/config/locales/en.yml +110 -107
  105. data/config/locales/es-MX.yml +82 -79
  106. data/config/locales/es-PY.yml +85 -82
  107. data/config/locales/es.yml +81 -78
  108. data/config/locales/eu.yml +103 -107
  109. data/config/locales/fi-plain.yml +87 -84
  110. data/config/locales/fi.yml +118 -115
  111. data/config/locales/fr-CA.yml +86 -83
  112. data/config/locales/fr.yml +78 -75
  113. data/config/locales/ga-IE.yml +1 -27
  114. data/config/locales/gl.yml +5 -104
  115. data/config/locales/he-IL.yml +0 -13
  116. data/config/locales/hu.yml +15 -88
  117. data/config/locales/id-ID.yml +2 -97
  118. data/config/locales/is-IS.yml +1 -33
  119. data/config/locales/it.yml +6 -96
  120. data/config/locales/ja.yml +110 -107
  121. data/config/locales/lb.yml +1 -0
  122. data/config/locales/lt.yml +3 -122
  123. data/config/locales/lv.yml +2 -96
  124. data/config/locales/nl.yml +6 -95
  125. data/config/locales/no.yml +3 -107
  126. data/config/locales/pl.yml +106 -113
  127. data/config/locales/pt-BR.yml +5 -81
  128. data/config/locales/pt.yml +5 -107
  129. data/config/locales/ro-RO.yml +6 -110
  130. data/config/locales/ru.yml +2 -53
  131. data/config/locales/sk.yml +2 -103
  132. data/config/locales/sv.yml +66 -206
  133. data/config/locales/tr-TR.yml +57 -103
  134. data/config/locales/uk.yml +3 -54
  135. data/config/locales/zh-CN.yml +2 -99
  136. data/config/locales/zh-TW.yml +3 -116
  137. data/db/migrate/20240110203500_add_withdrawn_at_field_to_proposals.rb +27 -0
  138. data/db/migrate/20240110203501_create_decidim_proposals_proposal_state.rb +14 -0
  139. data/db/migrate/20240110203502_add_state_id_to_decidim_proposals_proposals.rb +13 -0
  140. data/db/migrate/20240110203503_remove_state_from_decidim_proposals_proposals.rb +11 -0
  141. data/db/migrate/20240110203504_create_default_proposal_states.rb +31 -0
  142. data/db/migrate/20240209092404_change_color_fields_on_proposals_states.rb +54 -0
  143. data/decidim-proposals.gemspec +2 -2
  144. data/lib/decidim/api/proposal_type.rb +4 -0
  145. data/lib/decidim/proposals/admin_engine.rb +8 -0
  146. data/lib/decidim/proposals/admin_filter.rb +37 -0
  147. data/lib/decidim/proposals/component.rb +8 -5
  148. data/lib/decidim/proposals/engine.rb +1 -15
  149. data/lib/decidim/proposals/import/proposal_answer_creator.rb +6 -6
  150. data/lib/decidim/proposals/import/proposal_creator.rb +1 -1
  151. data/lib/decidim/proposals/markdown_to_proposals.rb +2 -8
  152. data/lib/decidim/proposals/proposal_serializer.rb +5 -3
  153. data/lib/decidim/proposals/seeds.rb +60 -51
  154. data/lib/decidim/proposals/test/factories.rb +64 -8
  155. data/lib/decidim/proposals/version.rb +1 -1
  156. data/lib/decidim/proposals.rb +84 -12
  157. data/lib/tasks/proposals/upgrade/decidim_proposals_upgrade_tasks.rake +32 -0
  158. metadata +50 -36
  159. data/app/events/decidim/proposals/accepted_proposal_event.rb +0 -17
  160. data/app/events/decidim/proposals/evaluating_proposal_event.rb +0 -11
  161. data/app/events/decidim/proposals/rejected_proposal_event.rb +0 -17
  162. data/app/forms/decidim/proposals/proposal_wizard_create_step_form.rb +0 -44
  163. data/app/queries/decidim/proposals/similar_proposals.rb +0 -67
  164. data/app/views/decidim/proposals/proposals/_endorsements_card_row.html.erb +0 -0
  165. data/app/views/decidim/proposals/proposals/_proposal_badge.html.erb +0 -3
  166. data/app/views/decidim/proposals/proposals/compare.html.erb +0 -24
  167. data/app/views/decidim/proposals/proposals/complete.html.erb +0 -31
  168. data/lib/tasks/proposals/upgrade/decdim_proposal_upgrade_tasks.rake +0 -34
@@ -8,6 +8,16 @@ module Decidim
8
8
  #
9
9
  module ProposalsHelper
10
10
  include Decidim::Admin::ResourceScopeHelper
11
+ include Decidim::TranslatableAttributes
12
+
13
+ def available_states
14
+ [
15
+ Decidim::Proposals::ProposalState.where(component: current_component).new(
16
+ token: "not_answered",
17
+ title: t("decidim.proposals.answers.not_answered")
18
+ )
19
+ ] + Decidim::Proposals::ProposalState.where(component: current_component).all
20
+ end
11
21
 
12
22
  # Public: A formatted collection of Meetings to be used
13
23
  # in forms.
@@ -32,96 +42,10 @@ module Decidim
32
42
  end
33
43
 
34
44
  def proposal_complete_state(proposal)
35
- return humanize_proposal_state(proposal.internal_state).html_safe if proposal.answered? && !proposal.published_state?
36
-
37
- humanize_proposal_state(proposal.state).html_safe
38
- end
39
-
40
- def proposals_admin_filter_tree
41
- {
42
- t("proposals.filters.type", scope: "decidim.proposals") => {
43
- link_to(t("proposals", scope: "decidim.proposals.application_helper.filter_type_values"), q: ransak_params_for_query(is_emendation_true: "0"),
44
- per_page:) => nil,
45
- link_to(t("amendments", scope: "decidim.proposals.application_helper.filter_type_values"), q: ransak_params_for_query(is_emendation_true: "1"),
46
- per_page:) => nil
47
- },
48
- t("models.proposal.fields.state", scope: "decidim.proposals") =>
49
- Decidim::Proposals::Proposal::STATES.each_pair do |state, hash|
50
- if state == "not_answered"
51
- hash[link_to((humanize_proposal_state state), q: ransak_params_for_query(state_null: 1), per_page:)] = nil
52
- else
53
- hash[link_to((humanize_proposal_state state), q: ransak_params_for_query(state_eq: state), per_page:)] = nil
54
- end
55
- end,
56
- t("models.proposal.fields.category", scope: "decidim.proposals") => admin_filter_categories_tree(categories.first_class),
57
- t("proposals.filters.scope", scope: "decidim.proposals") => admin_filter_scopes_tree(current_component.organization.id)
58
- }
59
- end
60
-
61
- def proposals_admin_search_hidden_params
62
- return unless params[:q]
45
+ return humanize_proposal_state(:withdrawn).html_safe if proposal.withdrawn?
46
+ return humanize_proposal_state("not_answered").html_safe if proposal.proposal_state.nil?
63
47
 
64
- tags = ""
65
- tags += hidden_field_tag "per_page", params[:per_page] if params[:per_page]
66
- tags += hidden_field_tag "q[is_emendation_true]", params[:q][:is_emendation_true] if params[:q][:is_emendation_true]
67
- tags += hidden_field_tag "q[state_eq]", params[:q][:state_eq] if params[:q][:state_eq]
68
- tags += hidden_field_tag "q[category_id_eq]", params[:q][:category_id_eq] if params[:q][:category_id_eq]
69
- tags += hidden_field_tag "q[scope_id_eq]", params[:q][:scope_id_eq] if params[:q][:scope_id_eq]
70
- tags.html_safe
71
- end
72
-
73
- def proposals_admin_filter_applied_filters
74
- html = []
75
- if params[:q][:is_emendation_true].present?
76
- html << tag.span(class: "label secondary") do
77
- tag = "#{t("filters.type", scope: "decidim.proposals.proposals")}: "
78
- tag += if params[:q][:is_emendation_true].to_s == "1"
79
- t("amendments", scope: "decidim.proposals.application_helper.filter_type_values")
80
- else
81
- t("proposals", scope: "decidim.proposals.application_helper.filter_type_values")
82
- end
83
- tag += icon_link_to("delete-bin-line", url_for(q: ransak_params_for_query_without(:is_emendation_true), per_page:), t("decidim.admin.actions.cancel"),
84
- class: "action-icon--remove")
85
- tag.html_safe
86
- end
87
- end
88
- if params[:q][:state_null]
89
- html << tag.span(class: "label secondary") do
90
- tag = "#{t("models.proposal.fields.state", scope: "decidim.proposals")}: "
91
- tag += humanize_proposal_state "not_answered"
92
- tag += icon_link_to("delete-bin-line", url_for(q: ransak_params_for_query_without(:state_null), per_page:), t("decidim.admin.actions.cancel"),
93
- class: "action-icon--remove")
94
- tag.html_safe
95
- end
96
- end
97
- if params[:q][:state_eq]
98
- html << tag.span(class: "label secondary") do
99
- tag = "#{t("models.proposal.fields.state", scope: "decidim.proposals")}: "
100
- tag += humanize_proposal_state params[:q][:state_eq]
101
- tag += icon_link_to("delete-bin-line", url_for(q: ransak_params_for_query_without(:state_eq), per_page:), t("decidim.admin.actions.cancel"),
102
- class: "action-icon--remove")
103
- tag.html_safe
104
- end
105
- end
106
- if params[:q][:category_id_eq]
107
- html << tag.span(class: "label secondary") do
108
- tag = "#{t("models.proposal.fields.category", scope: "decidim.proposals")}: "
109
- tag += translated_attribute categories.find(params[:q][:category_id_eq]).name
110
- tag += icon_link_to("delete-bin-line", url_for(q: ransak_params_for_query_without(:category_id_eq), per_page:), t("decidim.admin.actions.cancel"),
111
- class: "action-icon--remove")
112
- tag.html_safe
113
- end
114
- end
115
- if params[:q][:scope_id_eq]
116
- html << tag.span(class: "label secondary") do
117
- tag = "#{t("models.proposal.fields.scope", scope: "decidim.proposals")}: "
118
- tag += translated_attribute Decidim::Scope.where(decidim_organization_id: current_component.organization.id).find(params[:q][:scope_id_eq]).name
119
- tag += icon_link_to("delete-bin-line", url_for(q: ransak_params_for_query_without(:scope_id_eq), per_page:), t("decidim.admin.actions.cancel"),
120
- class: "action-icon--remove")
121
- tag.html_safe
122
- end
123
- end
124
- html.join(" ").html_safe
48
+ translated_attribute(proposal&.proposal_state&.title)
125
49
  end
126
50
 
127
51
  def icon_with_link_to_proposal(proposal)
@@ -28,16 +28,22 @@ module Decidim
28
28
  I18n.t(state, scope: "decidim.proposals.answers", default: :not_answered)
29
29
  end
30
30
 
31
+ def proposal_state_css_style(proposal)
32
+ return "" if proposal.emendation?
33
+
34
+ proposal.proposal_state&.css_style
35
+ end
36
+
31
37
  # Public: The css class applied based on the proposal state.
32
38
  #
33
39
  # proposal - The proposal to evaluate.
34
40
  #
35
41
  # Returns a String.
36
42
  def proposal_state_css_class(proposal)
37
- state = proposal.state
38
- state = proposal.internal_state if proposal.answered? && !proposal.published_state?
43
+ return "alert" if proposal.withdrawn?
44
+ return if proposal.state.blank?
39
45
 
40
- case state
46
+ case proposal.state
41
47
  when "accepted"
42
48
  "success"
43
49
  when "rejected", "withdrawn"
@@ -144,7 +150,7 @@ module Decidim
144
150
  return true if vote_limit_enabled?
145
151
  return true if threshold_per_proposal_enabled?
146
152
  return true if proposal_limit_enabled?
147
- return true if can_accumulate_supports_beyond_threshold?
153
+ return true if can_accumulate_votes_beyond_threshold?
148
154
  return true if minimum_votes_per_user_enabled?
149
155
  end
150
156
 
@@ -166,7 +172,7 @@ module Decidim
166
172
  base
167
173
  end
168
174
 
169
- # Explicitely commenting the used I18n keys so their are not flagged as unused
175
+ # Explicitly commenting the used I18n keys so their are not flagged as unused
170
176
  # i18n-tasks-use t('decidim.proposals.application_helper.filter_origin_values.official')
171
177
  # i18n-tasks-use t('decidim.proposals.application_helper.filter_origin_values.participants')
172
178
  # i18n-tasks-use t('decidim.proposals.application_helper.filter_origin_values.user_groups')
@@ -191,11 +197,11 @@ module Decidim
191
197
  Decidim::CheckBoxesTreeHelper::TreeNode.new(
192
198
  Decidim::CheckBoxesTreeHelper::TreePoint.new("", t("decidim.proposals.application_helper.filter_state_values.all")),
193
199
  [
194
- Decidim::CheckBoxesTreeHelper::TreePoint.new("accepted", t("decidim.proposals.application_helper.filter_state_values.accepted")),
195
- Decidim::CheckBoxesTreeHelper::TreePoint.new("evaluating", t("decidim.proposals.application_helper.filter_state_values.evaluating")),
196
- Decidim::CheckBoxesTreeHelper::TreePoint.new("state_not_published", t("decidim.proposals.application_helper.filter_state_values.not_answered")),
197
- Decidim::CheckBoxesTreeHelper::TreePoint.new("rejected", t("decidim.proposals.application_helper.filter_state_values.rejected"))
198
- ]
200
+ Decidim::CheckBoxesTreeHelper::TreePoint.new("state_not_published", t("decidim.proposals.application_helper.filter_state_values.not_answered"))
201
+ ] +
202
+ Decidim::Proposals::ProposalState.where(component: current_component).where.not(token: "not_answered").map do |state|
203
+ Decidim::CheckBoxesTreeHelper::TreePoint.new(state.token, translated_attribute(state.title))
204
+ end
199
205
  )
200
206
  end
201
207
 
@@ -14,7 +14,7 @@ module Decidim
14
14
  include Decidim::TranslatableAttributes
15
15
  include Decidim::CardHelper
16
16
 
17
- delegate :title, :state, :published_state?, :withdrawn?, :amendable?, :emendation?, to: :model
17
+ delegate :title, :proposal_state, :state, :published_state?, :withdrawn?, :amendable?, :emendation?, to: :model
18
18
 
19
19
  def has_actions?
20
20
  return context[:has_actions] if context[:has_actions].present?
@@ -53,14 +53,18 @@ module Decidim
53
53
  end
54
54
 
55
55
  def badge_name
56
+ return humanize_proposal_state(:withdrawn) if withdrawn?
57
+
56
58
  humanize_proposal_state state
57
59
  end
58
60
 
59
61
  def state_classes
62
+ return ["alert"] if withdrawn?
63
+
60
64
  case state
61
65
  when "accepted"
62
66
  ["success"]
63
- when "rejected", "withdrawn"
67
+ when "rejected"
64
68
  ["alert"]
65
69
  when "evaluating"
66
70
  ["warning"]
@@ -36,11 +36,11 @@ module Decidim
36
36
  component_settings.threshold_per_proposal
37
37
  end
38
38
 
39
- # Public: Checks if can accumulate more than maxium is enabled
39
+ # Public: Checks if can accumulate more than maximum is enabled
40
40
  #
41
41
  # Returns true if enabled, false otherwise.
42
- def can_accumulate_supports_beyond_threshold?
43
- component_settings.can_accumulate_supports_beyond_threshold
42
+ def can_accumulate_votes_beyond_threshold?
43
+ component_settings.can_accumulate_votes_beyond_threshold
44
44
  end
45
45
 
46
46
  # Public: Checks if voting is enabled in this step.
@@ -12,7 +12,7 @@ module Decidim
12
12
  active = current_step == wizard_step
13
13
  inactive_data = {} if active
14
14
  attributes = active ? { "aria-current": "step", "aria-label": "#{t("current_step", scope:)}: #{t(wizard_step, scope:)}", data: { active: "" } } : inactive_data
15
- content_tag(:li, t(wizard_step, scope:), attributes)
15
+ content_tag(:li, t(wizard_step, scope:), attributes.merge(class: "w-1/2"))
16
16
  end.join.html_safe
17
17
  end
18
18
  end
@@ -41,24 +41,21 @@ module Decidim
41
41
  end
42
42
 
43
43
  def proposal_wizard_steps
44
- [ProposalsController::STEP1, ProposalsController::STEP2, ProposalsController::STEP3, ProposalsController::STEP4]
44
+ [ProposalsController::STEP1, ProposalsController::STEP2]
45
45
  end
46
46
 
47
47
  private
48
48
 
49
49
  def total_steps
50
- 4
50
+ 2
51
51
  end
52
52
 
53
- # Renders the back link except for step_2: compare
54
53
  def proposal_wizard_aside_link_to_back(step)
55
54
  case step
56
55
  when ProposalsController::STEP1
57
56
  proposals_path
58
- when ProposalsController::STEP3
59
- compare_proposal_path
60
- when ProposalsController::STEP4
61
- edit_draft_proposal_path
57
+ when ProposalsController::STEP2
58
+ complete_proposal_path
62
59
  end
63
60
  end
64
61
 
@@ -6,39 +6,33 @@ module Decidim
6
6
  module ProposalsHelper
7
7
  def proposal_reason_callout_announcement
8
8
  {
9
- title: proposal_reason_callout_title,
9
+ title: translated_attribute(@proposal.proposal_state&.announcement_title),
10
10
  body: decidim_sanitize_editor_admin(translated_attribute(@proposal.answer))
11
11
  }
12
12
  end
13
13
 
14
- def proposal_reason_callout_class
15
- case @proposal.state
16
- when "accepted"
17
- "success"
18
- when "evaluating"
19
- "warning"
20
- when "rejected"
21
- "alert"
22
- else
23
- ""
24
- end
14
+ def proposal_has_costs?
15
+ @proposal.cost.present? &&
16
+ translated_attribute(@proposal.cost_report).present? &&
17
+ translated_attribute(@proposal.execution_period).present?
25
18
  end
26
19
 
27
- def proposal_reason_callout_title
28
- i18n_key = case @proposal.state
29
- when "evaluating"
30
- "proposal_in_evaluation_reason"
31
- else
32
- "proposal_#{@proposal.state}_reason"
33
- end
20
+ def toggle_view_mode_link(current_mode, target_mode, title)
21
+ path = proposals_path(view_mode: target_mode)
22
+ icon_name = target_mode == "grid" ? "layout-grid-fill" : "list-check"
23
+ icon_class = "view-icon--disabled" unless current_mode == target_mode
24
+
25
+ link_to path, remote: true, title: do
26
+ icon(icon_name, class: icon_class, role: "img", "aria-hidden": true)
27
+ end
28
+ end
34
29
 
35
- t(i18n_key, scope: "decidim.proposals.proposals.show")
30
+ def proposals_container_class(view_mode)
31
+ view_mode == "grid" ? "card__grid-grid" : "card__list-list"
36
32
  end
37
33
 
38
- def proposal_has_costs?
39
- @proposal.cost.present? &&
40
- translated_attribute(@proposal.cost_report).present? &&
41
- translated_attribute(@proposal.execution_period).present?
34
+ def card_size_for_view_mode(view_mode)
35
+ view_mode == "grid" ? :g : nil
42
36
  end
43
37
 
44
38
  def resource_version(resource, options = {})
@@ -29,9 +29,13 @@ module Decidim
29
29
  include Decidim::TranslatableAttributes
30
30
  include Decidim::FilterableResource
31
31
 
32
- translatable_fields :title, :body
32
+ def assign_state(token)
33
+ proposal_state = Decidim::Proposals::ProposalState.where(component:, token:).first
34
+
35
+ self.proposal_state = proposal_state
36
+ end
33
37
 
34
- STATES = { not_answered: 0, evaluating: 10, accepted: 20, rejected: -10, withdrawn: -20 }.freeze
38
+ translatable_fields :title, :body
35
39
 
36
40
  fingerprint fields: [:title, :body]
37
41
 
@@ -42,6 +46,13 @@ module Decidim
42
46
 
43
47
  component_manifest_name "proposals"
44
48
 
49
+ belongs_to :proposal_state,
50
+ class_name: "Decidim::Proposals::ProposalState",
51
+ foreign_key: "decidim_proposals_proposal_state_id",
52
+ inverse_of: :proposals,
53
+ optional: true,
54
+ counter_cache: true
55
+
45
56
  has_many :votes,
46
57
  -> { final },
47
58
  foreign_key: "decidim_proposal_id",
@@ -55,19 +66,31 @@ module Decidim
55
66
 
56
67
  geocoded_by :address
57
68
 
58
- enum state: STATES, _default: "not_answered"
69
+ scope :not_status, lambda { |status|
70
+ joins(:proposal_state).where.not(decidim_proposals_proposal_states: { token: status })
71
+ }
72
+
73
+ scope :only_status, lambda { |status|
74
+ joins(:proposal_state).where(decidim_proposals_proposal_states: { token: status })
75
+ }
76
+
77
+ scope :accepted, -> { state_published.only_status(:accepted) }
78
+ scope :rejected, -> { state_published.only_status(:rejected) }
79
+ scope :evaluating, -> { state_published.only_status(:evaluating) }
59
80
 
60
- scope :accepted, -> { state_published.where(state: "accepted") }
61
- scope :rejected, -> { state_published.where(state: "rejected") }
62
- scope :evaluating, -> { state_published.where(state: "evaluating") }
81
+ scope :gamified, -> { only_status(:accepted).where(decidim_proposals_proposal_states: { gamified: true }) }
63
82
 
64
83
  scope :answered, -> { where.not(answered_at: nil) }
65
84
  scope :not_answered, -> { where(answered_at: nil) }
66
85
 
67
86
  scope :state_not_published, -> { where(state_published_at: nil) }
68
87
  scope :state_published, -> { where.not(state_published_at: nil) }
69
- scope :except_rejected, -> { not_rejected.or(state_not_published) }
70
- scope :except_withdrawn, -> { not_withdrawn }
88
+
89
+ scope :except_rejected, -> { state_published.not_status(:rejected).or(state_not_published) }
90
+
91
+ scope :withdrawn, -> { where.not(withdrawn_at: nil) }
92
+ scope :not_withdrawn, -> { where(withdrawn_at: nil) }
93
+
71
94
  scope :drafts, -> { where(published_at: nil) }
72
95
  scope :published, -> { where.not(published_at: nil) }
73
96
  scope :order_by_most_recent, -> { order(created_at: :desc) }
@@ -77,7 +100,7 @@ module Decidim
77
100
  when "withdrawn"
78
101
  withdrawn
79
102
  else
80
- except_withdrawn
103
+ not_withdrawn
81
104
  end
82
105
  }
83
106
 
@@ -104,7 +127,34 @@ module Decidim
104
127
  order(valuation_assignments_count: :desc)
105
128
  }
106
129
 
107
- scope_search_multi :with_any_state, [:accepted, :rejected, :evaluating, :state_not_published, :state_published]
130
+ scope :state_eq, lambda { |state|
131
+ return withdrawn if state == "withdrawn"
132
+
133
+ only_status(state)
134
+ }
135
+
136
+ scope :with_any_state, lambda { |*value_keys|
137
+ possible_scopes = [:state_not_published, :state_published]
138
+ custom_states = Decidim::Proposals::ProposalState.distinct.pluck(:token)
139
+
140
+ search_values = value_keys.compact.compact_blank
141
+
142
+ conditions = possible_scopes.map do |scope|
143
+ search_values.member?(scope.to_s) ? try(scope) : nil
144
+ end.compact
145
+
146
+ additional_conditions = search_values & custom_states
147
+ conditions.push(state_published.only_status(additional_conditions)) if additional_conditions.any?
148
+
149
+ return self unless conditions.any?
150
+
151
+ scoped_query = where(id: conditions.shift)
152
+ conditions.each do |condition|
153
+ scoped_query = scoped_query.or(where(id: condition))
154
+ end
155
+
156
+ scoped_query
157
+ }
108
158
 
109
159
  def self.with_valuation_assigned_to(user, space)
110
160
  valuator_roles = space.user_roles(:valuator).where(user:)
@@ -145,7 +195,7 @@ module Decidim
145
195
  .where(decidim_coauthorships: { decidim_author_type: "Decidim::UserBaseEntity" })
146
196
  .not_hidden
147
197
  .published
148
- .except_withdrawn
198
+ .not_withdrawn
149
199
  end
150
200
 
151
201
  def self.newsletter_participant_ids(component)
@@ -194,10 +244,10 @@ module Decidim
194
244
  return amendment.state if emendation?
195
245
  return nil unless published_state? || withdrawn?
196
246
 
197
- super
247
+ proposal_state&.token || "not_answered"
198
248
  end
199
249
 
200
- # This is only used to define the setter, as the getter will be overriden below.
250
+ # This is only used to define the setter, as the getter will be overridden below.
201
251
  alias_attribute :internal_state, :state
202
252
 
203
253
  # Public: Returns the internal state of the proposal.
@@ -206,7 +256,7 @@ module Decidim
206
256
  def internal_state
207
257
  return amendment.state if emendation?
208
258
 
209
- self[:state]
259
+ proposal_state&.token || "not_answered"
210
260
  end
211
261
 
212
262
  # Public: Checks if the organization has published the state for the proposal.
@@ -227,7 +277,7 @@ module Decidim
227
277
  #
228
278
  # Returns Boolean.
229
279
  def withdrawn?
230
- internal_state == "withdrawn"
280
+ withdrawn_at.present?
231
281
  end
232
282
 
233
283
  # Public: Checks if the organization has accepted a proposal.
@@ -302,11 +352,11 @@ module Decidim
302
352
  votes.count >= maximum_votes
303
353
  end
304
354
 
305
- # Public: Can accumulate more votres than maximum for this proposal.
355
+ # Public: Can accumulate more votes than maximum for this proposal.
306
356
  #
307
357
  # Returns true if can accumulate, false otherwise
308
- def can_accumulate_supports_beyond_threshold
309
- component.settings.can_accumulate_supports_beyond_threshold
358
+ def can_accumulate_votes_beyond_threshold
359
+ component.settings.can_accumulate_votes_beyond_threshold
310
360
  end
311
361
 
312
362
  # Checks whether the user can edit the given proposal.
@@ -325,6 +375,11 @@ module Decidim
325
375
  user && !withdrawn? && authored_by?(user) && !copied_from_other_component?
326
376
  end
327
377
 
378
+ def withdraw!
379
+ self.withdrawn_at = Time.zone.now
380
+ save
381
+ end
382
+
328
383
  # Public: Whether the proposal is a draft or not.
329
384
  def draft?
330
385
  published_at.nil?
@@ -348,7 +403,7 @@ module Decidim
348
403
  end
349
404
 
350
405
  def self.ransackable_scopes(auth_object = nil)
351
- base = [:with_any_origin, :with_any_state, :voted_by, :coauthored_by, :related_to, :with_any_scope, :with_any_category]
406
+ base = [:with_any_origin, :with_any_state, :state_eq, :voted_by, :coauthored_by, :related_to, :with_any_scope, :with_any_category]
352
407
  return base unless auth_object&.admin?
353
408
 
354
409
  # Add extra scopes for admins for the admin panel searches
@@ -390,10 +445,6 @@ module Decidim
390
445
  Arel.sql(%{cast("decidim_proposals_proposals"."id" as text)})
391
446
  end
392
447
 
393
- ransacker :state, formatter: proc { |v| STATES[v.to_sym] } do |parent|
394
- parent.table[:state]
395
- end
396
-
397
448
  ransacker :is_emendation do |_parent|
398
449
  query = <<-SQL.squish
399
450
  (
@@ -430,13 +481,12 @@ module Decidim
430
481
  end
431
482
 
432
483
  def process_amendment_state_change!
433
- return unless %w(accepted rejected evaluating withdrawn).member?(amendment.state)
484
+ return withdraw! if amendment.withdrawn?
485
+ return unless %w(accepted rejected evaluating).member?(amendment.state)
434
486
 
435
487
  PaperTrail.request(enabled: false) do
436
- update!(
437
- state: amendment.state,
438
- state_published_at: Time.current
439
- )
488
+ assign_state(amendment.state)
489
+ update!(state_published_at: Time.current)
440
490
  end
441
491
  end
442
492
 
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ class ProposalState < Proposals::ApplicationRecord
6
+ include Decidim::HasComponent
7
+ include Decidim::Traceable
8
+ include Decidim::Loggable
9
+
10
+ include Decidim::TranslatableResource
11
+ include Decidim::TranslatableAttributes
12
+
13
+ before_validation :generate_token, on: :create
14
+
15
+ translatable_fields :title
16
+
17
+ validates :token, presence: true, uniqueness: { scope: :component }
18
+
19
+ has_many :proposals,
20
+ class_name: "Decidim::Proposals::Proposal",
21
+ foreign_key: "decidim_proposals_proposal_state_id",
22
+ inverse_of: :proposal_state,
23
+ dependent: :restrict_with_error,
24
+ counter_cache: :proposals_count
25
+
26
+ def self.log_presenter_class_for(_log)
27
+ Decidim::Proposals::AdminLog::ProposalStatePresenter
28
+ end
29
+
30
+ def css_style
31
+ "background-color: #{bg_color}; color: #{text_color}; border-color: #{text_color};"
32
+ end
33
+
34
+ def self.colors
35
+ Decidim::Proposals.proposal_states_colors
36
+ end
37
+
38
+ protected
39
+
40
+ def generate_token
41
+ self.token = ensure_unique_token(translated_attribute(title).parameterize(separator: "_"))
42
+ end
43
+
44
+ def ensure_unique_token(token)
45
+ step = 0
46
+ code = token
47
+ loop do
48
+ break if Decidim::Proposals::ProposalState.where(component:, token: code).empty?
49
+
50
+ code = "#{token}_#{step}"
51
+ step += 1
52
+ end
53
+
54
+ code
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,3 @@
1
- <!-- markdownlint-disable-file single-h1 strong-style emphasis-style -->
2
-
3
1
  # Section title 1: grouping content
4
2
 
5
3
  Participatory texts relay on the parsing of Markdown texts to produce a structured document.
@@ -54,4 +52,4 @@ Ordered lists will be parsed too:
54
52
 
55
53
  A link to Decidim's web site uses [this format](https://decidim.org).
56
54
 
57
- ![Important image for Decidim](https://meta.decidim.org/assets/decidim/decidim-logo-1f39092fb3e41d23936dc8aeadd054e2119807dccf3c395de88637e4187f0a3f.svg)
55
+ ![Important image for Decidim](https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg)
@@ -0,0 +1,15 @@
1
+ <svg viewBox="0 0 434 160" preserveAspectRatio="xMidYMid slice"
2
+ id="ri-proposal-placeholder-card-g">
3
+ <rect opacity="0.106" fill="fill-current" width="434" height="160"></rect>
4
+ <g opacity="0.2" clip-path="url(#clip0_256_5589)">
5
+ <path
6
+ d="M331.544 115.752C331.543 114.67 331.753 113.598 332.163 112.597C332.574 111.596 333.175 110.687 333.935 109.921C334.694 109.155 335.596 108.547 336.588 108.132C337.581 107.718 338.645 107.504 339.719 107.504H346.262C346.696 107.504 347.111 107.678 347.417 107.987C347.724 108.295 347.896 108.714 347.897 109.15V142.096C347.897 142.312 347.855 142.526 347.773 142.726C347.69 142.926 347.57 143.107 347.418 143.26C347.266 143.413 347.086 143.535 346.888 143.617C346.69 143.7 346.477 143.743 346.262 143.743H339.719C337.552 143.742 335.473 142.874 333.94 141.331C332.407 139.787 331.545 137.693 331.544 135.509V115.752Z"
7
+ fill="fill-current"/>
8
+ <path
9
+ d="M365.884 96H352.801C351.898 96 351.166 96.7372 351.166 97.6466V142.096C351.166 143.005 351.898 143.743 352.801 143.743H365.884C366.787 143.743 367.519 143.005 367.519 142.096V97.6466C367.519 96.7372 366.787 96 365.884 96Z"
10
+ fill="fill-current"/>
11
+ <path
12
+ d="M388.776 135.509C388.775 137.138 388.295 138.73 387.397 140.084C386.498 141.438 385.221 142.493 383.727 143.116C382.233 143.739 380.59 143.902 379.004 143.584C377.418 143.266 375.962 142.481 374.819 141.329C373.676 140.178 372.897 138.71 372.582 137.113C372.267 135.515 372.429 133.859 373.048 132.354C373.667 130.85 374.715 129.564 376.059 128.659C377.403 127.754 378.984 127.271 380.601 127.271C382.769 127.272 384.848 128.14 386.381 129.685C387.915 131.23 388.776 133.325 388.776 135.509Z"
13
+ fill="fill-current"/>
14
+ </g>
15
+ </svg>
@@ -11,6 +11,7 @@ $(() => {
11
11
  }
12
12
  $addressInputField.on("geocoder-suggest-coordinates.decidim", () => $map.show());
13
13
 
14
+ const markerAddedAnnouncement = $addressInputField.data("screen-reader-announcement");
14
15
  let latFieldName = "latitude";
15
16
  let longFieldName = "longitude";
16
17
 
@@ -35,6 +36,7 @@ $(() => {
35
36
  longitude: coordinates[1],
36
37
  address: $addressInputField.val()
37
38
  });
39
+ window.Decidim.announceForScreenReader(markerAddedAnnouncement);
38
40
  });
39
41
  });
40
42
  }