decidim-proposals 0.29.1 → 0.30.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/proposals/highlighted_proposals_for_component/show.erb +12 -12
  3. data/app/cells/decidim/proposals/highlighted_proposals_for_component_cell.rb +1 -1
  4. data/app/cells/decidim/proposals/participatory_text_proposal_cell.rb +1 -1
  5. data/app/cells/decidim/proposals/proposal_g/show.erb +13 -0
  6. data/app/cells/decidim/proposals/proposal_g_cell.rb +34 -0
  7. data/app/cells/decidim/proposals/proposal_history_cell.rb +107 -0
  8. data/app/cells/decidim/proposals/proposal_l/show.erb +37 -0
  9. data/app/cells/decidim/proposals/proposal_l_cell.rb +26 -18
  10. data/app/cells/decidim/proposals/proposal_metadata_cell.rb +2 -2
  11. data/app/cells/decidim/proposals/proposal_vote/show.erb +75 -0
  12. data/app/cells/decidim/proposals/proposal_vote_cell.rb +43 -0
  13. data/app/commands/decidim/proposals/accept_coauthorship.rb +62 -0
  14. data/app/commands/decidim/proposals/admin/assign_proposals_to_valuator.rb +14 -0
  15. data/app/commands/decidim/proposals/admin/create_proposal.rb +6 -14
  16. data/app/commands/decidim/proposals/admin/create_proposal_note.rb +20 -11
  17. data/app/commands/decidim/proposals/admin/import_proposals.rb +0 -5
  18. data/app/commands/decidim/proposals/admin/merge_proposals.rb +2 -2
  19. data/app/commands/decidim/proposals/admin/proposal_notes_methods.rb +48 -0
  20. data/app/commands/decidim/proposals/admin/reply_proposal_note.rb +92 -0
  21. data/app/commands/decidim/proposals/admin/split_proposals.rb +2 -2
  22. data/app/commands/decidim/proposals/admin/update_proposal.rb +10 -16
  23. data/app/commands/decidim/proposals/admin/update_proposal_taxonomies.rb +34 -0
  24. data/app/commands/decidim/proposals/cancel_coauthorship.rb +32 -0
  25. data/app/commands/decidim/proposals/create_collaborative_draft.rb +1 -2
  26. data/app/commands/decidim/proposals/create_proposal.rb +1 -2
  27. data/app/commands/decidim/proposals/invite_coauthor.rb +45 -0
  28. data/app/commands/decidim/proposals/publish_collaborative_draft.rb +1 -2
  29. data/app/commands/decidim/proposals/reject_coauthorship.rb +54 -0
  30. data/app/commands/decidim/proposals/update_collaborative_draft.rb +1 -2
  31. data/app/commands/decidim/proposals/update_proposal.rb +1 -2
  32. data/app/controllers/concerns/decidim/proposals/admin/filterable.rb +6 -2
  33. data/app/controllers/concerns/decidim/proposals/admin/needs_interpolations.rb +40 -0
  34. data/app/controllers/decidim/proposals/admin/proposal_answers_controller.rb +50 -2
  35. data/app/controllers/decidim/proposals/admin/proposal_notes_controller.rb +18 -0
  36. data/app/controllers/decidim/proposals/admin/proposals_controller.rb +41 -85
  37. data/app/controllers/decidim/proposals/collaborative_drafts_controller.rb +2 -4
  38. data/app/controllers/decidim/proposals/invite_coauthors_controller.rb +87 -0
  39. data/app/controllers/decidim/proposals/proposals_controller.rb +11 -40
  40. data/app/events/decidim/proposals/accepted_coauthorship_event.rb +8 -0
  41. data/app/events/decidim/proposals/admin/proposal_assigned_to_valuator_event.rb +27 -0
  42. data/app/events/decidim/proposals/admin/proposal_note_created_event.rb +5 -0
  43. data/app/events/decidim/proposals/coauthor_accepted_invite_event.rb +49 -0
  44. data/app/events/decidim/proposals/coauthor_invited_event.rb +45 -0
  45. data/app/events/decidim/proposals/coauthor_rejected_invite_event.rb +8 -0
  46. data/app/events/decidim/proposals/rejected_coauthorship_event.rb +8 -0
  47. data/app/events/decidim/proposals/update_proposal_taxonomies_event.rb +9 -0
  48. data/app/forms/decidim/proposals/admin/proposal_answer_form.rb +4 -3
  49. data/app/forms/decidim/proposals/admin/proposal_base_form.rb +3 -31
  50. data/app/forms/decidim/proposals/admin/proposal_form.rb +12 -7
  51. data/app/forms/decidim/proposals/admin/proposals_import_form.rb +6 -14
  52. data/app/forms/decidim/proposals/admin/valuation_assignment_form.rb +4 -1
  53. data/app/forms/decidim/proposals/collaborative_draft_form.rb +0 -8
  54. data/app/forms/decidim/proposals/proposal_form.rb +5 -32
  55. data/app/helpers/decidim/proposals/admin/proposal_bulk_actions_helper.rb +26 -1
  56. data/app/helpers/decidim/proposals/admin/proposals_helper.rb +0 -1
  57. data/app/helpers/decidim/proposals/application_helper.rb +23 -14
  58. data/app/helpers/decidim/proposals/collaborative_draft_helper.rb +7 -7
  59. data/app/helpers/decidim/proposals/map_helper.rb +0 -18
  60. data/app/helpers/decidim/proposals/proposal_votes_helper.rb +15 -2
  61. data/app/jobs/decidim/proposals/admin/proposal_answer_job.rb +20 -0
  62. data/app/models/decidim/proposals/collaborative_draft.rb +10 -1
  63. data/app/models/decidim/proposals/proposal.rb +67 -10
  64. data/app/models/decidim/proposals/proposal_note.rb +11 -0
  65. data/app/models/decidim/proposals/proposal_state.rb +1 -1
  66. data/app/packs/entrypoints/decidim_proposals.js +1 -0
  67. data/app/packs/entrypoints/decidim_proposals_geocoding.js +2 -0
  68. data/app/packs/src/decidim/proposals/admin/proposals.js +16 -1
  69. data/app/packs/src/decidim/proposals/exit_handler.js +73 -0
  70. data/app/packs/stylesheets/decidim/proposals/proposals.scss +246 -5
  71. data/app/permissions/decidim/proposals/admin/permissions.rb +2 -5
  72. data/app/permissions/decidim/proposals/permissions.rb +46 -3
  73. data/app/presenters/decidim/proposals/admin_log/proposal_presenter.rb +1 -1
  74. data/app/presenters/decidim/proposals/proposal_presenter.rb +1 -1
  75. data/app/queries/decidim/proposals/filtered_proposals.rb +2 -2
  76. data/app/queries/decidim/proposals/metrics/accepted_proposals_metric_manage.rb +2 -2
  77. data/app/queries/decidim/proposals/metrics/endorsements_metric_manage.rb +10 -10
  78. data/app/queries/decidim/proposals/metrics/proposal_followers_metric_measure.rb +4 -4
  79. data/app/queries/decidim/proposals/metrics/proposal_participants_metric_measure.rb +6 -6
  80. data/app/queries/decidim/proposals/metrics/proposals_metric_manage.rb +6 -6
  81. data/app/queries/decidim/proposals/metrics/votes_metric_manage.rb +6 -6
  82. data/app/services/decidim/proposals/collaborative_draft_diff_renderer.rb +22 -0
  83. data/app/services/decidim/proposals/diff_renderer.rb +2 -0
  84. data/app/services/decidim/proposals/proposal_builder.rb +2 -2
  85. data/app/views/decidim/proposals/admin/proposal_notes/_form.html.erb +3 -3
  86. data/app/views/decidim/proposals/admin/proposal_notes/_proposal_note.html.erb +28 -0
  87. data/app/views/decidim/proposals/admin/proposal_notes/_proposal_note_reply.html.erb +9 -0
  88. data/app/views/decidim/proposals/admin/proposal_notes/_proposal_notes.html.erb +4 -28
  89. data/app/views/decidim/proposals/admin/proposal_states/_form.html.erb +1 -1
  90. data/app/views/decidim/proposals/admin/proposals/_actions.html.erb +21 -0
  91. data/app/views/decidim/proposals/admin/proposals/_bulk-actions.html.erb +3 -2
  92. data/app/views/decidim/proposals/admin/proposals/_form.html.erb +16 -23
  93. data/app/views/decidim/proposals/admin/proposals/_proposal-tr.html.erb +12 -28
  94. data/app/views/decidim/proposals/admin/proposals/_proposals-thead.html.erb +45 -0
  95. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_apply_answer_template.html.erb +22 -0
  96. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb +15 -11
  97. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_taxonomy_change.html.erb +23 -0
  98. data/app/views/decidim/proposals/admin/proposals/index.html.erb +17 -48
  99. data/app/views/decidim/proposals/admin/proposals/manage_trash.html.erb +18 -0
  100. data/app/views/decidim/proposals/admin/proposals/publish_answers.js.erb +1 -1
  101. data/app/views/decidim/proposals/admin/proposals/show.html.erb +10 -22
  102. data/app/views/decidim/proposals/admin/proposals/update_attribute.js.erb +1 -1
  103. data/app/views/decidim/proposals/admin/proposals_imports/new.html.erb +2 -5
  104. data/app/views/decidim/proposals/collaborative_drafts/_collaborative_actions.html.erb +9 -0
  105. data/app/views/decidim/proposals/collaborative_drafts/_collaborative_draft_aside.html.erb +0 -15
  106. data/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb +4 -6
  107. data/app/views/decidim/proposals/collaborative_drafts/index.html.erb +6 -2
  108. data/app/views/decidim/proposals/collaborative_drafts/show.html.erb +27 -11
  109. data/app/views/decidim/proposals/proposal_votes/update_buttons_and_counters.js.erb +29 -9
  110. data/app/views/decidim/proposals/proposals/_actions.html.erb +4 -7
  111. data/app/views/decidim/proposals/proposals/_edit_form_fields.html.erb +17 -22
  112. data/app/views/decidim/proposals/proposals/_exit_modal.html.erb +17 -0
  113. data/app/views/decidim/proposals/proposals/_notification_alert_box.html.erb +1 -0
  114. data/app/views/decidim/proposals/proposals/_proposal_actions.html.erb +19 -0
  115. data/app/views/decidim/proposals/proposals/_proposal_aside.html.erb +9 -32
  116. data/app/views/decidim/proposals/proposals/_proposal_voting_rules.html.erb +33 -0
  117. data/app/views/decidim/proposals/proposals/_remaining_votes_count.html.erb +2 -2
  118. data/app/views/decidim/proposals/proposals/_remaining_votes_notification.html.erb +12 -0
  119. data/app/views/decidim/proposals/proposals/_update_proposal_voting_rules.html.erb +6 -0
  120. data/app/views/decidim/proposals/proposals/_vote_button.html.erb +12 -8
  121. data/app/views/decidim/proposals/proposals/_votes_count.html.erb +2 -1
  122. data/app/views/decidim/proposals/proposals/_voting_rules.html.erb +1 -7
  123. data/app/views/decidim/proposals/proposals/index.html.erb +10 -18
  124. data/app/views/decidim/proposals/proposals/index.js.erb +12 -0
  125. data/app/views/decidim/proposals/proposals/participatory_texts/_proposal_vote_button.html.erb +3 -1
  126. data/app/views/decidim/proposals/proposals/show.html.erb +36 -16
  127. data/config/locales/ar.yml +19 -75
  128. data/config/locales/bg.yml +7 -91
  129. data/config/locales/bn-BD.yml +1 -0
  130. data/config/locales/bs-BA.yml +87 -0
  131. data/config/locales/ca.yml +213 -73
  132. data/config/locales/cs.yml +229 -75
  133. data/config/locales/de.yml +217 -78
  134. data/config/locales/el.yml +9 -85
  135. data/config/locales/en.yml +209 -69
  136. data/config/locales/es-MX.yml +220 -80
  137. data/config/locales/es-PY.yml +215 -75
  138. data/config/locales/es.yml +222 -82
  139. data/config/locales/eu.yml +293 -147
  140. data/config/locales/fi-plain.yml +222 -81
  141. data/config/locales/fi.yml +239 -98
  142. data/config/locales/fr-CA.yml +119 -90
  143. data/config/locales/fr.yml +118 -89
  144. data/config/locales/ga-IE.yml +0 -19
  145. data/config/locales/gl.yml +9 -47
  146. data/config/locales/he-IL.yml +7 -0
  147. data/config/locales/hu.yml +7 -68
  148. data/config/locales/id-ID.yml +9 -36
  149. data/config/locales/is-IS.yml +0 -21
  150. data/config/locales/it.yml +10 -56
  151. data/config/locales/ja.yml +164 -91
  152. data/config/locales/lt.yml +9 -86
  153. data/config/locales/lv.yml +8 -47
  154. data/config/locales/nl.yml +7 -54
  155. data/config/locales/no.yml +9 -46
  156. data/config/locales/pl.yml +7 -91
  157. data/config/locales/pt-BR.yml +7 -77
  158. data/config/locales/pt.yml +9 -57
  159. data/config/locales/ro-RO.yml +17 -63
  160. data/config/locales/ru.yml +0 -25
  161. data/config/locales/sk.yml +9 -48
  162. data/config/locales/sl.yml +0 -4
  163. data/config/locales/sr-CS.yml +0 -14
  164. data/config/locales/sv.yml +132 -88
  165. data/config/locales/tr-TR.yml +9 -54
  166. data/config/locales/uk.yml +0 -25
  167. data/config/locales/zh-CN.yml +9 -54
  168. data/config/locales/zh-TW.yml +9 -87
  169. data/db/migrate/20171220084719_add_published_at_to_proposals.rb +1 -1
  170. data/db/migrate/20181016132225_add_organization_as_author.rb +1 -1
  171. data/db/migrate/20200120215928_move_proposal_endorsements_to_core_endorsements.rb +1 -1
  172. data/db/migrate/20200827154156_add_commentable_counter_cache_to_proposals.rb +3 -3
  173. data/db/migrate/20210310102839_add_followable_counter_cache_to_proposals.rb +1 -1
  174. data/db/migrate/20240110203504_create_default_proposal_states.rb +4 -3
  175. data/db/migrate/20240404202756_add_valuation_assignments_count_to_decidim_proposals_proposals.rb +1 -1
  176. data/db/migrate/20240617091140_add_email_on_assigned_proposals_to_users.rb +7 -0
  177. data/db/migrate/20240617170052_add_parent_relation_to_decidim_proposal_notes.rb +7 -0
  178. data/db/migrate/20240828103755_add_deleted_at_to_decidim_proposals_proposals.rb +8 -0
  179. data/decidim-proposals.gemspec +2 -2
  180. data/lib/decidim/api/functions/proposal_finder_helper.rb +12 -0
  181. data/lib/decidim/api/functions/proposal_list_helper.rb +12 -0
  182. data/lib/decidim/api/proposal_type.rb +30 -25
  183. data/lib/decidim/api/proposals_type.rb +5 -22
  184. data/lib/decidim/proposals/admin_engine.rb +12 -3
  185. data/lib/decidim/proposals/admin_filter.rb +3 -6
  186. data/lib/decidim/proposals/component.rb +4 -5
  187. data/lib/decidim/proposals/download_your_data_proposal_serializer.rb +15 -0
  188. data/lib/decidim/proposals/engine.rb +5 -0
  189. data/lib/decidim/proposals/import/proposal_creator.rb +4 -4
  190. data/lib/decidim/proposals/proposal_serializer.rb +15 -29
  191. data/lib/decidim/proposals/seeds.rb +21 -17
  192. data/lib/decidim/proposals/test/factories.rb +8 -6
  193. data/lib/decidim/proposals/version.rb +1 -1
  194. data/lib/decidim/proposals.rb +4 -0
  195. metadata +69 -30
  196. data/app/commands/decidim/proposals/admin/update_proposal_category.rb +0 -70
  197. data/app/commands/decidim/proposals/admin/update_proposal_scope.rb +0 -75
  198. data/app/events/decidim/proposals/admin/update_proposal_category_event.rb +0 -11
  199. data/app/events/decidim/proposals/admin/update_proposal_scope_event.rb +0 -11
  200. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_recategorize.html.erb +0 -15
  201. data/app/views/decidim/proposals/admin/proposals/bulk_actions/_scope-change.html.erb +0 -21
  202. data/app/views/decidim/proposals/collaborative_drafts/_actions.html.erb +0 -7
@@ -8,14 +8,13 @@ module Decidim
8
8
  include Decidim::TranslatableAttributes
9
9
  include Decidim::AttachmentAttributes
10
10
  include Decidim::ApplicationHelper
11
+ include Decidim::HasTaxonomyFormAttributes
11
12
 
12
13
  mimic :proposal
13
14
 
14
15
  attribute :address, String
15
16
  attribute :latitude, Float
16
17
  attribute :longitude, Float
17
- attribute :category_id, Integer
18
- attribute :scope_id, Integer
19
18
  attribute :attachment, AttachmentForm
20
19
  attribute :position, Integer
21
20
  attribute :created_in_meeting, Boolean
@@ -25,46 +24,19 @@ module Decidim
25
24
  attachments_attribute :photos
26
25
 
27
26
  validates :address, geocoding: true, if: ->(form) { form.has_address? && !form.geocoded? }
28
- validates :category, presence: true, if: ->(form) { form.category_id.present? }
29
- validates :scope, presence: true, if: ->(form) { form.scope_id.present? }
30
- validates :scope_id, scope_belongs_to_component: true, if: ->(form) { form.scope_id.present? }
31
27
  validates :meeting_as_author, presence: true, if: ->(form) { form.created_in_meeting? }
32
28
 
33
29
  validate :notify_missing_attachment_if_errored
34
30
 
35
- delegate :categories, to: :current_component
36
-
37
31
  def map_model(model)
38
32
  body = translated_attribute(model.body)
39
33
  @suggested_hashtags = Decidim::ContentRenderers::HashtagRenderer.new(body).extra_hashtags.map(&:name).map(&:downcase)
40
-
41
- return unless model.categorization
42
-
43
- self.category_id = model.categorization.decidim_category_id
44
- self.scope_id = model.decidim_scope_id
45
34
  end
46
35
 
47
36
  alias component current_component
48
37
 
49
- # Finds the Category from the category_id.
50
- #
51
- # Returns a Decidim::Category
52
- def category
53
- @category ||= categories.find_by(id: category_id)
54
- end
55
-
56
- # Finds the Scope from the given decidim_scope_id, uses participatory space scope if missing.
57
- #
58
- # Returns a Decidim::Scope
59
- def scope
60
- @scope ||= @attributes["scope_id"].value ? current_component.scopes.find_by(id: @attributes["scope_id"].value) : current_component.scope
61
- end
62
-
63
- # Scope identifier
64
- #
65
- # Returns the scope identifier related to the proposal
66
- def scope_id
67
- super || scope&.id
38
+ def participatory_space_manifest
39
+ @participatory_space_manifest ||= current_component.participatory_space.manifest.name
68
40
  end
69
41
 
70
42
  def geocoding_enabled?
@@ -6,27 +6,32 @@ module Decidim
6
6
  # A form object to be used when admin users want to create a proposal.
7
7
  class ProposalForm < Decidim::Proposals::Admin::ProposalBaseForm
8
8
  include Decidim::HasUploadValidations
9
+ include Decidim::AttachmentAttributes
9
10
 
10
11
  translatable_attribute :title, String do |field, _locale|
11
12
  validates field, length: { in: 15..150 }, if: proc { |resource| resource.send(field).present? }
12
13
  end
13
- translatable_attribute :body, String
14
+ translatable_attribute :body, Decidim::Attributes::RichText
15
+ attribute :attachment, AttachmentForm
16
+
17
+ attachments_attribute :documents
14
18
 
15
19
  validates :title, :body, translatable_presence: true
20
+ validates :title, :body, translated_etiquette: true
16
21
 
17
22
  validate :notify_missing_attachment_if_errored
18
23
 
19
24
  def map_model(model)
20
- super(model)
25
+ super
21
26
  presenter = ProposalPresenter.new(model)
22
27
 
23
28
  self.title = presenter.title(all_locales: title.is_a?(Hash))
24
29
  self.body = presenter.editor_body(all_locales: body.is_a?(Hash))
25
- self.attachment = if model.documents.first.present?
26
- { file: model.documents.first.file, title: translated_attribute(model.documents.first.title) }
27
- else
28
- {}
29
- end
30
+ self.documents = model.attachments
31
+ end
32
+
33
+ def notify_missing_attachment_if_errored
34
+ errors.add(:add_documents, :needs_to_be_reattached) if errors.any? && add_documents.present?
30
35
  end
31
36
  end
32
37
  end
@@ -6,6 +6,7 @@ module Decidim
6
6
  # A form object to be used when admin users want to import a collection of proposals
7
7
  # from another component.
8
8
  class ProposalsImportForm < Decidim::Form
9
+ include TranslatableAttributes
9
10
  mimic :proposals_import
10
11
 
11
12
  attribute :origin_component_id, Integer
@@ -13,31 +14,22 @@ module Decidim
13
14
  attribute :keep_answers, Boolean
14
15
  attribute :keep_authors, Boolean
15
16
  attribute :states, Array
16
- attribute :scope_ids, Array
17
17
 
18
18
  validates :origin_component_id, :origin_component, :states, :current_component, presence: true
19
19
  validates :import_proposals, allow_nil: false, acceptance: true
20
20
  validate :valid_states
21
21
 
22
- VALID_STATES = %w(accepted not_answered evaluating rejected).freeze
23
-
24
22
  def states_collection
25
- VALID_STATES.map do |state|
26
- OpenStruct.new(
27
- name: I18n.t(state, scope: "decidim.proposals.answers"),
28
- value: state
29
- )
30
- end
23
+ @states_collection ||= ProposalState.where(component: current_component) + [ProposalState.new(token: "not_answered",
24
+ title: I18n.t(
25
+ :not_answered, scope: "decidim.proposals.answers"
26
+ ))]
31
27
  end
32
28
 
33
29
  def states
34
30
  super.compact_blank
35
31
  end
36
32
 
37
- def scopes
38
- Decidim::Scope.where(organization: current_organization, id: scope_ids)
39
- end
40
-
41
33
  def origin_component
42
34
  @origin_component ||= origin_components.find_by(id: origin_component_id)
43
35
  end
@@ -56,7 +48,7 @@ module Decidim
56
48
 
57
49
  def valid_states
58
50
  return if states.all? do |state|
59
- VALID_STATES.include?(state)
51
+ states_collection.pluck(:token).include?(state)
60
52
  end
61
53
 
62
54
  errors.add(:states, :invalid)
@@ -16,7 +16,10 @@ module Decidim
16
16
  end
17
17
 
18
18
  def valuator_roles
19
- @valuator_roles ||= current_component.participatory_space.user_roles(:valuator).where(id: valuator_role_ids)
19
+ @valuator_roles ||= current_component.participatory_space
20
+ .user_roles(:valuator)
21
+ .order_by_name
22
+ .where(id: valuator_role_ids)
20
23
  end
21
24
 
22
25
  def same_participatory_space
@@ -4,14 +4,6 @@ module Decidim
4
4
  module Proposals
5
5
  # A form object to be used when public users want to create a Collaborative Draft.
6
6
  class CollaborativeDraftForm < Decidim::Proposals::ProposalForm
7
- def map_model(model)
8
- super
9
-
10
- return unless model.categorization
11
-
12
- self.category_id = model.categorization.decidim_category_id
13
- end
14
-
15
7
  def user_group
16
8
  @user_group ||= Decidim::UserGroup.find user_group_id if user_group_id.present?
17
9
  end
@@ -7,6 +7,7 @@ module Decidim
7
7
  include Decidim::TranslatableAttributes
8
8
  include Decidim::AttachmentAttributes
9
9
  include Decidim::HasUploadValidations
10
+ include Decidim::HasTaxonomyFormAttributes
10
11
 
11
12
  mimic :proposal
12
13
 
@@ -17,29 +18,24 @@ module Decidim
17
18
  attribute :address, String
18
19
  attribute :latitude, Float
19
20
  attribute :longitude, Float
20
- attribute :category_id, Integer
21
- attribute :scope_id, Integer
22
21
  attribute :attachment, AttachmentForm
23
22
  attribute :suggested_hashtags, Array[String]
24
23
 
25
24
  attachments_attribute :documents
26
25
 
27
- validates :title, :body, presence: true, etiquette: true
26
+ validates :title, :body, presence: true
27
+ validates :title, :body, etiquette: true
28
28
  validates :title, length: { in: 15..150 }
29
29
  validates :body, proposal_length: {
30
30
  minimum: 15,
31
31
  maximum: ->(record) { record.component.settings.proposal_length }
32
32
  }
33
33
  validates :address, geocoding: true, if: ->(form) { form.has_address? && !form.geocoded? }
34
- validates :category, presence: true, if: ->(form) { form.category_id.present? }
35
- validates :scope, presence: true, if: ->(form) { form.scope_id.present? }
36
- validates :scope_id, scope_belongs_to_component: true, if: ->(form) { form.scope_id.present? }
37
34
 
38
35
  validate :body_is_not_bare_template
39
36
  validate :notify_missing_attachment_if_errored
40
37
 
41
38
  alias component current_component
42
- delegate :categories, to: :current_component
43
39
 
44
40
  def map_model(model)
45
41
  self.title = translated_attribute(model.title)
@@ -50,34 +46,11 @@ module Decidim
50
46
  self.body = presenter.editor_body(all_locales: body.is_a?(Hash))
51
47
 
52
48
  self.user_group_id = model.user_groups.first&.id
53
- self.category_id = model.categorization.decidim_category_id if model.categorization
54
-
55
- # The scope attribute is with different key (decidim_scope_id), so it
56
- # has to be manually mapped.
57
- self.scope_id = model.scope.id if model.scope
58
-
59
49
  self.documents = model.attachments
60
50
  end
61
51
 
62
- # Finds the Category from the category_id.
63
- #
64
- # Returns a Decidim::Category
65
- def category
66
- @category ||= categories.find_by(id: category_id)
67
- end
68
-
69
- # Finds the Scope from the given scope_id, uses participatory space scope if missing.
70
- #
71
- # Returns a Decidim::Scope
72
- def scope
73
- @scope ||= @attributes["scope_id"].value ? current_component.scopes.find_by(id: @attributes["scope_id"].value) : current_component.scope
74
- end
75
-
76
- # Scope identifier
77
- #
78
- # Returns the scope identifier related to the proposal
79
- def scope_id
80
- super || scope&.id
52
+ def participatory_space_manifest
53
+ @participatory_space_manifest ||= current_component.participatory_space.manifest.name
81
54
  end
82
55
 
83
56
  def geocoding_enabled?
@@ -8,9 +8,34 @@ module Decidim
8
8
  Decidim::Proposals::Proposal.find(id)
9
9
  end
10
10
 
11
+ # Public: Generates a select field with the templates of the given component.
12
+ #
13
+ # component - A component instance.
14
+ # prompt - An i18n string to show as prompt
15
+ #
16
+ # Returns a String.
17
+ def bulk_templates_select(component, prompt, id: nil)
18
+ options_for_select = find_templates_for_select(component)
19
+ select(:template, :template_id, options_for_select, prompt:, id:)
20
+ end
21
+
22
+ def find_templates_for_select(component)
23
+ return [] unless Decidim.module_installed? :templates
24
+ return @templates_for_select if @templates_for_select
25
+
26
+ templates = Decidim::Templates::Template.where(
27
+ target: :proposal_answer,
28
+ templatable: component
29
+ ).order(:templatable_id)
30
+
31
+ @templates_for_select = templates.map do |template|
32
+ [translated_attribute(template.name), template.id]
33
+ end
34
+ end
35
+
11
36
  # find the valuators for the current space.
12
37
  def find_valuators_for_select(participatory_space, current_user)
13
- valuator_roles = participatory_space.user_roles(:valuator)
38
+ valuator_roles = participatory_space.user_roles(:valuator).order_by_name
14
39
  valuators = Decidim::User.where(id: valuator_roles.pluck(:decidim_user_id)).to_a
15
40
 
16
41
  filtered_valuator_roles = valuator_roles.filter do |role|
@@ -7,7 +7,6 @@ module Decidim
7
7
  # in order to use them in select forms for Proposals.
8
8
  #
9
9
  module ProposalsHelper
10
- include Decidim::Admin::ResourceScopeHelper
11
10
  include Decidim::TranslatableAttributes
12
11
 
13
12
  def available_states
@@ -144,8 +144,16 @@ module Decidim
144
144
  ).count
145
145
  end
146
146
 
147
+ def layout_item_classes
148
+ if show_voting_rules?
149
+ "layout-item lg:pt-4"
150
+ else
151
+ "layout-item"
152
+ end
153
+ end
154
+
147
155
  def show_voting_rules?
148
- return false if !votes_enabled? || current_settings.votes_blocked?
156
+ return false if !votes_enabled? || votes_blocked?
149
157
 
150
158
  return true if vote_limit_enabled?
151
159
  return true if threshold_per_proposal_enabled?
@@ -206,41 +214,38 @@ module Decidim
206
214
  end
207
215
 
208
216
  # rubocop:disable Metrics/CyclomaticComplexity
209
- # rubocop:disable Metrics/PerceivedComplexity
210
217
  def filter_sections
211
218
  @filter_sections ||= begin
212
219
  items = []
213
220
  if component_settings.proposal_answering_enabled && current_settings.proposal_answering_enabled
214
- items.append(method: :with_any_state, collection: filter_proposals_state_values, label_scope: "decidim.proposals.proposals.filters", id: "state")
221
+ items.append(method: :with_any_state, collection: filter_proposals_state_values, label: t("decidim.proposals.proposals.filters.state"), id: "state")
215
222
  end
216
- if current_component.has_subscopes?
217
- items.append(method: :with_any_scope, collection: filter_scopes_values, label_scope: "decidim.proposals.proposals.filters", id: "scope")
218
- end
219
- if current_component.categories.any?
220
- items.append(method: :with_any_category, collection: filter_categories_values, label_scope: "decidim.proposals.proposals.filters", id: "category")
223
+ current_component.available_taxonomy_filters.each do |taxonomy_filter|
224
+ items.append(method: "with_any_taxonomies[#{taxonomy_filter.root_taxonomy_id}]",
225
+ collection: filter_taxonomy_values_for(taxonomy_filter),
226
+ label: decidim_sanitize_translated(taxonomy_filter.name),
227
+ id: "taxonomy-#{taxonomy_filter.root_taxonomy_id}")
221
228
  end
222
229
  if component_settings.official_proposals_enabled
223
- items.append(method: :with_any_origin, collection: filter_origin_values, label_scope: "decidim.proposals.proposals.filters", id: "origin")
230
+ items.append(method: :with_any_origin, collection: filter_origin_values, label: t("decidim.proposals.proposals.filters.origin"), id: "origin")
224
231
  end
225
232
  if current_user
226
- items.append(method: :activity, collection: activity_filter_values, label_scope: "decidim.proposals.proposals.filters", id: "activity", type: :radio_buttons)
233
+ items.append(method: :activity, collection: activity_filter_values, label: t("decidim.proposals.proposals.filters.activity"), id: "activity", type: :radio_buttons)
227
234
  end
228
235
  if @proposals.only_emendations.any?
229
- items.append(method: :type, collection: filter_type_values, label_scope: "decidim.proposals.proposals.filters", id: "amendment_type", type: :radio_buttons)
236
+ items.append(method: :type, collection: filter_type_values, label: t("decidim.proposals.proposals.filters.amendment_type"), id: "amendment_type", type: :radio_buttons)
230
237
  end
231
238
  if linked_classes_for(Decidim::Proposals::Proposal).any?
232
239
  items.append(
233
240
  method: :related_to,
234
241
  collection: linked_classes_filter_values_for(Decidim::Proposals::Proposal),
235
- label_scope: "decidim.proposals.proposals.filters",
242
+ label: t("decidim.proposals.proposals.filters.related_to"),
236
243
  id: "related_to",
237
244
  type: :radio_buttons
238
245
  )
239
246
  end
240
247
  end
241
- # rubocop:enable Metrics/PerceivedComplexity
242
248
  # rubocop:enable Metrics/CyclomaticComplexity
243
-
244
249
  items.reject { |item| item[:collection].blank? }
245
250
  end
246
251
 
@@ -248,6 +253,10 @@ module Decidim
248
253
  i18n_key = controller_name == "collaborative_drafts" ? "decidim.proposals.collaborative_drafts.name" : "decidim.components.proposals.name"
249
254
  (defined?(current_component) && translated_attribute(current_component&.name).presence) || t(i18n_key)
250
255
  end
256
+
257
+ def templates_available?
258
+ Decidim.module_installed?(:templates) && defined?(Decidim::Templates::Template) && Decidim::Templates::Template.exists?(templatable: current_component)
259
+ end
251
260
  end
252
261
  end
253
262
  end
@@ -43,20 +43,20 @@ module Decidim
43
43
  items = [{
44
44
  method: :with_any_state,
45
45
  collection: filter_collaborative_drafts_state_values,
46
- label_scope: "decidim.proposals.collaborative_drafts.filters",
46
+ label: t("decidim.proposals.collaborative_drafts.filters.state"),
47
47
  id: "state"
48
48
  }]
49
- if current_component.has_subscopes?
50
- items.append(method: :with_any_scope, collection: filter_scopes_values, label_scope: "decidim.proposals.collaborative_drafts.filters", id: "scope")
51
- end
52
- if current_component.categories.any?
53
- items.append(method: :with_any_category, collection: filter_categories_values, label_scope: "decidim.proposals.collaborative_drafts.filters", id: "category")
49
+ current_component.available_taxonomy_filters.each do |taxonomy_filter|
50
+ items.append(method: "with_any_taxonomies[#{taxonomy_filter.root_taxonomy_id}]",
51
+ collection: filter_taxonomy_values_for(taxonomy_filter),
52
+ label: decidim_sanitize_translated(taxonomy_filter.name),
53
+ id: "taxonomy-#{taxonomy_filter.root_taxonomy_id}")
54
54
  end
55
55
  if linked_classes_for(Decidim::Proposals::CollaborativeDraft).any?
56
56
  items.append(
57
57
  method: :related_to,
58
58
  collection: linked_classes_filter_values_for(Decidim::Proposals::CollaborativeDraft),
59
- label_scope: "decidim.proposals.collaborative_drafts.filters",
59
+ label: t("decidim.proposals.collaborative_drafts.filters.related_to"),
60
60
  id: "related_to",
61
61
  type: :radio_buttons
62
62
  )
@@ -5,24 +5,6 @@ module Decidim
5
5
  # This helper include some methods for rendering proposals dynamic maps.
6
6
  module MapHelper
7
7
  include Decidim::ApplicationHelper
8
- # Serialize a collection of geocoded proposals to be used by the dynamic map component
9
- #
10
- # geocoded_proposals - A collection of geocoded proposals
11
- def proposals_data_for_map(geocoded_proposals)
12
- geocoded_proposals.map do |proposal|
13
- proposal_data_for_map(proposal)
14
- end
15
- end
16
-
17
- def proposal_data_for_map(proposal)
18
- proposal
19
- .slice(:latitude, :longitude, :address)
20
- .merge(
21
- title: decidim_html_escape(present(proposal).title),
22
- link: proposal_path(proposal),
23
- items: cell("decidim/proposals/proposal_metadata", proposal).send(:proposal_items_for_map).to_json
24
- )
25
- end
26
8
 
27
9
  def proposal_preview_data_for_map(proposal)
28
10
  {
@@ -47,14 +47,14 @@ module Decidim
47
47
  #
48
48
  # Returns true if enabled, false otherwise.
49
49
  def votes_enabled?
50
- current_settings.votes_enabled
50
+ current_settings.respond_to?(:votes_enabled) && current_settings.votes_enabled
51
51
  end
52
52
 
53
53
  # Public: Checks if voting is blocked in this step.
54
54
  #
55
55
  # Returns true if blocked, false otherwise.
56
56
  def votes_blocked?
57
- current_settings.votes_blocked
57
+ current_settings.respond_to?(:votes_blocked) && current_settings.votes_blocked
58
58
  end
59
59
 
60
60
  # Public: Checks if the current user is allowed to vote in this step.
@@ -76,6 +76,19 @@ module Decidim
76
76
  votes_count = ProposalVote.where(author: user, proposal: proposals).size
77
77
  component_settings.vote_limit - votes_count
78
78
  end
79
+
80
+ # Return the remaining minimum votes for a user if the current component has a vote limit
81
+ #
82
+ # user - A User object
83
+ #
84
+ # Returns a number with the remaining minimum votes for that user
85
+ def remaining_minimum_votes_count_for(user)
86
+ return 0 unless vote_limit_enabled?
87
+
88
+ votes_count = Decidim::Proposals::ProposalVote.joins(:proposal).where(decidim_proposals_proposals: { decidim_component_id: current_component.id }).where(author: user).count
89
+
90
+ component_settings.minimum_votes_per_user - votes_count
91
+ end
79
92
  end
80
93
  end
81
94
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ module Admin
6
+ class ProposalAnswerJob < ApplicationJob
7
+ queue_as :default
8
+
9
+ def perform(proposal, attributes, context)
10
+ answer_form = ProposalAnswerForm.from_params(attributes).with_context(**context)
11
+
12
+ Admin::AnswerProposal.call(answer_form, proposal) do
13
+ on(:ok) { Rails.logger.info "Proposal #{proposal.id} answered successfully." }
14
+ on(:invalid) { Rails.logger.error "Proposal ID #{proposal.id} could not be updated. Errors: #{answer_form.errors.full_messages}" }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -5,6 +5,7 @@ module Decidim
5
5
  class CollaborativeDraft < Proposals::ApplicationRecord
6
6
  include Decidim::Resourceable
7
7
  include Decidim::Coauthorable
8
+ include Decidim::Taxonomizable
8
9
  include Decidim::HasComponent
9
10
  include Decidim::ScopableResource
10
11
  include Decidim::HasReference
@@ -64,7 +65,15 @@ module Decidim
64
65
  ransacker_text_multi :search_text, [:title, :body]
65
66
 
66
67
  def self.ransackable_scopes(_auth_object = nil)
67
- [:with_any_state, :related_to, :with_any_scope, :with_any_category]
68
+ [:with_any_state, :related_to, :with_any_taxonomies]
69
+ end
70
+
71
+ def self.ransackable_attributes(_auth_object = nil)
72
+ %w(id_string search_text title body)
73
+ end
74
+
75
+ def self.ransackable_associations(_auth_object = nil)
76
+ %w(taxonomies)
68
77
  end
69
78
  end
70
79
  end
@@ -7,6 +7,7 @@ module Decidim
7
7
  include Decidim::Resourceable
8
8
  include Decidim::Coauthorable
9
9
  include Decidim::HasComponent
10
+ include Decidim::Taxonomizable
10
11
  include Decidim::ScopableResource
11
12
  include Decidim::HasReference
12
13
  include Decidim::HasCategory
@@ -28,6 +29,7 @@ module Decidim
28
29
  include Decidim::TranslatableResource
29
30
  include Decidim::TranslatableAttributes
30
31
  include Decidim::FilterableResource
32
+ include Decidim::SoftDeletable
31
33
 
32
34
  def assign_state(token)
33
35
  proposal_state = Decidim::Proposals::ProposalState.where(component:, token:).first
@@ -402,18 +404,22 @@ module Decidim
402
404
  where(query, value:)
403
405
  end
404
406
 
405
- def self.ransackable_scopes(auth_object = nil)
406
- base = [:with_any_origin, :with_any_state, :state_eq, :voted_by, :coauthored_by, :related_to, :with_any_scope, :with_any_category]
407
- return base unless auth_object&.admin?
408
-
409
- # Add extra scopes for admins for the admin panel searches
410
- base + [:valuator_role_ids_has]
407
+ def self.ransackable_scopes(_auth_object = nil)
408
+ [:with_any_origin, :with_any_state, :state_eq, :voted_by, :coauthored_by, :related_to, :with_any_taxonomies, :valuator_role_ids_has]
411
409
  end
412
410
 
413
411
  # Create i18n ransackers for :title and :body.
414
412
  # Create the :search_text ransacker alias for searching from both of these.
415
413
  ransacker_i18n_multi :search_text, [:title, :body]
416
414
 
415
+ def self.ransackable_attributes(_auth_object = nil)
416
+ %w(id_string search_text title body is_emendation comments_count proposal_votes_count published_at proposal_notes_count)
417
+ end
418
+
419
+ def self.ransackable_associations(_auth_object = nil)
420
+ %w(taxonomies proposal_state)
421
+ end
422
+
417
423
  ransacker :state_published do
418
424
  Arel.sql("CASE
419
425
  WHEN EXISTS (
@@ -459,7 +465,7 @@ module Decidim
459
465
  end
460
466
 
461
467
  def self.export_serializer
462
- Decidim::Proposals::ProposalSerializer
468
+ Decidim::Proposals::DownloadYourDataProposalSerializer
463
469
  end
464
470
 
465
471
  def self.download_your_data_images(user)
@@ -476,8 +482,18 @@ module Decidim
476
482
  return true if draft?
477
483
  return true if component.settings.proposal_edit_time == "infinite"
478
484
 
479
- limit = updated_at + component.settings.proposal_edit_before_minutes.minutes
480
- Time.current < limit
485
+ time_value, time_unit = component.settings.edit_time
486
+
487
+ limit_time = case time_unit
488
+ when "minutes"
489
+ updated_at + time_value.minutes
490
+ when "hours"
491
+ updated_at + time_value.hours
492
+ else
493
+ updated_at + time_value.days
494
+ end
495
+
496
+ Time.current < limit_time
481
497
  end
482
498
 
483
499
  def process_amendment_state_change!
@@ -490,10 +506,51 @@ module Decidim
490
506
  end
491
507
  end
492
508
 
509
+ def user_has_actions?(user)
510
+ return false if authors.include?(user)
511
+ return false if user&.blocked?
512
+ return false if user&.deleted?
513
+ return false unless user&.confirmed?
514
+
515
+ true
516
+ end
517
+
518
+ def actions_for_comment(comment, current_user)
519
+ return if comment.commentable != self
520
+ return unless authors.include?(current_user)
521
+ return unless user_has_actions?(comment.author)
522
+
523
+ if coauthor_invitations_for(comment.author).any?
524
+ [
525
+ {
526
+ label: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation"),
527
+ url: EngineRouter.main_proxy(component).cancel_proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id),
528
+ icon: "user-forbid-line",
529
+ method: :delete,
530
+ data: { confirm: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation_confirm") }
531
+ }
532
+ ]
533
+ else
534
+ [
535
+ {
536
+ label: I18n.t("decidim.proposals.actions.mark_as_coauthor"),
537
+ url: EngineRouter.main_proxy(component).proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id),
538
+ icon: "user-add-line",
539
+ method: :post,
540
+ data: { confirm: I18n.t("decidim.proposals.actions.mark_as_coauthor_confirm") }
541
+ }
542
+ ]
543
+ end
544
+ end
545
+
546
+ def coauthor_invitations_for(user)
547
+ Decidim::Notification.where(event_class: "Decidim::Proposals::CoauthorInvitedEvent", resource: self, user:)
548
+ end
549
+
493
550
  private
494
551
 
495
552
  def copied_from_other_component?
496
- linked_resources(:proposals, "copied_from_component").any?
553
+ linked_resources(:proposals, %w(splitted_from_component merged_from_component copied_from_component)).any?
497
554
  end
498
555
  end
499
556
  end