RAILS Tips - StanfordBioinformatics/pulsar_lims GitHub Wiki

Tips

  1. How to specify array parameters in the controller
  2. How to set the current user of a new association
  3. Event delegation
  4. Editing application.js
  5. Setting default selection value when list size is 1
  6. The difference between the edit and update controller actions
  7. Validation callbacks
  8. The :validate option
  9. The :valid option for associations
  10. Validation gotchas
  11. Nested model forms
  12. Creating a join table
  13. Installing nokogiri locally
  14. The dependency option of a relation specification
  15. Controller callbacks are class methods
  16. simple_fields_for
  17. When the default of null can be a bad thing

How to specify array parameters in the controller

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.

How to set the current user of a new association

This is a rather long topic, so I created it's own wiki page for it.

Event delegation

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.

Editing application.js

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.

Setting default selection value when list size is 1

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 difference between the edit and update controller actions

The edit action calls the update action.

Validation callbacks

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.

The :validate option

: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.

The :valid option for associations

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.

Callback gotchas

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

Nested model forms

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.

Creating a join table

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

Installing nokogiri locally

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.

The dependency option of a relation specification

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 are class methods

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

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 the default of null can be a bad thing

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.

⚠️ **GitHub.com Fallback** ⚠️