Skip to content

Instantly share code, notes, and snippets.

@JamesAndresCM
Last active January 19, 2025 04:46
Show Gist options
  • Save JamesAndresCM/323decc99c6c12e24f8f4954289dc1bd to your computer and use it in GitHub Desktop.
Save JamesAndresCM/323decc99c6c12e24f8f4954289dc1bd to your computer and use it in GitHub Desktop.
implement nested form instead nested attributes
require 'bundler/inline'
gemfile(true) do
source 'https://rubygems.org'
gem 'rails', '7.2.0'
gem 'activemodel', '~> 7.2'
gem 'sqlite3', '2.5.0'
gem 'byebug'
end
require 'active_record'
require 'minitest/autorun'
require 'logger'
# Configuración de la base de datos
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)
# Esquema de la base de datos
ActiveRecord::Schema.define do
create_table :articles, force: true do |t|
t.string :title
t.text :body
end
create_table :comments, force: true do |t|
t.text :content
t.integer :article_id
end
create_table :topics, force: true do |t|
t.string :name
t.integer :comment_id
end
end
# Modelos
class Article < ActiveRecord::Base
has_many :comments, dependent: :destroy
end
class Comment < ActiveRecord::Base
belongs_to :article
has_many :topics, dependent: :destroy
validates :content, uniqueness: true
end
class Topic < ActiveRecord::Base
belongs_to :comment
end
# Form Objects
class ArticleForm
include ActiveModel::Model
attr_accessor :title, :body, :comments_attributes
attr_reader :article
def initialize(article = Article.new)
@article = article
self.title = article.title
self.body = article.body
self.comments_attributes = article.comments.map { |comment| CommentForm.new(comment) }
end
def update(attributes)
assign_attributes(attributes)
return false unless valid?
ActiveRecord::Base.transaction do
remove_missing_comments!
article.update!(title: title, body: body)
update_comments!
end
true
rescue ActiveRecord::RecordInvalid
false
end
def assign_attributes(attributes)
self.title = attributes[:title]
self.body = attributes[:body]
self.comments_attributes = attributes[:comments_attributes].map do |comment_attrs|
existing_comment = article.comments.find_by(id: comment_attrs[:id])
CommentForm.new(existing_comment || article.comments.build, comment_attrs)
end
end
def valid?
comments_valid = comments_attributes.all?(&:valid?)
article.assign_attributes(title: title, body: body)
article.valid? && comments_valid
end
private
def update_comments!
comments_attributes.each(&:save!)
end
def remove_missing_comments!
existing_ids = comments_attributes.map(&:id).compact
article.comments.where.not(id: existing_ids).destroy_all
end
end
class CommentForm
include ActiveModel::Model
attr_accessor :id, :content, :topics_attributes
attr_reader :comment
def initialize(comment, attributes = {})
@comment = comment
self.id = comment.id
self.content = attributes[:content] || comment.content
# Crear o actualizar los temas correctamente
self.topics_attributes = attributes[:topics_attributes]&.map do |topic_attrs|
# Crear o actualizar temas, asegurándose de que sean persistidos correctamente
existing_topic = comment.topics.find_by(id: topic_attrs[:id])
TopicForm.new(existing_topic || comment.topics.build, topic_attrs)
end || []
end
def valid?
remove_missing_topics!
topics_valid = topics_attributes.all?(&:valid?)
comment.assign_attributes(content: content)
comment.valid? && topics_valid
end
def save!
# Guardar el comentario y los temas asociados
comment.save!
update_topics!
end
private
def update_topics!
# Aquí se asegura que cada tema sea guardado
topics_attributes.each(&:save!)
end
def remove_missing_topics!
existing_ids = topics_attributes.map(&:id).compact
comment.topics.where.not(id: existing_ids).destroy_all
end
end
class TopicForm
include ActiveModel::Model
attr_accessor :id, :name
attr_reader :topic
def initialize(topic, attributes = {})
@topic = topic
self.id = topic.id
self.name = attributes[:name] || topic.name
end
def valid?
topic.assign_attributes(name: name)
topic.valid?
end
def save!
topic.save!
end
end
# Tests
class BugTest < Minitest::Test
def test_updating_articles_with_comments_and_topics
article = Article.create!(title: 'Old Title', body: 'Old Body')
comment = article.comments.create!(content: 'Old Comment')
topic = comment.topics.create!(name: 'Old Topic')
comment_two = article.comments.create!(content: "delete this")
topic_two = comment.topics.create!(name: "delete this")
form = ArticleForm.new(article)
updated = form.update(
title: 'New Title',
body: 'New Body',
comments_attributes: [
{
id: comment.id,
content: 'Updated Comment',
topics_attributes: [
{ id: topic.id, name: 'Updated Topic' },
{ name: 'New Topic' }
]
},
{ content: 'New Comment', topics_attributes: [{ name: 'Another Topic' }] }
]
)
# Recargar el artículo y sus comentarios para obtener los cambios de la base de datos
article.reload
assert updated, "Form update should succeed"
assert_equal 'New Title', article.title
assert_equal 'Updated Comment', article.comments.first.content
# Recargar el primer comentario para ver los cambios
article.comments.first.reload
assert_equal 2, article.comments.first.topics.count # Ahora esperamos 2 temas en el primer comentario
# Verificar el segundo comentario
assert_equal 'New Comment', article.comments.last.content
assert_equal 1, article.comments.last.topics.count # Solo 1 tema para el segundo comentario
# Verificar que no existan los records no enviados dentro del payload
assert_nil Topic.find_by_id topic_two.id
assert_nil Comment.find_by_id comment_two.id
end
def test_validation_error
article = Article.create!(title: 'Test Article', body: 'Test Body')
article.comments.create!(content: 'Comment 1')
form = ArticleForm.new(article)
updated = form.update(
comments_attributes: [
{ content: 'Comment 1' }, # Duplicate content
{ content: 'Comment 1' }
]
)
refute updated, "Form update should fail due to validation error"
assert_equal 1, article.comments.count
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment