Created
February 17, 2025 12:15
-
-
Save ajazfarhad/b5006ed06480fae63ff1195ab45d5495 to your computer and use it in GitHub Desktop.
DeepDupable: A Rails Concern for Deep Duplicating Models with Associations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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