From 7c26e5e4a101e2784d8c627055b4caad5812015f Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 3 Sep 2024 11:37:45 -0400
Subject: [PATCH] Add `Reviewable` model concern (#31152)

---
 app/models/account.rb                         | 17 +-----
 app/models/concerns/reviewable.rb             | 21 ++++++++
 app/models/preview_card_provider.rb           | 17 +-----
 app/models/tag.rb                             | 18 +------
 spec/models/account_spec.rb                   |  2 +
 spec/models/preview_card_provider_spec.rb     |  2 +
 spec/models/tag_spec.rb                       |  2 +
 .../examples/models/concerns/reviewable.rb    | 54 +++++++++++++++++++
 8 files changed, 85 insertions(+), 48 deletions(-)
 create mode 100644 app/models/concerns/reviewable.rb
 create mode 100644 spec/support/examples/models/concerns/reviewable.rb

diff --git a/app/models/account.rb b/app/models/account.rb
index 04095890e..c20b72658 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -89,6 +89,7 @@ class Account < ApplicationRecord
   include DomainMaterializable
   include DomainNormalizable
   include Paginable
+  include Reviewable
 
   enum :protocol, { ostatus: 0, activitypub: 1 }
   enum :suspension_origin, { local: 0, remote: 1 }, prefix: true
@@ -426,22 +427,6 @@ class Account < ApplicationRecord
     @synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
   end
 
-  def requires_review?
-    reviewed_at.nil?
-  end
-
-  def reviewed?
-    reviewed_at.present?
-  end
-
-  def requested_review?
-    requested_review_at.present?
-  end
-
-  def requires_review_notification?
-    requires_review? && !requested_review?
-  end
-
   class << self
     def readonly_attributes
       super - %w(statuses_count following_count followers_count)
diff --git a/app/models/concerns/reviewable.rb b/app/models/concerns/reviewable.rb
new file mode 100644
index 000000000..1f70474b3
--- /dev/null
+++ b/app/models/concerns/reviewable.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Reviewable
+  extend ActiveSupport::Concern
+
+  def requires_review?
+    reviewed_at.nil?
+  end
+
+  def reviewed?
+    reviewed_at.present?
+  end
+
+  def requested_review?
+    requested_review_at.present?
+  end
+
+  def requires_review_notification?
+    requires_review? && !requested_review?
+  end
+end
diff --git a/app/models/preview_card_provider.rb b/app/models/preview_card_provider.rb
index 756707e3f..48944fe63 100644
--- a/app/models/preview_card_provider.rb
+++ b/app/models/preview_card_provider.rb
@@ -21,6 +21,7 @@ class PreviewCardProvider < ApplicationRecord
   include Paginable
   include DomainNormalizable
   include Attachmentable
+  include Reviewable
 
   ICON_MIME_TYPES = %w(image/x-icon image/vnd.microsoft.icon image/png).freeze
   LIMIT = 1.megabyte
@@ -36,22 +37,6 @@ class PreviewCardProvider < ApplicationRecord
   scope :reviewed, -> { where.not(reviewed_at: nil) }
   scope :pending_review, -> { where(reviewed_at: nil) }
 
-  def requires_review?
-    reviewed_at.nil?
-  end
-
-  def reviewed?
-    reviewed_at.present?
-  end
-
-  def requested_review?
-    requested_review_at.present?
-  end
-
-  def requires_review_notification?
-    requires_review? && !requested_review?
-  end
-
   def self.matching_domain(domain)
     segments = domain.split('.')
     where(domain: segments.map.with_index { |_, i| segments[i..].join('.') }).by_domain_length.first
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 9006e1f25..acf514919 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -21,6 +21,8 @@
 
 class Tag < ApplicationRecord
   include Paginable
+  include Reviewable
+
   # rubocop:disable Rails/HasAndBelongsToMany
   has_and_belongs_to_many :statuses
   has_and_belongs_to_many :accounts
@@ -97,22 +99,6 @@ class Tag < ApplicationRecord
 
   alias trendable? trendable
 
-  def requires_review?
-    reviewed_at.nil?
-  end
-
-  def reviewed?
-    reviewed_at.present?
-  end
-
-  def requested_review?
-    requested_review_at.present?
-  end
-
-  def requires_review_notification?
-    requires_review? && !requested_review?
-  end
-
   def decaying?
     max_score_at && max_score_at >= Trends.tags.options[:max_score_cooldown].ago && max_score_at < 1.day.ago
   end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 14528ed17..83f1585b6 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 RSpec.describe Account do
+  include_examples 'Reviewable'
+
   context 'with an account record' do
     subject { Fabricate(:account) }
 
diff --git a/spec/models/preview_card_provider_spec.rb b/spec/models/preview_card_provider_spec.rb
index 7425b9394..8b18b3d2b 100644
--- a/spec/models/preview_card_provider_spec.rb
+++ b/spec/models/preview_card_provider_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 describe PreviewCardProvider do
+  include_examples 'Reviewable'
+
   describe 'scopes' do
     let(:trendable_and_reviewed) { Fabricate(:preview_card_provider, trendable: true, reviewed_at: 5.days.ago) }
     let(:not_trendable_and_not_reviewed) { Fabricate(:preview_card_provider, trendable: false, reviewed_at: nil) }
diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb
index ff0a05511..18dd26be9 100644
--- a/spec/models/tag_spec.rb
+++ b/spec/models/tag_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 RSpec.describe Tag do
+  include_examples 'Reviewable'
+
   describe 'validations' do
     it 'invalid with #' do
       expect(described_class.new(name: '#hello_world')).to_not be_valid
diff --git a/spec/support/examples/models/concerns/reviewable.rb b/spec/support/examples/models/concerns/reviewable.rb
new file mode 100644
index 000000000..562183d1c
--- /dev/null
+++ b/spec/support/examples/models/concerns/reviewable.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+shared_examples 'Reviewable' do
+  subject { described_class.new(reviewed_at: reviewed_at, requested_review_at: requested_review_at) }
+
+  let(:reviewed_at) { nil }
+  let(:requested_review_at) { nil }
+
+  describe '#requires_review?' do
+    it { is_expected.to be_requires_review }
+
+    context 'when reviewed_at is not null' do
+      let(:reviewed_at) { 5.days.ago }
+
+      it { is_expected.to_not be_requires_review }
+    end
+  end
+
+  describe '#reviewed?' do
+    it { is_expected.to_not be_reviewed }
+
+    context 'when reviewed_at is not null' do
+      let(:reviewed_at) { 5.days.ago }
+
+      it { is_expected.to be_reviewed }
+    end
+  end
+
+  describe '#requested_review?' do
+    it { is_expected.to_not be_requested_review }
+
+    context 'when requested_reviewed_at is not null' do
+      let(:requested_review_at) { 5.days.ago }
+
+      it { is_expected.to be_requested_review }
+    end
+  end
+
+  describe '#requires_review_notification?' do
+    it { is_expected.to be_requires_review_notification }
+
+    context 'when reviewed_at is not null' do
+      let(:reviewed_at) { 5.days.ago }
+
+      it { is_expected.to_not be_requires_review_notification }
+    end
+
+    context 'when requested_reviewed_at is not null' do
+      let(:requested_review_at) { 5.days.ago }
+
+      it { is_expected.to_not be_requires_review_notification }
+    end
+  end
+end