discourse/app/models/category_featured_topic.rb
David Taylor 3332e9f4c3
DEV: Always pass --force to annotaterb and reorder annotations (#39977)
`.annotaterb.yml` has carried `classified_sort: true` since the project
switched from `annotate` to `annotaterb` (commit 0eab7daea4, July 2025),
but annotaterb's default behaviour is to compare the existing schema
block against what it would generate and skip the rewrite when the
column list matches — even when the *ordering* of those columns differs.
The result is that models which haven't had a schema change since the
config landed never get reordered, and `classified_sort` drift
accumulates indefinitely.

`--force` makes annotaterb always rewrite, so a single `bin/rake
annotate:clean` run brings every model into the canonical format and
keeps them there. Every schema block is now grouped primary-key →
regular columns → timestamps → foreign keys (alphabetical within each
group). Pure annotation comment change — no code modifications.

Also cleans up the rake task to avoid string interpolation for `system`
calls.
2026-05-13 14:12:48 +01:00

97 lines
3 KiB
Ruby

# frozen_string_literal: true
class CategoryFeaturedTopic < ActiveRecord::Base
belongs_to :category
belongs_to :topic
NEXT_CATEGORY_ID_KEY = "category-featured-topic:next-category-id"
DEFAULT_BATCH_SIZE = 100
# Populates the category featured topics.
def self.feature_topics(batched: false, batch_size: nil)
current = {}
CategoryFeaturedTopic
.select(:topic_id, :category_id)
.order(:rank)
.each { |f| (current[f.category_id] ||= []) << f.topic_id }
batch_size ||= DEFAULT_BATCH_SIZE
next_category_id = batched ? Discourse.redis.get(NEXT_CATEGORY_ID_KEY).to_i : 0
categories =
Category
.select(:id, :topic_id, :num_featured_topics)
.where("id >= ?", next_category_id)
.order("id ASC")
.limit(batch_size)
.to_a
if batched
if categories.length == batch_size
next_id = Category.where("id > ?", categories.last.id).order(:id).pick(:id)
next_id ? Discourse.redis.setex(NEXT_CATEGORY_ID_KEY, 1.day, next_id) : clear_batch!
else
clear_batch!
end
end
categories.each { |c| CategoryFeaturedTopic.feature_topics_for(c, current[c.id] || []) }
end
def self.clear_batch!
Discourse.redis.del(NEXT_CATEGORY_ID_KEY)
end
def self.feature_topics_for(c, existing = nil)
return if c.blank?
query_opts = {
per_page: c.num_featured_topics,
except_topic_ids: [c.topic_id],
visible: true,
no_definitions: true,
}
# It may seem a bit odd that we are running 2 queries here, when admin
# can clearly pull out all the topics needed.
# We do so, so anonymous will ALWAYS get some topics
# If we only fetched as admin we may have a situation where anon can see
# no featured topics (all the previous 2x topics are only visible to admins)
# Add topics, even if they're in secured categories or invisible
query = TopicQuery.new(Discourse.system_user, query_opts)
results = query.list_category_topic_ids(c)
# Add some topics that are visible to everyone:
anon_query = TopicQuery.new(nil, query_opts.merge(except_topic_ids: [c.topic_id] + results))
results += anon_query.list_category_topic_ids(c)
results.uniq!
return if results == existing
CategoryFeaturedTopic.transaction do
CategoryFeaturedTopic.where(category_id: c.id).delete_all
results.each_with_index do |topic_id, idx|
c.category_featured_topics.create(topic_id: topic_id, rank: idx)
end
end
end
end
# == Schema Information
#
# Table name: category_featured_topics
#
# id :integer not null, primary key
# rank :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
# category_id :integer not null
# topic_id :integer not null
#
# Indexes
#
# cat_featured_threads (category_id,topic_id) UNIQUE
# index_category_featured_topics_on_category_id_and_rank (category_id,rank)
#