Class: Decidim::Proposals::Proposal

Inherits:
ApplicationRecord show all
Includes:
Amendable, Coauthorable, DownloadYourData, FilterableResource, Fingerprintable, Followable, HasAttachments, HasCategory, HasComponent, HasReference, Likeable, Loggable, NewsletterParticipant, CommentableProposal, Evaluable, ParticipatoryTextSection, Decidim::Publicable, Randomable, Reportable, Resourceable, ScopableResource, Searchable, SoftDeletable, Taxonomizable, Traceable, TranslatableAttributes, TranslatableResource
Defined in:
app/models/decidim/proposals/proposal.rb

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

Class Method Details

.download_your_data_images(user) ⇒ Object



485
486
487
# File 'app/models/decidim/proposals/proposal.rb', line 485

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



409
410
411
412
413
414
415
416
417
418
419
# File 'app/models/decidim/proposals/proposal.rb', line 409

def self.evaluator_role_ids_has(value)
  query = <<~SQL.squish
    :value = any(
      (SELECT decidim_proposals_evaluation_assignments.evaluator_role_id
      FROM decidim_proposals_evaluation_assignments
      WHERE decidim_proposals_evaluation_assignments.decidim_proposal_id = decidim_proposals_proposals.id
      )
    )
  SQL
  where(query, value:)
end

.export_serializerObject



481
482
483
# File 'app/models/decidim/proposals/proposal.rb', line 481

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

.log_presenter_class_for(_log) ⇒ Object



209
210
211
# File 'app/models/decidim/proposals/proposal.rb', line 209

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

.most_commented_available?(component) ⇒ Boolean

Returns:

  • (Boolean)


177
178
179
180
181
182
183
184
185
186
# File 'app/models/decidim/proposals/proposal.rb', line 177

def self.most_commented_available?(component)
  return false unless component.settings.comments_enabled?

  where(component:)
    .published
    .not_hidden
    .not_withdrawn
    .where("comments_count > 0")
    .exists?
end

.most_liked_available?(component) ⇒ Boolean

Returns:

  • (Boolean)


188
189
190
191
192
193
194
195
# File 'app/models/decidim/proposals/proposal.rb', line 188

def self.most_liked_available?(component)
  where(component:)
    .published
    .not_hidden
    .not_withdrawn
    .where("likes_count > 0")
    .exists?
end

.newsletter_participant_ids(component) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'app/models/decidim/proposals/proposal.rb', line 232

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



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

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

.ransackable_associations(_auth_object = nil) ⇒ Object



433
434
435
# File 'app/models/decidim/proposals/proposal.rb', line 433

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

.ransackable_attributes(_auth_object = nil) ⇒ Object



429
430
431
# File 'app/models/decidim/proposals/proposal.rb', line 429

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



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

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



223
224
225
226
227
228
229
230
# File 'app/models/decidim/proposals/proposal.rb', line 223

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



450
451
452
453
# File 'app/models/decidim/proposals/proposal.rb', line 450

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



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

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.



215
216
217
218
219
220
221
# File 'app/models/decidim/proposals/proposal.rb', line 215

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 '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

.with_more_authors_available?(component) ⇒ Boolean

Returns:

  • (Boolean)


168
169
170
171
172
173
174
175
# File 'app/models/decidim/proposals/proposal.rb', line 168

def self.with_more_authors_available?(component)
  where(component:)
    .published
    .not_hidden
    .not_withdrawn
    .where("coauthorships_count > 1")
    .exists?
end

Instance Method Details

#accepted?Boolean

Public: Checks if the organization has accepted a proposal.

Returns Boolean.

Returns:

  • (Boolean)


307
308
309
# File 'app/models/decidim/proposals/proposal.rb', line 307

def accepted?
  state == "accepted"
end

#actions_for_comment(comment, current_user) ⇒ Object



532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'app/models/decidim/proposals/proposal.rb', line 532

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.

Returns:

  • (Boolean)


490
491
492
# File 'app/models/decidim/proposals/proposal.rb', line 490

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.

Returns:

  • (Boolean)


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

def answered?
  answered_at.present?
end

#assign_state(token) ⇒ Object



35
36
37
38
39
# File '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



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

def can_accumulate_votes_beyond_threshold
  component.settings.can_accumulate_votes_beyond_threshold
end

#coauthor_invitations_for(user) ⇒ Object



560
561
562
# File 'app/models/decidim/proposals/proposal.rb', line 560

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.

Returns:

  • (Boolean)


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

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

Returns:

  • (Boolean)


381
382
383
384
385
# File 'app/models/decidim/proposals/proposal.rb', line 381

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.

Returns:

  • (Boolean)


321
322
323
# File 'app/models/decidim/proposals/proposal.rb', line 321

def evaluating?
  state == "evaluating"
end

#internal_stateObject

Public: Returns the internal state of the proposal.

Returns Boolean.



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

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.



355
356
357
358
359
360
# File 'app/models/decidim/proposals/proposal.rb', line 355

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.

Returns:

  • (Boolean)


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

def maximum_votes_reached?
  return false unless maximum_votes

  votes.count >= maximum_votes
end

#official?Boolean

Public: Whether the proposal is official or not.

Returns:

  • (Boolean)


343
344
345
# File 'app/models/decidim/proposals/proposal.rb', line 343

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

#official_meeting?Boolean

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

Returns:

  • (Boolean)


348
349
350
# File 'app/models/decidim/proposals/proposal.rb', line 348

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.



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

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

#process_amendment_state_change!Object



513
514
515
516
517
518
519
520
521
# File 'app/models/decidim/proposals/proposal.rb', line 513

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.

Returns:

  • (Boolean)


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

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

#rejected?Boolean

Public: Checks if the organization has rejected a proposal.

Returns Boolean.

Returns:

  • (Boolean)


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

def rejected?
  state == "rejected"
end

#reported_attributesObject

Public: Overrides the ‘reported_attributes` Reportable concern method.



332
333
334
# File 'app/models/decidim/proposals/proposal.rb', line 332

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



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

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.



267
268
269
270
271
272
# File 'app/models/decidim/proposals/proposal.rb', line 267

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



252
253
254
# File 'app/models/decidim/proposals/proposal.rb', line 252

def update_votes_count
  update_columns(proposal_votes_count: votes.count)
end

#user_has_actions?(user) ⇒ Boolean

Returns:

  • (Boolean)


523
524
525
526
527
528
529
530
# File 'app/models/decidim/proposals/proposal.rb', line 523

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.

Returns:

  • (Boolean)


260
261
262
# File 'app/models/decidim/proposals/proposal.rb', line 260

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

#withdraw!Object



394
395
396
397
# File 'app/models/decidim/proposals/proposal.rb', line 394

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.

Returns:

  • (Boolean)


390
391
392
# File 'app/models/decidim/proposals/proposal.rb', line 390

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.

Returns:

  • (Boolean)


300
301
302
# File 'app/models/decidim/proposals/proposal.rb', line 300

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.

Returns:

  • (Boolean)


495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'app/models/decidim/proposals/proposal.rb', line 495

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