Add profile directory (#9427)

Fix #5578
th-downstream
Eugen Rochko 6 years ago committed by GitHub
parent 6d73528424
commit fd8ca11f19

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Admin
class TagsController < BaseController
before_action :set_tags, only: :index
before_action :set_tag, except: :index
before_action :set_filter_params
def index
authorize :tag, :index?
end
def hide
authorize @tag, :hide?
@tag.account_tag_stat.update!(hidden: true)
redirect_to admin_tags_path(@filter_params)
end
def unhide
authorize @tag, :unhide?
@tag.account_tag_stat.update!(hidden: true)
redirect_to admin_tags_path(@filter_params)
end
private
def set_tags
@tags = Tag.discoverable
@tags.merge!(Tag.hidden) if filter_params[:hidden]
end
def set_tag
@tag = Tag.find(params[:id])
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
end
def filter_params
params.permit(:hidden)
end
end
end

@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
private
def account_params
params.permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value])
params.permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
end
def user_settings_params

@ -0,0 +1,48 @@
# frozen_string_literal: true
class DirectoriesController < ApplicationController
layout 'public'
before_action :set_instance_presenter
before_action :set_tag, only: :show
before_action :set_tags
before_action :set_accounts
def index
render :index
end
def show
render :index
end
private
def set_tag
@tag = Tag.discoverable.find_by!(name: params[:id].downcase)
end
def set_tags
@tags = Tag.discoverable.limit(30)
end
def set_accounts
@accounts = Account.searchable.discoverable.page(params[:page]).per(50).tap do |query|
query.merge!(Account.tagged_with(@tag.id)) if @tag
if popular_requested?
query.merge!(Account.popular)
else
query.merge!(Account.by_recent_status)
end
end
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def popular_requested?
request.path.ends_with?('/popular')
end
end

@ -29,7 +29,7 @@ class Settings::ProfilesController < ApplicationController
private
def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value])
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
end
def set_account

@ -5,8 +5,9 @@ module Admin::FilterHelper
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
TAGS_FILTERS = %i(hidden).freeze
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
new_url = filtered_url_for(link_to_params)

@ -189,6 +189,11 @@
&--under-tabs {
border-radius: 0 0 4px 4px;
}
&--flexible {
box-sizing: border-box;
min-height: 100%;
}
}
.account-role {

@ -240,3 +240,168 @@
border-radius: 0;
}
}
.page-header {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
padding: 60px 15px;
text-align: center;
margin: 10px 0;
h1 {
color: $primary-text-color;
font-size: 36px;
line-height: 1.1;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 15px;
color: $darker-text-color;
}
}
.directory {
background: $ui-base-color;
border-radius: 0 0 4px 4px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&__tag {
box-sizing: border-box;
margin-bottom: 10px;
a {
display: flex;
align-items: center;
justify-content: space-between;
background: $ui-base-color;
border-radius: 4px;
padding: 15px;
text-decoration: none;
color: inherit;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&:hover,
&:active,
&:focus {
background: lighten($ui-base-color, 8%);
}
}
&.active a {
background: $ui-highlight-color;
cursor: default;
}
h4 {
flex: 1 1 auto;
font-size: 18px;
font-weight: 700;
color: $primary-text-color;
.fa {
color: $darker-text-color;
}
small {
display: block;
font-weight: 400;
font-size: 15px;
margin-top: 8px;
color: $darker-text-color;
}
}
&.active h4 {
&,
.fa,
small {
color: $primary-text-color;
}
}
.avatar-stack {
flex: 0 0 auto;
width: (36px + 4px) * 3;
}
&.active .avatar-stack .account__avatar {
border-color: $ui-highlight-color;
}
}
}
.avatar-stack {
display: flex;
justify-content: flex-end;
.account__avatar {
flex: 0 0 auto;
width: 36px;
height: 36px;
border-radius: 50%;
position: relative;
margin-left: -10px;
border: 2px solid $ui-base-color;
&:nth-child(1) {
z-index: 1;
}
&:nth-child(2) {
z-index: 2;
}
&:nth-child(3) {
z-index: 3;
}
}
}
.accounts-table {
width: 100%;
.account {
padding: 0;
border: 0;
}
thead th {
text-align: center;
text-transform: uppercase;
color: $darker-text-color;
font-weight: 700;
padding: 10px;
&:first-child {
text-align: left;
}
}
tbody td {
padding: 15px 0;
vertical-align: middle;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
tbody tr:last-child td {
border-bottom: 0;
}
&__count {
width: 120px;
text-align: center;
font-size: 15px;
font-weight: 500;
color: $primary-text-color;
small {
display: block;
color: $darker-text-color;
font-weight: 400;
font-size: 14px;
}
}
}

@ -43,11 +43,13 @@
# featured_collection_url :string
# fields :jsonb
# actor_type :string
# discoverable :boolean
#
class Account < ApplicationRecord
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
MIN_FOLLOWERS_DISCOVERY = 10
include AccountAssociations
include AccountAvatar
@ -89,6 +91,10 @@ class Account < ApplicationRecord
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :searchable, -> { where(suspended: false).where(moved_to_account_id: nil) }
scope :discoverable, -> { where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) }
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_recent_status, -> { order('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc') }
delegate :email,
:unconfirmed_email,
@ -174,6 +180,40 @@ class Account < ApplicationRecord
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
end
def tags_as_strings=(tag_names)
tag_names.map! { |name| name.mb_chars.downcase }
tag_names.uniq!(&:to_s)
# Existing hashtags
hashtags_map = Tag.where(name: tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag }
# Initialize not yet existing hashtags
tag_names.each do |name|
next if hashtags_map.key?(name)
hashtags_map[name.downcase] = Tag.new(name: name)
end
# Remove hashtags that are to be deleted
tags.each do |tag|
if hashtags_map.key?(tag.name)
hashtags_map.delete(tag.name)
else
transaction do
tags.delete(tag)
tag.decrement_count!(:accounts_count)
end
end
end
# Add hashtags that were so far missing
hashtags_map.each_value do |tag|
transaction do
tags << tag
tag.increment_count!(:accounts_count)
end
end
end
def fields
(self[:fields] || []).map { |f| Field.new(self, f) }
end

@ -1,5 +1,4 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_stats
@ -11,16 +10,25 @@
# followers_count :bigint(8) default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
# last_status_at :datetime
#
class AccountStat < ApplicationRecord
belongs_to :account, inverse_of: :account_stat
def increment_count!(key)
update(key => public_send(key) + 1)
update(attributes_for_increment(key))
end
def decrement_count!(key)
update(key => [public_send(key) - 1, 0].max)
end
private
def attributes_for_increment(key)
attrs = { key => public_send(key) + 1 }
attrs[:last_status_at] = Time.now.utc if key == :statuses_count
attrs
end
end

@ -0,0 +1,24 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_tag_stats
#
# id :bigint(8) not null, primary key
# tag_id :bigint(8) not null
# accounts_count :bigint(8) default(0), not null
# hidden :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class AccountTagStat < ApplicationRecord
belongs_to :tag, inverse_of: :account_tag_stat
def increment_count!(key)
update(key => public_send(key) + 1)
end
def decrement_count!(key)
update(key => [public_send(key) - 1, 0].max)
end
end

@ -49,5 +49,8 @@ module AccountAssociations
# Account migrations
belongs_to :moved_to_account, class_name: 'Account', optional: true
# Hashtags
has_and_belongs_to_many :tags
end
end

@ -16,6 +16,7 @@ module AccountCounters
:followers_count=,
:increment_count!,
:decrement_count!,
:last_status_at,
to: :account_stat
def account_stat

@ -11,12 +11,31 @@
class Tag < ApplicationRecord
has_and_belongs_to_many :statuses
has_and_belongs_to_many :accounts
has_one :account_tag_stat, dependent: :destroy
HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*'
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i }
scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(name: :asc) }
scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
delegate :accounts_count,
:accounts_count=,
:increment_count!,
:decrement_count!,
:hidden?,
to: :account_tag_stat
after_save :save_account_tag_stat
def account_tag_stat
super || build_account_tag_stat
end
def to_param
name
end
@ -43,4 +62,11 @@ class Tag < ApplicationRecord
Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
end
end
private
def save_account_tag_stat
return unless account_tag_stat&.changed?
account_tag_stat.save
end
end

@ -0,0 +1,15 @@
# frozen_string_literal: true
class TagPolicy < ApplicationPolicy
def index?
staff?
end
def hide?
staff?
end
def unhide?
staff?
end
end

@ -10,6 +10,7 @@ class UpdateAccountService < BaseService
authorize_all_follow_requests(account) if was_locked && !account.locked
check_links(account)
process_hashtags(account)
end
end
@ -24,4 +25,8 @@ class UpdateAccountService < BaseService
def check_links(account)
VerifyAccountLinksWorker.perform_async(account.id)
end
def process_hashtags(account)
account.tags_as_strings = Extractor.extract_hashtags(account.note)
end
end

@ -0,0 +1,12 @@
%tr
%td
= link_to explore_hashtag_path(tag) do
= fa_icon 'hashtag'
= tag.name
%td
= t('directories.people', count: tag.accounts_count)
%td
- if tag.hidden?
= table_link_to 'eye', t('admin.tags.unhide'), unhide_admin_tag_path(tag.id, **@filter_params), method: :post
- else
= table_link_to 'eye-slash', t('admin.tags.hide'), hide_admin_tag_path(tag.id, **@filter_params), method: :post

@ -0,0 +1,19 @@
- content_for :page_title do
= t('admin.tags.title')
.filters
.filter-subset
%strong= t('admin.reports.status')
%ul
%li= filter_link_to t('admin.tags.visible'), hidden: nil
%li= filter_link_to t('admin.tags.hidden'), hidden: '1'
.table-wrapper
%table.table
%thead
%tr
%th= t('admin.tags.name')
%th= t('admin.tags.accounts')
%th
%tbody
= render @tags

@ -0,0 +1,59 @@
- content_for :page_title do
= t('directories.explore_mastodon')
- content_for :header_tags do
%meta{ name: 'description', content: t('directories.explanation') }
= opengraph 'og:site_name', site_title
= opengraph 'og:title', t('directories.explore_mastodon', title: site_title)
= opengraph 'og:description', t('directories.explanation')
.page-header
%h1= t('directories.explore_mastodon', title: site_title)
%p= t('directories.explanation')
.grid
.column-0
.account__section-headline
= active_link_to t('directories.most_recently_active'), @tag ? explore_hashtag_path(@tag) : explore_path
= active_link_to t('directories.most_popular'), @tag ? explore_hashtag_popular_path(@tag) : explore_popular_path
- if @accounts.empty?
= nothing_here
- else
.directory
%table.accounts-table
%tbody
- @accounts.each do |account|
%tr
%td= account_link_to account
%td.accounts-table__count
= number_to_human account.statuses_count, strip_insignificant_zeros: true
%small= t('accounts.posts', count: account.statuses_count)
%td.accounts-table__count
= number_to_human account.followers_count, strip_insignificant_zeros: true
%small= t('accounts.followers', count: account.followers_count)
%td.accounts-table__count
- if account.last_status_at.present?
%time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
- else
\-
%small= t('accounts.last_active')
= paginate @accounts
.column-1
- if @tags.empty?
.nothing-here.nothing-here--flexible
- else
- @tags.each do |tag|
.directory__tag{ class: tag.id == @tag&.id ? 'active' : nil }
= link_to explore_hashtag_path(tag) do
%h4
= fa_icon 'hashtag'
= tag.name
%small= t('directories.people', count: tag.accounts_count)
.avatar-stack
- tag.accounts.limit(3).each do |account|
= image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar'

@ -8,6 +8,10 @@
.nav-left
= link_to root_url, class: 'brand' do
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
= link_to t('directories.directory'), explore_path, class: 'nav-link'
= link_to t('about.about_this'), about_more_path, class: 'nav-link'
= link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link'
.nav-center
.nav-right
- if user_signed_in?

@ -18,7 +18,6 @@
= f.input :avatar, wrapper: :with_label, input_html: { accept: AccountAvatar::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.avatar', dimensions: '400x400', size: number_to_human_size(AccountAvatar::LIMIT))
%hr.spacer/
.fields-group
@ -27,6 +26,9 @@
.fields-group
= f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')
.fields-group
= f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable_html', min_followers: Account::MIN_FOLLOWERS_DISCOVERY, path: explore_path)
%hr.spacer/
.fields-row

@ -48,6 +48,7 @@ en:
other: Followers
following: Following
joined: Joined %{date}
last_active: last active
link_verified_on: Ownership of this link was checked on %{date}
media: Media
moved_html: "%{name} has moved to %{new_profile_link}:"
@ -114,6 +115,7 @@ en:
media_attachments: Media attachments
memorialize: Turn into memoriam
moderation:
active: Active
all: All
silenced: Silenced
suspended: Suspended
@ -439,6 +441,14 @@ en:
proceed: Proceed
title: Suspend %{acct}
warning_html: 'Suspending this account will <strong>irreversibly</strong> delete data from this account, which includes:'
tags:
accounts: Accounts
hidden: Hidden
hide: Hide from directory
name: Hashtag
title: Hashtags
unhide: Show in directory
visible: Visible
title: Administration
admin_mailer:
new_report:
@ -517,6 +527,15 @@ en:
success_msg: Your account was successfully deleted
warning_html: Only deletion of content from this particular instance is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases.
warning_title: Disseminated content availability
directories:
directory: Profile directory
explanation: Discover users based on their interests
explore_mastodon: Explore %{title}
most_popular: Most popular
most_recently_active: Most recently active
people:
one: "%{count} person"
other: "%{count} people"
errors:
'403': You don't have permission to view this page.
'404': The page you were looking for doesn't exist.

@ -8,6 +8,7 @@ en:
bot: This account mainly performs automated actions and might not be monitored
context: One or multiple contexts where the filter should apply
digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
discoverable_html: The <a href="%{path}" target="_blank">directory</a> lets people find accounts based on interests and activity. Requires at least %{min_followers} followers
email: You will be sent a confirmation e-mail
fields: You can have up to 4 items displayed as a table on your profile
header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
@ -48,6 +49,7 @@ en:
context: Filter contexts
current_password: Current password
data: Data
discoverable: List this account on the directory
display_name: Display name
email: E-mail address
expires_in: Expire after

@ -28,6 +28,7 @@ SimpleNavigation::Configuration.run do |navigation|
admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts}
admin.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
admin.item :tags, safe_join([fa_icon('tag fw'), t('admin.tags.title')]), admin_tags_path
admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances}, if: -> { current_user.admin? }
admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}, if: -> { current_user.admin? }
admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? }

@ -80,6 +80,11 @@ Rails.application.routes.draw do
get '/interact/:id', to: 'remote_interaction#new', as: :remote_interaction
post '/interact/:id', to: 'remote_interaction#create'
get '/explore', to: 'directories#index', as: :explore
get '/explore/popular', to: 'directories#index', as: :explore_popular
get '/explore/:id', to: 'directories#show', as: :explore_hashtag
get '/explore/:id/popular', to: 'directories#show', as: :explore_hashtag_popular
namespace :settings do
resource :profile, only: [:show, :update]
resource :preferences, only: [:show, :update]
@ -207,6 +212,13 @@ Rails.application.routes.draw do
end
resources :account_moderation_notes, only: [:create, :destroy]
resources :tags, only: [:index] do
member do
post :hide
post :unhide
end
end
end
get '/admin', to: redirect('/admin/dashboard', status: 302)

@ -0,0 +1,8 @@
class CreateAccountsTagsJoinTable < ActiveRecord::Migration[5.2]
def change
create_join_table :accounts, :tags do |t|
t.index [:account_id, :tag_id]
t.index [:tag_id, :account_id], unique: true
end
end
end

@ -0,0 +1,5 @@
class AddDiscoverableToAccounts < ActiveRecord::Migration[5.2]
def change
add_column :accounts, :discoverable, :boolean
end
end

@ -0,0 +1,5 @@
class AddLastStatusAtToAccountStats < ActiveRecord::Migration[5.2]
def change
add_column :account_stats, :last_status_at, :datetime
end
end

@ -0,0 +1,11 @@
class CreateAccountTagStats < ActiveRecord::Migration[5.2]
def change
create_table :account_tag_stats do |t|
t.belongs_to :tag, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
t.bigint :accounts_count, default: 0, null: false
t.boolean :hidden, default: false, null: false
t.timestamps
end
end
end

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_11_27_130500) do
ActiveRecord::Schema.define(version: 2018_12_04_215309) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -63,9 +63,19 @@ ActiveRecord::Schema.define(version: 2018_11_27_130500) do
t.bigint "followers_count", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "last_status_at"
t.index ["account_id"], name: "index_account_stats_on_account_id", unique: true
end
create_table "account_tag_stats", force: :cascade do |t|
t.bigint "tag_id", null: false
t.bigint "accounts_count", default: 0, null: false
t.boolean "hidden", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["tag_id"], name: "index_account_tag_stats_on_tag_id", unique: true
end
create_table "accounts", force: :cascade do |t|
t.string "username", default: "", null: false
t.string "domain"
@ -106,6 +116,7 @@ ActiveRecord::Schema.define(version: 2018_11_27_130500) do
t.string "featured_collection_url"
t.jsonb "fields"
t.string "actor_type"
t.boolean "discoverable"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"
@ -113,6 +124,13 @@ ActiveRecord::Schema.define(version: 2018_11_27_130500) do
t.index ["url"], name: "index_accounts_on_url"
end
create_table "accounts_tags", id: false, force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "tag_id", null: false
t.index ["account_id", "tag_id"], name: "index_accounts_tags_on_account_id_and_tag_id"
t.index ["tag_id", "account_id"], name: "index_accounts_tags_on_tag_id_and_account_id", unique: true
end
create_table "admin_action_logs", force: :cascade do |t|
t.bigint "account_id"
t.string "action", default: "", null: false
@ -637,6 +655,7 @@ ActiveRecord::Schema.define(version: 2018_11_27_130500) do
add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade
add_foreign_key "account_pins", "accounts", on_delete: :cascade
add_foreign_key "account_stats", "accounts", on_delete: :cascade
add_foreign_key "account_tag_stats", "tags", on_delete: :cascade
add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
add_foreign_key "backups", "users", on_delete: :nullify

@ -0,0 +1,3 @@
Fabricator(:account_tag_stat) do
accounts_count ""
end

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe AccountTagStat, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end
Loading…
Cancel
Save