Refactor: Generating a unique slug for a Model - ruby-on-rails

I'm currently generating url slugs dynamically for my models (and implementing to_param/self.from_param to interpret them). My slug generation code feels verbose, and could use a refactor.
How would you refactor this so that it is still readable, but less verbose and perhaps more clear?
Relationships
User has_many :lists
List belongs_to :owner
Code
def generate_slug
if self.owner
slug_found = false
count = 0
temp_slug = to_slug
until slug_found
# increment the count
count += 1
# create a potential slug
temp_slug = if count > 1
suffix = "_" + count.to_s
to_slug + suffix
else
to_slug
end
# fetch an existing slug for this list's owner's lists
# (i.e. owner has many lists and list slugs should be unique per owner)
existing = self.owner.lists.from_param(temp_slug)
# if it doesn't exist, or it exists but is the current list, slug found!
if existing.nil? or (existing == self)
slug_found = true
end
end
# set the slug
self.slug = temp_slug
else
Rails.logger.debug "List (id: #{self.id}, slug: #{self.slug}) doesn't have an owner set!"
end
end

You could maybe do this
def generate_slug
return Rails.logger.debug "List (id: #{self.id}, slug: #{self.slug}) doesn't have an owner set!" if !self.owner
count = 1
begin
temp_slug = %Q!#{to_slug}#{"_#{count}" if count > 1}!
existing = self.owner.lists.from_param(temp_slug)
if existing.nil? or (existing == self)
self.slug = temp_slug
end
end while count += 1
end
But there is two things. First you have an infinite loop which is not good. Secondly, instead of looping to check each time if the object exists and that you need to increase your suffix, you better get the last existing list and add just one after that.

Related

Rails Check If Record Is First

I am iterating through a list of records. I need to check that if a record is first do XYZ and if not do ABC. Unfortunately I cant do this:
user = User.first
or
user = User.find(:id)
user.first?
Solution posted below
1. Make method to grab next and previous records
def next
[Model].where("id > ?", id).first
end
def prev
[Model].where("id < ?", id).last
end
2. Make method to check if record is first
def first?(record)
[Model].first == record
end
3. check if record is first
records.each do |record|
if record.first?(record)
record.update_attributes(attr: record.attr + record.attr)
else
prev_rec = [Model].find(record.id).prev
record.update_attributes(attr: prev_rec.attr + record.attr )
end
end
returns true or false
One improvement i would make sure that [Model].first is persistent so that it doesn't make a call to the database each time the loop is run.

Rails Remove Model from ActiveRecord::Relation Query

What's the best way to dynamically remove a model from a query? Basically I want to find all campaigns where a user hasn't already provided a response.
The below method delete_at actually deletes the model which isn't what I want. I only want it remove from the local 'campaigns' ActiveRecord::Relation query set that I got.
def self.appuser_campaigns appuser_id, language
appuser = Appuser.find(appuser_id)
campaigns = Campaign.check_language language
i = -1
campaigns.each do |campaign|
i = i + 1
responses = Response.where(appuser_id: appuser_id, campaign_id: campaign.id)
if responses.length > 0
campaigns.delete_at(i)
end
end
puts campaigns.class.name #"ActiveRecord::Relation"
campaigns
end
def self.check_language language
campaigns = Campaign.where(language: language, status: "In Progress")
end
You can do the following:
already_answered_campaign_ids = Appuser.find(appuser_id).responses.pluck(:campaign_id)
Campaign.where('id NOT IN (?)', already_answered_campaign_ids.presence || -1)

Quora-like duplicated slugs

I've looked on SO for an answer to this question, but can't seem to find one.
Basically what I want to do is something like Quora's Url Structure, where if a profile has the name Thomas Jefferson it becomes quora.com/thomas-jefferson. If there is already a Thomas Jefferson on the site, then it would become quora.com/thomas-jefferson-1, and so on for x number of duplicates.
The FriendlyId gem has something sort of like this, but instead of incrementing they generate a SecureRandom string, which is kind of ugly.
I have a Rails model that looks like this so far:
class Profile < ActiveRecord::Base
before_create :generate_slug
def generate_slug do
self.slug = loop do
slug = to_slug(self.name)
break slug unless Profile.exists?(slug: slug)
end
end
def to_slug(name)
self.transliterate.downcase.gsub(/[^a-z0-9 ]/, ' ').strip.gsub(/[ ]+/, '-')
end
end
I am assuming you have an index on name column in profiles table.
You can fire a query to database to get all entries like the currently generated slug and get the max value. If database does not return anything use current slug else parse the integer part of the max slug and increment it with 1 to get a new slug.
def generate_slug
slug = to_slug(self.name)
max_slug = Profile.where("slug like '#{slug}-%'").max.try(:slug)
self.slug = max_slug.present? ? slug : compute(slug, max_slug)
end
def compute(slug, max_slug)
max_count = max_slug.gsub("#{slug}-", "").to_i + 1
"#{slug}-#{max_count}"
end
*Untested code

Rails optimistic locking update within a loop appears to work until I check from outside of the loop

I'm using optimistic locking on a Rails model. Inside of a loop, I update and save this model (or, rather, many instances of this model).
From inside the loop, I output the "before" and "after" values, and the field appears to be updated correctly. But afterward, when I find the models by ID, I see that the field is not updated. Can anyone spot my error?
class Usage::LeadDistributionWeight < ActiveRecord::Base
attr_accessible :agent_id, :tag_value_id, :weight, :premium_limit, :countdown, :lock_version, :tag_value
def increment_countdown!
self.countdown = self.countdown + self.weight
save
rescue ActiveRecord::StaleObjectError
attempts_to_crement_countdown ||= 0
attempts_to_crement_countdown += 1
self.increment_countdown! unless attempts_to_crement_countdown > 5
false
end
def self.increment_countdowns parent_id, lead_type_id
if lead_type_id.present?
joins(:agent)
.where("#{reflect_on_association(:agent).table_name}.parent_id = ?", parent_id)
.where(tag_value_id:lead_type_id)
.all(readonly:false).each { |weight|
prev = weight.countdown
if weight.increment_countdown!
puts "#{prev} differs from #{weight.countdown}"
else
puts "no difference!"
end
}
end
end
end

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