Given that my slug candidate is my title, and it is already used, the slug will return something like: my-title-49c9938b-ece5-4175-a4a4-0bb2b0f26a27
Is it possible to have friendly_id return a smaller hash? Like: my-title-705d62eea60a
In that case you can create your own method, which you can pass then to friendly_id. There you can define what are going to be the combinations that FriendlyId will use in order to assign a unique identifier to your record as a slug.
For example:
friendly_id :column_candidates, use: :slugged
def column_candidates
[
:name,
[:name, :another_column],
[...more columns combinated as a fallback]
]
end
If FriendlyId can't create a unique record (by slug) after every combination of columns in column_candidates is evaluated, then it's going to append a UUID anyway.
You're free to add the objects you want to column_candidates, being strings, procs, lambdas or symbols. Also the method name doesn't have to be exactly that, you can modify it as needed.
As a last resource, and if a unique identifier can't be created, you can rely on creating your own short and always able to not be unique hash using Digest::SHA1:
...
[-> { Digest::SHA1.hexdigest(name).chars.sample(6).join }]
Related
I am building a Rails 5 team management app which lets users manage organizations and users. I would like to be able to change from using the :id in the path (e.g: /organizations/43) and use an alpha-numeric slug instead (e.g: /organizations/H6Y47Nr7). Similar to how Trello do this (i.e: https://trello.com/b/M9X71pE6/board-name). Is there an easy way of doing this?
I have seen the FriendlyId gem which could take care of the slugging in the path but what would be the best way to generate the slug in the first place?
Ideally, for the most bang for buck the slug would include A-Z, a-z and 0-9 (as I understand it, this is Base58?) and for the sake of not blowing out the url too much, 8 characters at the most. If my calculations are correct, this gives 218 trillion combinations, which should be plenty.
Am I on the right track? Any help would be much appreciated.
Thanks
To create a slug, easiest way is to use SecureRandom. You can add something like the following in your model
before_create :generate_slug
private
def generate_slug
begin
self.slug = SecureRandom.urlsafe_base64(8)
end while Organization.exists?(slug: slug)
end
One small caveat here with respect to what you want is that the slug will sometimes contain an underscore or a dash but that should be fine.
irb(main):014:0> SecureRandom.urlsafe_base64(8)
=> "HlHHV_6rN3k"
irb(main):015:0> SecureRandom.urlsafe_base64(8)
=> "naRqT-NmYDU"
irb(main):016:0> SecureRandom.urlsafe_base64(8)
=> "9h04l4jEEsM"
If you go that route, I would create a table were you save the slugs that you are generating and don't delete them even when you delete an organization. When you create a new organization query this model to make sure there are no duplicates slugs. Also add a unique index in the slug column of the organizations table.
You should not give up the id column with integers so in the show method you will need to do:
org = Organization.where(slug: params[:id]).first
On Rails 4.2, I would like to use Friendly Id for routing to a specific model, but dont wan't to create a slug column on the model table. I would instead prefer to use an accessor method on the model and dynamically generate the slug. Is this possible? I couldn't find this in the documentation.
You can not do this directly using friendly id because of the way it uses the slug to query the database (relevant source).
However it is not difficult to accomplish what you want. What you will need are two methods:
Model#slug method which will give you slug for a specific model
Model.find_by_slug method which will generate the relevant query for a specific slug.
Now in your controllers you can use Model.find_by_slug to get the relevant model from the path params. However implementing this method might be tricky especially if your Model#slug uses a non-reversible slugging implementation like Slugify because it simply get rids of unrecognized characters in text and normalizes multiple things to same character (eg. _ and - to -)
You can use URI::Escape.encode and URI::Escape.decode but you will end up with somewhat ugly slugs.
As discussed here I went with the following approach for custom routing based on dynamic slug.
I want custom route like this: /foos/the-title-parameterized-1 (where "1" is the id of the Foo object).
Foo model:
#...
attr_accessor :slug
#dynamically generate a slug, the Rails `parameterize`
#handles transforming of ugly url characters such as
#unicode or spaces:
def slug
"#{self.title.parameterize[0..200]}-#{self.id}"
end
def to_param
slug
end
#...
routes.rb:
get 'foos/:slug' => 'foos#show', :as => 'foo'
foos_controller.rb:
def show
#foo = Foo.find params[:slug].split("-").last.to_i
end
In my show view, I am able to use the default url helper method foo_path.
Okay so I'm working on making a friendly_id that allows me to scale it effectively. This is what I got so far. What it does is essentially add +1 on a count if you have a matching name. So if you are the 2nd person to register as John doe your url would be /john-doe-1.
extend FriendlyId
friendly_id :name_and_maybe_count, use: [:slugged, :history]
def name_and_maybe_count
number = User.where(name: name).count
return name if number == 0
return "#{name}-#{number}"
end
However this piece of code is still bringing me some issues.
If I have created a user called John Doe. Then register another user called John Doe the slug will be /john-doe-UUID. If I register a third user then it will receive the slug john-doe-1.
If I have two users. One that registered with the name first. Say Juan Pablo. Then he changes his name to 'Rodrigo', and then change it back to 'Juan Pablo'. His new slug for his original name will be 'juan-pablo-1-UUID'.
I know this is minor nitpick for most of you but it's something that I need to fix!
You want to overwrite to_param, include the id as the first bit of the friendly id
I extend active record like this in one of my apps,
module ActiveRecord
class Base
def self.uses_slug(attrib = :name)
define_method(:to_param) do
"#{self.id}-#{self.send(attrib).parameterize}"
end
end
end
so I can do
uses_slug :name
in any model
but this alone in any model should work:
def to_param
"#{self.id}-#{self.my_attrib.parameterize}"
end
The benefit of this is that in the controller you when you do
Model.find(params[:id])
you don't have to change anything because when rails tries to convert a string like "11-my-cool-friendly-id" it will just take the first numeric bits and leave the rest.
The context is pretty simple, I have a Course model that extends from FriendlyId as follow:
extend FriendlyId
friendly_id :friendly_name, use: [:slugged, :history]
def friendly_name
slugs = [self.type_name, self.name]
slugs << self.city.name if self.city
slugs << self.structure.name if self.structure
return slugs
end
And if I create a course with same type, name, city and structure I get the following error:
!! #<ActiveRecord::RecordNotUnique: PG::Error: ERROR: duplicate key value
violates unique constraint "index_courses_on_slug"
DETAIL: Key (slug)=(cours-sevillanas-copie-paris-12-la-trianera) already exists.
I don't understand why FriendlyId doesn't add a sequence number at the end of the slug...
Any suggestion is welcomed.
I have tried to return a string instead of an array in the friendly_name method but the error persists.
Edit
Removing :history fixes the problem.
I'have also tried other branches (4.0-stable, 4.1.x) of FriendlyId but it doesn't fixes the problem.
I had the same problem with the :history features: it's because FriendlyId will use a separate table to store slugs, and will not check the existing slug column.
You can create a migration and re-save the whole table to generate missing slugs in the new slugs table.
For example:
def up
MyModel.all.map(&:save)
end
I'm trying to use Friendly ID to create more custom urls. However, the column I want to use for a slug is still a number. I don't want to create a separate column just for the slug, since it would be identical.
I have a Lot that belongs to an Auction.
Auctions have many Lots (items for sale).
A Lot has a lot_number, which is unique to an Auction. It's not unique throughout the entire table, though. It's basically just a way to order the lots in each auction.
My URLs look like: /auctions/1/lots/21 (/auctions/:auction_id/lots/:id)
I want them to be: /auctions/1/lots/1 (/auctions/:auction_id/lots/:lot_number)
I added the following to lot.rb:
extend FriendlyId
friendly_id :lot_number
It almost works. It shows me a Lot with the correct lot number, but the wrong auction.
I read about scopes on the Friendly ID docs, which sounded perfect. I could scope the lot against the auction...
So I tried:
extend FriendlyId
friendly_id :lot_number, :use => :scoped, :scope => :auction_id
Now I see the following error:
SQLite3::SQLException: no such column: lots.slug: SELECT "lots".* FROM "lots" WHERE "lots"."slug" = '1' LIMIT 1
Why is the SQL WHERE lots.slug = 1? Shouldn't it be WHERE lots.auction_id = 1?
Am I using the wrong syntax? Not sure where I've gone wrong.
Any help would be appreciated. Thanks.
i think that you should have a look at from_param and to_param methods that are used in rails to lookup models: http://apidock.com/rails/ActiveRecord/Base/to_param