How do I pass a var from one model's method to another? - ruby-on-rails

Here is my one model..
CardSignup.rb
def credit_status_on_create
Organization.find(self.organization_id).update_credits
end
And here's my other model. As you can see what I wrote here is an incorrect way to pass the var
def update_credits
#organization = Organization.find(params[:id])
credit_count = #organization.card_signups.select { |c| c.credit_status == true}.count
end
If it can't be done by (params[:id]), what can it be done by?
Thanks!

Ideally the data accessible to the controller should be passed as parameter to model methods. So I advise you to see if it is possible to rewrite your code. But here are two possible solutions to your problem. I prefer the later approach as it is generic.
Approach 1: Declare a virtual attribute
class CardSignup
attr_accessor call_context
def call_context
#call_context || {}
end
end
In your controller code:
def create
cs = CardSignup.new(...)
cs.call_context = params
if cs.save
# success
else
# error
end
end
In your CardSignup model:
def credit_status_on_create
Organization.find(self.organization_id).update_credits(call_context)
end
Update the Organization model. Note the change to your count logic.
def update_credits
#organization = Organization.find(call_context[:id])
credit_count = #organization.card_signups.count(:conditions =>
{:credit_status => true})
end
Approach 2: Declare a thread local variable accessible to all models
Your controller code:
def create
Thread.local[:call_context] = params
cs = CardSignup.new(...)
if cs.save
# success
else
# error
end
end
Update the Organization model. Note the change to your count logic.
def update_credits
#organization = Organization.find((Thread.local[:call_context] ||{})[:id])
credit_count = #organization.card_signups.count(:conditions =>
{:credit_status => true})
end

Use an attr_accessor.
E.g.,
class << self
#myvar = "something for all instances of model"
attr_accessor :myvar
end
#myothervar = "something for initialized instances"
attr_accessor :myothervar
then you can access them as ModelName.myvar and ModelName.new.myvar respectively.

You don't say whether you're using Rails 2 or 3 but let's assume Rails 2 for this purpose (Rails 3 provides the a new DSL for constructing queries).
You could consider creating a named scope for in your Organization model as follows:
named_scope :update_credits,
lambda { |id| { :include => :card_signup, :conditions => [ "id = ? AND card_signups.credit_status = TRUE", id ] } }
And then use it as follows:
def credit_status_on_create
Organization.update_credits(self.organization_id)
end
Admittedly I don't quite understand the role of the counter in your logic but I'm sure you could craft that back into this suggestion if you adopt it.

Related

How to connect with different tables depending on parameter

I have model Board and BoardController where i can find all boards of my project.
All records have filled "board_layout" column with enum.
Now i have show method in BoardController and i want to load there different data from different table depending on board_layout column value.
I could do it like this:
def show
#board = Board.find(params[:id])
if #board.board_layout == 1
#tasks = Car.where(board_id: #board.id)
elsif #board.board_layout == 2
#tasks = Truck.where(board_id: #board.id)
end
end
But it's not elegant + it is not DRY (i need to use some sort of if statement anytime when i want to do something with those 2 tables).
So i have tried to create Concern and create case statement inside, now it looks like:
def show
#board = Board.find(params[:id])
#tasks = get_class_name(#board.board_layout).where(board_id: #board.id)
end
# inside my concern
def get_class_name(scope)
case scope
when 1
Car
when 2
Truck
end
end
My Question:
Is there better way to do it? Is my solution safe and clear?
What is the best solution to resolve problem like this?
I would appreciate any help.
maybe you can abstract that out into a class, so you can define multiple layout and their correspondent classes, like:
class LayoutClassGetter
CLASS_BY_LAYOUT = { '1' => Car, '2' => Truck }.freeze
def initialize(layout_number)
#layout_number = layout_number
end
def layout_class
CLASS_BY_LAYOUT[#layout_number]
end
def self.layout_class(layout_number)
new(layout_number).layout
end
end
And then use it:
def show
#board = Board.find(params[:id])
#tasks = layout_class(#board.board_layout).where(board_id: #board.id)
end
def layout_class(scope)
LayoutClassGetter.layout_class(scope)
end

Rails 5 - iterate until field matches regex

In my app that I am building to learn Rails and Ruby, I have below iteration/loop which is not functioning as it should.
What am I trying to achieve?
I am trying to find the business partner (within only the active once (uses a scope)) where the value of the field business_partner.bank_account is contained in the field self_extracted_data and then set the business partner found as self.sender (self here is a Document).
So once a match is found, I want to end the loop. A case exists where no match is found and sender = nil so a user needs to set it manually.
What happens now, is that on which ever record of the object I save (it is called as a callback before_save), it uses the last identified business partner as sender and the method does not execute again.
Current code:
def set_sender
BusinessPartner.active.where.not(id: self.receiver_id).each do |business_partner|
bp_bank_account = business_partner.bank_account.gsub(/\s+/, '')
rgx = /(?<!\w)(#{Regexp.escape(bp_bank_account)})?(?!\‌​w)/
if self.extracted_data.gsub(/\s+/, '') =~ rgx
self.sender = business_partner
else
self.sender = nil
end
end
end
Thanks for helping me understand how to do this kind of case.
p.s. have the pickaxe book here yet this is so much that some help / guidance would be great. The regex works.
Using feedback from #moveson, this code works:
def match_with_extracted_data?(rgx_to_match)
extracted_data.gsub(/\s+/, '') =~ rgx_to_match
end
def set_sender
self.sender_id = matching_business_partner.try(:id) #unless self.sender.id.present? # Returns nil if no matching_business_partner exists
end
def matching_business_partner
BusinessPartner.active.excluding_receiver(receiver_id).find { |business_partner| sender_matches?(business_partner) }
end
def sender_matches?(business_partner)
rgx_registrations = /(#{Regexp.escape(business_partner.bank_account.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.registration.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.vat_id.gsub(/\s+/, ''))})/
match_with_extracted_data?(rgx_registrations)
end
In Ruby you generally want to avoid loops and #each and long, procedural methods in favor of Enumerable iterators like #map, #find, and #select, and short, descriptive methods that each do a single job. Without knowing more about your project I can't be sure exactly what will work, but I think you want something like this:
# /models/document.rb
class Document < ActiveRecord::Base
def set_sender
self.sender = matching_business_partner.try(:id) || BusinessPartner.active.default.id
end
def matching_business_partners
other_business_partners.select { |business_partner| account_matches?(business_partner) }
end
def matching_business_partner
matching_business_partners.first
end
def other_business_partners
BusinessPartner.excluding_receiver_id(receiver_id)
end
def account_matches?(business_partner)
rgx = /(?<!\w)(#{Regexp.escape(business_partner.stripped_bank_account)})?(?!\‌​w)/
data_matches_bank_account?(rgx)
end
def data_matches_bank_account?(rgx)
extracted_data.gsub(/\s+/, '') =~ rgx
end
end
# /models/business_partner.rb
class BusinessPartner < ActiveRecord::Base
scope :excluding_receiver_id, -> (receiver_id) { where.not(id: receiver_id) }
def stripped_bank_account
bank_account.gsub(/\s+/, '')
end
end
Note that I am assigning an integer id, rather than an ActiveRecord object, to self.sender. I think that's what you want.
I didn't try to mess with the database relations here, but it does seem like Document could include a belongs_to :business_partner, which would give you the benefit of Rails methods to help you find one from the other.
EDIT: Added Document#matching_business_partners method and changed Document#set_sender method to return nil if no matching_business_partner exists.
EDIT: Added BusinessPartner.active.default.id as the return value if no matching_business_partner exists.

How to refactor complex method in Rails model with Rspec?

I have the following complex method. I'm trying to find and implement possible improvements. Right now I moved last if statement to Access class.
def add_access(access)
if access.instance_of?(Access)
up = UserAccess.find(:first, :conditions => ['user_id = ? AND access_id = ?', self.id, access.id])
if !up && company
users = company.users.map{|u| u.id unless u.blank?}.compact
num_p = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', users, access.id])
if num_p < access.limit
UserAccess.create(:user => self, :access => access)
else
return "You have exceeded the maximum number of alotted permissions"
end
end
end
end
I would like to add also specs before refactoring. I added first one. How should looks like others?
describe "#add_permission" do
before do
#permission = create(:permission)
#user = create(:user)
end
it "allow create UserPermission" do
expect {
#user.add_permission(#permission)
}.to change {
UserPermission.count
}.by(1)
end
end
Here is how I would do it.
Make the check on the Access more like an initial assertion, and raise an error if that happens.
Make a new method to check for an existing user access - that seems reusable, and more readable.
Then, the company limit is more like a validation to me, move this to the UserAccess class as a custom validation.
class User
has_many :accesses, :class_name=>'UserAccess'
def add_access(access)
raise "Can only add a Access: #{access.inspect}" unless access.instance_of?(Access)
if has_access?(access)
logger.debug("User #{self.inspect} already has the access #{access}")
return false
end
accesses.create(:access => access)
end
def has_access?(access)
accesses.find(:first, :conditions => {:access_id=> access.id})
end
end
class UserAccess
validate :below_company_limit
def below_company_limit
return true unless company
company_user_ids = company.users.map{|u| u.id unless u.blank?}.compact
access_count = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', company_user_ids, access.id])
access_count < access.limit
end
end
Do you have unit and or integration tests for this class?
I would write some first before refactoring.
Assuming you have tests, the first goal might be shortening the length of this method.
Here are some improvements to make:
Move the UserAccess.find call to the UserAccess model and make it a named scope.
Likewise, move the count method as well.
Retest after each change and keep extracting until it's clean. Everyone has a different opinion of clean, but you know it when you see it.
Other thought, not related to moving the code but still cleaner :
users = company.users.map{|u| u.id unless u.blank?}.compact
num_p = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', users, access.id])
Can become :
num_p = UserAccess.where(user_id: company.users, access_id: access.id).count

How can I combine the results of different variables into one variable?

I have following four variables in my controller index action which are retrieving data from different models as follows:
#forum = Forum.where(:user_id => #users.collect(&:user_id)).all
#poll=Poll.where(:created_by => #users.collect(&:user_id)).all
#article = Article.where(:user_id => #users.collect(&:user_id)).all
#jobpost = Jobplacement.where(:user_id => #users.collect(&:user_id)).all
I want to join all these variables' data into a single variable #post. How can I do this?
It is not good to have different type of objects in single collection.
But as you asked try
#post = [#forum,#forum,#article,#jobpost].flatten
Update:
I wrote this answer when I was a newbie in Ruby. When I look this answer I can not control my smile. The purpose of the flatten is to make a single array from the nested arrays. The answer does not relate to the question. But I am surprised about the upvotes :)
Put them in a hash:
#post = Hash.new
#post['forum'] = Forum.where(:user_id => #users.collect(&:user_id)).all
#post['poll'] = Poll.where(:created_by => #users.collect(&:user_id)).all
#post['article'] = Article.where(:user_id => #users.collect(&:user_id)).all
#post['job_placement'] = Jobplacement.where(:user_id => #users.collect(&:user_id)).all
They are not joined, but they are in one single variable. You can access them whenever you want, and do with them whatever you want.
Something like this:
conditions = { :user_id => #users } # assuming primary_key is set correctly
# in the User model
#post = Forum.where( conditions ).all +
Poll.where( conditions ).all +
Article.where( conditions ).all +
Jobplacement.where( conditions ).all
Or if you want to get fancy:
models = [ Forum, Poll, Article, Jobplacement ]
#post = models.reduce [] do |records, model|
records.push *model.where( :user_id => #users ).all
end
Note: .all might be unnecessary in both cases since it's usually called automatically by Rails when necessary, but I'm not certain.
I think you need like view model concept. Create a simple model class without inherit from ActiveRecord::Base and add all objects as attributes in the new class and initialize that.
class Post
attr_accessor :forum, :poll, :article, :jobpost
def initialize(forum,poll,article,jobpost)
#forum = forum
#poll = poll
#article = article
#jobpost = jobpost
end
end
In the controller action add the following;
#post = Post.new(#forum,#poll,#article,#jobpost)

Rails How can one query association definitions

I have a lot of dynamic code which keeps complex relations in a string.
ex:
"product.country.continent.planet.galaxy.name"
How can I check if these relations exist?
I want a method like the following:
raise "n00b" unless Product.has_associations?("product.country.planet.galaxy")
How could I implement this?
Try this:
def has_associations?(assoc_str)
klass = self.class
assoc_str.split(".").all? do |name|
(klass = klass.reflect_on_association(name.to_sym).try(:klass)).present?
end
end
If these are active record associations, here's how you can do it:
current_class = Product
has_associations = true
paths = "country.planet.galaxy".split('.')
paths.each |item|
association = current_class.reflect_on_association( item )
if association
current_class = association.klass
else
has_associations = false
end
end
puts has_association
And this will tell you if this specific path has all the associations.
If indeed you are storing the AR associations in a string like that, this code placed in an initializer should let you do what you want. For the life of me I can't quite figure out why you'd want to do this, but I trust you have your reasons.
class ActiveRecord::Base
def self.has_associations?(relation_string="")
klass = self
relation_string.split('.').each { |j|
# check to see if this is an association for this model
# and if so, save it so that we can get the class_name of
# the associated model to repeat this step
if assoc = klass.reflect_on_association(j.to_sym)
klass = Kernel.const_get(assoc.class_name)
# alternatively, check if this is a method on the model (e.g.: "name")
elsif klass.instance_method_already_implemented?(j)
true
else
raise "Association/Method #{klass.to_s}##{j} does not exist"
end
}
return true
end
end
With this you'll need to leave off the initial model name, so for your example it would be:
Product.has_associations?("country.planet.galaxy")

Resources