Class: Reviewable

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/reviewable.rb,
lib/reviewable/actions.rb,
lib/reviewable/collection.rb,
lib/reviewable/conversation.rb,
lib/reviewable/perform_result.rb,
lib/reviewable/editable_fields.rb

Defined Under Namespace

Classes: Actions, Collection, Conversation, EditableFields, InvalidAction, PerformResult, UpdateConflict

Constant Summary collapse

TYPE_TO_BASIC_SERIALIZER =
{
  ReviewableFlaggedPost: BasicReviewableFlaggedPostSerializer,
  ReviewableQueuedPost: BasicReviewableQueuedPostSerializer,
  ReviewableUser: BasicReviewableUserSerializer,
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#created_newObject

Returns the value of attribute created_new.



22
23
24
# File 'app/models/reviewable.rb', line 22

def created_new
  @created_new
end

Class Method Details

.action_aliasesObject

Can be used if several actions are equivalent



54
55
56
# File 'app/models/reviewable.rb', line 54

def self.action_aliases
  {}
end

.add_custom_filter(new_filter) ⇒ Object



80
81
82
# File 'app/models/reviewable.rb', line 80

def self.add_custom_filter(new_filter)
  custom_filters << new_filter
end

.basic_serializers_for_list(reviewables, user) ⇒ Object



570
571
572
# File 'app/models/reviewable.rb', line 570

def self.basic_serializers_for_list(reviewables, user)
  reviewables.map { |r| r.basic_serializer.new(r, scope: user.guardian, root: nil) }
end

.bulk_perform_targets(performed_by, action, type, target_ids, args = nil) ⇒ Object



386
387
388
389
390
391
# File 'app/models/reviewable.rb', line 386

def self.bulk_perform_targets(performed_by, action, type, target_ids, args = nil)
  args ||= {}
  viewable_by(performed_by)
    .where(type: type, target_id: target_ids)
    .each { |r| r.perform(performed_by, action, args) }
end

.clear_custom_filters!Object



84
85
86
# File 'app/models/reviewable.rb', line 84

def self.clear_custom_filters!
  @reviewable_filters = []
end

.count_by_date(start_date, end_date, category_id = nil, include_subcategories = false) ⇒ Object



609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
# File 'app/models/reviewable.rb', line 609

def self.count_by_date(start_date, end_date, category_id = nil, include_subcategories = false)
  query =
    scores_with_topics.where("reviewable_scores.created_at BETWEEN ? AND ?", start_date, end_date)

  if category_id
    if include_subcategories
      query = query.where("topics.category_id IN (?)", Category.subcategory_ids(category_id))
    else
      query = query.where("topics.category_id = ?", category_id)
    end
  end

  query
    .group("date(reviewable_scores.created_at)")
    .order("date(reviewable_scores.created_at)")
    .count
end

.custom_filtersObject



76
77
78
# File 'app/models/reviewable.rb', line 76

def self.custom_filters
  @reviewable_filters ||= []
end

.default_visibleObject



64
65
66
# File 'app/models/reviewable.rb', line 64

def self.default_visible
  where("score >= ?", min_score_for_priority)
end

.list_for(user, ids: nil, status: :pending, category_id: nil, topic_id: nil, type: nil, limit: nil, offset: nil, priority: nil, username: nil, reviewed_by: nil, sort_order: nil, from_date: nil, to_date: nil, additional_filters: {}, preload: true, include_claimed_by_others: true, flagged_by: nil, score_type: nil) ⇒ Object



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
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
530
531
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
# File 'app/models/reviewable.rb', line 434

def self.list_for(
  user,
  ids: nil,
  status: :pending,
  category_id: nil,
  topic_id: nil,
  type: nil,
  limit: nil,
  offset: nil,
  priority: nil,
  username: nil,
  reviewed_by: nil,
  sort_order: nil,
  from_date: nil,
  to_date: nil,
  additional_filters: {},
  preload: true,
  include_claimed_by_others: true,
  flagged_by: nil,
  score_type: nil
)
  order =
    case sort_order
    when "score_asc"
      "reviewables.score ASC, reviewables.created_at DESC"
    when "created_at"
      "reviewables.created_at DESC, reviewables.score DESC"
    when "created_at_asc"
      "reviewables.created_at ASC, reviewables.score DESC"
    else
      "reviewables.score DESC, reviewables.created_at DESC"
    end

  if username.present?
    user_id = User.find_by_username(username)&.id
    return none if user_id.blank?
  end
  return none if user.blank?

  result = viewable_by(user, order: order, preload: preload)
  result = by_status(result, status)
  result = result.where(id: ids) if ids
  result = result.where("reviewables.type = ?", Reviewable.sti_class_for(type).sti_name) if type
  result = result.where("reviewables.category_id = ?", category_id) if category_id
  result = result.where("reviewables.topic_id = ?", topic_id) if topic_id
  result = result.where("reviewables.created_at >= ?", from_date) if from_date
  result = result.where("reviewables.created_at <= ?", to_date) if to_date

  if flagged_by
    flagged_by_id = User.find_by_username(flagged_by)&.id
    return none if flagged_by_id.nil?
    result = result.where("      EXISTS(\n        SELECT 1 FROM reviewable_scores\n        WHERE reviewable_scores.reviewable_id = reviewables.id AND reviewable_scores.user_id = :flagged_by_id\n      )\n    SQL\n  end\n\n  if score_type\n    score_type = score_type.to_i\n    result = result.where(<<~SQL, score_type: score_type)\n    EXISTS(\n      SELECT 1 FROM reviewable_scores\n      WHERE reviewable_scores.reviewable_id = reviewables.id AND reviewable_scores.reviewable_score_type = :score_type\n    )\n    SQL\n  end\n\n  if reviewed_by\n    reviewed_by_id = User.find_by_username(reviewed_by)&.id\n    return none if reviewed_by_id.nil?\n\n    result = result.joins(<<~SQL)\n      INNER JOIN(\n        SELECT reviewable_id\n        FROM reviewable_histories\n        WHERE reviewable_history_type = \#{ReviewableHistory.types[:transitioned]} AND\n        status <> \#{statuses[:pending]} AND created_by_id = \#{reviewed_by_id}\n      ) AS rh ON rh.reviewable_id = reviewables.id\n    SQL\n  end\n\n  min_score = min_score_for_priority(priority)\n\n  if min_score > 0 && status == :pending\n    result = result.where(\"reviewables.score >= ? OR reviewables.force_review\", min_score)\n  elsif min_score > 0\n    result = result.where(\"reviewables.score >= ?\", min_score)\n  end\n\n  if !custom_filters.empty?\n    result =\n      custom_filters.reduce(result) do |memo, filter|\n        key = filter.first\n        filter_query = filter.last\n\n        next(memo) unless additional_filters[key]\n        filter_query.call(result, additional_filters[key])\n      end\n  end\n\n  # If a reviewable doesn't have a target, allow us to filter on who created that reviewable.\n  # A ReviewableQueuedPost may have a target_created_by_id even before a target get's assigned\n  if user_id\n    result =\n      result.where(\n        \"(reviewables.target_id IS NULL AND reviewables.created_by_id = :user_id)\n      OR (reviewables.target_created_by_id = :user_id)\",\n        user_id: user_id,\n      )\n  end\n\n  if !include_claimed_by_others\n    result =\n      result.joins(\n        \"LEFT JOIN reviewable_claimed_topics rct ON reviewables.topic_id = rct.topic_id\",\n      ).where(\"rct.user_id IS NULL OR rct.user_id = ?\", user.id)\n  end\n  result = result.limit(limit) if limit\n  result = result.offset(offset) if offset\n  result\nend\n", flagged_by_id: flagged_by_id)

.lookup_serializer_for(type) ⇒ Object



586
587
588
589
590
# File 'app/models/reviewable.rb', line 586

def self.lookup_serializer_for(type)
  "#{type}Serializer".constantize
rescue NameError
  ReviewableSerializer
end

.min_score_for_priority(priority = nil) ⇒ Object



251
252
253
254
255
256
# File 'app/models/reviewable.rb', line 251

def self.min_score_for_priority(priority = nil)
  priority ||= SiteSetting.reviewable_default_visibility
  id = priorities[priority]
  return 0.0 if id.nil?
  PluginStore.get("reviewables", "priority_#{id}").to_f
end

.needs_review!(target: nil, topic: nil, created_by:, payload: nil, reviewable_by_moderator: false, potential_spam: true, potentially_illegal: false, target_created_by: nil) ⇒ Object

Create a new reviewable, or if the target has already been reviewed return it to the pending state and re-use it.

You probably want to call this to create your reviewable rather than ‘.create`.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'app/models/reviewable.rb', line 99

def self.needs_review!(
  target: nil,
  topic: nil,
  created_by:,
  payload: nil,
  reviewable_by_moderator: false,
  potential_spam: true,
  potentially_illegal: false,
  target_created_by: nil
)
  reviewable =
    new(
      target: target,
      topic: topic,
      created_by: created_by,
      reviewable_by_moderator: reviewable_by_moderator,
      payload: payload,
      potential_spam: potential_spam,
      potentially_illegal: potentially_illegal,
      target_created_by: target_created_by,
    )
  reviewable.created_new!

  if target.blank? || !Reviewable.where(target: target, type: reviewable.type).exists?
    # If there is no target, or no existing reviewable with matching target and type, there's no chance of a conflict
    reviewable.save!
  else
    # In this case, a reviewable might already exist for this (type, target_id) index.
    # ActiveRecord can only validate indexes using a SELECT before the INSERT which
    # is not safe under concurrency. Instead, we perform an UPDATE on the status, and return
    # the previous value. We then know:
    #
    #   a) if a previous row existed
    #   b) if it was changed
    #
    # And that allows us to complete our logic.

    update_args = {
      status: statuses[:pending],
      id: target.id,
      type: target.class.polymorphic_name,
      potential_spam: potential_spam == true ? true : nil,
      potentially_illegal: potentially_illegal == true ? true : nil,
    }

    row = DB.query_single("      UPDATE reviewables\n      SET status = :status,\n        potential_spam = COALESCE(:potential_spam, reviewables.potential_spam),\n        potentially_illegal = COALESCE(:potentially_illegal, reviewables.potentially_illegal)\n      FROM reviewables AS old_reviewables\n      WHERE reviewables.target_id = :id\n        AND reviewables.target_type = :type\n      RETURNING old_reviewables.status\n    SQL\n    old_status = row[0]\n\n    if old_status.blank?\n      reviewable.save!\n    else\n      reviewable = find_by(target: target)\n\n      if old_status != statuses[:pending]\n        # If we're transitioning back from reviewed to pending, we should recalculate\n        # the score to prevent posts from being hidden.\n        reviewable.recalculate_score\n        reviewable.log_history(:transitioned, created_by)\n      end\n    end\n  end\n\n  reviewable\nend\n", update_args)

.pending_count(user) ⇒ Object



426
427
428
# File 'app/models/reviewable.rb', line 426

def self.pending_count(user)
  list_for(user).count
end

.score_required_to_hide_postObject



247
248
249
# File 'app/models/reviewable.rb', line 247

def self.score_required_to_hide_post
  sensitivity_score(SiteSetting.hide_post_sensitivity)
end

.score_to_auto_close_topicObject



239
240
241
# File 'app/models/reviewable.rb', line 239

def self.score_to_auto_close_topic
  sensitivity_score(SiteSetting.auto_close_topic_sensitivity, scale: 2.5)
end

.scores_with_topicsObject



605
606
607
# File 'app/models/reviewable.rb', line 605

def self.scores_with_topics
  ReviewableScore.joins(reviewable: :topic).where("reviewables.type = ?", name)
end

.sensitivity_score(sensitivity, scale: 1.0) ⇒ Object



232
233
234
235
236
237
# File 'app/models/reviewable.rb', line 232

def self.sensitivity_score(sensitivity, scale: 1.0)
  # If the score is less than the default visibility, bring it up to that level.
  # Otherwise we have the confusing situation where a post might be hidden and
  # moderators would never see it!
  [sensitivity_score_value(sensitivity, scale), min_score_for_priority].max
end

.sensitivity_score_value(sensitivity, scale) ⇒ Object



221
222
223
224
225
226
227
228
229
230
# File 'app/models/reviewable.rb', line 221

def self.sensitivity_score_value(sensitivity, scale)
  return Float::MAX if sensitivity == 0

  ratio = sensitivity / sensitivities[:low].to_f
  high =
    (PluginStore.get("reviewables", "priority_#{priorities[:high]}") || typical_sensitivity).to_f

  # We want this to be hard to reach
  ((high.to_f * ratio) * scale).truncate(2)
end

.serializer_for(reviewable) ⇒ Object



592
593
594
595
596
# File 'app/models/reviewable.rb', line 592

def self.serializer_for(reviewable)
  type = reviewable.type
  @@serializers ||= {}
  @@serializers[type] ||= lookup_serializer_for(type)
end

.set_priorities(values) ⇒ Object



214
215
216
217
218
219
# File 'app/models/reviewable.rb', line 214

def self.set_priorities(values)
  values.each do |k, v|
    id = priorities[k]
    PluginStore.set("reviewables", "priority_#{id}", v) unless id.nil?
  end
end

.spam_score_to_silence_new_userObject



243
244
245
# File 'app/models/reviewable.rb', line 243

def self.spam_score_to_silence_new_user
  sensitivity_score(SiteSetting.silence_new_user_sensitivity, scale: 0.6)
end

.typesObject



72
73
74
# File 'app/models/reviewable.rb', line 72

def self.types
  [ReviewableFlaggedPost, ReviewableQueuedPost, ReviewableUser, ReviewablePost]
end

.typical_sensitivityObject

This number comes from looking at forums in the wild and what numbers work. As the site accumulates real data it’ll be based on the site activity instead.



60
61
62
# File 'app/models/reviewable.rb', line 60

def self.typical_sensitivity
  12.5
end

.unseen_list_for(user, preload: true, limit: nil) ⇒ Object



558
559
560
561
562
563
564
# File 'app/models/reviewable.rb', line 558

def self.unseen_list_for(user, preload: true, limit: nil)
  results = list_for(user, preload: preload, limit: limit, include_claimed_by_others: false)
  if user.last_seen_reviewable_id
    results = results.where("reviewables.id > ?", user.last_seen_reviewable_id)
  end
  results
end

.unseen_reviewable_count(user) ⇒ Object



430
431
432
# File 'app/models/reviewable.rb', line 430

def self.unseen_reviewable_count(user)
  self.unseen_list_for(user).count
end

.user_menu_list_for(user, limit: 30) ⇒ Object



566
567
568
# File 'app/models/reviewable.rb', line 566

def self.user_menu_list_for(user, limit: 30)
  list_for(user, limit: limit, status: :pending, include_claimed_by_others: false).to_a
end

.valid_type?(type) ⇒ Boolean

Returns:

  • (Boolean)


68
69
70
# File 'app/models/reviewable.rb', line 68

def self.valid_type?(type)
  type.to_s.safe_constantize.in?(types)
end

.viewable_by(user, order: nil, preload: true) ⇒ Object



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'app/models/reviewable.rb', line 393

def self.viewable_by(user, order: nil, preload: true)
  return none if user.blank?

  result = self.order(order || "reviewables.score desc, reviewables.created_at desc")

  if preload
    result =
      result.includes(
        { created_by: :user_stat },
        :topic,
        :target,
        :target_created_by,
        :reviewable_histories,
      ).includes(reviewable_scores: { user: :user_stat, meta_topic: :posts })
  end
  return result if user.admin?

  group_ids =
    SiteSetting.enable_category_group_moderation? ? user.group_users.pluck(:group_id) : []

  result
    .left_joins(category: :category_moderation_groups)
    .where(
      "(reviewables.reviewable_by_moderator AND :moderator) OR (category_moderation_groups.group_id IN (:group_ids))",
      moderator: user.moderator?,
      group_ids: group_ids,
    )
    .where(
      "reviewables.category_id IS NULL OR reviewables.category_id IN (?)",
      Guardian.new(user).allowed_category_ids,
    )
end

Instance Method Details

#actions_for(guardian, args = nil) ⇒ Object



271
272
273
274
275
276
277
278
279
280
281
282
# File 'app/models/reviewable.rb', line 271

def actions_for(guardian, args = nil)
  args ||= {}
  built_actions =
    Actions.new(self, guardian).tap { |actions| build_actions(actions, guardian, args) }

  # Empty bundles can cause big issues on the client side, so we remove them
  # here. It's not valid anyway to have a bundle with no actions, but you can
  # add a bundle via actions.add_bundle and then not add any actions to it.
  built_actions.bundles.reject!(&:empty?)

  built_actions
end

#add_score(user, reviewable_score_type, reason: nil, created_at: nil, take_action: false, meta_topic_id: nil, force_review: false) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'app/models/reviewable.rb', line 173

def add_score(
  user,
  reviewable_score_type,
  reason: nil,
  created_at: nil,
  take_action: false,
  meta_topic_id: nil,
  force_review: false
)
  type_bonus = PostActionType.where(id: reviewable_score_type).pluck(:score_bonus)[0] || 0
  take_action_bonus = take_action ? 5.0 : 0.0
  user_accuracy_bonus = ReviewableScore.user_accuracy_bonus(user)
  sub_total = ReviewableScore.calculate_score(user, type_bonus, take_action_bonus)

  rs =
    reviewable_scores.new(
      user: user,
      status: :pending,
      reviewable_score_type: reviewable_score_type,
      score: sub_total,
      user_accuracy_bonus: user_accuracy_bonus,
      meta_topic_id: meta_topic_id,
      take_action_bonus: take_action_bonus,
      created_at: created_at || Time.zone.now,
    )
  rs.reason = reason.to_s if reason
  rs.save!

  update(score: self.score + rs.score, latest_score: rs.created_at, force_review: force_review)
  topic.update(reviewable_score: topic.reviewable_score + rs.score) if topic

  # Flags are cached for performance reasons.
  # However, when the reviewable item is created, we need to clear the cache to mark flag as used.
  # Used flags cannot be deleted or update by admins, only disabled.
  Flag.reset_flag_settings! if PostActionType.notify_flag_type_ids.include?(reviewable_score_type)

  DiscourseEvent.trigger(:reviewable_score_updated, self)

  rs
end

#basic_serializerObject



578
579
580
# File 'app/models/reviewable.rb', line 578

def basic_serializer
  TYPE_TO_BASIC_SERIALIZER[self.type.to_sym] || BasicReviewableSerializer
end

#build_actions(actions, guardian, args) ⇒ Object

subclasses must implement “build_actions” to list the actions they’re capable of

Raises:

  • (NotImplementedError)


292
293
294
# File 'app/models/reviewable.rb', line 292

def build_actions(actions, guardian, args)
  raise NotImplementedError
end

#build_editable_fields(actions, guardian, args) ⇒ Object

subclasses can implement “build_editable_fields” to list stuff that can be edited



297
298
# File 'app/models/reviewable.rb', line 297

def build_editable_fields(actions, guardian, args)
end

#create_result(status, transition_to = nil) {|result| ... } ⇒ Object

Yields:

  • (result)


598
599
600
601
602
603
# File 'app/models/reviewable.rb', line 598

def create_result(status, transition_to = nil)
  result = PerformResult.new(self, status)
  result.transition_to = transition_to
  yield result if block_given?
  result
end

#created_new!Object



88
89
90
91
92
93
# File 'app/models/reviewable.rb', line 88

def created_new!
  self.created_new = true
  self.topic = target.topic if topic.blank? && target.is_a?(Post)
  self.target_created_by_id ||= target.is_a?(Post) ? target.user_id : nil
  self.category_id = topic.category_id if category_id.blank? && topic.present?
end

#delete_user_actions(actions, bundle = nil, require_reject_reason: false) ⇒ Object



695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
# File 'app/models/reviewable.rb', line 695

def (actions, bundle = nil, require_reject_reason: false)
  bundle ||=
    actions.add_bundle(
      "reject_user",
      icon: "user-xmark",
      label: "reviewables.actions.reject_user.title",
    )

  actions.add(:delete_user, bundle: bundle) do |a|
    a.icon = "user-xmark"
    a.label = "reviewables.actions.reject_user.delete.title"
    a.require_reject_reason = require_reject_reason
  end

  actions.add(:delete_user_block, bundle: bundle) do |a|
    a.icon = "ban"
    a.label = "reviewables.actions.reject_user.block.title"
    a.require_reject_reason = require_reject_reason
    a.description = "reviewables.actions.reject_user.block.description"
  end
end

#editable_for(guardian, args = nil) ⇒ Object



284
285
286
287
288
289
# File 'app/models/reviewable.rb', line 284

def editable_for(guardian, args = nil)
  args ||= {}
  EditableFields
    .new(self, guardian, args)
    .tap { |fields| build_editable_fields(fields, guardian, args) }
end

#explain_scoreObject



627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
# File 'app/models/reviewable.rb', line 627

def explain_score
  DB.query("    SELECT rs.reviewable_id,\n      rs.user_id,\n      CASE WHEN (u.admin OR u.moderator) THEN 5.0 ELSE u.trust_level END AS trust_level_bonus,\n      us.flags_agreed,\n      us.flags_disagreed,\n      us.flags_ignored,\n      rs.score,\n      rs.user_accuracy_bonus,\n      rs.take_action_bonus,\n      COALESCE(pat.score_bonus, 0.0) AS type_bonus\n    FROM reviewable_scores AS rs\n    INNER JOIN users AS u ON u.id = rs.user_id\n    LEFT OUTER JOIN user_stats AS us ON us.user_id = rs.user_id\n    LEFT OUTER JOIN post_action_types AS pat ON pat.id = rs.reviewable_score_type\n      WHERE rs.reviewable_id = :reviewable_id\n  SQL\nend\n", reviewable_id: id)

#historyObject



258
259
260
# File 'app/models/reviewable.rb', line 258

def history
  reviewable_histories.order(:created_at)
end

#log_history(reviewable_history_type, performed_by, edited: nil) ⇒ Object



262
263
264
265
266
267
268
269
# File 'app/models/reviewable.rb', line 262

def log_history(reviewable_history_type, performed_by, edited: nil)
  reviewable_histories.create!(
    reviewable_history_type: reviewable_history_type,
    status: status,
    created_by: performed_by,
    edited: edited,
  )
end

#perform(performed_by, action_id, args = nil) ⇒ Object

Delegates to a ‘perform_#action_id` method, which returns a `PerformResult` with the result of the operation and whether the status of the reviewable changed.

Raises:



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'app/models/reviewable.rb', line 322

def perform(performed_by, action_id, args = nil)
  args ||= {}
  # Support this action or any aliases
  aliases = self.class.action_aliases
  valid = [action_id, aliases.to_a.select { |k, v| v == action_id }.map(&:first)].flatten

  # Ensure the user has access to the action
  actions = actions_for(args[:guardian] || Guardian.new(performed_by), args)
  raise InvalidAction.new(action_id, self.class) unless valid.any? { |a| actions.has?(a) }

  perform_method = "perform_#{aliases[action_id] || action_id}".to_sym
  raise InvalidAction.new(action_id, self.class) unless respond_to?(perform_method)

  result = nil
  update_count = false
  Reviewable.transaction do
    increment_version!(args[:version])
    result = public_send(perform_method, performed_by, args)

    raise ActiveRecord::Rollback unless result.success?

    update_count = transition_to(result.transition_to, performed_by) if result.transition_to
    update_flag_stats(**result.update_flag_stats) if result.update_flag_stats

    recalculate_score if result.recalculate_score
  end
  result.after_commit.call if result && result.after_commit

  if update_count || result.remove_reviewable_ids.present?
    Jobs.enqueue(
      :notify_reviewable,
      reviewable_id: self.id,
      performing_username: performed_by.username,
      updated_reviewable_ids: result.remove_reviewable_ids,
    )
  end

  result
end

#recalculate_scoreObject



647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
# File 'app/models/reviewable.rb', line 647

def recalculate_score
  # pending/agreed scores count
  sql = "    UPDATE reviewables\n    SET score = COALESCE((\n      SELECT sum(score)\n      FROM reviewable_scores AS rs\n      WHERE rs.reviewable_id = :id\n        AND rs.status IN (:pending, :agreed)\n    ), 0.0)\n    WHERE id = :id\n    RETURNING score\n  SQL\n\n  result =\n    DB.query(\n      sql,\n      id: self.id,\n      pending: ReviewableScore.statuses[:pending],\n      agreed: ReviewableScore.statuses[:agreed],\n    )\n\n  # Update topic score\n  sql = <<~SQL\n    UPDATE topics\n    SET reviewable_score = COALESCE((\n      SELECT SUM(score)\n      FROM reviewables AS r\n      WHERE r.topic_id = :topic_id\n        AND r.status IN (:pending, :approved)\n    ), 0.0)\n    WHERE id = :topic_id\n  SQL\n\n  DB.query(\n    sql,\n    topic_id: topic_id,\n    pending: self.class.statuses[:pending],\n    approved: self.class.statuses[:approved],\n  )\n\n  self.score = result[0].score\n\n  DiscourseEvent.trigger(:reviewable_score_updated, self)\n\n  self.score\nend\n"

#serializerObject



574
575
576
# File 'app/models/reviewable.rb', line 574

def serializer
  self.class.serializer_for(self)
end

#transition_to(status_symbol, performed_by) ⇒ Object



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'app/models/reviewable.rb', line 368

def transition_to(status_symbol, performed_by)
  self.status = status_symbol
  save!

  log_history(:transitioned, performed_by)
  DiscourseEvent.trigger(:reviewable_transitioned_to, status_symbol, self)

  if score_status = ReviewableScore.score_transitions[status_symbol]
    updatable_reviewable_scores.update_all(
      status: score_status,
      reviewed_by_id: performed_by.id,
      reviewed_at: Time.zone.now,
    )
  end

  status_previously_changed?(from: "pending")
end

#type_classObject



582
583
584
# File 'app/models/reviewable.rb', line 582

def type_class
  Reviewable.sti_class_for(self.type)
end

#updatable_reviewable_scoresObject

Override this in specific reviewable type to include scores for non-pending reviewables



364
365
366
# File 'app/models/reviewable.rb', line 364

def updatable_reviewable_scores
  reviewable_scores.pending
end

#update_fields(params, performed_by, version: nil) ⇒ Object



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'app/models/reviewable.rb', line 300

def update_fields(params, performed_by, version: nil)
  return true if params.blank?

  (params[:payload] || {}).each { |k, v| self.payload[k] = v }
  self.category_id = params[:category_id] if params.has_key?(:category_id)

  result = false

  Reviewable.transaction do
    increment_version!(version)
    changes_json = changes.as_json
    changes_json.delete("version")

    result = save
    log_history(:edited, performed_by, edited: changes_json) if result
  end

  result
end