# frozen_string_literal: true
# == Schema Information
#
# Table name: tags
#
# id :bigint(8) not null, primary key
# name :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# usable :boolean
# trendable :boolean
# listable :boolean
# reviewed_at :datetime
# requested_review_at :datetime
# last_status_at :datetime
# max_score :float
# max_score_at :datetime
#
class Tag < ApplicationRecord
has_and_belongs_to_many :statuses
has_and_belongs_to_many :accounts
has_many :featured_tags , dependent : :destroy , inverse_of : :tag
HASHTAG_SEPARATORS = " _ \ u00B7 \ u200c "
HASHTAG_NAME_RE = " ([[:word:]_][[:word:] #{ HASHTAG_SEPARATORS } ]*[[:alpha:] #{ HASHTAG_SEPARATORS } ][[:word:] #{ HASHTAG_SEPARATORS } ]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*) "
HASHTAG_RE = / (?:^|[^ \/ \ ) \ w]) # ( #{ HASHTAG_NAME_RE } ) /i
validates :name , presence : true , format : { with : / \ A( #{ HASHTAG_NAME_RE } ) \ z /i }
validate :validate_name_change , if : - > { ! new_record? && name_changed? }
scope :reviewed , - > { where . not ( reviewed_at : nil ) }
scope :unreviewed , - > { where ( reviewed_at : nil ) }
scope :pending_review , - > { unreviewed . where . not ( requested_review_at : nil ) }
scope :usable , - > { where ( usable : [ true , nil ] ) }
scope :listable , - > { where ( listable : [ true , nil ] ) }
scope :trendable , - > { Setting . trendable_by_default ? where ( trendable : [ true , nil ] ) : where ( trendable : true ) }
scope :recently_used , - > ( account ) { joins ( :statuses ) . where ( statuses : { id : account . statuses . select ( :id ) . limit ( 1000 ) } ) . group ( :id ) . order ( Arel . sql ( 'count(*) desc' ) ) }
scope :matches_name , - > ( term ) { where ( arel_table [ :name ] . lower . matches ( arel_table . lower ( " #{ sanitize_sql_like ( Tag . normalize ( term ) ) } % " ) , nil , true ) ) } # Search with case-sensitive to use B-tree index
update_index ( 'tags' , :self )
def to_param
name
end
def usable
boolean_with_default ( 'usable' , true )
end
alias usable? usable
def listable
boolean_with_default ( 'listable' , true )
end
alias listable? listable
def trendable
boolean_with_default ( 'trendable' , Setting . trendable_by_default )
end
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 use! ( account , status : nil , at_time : Time . now . utc )
TrendingTags . record_use! ( self , account , status : status , at_time : at_time )
end
def trending?
TrendingTags . trending? ( self )
end
def history
days = [ ]
7 . times do | i |
day = i . days . ago . beginning_of_day . to_i
days << {
day : day . to_s ,
uses : Redis . current . get ( " activity:tags: #{ id } : #{ day } " ) || '0' ,
accounts : Redis . current . pfcount ( " activity:tags: #{ id } : #{ day } :accounts " ) . to_s ,
}
end
days
end
class << self
def find_or_create_by_names ( name_or_names )
Array ( name_or_names ) . map ( & method ( :normalize ) ) . uniq { | str | str . mb_chars . downcase . to_s } . map do | normalized_name |
tag = matching_name ( normalized_name ) . first || create ( name : normalized_name )
yield tag if block_given?
tag
end
end
def search_for ( term , limit = 5 , offset = 0 , options = { } )
stripped_term = term . strip
query = Tag . listable . matches_name ( stripped_term )
query = query . merge ( matching_name ( stripped_term ) . or ( where . not ( reviewed_at : nil ) ) ) if options [ :exclude_unreviewed ]
Add type, limit, offset, min_id, max_id, account_id to search API (#10091)
* Add type, limit, offset, min_id, max_id, account_id to search API
Fix #8939
* Make the offset work on accounts and hashtags search as well
* Assure brakeman we are not doing mass assignment here
* Do not allow paginating unless a type is chosen
* Fix search query and index id field on statuses instead of created_at
6 years ago
query . order ( Arel . sql ( 'length(name) ASC, name ASC' ) )
. limit ( limit )
. offset ( offset )
end
def find_normalized ( name )
matching_name ( name ) . first
end
def find_normalized! ( name )
find_normalized ( name ) || raise ( ActiveRecord :: RecordNotFound )
end
def matching_name ( name_or_names )
names = Array ( name_or_names ) . map { | name | arel_table . lower ( normalize ( name ) ) }
if names . size == 1
where ( arel_table [ :name ] . lower . eq ( names . first ) )
else
where ( arel_table [ :name ] . lower . in ( names ) )
end
end
def normalize ( str )
str . gsub ( / \ A # / , '' )
end
end
private
def validate_name_change
errors . add ( :name , I18n . t ( 'tags.does_not_match_previous_name' ) ) unless name_was . mb_chars . casecmp ( name . mb_chars ) . zero?
end
end