validates_presence_of with belongs_to associations, the right way - ruby-on-rails

I'm investigating on how validates_presence_of actually works. Suppose I have two models
class Project < ActiveRecord::Base
[...]
has_many :roles
end
and
class Role < ActiveRecord::Base
validates_presence_of :name, :project
belongs_to :project
end
I want it so that role always belongs to an existing project but I just found out from this example that this could lead to invalid (orphaned) roles saved into the db. So the right way to do that is to insert the validates_presence_of :project_id in my Role model and it seems to work, even if I think that semantically has more sense to validate the presence of a project instead of a project id.
Besides that I was thinking that I could put an invalid id (for a non existing project) if I just validate the presence of project_id, since by default AR doesn't add integrity checks to migrations, and even if I add them manually some DB does not support them (i.e. MySQL with MyISAM or sqlite). This example prove that
# with validates_presence_of :name, :project, :project_id in the role class
Role.create!(:name => 'foo', :project_id => 1334, :project => Project.new)
AREL (0.4ms) INSERT INTO "roles" ("name", "project_id") VALUES ('foo', NULL)
+----+------+------------+
| id | name | project_id |
+----+------+------------+
| 7 | foo | |
+----+------+------------+
Of course I won't write code like this, but I want to prevent this kind of wrong data in DB.
I'm wondering how to ensure that a role ALWAYS has a (real and saved) project associated.
I found the validates_existence gem, but I prefer to not add a gem into my project unless is strictly necessary.
Any thought on this?
Update
validates_presence_of :project and adding :null => false for the project_id column in the migration seems to be a cleaner solution.

Rails will try a find on the id and add validation error if an object with an id is not found.
class Role < AR::Base
belongs_to :project
validates_presence_of :project, :name
end
Role.create!(:name => "admin", :project_id => 1334)# Project 1334 does not exist
# => validation error raised
I see your problem also wants to deal with the situation where the author object is provided but is new and not in db. In the case the presence check doesnt work. Will solve.
Role.create!(:name => "admin", :project => Project.new) # Validation passes when it shouldn't.
Update:
To some extent you can mitigate the effect of passing a dummy new object by doing a validation on the associated :project.
class Role < ActiveRecord::Base
belongs_to :project
validates_presence_of :project
validates_associated :project
end
If Project.new.valid? is false then Role.create!(:name => "admin", :project => Project.new) will also raise an error. If however, Project.new.valid? is true then the above will create a project object when saving.
Does using validates_associated :project help you?

I tried a lot of combinations of validators, but the cleanest solution is to use the validates_existence gem. With that I can write code like this
r = Role.new(:name => 'foo', :project => Project.new) # => #<Role id: nil, name: "foo", project_id: nil, created_at: nil, updated_at: nil>
r.valid? # => false
r.errors # => {:project=>["does not exist"], :project_id=>["does not exist"]}
So my final model is as simple as
class Role < ActiveRecord::Base
belongs_to :project
validates_existence_of :project
# or with alternate syntax
validates :project, :existence => true
[...]
end
With db validation plus Aditya solution (i.e. :null => false in the migration and validates_presence_of :project in the model) Role#valid? will return true and Role#save will raise an exception at database level when project_id is null.

Related

Update attributes on has_many through associations and working with the unsaved object

This has something to do with my last quesion about unsaved objects, but now it is more about a specific problem how to use rails.
The models I have are:
class User < ActiveRecord::Base
has_many :project_participations
has_many :projects, through: :project_participations, inverse_of: :users
end
class ProjectParticipation < ActiveRecord::Base
belongs_to :user
belongs_to :project
enum role: { member: 0, manager: 1 }
end
class Project < ActiveRecord::Base
has_many :project_participations
has_many :users, through: :project_participations, inverse_of: :projects
accepts_nested_attributes_for :project_participations
end
With this models, when I create a new project I can do it by a form (fields_for etc) and then I can call update_attributes in the controller. So if I have users in the database already, I can do this:
u = Users.create # save one user in database (so we have at least one saved user)
p = Project.new
# add the user to the project as a manager
# the attributes could come from a form with `.fields_for :project_participations`
p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> true
This works fine until I want to do something with the users of a project. For example I want add a validations that there must be at least one user for a project:
class Project < ActiveRecord::Base
...
validates :users, presence: true # there must be at least one user in a project
...
end
This now gives:
u = Users.create
p = Project.new
p.update_attributes(project_participations_attributes: [{user_id: u.id, role: 1}])
=> false
p.errors
=> #<ActiveModel::Errors:... #base=#<Project id: nil>, #messages={:users=>["can't be blank"]}>
p.users
=> #<ActiveRecord::Associations::CollectionProxy []>
p.project_participations
=> #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: 1, project_id: nil>]>
So on unsaved projects the .users is empty. This already bugs me (see my last quesion about unsaved objects). But in this case I can of course now work around this by doing validates :project_participations, presence: true instead of validates :users, presence: true and it should mean the same.
But this would mean I should never use the .users method (in any helper, model, view, ...) unless I am totally sure that I work with a saved object. Which in fact renders the .users method unusable (like it does with the validation of user`s presence).
If I call update_attributes like this, the validations works and it saves:
p.update_attributes(users: [u])
With this it creates the project_participation by itself so p.users works as expected. But here I cannot set any data like role for project_participation of that user.
So my questions are: Can I make the .users method work whether or not the object is saved (I think not)? But then, how can I add users to a unsaved project as a manager/member and work with the unsaved project?
I hope my problem is clear.
I think I understand you question, and you're correct in assuming that you cannot use the .users method whether or not the project model is saved. The reason for this is that in defining an association in Project (ie. has_many :users, through: :project_participations, inverse_of: :projects) you're telling rails to read the users attribute out of the database via the project_participations join table and when you haven't saved the project you have nothing to read out of the database.
In order to add a User to your project in a particular role you will need to create a new ProjectParticipation model which you will then associate to your project. If you then remove the users association and write your own users method you should be able to access your collection of users regardless of whether or not the project has been saved.
class Project < ActiveRecord::Base
has_many :project_participations
...
def users
project_participations.collect { |pp| pp.user }
end
end
Then something like:
u = Users.create
p = Project.new
pp = ProjectParticipation.new({user: u, project: p, role: 1})
p.project_participations << pp
p.users
Hopefully that helps.

The perfect way to validate and test Rails 3 associations (using RSpec/Remarkable)?

I'm still pretty new to testing in Rails 3, and I use RSpec and Remarkable. I read through a lot of posts and some books already, but I'm still kind of stuck in uncertainty when to use the association's name, when its ID.
class Project < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :project
end
Because of good practice, I want to protect my attributes from mass assignments:
class Task < ActiveRecord::Base
attr_accessible :project # Or is it :project_id??
belongs_to :project
end
First of all, I want to make sure that a project never exists without a valid task:
class Task < ActiveRecord::Base
validates :project, :presence => true # Which one is the...
validates :project_id, :presence => true # ...right way to go??
end
I also want to make sure that the assigned project or project ID is always valid:
class Task < ActiveRecord::Base
validates :project, :associated => true # Again, which one is...
validates :project_id, :associated => true # ...the right way to go?
end
...and do I need the validation on :presence when I use :associated??
Thanks a lot for clarifying, it seems that after hours of reading and trying to test stuff using RSpec/Shoulda/Remarkable I don't see the forest because of all the trees anymore...
This seems to be the right way to do it:
attr_accessible :project_id
You don't have to put :project there, too! It's anyway possible to do task.project=(Project.first!)
Then check for the existence of the :project_id using the following (:project_id is also set when task.project=(...) is used):
validates :project_id, :presence => true
Now make sure than an associated Project is valid like this:
validates :project, :associated => true
So:
t = Task.new
t.project_id = 1 # Value is accepted, regardless whether there is a Project with ID 1
t.project = Project.first # Any existing valid project is accepted
t.project = Project.new(:name => 'valid value') # A new valid project is accepted
t.project = Project.new(:name => 'invalid value') # A new invalid (or an existing invalid) project is NOT accepted!
It's a bit a pity that when assigning an ID through t.project_id = it's not checked whether this specific ID really exists. You have to check this using a custom validation or using the Validates Existence GEM.
To test these associations using RSpec with Remarkable matchers, do something like:
describe Task do
it { should validate_presence_of :project_id }
it { should validate_associated :project }
end
validates :project, :associated => true
validates :project_id, :presence => true
If you want to be sure that an association is present, you’ll need to
test whether the foreign key used to map the association is present,
and not the associated object itself.
http://guides.rubyonrails.org/active_record_validations_callbacks.html
attr_accessible :project_id
EDIT: assuming that the association is not optional...
The only way I can get it to thoroughly validate is this:
validates_associated :project
validates_presence_of :project_id,
:unless => Proc.new {|o| o.project.try(:new_record?)}
validates_presence_of :project, :if => Proc.new {|o| o.project_id}
The first line validates whether the associated Project is valid, if there is one. The second line insists on the project_id being present, unless the associated Project exists and is new (if it's a new record, it won't have an ID yet). The third line ensures that the Project is present if there is an ID present, i.e., if the associated Project has already been saved.
ActiveRecord will assign a project_id to the task if you assign a saved Project to project. If you assign an unsaved/new Project to project, it will leave project_id blank. Thus, we want to ensure that project_id is present, but only when dealing with a saved Project; this is accomplished in line two above.
Conversely, if you assign a number to project_id that represents a real Project, ActiveRecord will fill in project with the corresponding Project object. But if the ID you assigned is bogus, it will leave project as nil. Thus line three above, which ensures that we have a project if project_id is filled in -- if you supply a bogus ID, this will fail.
See RSpec examples that test these validations: https://gist.github.com/kianw/5085085
Joshua Muheim's solution works, but I hate being not be able to simply link a project to a task with an id like this:
t = Task.new
t.project_id = 123 # Won't verify if it's valid or not.
So I came up with this instead:
class Task < ActiveRecord:Base
belongs_to :project
validates :project_id, :presence => true
validate :project_exists
private
def project_exists
# Validation will pass if the project exists
valid = Project.exists?(self.project_id)
self.errors.add(:project, "doesn't exist.") unless valid
end
end

nested mass assignment with mongoid

with a has_one/belongs_to relationship, i cannot seem to update nested records via mass assignment.
models:
class ProductVariation
include Mongoid::Document
has_one :shipping_profile, :inverse_of => :variation
field :quantity
attr_accessible :shipping_profile_attributes
accepts_nested_attributes_for :shipping_profile
end
class ShippingProfile
include Mongoid::Document
belongs_to :variation, :class_name => "ProductVariation"
field :weight, :type => Float
attr_accessible :weight
end
controller:
#variation = ProductVariation.find(params[:id])
#variation.update_attributes(params[:product_variation])
post request:
Parameters:{
"product_variation"=>{
"quantity"=>"13",
"shipping_profile_attributes"=>{
"weight"=>"66",
"id"=>"4dae758ce1607c1d18000074"
}
},
"id"=>"4dae758ce1607c1d18000073"
}
mongo query:
MONGODB app_development['product_variations'].update({"_id"=>BSON::ObjectId('4dae758ce1607c1d18000073')}, {"$set"=>{"quantity"=>13, "updated_at"=>2011-04-28 06:59:17 UTC}})
and i dont even get a mongo update query if the product_variation doesnt have any changed attributes... what am i missing here?
Working models and a unit test are below to demonstrate that you can update child parameters and save to the database
through the parent as intended via accepts_nested_attributes_for and the autosave: true option for relations.
The Mongoid documentation says that an error will be raised for an attempt to set a protected field via mass assignment,
but this is out of date.
Instead, messages like the following are printed to the log file.
WARNING: Can't mass-assign protected attributes: id
You should carefully look for for these messages in the appropriate log file to diagnose your problem.
This will help you notice that you have a nested id field for the shipping profile in your parameters,
and this seems to cause the weight to be rejected as well, probably along with all child parameters.
After adding "attr_accessible :id" to the ShippingProfile model, the weight now gets assigned.
You also need to add "attr_accessible :quantity" (and I've added :id for the unit test) to the ProductVariation model
The next issue is that you need "autosave: true" appended to the has_one relation
in order to have the child updated through the parent,
otherwise you will have to save the child manually.
You might also be interested in sanitize_for_mass_assignment, which can be used to launder out ids.
include ActiveModel::MassAssignmentSecurity
p sanitize_for_mass_assignment(params['product_variation'], :default)
The unit test should make the whole subject clear, I'll leave the controller work to you.
Hope that this is clear and that it helps.
class ProductVariation
include Mongoid::Document
has_one :shipping_profile, :inverse_of => :variation, autosave: true
field :quantity
accepts_nested_attributes_for :shipping_profile
attr_accessible :id
attr_accessible :quantity
attr_accessible :shipping_profile_attributes
end
class ShippingProfile
include Mongoid::Document
belongs_to :variation, :class_name => "ProductVariation"
field :weight, :type => Float
attr_accessible :id
attr_accessible :weight
end
test/unit/product_varitation_test.rb
require 'test_helper'
class ProductVariationTest < ActiveSupport::TestCase
def setup
ProductVariation.delete_all
ShippingProfile.delete_all
end
test "mass assignment" do
params = {
"product_variation"=>{
"quantity"=>"13",
"shipping_profile_attributes"=>{
"weight"=>"66",
"id"=>"4dae758ce1607c1d18000074"
}
},
"id"=>"4dae758ce1607c1d18000073"
}
product_variation_id = params['id']
shipping_profile_id = params['product_variation']['shipping_profile_attributes']['id']
product_variation = ProductVariation.create("id" => product_variation_id)
shipping_profile = ShippingProfile.create("id" => shipping_profile_id)
product_variation.shipping_profile = shipping_profile
assert_equal(1, ProductVariation.count)
assert_equal(1, ShippingProfile.count)
product_variation.update_attributes(params['product_variation'])
assert_equal('13', ProductVariation.find(product_variation_id)['quantity'])
assert_equal(66.0, ShippingProfile.find(shipping_profile_id)['weight'])
p ProductVariation.find(product_variation_id)
p ShippingProfile.find(shipping_profile_id)
end
end
test output
Run options: --name=test_mass_assignment
# Running tests:
#<ProductVariation _id: 4dae758ce1607c1d18000073, _type: nil, quantity: "13">
#<ShippingProfile _id: 4dae758ce1607c1d18000074, _type: nil, variation_id: BSON::ObjectId('4dae758ce1607c1d18000073'), weight: 66.0>
.
Finished tests in 0.014682s, 68.1106 tests/s, 272.4424 assertions/s.
1 tests, 4 assertions, 0 failures, 0 errors, 0 skips
Process finished with exit code 0

Rails AR validates_uniqueness_of against polymorphic relationship

Is it posible to validate the uniqueness of a child model's attribute scoped against a polymorphic relationship?
For example I have a model called field that belongs to fieldable:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :name, :scope => :fieldable_id
end
I have several other models (Pages, Items) which have many Fields. So what I want is to validate the uniqueness of the field name against the parent model, but the problem is that occasionally a Page and an Item share the same ID number, causing the validations to fail when they shouldn't.
Am I just doing this wrong or is there a better way to do this?
Just widen the scope to include the fieldable type:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :name, :scope => [:fieldable_id, :fieldable_type]
end
You can also add a message to override the default message, or use scope to add the validation:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :fieldable_id, :scope => [:fieldable_id, :fieldable_type], :message => 'cannot be duplicated'
end
As a bonus if you go to your en.yml, and enter:
activerecord:
attributes:
field:
fieldable_id: 'Field'
You are going to replace the default 'subject' that rails add to the errors with the one you specify here. So instead of saying: Fieldable Id has been already taken or so, it would say:
Field cannot be duplicated

Save has_and_belongs_to_many child

I have a User model and a Role model. They are joined by a has_and_belongs_to_many relationship. When an admin creates a user I want them to be able to assign a role to the user and have it saved when I call #user.save
The thing is though is that I get a warning that I can't mass-assign the roles relationship.
Any suggestions on how to go about this, I am on Rails 2.3.2
Thanks.
Edit: Code as requested.
user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :roles,
:join_table => "users_roles",
:foreign_key => "role_id",
:associated_foreign_key => "user_id"
end
role.rb
class Role < ActiveRecord::Base
has_and_belongs_to_many :users,
:join_table => "users_roles",
:foreign_key => "user_id",
:association_foreign_key => "role_id"
end
View: new.html.haml
- form_for(#user, users_path(:subdomain => current_account.subdomain)) do |f|
.input_set
= f.label(:login, "Username")
= f.text_field(:login)
.input_set
= f.label(:name)
= f.text_field(:name)
.input_set
= f.label(:email)
= f.text_field(:email)
- fields_for("user[roles][]", Role)do |user_role|
.input_set
%label Role
= user_role.select(:name, Role.all.map{|r| [r.name, r.id] })
.input_set
= f.label(:password)
= f.password_field(:password)
.input_set
= f.label(:password_confirmation, "Password Again")
= f.password_field(:password_confirmation)
.input_set
%label
= f.submit "Add User"
And I want the Role to be saved to the user by calling #user.save in my create option. Is that possible? Or is this a relationship I can't use that way, would it need to a has_many relationship for me to be able to do this.
I believe you need to call attr_accessible on the attributes in the model that you want to save in order to avoid the mass-assign error.
You cannot use accepts_nested_attributes_for for a habtm relationship.
You can however set the role_ids, see Railscast Episode 17 for details
In your case the problem is that you set only a single role but have a habtm relationship, why not a belongs_to?
Are you using the new accepts_nested_attributes_for method?
It will probably look something like this:
class User < ActiveRecord::Base
accepts_nested_attributes_for :roles, :allow_destroy => true
end
Check out this sample app for more detail.
Given the time since the question was asked, you've probably worked this out on your own...
The thing is though is that I get a
warning that I can't mass-assign the
roles relationship.
This is caused by one of two things in your User model.
You called attr_accessible and the list of symbols provide does not include :roles
You called attr_protected and the list of symbols includes :roles
Either add :roles to your attr_accessible call or remove it from the attr_protected call.
If I could edit/add to an answer I would. I had something similar required to what #EmFi mentioned. I had attr_accessible set, and had to add the equivalent of
:role_ids
to the attr_accessible of the user model. Note the pluralization. The following options did not work:
:role
:roles
:role_id
Just to be clear about the error message that I got:
WARNING: Can't mass-assign these protected attributes: role_ids
The warning didn't make a lot of sense to me since I'm using a habtm relationship. Nevertheless, it was correct.

Resources