Merge remote-tracking branch 'parent/main' into upstream-20231204
This commit is contained in:
commit
94c2396a34
179 changed files with 1036 additions and 775 deletions
|
@ -74,21 +74,22 @@ class Account < ApplicationRecord
|
|||
URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+}
|
||||
USERNAME_ONLY_RE = /\A#{USERNAME_RE}\z/i
|
||||
|
||||
include Attachmentable
|
||||
include AccountAssociations
|
||||
include AccountAvatar
|
||||
include AccountFinderConcern
|
||||
include AccountHeader
|
||||
include AccountInteractions
|
||||
include Paginable
|
||||
include AccountCounters
|
||||
include DomainNormalizable
|
||||
include Attachmentable # Load prior to Avatar & Header concerns
|
||||
|
||||
include Account::Associations
|
||||
include Account::Avatar
|
||||
include Account::Counters
|
||||
include Account::FinderConcern
|
||||
include Account::Header
|
||||
include Account::Interactions
|
||||
include Account::Merging
|
||||
include Account::Search
|
||||
include Account::StatusesSearch
|
||||
include Account::OtherSettings
|
||||
include Account::MasterSettings
|
||||
include DomainMaterializable
|
||||
include AccountMerging
|
||||
include AccountSearch
|
||||
include AccountStatusesSearch
|
||||
include AccountOtherSettings
|
||||
include AccountMasterSettings
|
||||
include DomainNormalizable
|
||||
include Paginable
|
||||
|
||||
enum protocol: { ostatus: 0, activitypub: 1 }
|
||||
enum suspension_origin: { local: 0, remote: 1 }, _prefix: true
|
||||
|
@ -273,6 +274,9 @@ class Account < ApplicationRecord
|
|||
suspended? && deletion_request.present?
|
||||
end
|
||||
|
||||
alias unavailable? suspended?
|
||||
alias permanently_unavailable? suspended_permanently?
|
||||
|
||||
def suspend!(date: Time.now.utc, origin: :local, block_email: true)
|
||||
transaction do
|
||||
create_deletion_request!
|
||||
|
|
|
@ -24,12 +24,12 @@ class Admin::ActionLog < ApplicationRecord
|
|||
belongs_to :account
|
||||
belongs_to :target, polymorphic: true, optional: true
|
||||
|
||||
default_scope -> { order('id desc') }
|
||||
|
||||
before_validation :set_human_identifier
|
||||
before_validation :set_route_param
|
||||
before_validation :set_permalink
|
||||
|
||||
scope :latest, -> { order(id: :desc) }
|
||||
|
||||
def action
|
||||
super.to_sym
|
||||
end
|
||||
|
|
|
@ -72,7 +72,7 @@ class Admin::ActionLogFilter
|
|||
end
|
||||
|
||||
def results
|
||||
scope = Admin::ActionLog.includes(:target)
|
||||
scope = latest_action_logs.includes(:target)
|
||||
|
||||
params.each do |key, value|
|
||||
next if key.to_s == 'page'
|
||||
|
@ -88,14 +88,18 @@ class Admin::ActionLogFilter
|
|||
def scope_for(key, value)
|
||||
case key
|
||||
when 'action_type'
|
||||
Admin::ActionLog.where(ACTION_TYPE_MAP[value.to_sym])
|
||||
latest_action_logs.where(ACTION_TYPE_MAP[value.to_sym])
|
||||
when 'account_id'
|
||||
Admin::ActionLog.where(account_id: value)
|
||||
latest_action_logs.where(account_id: value)
|
||||
when 'target_account_id'
|
||||
account = Account.find_or_initialize_by(id: value)
|
||||
Admin::ActionLog.where(target: [account, account.user].compact)
|
||||
latest_action_logs.where(target: [account, account.user].compact)
|
||||
else
|
||||
raise Mastodon::InvalidParameterError, "Unknown filter: #{key}"
|
||||
end
|
||||
end
|
||||
|
||||
def latest_action_logs
|
||||
Admin::ActionLog.latest
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ class Admin::StatusFilter
|
|||
def scope_for(key, _value)
|
||||
case key.to_s
|
||||
when 'media'
|
||||
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id).reorder('statuses.id desc')
|
||||
Status.joins(:media_attachments).merge(@account.media_attachments).group(:id).reorder('statuses.id desc')
|
||||
else
|
||||
raise Mastodon::InvalidParameterError, "Unknown filter: #{key}"
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountAssociations
|
||||
module Account::Associations
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountAvatar
|
||||
module Account::Avatar
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
|
|
@ -1,12 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountCounters
|
||||
module Account::Counters
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count).freeze
|
||||
|
||||
included do
|
||||
has_one :account_stat, inverse_of: :account
|
||||
has_one :account_stat, inverse_of: :account, dependent: nil
|
||||
after_save :save_account_stat
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountFinderConcern
|
||||
module Account::FinderConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountHeader
|
||||
module Account::Header
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountInteractions
|
||||
module Account::Interactions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountMasterSettings
|
||||
module Account::MasterSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountMerging
|
||||
module Account::Merging
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def merge_with!(other_account)
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountOtherSettings
|
||||
module Account::OtherSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountSearch
|
||||
module Account::Search
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountStatusesSearch
|
||||
module Account::StatusesSearch
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
|
@ -5,7 +5,7 @@ module Remotable
|
|||
|
||||
class_methods do
|
||||
def remotable_attachment(attachment_name, limit, suppress_errors: true, download_on_assign: true, attribute_name: nil)
|
||||
attribute_name ||= "#{attachment_name}_remote_url".to_sym
|
||||
attribute_name ||= :"#{attachment_name}_remote_url"
|
||||
|
||||
define_method("download_#{attachment_name}!") do |url = nil|
|
||||
url ||= self[attribute_name]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StatusSafeReblogInsert
|
||||
module Status::SafeReblogInsert
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StatusSearchConcern
|
||||
module Status::SearchConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StatusSnapshotConcern
|
||||
module Status::SnapshotConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StatusThreadingConcern
|
||||
module Status::ThreadingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def ancestors(limit, account = nil)
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module HasUserSettings
|
||||
module User::HasSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module LdapAuthenticable
|
||||
module User::LdapAuthenticable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Omniauthable
|
||||
module User::Omniauthable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
TEMP_EMAIL_PREFIX = 'change@me'
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PamAuthenticable
|
||||
module User::PamAuthenticable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
|
@ -15,7 +15,7 @@
|
|||
class Conversation < ApplicationRecord
|
||||
validates :uri, uniqueness: true, if: :uri?
|
||||
|
||||
has_many :statuses
|
||||
has_many :statuses, dependent: nil
|
||||
belongs_to :ancestor_status, class_name: 'Status', inverse_of: :owned_conversation, optional: true
|
||||
|
||||
def local?
|
||||
|
|
|
@ -39,7 +39,7 @@ class CustomEmoji < ApplicationRecord
|
|||
IMAGE_MIME_TYPES = %w(image/png image/gif image/webp image/jpeg).freeze
|
||||
|
||||
belongs_to :category, class_name: 'CustomEmojiCategory', optional: true
|
||||
has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode, inverse_of: false
|
||||
has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode, inverse_of: false, dependent: nil
|
||||
has_many :emoji_reactions, inverse_of: :custom_emoji, dependent: :destroy
|
||||
|
||||
has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' } }, validate_media_type: false
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
|
||||
class CustomEmojiCategory < ApplicationRecord
|
||||
has_many :emojis, class_name: 'CustomEmoji', foreign_key: 'category_id', inverse_of: :category
|
||||
has_many :emojis, class_name: 'CustomEmoji', foreign_key: 'category_id', inverse_of: :category, dependent: nil
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
end
|
||||
|
|
|
@ -40,7 +40,7 @@ class DomainBlock < ApplicationRecord
|
|||
|
||||
validates :domain, presence: true, uniqueness: true, domain: true
|
||||
|
||||
has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false
|
||||
has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false, dependent: nil
|
||||
delegate :count, to: :accounts, prefix: true
|
||||
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
|
|
|
@ -20,7 +20,7 @@ class Invite < ApplicationRecord
|
|||
include Expireable
|
||||
|
||||
belongs_to :user, inverse_of: :invites
|
||||
has_many :users, inverse_of: :invite
|
||||
has_many :users, inverse_of: :invite, dependent: nil
|
||||
|
||||
scope :available, -> { where(expires_at: nil).or(where('expires_at >= ?', Time.now.utc)) }
|
||||
|
||||
|
|
|
@ -210,13 +210,11 @@ class MediaAttachment < ApplicationRecord
|
|||
validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? }
|
||||
|
||||
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
|
||||
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
|
||||
scope :local, -> { where(remote_url: '') }
|
||||
scope :remote, -> { where.not(remote_url: '') }
|
||||
scope :cached, -> { remote.where.not(file_file_name: nil) }
|
||||
scope :local_attached, -> { attached.where(remote_url: '') }
|
||||
|
||||
default_scope { order(id: :asc) }
|
||||
scope :local, -> { where(remote_url: '') }
|
||||
scope :ordered, -> { order(id: :asc) }
|
||||
scope :remote, -> { where.not(remote_url: '') }
|
||||
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
|
||||
|
||||
attr_accessor :skip_download
|
||||
|
||||
|
|
|
@ -131,25 +131,25 @@ class Report < ApplicationRecord
|
|||
Admin::ActionLog.where(
|
||||
target_type: 'Report',
|
||||
target_id: id
|
||||
).unscope(:order).arel,
|
||||
).arel,
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Account',
|
||||
target_id: target_account_id
|
||||
).unscope(:order).arel,
|
||||
).arel,
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Status',
|
||||
target_id: status_ids
|
||||
).unscope(:order).arel,
|
||||
).arel,
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'AccountWarning',
|
||||
target_id: AccountWarning.where(report_id: id).select(:id)
|
||||
).unscope(:order).arel,
|
||||
).arel,
|
||||
].reduce { |union, query| Arel::Nodes::UnionAll.new(union, query) }
|
||||
|
||||
Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
|
||||
Admin::ActionLog.latest.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -36,14 +36,14 @@
|
|||
require 'ostruct'
|
||||
|
||||
class Status < ApplicationRecord
|
||||
include Cacheable
|
||||
include Discard::Model
|
||||
include Paginable
|
||||
include Cacheable
|
||||
include StatusThreadingConcern
|
||||
include StatusSnapshotConcern
|
||||
include RateLimitable
|
||||
include StatusSafeReblogInsert
|
||||
include StatusSearchConcern
|
||||
include Status::SafeReblogInsert
|
||||
include Status::SearchConcern
|
||||
include Status::SnapshotConcern
|
||||
include Status::ThreadingConcern
|
||||
include DtlHelper
|
||||
|
||||
rate_limit by: :account, family: :statuses
|
||||
|
@ -78,13 +78,11 @@ class Status < ApplicationRecord
|
|||
has_many :bookmarks, inverse_of: :status, dependent: :destroy
|
||||
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
|
||||
has_many :reblogged_by_accounts, through: :reblogs, class_name: 'Account', source: :account
|
||||
has_many :quotes, foreign_key: 'quote_of_id', class_name: 'Status', inverse_of: :quote
|
||||
has_many :quotes, foreign_key: 'quote_of_id', class_name: 'Status', inverse_of: :quote, dependent: nil
|
||||
has_many :quoted_by_accounts, through: :quotes, class_name: 'Account', source: :account
|
||||
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
|
||||
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread, dependent: nil
|
||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
|
||||
has_many :silent_mentions, -> { silent }, class_name: 'Mention', inverse_of: :status
|
||||
has_many :media_attachments, dependent: :nullify
|
||||
has_many :reference_objects, class_name: 'StatusReference', inverse_of: :status, dependent: :destroy
|
||||
has_many :references, through: :reference_objects, class_name: 'Status', source: :target_status
|
||||
|
@ -95,6 +93,10 @@ class Status < ApplicationRecord
|
|||
has_many :bookmark_categories, class_name: 'BookmarkCategory', through: :bookmark_category_relationships, source: :bookmark_category
|
||||
has_many :joined_bookmark_categories, class_name: 'BookmarkCategory', through: :bookmark_category_relationships, source: :bookmark_category
|
||||
|
||||
# The `dependent` option is enabled by the initial `mentions` association declaration
|
||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
|
||||
has_many :silent_mentions, -> { silent }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
|
||||
|
||||
# Those associations are used for the private search index
|
||||
has_many :local_mentioned, -> { merge(Account.local) }, through: :active_mentions, source: :account
|
||||
has_many :local_favorited, -> { merge(Account.local) }, through: :favourites, source: :account
|
||||
|
@ -106,11 +108,13 @@ class Status < ApplicationRecord
|
|||
|
||||
has_and_belongs_to_many :tags
|
||||
|
||||
has_one :preview_cards_status, inverse_of: :status # Because of a composite primary key, the dependent option cannot be used
|
||||
# Because of a composite primary key, the `dependent` option cannot be used on this association
|
||||
has_one :preview_cards_status, inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
|
||||
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
has_one :status_stat, inverse_of: :status
|
||||
has_one :status_stat, inverse_of: :status, dependent: nil
|
||||
has_one :poll, inverse_of: :status, dependent: :destroy
|
||||
has_one :trend, class_name: 'StatusTrend', inverse_of: :status
|
||||
has_one :trend, class_name: 'StatusTrend', inverse_of: :status, dependent: nil
|
||||
has_one :scheduled_expiration_status, inverse_of: :status, dependent: :destroy
|
||||
has_one :circle_status, inverse_of: :status, dependent: :destroy
|
||||
has_many :list_status, inverse_of: :status, dependent: :destroy
|
||||
|
|
|
@ -37,7 +37,7 @@ class Trends::History
|
|||
end
|
||||
|
||||
def uses
|
||||
with_redis { |redis| redis.get(key_for(:uses))&.to_i || 0 }
|
||||
with_redis { |redis| redis.get(key_for(:uses)).to_i }
|
||||
end
|
||||
|
||||
def add(account_id)
|
||||
|
|
|
@ -53,9 +53,12 @@ class User < ApplicationRecord
|
|||
filtered_languages
|
||||
)
|
||||
|
||||
include Redisable
|
||||
include LanguagesHelper
|
||||
include HasUserSettings
|
||||
include Redisable
|
||||
include User::HasSettings
|
||||
include User::LdapAuthenticable
|
||||
include User::Omniauthable
|
||||
include User::PamAuthenticable
|
||||
|
||||
# The home and list feeds will be stored in Redis for this amount
|
||||
# of time, and status fan-out to followers will include only people
|
||||
|
@ -77,22 +80,18 @@ class User < ApplicationRecord
|
|||
devise :registerable, :recoverable, :validatable,
|
||||
:confirmable
|
||||
|
||||
include Omniauthable
|
||||
include PamAuthenticable
|
||||
include LdapAuthenticable
|
||||
|
||||
belongs_to :account, inverse_of: :user
|
||||
belongs_to :invite, counter_cache: :uses, optional: true
|
||||
belongs_to :created_by_application, class_name: 'Doorkeeper::Application', optional: true
|
||||
belongs_to :role, class_name: 'UserRole', optional: true
|
||||
accepts_nested_attributes_for :account
|
||||
|
||||
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
|
||||
has_many :backups, inverse_of: :user
|
||||
has_many :invites, inverse_of: :user
|
||||
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: nil
|
||||
has_many :backups, inverse_of: :user, dependent: nil
|
||||
has_many :invites, inverse_of: :user, dependent: nil
|
||||
has_many :markers, inverse_of: :user, dependent: :destroy
|
||||
has_many :webauthn_credentials, dependent: :destroy
|
||||
has_many :ips, class_name: 'UserIp', inverse_of: :user
|
||||
has_many :ips, class_name: 'UserIp', inverse_of: :user, dependent: nil
|
||||
|
||||
has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy
|
||||
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text }
|
||||
|
@ -252,7 +251,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def functional_or_moved?
|
||||
confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial?
|
||||
confirmed? && approved? && !disabled? && !account.unavailable? && !account.memorial?
|
||||
end
|
||||
|
||||
def unconfirmed?
|
||||
|
|
|
@ -51,7 +51,7 @@ class UserRole < ApplicationRecord
|
|||
invite_users
|
||||
).freeze,
|
||||
|
||||
moderation: %w(
|
||||
moderation: %i(
|
||||
view_dashboard
|
||||
view_audit_log
|
||||
manage_users
|
||||
|
@ -67,7 +67,7 @@ class UserRole < ApplicationRecord
|
|||
manage_sensitive_words
|
||||
).freeze,
|
||||
|
||||
administration: %w(
|
||||
administration: %i(
|
||||
manage_settings
|
||||
manage_rules
|
||||
manage_roles
|
||||
|
@ -76,7 +76,7 @@ class UserRole < ApplicationRecord
|
|||
manage_announcements
|
||||
).freeze,
|
||||
|
||||
devops: %w(
|
||||
devops: %i(
|
||||
view_devops
|
||||
).freeze,
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ class UserSettings::Setting
|
|||
|
||||
def key
|
||||
if namespace
|
||||
"#{namespace}.#{name}".to_sym
|
||||
:"#{namespace}.#{name}"
|
||||
else
|
||||
name
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ class Web::PushSubscription < ApplicationRecord
|
|||
belongs_to :user, optional: true
|
||||
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', optional: true
|
||||
|
||||
has_one :session_activation, foreign_key: 'web_push_subscription_id', inverse_of: :web_push_subscription
|
||||
has_one :session_activation, foreign_key: 'web_push_subscription_id', inverse_of: :web_push_subscription, dependent: nil
|
||||
|
||||
validates :endpoint, presence: true
|
||||
validates :key_p256dh, presence: true
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue