Skip to content

Instantly share code, notes, and snippets.

@ajazfarhad
Created February 17, 2025 12:15
Show Gist options
  • Save ajazfarhad/b5006ed06480fae63ff1195ab45d5495 to your computer and use it in GitHub Desktop.
Save ajazfarhad/b5006ed06480fae63ff1195ab45d5495 to your computer and use it in GitHub Desktop.
DeepDupable: A Rails Concern for Deep Duplicating Models with Associations
# The DeepDupable module provides a reusable `dup_with_assoc` method that allows deep duplication of ActiveRecord models, including their associations. It also supports prefixing a specified attribute (e.g., appending "Copy" to a title) and provides an option to automatically save the duplicated record.
###
# Deep Duplication: Clones the record along with all associated records (except `belongs_to` associations).
# ActionText Support: Handles duplication of ActionText::RichText fields properly.
# Attribute Prefixing: Allows adding a prefix to a specified attribute (e.g., "Copy of Title").
# Configurable Save: Can return an unsaved record or save it automatically.
module DeepDupable
extend ActiveSupport::Concern
# Method for duplicating a record and applying a prefix to a given attribute.
def dup_with_assoc(prefix_attr: nil, prefix: nil, auto_save: true)
new_record = dup
apply_prefix(new_record, prefix_attr, prefix) if prefix_attr && prefix
duplicate_associations(new_record)
auto_save ? save_record!(new_record) : new_record
end
private
def save_record!(new_record)
new_record.save!
new_record
end
# Apply prefix to the specified attribute if it's valid and present
def apply_prefix(new_record, prefix_attr, prefix)
validate_prefix(prefix_attr, prefix)
new_record.public_send("#{prefix_attr}=", "#{prefix} #{new_record.public_send(prefix_attr)}")
end
# Validate prefix attributes for proper setup
def validate_prefix(prefix_attr, prefix)
unless respond_to?(prefix_attr.to_sym)
raise ArgumentError, "Undefined attribute '#{prefix_attr}' on the model."
end
unless prefix
raise ArgumentError, "Prefix is required when prefix_attr is provided."
end
end
# Duplicate all associations, skipping belongs_to and blank associations
def duplicate_associations(new_record)
self.class.reflect_on_all_associations.each do |association|
next if association.belongs_to? # Skip belongs_to associations.
associated_records = public_send(association.name)
next if associated_records.blank? # Skip empty associations.
duplicate_association(new_record, association, associated_records)
end
end
# Handle duplication of individual associations
def duplicate_association(new_record, association, associated_records)
if association.class_name == "ActionText::RichText"
new_record.public_send("build_#{association.name}", body: associated_records.body.to_s)
else
new_record.public_send("#{association.name}=", associated_records.dup)
end
end
end
# class Post < ApplicationRecord
# include DeepDupable
# has_rich_text :content
# belongs_to :category
# belongs_to :user
# has_many :comments
# => post = Post.find(1)
# => new_post = post.dup_with_assoc(prefix_attr: :title, prefix: "Copy of", auto_save: true)
# => puts new_post.title # => "Copy of Original Title"
require 'rails_helper'
RSpec.describe Post, type: :model do
let(:category) { Category.create(name: 'Feature') }
let(:user) { User.create! email_address: '[email protected]', password: '12345678' }
let!(:original_post) { Post.create(
title: "Original Title",
content: "<p>Original rich text content</p>",
category: category,
status: "draft",
publication_date: Time.current,
user: user
) }
let!(:comment) { Comment.create(post_id: original_post.id, body: "Great post!") }
describe '#dup_with_assoc' do
context 'when duplicating a record with a prefix' do
it 'duplicates the post and applies the prefix to the title' do
new_post = original_post.dup_with_assoc(prefix_attr: :title, prefix: "Copy of")
# Check that the title is duplicated and prefix is applied
expect(new_post.title).to eq("Copy of Original Title")
expect(new_post.title).not_to eq(original_post.title) # Ensure title is not the same
expect(new_post.comments.count).to eq(1)
expect(new_post.category.name).to eq(original_post.category.name)
end
end
context 'when duplicating ActionText content' do
it 'duplicates the ActionText content correctly' do
new_post = original_post.dup_with_assoc(prefix_attr: :title, prefix: "Copy of")
content_text = Nokogiri::HTML.fragment(new_post.content.body.to_html).css('p').text
expect(content_text).to eq("Original rich text content")
end
end
context 'when no prefix is provided' do
it 'duplicates the post without applying any prefix' do
new_post = original_post.dup_with_assoc(prefix_attr: :title, prefix: nil)
# Ensure the title is just duplicated without a prefix
expect(new_post.title).to eq(original_post.title)
end
end
context 'when auto_save is false' do
it 'does not save the new record' do
new_post = original_post.dup_with_assoc(prefix_attr: :title, prefix: "Copy of", auto_save: false)
# Ensure the new post is not saved
expect(new_post.persisted?).to eq(false)
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment