Stack level too deep on user.save - ruby-on-rails

I want to assign a confirmation code to my users while creating one. And I also titleize some columns before saving-updating them. So my user.rb looks like this (it may be a bit messy):
// user.rb
*** some code ***
before_save { titleize_column(:name)
titleize_column(:surname)
capitalize_column(:complaints)
capitalize_column(:education)
capitalize_column(:job)
capitalize_column(:complaintsdetails)
capitalize_column(:prediagnosis)
capitalize_column(:existingdiagnosis)
capitalize_column(:knownilnessesother)
capitalize_column(:usedmedicine)
capitalize_column(:operation)
capitalize_column(:trauma)
capitalize_column(:allergy)
capitalize_column(:otherhabits)
capitalize_column(:motherother)
capitalize_column(:fatherother)
capitalize_column(:siblingsother)
}
before_save :generate_confirmation_code
protected
def generate_confirmation_code
unless self[:confirmed]
if(self[:type] == 'Patient')
update_attribute :confirmation_code, SecureRandom.urlsafe_base64(20)
update_attribute :confirmed, false
else
update_attribute :confirmed, true
end
end
end
protected
def capitalize_column(attr)
unless self[attr].nil?
self[attr] = Unicode::capitalize self[attr]
end
end
protected
def titleize_column(attr)
unless self[attr].nil?
words = self[attr].split
words.each_with_index do |v,i|
words[i] = Unicode::capitalize v
end
self[attr] = words.join(" ")
end
end
I'm using separate methods for titleizing and capitalizing columns because they may be nil when first creating a user, so I'm checking if it is null or not in those methods. This structure works fine on a normal signup with strong parameters. However, if I try to use twitter signup with the method below, it gives me the error 'stack level too deep' and I can see that it calls the generate_confirmation_code 123 times from the application trace and then these happens:
app/models/user.rb:83:in each'
app/models/user.rb:83:ineach_with_index'
app/models/user.rb:83:in titleize_column'
app/models/user.rb:20:inblock in '
app/models/user.rb:64:in generate_confirmation_code' (x123 times)
app/models/user.rb:101:infrom_omniauth'
app/controllers/socials_controller.rb:4:in `create'
// method for signing up/logging in a user from twitter
class << self
def from_omniauth(auth_hash)
if exists?(uid: auth_hash['uid'])
user = find_by(uid: auth_hash['uid'])
else
user = find_or_create_by(uid: auth_hash['uid'], provider: auth_hash['provider'], type: 'Patient')
user.password_digest = User.digest('111111')
user.name = auth_hash['info']['name']
user.location = get_social_location_for user.provider, auth_hash['info']['location']
user.avatar = auth_hash['info']['image']
user.url = get_social_url_for user.provider, auth_hash['info']['urls']
user.save! // THIS IS THE LINE 101!
conversation = Conversation.create()
user.conversation = conversation
admin = Admin.first
admin.conversations << conversation
user.progress = Progress.create(active_state:1)
end
user
end
I think I'm messing up by using before_save not properly, but do not know how to do it right. What am I doing wrong here?

update_attribute also fires the save callbacks, thereby looping the before_save infinitely, thus producing stack level too deep.
You can just simply assign values in a before_save callback methods, because they will simply be saved afterwards anyway. See the following:
def generate_confirmation_code
unless self[:confirmed]
if(self[:type] == 'Patient')
self.confirmation_code = SecureRandom.urlsafe_base64(20)
self.confirmed = false
else
self.confirmed = true
end
end
end

You are calling update_attribute inside before_save callback method, instead you can just assign values to attributes. The method signature generate_confirmation_code should be like below -
def generate_confirmation_code
unless self[:confirmed]
if(self[:type] == 'Patient')
self.confirmation_code = SecureRandom.urlsafe_base64(20)
self.confirmed = false
else
self.confirmed = true
end
end
end

Related

updating the database after create action doesn't work

set_bonus(member_id, cookie) method does not work. I'm trying to update the same model that that the self.set_signup_attribution(member_id, cookie, origin) returns.
The new_has_value variable returns {"currency"=>"usd", "type"=>"flat", "amount"=>1000}
Model.rb
# THIS METHOD WORKS
def self.set_signup_attribution(member_id, cookie, origin)
return unless cookie.present?
tracking_code = cookie
attribution_channel = AttributionChannel.find_by tracking_code: tracking_code
associated_member_record = Member.find member_id
if attribution_channel.present?
Attribution.create!({
event: Attribution::SIGN_UP,
attribution_channel: attribution_channel,
associated: associated_member_record,
extra: origin
})
set_bonus(member_id, cookie)
else
Rails.logger.info "Unknown Attribution Channel for tracking code: '#{ tracking_code }'"
end
end
# THIS METHOD DOES NOT WORK. UPDATES THE DATABASE.
def self.set_bonus(member_id, cookie)
epoch = Member.find_by(id: member_id).attribution_epoch
attribution_code = AttributionChannel.find_by(tracking_code: cookie)
duration_value = attribution_code.attribution_duration.downcase.split(' ')
duration = duration_value.first.to_i.send(duration_value.last)
return if cookie.present? && epoch.present?
current_time = Time.now
if attribution_code.bonus_config.present?
if (current_time - epoch).to_i < duration
hash_value = attribution_code.bonus_config
new_hash_value = hash_value.assoc("sign_up")[1]
value = Attribution.where(attribution_channel_id: attribution_code)
if new_hash_value["type"] == "flat"
value.update_all(
bonus_amount: new_hash_value["amount"],
bonus_currency: new_hash_value["currency"]
)
elsif new_hash_value["type"] == "percentage"
value.update_all(
bonus_amount: new_hash_value["amount"],
bonus_currency: new_hash_value["currency"]
)
else
{
bonus_amount: "Doesn't exist",
bonus_currency: "Doesn't exist"
}
end
else
"Do nothing"
end
else
"Do nothing"
end
#cookie = nil
binding.pry
end
Controller.rb
def index
unless session[:just_signed_up]
redirect_back_or_settings_page
end
Attribution.set_signup_attribution(current_user, cookies[:visit_attr], request.referer)
Attribution.set_bonus(current_user, cookies[:visit_attr])
session[:just_signed_up] = false
#email = current_user.email
end
How do I go about this? That is what I have tried and doesn't work. Can I merge set_bonus method to set_signup_attribution method or something?
Any help will be appreciated.
So drilling this further:
I merged set_bonus with set_signup_attribution and the two fields (bonus_amount and bonus_currency) which set_bonus method is supposed to update returns nil:
Attribution.create!(
{
event: Attribution::SIGN_UP,
attribution_channel: attribution_channel,
associated: associated_member_record,
extra: origin
}.merge(self.set_bonus(member_id, cookie).to_h)
)
With this drill after using binding.pry on that set_bonus method, I figured out it worked but it's returning nil and I don't know why. Could it be because member_id is not available in the model or something?
in your if statement you should call set_bonus method on appropriate object.
attribution = Attribution.create!({
event: Attribution::SIGN_UP,
attribution_channel: attribution_channel,
associated: associated_member_record,
extra: origin
})
attribution.set_bonus(member_id, cookie) if attribution.persisted?
Just be careful as .create! will raise an error in case there is something wrong, so maybe would be better to use
attribution = Attribution.new(.....)
if attribution.save
attribution.set_bonus(.....)
else
Rails.logger.info attribution.errors
end
I hope this would help.
Cheers

Extending ActiveModel::Serializer with custom attributes method

I am trying to create my own attributes method called secure_attributes where I pass it an array of attributes and the minimum level the authorized user needs to be to view those attributes. I pass the current level of the authorized user as an instance_option. I'd like to extend the Serializer class so I can use this method in multiple serializers, but Im having issues.
This is what i have so far:
in config/initializers/secure_attributes.rb
module ActiveModel
class Serializer
def self.secure_attributes(attributes={}, minimum_level)
attributes.delete_if {|attr| attr == :attribute_name } unless has_access?(minimum_level)
attributes.each_with_object({}) do |name, hash|
unless self.class._fragmented
hash[name] = send(name)
else
hash[name] = self.class._fragmented.public_send(name)
end
end
end
end
end
and then in the individual serializer I have things like this:
secure_attributes([:id, :name, :password_hint], :guest)
and then
def has_access?(minimum_level=nil)
return false unless minimum_level
return true # based on a bunch of logic...
end
But obviously secure_attributes cannot see the has_access? method and if I put has_access inside the Serializer class, it cannot access the instance_options.
Any idea how I can accomplish what I need?
Maybe you want to do following - but I still do not get your real purpose, since you never did anything with the attributes but calling them:
module ActiveRecord
class JoshsSerializer < Serializer
class << self
def secure_attributes(attributes={}, minimum_level)
#secure_attributes = attributes
#minimum_level = minimum_level
end
attr_reader :minimum_level, :secure_attributes
end
def initialize(attr, options)
super attr, options
secure_attributes = self.class.secure_attributes.dup
secure_attributes.delete :attribute_name unless has_access?(self.class.minimum_level)
secure_attributes.each_with_object({}) do |name, hash|
if self.class._fragmented
hash[name] = self.class._fragmented.public_send(name)
else
hash[name] = send(name)
end
end
def has_access?(minimum_level=nil)
return false unless minimum_level
return true # based on a bunch of logic...
end
end
end

Why is my service not changing my instance variable?

My service is supposed to match the current user with an opponent. I have a method in my service that is supposed to find this opponent but it's not saving it in my controller. I realize this is a very basic question but I'm very new to rails, thank you!
Here is my service:
class MatchUser
attr_accessor :user_params
def initialize(user_params)
#user_params = user_params
end
def match(opponent)
return false if user_params[:matched] == true
#unmatched_users = User.where(matched: false)
#unique_unmatched_users = #unmatched_users.where.not(id: user_params[:id])
#same_league_unmatched_users = #unique_unmatched_users.where(league_id:
user_params[:league_id])
return false if #same_league_unmatched_users.empty?
opponent = #same_league_unmatched_users.sample
opponent.faceoff_date = Time.now
user_params[:faceoff_date] = Time.now
opponent.save!
end
end
Here is the part in my controller where I'm getting an error when I try to assign #matched_user_team because #matched_user is nil
#user = current_user
#matching = MatchUser.new(#user)
if #matching.match(#matched_user)
#matched_user_team = #matched_user.teams.last.chars
end

How should i transform this concern in service object?

I have a concern allowing me to give the back end user the ability to sort elements. I use it for a few different elements. The rails community seems to be pretty vocal against concern and callbacks, i'd like to have a few pointers on how to better model the following code :
require 'active_support/concern'
module Rankable
extend ActiveSupport::Concern
included do
validates :row_order, :presence => true
scope :next_rank, lambda { |rank| where('row_order > ?',rank).order("row_order asc").limit(1)}
scope :previous_rank, lambda { |rank| where('row_order < ?',rank).order("row_order desc").limit(1)}
scope :bigger_rank, order("row_order desc").limit('1')
before_validation :assign_rank
end
def invert(target)
a = self.row_order
b = target.row_order
self.row_order = target.row_order
target.row_order = a
if self.save
if target.save
true
else
self.row_order = a
self.save
false
end
else
false
end
end
def increase_rank
return false unless self.next_rank.first && self.invert(self.next_rank.first)
end
def decrease_rank
return false unless self.previous_rank.first && self.invert(self.previous_rank.first)
end
private
def assign_default_rank
if !self.row_order
if self.class.bigger_rank.first
self.row_order = self.class.bigger_rank.first.row_order + 1
else
self.row_order=0
end
end
end
end
I think a Concern is a good choice for what you are trying to accomplish (particularly with validations and scopes because ActiveRecord does those two very well). However, if you did want to move things out of the Concern, apart from validations and scopes, here is a possibility. Just looking at the code it seems like you have a concept of rank which is represented by an integer but can become it's own object:
class Rank
def initialize(rankable)
#rankable = rankable
#klass = rankable.class
end
def number
#rankable.row_order
end
def increase
next_rank ? RankableInversionService.call(#rankable, next_rank) : false
end
def decrease
previous_rank ? RankableInversionService.call(#rankable, previous_rank) : false
end
private
def next_rank
#next_rank ||= #klass.next_rank.first
end
def previous_rank
#previous_rank ||= #klass.previous_rank.first
end
end
To extract out the #invert method we could create a RankableInversionService (referenced above):
class RankableInversionService
def self.call(rankable, other)
new(rankable, other).call
end
def initialize(rankable, other)
#rankable = rankable
#other = other
#original_rankable_rank = rankable.rank
#original_other_rank = other.rank
end
def call
#rankable.rank = #other.rank
#other.rank = #rankable.rank
if #rankable.save && #other.save
true
else
#rankable.rank = #original_rankable_rank
#other.rank = #original_other_rank
#rankable.save
#other.save
false
end
end
end
To extract out the callback you could have a RankableUpdateService which will assign the default rank prior to saving the object:
class RankableUpdateService
def self.call(rankable)
new(rankable).call
end
def initialize(rankable)
#rankable = rankable
#klass = rankable.class
end
def call
#rankable.rank = bigger_rank unless #rankable.ranked?
#rankable.save
end
private
def bigger_rank
#bigger_rank ||= #klass.bigger_rank.first.try(:rank)
end
end
Now you concern becomes:
module Rankable
extend ActiveSupport::Concern
included do
# validations
# scopes
end
def rank
#rank ||= Rank.new(self)
end
def rank=(rank)
self.row_order = rank.number; #rank = rank
end
def ranked?
rank.number.present?
end
end
I'm sure there are issues with this code if you use it as is, but you get the concept. Overall I think the only thing that might be good to do here is extracting out a Rank object, other than that it might be too much complexity that the concern encapsulates pretty nicely.

inject method to retrieve data from hash

I'm having trouble getting the method below in my user model to handle a hash ('auth') I'm getting from LinkedIn for user signin:
def self.deep_get auth, *fields
auth.inject(auth) { |acc, e| acc[e] if acc }
end
I call the 'deep_get' method later in my user model as I create a user using omniauth/linkedin gem. However, it's returning nil values for the provider/uid/headline/email user fields that I know are not nil.
I included first_name and last_name fields as an example because this approach is working (not returning nil values), but (as I realize) bad style/exception handling. Any ideas as to why my deep_get inject method isn't working to retrieve the data in the hash as I'd like it to?
def self.create_from_omniauth(auth)
create! do |user|
# i'd like to retrieve user information from linkedin per the following with my inject method, but i am getting nil values when i should be getting data.
# :provider and :uid are on the same branch level of data. first_name,last_name,email,etc. are on a branch just below called 'info'
user.provider = deep_get(auth, :provider)
user.uid = deep_get(auth, :uid)
user.headline = deep_get(auth, :info, :headline)
user.email = deep_get(auth, :info, :email)
# the below is working but i know pokemon exception handling is not good style.
begin
user.first_name = auth["info"]["first_name"]
rescue
end
begin
user.last_name = auth["info"]["last_name"]
rescue
end
try this
def deep_find(obj,key)
if obj.respond_to?(:key?) && obj.key?(key)
obj[key]
elsif obj.respond_to?(:each)
r = nil
obj.find{ |*a| r=deep_find(a.last,key) }
r
end
end
or try this
class Hash
def deep_fetch(key, default = nil)
default = yield if block_given?
(deep_find(key) or default) or nil
end
def deep_find(key)
if key?(key)
self[key]
else
self.values.inject(nil) do |memo, v|
memo = v.deep_find(key) if v.respond_to?(:deep_find)
memo unless memo.nil?
end
end
end
end

Resources