Class: Decidim::Proposals::Proposal

Overview

The data store for a Proposal in the Decidim::Proposals component.

Constant Summary

Constants included from ParticipatoryTextSection

Decidim::Proposals::ParticipatoryTextSection::LEVELS

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Decidim::Publicable

#previously_published?, #publish!, #published?, #unpublish!

Methods included from TranslatableAttributes

#attachment?, #default_locale?

Methods included from Amendable

#add_author, #amendable?, #amendable_fields, #amendable_form, #amendment, #emendation?, #linked_promoted_resource, #notifiable_identities, #visible_amendments_for, #visible_emendations_for

Methods included from Fingerprintable

#fingerprint

Methods included from Searchable

searchable_resources, searchable_resources_by_type, searchable_resources_of_type_comment, searchable_resources_of_type_component, searchable_resources_of_type_participant, searchable_resources_of_type_participatory_space

Methods included from Followable

#followers

Methods included from HasAttachments

#attachment_context

Class Method Details

.download_your_data_images(user) ⇒ Object



456
457
458
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 456

def self.download_your_data_images(user)
  user_collection(user).map { |p| p.attachments.collect(&:file) }
end

.evaluator_role_ids_has(value) ⇒ Object

method to filter by assigned evaluator role ID



380
381
382
383
384
385
386
387
388
389
390
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 380

def self.evaluator_role_ids_has(value)
  query = "  :value = any(\n    (SELECT decidim_proposals_evaluation_assignments.evaluator_role_id\n    FROM decidim_proposals_evaluation_assignments\n    WHERE decidim_proposals_evaluation_assignments.decidim_proposal_id = decidim_proposals_proposals.id\n    )\n  )\n  SQL\n  where(query, value:)\nend\n".squish

.export_serializerObject



452
453
454
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 452

def self.export_serializer
  Decidim::Proposals::DownloadYourDataProposalSerializer
end

.log_presenter_class_for(_log) ⇒ Object



180
181
182
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 180

def self.log_presenter_class_for(_log)
  Decidim::Proposals::AdminLog::ProposalPresenter
end

.newsletter_participant_ids(component) ⇒ Object



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 203

def self.newsletter_participant_ids(component)
  proposals = retrieve_proposals_for(component).uniq

  coauthors_recipients_ids = proposals.map { |p| p.notifiable_identities.pluck(:id) }.flatten.compact.uniq

  participants_has_voted_ids = Decidim::Proposals::ProposalVote.joins(:proposal).where(proposal: proposals).joins(:author).map(&:decidim_author_id).flatten.compact.uniq

  likes_participants_ids = Decidim::Like.where(resource: proposals)
                                        .where(decidim_author_type: "Decidim::UserBaseEntity")
                                        .pluck(:decidim_author_id).to_a.compact.uniq

  commentators_ids = Decidim::Comments::Comment.user_commentators_ids_in(proposals)

  (likes_participants_ids + participants_has_voted_ids + coauthors_recipients_ids + commentators_ids).flatten.compact.uniq
end

.ransack(params = {}, options = {}) ⇒ Object



375
376
377
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 375

def self.ransack(params = {}, options = {})
  ProposalSearch.new(self, params, options)
end

.ransackable_associations(_auth_object = nil) ⇒ Object



404
405
406
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 404

def self.ransackable_associations(_auth_object = nil)
  %w(taxonomies proposal_state)
end

.ransackable_attributes(_auth_object = nil) ⇒ Object



400
401
402
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 400

def self.ransackable_attributes(_auth_object = nil)
  %w(id_string search_text title body is_emendation comments_count proposal_votes_count published_at proposal_notes_count)
end

.ransackable_scopes(_auth_object = nil) ⇒ Object



392
393
394
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 392

def self.ransackable_scopes(_auth_object = nil)
  [:with_any_origin, :with_any_state, :state_eq, :voted_by, :coauthored_by, :related_to, :with_any_taxonomies, :evaluator_role_ids_has]
end

.retrieve_proposals_for(component) ⇒ Object



194
195
196
197
198
199
200
201
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 194

def self.retrieve_proposals_for(component)
  Decidim::Proposals::Proposal.where(component:).joins(:coauthorships)
                              .includes(:votes, :likes)
                              .where(decidim_coauthorships: { decidim_author_type: "Decidim::UserBaseEntity" })
                              .not_hidden
                              .published
                              .not_withdrawn
end

.sort_by_translated_title_ascObject



421
422
423
424
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 421

def self.sort_by_translated_title_asc
  field = Arel::Nodes::InfixOperation.new("->>", arel_table[:title], Arel::Nodes.build_quoted(I18n.locale))
  order(Arel::Nodes::InfixOperation.new("", field, Arel.sql("ASC")))
end

.sort_by_translated_title_descObject



426
427
428
429
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 426

def self.sort_by_translated_title_desc
  field = Arel::Nodes::InfixOperation.new("->>", arel_table[:title], Arel::Nodes.build_quoted(I18n.locale))
  order(Arel::Nodes::InfixOperation.new("", field, Arel.sql("DESC")))
end

.user_collection(author) ⇒ Object

Returns a collection scoped by an author. Overrides this method in DownloadYourData to support Coauthorable.



186
187
188
189
190
191
192
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 186

def self.user_collection(author)
  return unless author.is_a?(Decidim::User)

  joins(:coauthorships)
    .where(decidim_coauthorships: { coauthorable_type: name })
    .where("decidim_coauthorships.decidim_author_id = ? AND decidim_coauthorships.decidim_author_type = ? ", author.id, author.class.base_class.name)
end

.with_evaluation_assigned_to(user, space) ⇒ Object



161
162
163
164
165
166
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 161

def self.with_evaluation_assigned_to(user, space)
  evaluator_roles = space.user_roles(:evaluator).where(user:)

  includes(:evaluation_assignments)
    .where(decidim_proposals_evaluation_assignments: { evaluator_role_id: evaluator_roles })
end

Instance Method Details

#accepted?Boolean

Public: Checks if the organization has accepted a proposal.

Returns Boolean.



278
279
280
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 278

def accepted?
  state == "accepted"
end

#actions_for_comment(comment, current_user) ⇒ Object



503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 503

def actions_for_comment(comment, current_user)
  return if comment.commentable != self
  return unless authors.include?(current_user)
  return unless user_has_actions?(comment.author)

  if coauthor_invitations_for(comment.author).any?
    [
      {
        label: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation"),
        url: EngineRouter.main_proxy(component).cancel_proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id),
        icon: "user-forbid-line",
        method: :delete,
        data: { confirm: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation_confirm") }
      }
    ]
  else
    [
      {
        label: I18n.t("decidim.proposals.actions.mark_as_coauthor"),
        url: EngineRouter.main_proxy(component).proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id),
        icon: "user-add-line",
        method: :post,
        data: { confirm: I18n.t("decidim.proposals.actions.mark_as_coauthor_confirm") }
      }
    ]
  end
end

#allow_resource_permissions?Boolean

Public: Overrides the allow_resource_permissions? Resourceable concern method.



461
462
463
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 461

def allow_resource_permissions?
  component.settings.resources_permissions_enabled
end

#answered?Boolean

Public: Checks if the organization has given an answer for the proposal.

Returns Boolean.



264
265
266
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 264

def answered?
  answered_at.present?
end

#assign_state(token) ⇒ Object



35
36
37
38
39
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 35

def assign_state(token)
  proposal_state = Decidim::Proposals::ProposalState.where(component:, token:).first

  self.proposal_state = proposal_state
end

#can_accumulate_votes_beyond_thresholdObject

Public: Can accumulate more votes than maximum for this proposal.

Returns true if can accumulate, false otherwise



345
346
347
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 345

def can_accumulate_votes_beyond_threshold
  component.settings.can_accumulate_votes_beyond_threshold
end

#coauthor_invitations_for(user) ⇒ Object



531
532
533
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 531

def coauthor_invitations_for(user)
  Decidim::Notification.where(event_class: "Decidim::Proposals::CoauthorInvitedEvent", resource: self, user:)
end

#draft?Boolean

Public: Whether the proposal is a draft or not.



371
372
373
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 371

def draft?
  published_at.nil?
end

#editable_by?(user) ⇒ Boolean

Checks whether the user can edit the given proposal.

user - the user to check for authorship



352
353
354
355
356
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 352

def editable_by?(user)
  return true if draft? && created_by?(user)

  !published_state? && within_edit_time_limit? && !copied_from_other_component? && created_by?(user)
end

#evaluating?Boolean

Public: Checks if the organization has marked the proposal as evaluating it.

Returns Boolean.



292
293
294
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 292

def evaluating?
  state == "evaluating"
end

#internal_stateObject

Public: Returns the internal state of the proposal.

Returns Boolean.



248
249
250
251
252
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 248

def internal_state
  return amendment.state if emendation?

  proposal_state&.token || "not_answered"
end

#maximum_votesObject

Public: The maximum amount of votes allowed for this proposal.

Returns an Integer with the maximum amount of votes, nil otherwise.



326
327
328
329
330
331
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 326

def maximum_votes
  maximum_votes = component.settings.threshold_per_proposal
  return nil if maximum_votes.zero?

  maximum_votes
end

#maximum_votes_reached?Boolean

Public: The maximum amount of votes allowed for this proposal. 0 means infinite.

Returns true if reached, false otherwise.



336
337
338
339
340
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 336

def maximum_votes_reached?
  return false unless maximum_votes

  votes.count >= maximum_votes
end

#official?Boolean

Public: Whether the proposal is official or not.



314
315
316
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 314

def official?
  authors.first.is_a?(Decidim::Organization)
end

#official_meeting?Boolean

Public: Whether the proposal is created in a meeting or not.



319
320
321
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 319

def official_meeting?
  authors.first.instance_of?(Decidim::Meetings::Meeting)
end

#presenterObject

Returns the presenter for this author, to be used in the views. Required by ResourceRenderer.



298
299
300
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 298

def presenter
  Decidim::Proposals::ProposalPresenter.new(self)
end

#process_amendment_state_change!Object



484
485
486
487
488
489
490
491
492
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 484

def process_amendment_state_change!
  return withdraw! if amendment.withdrawn?
  return unless %w(accepted rejected evaluating).member?(amendment.state)

  PaperTrail.request(enabled: false) do
    assign_state(amendment.state)
    update!(state_published_at: Time.current)
  end
end

#published_state?Boolean

Public: Checks if the organization has published the state for the proposal.

Returns Boolean.



257
258
259
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 257

def published_state?
  emendation? || state_published_at.present?
end

#rejected?Boolean

Public: Checks if the organization has rejected a proposal.

Returns Boolean.



285
286
287
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 285

def rejected?
  state == "rejected"
end

#reported_attributesObject

Public: Overrides the reported_attributes Reportable concern method.



303
304
305
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 303

def reported_attributes
  [:title, :body]
end

#reported_searchable_content_extrasObject

Public: Overrides the reported_searchable_content_extras Reportable concern method. Returns authors name or title in case it is a meeting



309
310
311
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 309

def reported_searchable_content_extras
  [authors.map { |p| p.respond_to?(:name) ? p.name : p.title }.join("\n")]
end

#stateObject

Public: Returns the published state of the proposal.

Returns Boolean.



238
239
240
241
242
243
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 238

def state
  return amendment.state if emendation?
  return nil unless published_state? || withdrawn?

  proposal_state&.token || "not_answered"
end

#update_votes_countObject

Public: Updates the vote count of this proposal.

Returns nothing. rubocop:disable Rails/SkipsModelValidations



223
224
225
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 223

def update_votes_count
  update_columns(proposal_votes_count: votes.count)
end

#user_has_actions?(user) ⇒ Boolean



494
495
496
497
498
499
500
501
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 494

def user_has_actions?(user)
  return false if authors.include?(user)
  return false if user&.blocked?
  return false if user&.deleted?
  return false unless user&.confirmed?

  true
end

#voted_by?(user) ⇒ Boolean

Public: Check if the user has voted the proposal.

Returns Boolean.



231
232
233
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 231

def voted_by?(user)
  ProposalVote.where(proposal: self, author: user).any?
end

#withdraw!Object



365
366
367
368
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 365

def withdraw!
  self.withdrawn_at = Time.zone.now
  save
end

#withdrawable_by?(user) ⇒ Boolean

Checks whether the user can withdraw the given proposal.

user - the user to check for withdrawability.



361
362
363
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 361

def withdrawable_by?(user)
  user && !withdrawn? && authored_by?(user) && !copied_from_other_component?
end

#withdrawn?Boolean

Public: Checks if the author has withdrawn the proposal.

Returns Boolean.



271
272
273
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 271

def withdrawn?
  withdrawn_at.present?
end

#within_edit_time_limit?Boolean

Checks whether the proposal is inside the time window to be editable or not once published.



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'decidim-proposals/app/models/decidim/proposals/proposal.rb', line 466

def within_edit_time_limit?
  return true if draft?
  return true if component.settings.proposal_edit_time == "infinite"

  time_value, time_unit = component.settings.edit_time

  limit_time = case time_unit
               when "minutes"
                 updated_at + time_value.minutes
               when "hours"
                 updated_at + time_value.hours
               else
                 updated_at + time_value.days
               end

  Time.current < limit_time
end