Add support for language preferences for trending statuses and links (#18288)
This commit is contained in:
		
					parent
					
						
							
								678fc4d292
							
						
					
				
			
			
				commit
				
					
						45ebdb72ca
					
				
			
		
					 29 changed files with 274 additions and 121 deletions
				
			
		|  | @ -4,6 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController | ||||||
|   def index |   def index | ||||||
|     authorize :preview_card, :review? |     authorize :preview_card, :review? | ||||||
| 
 | 
 | ||||||
|  |     @locales       = PreviewCardTrend.pluck('distinct language') | ||||||
|     @preview_cards = filtered_preview_cards.page(params[:page]) |     @preview_cards = filtered_preview_cards.page(params[:page]) | ||||||
|     @form          = Trends::PreviewCardBatch.new |     @form          = Trends::PreviewCardBatch.new | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ class Admin::Trends::StatusesController < Admin::BaseController | ||||||
|   def index |   def index | ||||||
|     authorize :status, :review? |     authorize :status, :review? | ||||||
| 
 | 
 | ||||||
|  |     @locales  = StatusTrend.pluck('distinct language') | ||||||
|     @statuses = filtered_statuses.page(params[:page]) |     @statuses = filtered_statuses.page(params[:page]) | ||||||
|     @form     = Trends::StatusBatch.new |     @form     = Trends::StatusBatch.new | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -28,7 +28,9 @@ class Api::V1::Trends::LinksController < Api::BaseController | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def links_from_trends |   def links_from_trends | ||||||
|     Trends.links.query.allowed.in_locale(content_locale) |     scope = Trends.links.query.allowed.in_locale(content_locale) | ||||||
|  |     scope = scope.filtered_for(current_account) if user_signed_in? | ||||||
|  |     scope | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def insert_pagination_headers |   def insert_pagination_headers | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ class AdminMailer < ApplicationMailer | ||||||
|   layout 'plain_mailer' |   layout 'plain_mailer' | ||||||
| 
 | 
 | ||||||
|   helper :accounts |   helper :accounts | ||||||
|  |   helper :languages | ||||||
| 
 | 
 | ||||||
|   def new_report(recipient, report) |   def new_report(recipient, report) | ||||||
|     @report   = report |     @report   = report | ||||||
|  | @ -37,11 +38,8 @@ class AdminMailer < ApplicationMailer | ||||||
| 
 | 
 | ||||||
|   def new_trends(recipient, links, tags, statuses) |   def new_trends(recipient, links, tags, statuses) | ||||||
|     @links                  = links |     @links                  = links | ||||||
|     @lowest_trending_link   = Trends.links.query.allowed.limit(Trends.links.options[:review_threshold]).last |  | ||||||
|     @tags                   = tags |     @tags                   = tags | ||||||
|     @lowest_trending_tag    = Trends.tags.query.allowed.limit(Trends.tags.options[:review_threshold]).last |  | ||||||
|     @statuses               = statuses |     @statuses               = statuses | ||||||
|     @lowest_trending_status = Trends.statuses.query.allowed.limit(Trends.statuses.options[:review_threshold]).last |  | ||||||
|     @me                     = recipient |     @me                     = recipient | ||||||
|     @instance               = Rails.configuration.x.local_domain |     @instance               = Rails.configuration.x.local_domain | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -48,6 +48,7 @@ class PreviewCard < ApplicationRecord | ||||||
|   enum link_type: [:unknown, :article] |   enum link_type: [:unknown, :article] | ||||||
| 
 | 
 | ||||||
|   has_and_belongs_to_many :statuses |   has_and_belongs_to_many :statuses | ||||||
|  |   has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy | ||||||
| 
 | 
 | ||||||
|   has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }, validate_media_type: false |   has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }, validate_media_type: false | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								app/models/preview_card_trend.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/models/preview_card_trend.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | # == Schema Information | ||||||
|  | # | ||||||
|  | # Table name: preview_card_trends | ||||||
|  | # | ||||||
|  | #  id              :bigint(8)        not null, primary key | ||||||
|  | #  preview_card_id :bigint(8)        not null | ||||||
|  | #  score           :float            default(0.0), not null | ||||||
|  | #  rank            :integer          default(0), not null | ||||||
|  | #  allowed         :boolean          default(FALSE), not null | ||||||
|  | #  language        :string | ||||||
|  | # | ||||||
|  | class PreviewCardTrend < ApplicationRecord | ||||||
|  |   belongs_to :preview_card | ||||||
|  |   scope :allowed, -> { where(allowed: true) } | ||||||
|  | end | ||||||
|  | @ -75,6 +75,7 @@ class Status < ApplicationRecord | ||||||
|   has_one :notification, as: :activity, dependent: :destroy |   has_one :notification, as: :activity, dependent: :destroy | ||||||
|   has_one :status_stat, inverse_of: :status |   has_one :status_stat, inverse_of: :status | ||||||
|   has_one :poll, inverse_of: :status, dependent: :destroy |   has_one :poll, inverse_of: :status, dependent: :destroy | ||||||
|  |   has_one :trend, class_name: 'StatusTrend', inverse_of: :status | ||||||
| 
 | 
 | ||||||
|   validates :uri, uniqueness: true, presence: true, unless: :local? |   validates :uri, uniqueness: true, presence: true, unless: :local? | ||||||
|   validates :text, presence: true, unless: -> { with_media? || reblog? } |   validates :text, presence: true, unless: -> { with_media? || reblog? } | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								app/models/status_trend.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/models/status_trend.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | # == Schema Information | ||||||
|  | # | ||||||
|  | # Table name: status_trends | ||||||
|  | # | ||||||
|  | #  id         :bigint(8)        not null, primary key | ||||||
|  | #  status_id  :bigint(8)        not null | ||||||
|  | #  account_id :bigint(8)        not null | ||||||
|  | #  score      :float            default(0.0), not null | ||||||
|  | #  rank       :integer          default(0), not null | ||||||
|  | #  allowed    :boolean          default(FALSE), not null | ||||||
|  | #  language   :string | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | class StatusTrend < ApplicationRecord | ||||||
|  |   belongs_to :status | ||||||
|  |   belongs_to :account | ||||||
|  | 
 | ||||||
|  |   scope :allowed, -> { where(allowed: true) } | ||||||
|  | end | ||||||
|  | @ -26,7 +26,7 @@ module Trends | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def self.request_review! |   def self.request_review! | ||||||
|     return unless enabled? |     return if skip_review? || !enabled? | ||||||
| 
 | 
 | ||||||
|     links_requiring_review    = links.request_review |     links_requiring_review    = links.request_review | ||||||
|     tags_requiring_review     = tags.request_review |     tags_requiring_review     = tags.request_review | ||||||
|  | @ -43,6 +43,10 @@ module Trends | ||||||
|     Setting.trends |     Setting.trends | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def skip_review? | ||||||
|  |     Setting.trendable_by_default | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def self.available_locales |   def self.available_locales | ||||||
|     @available_locales ||= I18n.available_locales.map { |locale| locale.to_s.split(/[_-]/).first }.uniq |     @available_locales ||= I18n.available_locales.map { |locale| locale.to_s.split(/[_-]/).first }.uniq | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -98,4 +98,8 @@ class Trends::Base | ||||||
|       pipeline.rename(from_key, to_key) |       pipeline.rename(from_key, to_key) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def skip_review? | ||||||
|  |     Setting.trendable_by_default | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -11,6 +11,40 @@ class Trends::Links < Trends::Base | ||||||
|     decay_threshold: 1, |     decay_threshold: 1, | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   class Query < Trends::Query | ||||||
|  |     def filtered_for!(account) | ||||||
|  |       @account = account | ||||||
|  |       self | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def filtered_for(account) | ||||||
|  |       clone.filtered_for!(account) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def to_arel | ||||||
|  |       scope = PreviewCard.joins(:trend).reorder(score: :desc) | ||||||
|  |       scope = scope.reorder(language_order_clause.desc, score: :desc) if preferred_languages.present? | ||||||
|  |       scope = scope.merge(PreviewCardTrend.allowed) if @allowed | ||||||
|  |       scope = scope.offset(@offset) if @offset.present? | ||||||
|  |       scope = scope.limit(@limit) if @limit.present? | ||||||
|  |       scope | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     private | ||||||
|  | 
 | ||||||
|  |     def language_order_clause | ||||||
|  |       Arel::Nodes::Case.new.when(PreviewCardTrend.arel_table[:language].in(preferred_languages)).then(1).else(0) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def preferred_languages | ||||||
|  |       if @account&.chosen_languages.present? | ||||||
|  |         @account.chosen_languages | ||||||
|  |       else | ||||||
|  |         @locale | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def register(status, at_time = Time.now.utc) |   def register(status, at_time = Time.now.utc) | ||||||
|     original_status = status.proper |     original_status = status.proper | ||||||
| 
 | 
 | ||||||
|  | @ -28,16 +62,24 @@ class Trends::Links < Trends::Base | ||||||
|     record_used_id(preview_card.id, at_time) |     record_used_id(preview_card.id, at_time) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def query | ||||||
|  |     Query.new(key_prefix, klass) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def refresh(at_time = Time.now.utc) |   def refresh(at_time = Time.now.utc) | ||||||
|     preview_cards = PreviewCard.where(id: (recently_used_ids(at_time) + currently_trending_ids(false, -1)).uniq) |     preview_cards = PreviewCard.where(id: (recently_used_ids(at_time) + PreviewCardTrend.pluck(:preview_card_id)).uniq) | ||||||
|     calculate_scores(preview_cards, at_time) |     calculate_scores(preview_cards, at_time) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def request_review |   def request_review | ||||||
|     preview_cards = PreviewCard.where(id: currently_trending_ids(false, -1)) |     PreviewCardTrend.pluck('distinct language').flat_map do |language| | ||||||
|  |       score_at_threshold  = PreviewCardTrend.where(language: language, allowed: true).order(rank: :desc).where('rank <= ?', options[:review_threshold]).first&.score || 0 | ||||||
|  |       preview_card_trends = PreviewCardTrend.where(language: language, allowed: false).joins(:preview_card) | ||||||
| 
 | 
 | ||||||
|     preview_cards.filter_map do |preview_card| |       preview_card_trends.filter_map do |trend| | ||||||
|       next unless would_be_trending?(preview_card.id) && !preview_card.trendable? && preview_card.requires_review_notification? |         preview_card = trend.preview_card | ||||||
|  | 
 | ||||||
|  |         next unless trend.score > score_at_threshold && !preview_card.trendable? && preview_card.requires_review_notification? | ||||||
| 
 | 
 | ||||||
|         if preview_card.provider.nil? |         if preview_card.provider.nil? | ||||||
|           preview_card.provider = PreviewCardProvider.create(domain: preview_card.domain, requested_review_at: Time.now.utc) |           preview_card.provider = PreviewCardProvider.create(domain: preview_card.domain, requested_review_at: Time.now.utc) | ||||||
|  | @ -48,6 +90,7 @@ class Trends::Links < Trends::Base | ||||||
|         preview_card |         preview_card | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
|   protected |   protected | ||||||
| 
 | 
 | ||||||
|  | @ -62,10 +105,7 @@ class Trends::Links < Trends::Base | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def calculate_scores(preview_cards, at_time) |   def calculate_scores(preview_cards, at_time) | ||||||
|     global_items = [] |     items = preview_cards.map do |preview_card| | ||||||
|     locale_items = Hash.new { |h, key| h[key] = [] } |  | ||||||
| 
 |  | ||||||
|     preview_cards.each do |preview_card| |  | ||||||
|       expected  = preview_card.history.get(at_time - 1.day).accounts.to_f |       expected  = preview_card.history.get(at_time - 1.day).accounts.to_f | ||||||
|       expected  = 1.0 if expected.zero? |       expected  = 1.0 if expected.zero? | ||||||
|       observed  = preview_card.history.get(at_time).accounts.to_f |       observed  = preview_card.history.get(at_time).accounts.to_f | ||||||
|  | @ -89,26 +129,24 @@ class Trends::Links < Trends::Base | ||||||
|         preview_card.update_columns(max_score: max_score, max_score_at: max_time) |         preview_card.update_columns(max_score: max_score, max_score_at: max_time) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       decaying_score = max_score * (0.5**((at_time.to_f - max_time.to_f) / options[:max_score_halflife].to_f)) |       decaying_score = begin | ||||||
| 
 |         if max_score.zero? || !valid_locale?(preview_card.language) | ||||||
|       next unless decaying_score >= options[:decay_threshold] |           0 | ||||||
| 
 |         else | ||||||
|       global_items << { score: decaying_score, item:  preview_card } |           max_score * (0.5**((at_time.to_f - max_time.to_f) / options[:max_score_halflife].to_f)) | ||||||
|       locale_items[preview_card.language] << { score: decaying_score, item: preview_card } if valid_locale?(preview_card.language) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     replace_items('', global_items) |  | ||||||
| 
 |  | ||||||
|     Trends.available_locales.each do |locale| |  | ||||||
|       replace_items(":#{locale}", locale_items[locale]) |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|   def filter_for_allowed_items(items) |       [decaying_score, preview_card] | ||||||
|     items.select { |item| item[:item].trendable? } |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|   def would_be_trending?(id) |     to_insert = items.filter { |(score, _)| score >= options[:decay_threshold] } | ||||||
|     score(id) > score_at_rank(options[:review_threshold] - 1) |     to_delete = items.filter { |(score, _)| score < options[:decay_threshold] } | ||||||
|  | 
 | ||||||
|  |     PreviewCardTrend.transaction do | ||||||
|  |       PreviewCardTrend.upsert_all(to_insert.map { |(score, preview_card)| { preview_card_id: preview_card.id, score: score, language: preview_card.language, allowed: preview_card.trendable? || false } }, unique_by: :preview_card_id) if to_insert.any? | ||||||
|  |       PreviewCardTrend.where(preview_card_id: to_delete.map { |(_, preview_card)| preview_card.id }).delete_all if to_delete.any? | ||||||
|  |       PreviewCardTrend.connection.exec_update('UPDATE preview_card_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM preview_card_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE preview_card_trends.id = t0.id') | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -13,10 +13,10 @@ class Trends::PreviewCardFilter | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def results |   def results | ||||||
|     scope = PreviewCard.unscoped |     scope = initial_scope | ||||||
| 
 | 
 | ||||||
|     params.each do |key, value| |     params.each do |key, value| | ||||||
|       next if %w(page locale).include?(key.to_s) |       next if %w(page).include?(key.to_s) | ||||||
| 
 | 
 | ||||||
|       scope.merge!(scope_for(key, value.to_s.strip)) if value.present? |       scope.merge!(scope_for(key, value.to_s.strip)) if value.present? | ||||||
|     end |     end | ||||||
|  | @ -26,21 +26,30 @@ class Trends::PreviewCardFilter | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|  |   def initial_scope | ||||||
|  |     PreviewCard.select(PreviewCard.arel_table[Arel.star]) | ||||||
|  |                .joins(:trend) | ||||||
|  |                .eager_load(:trend) | ||||||
|  |                .reorder(score: :desc) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def scope_for(key, value) |   def scope_for(key, value) | ||||||
|     case key.to_s |     case key.to_s | ||||||
|     when 'trending' |     when 'trending' | ||||||
|       trending_scope(value) |       trending_scope(value) | ||||||
|  |     when 'locale' | ||||||
|  |       PreviewCardTrend.where(language: value) | ||||||
|     else |     else | ||||||
|       raise "Unknown filter: #{key}" |       raise "Unknown filter: #{key}" | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def trending_scope(value) |   def trending_scope(value) | ||||||
|     scope = Trends.links.query |     case value | ||||||
| 
 |     when 'allowed' | ||||||
|     scope = scope.in_locale(@params[:locale].to_s) if @params[:locale].present? |       PreviewCardTrend.allowed | ||||||
|     scope = scope.allowed if value == 'allowed' |     else | ||||||
| 
 |       PreviewCardTrend.all | ||||||
|     scope.to_arel |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -13,10 +13,10 @@ class Trends::StatusFilter | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def results |   def results | ||||||
|     scope = Status.unscoped.kept |     scope = initial_scope | ||||||
| 
 | 
 | ||||||
|     params.each do |key, value| |     params.each do |key, value| | ||||||
|       next if %w(page locale).include?(key.to_s) |       next if %w(page).include?(key.to_s) | ||||||
| 
 | 
 | ||||||
|       scope.merge!(scope_for(key, value.to_s.strip)) if value.present? |       scope.merge!(scope_for(key, value.to_s.strip)) if value.present? | ||||||
|     end |     end | ||||||
|  | @ -26,21 +26,30 @@ class Trends::StatusFilter | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|  |   def initial_scope | ||||||
|  |     Status.select(Status.arel_table[Arel.star]) | ||||||
|  |           .joins(:trend) | ||||||
|  |           .eager_load(:trend) | ||||||
|  |           .reorder(score: :desc) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def scope_for(key, value) |   def scope_for(key, value) | ||||||
|     case key.to_s |     case key.to_s | ||||||
|     when 'trending' |     when 'trending' | ||||||
|       trending_scope(value) |       trending_scope(value) | ||||||
|  |     when 'locale' | ||||||
|  |       StatusTrend.where(language: value) | ||||||
|     else |     else | ||||||
|       raise "Unknown filter: #{key}" |       raise "Unknown filter: #{key}" | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def trending_scope(value) |   def trending_scope(value) | ||||||
|     scope = Trends.statuses.query |     case value | ||||||
| 
 |     when 'allowed' | ||||||
|     scope = scope.in_locale(@params[:locale].to_s) if @params[:locale].present? |       StatusTrend.allowed | ||||||
|     scope = scope.allowed if value == 'allowed' |     else | ||||||
| 
 |       StatusTrend.all | ||||||
|     scope.to_arel |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -20,13 +20,27 @@ class Trends::Statuses < Trends::Base | ||||||
|       clone.filtered_for!(account) |       clone.filtered_for!(account) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     def to_arel | ||||||
|  |       scope = Status.joins(:trend).reorder(score: :desc) | ||||||
|  |       scope = scope.reorder(language_order_clause.desc, score: :desc) if preferred_languages.present? | ||||||
|  |       scope = scope.merge(StatusTrend.allowed) if @allowed | ||||||
|  |       scope = scope.not_excluded_by_account(@account).not_domain_blocked_by_account(@account) if @account.present? | ||||||
|  |       scope = scope.offset(@offset) if @offset.present? | ||||||
|  |       scope = scope.limit(@limit) if @limit.present? | ||||||
|  |       scope | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|     def apply_scopes(scope) |     def language_order_clause | ||||||
|       if @account.nil? |       Arel::Nodes::Case.new.when(StatusTrend.arel_table[:language].in(preferred_languages)).then(1).else(0) | ||||||
|         scope |     end | ||||||
|  | 
 | ||||||
|  |     def preferred_languages | ||||||
|  |       if @account&.chosen_languages.present? | ||||||
|  |         @account.chosen_languages | ||||||
|       else |       else | ||||||
|         scope.not_excluded_by_account(@account).not_domain_blocked_by_account(@account) |         @locale | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | @ -36,9 +50,6 @@ class Trends::Statuses < Trends::Base | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def add(status, _account_id, at_time = Time.now.utc) |   def add(status, _account_id, at_time = Time.now.utc) | ||||||
|     # We rely on the total reblogs and favourites count, so we |  | ||||||
|     # don't record which account did the what and when here |  | ||||||
| 
 |  | ||||||
|     record_used_id(status.id, at_time) |     record_used_id(status.id, at_time) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -47,20 +58,25 @@ class Trends::Statuses < Trends::Base | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def refresh(at_time = Time.now.utc) |   def refresh(at_time = Time.now.utc) | ||||||
|     statuses = Status.where(id: (recently_used_ids(at_time) + currently_trending_ids(false, -1)).uniq).includes(:account, :media_attachments) |     statuses = Status.where(id: (recently_used_ids(at_time) + StatusTrend.pluck(:status_id)).uniq).includes(:status_stat, :account) | ||||||
|     calculate_scores(statuses, at_time) |     calculate_scores(statuses, at_time) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def request_review |   def request_review | ||||||
|     statuses = Status.where(id: currently_trending_ids(false, -1)).includes(:account) |     StatusTrend.pluck('distinct language').flat_map do |language| | ||||||
|  |       score_at_threshold = StatusTrend.where(language: language, allowed: true).order(rank: :desc).where('rank <= ?', options[:review_threshold]).first&.score || 0 | ||||||
|  |       status_trends      = StatusTrend.where(language: language, allowed: false).joins(:status).includes(status: :account) | ||||||
| 
 | 
 | ||||||
|     statuses.filter_map do |status| |       status_trends.filter_map do |trend| | ||||||
|       next unless would_be_trending?(status.id) && !status.trendable? && status.requires_review_notification? |         status = trend.status | ||||||
| 
 | 
 | ||||||
|  |         if trend.score > score_at_threshold && !status.trendable? && status.requires_review_notification? | ||||||
|           status.account.touch(:requested_review_at) |           status.account.touch(:requested_review_at) | ||||||
|           status |           status | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
|   protected |   protected | ||||||
| 
 | 
 | ||||||
|  | @ -75,14 +91,11 @@ class Trends::Statuses < Trends::Base | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def eligible?(status) |   def eligible?(status) | ||||||
|     status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? |     status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def calculate_scores(statuses, at_time) |   def calculate_scores(statuses, at_time) | ||||||
|     global_items = [] |     items = statuses.map do |status| | ||||||
|     locale_items = Hash.new { |h, key| h[key] = [] } |  | ||||||
| 
 |  | ||||||
|     statuses.each do |status| |  | ||||||
|       expected  = 1.0 |       expected  = 1.0 | ||||||
|       observed  = (status.reblogs_count + status.favourites_count).to_f |       observed  = (status.reblogs_count + status.favourites_count).to_f | ||||||
| 
 | 
 | ||||||
|  | @ -94,29 +107,24 @@ class Trends::Statuses < Trends::Base | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       decaying_score = score * (0.5**((at_time.to_f - status.created_at.to_f) / options[:score_halflife].to_f)) |       decaying_score = begin | ||||||
| 
 |         if score.zero? || !eligible?(status) | ||||||
|       next unless decaying_score >= options[:decay_threshold] |           0 | ||||||
| 
 |         else | ||||||
|       global_items << { score: decaying_score, item: status } |           score * (0.5**((at_time.to_f - status.created_at.to_f) / options[:score_halflife].to_f)) | ||||||
|       locale_items[status.language] << { account_id: status.account_id, score: decaying_score, item: status } if valid_locale?(status.language) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     replace_items('', global_items) |  | ||||||
| 
 |  | ||||||
|     Trends.available_locales.each do |locale| |  | ||||||
|       replace_items(":#{locale}", locale_items[locale]) |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|   def filter_for_allowed_items(items) |       [decaying_score, status] | ||||||
|     # Show only one status per account, pick the one with the highest score |  | ||||||
|     # that's also eligible to trend |  | ||||||
| 
 |  | ||||||
|     items.group_by { |item| item[:account_id] }.values.filter_map { |account_items| account_items.select { |item| item[:item].trendable? && item[:item].account.discoverable? }.max_by { |item| item[:score] } } |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|   def would_be_trending?(id) |     to_insert = items.filter { |(score, _)| score >= options[:decay_threshold] } | ||||||
|     score(id) > score_at_rank(options[:review_threshold] - 1) |     to_delete = items.filter { |(score, _)| score < options[:decay_threshold] } | ||||||
|  | 
 | ||||||
|  |     StatusTrend.transaction do | ||||||
|  |       StatusTrend.upsert_all(to_insert.map { |(score, status)| { status_id: status.id, account_id: status.account_id, score: score, language: status.language, allowed: status.trendable? || false } }, unique_by: :status_id) if to_insert.any? | ||||||
|  |       StatusTrend.where(status_id: to_delete.map { |(_, status)| status.id }).delete_all if to_delete.any? | ||||||
|  |       StatusTrend.connection.exec_update('UPDATE status_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM status_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE status_trends.id = t0.id') | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -18,9 +18,9 @@ | ||||||
| 
 | 
 | ||||||
|       = t('admin.trends.links.shared_by_over_week', count: preview_card.history.reduce(0) { |sum, day| sum + day.accounts }) |       = t('admin.trends.links.shared_by_over_week', count: preview_card.history.reduce(0) { |sum, day| sum + day.accounts }) | ||||||
| 
 | 
 | ||||||
|       - if preview_card.trendable? && (rank = Trends.links.rank(preview_card.id, locale: params[:locale].presence)) |       - if preview_card.trend.allowed? | ||||||
|         • |         • | ||||||
|         %abbr{ title: t('admin.trends.tags.current_score', score: Trends.links.score(preview_card.id, locale: params[:locale].presence)) }= t('admin.trends.tags.trending_rank', rank: rank + 1) |         %abbr{ title: t('admin.trends.tags.current_score', score: preview_card.trend.score) }= t('admin.trends.tags.trending_rank', rank: preview_card.trend.rank) | ||||||
| 
 | 
 | ||||||
|         - if preview_card.decaying? |         - if preview_card.decaying? | ||||||
|           • |           • | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ | ||||||
|     .filter-subset.filter-subset--with-select |     .filter-subset.filter-subset--with-select | ||||||
|       %strong= t('admin.follow_recommendations.language') |       %strong= t('admin.follow_recommendations.language') | ||||||
|       .input.select.optional |       .input.select.optional | ||||||
|         = select_tag :locale, options_for_select(Trends.available_locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), include_blank: true |         = select_tag :locale, options_for_select(@locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), include_blank: true | ||||||
|     .filter-subset |     .filter-subset | ||||||
|       %strong= t('admin.trends.trending') |       %strong= t('admin.trends.trending') | ||||||
|       %ul |       %ul | ||||||
|  |  | ||||||
|  | @ -25,9 +25,9 @@ | ||||||
|     - if status.trendable? && !status.account.discoverable? |     - if status.trendable? && !status.account.discoverable? | ||||||
|       • |       • | ||||||
|       = t('admin.trends.statuses.not_discoverable') |       = t('admin.trends.statuses.not_discoverable') | ||||||
|     - if status.trendable? && (rank = Trends.statuses.rank(status.id, locale: params[:locale].presence)) |     - if status.trend.allowed? | ||||||
|       • |       • | ||||||
|       %abbr{ title: t('admin.trends.tags.current_score', score: Trends.statuses.score(status.id, locale: params[:locale].presence)) }= t('admin.trends.tags.trending_rank', rank: rank + 1) |       %abbr{ title: t('admin.trends.tags.current_score', score: status.trend.score) }= t('admin.trends.tags.trending_rank', rank: status.trend.rank) | ||||||
|     - elsif status.requires_review? |     - elsif status.requires_review? | ||||||
|       • |       • | ||||||
|       = t('admin.trends.pending_review') |       = t('admin.trends.pending_review') | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ | ||||||
|     .filter-subset.filter-subset--with-select |     .filter-subset.filter-subset--with-select | ||||||
|       %strong= t('admin.follow_recommendations.language') |       %strong= t('admin.follow_recommendations.language') | ||||||
|       .input.select.optional |       .input.select.optional | ||||||
|         = select_tag :locale, options_for_select(Trends.available_locales.map { |key| [standard_locale_name(key), key]}, params[:locale]), include_blank: true |         = select_tag :locale, options_for_select(@locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), include_blank: true | ||||||
|     .filter-subset |     .filter-subset | ||||||
|       %strong= t('admin.trends.trending') |       %strong= t('admin.trends.trending') | ||||||
|       %ul |       %ul | ||||||
|  |  | ||||||
|  | @ -2,13 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| <% @links.each do |link| %> | <% @links.each do |link| %> | ||||||
| - <%= link.title %> • <%= link.url %> | - <%= link.title %> • <%= link.url %> | ||||||
|   <%= raw t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.links.score(link.id).round(2)) %> |   <%= standard_locale_name(link.language) %> • <%= raw t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: link.trend.score.round(2)) %> | ||||||
| <% end %> |  | ||||||
| 
 |  | ||||||
| <% if @lowest_trending_link %> |  | ||||||
| <%= raw t('admin_mailer.new_trends.new_trending_links.requirements', lowest_link_title: @lowest_trending_link.title, lowest_link_score: Trends.links.score(@lowest_trending_link.id).round(2), rank: Trends.links.options[:review_threshold]) %> |  | ||||||
| <% else %> |  | ||||||
| <%= raw t('admin_mailer.new_trends.new_trending_links.no_approved_links') %> |  | ||||||
| <% end %> | <% end %> | ||||||
| 
 | 
 | ||||||
| <%= raw t('application_mailer.view')%> <%= admin_trends_links_url %> | <%= raw t('application_mailer.view')%> <%= admin_trends_links_url %> | ||||||
|  |  | ||||||
|  | @ -2,13 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| <% @statuses.each do |status| %> | <% @statuses.each do |status| %> | ||||||
| - <%= ActivityPub::TagManager.instance.url_for(status) %> | - <%= ActivityPub::TagManager.instance.url_for(status) %> | ||||||
|   <%= raw t('admin.trends.tags.current_score', score: Trends.statuses.score(status.id).round(2)) %> |   <%= standard_locale_name(status.language) %> • <%= raw t('admin.trends.tags.current_score', score: status.trend.score.round(2)) %> | ||||||
| <% end %> |  | ||||||
| 
 |  | ||||||
| <% if @lowest_trending_status %> |  | ||||||
| <%= raw t('admin_mailer.new_trends.new_trending_statuses.requirements', lowest_status_url: ActivityPub::TagManager.instance.url_for(@lowest_trending_status), lowest_status_score: Trends.statuses.score(@lowest_trending_status.id).round(2), rank: Trends.statuses.options[:review_threshold]) %> |  | ||||||
| <% else %> |  | ||||||
| <%= raw t('admin_mailer.new_trends.new_trending_statuses.no_approved_statuses') %> |  | ||||||
| <% end %> | <% end %> | ||||||
| 
 | 
 | ||||||
| <%= raw t('application_mailer.view')%> <%= admin_trends_statuses_url %> | <%= raw t('application_mailer.view')%> <%= admin_trends_statuses_url %> | ||||||
|  |  | ||||||
|  | @ -939,12 +939,8 @@ en: | ||||||
|     new_trends: |     new_trends: | ||||||
|       body: 'The following items need a review before they can be displayed publicly:' |       body: 'The following items need a review before they can be displayed publicly:' | ||||||
|       new_trending_links: |       new_trending_links: | ||||||
|         no_approved_links: There are currently no approved trending links. |  | ||||||
|         requirements: 'Any of these candidates could surpass the #%{rank} approved trending link, which is currently "%{lowest_link_title}" with a score of %{lowest_link_score}.' |  | ||||||
|         title: Trending links |         title: Trending links | ||||||
|       new_trending_statuses: |       new_trending_statuses: | ||||||
|         no_approved_statuses: There are currently no approved trending posts. |  | ||||||
|         requirements: 'Any of these candidates could surpass the #%{rank} approved trending post, which is currently %{lowest_status_url} with a score of %{lowest_status_score}.' |  | ||||||
|         title: Trending posts |         title: Trending posts | ||||||
|       new_trending_tags: |       new_trending_tags: | ||||||
|         no_approved_tags: There are currently no approved trending hashtags. |         no_approved_tags: There are currently no approved trending hashtags. | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								db/migrate/20220824233535_create_status_trends.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/migrate/20220824233535_create_status_trends.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | class CreateStatusTrends < ActiveRecord::Migration[6.1] | ||||||
|  |   def change | ||||||
|  |     create_table :status_trends do |t| | ||||||
|  |       t.references :status, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true } | ||||||
|  |       t.references :account, null: false, foreign_key: { on_delete: :cascade } | ||||||
|  |       t.float :score, null: false, default: 0 | ||||||
|  |       t.integer :rank, null: false, default: 0 | ||||||
|  |       t.boolean :allowed, null: false, default: false | ||||||
|  |       t.string :language | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										11
									
								
								db/migrate/20221006061337_create_preview_card_trends.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								db/migrate/20221006061337_create_preview_card_trends.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | class CreatePreviewCardTrends < ActiveRecord::Migration[6.1] | ||||||
|  |   def change | ||||||
|  |     create_table :preview_card_trends do |t| | ||||||
|  |       t.references :preview_card, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true } | ||||||
|  |       t.float :score, null: false, default: 0 | ||||||
|  |       t.integer :rank, null: false, default: 0 | ||||||
|  |       t.boolean :allowed, null: false, default: false | ||||||
|  |       t.string :language | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										25
									
								
								db/schema.rb
									
										
									
									
									
								
							
							
						
						
									
										25
									
								
								db/schema.rb
									
										
									
									
									
								
							|  | @ -10,7 +10,7 @@ | ||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||||
| 
 | 
 | ||||||
| ActiveRecord::Schema.define(version: 2022_08_29_192658) do | ActiveRecord::Schema.define(version: 2022_10_06_061337) do | ||||||
| 
 | 
 | ||||||
|   # These are extensions that must be enabled in order to support this database |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "plpgsql" |   enable_extension "plpgsql" | ||||||
|  | @ -735,6 +735,15 @@ ActiveRecord::Schema.define(version: 2022_08_29_192658) do | ||||||
|     t.index ["domain"], name: "index_preview_card_providers_on_domain", unique: true |     t.index ["domain"], name: "index_preview_card_providers_on_domain", unique: true | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   create_table "preview_card_trends", force: :cascade do |t| | ||||||
|  |     t.bigint "preview_card_id", null: false | ||||||
|  |     t.float "score", default: 0.0, null: false | ||||||
|  |     t.integer "rank", default: 0, null: false | ||||||
|  |     t.boolean "allowed", default: false, null: false | ||||||
|  |     t.string "language" | ||||||
|  |     t.index ["preview_card_id"], name: "index_preview_card_trends_on_preview_card_id", unique: true | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   create_table "preview_cards", force: :cascade do |t| |   create_table "preview_cards", force: :cascade do |t| | ||||||
|     t.string "url", default: "", null: false |     t.string "url", default: "", null: false | ||||||
|     t.string "title", default: "", null: false |     t.string "title", default: "", null: false | ||||||
|  | @ -894,6 +903,17 @@ ActiveRecord::Schema.define(version: 2022_08_29_192658) do | ||||||
|     t.index ["status_id"], name: "index_status_stats_on_status_id", unique: true |     t.index ["status_id"], name: "index_status_stats_on_status_id", unique: true | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   create_table "status_trends", force: :cascade do |t| | ||||||
|  |     t.bigint "status_id", null: false | ||||||
|  |     t.bigint "account_id", null: false | ||||||
|  |     t.float "score", default: 0.0, null: false | ||||||
|  |     t.integer "rank", default: 0, null: false | ||||||
|  |     t.boolean "allowed", default: false, null: false | ||||||
|  |     t.string "language" | ||||||
|  |     t.index ["account_id"], name: "index_status_trends_on_account_id" | ||||||
|  |     t.index ["status_id"], name: "index_status_trends_on_status_id", unique: true | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   create_table "statuses", id: :bigint, default: -> { "timestamp_id('statuses'::text)" }, force: :cascade do |t| |   create_table "statuses", id: :bigint, default: -> { "timestamp_id('statuses'::text)" }, force: :cascade do |t| | ||||||
|     t.string "uri" |     t.string "uri" | ||||||
|     t.text "text", default: "", null: false |     t.text "text", default: "", null: false | ||||||
|  | @ -1171,6 +1191,7 @@ ActiveRecord::Schema.define(version: 2022_08_29_192658) do | ||||||
|   add_foreign_key "poll_votes", "polls", on_delete: :cascade |   add_foreign_key "poll_votes", "polls", on_delete: :cascade | ||||||
|   add_foreign_key "polls", "accounts", on_delete: :cascade |   add_foreign_key "polls", "accounts", on_delete: :cascade | ||||||
|   add_foreign_key "polls", "statuses", on_delete: :cascade |   add_foreign_key "polls", "statuses", on_delete: :cascade | ||||||
|  |   add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade | ||||||
|   add_foreign_key "report_notes", "accounts", on_delete: :cascade |   add_foreign_key "report_notes", "accounts", on_delete: :cascade | ||||||
|   add_foreign_key "report_notes", "reports", on_delete: :cascade |   add_foreign_key "report_notes", "reports", on_delete: :cascade | ||||||
|   add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify |   add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify | ||||||
|  | @ -1185,6 +1206,8 @@ ActiveRecord::Schema.define(version: 2022_08_29_192658) do | ||||||
|   add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade |   add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade | ||||||
|   add_foreign_key "status_pins", "statuses", on_delete: :cascade |   add_foreign_key "status_pins", "statuses", on_delete: :cascade | ||||||
|   add_foreign_key "status_stats", "statuses", on_delete: :cascade |   add_foreign_key "status_stats", "statuses", on_delete: :cascade | ||||||
|  |   add_foreign_key "status_trends", "accounts", on_delete: :cascade | ||||||
|  |   add_foreign_key "status_trends", "statuses", on_delete: :cascade | ||||||
|   add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", name: "fk_c7fa917661", on_delete: :nullify |   add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", name: "fk_c7fa917661", on_delete: :nullify | ||||||
|   add_foreign_key "statuses", "accounts", name: "fk_9bda1543f7", on_delete: :cascade |   add_foreign_key "statuses", "accounts", name: "fk_9bda1543f7", on_delete: :cascade | ||||||
|   add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify |   add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify | ||||||
|  |  | ||||||
|  | @ -11,4 +11,5 @@ Fabricator(:account) do | ||||||
|   suspended_at        { |attrs| attrs[:suspended] ? Time.now.utc : nil } |   suspended_at        { |attrs| attrs[:suspended] ? Time.now.utc : nil } | ||||||
|   silenced_at         { |attrs| attrs[:silenced] ? Time.now.utc : nil } |   silenced_at         { |attrs| attrs[:silenced] ? Time.now.utc : nil } | ||||||
|   user                { |attrs| attrs[:domain].nil? ? Fabricate.build(:user, account: nil) : nil } |   user                { |attrs| attrs[:domain].nil? ? Fabricate.build(:user, account: nil) : nil } | ||||||
|  |   discoverable        true | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ class AdminMailerPreview < ActionMailer::Preview | ||||||
| 
 | 
 | ||||||
|   # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_trends |   # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_trends | ||||||
|   def new_trends |   def new_trends | ||||||
|     AdminMailer.new_trends(Account.first, PreviewCard.limit(3), Tag.limit(3), Status.where(reblog_of_id: nil).limit(3)) |     AdminMailer.new_trends(Account.first, PreviewCard.joins(:trend).limit(3), Tag.limit(3), Status.joins(:trend).where(reblog_of_id: nil).limit(3)) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal |   # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								spec/models/preview_card_trend_spec.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								spec/models/preview_card_trend_spec.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe PreviewCardTrend, type: :model do | ||||||
|  | end | ||||||
							
								
								
									
										4
									
								
								spec/models/status_trend_spec.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								spec/models/status_trend_spec.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe StatusTrend, type: :model do | ||||||
|  | end | ||||||
|  | @ -9,8 +9,8 @@ RSpec.describe Trends::Statuses do | ||||||
|     let!(:query) { subject.query } |     let!(:query) { subject.query } | ||||||
|     let!(:today) { at_time } |     let!(:today) { at_time } | ||||||
| 
 | 
 | ||||||
|     let!(:status1) { Fabricate(:status, text: 'Foo', trendable: true, created_at: today) } |     let!(:status1) { Fabricate(:status, text: 'Foo', language: 'en', trendable: true, created_at: today) } | ||||||
|     let!(:status2) { Fabricate(:status, text: 'Bar', trendable: true, created_at: today) } |     let!(:status2) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today) } | ||||||
| 
 | 
 | ||||||
|     before do |     before do | ||||||
|       15.times { reblog(status1, today) } |       15.times { reblog(status1, today) } | ||||||
|  | @ -69,9 +69,9 @@ RSpec.describe Trends::Statuses do | ||||||
|     let!(:today) { at_time } |     let!(:today) { at_time } | ||||||
|     let!(:yesterday) { today - 1.day } |     let!(:yesterday) { today - 1.day } | ||||||
| 
 | 
 | ||||||
|     let!(:status1) { Fabricate(:status, text: 'Foo', trendable: true, created_at: yesterday) } |     let!(:status1) { Fabricate(:status, text: 'Foo', language: 'en', trendable: true, created_at: yesterday) } | ||||||
|     let!(:status2) { Fabricate(:status, text: 'Bar', trendable: true, created_at: today) } |     let!(:status2) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today) } | ||||||
|     let!(:status3) { Fabricate(:status, text: 'Baz', trendable: true, created_at: today) } |     let!(:status3) { Fabricate(:status, text: 'Baz', language: 'en', trendable: true, created_at: today) } | ||||||
| 
 | 
 | ||||||
|     before do |     before do | ||||||
|       13.times { reblog(status1, today) } |       13.times { reblog(status1, today) } | ||||||
|  | @ -95,10 +95,10 @@ RSpec.describe Trends::Statuses do | ||||||
| 
 | 
 | ||||||
|     it 'decays scores' do |     it 'decays scores' do | ||||||
|       subject.refresh(today) |       subject.refresh(today) | ||||||
|       original_score = subject.score(status2.id) |       original_score = status2.trend.score | ||||||
|       expect(original_score).to be_a Float |       expect(original_score).to be_a Float | ||||||
|       subject.refresh(today + subject.options[:score_halflife]) |       subject.refresh(today + subject.options[:score_halflife]) | ||||||
|       decayed_score = subject.score(status2.id) |       decayed_score = status2.trend.reload.score | ||||||
|       expect(decayed_score).to be <= original_score / 2 |       expect(decayed_score).to be <= original_score / 2 | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue