Add featured hashtags to profiles (#9755)
* Add hashtag filter to profiles GET /@:username/tagged/:hashtag GET /api/v1/accounts/:id/statuses?tagged=:hashtag * Display featured hashtags on public profile * Use separate model for featured tags * Update featured hashtag counters on-write * Limit featured tags to 10
This commit is contained in:
		| @@ -57,6 +57,7 @@ class AccountsController < ApplicationController | ||||
|  | ||||
|   def filtered_statuses | ||||
|     default_statuses.tap do |statuses| | ||||
|       statuses.merge!(hashtag_scope)    if tag_requested? | ||||
|       statuses.merge!(only_media_scope) if media_requested? | ||||
|       statuses.merge!(no_replies_scope) unless replies_requested? | ||||
|     end | ||||
| @@ -78,12 +79,15 @@ class AccountsController < ApplicationController | ||||
|     Status.without_replies | ||||
|   end | ||||
|  | ||||
|   def hashtag_scope | ||||
|     Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id) | ||||
|   end | ||||
|  | ||||
|   def set_account | ||||
|     @account = Account.find_local!(params[:username]) | ||||
|   end | ||||
|  | ||||
|   def older_url | ||||
|     ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}") | ||||
|     pagination_url(max_id: @statuses.last.id) | ||||
|   end | ||||
|  | ||||
| @@ -92,7 +96,9 @@ class AccountsController < ApplicationController | ||||
|   end | ||||
|  | ||||
|   def pagination_url(max_id: nil, min_id: nil) | ||||
|     if media_requested? | ||||
|     if tag_requested? | ||||
|       short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) | ||||
|     elsif media_requested? | ||||
|       short_account_media_url(@account, max_id: max_id, min_id: min_id) | ||||
|     elsif replies_requested? | ||||
|       short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) | ||||
| @@ -109,6 +115,10 @@ class AccountsController < ApplicationController | ||||
|     request.path.ends_with?('/with_replies') | ||||
|   end | ||||
|  | ||||
|   def tag_requested? | ||||
|     request.path.ends_with?("/tagged/#{params[:tag]}") | ||||
|   end | ||||
|  | ||||
|   def filtered_status_page(params) | ||||
|     if params[:min_id].present? | ||||
|       filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse | ||||
|   | ||||
| @@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | ||||
|     statuses.merge!(only_media_scope) if truthy_param?(:only_media) | ||||
|     statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) | ||||
|     statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) | ||||
|     statuses.merge!(hashtag_scope)    if params[:tagged].present? | ||||
|  | ||||
|     statuses | ||||
|   end | ||||
| @@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | ||||
|     Status.without_reblogs | ||||
|   end | ||||
|  | ||||
|   def hashtag_scope | ||||
|     Status.tagged_with(Tag.find_by(name: params[:tagged])&.id) | ||||
|   end | ||||
|  | ||||
|   def pagination_params(core_params) | ||||
|     params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) | ||||
|   end | ||||
|   | ||||
							
								
								
									
										51
									
								
								app/controllers/settings/featured_tags_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/controllers/settings/featured_tags_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Settings::FeaturedTagsController < Settings::BaseController | ||||
|   layout 'admin' | ||||
|  | ||||
|   before_action :authenticate_user! | ||||
|   before_action :set_featured_tags, only: :index | ||||
|   before_action :set_featured_tag, except: [:index, :create] | ||||
|   before_action :set_most_used_tags, only: :index | ||||
|  | ||||
|   def index | ||||
|     @featured_tag = FeaturedTag.new | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|     @featured_tag = current_account.featured_tags.new(featured_tag_params) | ||||
|     @featured_tag.reset_data | ||||
|  | ||||
|     if @featured_tag.save | ||||
|       redirect_to settings_featured_tags_path | ||||
|     else | ||||
|       set_featured_tags | ||||
|       set_most_used_tags | ||||
|  | ||||
|       render :index | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def destroy | ||||
|     @featured_tag.destroy! | ||||
|     redirect_to settings_featured_tags_path | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def set_featured_tag | ||||
|     @featured_tag = current_account.featured_tags.find(params[:id]) | ||||
|   end | ||||
|  | ||||
|   def set_featured_tags | ||||
|     @featured_tags = current_account.featured_tags.reject(&:new_record?) | ||||
|   end | ||||
|  | ||||
|   def set_most_used_tags | ||||
|     @most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) | ||||
|   end | ||||
|  | ||||
|   def featured_tag_params | ||||
|     params.require(:featured_tag).permit(:name) | ||||
|   end | ||||
| end | ||||
| @@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController | ||||
|   end | ||||
|  | ||||
|   def set_account | ||||
|     @account = current_user.account | ||||
|     @account = current_account | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Settings::SessionsController < Settings::BaseController | ||||
|   before_action :authenticate_user! | ||||
|   before_action :set_session, only: :destroy | ||||
|  | ||||
|   def destroy | ||||
|   | ||||
| @@ -288,3 +288,7 @@ | ||||
|     border-bottom: 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .directory__tag .trends__item__current { | ||||
|   width: auto; | ||||
| } | ||||
|   | ||||
| @@ -153,10 +153,15 @@ $content-width: 840px; | ||||
|       font-weight: 500; | ||||
|     } | ||||
|  | ||||
|     .directory__tag a { | ||||
|     .directory__tag > a, | ||||
|     .directory__tag > div { | ||||
|       box-shadow: none; | ||||
|     } | ||||
|  | ||||
|     .directory__tag .table-action-link .fa { | ||||
|       color: inherit; | ||||
|     } | ||||
|  | ||||
|     .directory__tag h4 { | ||||
|       font-size: 18px; | ||||
|       font-weight: 700; | ||||
|   | ||||
| @@ -269,7 +269,8 @@ | ||||
|     box-sizing: border-box; | ||||
|     margin-bottom: 10px; | ||||
|  | ||||
|     a { | ||||
|     & > a, | ||||
|     & > div { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: space-between; | ||||
| @@ -279,7 +280,9 @@ | ||||
|       text-decoration: none; | ||||
|       color: inherit; | ||||
|       box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); | ||||
|     } | ||||
|  | ||||
|     & > a { | ||||
|       &:hover, | ||||
|       &:active, | ||||
|       &:focus { | ||||
| @@ -287,7 +290,7 @@ | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &.active a { | ||||
|     &.active > a { | ||||
|       background: $ui-highlight-color; | ||||
|       cursor: default; | ||||
|     } | ||||
|   | ||||
| @@ -55,5 +55,6 @@ module AccountAssociations | ||||
|  | ||||
|     # Hashtags | ||||
|     has_and_belongs_to_many :tags | ||||
|     has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										46
									
								
								app/models/featured_tag.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								app/models/featured_tag.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| # frozen_string_literal: true | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: featured_tags | ||||
| # | ||||
| #  id             :bigint(8)        not null, primary key | ||||
| #  account_id     :bigint(8) | ||||
| #  tag_id         :bigint(8) | ||||
| #  statuses_count :bigint(8)        default(0), not null | ||||
| #  last_status_at :datetime | ||||
| #  created_at     :datetime         not null | ||||
| #  updated_at     :datetime         not null | ||||
| # | ||||
|  | ||||
| class FeaturedTag < ApplicationRecord | ||||
|   belongs_to :account, inverse_of: :featured_tags, required: true | ||||
|   belongs_to :tag, inverse_of: :featured_tags, required: true | ||||
|  | ||||
|   delegate :name, to: :tag, allow_nil: true | ||||
|  | ||||
|   validates :name, presence: true | ||||
|   validate :validate_featured_tags_limit, on: :create | ||||
|  | ||||
|   def name=(str) | ||||
|     self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s) | ||||
|   end | ||||
|  | ||||
|   def increment(timestamp) | ||||
|     update(statuses_count: statuses_count + 1, last_status_at: timestamp) | ||||
|   end | ||||
|  | ||||
|   def decrement(deleted_status_id) | ||||
|     update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) | ||||
|   end | ||||
|  | ||||
|   def reset_data | ||||
|     self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count | ||||
|     self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def validate_featured_tags_limit | ||||
|     errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= 10 | ||||
|   end | ||||
| end | ||||
| @@ -14,6 +14,7 @@ class Tag < ApplicationRecord | ||||
|   has_and_belongs_to_many :accounts | ||||
|   has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account' | ||||
|  | ||||
|   has_many :featured_tags, dependent: :destroy, inverse_of: :tag | ||||
|   has_one :account_tag_stat, dependent: :destroy | ||||
|  | ||||
|   HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*' | ||||
| @@ -23,6 +24,7 @@ class Tag < ApplicationRecord | ||||
|  | ||||
|   scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } | ||||
|   scope :hidden, -> { where(account_tag_stats: { hidden: true }) } | ||||
|   scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } | ||||
|  | ||||
|   delegate :accounts_count, | ||||
|            :accounts_count=, | ||||
|   | ||||
| @@ -2,12 +2,22 @@ | ||||
|  | ||||
| class ProcessHashtagsService < BaseService | ||||
|   def call(status, tags = []) | ||||
|     tags = Extractor.extract_hashtags(status.text) if status.local? | ||||
|     tags    = Extractor.extract_hashtags(status.text) if status.local? | ||||
|     records = [] | ||||
|  | ||||
|     tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name| | ||||
|       tag = Tag.where(name: name).first_or_create(name: name) | ||||
|  | ||||
|       status.tags << tag | ||||
|       records << tag | ||||
|  | ||||
|       TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility? | ||||
|     end | ||||
|  | ||||
|     return unless status.public_visibility? || status.unlisted_visibility? | ||||
|  | ||||
|     status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| | ||||
|       featured_tag.increment(status.created_at) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -131,6 +131,10 @@ class RemoveStatusService < BaseService | ||||
|   end | ||||
|  | ||||
|   def remove_from_hashtags | ||||
|     @account.featured_tags.where(tag_id: @status.tags.pluck(:id)).each do |featured_tag| | ||||
|       featured_tag.decrement(@status.id) | ||||
|     end | ||||
|  | ||||
|     return unless @status.public_visibility? | ||||
|  | ||||
|     @tags.each do |hashtag| | ||||
|   | ||||
| @@ -63,4 +63,17 @@ | ||||
|         - @endorsed_accounts.each do |account| | ||||
|           = account_link_to account | ||||
|  | ||||
|     - @account.featured_tags.each do |featured_tag| | ||||
|       .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } | ||||
|         = link_to short_account_tag_path(@account, featured_tag.tag) do | ||||
|           %h4 | ||||
|             = fa_icon 'hashtag' | ||||
|             = featured_tag.name | ||||
|             %small | ||||
|               - if featured_tag.last_status_at.nil? | ||||
|                 = t('accounts.nothing_here') | ||||
|               - else | ||||
|                 %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at | ||||
|           .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true | ||||
|  | ||||
|     = render 'application/sidebar' | ||||
|   | ||||
							
								
								
									
										27
									
								
								app/views/settings/featured_tags/index.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/views/settings/featured_tags/index.html.haml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| - content_for :page_title do | ||||
|   = t('settings.featured_tags') | ||||
|  | ||||
| = simple_form_for @featured_tag, url: settings_featured_tags_path do |f| | ||||
|   = render 'shared/error_messages', object: @featured_tag | ||||
|  | ||||
|   .fields-group | ||||
|     = f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@most_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ') | ||||
|  | ||||
|   .actions | ||||
|     = f.button :button, t('featured_tags.add_new'), type: :submit | ||||
|  | ||||
| %hr.spacer/ | ||||
|  | ||||
| - @featured_tags.each do |featured_tag| | ||||
|   .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } | ||||
|     %div | ||||
|       %h4 | ||||
|         = fa_icon 'hashtag' | ||||
|         = featured_tag.name | ||||
|         %small | ||||
|           - if featured_tag.last_status_at.nil? | ||||
|             = t('accounts.nothing_here') | ||||
|           - else | ||||
|             %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at | ||||
|           = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } | ||||
|       .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true | ||||
| @@ -588,6 +588,10 @@ en: | ||||
|     lists: Lists | ||||
|     mutes: You mute | ||||
|     storage: Media storage | ||||
|   featured_tags: | ||||
|     add_new: Add new | ||||
|     errors: | ||||
|       limit: You have already featured the maximum amount of hashtags | ||||
|   filters: | ||||
|     contexts: | ||||
|       home: Home timeline | ||||
| @@ -807,6 +811,7 @@ en: | ||||
|     development: Development | ||||
|     edit_profile: Edit profile | ||||
|     export: Data export | ||||
|     featured_tags: Featured hashtags | ||||
|     followers: Authorized followers | ||||
|     import: Import | ||||
|     migrate: Account migration | ||||
|   | ||||
| @@ -37,6 +37,8 @@ en: | ||||
|         setting_theme: Affects how Mastodon looks when you're logged in from any device. | ||||
|         username: Your username will be unique on %{domain} | ||||
|         whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word | ||||
|       featured_tag: | ||||
|         name: 'You might want to use one of these:' | ||||
|       imports: | ||||
|         data: CSV file exported from another Mastodon instance | ||||
|       sessions: | ||||
| @@ -110,6 +112,8 @@ en: | ||||
|         username: Username | ||||
|         username_or_email: Username or Email | ||||
|         whole_word: Whole word | ||||
|       featured_tag: | ||||
|         name: Hashtag | ||||
|       interactions: | ||||
|         must_be_follower: Block notifications from non-followers | ||||
|         must_be_following: Block notifications from people you don't follow | ||||
|   | ||||
| @@ -6,6 +6,7 @@ SimpleNavigation::Configuration.run do |navigation| | ||||
|  | ||||
|     primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings| | ||||
|       settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration} | ||||
|       settings.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url | ||||
|       settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url | ||||
|       settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url | ||||
|       settings.item :password, safe_join([fa_icon('lock fw'), t('auth.security')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete} | ||||
|   | ||||
| @@ -74,6 +74,7 @@ Rails.application.routes.draw do | ||||
|   get '/@:username', to: 'accounts#show', as: :short_account | ||||
|   get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies | ||||
|   get '/@:username/media', to: 'accounts#show', as: :short_account_media | ||||
|   get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag | ||||
|   get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status | ||||
|   get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status | ||||
|  | ||||
| @@ -116,6 +117,7 @@ Rails.application.routes.draw do | ||||
|     resource :migration, only: [:show, :update] | ||||
|  | ||||
|     resources :sessions, only: [:destroy] | ||||
|     resources :featured_tags, only: [:index, :create, :destroy] | ||||
|   end | ||||
|  | ||||
|   resources :media, only: [:show] do | ||||
|   | ||||
| @@ -7,6 +7,7 @@ class CreateAccountModerationNotes < ActiveRecord::Migration[5.1] | ||||
|  | ||||
|       t.timestamps | ||||
|     end | ||||
|  | ||||
|     add_foreign_key :account_moderation_notes, :accounts, column: :target_account_id | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										12
									
								
								db/migrate/20190203180359_create_featured_tags.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/migrate/20190203180359_create_featured_tags.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| class CreateFeaturedTags < ActiveRecord::Migration[5.2] | ||||
|   def change | ||||
|     create_table :featured_tags do |t| | ||||
|       t.references :account, foreign_key: { on_delete: :cascade } | ||||
|       t.references :tag, foreign_key: { on_delete: :cascade } | ||||
|       t.bigint :statuses_count, default: 0, null: false | ||||
|       t.datetime :last_status_at | ||||
|  | ||||
|       t.timestamps | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										15
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								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: 2019_02_01_012802) do | ||||
| ActiveRecord::Schema.define(version: 2019_02_03_180359) do | ||||
|  | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "plpgsql" | ||||
| @@ -250,6 +250,17 @@ ActiveRecord::Schema.define(version: 2019_02_01_012802) do | ||||
|     t.index ["status_id"], name: "index_favourites_on_status_id" | ||||
|   end | ||||
|  | ||||
|   create_table "featured_tags", force: :cascade do |t| | ||||
|     t.bigint "account_id" | ||||
|     t.bigint "tag_id" | ||||
|     t.bigint "statuses_count", default: 0, null: false | ||||
|     t.datetime "last_status_at" | ||||
|     t.datetime "created_at", null: false | ||||
|     t.datetime "updated_at", null: false | ||||
|     t.index ["account_id"], name: "index_featured_tags_on_account_id" | ||||
|     t.index ["tag_id"], name: "index_featured_tags_on_tag_id" | ||||
|   end | ||||
|  | ||||
|   create_table "follow_requests", force: :cascade do |t| | ||||
|     t.datetime "created_at", null: false | ||||
|     t.datetime "updated_at", null: false | ||||
| @@ -708,6 +719,8 @@ ActiveRecord::Schema.define(version: 2019_02_01_012802) do | ||||
|   add_foreign_key "custom_filters", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade | ||||
|   add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade | ||||
|   add_foreign_key "featured_tags", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "featured_tags", "tags", on_delete: :cascade | ||||
|   add_foreign_key "follow_requests", "accounts", column: "target_account_id", name: "fk_9291ec025d", on_delete: :cascade | ||||
|   add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade | ||||
|   add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade | ||||
|   | ||||
							
								
								
									
										6
									
								
								spec/fabricators/featured_tag_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								spec/fabricators/featured_tag_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| Fabricator(:featured_tag) do | ||||
|   account | ||||
|   tag | ||||
|   statuses_count 1_337 | ||||
|   last_status_at Time.now.utc | ||||
| end | ||||
							
								
								
									
										4
									
								
								spec/models/featured_tag_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								spec/models/featured_tag_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe FeaturedTag, type: :model do | ||||
| end | ||||
		Reference in New Issue
	
	Block a user