I am using the friendly_id gem. In the portfolio.rb I placed these two lines:
extend FriendlyId
friendly_id :title, use: :slugged
As you can see I am also using the slug option. When I create a project with title "example" it works find and I can find the project under mysite.com/projects/example. Now, if I create a second one with the same title I get a title for it like this one: mysite.com/projects/example-74b6c506-5c61-41a3-8b77-a261e3fab5d3. I don't really like this title. I was hoping for a friendlier title like example-2.
At this question, RSB (user) told me that its friendly_id that causes that. I was wondering if there is a way to create a more friendly. At first I thought of "manually" checking if the same title exists (in a while loop) and assigning another title using either example-2 or example-3 or... example-N.
However do I need to do something like that or am I missing something? Is there an easier way to do something like that?
Check the documentation for the latest version of friendly_id:
A new "candidates" functionality which makes it easy to set up a list of alternate slugs that can be used to uniquely distinguish records, rather than appending a sequence.
Example straight from the docs:
class Restaurant < ActiveRecord::Base
extend FriendlyId
friendly_id :slug_candidates, use: :slugged
# Try building a slug based on the following fields in
# increasing order of specificity.
def slug_candidates
[
:name,
[:name, :city],
[:name, :street, :city],
[:name, :street_number, :street, :city]
]
end
end
UUID
The problem you're alluding to is the way in which friendly-id appends a hash (they call it a UUID) to duplicate entries:
Now that candidates have been added, FriendlyId no longer uses a
numeric sequence to differentiate conflicting slug, but rather a UUID
(e.g. something like 2bc08962-b3dd-4f29-b2e6-244710c86106). This makes
the codebase simpler and more reliable when running concurrently, at
the expense of uglier ids being generated when there are conflicts.
I don't understand why they have done this, as it goes against the mantra of friendly ID, but nonetheless, you have to appreciate that's how it works. And whilst I don't think the slug_candidates method above will prove any more successful, I do think that you'll be able to use something like a custom method to define the slug you wish
--
You'll want to read this documentation (very informative)
It says there are two ways to determine the "slug" your records are assigned, either by using a custom method, or by overriding the normalize_friendly_id method. Here's my interpretation of both of these for you:
Custom Method
#app/models/project.rb
Class Project < ActiveRecord::Base
extend FriendlyID
friendly_id :custom_name, use: :slugged
def custom_name
name = self.count "name = #{name}"
count = (name > 0) ? "-" + name : nil
"#{name}#{count}"
end
end
Normalize_Friendly_ID
#app/models/project.rb
Class Project < ActiveRecord::Base
extend FriendlyID
friendly_id :name, use: :slugged
def normalize_friendly_id
count = self.count "name = #{name}"
super + "-" + count if name > 0
end
end
Related
I am very new to friendly_id and it matches my need to provide friendly URLs 👍
I have a Group model (i.e. a group of users) for which I generate a unique code upon creation. FYI, this code attribute is set as a unique index in my PostgreSQL database.
class Group < ApplicationRecord
include FriendlyId
friendly_id :code
after_create :generate_group_code
private
def normalize_friendly_id(value)
super.upcase
end
def generate_group_code(size = 8)
allowed_chars = ("a".."z").to_a
code = (1..size).map { allowed_chars[rand(26)] }.join while Group.exists?(code: code)
update(code: code)
end
end
I think I have followed the gem's guide properly, I just want the generated code to be upcased in the URLs (i.e. /group/ABCDEFGH).
The friendly_id is indeed set as my code attribute, but it is not upcased. I placed a byebug in the normalize_friendly_id method but it is never triggered. What am I missing?
The normalize_friendly_id is only called when using the slugged module to use a slug column to store and find by this column:
friendly_id :code, use: :slugged
Using this you can then override the normalize_friendly_id method.
Sunny's way is probably the way to go in general, as the slugged module is required to edit internal methods such as normalize_friendly_id.
In my case, I already have a code attribute that is unique. Using the slugged module would create a new attribute called slug, which would be exactly the same as my code. I want to avoid that duplication.
In the end, I decided to dodge the friendly_id gem and directly override the to_param method the my model (inspired by this gist):
class Group < ApplicationRecord
validates :code, format: { with: /\A[a-z]{8}\z/ }, uniqueness: true, on: :update
after_create :generate_group_code
# Override the method to allow '/group/MYGROUPA' paths
def to_param
code.upcase
end
# A group code is made of letters exclusively.
# Converting a String (or nil) to an integer leads to 0.
# In this case, lookup by code, otherwise, lookup by id.
# Note the 'downcase', which allows URLs to be case insensitive.
def self.find(input)
input.to_i == 0 ? find_by_code!(input.downcase) : super
end
private
def generate_group_code(size = 8)
allowed_chars = ("a".."z").to_a
code = (1..size).map { allowed_chars[rand(26)] }.join while Group.exists?(code: code)
update(code: code)
end
end
I'll edit this answer if I encounter any side effect, but for now it works.
I have Job model and Location model in my rails application. I am using postgresql as a database.so i have location_ids as an array field in my Job model for holding locations. I am using FeriendlyId in my application to make my url friendly. when i go to my job show page i am getting this friendly url
http://localhost:3000/jobs/seo-trainee
but now i also want to include the locations the job has in my url , something like this
http://localhost:3000/jobs/seo-trainee-mumbai-tokyo
i know we can use slug_candidates for this purpose. but i dont know how can i achieve this exactly
currently i have this in my Job model
extend FriendlyId
friendly_id :slug_candidates, use: [:slugged, :finders]
def slug_candidates
[
:title,
[:title, :id]
]
end
You need to define a custom method to generate your slug definition, and then tell FriendlyId to use that method.
The documentation gives this example:
class Person < ActiveRecord::Base
friendly_id :name_and_location
def name_and_location
"#{name} from #{location}"
end
end
bob = Person.create! :name => "Bob Smith", :location => "New York City"
bob.friendly_id #=> "bob-smith-from-new-york-city"
So in your case, you would use something like this:
class SomeClass
friendly_id :job_name_and_location
def job_name_and_location
"#{name} #{locations.map(&:name).join(' ')}"
end
end
I've made a few assumptions:
Your job model has a name attribute (seo training)
Your job model has_many locations, each of which have a name attribute
We then create a method which defines the non-friendly string which FriendlyId will use to create a slug from. In this case it'll come up with something like SEO Training Mumbai Tokyo and use that to create your seo-training-mumbai-tokyo slug.
You can use something like the following:
extend FriendlyId
friendly_id :slug_candidates, use: [:slugged, :finders]
def slug_candidates
locs = Location.where("id IN(?)", self.location_ids).collect{|l| l.name}.join("-") // here, we find the locations for the current job, then joins the each locations name with a '-' sign
return self.title+"-"+locs // here, returns the job title with the location names
end
So, if your current Job holds location_ids = [1,2,3]
then from Location table we find the locations with id = 1,2,3. Then join their names.
I'm building an app that uses the globalize gem. I'm trying to add the Friendly ID gem for permalinks.
My Product table doesn't actually have a name column. The name column is stored in Product::Translation, so I can't use the friendly_id :name, use: :slugged set up that they recommend in the github page.
I want to set the permalink to the name attribute of the english translation. This is what I have so far:
before_save :update_slug
def update_slug
english_translation = translations.find { |t| t.locale == :en }
self.slug = english_translation.name if english_translation
end
However, this doesn't convert the english_translation.name variable to a parameter friendly format. If I have the name as something sdfsdfs sdfsf, the slug will be the same and that would break things since the path would be /products/something sdfsdfs sdfsf with all the spaces.
Is there a method I can run to convert a string into slug format? Something like:
before_save :update_slug
def update_slug
english_translation = translations.find { |t| t.locale == :en }
self.slug = slugify(english_translation.name)
end
I need a method that I can run manually with a parameter to generate the slug.
Friendly_ID
As Greg Burgardt said, your goal is to get the value into friendly_id, allowing it to build your slug.
Your method is to try and manually create the slug attribute - you'll be much better taking the value you derive from the translation and passing it into friendly_id. To do this, here is what I'd do:
#app/models/product.rb
class Product < ActiveRecord::Base
extend Friendly_ID
friendly_id :name, use: :slugged
def name
english_translation = translations.find { |t| t.locale == :en }
english_translation.name
end
end
Friendly_ID builds your slug, meaning you don't want to create the slug yourself:
Since UUIDs are ugly, FriendlyId provides a "slug candidates"
functionality to let you specify alternate slugs to use in the event
the one you want to use is already taken. For example:
class Restaurant < ActiveRecord::Base
extend FriendlyId
friendly_id :slug_candidates, use: :slugged
# Try building a slug based on the following fields in
# increasing order of specificity.
def slug_candidates
[
:name,
[:name, :city],
[:name, :street, :city],
[:name, :street_number, :street, :city]
]
end
end
r1 = Restaurant.create! name: 'Plaza Diner', city: 'New Paltz'
r2 = Restaurant.create! name: 'Plaza Diner', city: 'Kingston'
r1.friendly_id #=> 'plaza-diner'
r2.friendly_id #=> 'plaza-diner-kingston'
To use candidates, make your FriendlyId base method return an array.
The method need not be named slug_candidates; it can be anything you
want. The array may contain any combination of symbols, strings, procs
or lambdas and will be evaluated lazily and in order. If you include
symbols, FriendlyId will invoke a method on your model class with the
same name. Strings will be interpreted literally. Procs and lambdas
will be called and their return values used as the basis of the
friendly id. If none of the candidates can generate a unique slug,
then FriendlyId will append a UUID to the first candidate as a last
resort.
I guess for the sake of posterity:
class Product < ActiveRecord::Base
friendly_id :default_slug, use: :slugged
def default_slug
english_translation = translations.find { |t| t.locale == :en }
english_translation.try(:name)
end
end
I've run into this situation many times, where I need to store something like a status persay, so it would be something like this:
class Order < ActiveRecord::Base
INCOMPLETE = 'Incomplete'
IN_PROGRESS = 'In progress'
SHIPPED = 'Shipped'
CANCELLED = 'Cancelled'
...
end
Order would have a status attribute, and when creating an order I would just use collection_select with [INCOMPLETE, IN_PROGRESS, SHIPPED, CANCELLED] as the options.
Is there a cleaner way of doing this without using hardcoded strings, like using a Status association or some sort of PORO? I feel like this would be a bit brittle, like if someone changed the INCOMPLETE = 'Incomplete' to INCOMPLETE = 'Incompletezzzzz' then all the statuses of my existing records would not match.
I would advise you to use another model in this situation, something like OrderStatus:
class OrderStatus < ActiveRecord::Base
validates :internal_reference, presence: true, uniqueness: true
has_many :orders
def translated(locale = :en)
I18n.t("activerecord.attributes.order_status.#{self.internal_reference}", locale: locale, default: self.internal_reference)
end
class Order < ActiveRecord::Base
belongs_to :order_status
validates :order_status_id, presence: true
And the records will look like this:
OrderStatus.first
# => OrderStatus id: 1, internal_reference: canceled
Order.first
# => Order id: 3, order_status_id: 1 # etc.
In your views, it could be:
order.order_status.translated(I18n.locale)
# looks for activerecord.attributes.order_status.canceled
# if nothing found, returns the internal_reference, here it would return `'canceled'`
This configuration is better than just constants :
You can create as many statuses as you want,
You can translate them directly (using internal_reference as key for I18n),
The statuses can be tested either if they are in english, french or whatever (thanks to internal_reference,
You can create statuses directly in your app, without rebooting it,
You can set an attribute, like status_code and makes ranges (kind of like HTTP requests statuses) and group them (ex: if status_code > 100)
You can also add an boolean attribute cant_be_deleted to prevent from deleting Statuses used in the code.
You might think it is overkill to do so, but I guarantee that the day you will want to translate / add / remove / change your Statuses, it will be much easier with Models rather than Constants. Trust me, I worked for an Online shop, handling Carts, Orders and Products, I know how painful it is to change from constants to models, everywhere in your already existing code ;-)
I think enumerize gem can help
You can write your code like this
class Order < ActiveRecord:Base
extend Enumerize
enumerize :status, in: [:incomplete, :in_progress, :shipped, :cancelled]
end
It also works perfectly with I18n.
https://github.com/brainspec/enumerize
Trying to get my app running the FriendlyId gem (version 4.0.1)
I think I'm doing this in the wrong order, but I want to strip out apostrophes before my friendly_id slug is generated when creating a new record. But I think the method normalize_friend_id is being called after the id is already generated.
I've added the following to my model:
class Team < ActiveRecord::Base
extend FriendlyId
friendly_id :name, :use => :slugged
def normalize_friendly_id(string)
super.gsub("\'", "")
end
end
super calls the superclass first, meaning the friendly id is being generated then you're running gsub on THAT result. What you really want is to override this method completely.
Refer to: https://github.com/norman/friendly_id/blob/master/lib/friendly_id/slugged.rb#L244-246
your code should look like this:
def normalize_friendly_id(string)
string.to_s.gsub("\'", "").parameterize
end
or
def normalize_friendly_id(string)
super(string.to_s.gsub("\'", ""))
end
Hope that helps