accepts_nested_attributes_for :reject_if to trigger another method - ruby-on-rails

I've got a multi-level nested form using formtastic_cocoon (jquery version of formtastic).
I am trying to do some validation in the sense of
if value is_numeric do
insert into database
else do
database lookup on text
insert id as association
end
I was hoping tha the accepts_nested_attributes_for would have an :if option, but apparently there is only the :reject_if.
Is there a way to create a validation like I describe as part of the accepts_nested_attributes_for??
-----------------------Updated as per Zubin's Response ---------------------------
I believe Zubin is on the right track with a method, but I can't seem to get it working just right. The method I am using is
def lookup_prereq=(lookup_prereq)
return if lookup_prereq.blank?
case lookup_prereq
when lookup_prereq.is_a?(Numeric) == true
self.task_id = lookup_prereq
else
self.task = Task.find_by_title(lookup_prereq)
end
end
When I trigger this function, the self.task_id is being put in the database as '0' rather than the Task.id.
I'm wondering if I'm missing something else.
I'm not completely sure that the method is actually being called. Shouldn't I need to say
lookup_prereq(attr[:prereq_id)
at some point?
-------------------further edit -----------------------
I think from what I can find that the method is called only if it is named with the same name as the value for the database, therefore I've changed the method to
def completed_task=(completed_task)
Unfortunately this is still resulting in 0 as the value in the database.

Sounds like you need a method in your nested model to handle that, eg:
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author
def lookup_author=(lookup_author)
return if lookup_author.blank?
case lookup_author
when /^\d+$/
self.author_id = lookup_author
else
self.author = Author.find_by_name(lookup_author)
end
end
end

Related

Access to model instance up the where chain in Rails

Is it possible to, within the record found through an association, retain access to the related model instance which found it?
Example:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
end
p = Person.first
p.info_of_the_moment = "I don't want this in the db"
assignment = p.assignments.first
assignment.somehow_get_p.info_of_the_moment # or some such magic!
And/or is there a way to "hang on to" the parameters of a scope and have access to them from within the found model instance? Like:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
scope :fun_assignments, -> (info) { where(fun: true) }
end
class Assignment < ApplicationRecord
belongs_to :person
def get_original_info
# When I was found, info was passed into the scope. What was it?
end
end
You can add your own extension methods to an association and those methods can get at the association's owner through proxy_association:
has_many :things do
def m
# Look at proxy_association.owner in here
end
end
So you could say things like:
class Person < ApplicationRecord
has_many :assignments do
def with_info
info = proxy_association.owner.info_of_the_moment
# Then we wave our hands and some magic happens to encode
# `info` into a properly escaped SQL literal that we can
# toss in a `select` call. If you're working with PostgreSQL
# then JSON would be a reasonable first choice if the info
# was, say, a hash.
#
# The `::jsonb` in the `select` call is there to tell everyone
# that the `info_of_the_moment` column is JSON and should be
# decoded as such by ActiveRecord.
encoded_info = ApplicationRecord.connection.quote(info.to_json)
select("assignments.*, #{encoded_info}::jsonb as info_of_the_moment")
end
end
#...
end
p = Person.first
p.info_of_the_moment = { 'some hash' => 'that does', 'not go in' => 'the database' }
assignment = p.assignments.with_info.first
assignment.info_of_the_moment # And out comes the hash but with stringified keys regardless of the original format.
# These will also include the `info_of_the_moment`
p.assignments.where(...).with_info
p.assignments.with_info.where(...)
Things of note:
All the columns in select show up as methods even when they're not part of the table in question.
You can add "extension" methods to an association by including a block with those methods when calling the association's method.
An SQL SELECT can include values that aren't columns, literals work just fine.
What format you use to tunnel your extra information through the association depends on the underlying database.
If the encoded extra information is large then this can get expensive.
This is admittedly a bit kludgey and brittle so I'd agree with you that rethinking your whole approach is a better idea.

Validate that the sum of the part is equal to the total with parent/children records

I have 2 models:
Invoice has_many :lines
Line belongs_to :invoice
I want to ensure that the sum of the Line for a given Invoice match the total of the related Invoice.
I've tried this:
validate :total_amount
def total_amount
inv_id = self.invoice_id
target_amount = Invoice.find(inv_id).total
total_lines = Line.where(invoice_id: inv_id).sum(:line_value)
errors.add(:total, " should be lower or equal to the total amount of the invoice") if total_lines > target_amount
end
But
it doesn't work for new objects (just updates)
even for updates it systematically throws an error
I've also seen a question talking about AssociatedValidator, but I haven't been able to grasp how to use that :(
It's not clear what exactly you want to validate, since your example is different from what you were describing prior to that.
I think something like this should work, using a before_add callback:
class Invoice < AR::Base
has_many :lines, :before_add => :validate_total
def validate_total(invoice, line)
totals = invoice.lines.sum(:line_value)
if totals + line.line_value > invoice.total
invoice.errors.add(:total, " should be lower or equal to the total amount of the invoice")
return false # I think you can alternatively raise an exception here
end
...
I might be interpreting it wrong, but if total is a column in the invoices table, I suggest removing it. Instead, have it as a method and have the method add up the Line prices plus any adjustments. Otherwise, you have duplication in the database. And that way you won't need to validate anything anyway :)
On a more general note, adding validations on associated models in ActiveRecord is not working very well. In some cases it's almost impossible, in other - pretty hard to get right. I think you've seen that it goes wrong easily. I suggest avoiding it and trying to design your database so that you won't need to (having Invoice#total as a method in this case).
It took a little while to find an question/answer to a problem that cropped up using accepts_nested_attributes_for. But the answer just said It's hard, if not impossible!. accepts_nested_attributes_for is a somewhat complicated approach, but it works - unless you are trying to validate a model based on a calculation of the children model. I may have found a way to to overcome the calculation problem.
I'm working on a web based double entry accounting application that had the following basic models;
class Account < ApplicationRecord
has_many :splits
has_many :entries, through: :splits
end
class Entry < ApplicationRecord
has_many :splits, -> {order(:account_id)}, dependent: :destroy, inverse_of: :entry
validate :balanced?
end
class Split < ApplicationRecord
belongs_to :entry, inverse_of: :splits
belongs_to :account
validates_associated :account
validates_associated :entry
end
Entries(transactions) must have at least two Splits that the sum of the Amount attribute(or Debits/Credits) in the Splits must equal 0. I though the validate :balanced? would take care of it, but an apparent Javascript error allowed an unbalance entry. I've yet to track the bug down, but since the Entry was unbalanced, I could not update it since valid? does not work (returns false) on new Splits that I tried to add.
The ledger accepts_nested_attributes_for form has quit a bit of Javascript that is not supposed to allow an unbalanced transaction to be submitted. Balanced? did not set an error on create, but its there on update. My approach to fixing it is not used validations that don't work, but to rely on a method called in conjunction with #entry.update(entry_params):
class Entry < ApplicationRecord
has_many :splits, -> {order(:account_id)}, dependent: :destroy, inverse_of: :entry
# validate :balanced? # took this out since its after the fact, balanced? method can still be called
accepts_nested_attributes_for :splits,
:reject_if => proc { |att| att[:amount].to_i.zero? && att['account_id'].to_i.zero?},
allow_destroy: true
def valid_params?(params)
split_sum = 0
params_hash = params.to_h
params_hash[:splits_attributes].each{|k,s| split_sum += s[:amount].to_i if s[:_destroy].to_i.zero?}
unless split_sum.zero?
errors.add(:amount, "Unbalanced: debits, credits must balance")
return false
else
return true
end
end
end
end
# update action from Entry Controller
def update
respond_to do |format|
if #entry.valid_params?(entry_params) && #entry.update(entry_params)
format.html { redirect_to account_path(session[:current_acct]), notice: 'Entry was successfully updated.' }
format.json { render :show, status: :ok, location: #entry }
else
# ... errr
end
end
end
Again, this in nothing more than validating the params verses the model validation that does not work for this condition.
This may be about same as answer 2, but not using a callback, just calling in controller

Proper way to do this with ActiveRecord?

Say I have two classes,
Image and Credit
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
end
I want associate a Credit when Image is created, acting a bit like a tag. Essentially, I want behavior like Credit.find_or_create_by_name, but in the client code using Credit, it would be much cleaner if it was just a Create. I can't seem to figure out a way to bake this into the model.
Try this:
class Image < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
after_create { Credit.associate_object(self) }
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
def self.associate_object(object, association='images')
credit = self.find_or_create_by_name(object.credit_name)
credit.send(association) << object
credit.save
end
end
Then when you create an image what you can do is something like
Image.create(:attr1 => 'value1', :attr2 => 'value2', ..., :credit_name => 'some_name')
And it will take the name that you feed into the :credit_name value and use it in the after_create callback.
Note that if you decided to have a different object associated with Credit later on (let's say a class called Text), you could do still use this method like so:
class Text < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
before_create { Credit.associate_object(self, 'texts') }
end
Although at that point you probably would want to consider making a SuperClass for all of the classes that belong_to credit, and just having the superclass handle the association. You might also want to look at polymorphic relationships.
This is probably more trouble than it's worth, and is dangerous because it involves overriding the Credit class's initialize method, but I think this might work. My advice to you would be to try the solution I suggested before and ditch those gems or modify them so they can use your method. That being said, here goes nothing:
First you need a way to get at the method caller for the Credit initializer. Let's use a class I found on the web called CallChain, but we'll modify it for our purposes. You would probably want to put this in your lib folder.
class CallChain
require 'active_support'
def self.caller_class
caller_file.split('/').last.chomp('.rb').classify.constantize
end
def self.caller_file(depth=1)
parse_caller(caller(depth+1).first).first
end
private
#Stolen from ActionMailer, where this was used but was not made reusable
def self.parse_caller(at)
if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
file = Regexp.last_match[1]
line = Regexp.last_match[2].to_i
method = Regexp.last_match[3]
[file, line, method]
end
end
end
Now we need to overwrite the Credit classes initializer because when you make a call to Credit.new or Credit.create from another class (in this case your Image class), it is calling the initializer from that class. You also need to ensure that when you make a call to Credit.create or Credit.new that you feed in :caller_class_id => self.id to the attributes argument since we can't get at it from the initializer.
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
attr_accessor :caller_class_id
def initialize(args = {})
super
# only screw around with this stuff if the caller_class_id has been set
if caller_class_id
caller_class = CallChain.caller_class
self.send(caller_class.to_param.tableize) << caller_class.find(caller_class_id)
end
end
end
Now that we have that setup, we can make a simple method in our Image class which will create a new Credit and setup the association properly like so:
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
# for building
def build_credit
Credit.new(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
# for creating
# if you wanted to have this happen automatically you could make the method get called by an 'after_create' callback on this class.
def create_credit
Credit.create(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
end
Again, I really wouldn't recommend this, but I wanted to see if it was possible. Give it a try if you don't mind overriding the initialize method on Credit, I believe it's a solution that fits all your criteria.

Method ignoring parameter value in Ruby on Rails, using default value instead

I'm having some issues in RoR with some model methods I am setting. I'm trying to build a method on one model, with an argument that gets supplied a default value (nil). The ideal is that if a value is passed to the method, it will do something other than the default behavior. Here is the setup:
I currently have four models: Market, Deal, Merchant, and BusinessType
Associations look like this:
class Deal
belongs_to :market
belongs_to :merchant
end
class Market
has_many :deals
has_many :merchants
end
class Merchant
has_many :deals
belongs_to :market
belongs_to :business_type
end
class BusinessType
has_many :merchants
has_many :deals, :through => :merchants
end
I am trying to pull some data based on Business Type (I have greatly simplified the return, for the sake of brevity):
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals(:conditions => ['market_id = ?',market]).sum('price')
end
end
end
So, if I do something like:
puts BusinessType.first.revenue
I get the expected result, that is the sum of the price of all deals associated with that business type. However, when I do this:
puts BusinessType.first.revenue(1)
It still returns the sum price of all deals, NOT the sum price of all deals from market 1. I've also tried:
puts BusinessType.first.revenue(market=1)
Also with no luck.
What am I missing?
Thanks!
Try this:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.all.sum(&:price)
else
return self.deals.find(:all, :conditions => ['market_id = ?',market]).sum(&:price)
end
end
end
That should work for you, or at least it did for some basic testing I did first.
As I have gathered, this is because the sum method being called is on enumerable, not the sum method from ActiveRecord as you might have expected.
Note:
I just looked a bit further, and noticed you can still use your old code with a smaller tweak than the one I noted:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals.sum('price', :conditions => ['market_id = ?', market])
end
end
end
Try this!
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum(:price)
else
return self.deals.sum(:price,:conditions => ['market_id = ?',market])
end
end
end
You can refer this link for other functions. http://en.wikibooks.org/wiki/Ruby_on_Rails/ActiveRecord/Calculations

I feel like this needs to be refactored - any help? Ruby modeling

So let's say you have
line_items
and line_items belong to a make and a model
a make has many models and line items
a model belongs to a make
For the bare example idea LineItem.new(:make => "Apple", :model => "Mac Book Pro")
When creating a LinteItem you want a text_field box for a make and a model. Makes and models shouldn't exist more than once.
So I used the following implementation:
before_save :find_or_create_make, :if => Proc.new {|line_item| line_item.make_title.present? }
before_save :find_or_create_model
def find_or_create_make
make = Make.find_or_create_by_title(self.make_title)
self.make = make
end
def find_or_create_model
model = Model.find_or_create_by_title(self.model_title) {|u| u.make = self.make}
self.model = model
end
However using this method means I have to run custom validations instead of a #validates_presence_of :make due to the associations happening off a virtual attribute
validate :require_make_or_make_title, :require_model_or_model_title
def require_make_or_make_title
errors.add_to_base("Must enter a make") unless (self.make || self.make_title)
end
def require_model_or_model_title
errors.add_to_base("Must enter a model") unless (self.model || self.model_title)
end
Meh, this is starting to suck. Now where it really sucks is editing with forms. Considering my form fields are a partial, my edit is rendering the same form as new. This means that :make_title and :model_title are blank on the form.
I'm not really sure what the best way to rectify the immediately above problem is, which was the final turning point on me thinking this needs to be refactored entirely.
If anyone can provide any feedback that would be great.
Thanks!
I don't think line_items should belong to a make, they should only belong to a model. And a model should have many line items. A make could have many line items through a model. You are missing a couple of methods to have your fields appear.
class LineItem
belongs_to :model
after_save :connect_model_and_make
def model_title
self.model.title
end
def model_title=(value)
self.model = Model.find_or_create_by_title(value)
end
def make_title
self.model.make.title
end
def make_title=(value)
#make = Make.find_or_create_by_title(value)
end
def connect_model_and_make
self.model.make = #make
end
end
class Model
has_many :line_items
belongs_to :make
end
class Make
has_many :models
has_many :line_items, :through => :models
end
It's really not that bad, there's just not super easy way to do it. I hope you put an autocomplete on those text fields at some point.

Resources