How to understand the colon operator usage in a Ruby class - ruby-on-rails

I am learning both Ruby (2.3.x) & Rails (4.x). I was going through the Ruby On Rails Tutorial and I encountered this syntax and am having trouble reading it:
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true
end
Does this class define validates as a method which takes in a :name symbol and a hash presence:true? The same thing applies to line 3.
Or it is something entirely else? All attempts to run it resulted in:
uninitialized constant ApplicationRecord.
I looked at the source(maybe?) but am still not clear.

This is a special DSL introduced by ApplicationRecord. What you are actually doing is calling those methods inside class during declaration. It adds those validations to your class, so whenever you try to save a record it will fail if you don't have email or name
Try this
user = User.new
user.save
user.valid? # false
And try to do the same without validates.
If it will make things more clear for you, you can try write this class like this
class User < ApplicationRecord
validates(:name, presence: true)
validates(:email, presence: true)
end

validates is implemented as a class method in ActiveModel::Validations.
The ActiveModel::Validations module is included in ApplicationRecord, therefore you are able to call that method when your User class is loaded.
validates accepted an array and treats the last element of that array as an options hash (if the last element is an hash).

validates is a pre-defined helper that Active Record offers to use in Rails to make the validation work easier, this way you can with some single lines of code manage several validations of several attributes.
As it's a helper within Rails it's also a method defined in the ActiveModel module, in the core of the framework, see: active_model/validations.rb
The most common is the presence attribute which you're facing the trouble with, that specifies that the attribute used isn't empty, doing it in the Ruby way through the blank? method to check if the value passed isn't blank nor nil.

Related

Clarification on Ruby syntax in Rails

I'm learning Rails and in going through the official guides, I came across some code which I could not really understand the meaning of.
Case 1 -
class Person < ApplicationRecord
validates :name, presence: true
end
It looks to me that validates is a method that takes a symbol called :name as an argument. But then, what is presence? Is it also a method? But if it is, what is the significance of the : right after presence. I understand that the value true is being set for presence, which serves as kind of a validation, requiring the presence of (in other words). But I'm not quite clear on the syntax.
It might also be possible that presence: true is just a hash, where :presence (the symbol) is the key, and true is the value.
Case 2 -
class Person < ApplicationRecord
validates :terms_of_service, acceptance: true, message: 'must be abided'
end
Again, validates is the method that takes a symbol :terms_of_service as an argument. But what about the rest? Is it a hash with 2 key-value pairs, somewhat like {acceptance: true, message: 'must be abided'}?
And if it is indeed a hash, why is it tacked on to the validates method in each case? Why can't it be
validates :terms_of_service
acceptance: true, message: 'must be abided'
Thanks for the help!
That is the syntax for passing a hash to the method. What that is doing is the same thing as validates(:terms_of_service, {acceptance: true, message: 'must be abided'}). It's a common way of passing extra options to a method.
In Ruby there's a strong tradition for passing in options as a Hash as the last argument, strong enough that this tradition became new feature borrowed from Python: Keyword arguments.
In classic Ruby the method would be defined as this:
def validates(*args)
options = args.last.is_a?(Hash) ? args.pop : { }
# args is a list of fields
end
In Ruby 2.3 you can do this:
def validates(*args, **options)
# options is automatically any hash-style arguments if present
end
In Ruby 2.0+ you can also do this:
def validates(*args, acceptance: false, message: nil)
end
Where that defines options as first-class variables.
It's a common Ruby pattern, so it's good to understand what's going on here. Try writing your own methods that take options and you'll see how it plays out.

ActiveRecord, validates_uniqueness_of :name not catching non-uniquness if I have a capitalize method

I have a simple capitalize method so that when user submits a new band in the band page it returns it with the first letter capitalized.
Inside my Band class I also have a validates_uniqueness_of :band_name to see if there is already a band with the same entry. See code below:
class Band < ActiveRecord::Base
has_and_belongs_to_many :venues
validates :band_name, :presence => true
before_save :title_case
validates_uniqueness_of :band_name
private
def title_case
self.band_name.capitalize!
end
end
So if I type in someband, it creates it and displays it as Someband. If I type someband again, ActiveRecord sees it as unique and I'll get another Someband. The only way it works is if I type Someband. How would I remedy this situation?
I think what you want to do is this
validates_uniqueness_of :band_name, :case_sensitive :false, allow_blank: false
Take a look at http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html
:case_sensitive - Looks for an exact match. Ignored by non-text
columns (true by default).
The reason your code doesn't work is because validations happen before the before_save callbacks are triggered. Check out the list of ActiveRecord::Callbacks for the order in which things are called.
MZaragoza's answer is a great option for making your validation work regardless of what casing your users might enter. It will prevent things like "someband" and "SomeBand" from being added. I recommend including that as part of your solution.
Another option very similar to the code you already have is to switch to using the before_validation callback:
before_validation :title_case
I highly recommend using the before_validation callbacks instead of before_save callbacks whenever data changes that may be relevant to your validation rules, regardless of what other changes you make. That ensures that you are checking that actual state of the model that you plan to save to the database.
You can use attribute setter instead of before_save callback to capitalize your value without postponing.
def band_name=(value)
self['band_name'] = value && value.mb_chars.capitalize
end

Why won't rails create associated records/objects from nested form using strong parameters?

I'm trying to create a record and it's associated records from a nested form using strong parameters. My primary model is:
class MaterialDonationRequest < ActiveRecord::Base
has_many :donation_items, dependent: :destroy
accepts_nested_attributes_for :donation_items, allow_destroy: true
validates :name, presence: true
attr_accessor :due_on_event, :date, :donation_items_attributes, :event_id
end
My associated (nested) model is:
class DonationItem < ActiveRecord::Base
validates :name, presence: true
belongs_to :material_donation_request
belongs_to :global_profile
validates :name, presence: true
attr_accessor :_destroy
end
In my material_donation_requests_controller.rb, I have the following for strong parameters:
def material_donation_request_params
params.require(:material_donation_request).permit(:name, :description, :event_flag, :due_on_event, :date, :event_id, donation_items_attributes: [:id, :name, :description, :amount, :_destroy])
end
Here's the line in my create method where I create the object:
#material_donation_request = MaterialDonationRequest.new(material_donation_request_params)
After doing this, #material_donation_request is created and populated correctly from the form. But the associated donation_items do not get created. For instance, in the debugger, when I enter #material_donation_request.donation_items.first, Rails returns nil.
For reference, here is what Rails returns for material_donation_request_params in the manual tests I'm running:
{"name"=>"Name", "description"=>"", "due_on_event"=>"true", "date"=>"", "donation_items_attributes"=>{"0"=>{"name"=>"", "amount"=>"1", "_destroy"=>""}, "1427122183210"=>{"name"=>"", "amount"=>"2", "_destroy"=>""}}}
Why isn't Rails creating the associated objects from the form as well? Everywhere I've looked, it seems like this structure should create everything, and a subsequent save should save everything (or at least throw validation errors as in this case-see update below). Is there something I'm missing?
Update
Since it was brought up in the answers, yes, the material_donation_params shown above would not pass validation. That's the scenario I've been manually testing. It should generate a validation error on save, but instead, simply saves the MaterialDonationRequest with no errors of any kind, and saves nothing to DonationItems.
To be clear, though, if I fill out the form completely and get the following material_donation_request_params:
{"name"=>"Name", "description"=>"", "due_on_event"=>"true", "date"=>"", "donation_items_attributes"=>{"0"=>{"name"=>"first", "amount"=>"1", "_destroy"=>""}, "1427122183210"=>{"name"=>"second", "amount"=>"2", "_destroy"=>""}}}
and then do #material_donation_request.save, it only saves the MaterialDonationRequest, and not any of the DonationItems.
Final Update
Okay. I've deleted my previous "final update" because what I wrote, and what I wrote in some of the comments was wrong. What ended up fixing this was not an update to Rails 4.1.8. I ran the bundle update command before actually saving the gem file with the new Rails version. So really, what ended up fixing this was simply updating all the gems that didn't have fixed version numbers. God only knows why things weren't working with the previous set of gems. Sorry that this isn't so helpful...
From Rails Validations guide
presence
This helper validates that the specified attributes are not empty. It uses the blank? method to check if the value is either nil or a blank string, that is, a string that is either empty or consists of whitespace.
You are requiring donation_item to be present, but your resulting params hash clearly has donation names blank, validation is failing. Calling save! when debugging these things can be helpful since it would throw a error on failure.
I figured out the answer. In total desperation, I upgraded my Rails version from 4.0.2 which is what I had been using, to 4.1.8. After doing this, with no other changes whatsoever (except gem dependencies, of course), it just started working the way it's supposed to. So I guess Rails 4.0.2 has a problem with nested forms and strong parameters.

Selecting all current attributes of a model for validations in Rails

When setting up my models I often find myself having to write out all of its attributes when setting up certain validations. A common example is when I use the presence parameter:
validates :first_name, :last_name, :username, :email, presence: true
Is there a clever way to select all of its attributes without explicitly writing them all out similar to how you can retrieve them in the rails console?
User.columns
And pass it as an argument in the validates method?
ALL_ATTRIBUTES = User.columns
validates ALL_ATTRIBUTES, presence: true
Trying something like this out I got this error undefined method 'to_sym'
I will NOT encourage you or anyone to do this. Reason being when you run into issues, when an object of your model doesn't get saved and throw errors because of a new column which was added to application after some time in future, and you or new developers will wonder WHY?!?!.
However, if you must do then here you go:
validates *self.column_names.map(&:to_sym), presence: true
Here, * in Ruby is known as splat operator and here's the explanation on &:.
This is an awful idea. But you can do it this way:
attrs = column_names.map { |column| column.to_sym }
validates *attrs, presence: true
Why is it a bad idea? Because it's not very clear what is being validated it. It makes debugging hard, and could cause you have strange bugs. If you add a column in the future that does not require presence validation, you will trip up. Also, some things my not require presence. For example, an email field will need a regex validation, which automatically knows that a blank string is invalid. So a presence validator is redundant.
Beware of being too clever, as it's sometimes not so clever after all.

How to Skip Validations w/ find_or_create_by_?

Is it possible to skip validations with a dynamic find/create by method?
For example with regular save I can do something like:
p = Post.new
p.title = nil
p.body = nil
p.save(:validate => false)
Would love to do the same with find_or_create_by_title.
It dosnt look possible with the code in Rails right now however you may have better luck being a little more verbose in how you write the code. You can use find_or_initialize_by_ which creates a new object but does not save it. You can then call save with your custom options, also in the documentation they have a neat demonstration that is hard to find so I will include it below:
# No 'Winter' tag exists
winter = Tag.find_or_initialize_by_name("Winter")
winter.new_record? # true
Good luck and let me know if you need more pointers in the right direction.
For some cases, find_or_initialize_by_ will not be useful and need to skip validations with find_or_create_by.
For this, you can use below alternative flow and method of ROR:
Update your model like this:
class Post < ActiveRecord::Base
attr_accessor :skip_validation
belongs_to :user
validates_presence_of :title, unless: :skip_validation
end
You can use it now like this:
Post.where(user_id: self.id).first_or_create!(skip_validation: true)
I have used first_or_create instead of find_or_create_by here. You can pass more column names and values with this, and your validation will not be worked with this.
You can continue without any changes for strong parameters end and no need to permit this 'skip_validation' so it will work with validations while adding entries.
Using this, you can use it with and without validations by passing a parameter.
Currently skipping validation DOES work with find_or_create_by.
For example, running:
Contact.find_or_create_by(email: "hello#corsego.com).save(validate: false)
will skip a validation like:
validates :name, :email, presence: true, uniqueness: true

Resources