Rails: How do I validate model based on parent's attributes? - ruby-on-rails

I have three models named Metric, Entry, and Measurement:
class Metric < ActiveRecord::Base
has_many :entries
attr_accessible :name, :required_measurements, :optional_measurements
end
class Entry < ActiveRecord::Base
belongs_to :metric
has_many :measurements
attr_accessible :metric_id
end
class Measurement < ActiveRecord::Base
belongs_to :entry
attr_accessible :name, :value
end
They are associated, and I can create nested instances of each this way:
bp = Metric.create(:name => "Blood Pressure", :required_measurement_names => ["Diastolic", "Systolic"])
bp_entry = bp.entries.create()
bp_entry.measurements.create({:name => "Systolic", :value =>140}, {:name => "Diastolic", :value =>90})
How do I validate the measurements for blood pressure based on the :required_measurement_names attribute in the Metric model? For example, how would I ensure that only "Systolic" and "Diastolic" are entered as measurements?
Is there a better way to about setting up these associations and validations?
Thanks!

Looks like entry is a join model between Measurement and Metric. so it makes sense for your validation to go there.
To ensure that all metrics required metrics are covered
def validate_required_measurements
metric.required_measurements.each{ |requirement|
unless (measurements.map{|measurement| measurement.name}.includes requirement)
errors.add_to_base "required measurement #{requirement} is missing"
end
}
end
To ensure that only accepted metrics are included (assuming optional metrics are accepted too).
def validate_accepted_measurements
measurements.each{ |measurement|
unless ((metric.required_measurements + metric.optional_measurements).include measurement.name )
errors.add_to_base "invalid measurement #{measurement.name} for metric"
end
}
end
Putting it all together add the above and following to the Entry model
validate: validate_accepted_measurements, :validate_required_measurements
N.B. my Ruby is a little rusty so this isn't guaranteed to work after copying and pasting, but it should get you close enough to fix the syntax errors that slipped through.

Related

Filling out an inherited mongoid document using nested attributes

Given the following models:
class Company
include Mongoid::Document
has_many :workers, autosave: true
accepts_nested_attributes_for :workers
attr_accessible :workers_attributes
end
class Worker
include Mongoid::Document
field :hours
attr_accessible :hours
belongs_to :company
end
class Manager < Worker
field :order
has_many :contributors, :class_name => "Worker"
attr_accessible :order, :contributors
end
class Contributor < Worker
field :task
belongs_to :manager, :class_name => "Worker"
attr_accessible :task
end
How does one create a manager in a company in the controller and view using nested attributes?
Here's my guess:
def new
#company = Company.new
#company.workers = [Manager.new]
end
def create
#company = Company.new params[:user]
if #company.save
redirect_to root_url, :notice => "Company with manager created."
else
render :new
end
end
= semantic_form_for #company do |f|
= f.semantic_fields_for :workers do |worker_fields|
= worker_fields.inputs do
= worker_fields.input :hours
= worker_fields.input :order
problem is the order field which specifically belongs to the manager is not persisting after the create. Also when the data is improperly filled there is an error:
undefined method `order' for #<Worker:0x0000000646f018> (ActionView::Template::Error)
So is there a way for nested attributes to handle inheritance in the models from mongoid?
The question is related to Can nested attributes be used in combination with inheritance? except instead of active record using mongoid.
Honestly, this is a paraphrasing of my code... the real code is more complex situation although i believe these are all of the relevant parts. If you have more questions ask.
UPDATE:
I changed the view to the following:
= semantic_form_for #company do |f|
- #company.workers.each do |worker|
- if worker._type == "Manager"
= f.semantic_fields_for :workers, worker do |worker_fields|
= worker_fields.inputs do
= worker_fields.input :hours
= worker_fields.input :order
I do not get the error anymore, however the nested attributes do not update the company object properly. The params are the following:
{"company"=> {"workers_attributes"=>{"0"=>{"hours"=>"30", "order" => "fish", "id"=>"4e8aa6851d41c87a63000060"}}}}
Again edited for brevity. So the key part is that there is a hash between "0" => {data for manager}. The workers data seems to be held in a hash. I would expect the data to look more like the following:
params = { company => {
workers_attributes => [
{ hours => "30", "order" => "fish" }
]}}
This is different because the workers data is held in an array instead of a hash. Is there another step to get the nested attributes to save properly?
Thanks
what version of Mongoid are you using? Because I don't think the use of refereneces_many is encouraged -- Not that that's related to your problem here, just wanted to probe what version you're using. In the doc on the gorgeous Mongoid.org, get this, I had to learn it the hard way, they say for Updating your records, you need to the autossave set to true. That's NOT accurate. You need it for even creating
so:
class Company
include Mongoid::Document
has_many :workers, :autossave => true # your money shot
accepts_nested_attributes_for :workers
attr_accessible :workers_attributes
end
ADDED:
I was re-reading your code, I spotted the following that might be the problem: Your Company model is set to has_many :workers and is set to accept nested attribbutes for Worker when changes come in, correct? And there is a field named Order in your Manager model which is subclassed from Worker. Yet you're having a form whose nested fields part is pointed at Worker not at Manager, the model that actually has the Order field. And that's obviously not enough, because Company isn't having_many :managers yet, you may need to set it to has_many :managers in the Company model as well.

rails - validate product ordered has the available quantity

I have two related validations on separate models. I can validate one way but not another. The post validation works: if I try to change the quantity available as the seller, and people have already ordered some of it, it (correctly) won't let me change the quantity below the amount ordered already.
The order validation does not work: if I try changing the order amount to more than is available, it lets me. It checks only the current sum (I guess before the save), and does not notice the buyer trying to sneak out more than what's available. When I try and change the order back to less than what is available, it won't let me, checking the current sum ordered (before save).
I also tried validating the quantity of the order + the remaining available stock (available -sum ordered), but have the same problem.
How would I get the validation to check what the quantity would be after the save, and then not save it should the value be invalid? It would have to work also for editing an order
class Post < ActiveRecord::Base
belongs_to :product
belongs_to :event
has_many :orders, :dependent => :destroy
attr_accessible :quantity, :deadline, :product_id, :event_id
validate :quantity_gt_ordered
def quantity_gt_ordered
self.errors.add(:quantity, " - People have already ordered more than this") unless self.quantity >= self.sum_orders
end
def sum_orders
self.orders.sum(:quantity)
end
class Order < ActiveRecord::Base
belongs_to :user
belongs_to :post
attr_accessible :order_price, :post_id, :user_id, :quantity
validate :quantity_is_available
def quantity_is_available
self.errors.add(:quantity, " - Please order only what's available") unless self.sum_post_orders <= self.post.quantity
end
def sum_post_orders
Order.where(:post => self.post).sum(:quantity)
end
You should just be able to compare to the available quantity minus the ordered quantity:
available_quantity = self.post.quantity - self.sum_post_order
if quantity > available_quantity
self.errors.add(:quantity, " - Please order only what's available")
end
And also make sure your sum_post_orders doesn't include the current order:
Order.where("id != ?", self.id).where(:post => self.post).sum(:quantity)

rails validate a belongs_to relation

Given a simple relationship where Person has_many Telephones. And a telephone only contains a telephonenumber which must be unique!
class Telephone < ActiveRecord::Base
validates_presence_of :contact_id
belongs_to :contact
validates :telephone, {:presence => true, :uniqueness => true}
end
class Contact < ActiveRecord::Base
has_many :telephones
validates_associated :telephones
has_many :emails
has_many :addresses
validates_presence_of :firstname
accepts_nested_attributes_for :telephones, :allow_destroy=>true
validates_presence_of :lastname
end
test "telephone number must be unique" do
john = contacts :johndoe #johndoe is a person with 1 existing number
2.times do
john.telephones.build :telephone=> "123" # 123 doesnt exist yet
end
puts Telephone.count # this gives 1
john.save
puts Telephone.count # this gives 3 !!!! ???
assert not(john.valid?) # This validates unless I remove the save above
end
Can someone explain the outcome of this test.
just calling valid? fails, but that is mentioned in the rdoc (must save first)
saving first does make valid? pass
BUT now I actually have 3 records in the database which breaks my unique requirement.
Is there a better way to do this? I don't understand the outcome of this test, it really goes against my expectations.
Ok if you read the ruby documentation you will notice that they mention that validating a model is not sufficient for uniqueness. YOU MUST use database unique constraints whenever possible. Otherwise it is possible when using two processes/threads/whatever that both will do a validation check, pass as unique, and then insert same values.
tl;dr: Add a unique constraint to the db column.

How to create nested objects using accepts_nested_attributes_for

I've upgraded to Rails 2.3.3 (from 2.1.x) and I'm trying to figure out the accepts_nested_attributes_for method. I can use the method to update existing nested objects, but I can't use it to create new nested objects. Given the contrived example:
class Product < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :notes
end
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id, :body
end
If I try to create a new Product, with a nested Note, as follows:
params = {:name => 'Test', :notes_attributes => {'0' => {'body' => 'Body'}}}
p = Product.new(params)
p.save!
It fails validations with the message:
ActiveRecord::RecordInvalid: Validation failed: Notes product can't be blank
I understand why this is happening -- it's because of the validates_presence_of :product_id on the Note class, and because at the time of saving the new record, the Product object doesn't have an id. However, I don't want to remove this validation; I think it would be incorrect to remove it.
I could also solve the problem by manually creating the Product first, and then adding the Note, but that defeats the simplicity of accepts_nested_attributes_for.
Is there a standard Rails way of creating nested objects on new records?
This is a common, circular dependency issue. There is an existing LightHouse ticket which is worth checking out.
I expect this to be much improved in Rails 3, but in the meantime you'll have to do a workaround. One solution is to set up a virtual attribute which you set when nesting to make the validation conditional.
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id, :unless => :nested
attr_accessor :nested
end
And then you would set this attribute as a hidden field in your form.
<%= note_form.hidden_field :nested %>
That should be enough to have the nested attribute set when creating a note through the nested form. Untested.
check this document if you use Rails3.
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#label-Validating+the+presence+of+a+parent+model
Ryan's solution is actually really cool.
I went and made my controller fatter so that this nesting wouldn't have to appear in the view. Mostly because my view is sometimes json, so I want to be able to get away with as little as possible in there.
class Product < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :note
end
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id unless :nested
attr_accessor :nested
end
class ProductController < ApplicationController
def create
if params[:product][:note_attributes]
params[:product][:note_attributes].each { |attribute|
attribute.merge!({:nested => true})
}
end
# all the regular create stuff here
end
end
Best solution yet is to use parental_control plugin: http://github.com/h-lame/parental_control

Alternatives to use polymorphism in Ruby on Rails

I'm currently writing some intranet web application where people could submit to admins requests for adding different resources. The example requests would be:
installing programs, in this case user will select which program he wants installed
increasing quota, in this case user will just enter the amount of disk space he needs or maybe he will select the predefined quantities - 1GB, 10GB etc...
create new email alias, in this case user will just type the alias.
...
I was thinking about having just one model UserRequests with the reference to the sender and
two optional attributes one would be reference_id that would refefrence to other tables (for
example the Program that he wants installed) and another would be used for free type fields
like email alias or quota.
So my problem is that based on the type of the request the model should contain either:
reference to other table
integer data
string data
Based on the type of the request the given action should be taken - probably email alias
could be added from rails but the application on users computer will be installed by hand.
Does anyone had similar problem? Do you think using polymorphism for this kind of stuff is a good idea? Do you have any suggestions on how to organize data in the tables?
Single Table Inheritance! This way you can have each type of request have custom validations, while still having every request live in the same table.
class CreateUserRequests < ActiveRecord::Migration
def self.up
create_table :user_requests do |t|
t.string :string_data, :type
t.integer :user_id, :integer_data
t.timestamps
end
end
def self.down
drop_table :user_requests
end
end
class UserRequest < ActiveRecord::Base
belongs_to :user
end
class EmailAliasRequest < UserRequest
validates_presence_of :string_data
validates_format_of :string_data, :with => EMAIL_REGEX
end
class ProgramInstallRequest < UserRequest
belongs_to :program, :class_name => "Program", :foreign_key => "integer_data"
validates_presence_of :integer_data
end
class QuotaIncreaseRequest < UserRequest
validates_presence_of :string_data
validates_inclusion_of :string_data, :in => %w( 1GB 5GB 10GB 15GB )
end
And of course, alias your string_data and integer_data to email or whatnot to make your other code have a little more meaning. Let the model be the little black box that hides it all away.
I would use polymorphic associations, which let a model belong to more than one other model using a single association. Something like this:
class AdminRequest < ActiveRecord::Base
belongs_to :user
belongs_to :requestable, :polymorphic => true
end
class EmailAlias < ActiveRecord::Base
has_many :admin_requests, :as => :requestable
end
class ProgramInstall < ActiveRecord::Base
has_many :admin_requests, :as => :requestable
end
class QuotaIncrease < ActiveRecord::Base
has_many :admin_requests, :as => :requestable
end
As ever, Ryan Bates has an excellent Railscast on the subject.

Resources