RAILS Tips - StanfordBioinformatics/pulsar_lims GitHub Wiki
Tips
- How to specify array parameters in the controller
- How to set the current user of a new association
- Event delegation
- Editing application.js
- Setting default selection value when list size is 1
- The difference between the edit and update controller actions
- Validation callbacks
- The :validate option
- The :valid option for associations
- Validation gotchas
- Nested model forms
- Creating a join table
- Installing nokogiri locally
- The dependency option of a relation specification
- Controller callbacks are class methods
- simple_fields_for
- When the default of null can be a bad thing
One of the models I have is called donor_constructs. Each controller template generated by RAILS has a "params" method, i.e. in the case of donor_constructs it is donor_constructs_params. This is used to whitelist parameters that can be used in controller actions and model methods. I show below the code for that method as defined in the donor_constructs controller:
def donor_construct_params
params.require(:donor_construct).permit(:name, :cloning_vector_id, :vendor_id, :vendor_product_identifier, :target_id, :description, :insert_sequence, :construct_tag_ids => [], construct_tags_attributes: [:id,:_destroy] )
end
The only part I have modified is the set of permitted parameters by adding more as added more associations to the donor_constructs model. Would I'd like to point out in particular here is the use of array parameters. Array parameters are used when a model has a has_many relationship with another model. In this example, donor_constructs has_many construct_tags. Thus, in a form where a user is creating a new DonorConstruct, it is possible that many ConstructTags can be specified. To specify that a permitted parameter can take on many values as an array, you use the syntax seen above, i.e.
:construct_tags_ids => []
The thing to keep in mind when using array parameters is that you must write them all at the end of your parameter list, otherwise, during requests the controller will ignore them silently.
This is a rather long topic, so I created it's own wiki page for it.
Event delegation is the way that jQuery allows you to bind an event handler to one or more elements that may not yet exist on the page yet. In today's world of apps, where HTML content is asynchronously being added and removed from pages, this has great utility. The idea is that you bind an event handler to an element that you know will always exist on the page, such as the document root, but only handle the event when it is fired from a specific type of child element which may come into existence at a later time.
When doing event delegation, the named event-handler methods of a jQuery collection object don't appear to work; instead, one must use the .on()
method. This is
the case for both jQuery and CoffeeScript. For example, instead of:
$(funcion() {
$(document).click(".someClass",function() {
...
})
});
it should instead be:
$(function() {
$(document).on("click",".someClass",function() {
...
})
});
For details on event delegation, see jQuery in Action, 3rd Edition.
While in dev mode, it appears that any updates to this file are not reflected in the browser until the browser cache has been cleared. This is at least the case with Google Chrome. Modifying other JavaScript files in the directory don't appear to require this, as the changes take effect immediately.
When we have a selection box and there is only one item to select, it makes for a nicer user experience to go ahead and already have that value selected. We can set that functionality by adding include_blank: false
to the input, i.e.
<%= f.association :barcode, collection: barcodes, include_blank: false, label: false, wrapper: false %>
Note that this should only be used when the user is required to select a value, otherwise we are forcing the user to select a value when it's optional.
The edit action calls the update action.
Callbacks that use a custom validation with the validate keyword will run each time you call the validate method. According to the Rails document at http://guides.rubyonrails.org/v3.2.13/active_record_validations_callbacks.html:
It is also possible to control when to run these custom validations by giving an :on option to the validate method, with either :create or :update. validate :propagate_update_if_prototype, :on => :create
However, it doesn't work as documented. It only works by limiting the validation to run prior to object creation time when supplying on: :create
. I found that the valid? method will always run a custom validation when we have on: :update
, or when omitting :on entirely.
:validate is a boolean that defaults to true for the has_many and has_and_belongs_to_many relationships, and to false for the has_one and belongs_to relationships.
If you try to save an object that has a freshly built association, but that association has a validation error, i.e. a missing field, then save will not report any errors and the associated object you were trying to create will be silently ignored. I'll provide a simple example to make this more clear.
Say that you are modeling a person living in an apartment complex that is allowed to have up-to 1 pet dog or cat.
class Person < ActiveRecord::Base
has_one :pet
end
class Pet < ActiveRecord::Base
PET_TYPES = ["Dog", "Cat"]
validates :name, presence: true
validates :type, presence: true, inclusion: PET_TYPES
end
Thus, a Person can have a Pet, the latter of which has two required fields, :name and :type, where the latter can only have a value of "Dog" or "Cat". Given a random Person record person, we'll add a Pet association:
person.build_pet( {name: "tubby the cat", type: "kitten"} )
The issue here is that we have specified :type as "kitten" instead of "Cat". Calling person.valid?
will return true, and calling person.save
will also return true, however, the association will not exist in the database, and no validation errors will be present on the person object (person.errors
will be empty). However, you will find that a validation error was issued with your pet. Assuming that you have already run validations (i.e. by calling the valid?
or validate
or save
method on the person record, then you'll find the following validation error on the pet:
person.pet.errors.messages
=> {:type=>["is not included in the list"]}
If you don't check that both a given Person record and Pet record are both valid after building an association then you can end up with a sad person without a pet, unfortunately. It would be more intuitive if we could have the validation errors in the pet escalate up to the person so that a call such as person.save
will return false. In turns out there is a simple solution to that - the :validate option. Lets update the Person model to incorporate this option:
class Person < ActiveRecord::Base
has_one :pet, validate: true
end
Now, given the same example from above, we'll get an error when trying to save the person:
person.build_pet( {name: "tubby the cat", type: "kitten"} )
person.save #returns false
person.errors.messages
=> {:pet=>["is invalid"]}
If you want to know the exact validation error with the pet, you'd have to explicitly check the errors, i.e. person.pet.errors.messages
. Nonetheless, calling person.save
will fail since there are validation errors on the person object, which is what we want.
Note that for a has_many or has_many_and_belongs_to association, we don't need to explicitly set the :validate option to true, since that is the default for this type of association, unlike has_one or belongs_to.
If you have a before_validation callback in your model that returns false, then the call to the save() method from ActiveRecord::Base, whether called directly as normally done in the create controller action, or indirectly through update() in the update controller action, will return false and none of any defined validations will have had a chance to run. This is a bad thing if the callback isn't meaning to return false.
As an example, a Library instance in Pulsar has an attribute called plated
, which is set to true if the library belongs to a biosample that in turn belongs to a well on a plate, and false otherwise. This allows for filtering of Libraries that are plated vs non-plated. In the library.rb model file, there is a before_validation
callback that runs a function called :check_plated
, which is responsible for setting the plated
attribute to true or false, each time before a Library instance is saved (including updates) to the database. Below I show the relevant parts of the code:
class Library < ActiveRecord::Base
...
before_validation :check_plated
...
def check_plated
if self.biosample.well.present?
self.plated = true
else
self.plated = false
end
end
...
end
If a user is updating a Library record that doesn't have a biosample with an associated well, then when saving this record the else
block will execute, setting self.plated = false
. The return value of a function is the value of the last executed expression, or nil if there aren't any expressions executed. In the case described above, the return value would thus be false
. This instantly causes the save() method to exit with a false
return value. No validations will have been run, thus there won't be any error messages to display to the user when the Library edit page is rerendered. What that leaves is an unknown state - the user clicks the update button and the page flashes as it rerenders, and nothing else changes. In this scenario, the fix is to add a truthful return value to the callback. The actual code in the check_plated()
callback does just this by making the last expression be return true
:
def check_plated
if self.biosample.well.present?
self.plated = true
else
self.plated = false
end
return true
end
Revisiting our association where
Person has_a Pet
Pet belongs_to Person
we can show an example of a nested model form inside of another:
<%= simple_form_for @person do |f| %>
<%= f.simple_fields_for :pet, @pet do |ff| %>
<%= ff.input :name %>
<%= ff.input :type, collection:
<% end %>
<% end %>
Such form nesting would be useful whenever you want to allow the user to update an associated object or create one (as in this case) from one of the views of the parent object (the Person). Note that for this to work properly, you must also add accepts_nested_attributes_for :pet
in the Person model, and in your Person controller, you'd need to add some parameters to permit in the method named person_params(), which would look something like
pet_attributes: [:name, :type:]
Breaking it down a bit,
<%= f.simple_fields_for :pet, @pet do |ff| %>
indicates that we are creating a nested form for a Pet inside of the context of a Person. As such, for each input element (name and type), RAILS will automatically mangle the value of its "name" and "id" attributes. For example, the value for the "id" attribute of the name input won't simply be pet_name
, but rather person_pet_attributes_name
. So it adds a prefix of parent model name, followed by "pet_attributes", followed by the input field name.
You may be wondering why I have specified <%= f.simple_fields_for :pet, @pet do |ff| %>
instead of simply <%= f.simple_fields_for :pet do |ff| %>
. Either is fine, it all depends on whether you have initialized a Pet object with some values first before saving it, and want to give the user the ability to fill in the values for the rest of the Pet attributes, or even overwrite any defaults you may have set. If you didn't initialize a new Pet object for the subform to use as a basis, then you'd have to use the latter form.
As an additional note, make sure inspect the parameters that the form is sending to the server in order to verify that you have things working together correctly. For example, you should have a parameters hash with a key named "person", and in that another hash with a key named "pet_attributes". If you don't see this latter key, then you've done something wrong and your controller won't be able to accept the nested object information (it will simply ignore it); so you'll have to do some troubleshooting until you get the key name right in the paremeters hash.
A join table is needed when two models relate to each other, each through a has_many relationship. For example, a join table was created to link the Treatments model with the Biosamples model. The migration command was:
rails g migration create_join_table_biosamples_treatments biosamples treatments
Nokogiri has a dependency on libiconv that can be difficult to satisfy. The first time I tried to install it, I got the error that this library was missing. So, on my Mac, I installed it with Homebrew:
brew install libiconv
And that worked, however, it didn't link to my system:
$ brew link libiconv
Warning: libiconv is keg-only and must be linked with --force
Note that doing so can interfere with building software.
If you need to have this software first in your PATH instead consider running:
echo 'export PATH="/usr/local/opt/libiconv/bin:$PATH"' >> ~/.bash_profile
Turns out though that, with Xcode Command Line Tools already installed, many of the system library dependency issues will already be satisified. You just need to tell the nokogiri to use the system libraries instead of the bundled ones by using the --use-system-libraries
flag. That allowed me to install nokogiri as follows:
gem install nokogiri -- --use-system-libraries
The documentation for that flag is:
Use system libraries instead of building and using the bundled libraries.
which lives in nokogiri's extconf.rb
file.
When specifying a relationship in one model to another model, i.e. via the has_many, has_one, belongs_to ..., there is the dependent
option that you can use to specify what happens when a model record is deleted. This option can take one of several values, and the one that we'll expand on here about is the restrict_with_exception
option. Only the has_one and has_many relations can use this dependency option.
Let's imagine an easy scenario where a book has only one author, but an author can have many books. In the author model, we can specify the relationship between authors and books by using has_many :books
. In the book model, we'll also specify a reverse relationship, so that anytime someone is viewing a book they can easily see the author of the book. To do that, in the book model we write belongs_to :author
.
Next, we specify the restrict_with_exception
dependency in the author model like so:
has_many :books, dependent: :restrict_with_exception
This changes the default behavior when an author is deleted from the database. If the author doesn't have any books, then none of this matters. But if the author does has one or more books, then when restrict_with_exception
is set, an exception by the class of ActiveRecord::DeleteRestrictionError
will be raised, which you should handle gracefully in the application controller. I wanted to detail this since the documentation doesn't indicate the type of error that is raised and the example are poor in the standard documentation.
What happens if we don't specify the dependent
option at all? Well, the default value kicks in, and whichever value that is depends on the type of relationship at hand. For a has_many, the default value is nullify
, which means to set the foreign key to NULL. In our example, deleting an author with books using the default dependency strategy would mean that the associated books would get a NULL value in place of the author foreign key.
Controller callbacks work as class methods - Inside the method, self is the controller class. That means you can't use callbacks to set instance state for your model records.
simple_fields_for from (simple_form)[https://github.com/plataformatec/simple_form] is nice wrapper over RAILS's fields_for. You can use simple_fields_for do either edit an association or create it dynamically. In this latter case, you must "build" the association before editing it with simple_fields_for. For example, in Pulsar, a SequencingRun belongs_to a DataStorage that designates where the sequencing results are stored. When a user is entering a new SequencingRun in Pulsar, the user can either select an existing DataStorage record to associate it to, or create a new DataStorage record within the same form using simple_fields_for. Given a form object f for the SequencingRun, one might be tempted to try this:
<%= f.simple_fields_for :data_storage do |f| %>
<%= f.input :s3_bucket_name %>
<% end %>
But that won't work, and the block you define with simple_fields_for won't render. RAILS won't even complain. The problem is that you must write it like this:
<%= f.simple_fields_for, :data_storage, @sequencing_run.data_storage do |f| %>
<%= f.input :s3_bucket_name %>
<% end %>
What I did is add @sequencing_run.data_storage
. But we're not done yet - the simple_fields_for block still won't render as is. It needs a non-null value for data_storage. So, in the SequencingRuns controller, I can go in the "new" action and build the association as follows:
def new
@sequencing_run = SequencingRun.new
@sequencing_run.build_data_storage({user_id: current_user.id})
end
When creating a new record, you may not need to provide a value for each field. For example, in the Shippings model, users often don't enter a value for the tracking number when the record is being created. Since the model doesn't define defaults for these fields, the database will then use the null value. A Ruby database adaptor will report this as a nil values, and Python as None. A default of null in a database field is meant to mean that the value is unknown. For example, when shipping a package, the tracking number may not be immediately assigned. Thus, it makes sense to let the database store the initial value of the tracking number as null, until the proper value can be entered once it is known. A default of null, however, can be quite problematic in some situations.
Consider the situation in which you have a unique index over several columns. That is just the case in the Shippings model, where there exists a composite unique index over the fields :tracking_number, :carrier, :date_shipped, :from, :to, and :biosample. The logic is that a sample of a biosample stored in a lab somewhere can't be shipped more than once on the same date to the same person with the same tracking code. Of course, it does make sense to allow more than one shipping of particular biosample under different tracking codes (replicates). What you have to consider though is that a unique index will not be used to check a record for uniqueness if any of the fields that it covers has a null value. Thus, a user could enter two records with the same :tracking_number, same :carrier, same :from and :to, and same :biosample, all because the :date_shipped was left blank (meaning it got a null value in each record that was created). But still, this would allow a user to enter multiple Shipping records for the same :biosample, :from, :to, and :date_shipped when both :carrier and :tracking_number are not set. This is undesirable, more-so in thinking in terms of a programmatic interface to your app where the same shipping details may be unknowingly set multiple times for the same Biosample record. The solution is to explicitly set non-null defaults for the fields :carrier and :tracking_number, and the right default to use is the empty string. Now, with non-null default values, the fields :carrier and :tracking_number will never invalidate the composite unique index.
As a final note, you have have asked: why not just create a unique index on the :tracking_number? That wouldn't mix well with the composite unique index. It would mean that only one Shipping record for a given Biosample could have the :tracking_number empty, and sometimes the user plainly doesn't care to store the tracking number in the first place.