Skip to content

Commit

Permalink
add canonical topic with admin tools (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrienpoly committed Aug 17, 2024
1 parent db3a790 commit 0089087
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 3 deletions.
23 changes: 23 additions & 0 deletions app/avo/actions/assign_canonical_topic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Avo::Actions::AssignCanonicalTopic < Avo::BaseAction
self.name = "Assign Canonical Topic"
# self.visible = -> do
# true
# end

def fields
field :topic_id, as: :select, name: "Canonical topic",
help: "The name of the topic to be set as canonical",
options: -> { Topic.order(:name).pluck(:name, :id) }
end

def handle(query:, fields:, current_user:, resource:, **args)
canonical_topic = Topic.find(fields[:topic_id])

query.each do |record|
record.assign_canonical_topic!(canonical_topic: canonical_topic)
end

succeed "Assigning canonical topic #{canonical_topic.name} to #{query.count} topics"
redirect_to avo.resources_topic_path(canonical_topic)
end
end
1 change: 1 addition & 0 deletions app/avo/resources/topic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def fields
def actions
action Avo::Actions::ApproveTopic
action Avo::Actions::RejectTopic
action Avo::Actions::AssignCanonicalTopic
end

def filters
Expand Down
29 changes: 27 additions & 2 deletions app/models/topic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,48 @@ class Topic < ApplicationRecord

has_many :talk_topics
has_many :talks, through: :talk_topics
belongs_to :canonical, class_name: "Topic", optional: true
has_many :aliases, class_name: "Topic", foreign_key: "canonical_id"

# validations
validates :name, presence: true, uniqueness: true
validates :canonical, exclusion: {in: ->(topic) { [topic] }, message: "can't be itself"}

# normalize attributes
normalizes :name, with: ->(name) { name.squish }

# scopes
scope :with_talks, -> { joins(:talks).distinct }
scope :without_talks, -> { where.missing(:talk_topics) }
scope :canonical, -> { where(canonical_id: nil) }
scope :with_aliases, -> { where.not(canonical_id: nil) }

# enums
enum :status, %w[pending approved rejected].index_by(&:itself)
enum :status, %w[pending approved rejected duplicate].index_by(&:itself)

def self.create_from_list(topics, status: :pending)
topics.map { |topic|
Topic.find_by(name: topic) || Topic.find_or_create_by(name: topic, status: status)
Topic.find_by(name: topic)&.primary_topic || Topic.find_or_create_by(name: topic, status: status)
}.uniq
end

def assign_canonical_topic!(canonical_topic:)
ActiveRecord::Base.transaction do
self.canonical = canonical_topic
save!

talk_topics.each do |talk_topic|
talk_topic.update(topic: canonical_topic)
end

# We need to destroy the remaining topics. They can be remaining topics given the unicity constraint
# on the talk_topics table. the update above swallows the error if the talk_topic duet exists already
TalkTopic.where(topic_id: id).destroy_all
duplicate!
end
end

def primary_topic
canonical || self
end
end
5 changes: 5 additions & 0 deletions db/migrate/20240817083428_add_canonical_reference_to_topic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddCanonicalReferenceToTopic < ActiveRecord::Migration[7.2]
def change
add_reference :topics, :canonical, foreign_key: {to_table: :topics}
end
end
5 changes: 4 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions test/models/topic_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,33 @@ class TopicTest < ActiveSupport::TestCase
Topic.create_from_list(topics)
end
end

test "can assign_canonical_topic!" do
@talk = talks(:one)
duplicate_topic = Topic.create(name: "Rails")
TalkTopic.create!(topic: duplicate_topic, talk: @talk)
canonical_topic = Topic.create(name: "Ruby on Rails")

assert duplicate_topic.talks.ids.include?(@talk.id)
duplicate_topic.assign_canonical_topic!(canonical_topic: canonical_topic)

assert_equal canonical_topic, duplicate_topic.reload.canonical
assert duplicate_topic.duplicate?
assert duplicate_topic.reload.talks.empty?
assert canonical_topic.reload.talks.ids.include?(@talk.id)
end

test "create_from_list with canonical topic" do
topics = ["Rails", "Ruby on Rails"]
canonical_topic = Topic.create(name: "Ruby on Rails", status: :approved)
topic = Topic.create(name: "Rails", canonical: canonical_topic, status: :duplicate)

assert_equal topic.primary_topic, canonical_topic
assert_no_changes "Topic.count" do
@topics = Topic.create_from_list(topics)
end

assert_equal 1, @topics.length
assert_equal "Ruby on Rails", @topics.first.name
end
end

0 comments on commit 0089087

Please sign in to comment.