diff --git a/app/models/article.rb b/app/models/article.rb index 6ee53c5ae..b0db1b2d6 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -28,6 +28,8 @@ class Article < ApplicationRecord counter_culture :organization has_many :comments, as: :commentable, inverse_of: :commentable + has_many :user_subscriptions, as: :user_subscription_sourceable + has_many :sourced_subscribers, class_name: "User", through: :user_subscriptions, source: :subscriber, foreign_key: :user_id has_many :top_comments, -> { where("comments.score > ? AND ancestry IS NULL and hidden_by_commentable_user is FALSE and deleted is FALSE", 10).order("comments.score DESC") }, as: :commentable, diff --git a/app/models/user.rb b/app/models/user.rb index fbdf33552..fd8824915 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -67,6 +67,8 @@ class User < ApplicationRecord has_many :display_ad_events, dependent: :destroy has_many :email_authorizations, dependent: :delete_all has_many :email_messages, class_name: "Ahoy::Message", dependent: :destroy + has_many :user_subscriptions, foreign_key: :author_id, inverse_of: :author, dependent: :destroy + has_many :subscribers, through: :user_subscriptions, dependent: :destroy has_many :field_test_memberships, class_name: "FieldTest::Membership", as: :participant, dependent: :destroy has_many :github_repos, dependent: :destroy has_many :html_variants, dependent: :destroy diff --git a/app/models/user_subscription.rb b/app/models/user_subscription.rb new file mode 100644 index 000000000..5cf84fed4 --- /dev/null +++ b/app/models/user_subscription.rb @@ -0,0 +1,15 @@ +# This model handles a user (subscriber) subscribing to another user (author). +# We also record the source of the subscription (Article, Comment, etc.) via a +# polymorphic association (user_subscription_source/able). +class UserSubscription < ApplicationRecord + ALLOWED_TYPES = %w[Article].freeze + + belongs_to :author, class_name: "User", foreign_key: :author_id, inverse_of: :user_subscriptions + belongs_to :subscriber, class_name: "User", foreign_key: :subscriber_id, inverse_of: :user_subscriptions + belongs_to :user_subscription_sourceable, polymorphic: true + + validates :author_id, presence: true + validates :subscriber_id, presence: true, uniqueness: { scope: %i[user_subscription_sourceable_type user_subscription_sourceable_id] } + validates :user_subscription_sourceable_id, presence: true + validates :user_subscription_sourceable_type, presence: true, inclusion: { in: ALLOWED_TYPES } +end diff --git a/db/migrate/20200609192545_create_user_subscriptions.rb b/db/migrate/20200609192545_create_user_subscriptions.rb new file mode 100644 index 000000000..7d522d9da --- /dev/null +++ b/db/migrate/20200609192545_create_user_subscriptions.rb @@ -0,0 +1,18 @@ +class CreateUserSubscriptions < ActiveRecord::Migration[6.0] + def change + create_table :user_subscriptions do |t| + t.references :user_subscription_sourceable, polymorphic: true, null: false, index: { name: :index_on_user_subscription_sourcebable_type_and_id } + t.references :subscriber, references: :users, foreign_key: { to_table: :users }, null: false + t.references :author, references: :users, foreign_key: { to_table: :users }, null: false + + t.timestamps + end + + add_index( + :user_subscriptions, + %i[subscriber_id user_subscription_sourceable_id user_subscription_sourceable_type], + unique: true, + name: :index_on_subscriber_id_user_subscription_sourceable_type_and_id + ) + end +end diff --git a/db/schema.rb b/db/schema.rb index 5e8ebe287..02d69cd02 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_06_09_191943) do +ActiveRecord::Schema.define(version: 2020_06_09_192545) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1140,6 +1140,19 @@ ActiveRecord::Schema.define(version: 2020_06_09_191943) do t.index ["user_id"], name: "index_user_optional_fields_on_user_id" end + create_table "user_subscriptions", force: :cascade do |t| + t.bigint "author_id", null: false + t.datetime "created_at", precision: 6, null: false + t.bigint "subscriber_id", null: false + t.datetime "updated_at", precision: 6, null: false + t.bigint "user_subscription_sourceable_id", null: false + t.string "user_subscription_sourceable_type", null: false + t.index ["author_id"], name: "index_user_subscriptions_on_author_id" + t.index ["subscriber_id", "user_subscription_sourceable_id", "user_subscription_sourceable_type"], name: "index_on_subscriber_id_user_subscription_sourceable_type_and_id", unique: true + t.index ["subscriber_id"], name: "index_user_subscriptions_on_subscriber_id" + t.index ["user_subscription_sourceable_type", "user_subscription_sourceable_id"], name: "index_on_user_subscription_sourcebable_type_and_id" + end + create_table "users", id: :serial, force: :cascade do |t| t.integer "articles_count", default: 0, null: false t.string "available_for" @@ -1326,6 +1339,8 @@ ActiveRecord::Schema.define(version: 2020_06_09_191943) do add_foreign_key "user_blocks", "users", column: "blocker_id" add_foreign_key "user_counters", "users", on_delete: :cascade add_foreign_key "user_optional_fields", "users" + add_foreign_key "user_subscriptions", "users", column: "author_id" + add_foreign_key "user_subscriptions", "users", column: "subscriber_id" add_foreign_key "users_roles", "users", on_delete: :cascade add_foreign_key "webhook_endpoints", "oauth_applications" add_foreign_key "webhook_endpoints", "users" diff --git a/spec/factories/user_subscription.rb b/spec/factories/user_subscription.rb new file mode 100644 index 000000000..2cebe5a1b --- /dev/null +++ b/spec/factories/user_subscription.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :user_subscription do + association :subscriber, factory: :user, strategy: :create + association :user_subscription_sourceable, factory: :article + + author { user_subscription_sourceable.user } + end +end diff --git a/spec/models/user_subscription_spec.rb b/spec/models/user_subscription_spec.rb new file mode 100644 index 000000000..6ffe5e065 --- /dev/null +++ b/spec/models/user_subscription_spec.rb @@ -0,0 +1,14 @@ +require "rails_helper" + +RSpec.describe UserSubscription, type: :model do + subject { build(:user_subscription) } + + describe "validations" do + it { is_expected.to validate_presence_of(:user_subscription_sourceable_id) } + it { is_expected.to validate_presence_of(:user_subscription_sourceable_type) } + it { is_expected.to validate_presence_of(:subscriber_id) } + it { is_expected.to validate_presence_of(:author_id) } + it { is_expected.to validate_inclusion_of(:user_subscription_sourceable_type).in_array(%w[Article]) } + it { is_expected.to validate_uniqueness_of(:subscriber_id).scoped_to(:user_subscription_sourceable_type, :user_subscription_sourceable_id) } + end +end