Building nested attributes from form checkboxes in a model callback? - ruby-on-rails

I am building a simple sample tracker for a small analytical lab and I'm bashing my head against a problem. Structurally, the database consists of users, batches, samples, instruments, and assays. Please excuse messy or poor code, I'm new to Rails and programming. Generally I know where I'm being sloppy and how to fix it, it's just a matter of me breaking a problem down. Here's some explicit abridged code:
class Batch
belongs_to :user
has_many :samples
has_many :submissions
has_many :assays, :through => :submissions
accepts_nested_attributes_for :samples
accepts_nested_attributes_for :submissions
end
class Assay
belongs_to :instrument
has_many :submissions
has_many :batches, :through => :submissions
end
class Submission
belongs_to :batches
belongs_to :assays
end
I have created a batch submission form that accepts batch information, sample names as a nested attribute, and I'm trying to accept submissions as a nested attribute as well. My form currently looks like this (Please note comment line):
= semantic_form_for #batch do |f|
- #instruments.each do |instrument|
%ol
%li
= instrument.instrument_name
%ol
- instrument.assays.each do |assay|
%li
= assay.assay_name
# Some magic check boxes that fix my problem.
= f.inputs :name => "Batch Info" do
= f.input :sampling_date, :required => false
= f.input :experiment_name, :required => false
= f.input :notes, :as => :text
= f.semantic_fields_for :samples do |sample_form|
= sample_form.input :sample_name
= f.buttons do
= f.commit_button :label => "Submit batch"
What I would like to have is a check box for each assay that passes the assay_ids to the submissions table as a nested batch attribute, magically making sure that each batch is associated with the chosen assays.
My problem is deciding when and how to build the submissions in the controller/model and how to populate them. It seems like I should make some sort of paramater that holds the assay_ids, and use some sort of model callback (before_create or before_save) that builds a params[assay_ids].length number of dummy submissions that I can fill with the assay_id params. Unfortunately all my attempts at building a method that does this have been futile.
I've watched all pertinent Railscasts I could find, read API entries, and I have been grinding on this for far too many hours. I can feel the answer on the very tip of my brain, and I am in desperate need of an Aha! moment. Thanks for any help you can provide!

Notice: This method is very brittle! I worked out a solution to make batches a true nested attribute and I will write it up ASAP.
So after a day of thinking the answer popped into my head (I swear I use Stack Overflow to officially start a 12 hour timer on when I'll epiphanically figure out what I got wrong). Here's how I did it:
Essentially I was trying to only make as many submissions as I needed so I didn't end up crapping my database up with empty ones, but I also wanted them built on the fly in-form. Because I didn't want to shove a bunch of extra logic into my view, I set up my magic check box as check_box_tag "assay_ids[]", assay.id to make a nice param separate from the batches param. Then, in my batches controller under create I built a little helper method:
def build_submissions(params)
#submitted_assays = params[:assay_ids]
for submitted_assay in #submitted_assays do
#batch.submissions.build(:assay_id => submitted_assay)
end
end
That builds up just as many submissions as I need. This fix works wonderfully, but I would welcome any suggestions as to how to solve the problem differently.

Related

Rails polymorphic association in a form

This is a slightly unique version of a polymorphic association. It's one of those "real world" problems that I'm struggling to solve and haven't come across many good answers so I made my own.
A Transaction record has many Tasks and each Task has an Assignee, which can be from multiple tables.
# Models
class Transaction < ApplicationRecord
has_many :tasks
has_many :borrowers
has_many :partners
# Combine into a single array to display on the collection_select
def assignees
borrowers + partners
end
end
class Task < ApplicationRecord
# has attribute :assignee_type_and_id (string)
belongs_to :transaction
# Reverse engineer single attribute into type/id parts
def assignee
if assignee_type_and_id
parts = assignee_type_and_id.split(".")
type = parts.first
id = parts.last
if type.to_s.downcase == "borrower"
Borrower.find(id)
elsif type.to_s.downcase == "partner"
Partner.find(id)
end
end
end
end
class Borrower < ApplicationRecord
# has attribute :name
belongs_to :transaction
def type_and_id
"borrower.#{id}"
end
end
class Partner < ApplicationRecord
# has attribute :name
belongs_to :transaction
def type_and_id
"partner.#{id}"
end
end
On the Task form pages, I want a single HTML select that combines BOTH the Borrowers and Partners.
Classic polymorphism says to add an assignee_type column, but now I'm working with 2 fields instead of one.
My solution is to combine these 2 into a single select such that the final value is of the format assignee_type.assignee_id.
# form.html.erb
<%= form_for #task do |f| %>
<%= f.label :assignee_type_and_id, "Assignee" %>
<%= f.collection_select :assignee_type_and_id, #transaction.assignees, :name, :type_and_id %>
<% end %>
When the form is submitted, it POSTs values in the format borrower.123, partner.57, etc, and that value gets stored in the DB column.
When I want to retrieve the actual task's Assignee, I have to do a little reverse engineering as noted above in the Task#assignee method.
Question
Is there a more appropriate way to do this? I came up with this myself, which scares me because I know problems like this must have been solved by people much smarter than me...
Is there a way to make this work with "normal" polymorphism instead of forcing my own hybrid version?
Update
I happened upon Rails 4.2+ GlobalID, which seems to do this very thing. Unless there's a reason not to use that, I may use that implementation instead of my own "bastardized" version. Is there any better solution to a situation like this?
For these type of problems where a form spans multiple models/complex associations I use a form backing object. It keeps everything clean and modular. Here is a good write up: https://content.pivotal.io/blog/form-backing-objects-for-fun-and-profit

Ruby on Rails 4 Nested Forms with Active Record

I am new to Rails and just building my first app (coming from a PHP and .NET background and loving it btw) but I have run into a problem that I am struggling to find an answer to, even though I am sure there is an easy one!!
My project has 3 main Models; Locations, Services and Location Services
There are multiple services available and a Location can have any number of them. Basically I am using a record in Locations Services to store the ID of the selected service and the ID of the location.
A simplified version of my models are as below:
class Location < ActiveRecord::Base
has_many :location_services
end
class Service < ActiveRecord::Base
has_many :location_services
end
class LocationService < ActiveRecord::Base
belongs_to :location
belongs_to :service
end
I have read up about nested forms and using 'accepts_nested_attributes_for' to allow sub forms to edit data taken from another model which sounds very similar to what I want, except I don't want to just be able to edit the Location Services that I have, I want to be able to choose from every single available Service as checkboxes, then when checked and my Location is saved, I want it to create a record for each selected service in the Location Services table using the ID of the Location and the ID of the service
Im sure I could easily generate all the tickboxes with Services.all and then loop through that and then in my controller grab all of the ticked checkboxes from the POST, loop through them and build an array of all of them and then pass that array to Location.location_services.create([]) but this is rails and I feel like there is probably a better way to do it?
So firstly, am i going about this in a stupid way? Rather than having 3 tables, is there a better way of doing it? And is there a nice way of generating and saving all of the services?
Many thanks in advance
David
Many thanks Yan for your help on this one, I have finally managed to resolve my issue and it actually turned out to be really simple. I am posting here in the hope it helps someone else.
What I needed to do was add a has_many relation to services through location services so my model now looks like below:
class Location < ActiveRecord::Base
has_many :services, :through => :location_services
has_many :location_services
accepts_nested_attributes_for :location_services
end
I updated my view to include:
<%= f.collection_check_boxes(:service_ids, Service.all, :id, :name) do |b| %>
<%= b.label(class: "check_box") do %>
<%= b.check_box %>
<%= b.object.name %>
<% end %>
<% end %>
Then in my controller I have:
def location_params
params.require(:location).permit(:service_ids => [])
end
I have stripped out all of my other fields for simplicity. Then finally in the Update method, it is as simple as:
def update
#location.update(location_params)
redirect_to #location, notice: 'Location was successfully updated.'
end
Hope this helps someone out!!
Many thanks
David
A has_many relationship adds a number of methods to your model. From which you only need the collection_singular_ids method, which does the following:
Replace the collection with the objects identified by the primary keys
in ids. This method loads the models and calls collection=.
The above method can be combined with collection_check_boxes as explained in this tutorial. So in your case you'll have something like:
f.collection_check_boxes :location_service_ids, LocationService.all, :id, :name
Note that the last parameter (:name here) is the text_method_option which generates the labels for your check boxes.
Last but not least: don't forget to use accepts_nested_attributes properly.

Only allow several several values for has_many association

I have a homepage for my sports club wich consists of several departments.
At the moment the authorization system is realized by using cancan.
Each department can have multiple users and each user can belong to multiple departments:
#department.rb
has_and_belongs_to_many :users
\user.rb
has_and_belongs_to_many :departments
This works very well. But I want to have a possibility to administrate this association in the User form. There I have a group of checkboxes for each department. This is realized by this line (using simple_form):
<%= f.association :departments,
:as => :check_boxes,
:collection => Department.specific.order('name' => :asc),
:label_method => :name,
:value_method => :id %>
Now I only want to allow to change several values. On client side I can achieve this by simply hiding or disabling some checkboxes. But this is not save on server when I do not check it again.
The checkbox values will be transmitted by an array of ids. There is a huge potential to manipulate ids in this array.
On the server side I would have to check if the current user has the permission to assign a user to the departments. When he has no rights I need to leave this association unchanged.
Is there an elegant way to achieve this?
I am using Rails 4.0 with strong parameters.
Thanks
So your question is basically about server-side validation -- checking if a user can edit an association on the server side?
I'd look at setting dynamic strong params to start with, with a view to integrate into the model if it works:
#app/controllers/your_controller.rb
def update
#item = Model.find(params[:id])
#params = (can? :update, #item) ? "" : "departments: []"
end
private
def controller_params
params.require(:controller).permit(:attributes, #params.to_sym)
end

Custom Active Admin form inputs for has_and_belongs_to_many relationship

I have a very simple model
class Lifestyle < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :profiles
end
that has a has_and_belongs_to_many relationship with Profile
class Profile < ActiveRecord::Base
attr_accessible ...
belongs_to :occupation
has_and_belongs_to_many :lifestyles
accepts_nested_attributes_for :lifestyles
end
I want to use ActiveAdmin to edit the Profile object, but also assign Lifestyles to a profile. It should be similar to dealing with belongs_to :occupation, as this is sorted out automatically by ActiveAdmin to a dropbox with the options pre-filled with available occupations.
I've tried to use the has_many form builder method, but that only got me to show a form to type in the name of the Lifestyle and on submission, it returned an error.
f.object.lifestyles.build
f.has_many :lifestyles do |l|
l.input :name
end
Error I get:
Can't mass-assign protected attributes: lifestyles_attributes
The perfect way for me would be to build several checkboxes, one for each Lifestyle in the DB. Selected means that the lifestyle is connected to the profile, and unselected means to delete the relation.
I'm having great doubts that this is possible using ActiveAdmin and without having to create very complex logic to deal with this. I would really appreciate it if you'd give your opinion and advise me if I should go this way or approach it differently.
After some research, I am ready to answer my own question.
First, I have to say thanks to #Lichtamberg for suggesting the fix. However, that only complicates things (also regarding security, though not an issue in this case), and doesn't help me reach my ideal solution.
Digging more, I found out that this is a very common scenario in Rails, and it's actually explained in Ryan Bates' screencast no #17.
Therefore, in Rails, if you have a has_and_belongs_to_many (short form HABTM) association, you can easily set the ids of the other associated object through this method:
profile.lifestyle_ids = [1,2]
And this obviously works for forms if you've set the attr_accessible for lifestyle_ids:
class Profile < ActiveRecord::Base
attr_accessible :lifestyle_ids
end
In ActiveAdmin, because it uses Formtastic, you can use this method to output the correct fields (in this case checkboxes):
f.input :lifestyles, as: :check_boxes, collection: Lifestyle.all
Also, I have simplified my form view so it's now merely this:
form do |f|
f.inputs # Include the default inputs
f.inputs "Lifestlyes" do # Make a panel that holds inputs for lifestyles
f.input :lifestyles, as: :check_boxes, collection: Lifestyle.all # Use formtastic to output my collection of checkboxes
end
f.actions # Include the default actions
end
Ok, now this rendered perfectly in the view, but if I try and submit my changes, it gives me this database error:
PG::Error: ERROR: null value in column "created_at" violates not-null constraint
: INSERT INTO "lifestyles_profiles" ("profile_id", "lifestyle_id") VALUES (2, 1) RETURNING "id"
I found out that this is due to the fact that Rails 3.2 doesn't automatically update the timestamps for a HABTM association table (because they are extra attributes, and Rails only handles the _id attributes.
There are 2 solutions to fix this:
Either convert the association into a hm:t (has_many, :through =>)
Or remove the timestamps from the table
I'm going to go for 2) because I will never need the timestamps or any extra attributes.
I hope this helps other people having the same problems.
Edit: #cdesrosiers was closest to the solution but I already wrote this answer before I read his. Anyway, this is great nevertheless. I'm learning a lot.
Active Admin creates a thin DSL (Domain-Specific Language) over formtastic, so it's best to look at the formastic doc when you need form customization. There, you'll find that you might be able to use f.input :lifestyles, :as => :check_boxes to modify a has_and_belongs_to_many relationship.
I say "might" because I haven't tried this helper myself for your particular case, but these things have a tendency to just work automagically, so try it out.
Also, you probably won't need accepts_nested_attributes_for :lifestyles unless you actually want to modify the attributes of lifestyles from profiles, which I don't think is particularly useful when using active admin (just modify lifestyles directly).
Add
attr_accessible :lifestyles_attributes
f.e.:
class AccountsController < ApplicationController
attr_accessible :first_name, :last_name
end

nested record can't validate because its nested belongs_to doesn't save

I'm trying to create a nested child and grandchild record. The child belongs_to both the parent and the grandchild. The child won't validates_presence_of the grandchild because it hasn't been saved yet.
I'm using Rails 2.3.11, Formtastic, InheritedResources, and Haml, and everything else seems to work correctly - for example, validation errors on the grandchild populate properly in the parent form, and the invalid values are remembered and presented to the user. The parent model doesn't even try to update unless everything is valid, just as it should be.
My code is something like this, though in a different problem domain:
class Project < ActiveRecord::Base
has_many :meetings, :dependent => :destroy
accepts_nested_attributes_for :meetings
end
class Meeting < ActiveRecord::Base
belongs_to :project
belongs_to :task
accepts_nested_attributes_for :task
validates_presence_of :task_id, :project_id
end
class Task < ActiveRecord::Base
has_many :meetings, :dependent => :destroy
end
The Project ALWAYS exists already, and may already have Meetings that we don't want to see. Tasks may belong to other Projects through other Meetings, but in this case, the Task and Meeting are ALWAYS new.
In the controller, I build a blank record only on the new action
#project.meetings.build
and save the data like this:
#project.update_attributes(params[:project])
and in the view
- semantic_form_for #project do |f|
- f.semantic_fields_for :meetings do |m|
- next unless m.object.new_record?
= m.semantic_errors :task_id
- m.object.build_task unless i.object.task
- m.semantic_fields_for :task do |t|
- f.inputs do
= t.input :task_field
= m.input :meeting_field
When I try to save the form, I get a validation error of "Task can't be blank." Well, sure, the Task hasn't been saved yet, I'm trying to validate, and I don't have an ID for it.
Is there a simple and elegant way to make sure that the grandchild record (Task) gets built before the child record?
I've tried something like this in the Meeting model:
before_validation_on_create do |meeting|
meeting.task.save if meeting.task.valid?
end
and that seems to save the Task, but the Meeting still doesn't get the right ID. Same error, but the Task record gets created.
I've also tried this:
before_validation_on_create do |meeting|
new_task = meeting.task.save if meeting.task.valid?
meeting.task = new_task
end
Which has the strange behaviour of raising ActiveRecord::RecordNotFound "Couldn't find Task with ID=XX for Meeting with ID=" - which I sort of get, but seems like a red herring.
I also tried adding :inverse_of to all the relationships and validating :task instead of :task_id. The latter, oddly, fails but seems to give no error message.
My actual goal here is to create more than one Task, each with an initial Meeting on a previously selected Project... so I could take another approach with my problem - I could do something simple and ugly in the controller, or create the first Meeting in an after_create on the Project. But this is so pretty and soooo close to working. The fact that I'm getting proper validation errors on :task_field and :meeting_field implies that I'm on the right track.
I see what the problem is, but not how to solve it: I suspect I'm missing something obvious.
Thank you!
Well, I found a solution, based on one of the similar questions out there, but the short of it is "rails 2.3 doesn't seem to be very good at this." I think I can put the answer in a more succinct way than any of the other answers I've seen.
What you do is you skip the validation of the :task_id, but only if task is valid! Most of the other answers I've seen use a proc, but I think it's more readable using delegate, like this:
delegate :valid?, :to => :task, :prefix => true, :allow_nil => true
validates_presence_of :task_id, :unless => :task_valid?
I also had another problem hidden under the waterline - in the case, the "Project" is actually a special sort of record that I wanted to protect, which has a validation that (intentionally) fails only for this special record, and I also set readonly? to true for the special record.
Even though I'm not actually changing that special record, it still needs to validate and can't be readonly to update children through it. For some reason, I wasn't seeing the error message for that validation. To solve that, I made the validation on the Project only applicable :on => :create, and I took out the readonly? thing.
But the general solution is "don't validate presence of the unbuilt belongs_to object if the object itself is valid." Nil is never valid, therefore the validation still works if you just have an object_id.
(Please don't vote down a sincere question unless you have an answer or a link to one. I'm aware the question has been asked by others in other ways, I read many of those other questions, none seemed to be precisely the same problem, and I had not found a solution.)

Resources