Last active
January 19, 2025 04:46
-
-
Save JamesAndresCM/323decc99c6c12e24f8f4954289dc1bd to your computer and use it in GitHub Desktop.
implement nested form instead nested attributes
This file contains 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 '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