I am trying to build a polymorphic relationship from a nested form that's backwards to all the examples I've found. I am hoping someone to point out the error of my ways.
class Container < ActiveRecord::Base
belongs_to :content, :polymorphic => true
end
class Notice < ActiveRecord::Base
has_one :container, :as => :content
end
class Form < ActiveRecord::Base
has_one :container, :as => :content
end
It seems most people would build a Container from a Notice or Form, but in my case the notice or form contains a small amount of content (file location or a couple db fields) so it's much dry'er to build the Notice or Form from the Container.
I thought I could solve by adding accepts_nested_attributes_for :content but that gives me an unrecognized attribute :notice when I try to create a Container with a nested Notice (looking for content, not the polymorphic association)
I can do it manually and explicitly exclude the nested fields like
if params[:container].has_key('notice')
#c = Container.new(params[:container].except(:notice))
and then build, but isn't that a smell? Is there a better way?
Thank you for reading!
Nested attributes are designed to work from the parent down to the children, not the other way around. Moreover, in this scenario, how would nested attributes know whether you are trying to create a Notice or Form object?
If you find it DRYer to build the content from the container, you probably have your associations inside out - try changing your schema to:
class Container < ActiveRecord::Base
has_one :notice
has_one :form
end
class Notice < ActiveRecord::Base
belongs_to :container
end
class Form < ActiveRecord::Base
belongs_to :container
end
You can use validation to ensure only one child (:notice or :form) is actually associated if need be.
Related
I have an issue creating a new record with nested associations in a clean way. Here's the controller code:
#listing = current_user.listings.build(params[:listing].permit(ATTRIBUTES_FOR_CREATE))
This builds an entity with several nested associations, like this:
class ListingDataField < ActiveRecord::Base
belongs_to :listing
validates_presence_of :listing
end
However, when I do #listing.save in controller, I get validation errors on those nested ListingDataField entities that 'listing can't be blank'. If I understand correctly, AutosaveAssociation first validates and saves nested associations, and eventually saves top-level entity. Thus, it fails validating ListingDataField, because Listing is not yet saved.
But I believe it's right having :listing validation in ListingDataField, so I wouldn't consider removing this. I can see 2 solutions:
in transaction - save Listing record, then build nested associations
one by one
#listing.save(:validate => false) but this is too ugly
Both aren't as much elegant as current_user.listings.build(...), so my question is - what is the proper Rails way for this?
P.S. I searched SO for similar question but I couldn't find any, hopefully this is not a duplicate :)
Have you tried adding:
class ListingDataField < ActiveRecord::Base
belongs_to :listing, inverse_of: :listing_data_fields
validates :listing, presence: true
end
and
class Listing < ActiveRecord::Base
has_many :listing_data_fields, inverse_of: :listing
end
This should make validation of presence work.
invoice_serializer.rb
class InvoiceSerializer < ActiveModel::Serializer
attributes :id, :document_no, :customer_id, :currency_id, :date,
:due_date, :notes, :invoice_status_id, :total, :tax_total, :grand_total
# This is not working, associated objects are still rendered
unless #object.respond_to? :count
has_many :invoice_lines
end
has_many :invoice_payments
has_one :customer
has_one :invoice_status
end
This is my serializer class. I have two 'has_many' associations and I don't want them in a collection. I only want them in singular form. Just to be more clear, I don't want has_many association in invoices#index, it affects performance in a bad way but I want them in invoices#show, invoices#edit actions.
How do I achieve that? How can I associate models conditionally?
My gut reaction would be to create another serializer and extend this one, adding has_many :invoice_payments to it.
Then simply use the extended controller when you want the associations, and the original one when you don't.
def InvoiceIndexSerializer < InvoiceSerializer
has_many :invoice_payments
end
(NB: untested)
This way your serializer stays ignorant of an outside state like which action you're using.
I've created a complex nested form, but I'm having trouble with the part where my app creates/edits records based on the input from that form correctly.
To-Do scenario: I need to enter Tasks one-by-one into my form (one task per "submit" press), from a pile of little pieces of papers with tasks written on them. Each Task has a :project_number, a :project_number_type, and an :owner written on it. In my app, these attributes are in the models ProjectNumber (for the first two) and Owner, for the last one. (I had to make a separate ProjectNumber model because my Projects can at times have multiple :project_numbers of different project_number_types.)
class Owner < ActiveRecord::Base
has_many :projects
has_many :tasks, :through => :projects
end
class Project < ActiveRecord::Base
belongs_to :owner
has_many :tasks
has_many :project_numbers
end
class ProjectNumber < ActiveRecord::Base
belongs_to :project
end
class Task < ActiveRecord::Base
belongs_to :project
delegate :owner, :to => :project, :allow_nil => true
end
My form shows the correct nested fields_for fields for each of the attributes mentioned above. (I omitted things like my accepts_nested_attributes_for).
2 related (I think) issues:
My form never creates any new projects! The Cocoon gem I'm using to create nested forms has me creating form partials for each nested model. In the Project partial, since my Project model doesn't have a :number attribute itself, but instead has an association to the ProjectNumbers model which does, I just have a reference to the ProjectNumbers partial and its attributes.
I need my app to edit an existing Project with the new Task if the Task's :project_number, :project_number_type, and :owner all match those associated with an existing Project. Otherwise, it should create a new Project for that Task to belong to.
I read lots of little bits of code here and elsewhere, but I need something just a little more, maybe that includes the "what" as in .first_or_initialize and also the "where", like as in /models/task.rb, or tasks_controller.rb? Maybe I need to create something custom? Thanks so much!
Edit: Here are the form partials
==== /views/tasks/_form.html.slim
= simple_form_for #task do |f|
#project
= f.simple_fields_for :project do |project|
= render 'project_fields', :f => project
==== /views/tasks/_project_fields.html.slim
.nested-fields
.project_numbers
= f.simple_fields_for :project_numbers do |project_number|
= render 'project_number_fields', :f => project_number
I've got my models setup for a many-to-many relationship:
class Workshop < ActiveRecord::Base
has_many :workshop_students
has_many :students, :through => :student_workshops
accepts_nested_attributes_for :students
end
class Student < ActiveRecord::Base
has_many :student_workshops
has_many :workshops, :through => :student_workshops
accepts_nested_attributes_for :products
end
class StudentWorkshop < ActiveRecord::Base
belongs_to :student
belongs_to :workshop
end
As you can see above, a student can have many workshops and workshop can have many students.
I've looked at the following Rails casts: here and here. And most of the online sources I stumble across only show how to do nested forms for creating new objects within the parent form.
I don't want to create a new object. I only want to add an existing object to the parent form. So for example. If I decide to create a new workshop, I'd like to assign existing students to the workshop.
One thing I don't understand is, how do I link students into the workshop form? Second, when the params are passed, what should be in the controller method for update/create?
If anyone can point me to the right direction, I would appreciate it.
The easiest thing to do is:
<%= f.collection_select(:student_ids, Student.all, :id, :name, {:include_blank => true}, {:selected => #workshop.student_ids, :multiple => true} )%>
You should not have to do anything in the create action.
Ok, for anyone coming across the same issue in the future. The solution I came up with was in def create. I am able to access a POST attribute called student_ids, which comes in the form of an array
I'm using accepts_nested_attributes_for in one of my Rails models, and I want to save the children after creating the parent.
The form works perfectly, but the validation is failing. For simplicity's sake imagine the following:
class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks
end
class Task < ActiveRecord::Base
belongs_to :project
validates_presence_of :project_id
validates_associated :project
end
And I am running:
Project.create!(
:name => 'Something',
:task_attributes => [ { :name => '123' }, { :name => '456' } ]
)
Upon saving the project model, the validation is failing on the tasks because they don't have a project_id (since the project hasn't been saved).
It seems like Rails is following the pattern below:
Validate Project
Validate Tasks
Save Project
Save Tasks
The pattern should be:
Validate Project
On Pass: Save Project and continue...
Validate Tasks
On Pass: Save Tasks
On Fail: Delete Project (rollback maybe?)
So my question boils down to: How can I get Rails to run the project_id= (or project=) method and validation on the children (tasks) AFTER the parent (project) has been saved, but NOT save the parent (project) model if any child (task) is invalid?
Any ideas?
Use :inverse_of and validates_presence_of :parent. This should fix your validation problem.
class Dungeon < ActiveRecord::Base
has_many :traps, :inverse_of => :dungeon
end
class Trap < ActiveRecord::Base
belongs_to :dungeon, :inverse_of => :traps
validates_presence_of :dungeon
end
http://apidock.com/rails/ActiveModel/Validations/HelperMethods/validates_presence_of
https://github.com/rails/rails/blob/73f2d37505025a446bb5314a090f412d0fceb8ca/activerecord/test/cases/nested_attributes_test.rb
Use this answer for Rails 2, otherwise see below for the :inverse_of answer
You can work around this by not checking for the project_id if the associated project is valid.
class Task < ActiveRecord::Base
belongs_to :project
validates_presence_of :project_id, :unless => lambda {|task| task.project.try(:valid?)}
validates_associated :project
end
Only validate the relationship, not the ID:
class Task < ActiveRecord::Base
belongs_to :project
validates_presence_of :project
end
As soon as the association is populated, ActiveRecord will consider the validation to have succeeded, whether or not the model is saved. You might want to investigate autosaving as well, to ensure the task's project is always saved:
class Task < ActiveRecord::Base
belongs_to :project, :autosave => true
validates_presence_of :project
end
Unfortunately none of the above suggestions work for me with Rails 2.3.5.
In my case, the project in a task is always nil if both are created using nested attributes. Only if I remove the validates_presence_of, the create goes through successfully. The unit test and the log show that all is created correctly.
So I'd now tend to add constraints to the DB instead of Rails as that seems to be more reliable in the first place.
You could just create the project and only add the projects if it passes validation:
tasks = params.delete(:task_attributes)
if Project.create(params)
Project.update_attributes(:task_attributes => tasks)
end
Ciao
Contrary to what bigo suggests, it's not always acceptable to save the parent object first and then the children. Usually you want to make sure all objects validate before you start saving them. That gives the user the chance to re-edit the input form and correct any errors.
The problem you describe will be fixed in Rails 3.0. I would have posted a link to the Lighthouse ticket, but stackoverflow.com does not allow this because I'm a new user (#fail). But for the time being, you can use the plugin "parental_control", which will fix your "bug".