Jacob Brown
Jacob Brown

Reputation: 7561

How to use `first_or_initialize` step with `accepts_nested_attributes_for` - Mongoid

I'd like to incorporate a step to check for an existing relation object as part of my model creation/form submission process. For example, say I have a Paper model that has_and_belongs_to_many :authors. On my "Create Paper" form, I'd like to have a authors_attributes field for :name, and then, in my create method, I'd like to first look up whether this author exists in the "database"; if so, then add that author to the paper's authors, if not, perform the normal authors_attributes steps of initializing a new author.

Basically, I'd like to do something like:

# override authors_attributes
def authors_attributes(attrs)
  attrs.map!{ |attr| Author.where(attr).first_or_initialize.attributes }
  super(attrs)
end

But this doesn't work for a number of reasons (it messes up Mongoid's definition of the method, and you can't include an id in the _attributes unless it's already registered with the model).

I know a preferred way of handling these types of situations is to use a "Form Object" (e.g., with Virtus). However, I'm somewhat opposed to this pattern because it requires duplicating field definitions and validations (at least as I understand it).

Is there a simple way to handle this kind of behavior? I feel like it must be a common situation, so I must be missing something...

Upvotes: 0

Views: 858

Answers (3)

Jacob Brown
Jacob Brown

Reputation: 7561

I followed the suggestion of the accepted answer for this question and implemented a reject_if guard on the accepts_nested_attributes_for statement like:

accepts_nested_attributes_for :authors, reject_if: :check_author

def check_author(attrs)
  if existing = Author.where(label: attrs['label']).first
    self.authors << existing
    true
  else
    false
  end
end

This still seems like a hack, but it works in Mongoid as well...

Upvotes: 0

user1454117
user1454117

Reputation:

The way I've approached this problem in the past is to allow existing records to be selected from some sort of pick list (either a search dialog for large reference tables or a select box for smaller ones). Included in the dialog or dropdown is a way to create a new reference instead of picking one of the existing items.

With that approach, you can detect whether the record already exists or needs to be created. It avoids the need for the first_or_initialize since the user's intent should be clear from what is submitted to the controller.

This approach struggles when users don't want to take the time to find what they want in the list though. If a validation error occurs, you can display something friendly for the user like, "Did you mean to pick [already existing record]?" That might help some as well.

Upvotes: 2

raviolicode
raviolicode

Reputation: 2175

If I have a model Paper:

class Paper
  include Mongoid::Document

  embeds_many :authors
  accepts_nested_attributes_for :authors

  field :title, type: String
end

And a model Author embedded in Paper:

class Author
  include Mongoid::Document

  embedded_in :paper, inverse_of: :authors

  field :name, type: String
end

I can do this in the console:

> paper = Paper.create(title: "My Paper")

> paper.authors_attributes = [ {name: "Raviolicode"} ]

> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Raviolicode">]

> paper.authors_attributes = [ {id: paper.authors.first, name: "Lucia"}, {name: "Kardeiz"} ]

> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Lucia">, #<Author _id: 531cd95931302ea603010000, name: "Kardeiz">]

As you can see, I can update and add authors in the same authors_attributes hash.

For more information see Mongoid nested_attributes article

Upvotes: 0

Related Questions