I am using friendly_id gem for slugging my models. Since the slug has to be unique when i enter the same data to check i get a long hashed appending in the slug.
Explore explore
Explore explore-7a8411ac-5af5-41a3-ab08-d32387679f2b
Is there a way to tell friendly_id to give better formatted slugs like explore-1
and explore-2
Version: friendly_id 5.0.4
Agreed, it seems like pretty rough behavior.
If you look at code of friendly_id/slugged.rb
, there are 2 functions handing conflicts resolution logic:
def resolve_friendly_id_conflict(candidates)
candidates.first + friendly_id_config.sequence_separator + SecureRandom.uuid
end
# Sets the slug.
def set_slug(normalized_slug = nil)
if should_generate_new_friendly_id?
candidates = FriendlyId::Candidates.new(self, normalized_slug || send(friendly_id_config.base))
slug = slug_generator.generate(candidates) || resolve_friendly_id_conflict(candidates)
send "#{friendly_id_config.slug_column}=", slug
end
end
So, the idea is just to monkey patch it. I see 2 options:
Just patch
resolve_friendly_id_conflict
, add your random suffix.Change logic of both methods with intention of trying all candidates until
slug_generator.generate(candidates)
returns something not empty. If all candidates givenil
then fallback toresolve_friendly_id_conflict
method. Using this technique you can use slug candidates to append model'sid
when slug is not unique.
Ideally, it would be nice if gem's authors added a config option to handle unique slugs resolution (method symbol or proc taking generator and candidates as params) or just check if model responds to some method.
Besides, in some use cases unique slugs resolution in not needed at all. For example, if we just want to rely on validates_uniqueness_of :slug
or uniqueness validation of candidates.
So if anyone comes across this at some point, I have an update I would have preferred to put as a comment in tirdadc's comment, but I can't (not enough reputation). So, here you go:
Tirdadc's answer is perfect, in theory, but unfortunately the id of an object isn't yet assigned at the point slug_candidates is called, so you need to do a little trickery. Here's the full way to get a slug with the id of the object in there:
class YourModel < ActiveRecord::Base
extend FriendlyId
friendly_id :slug_candidates, use: :slugged
after_create :remake_slug
# Try building a slug based on the following fields in
# increasing order of specificity.
def slug_candidates
[
:name,
[:name, :id],
]
end
def remake_slug
self.update_attribute(:slug, nil)
self.save!
end
#You don't necessarily need this bit, but I have it in there anyways
def should_generate_new_friendly_id?
new_record? || self.slug.nil?
end
end
So you're basically setting the slug after the creation of the object and then after the object is done being created, you nil out the slug and perform a save, which will reassign the slug (now with the id intact). Is saving an object in an after_create call dangerous? Probably, but it seems to work for me.
I would recommend using the :scoped
module if you want to avoid UUIDs in your slugs when dealing with collisions. Here's the documentation along with an example:
http://norman.github.io/friendly_id/file.Guide.html#Unique_Slugs_by_Scope
Try using :scope => :id
since each id will be unique anyway and see if that works for you.
UPDATE:
To get exactly what you want, you now have candidates
for that purpose in version 5:
class YourModel < 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, :id],
]
end
end
Today I came accross this issue and while other answer helped me get started, I wasn't satisfied because, like you, I wanted to have the slugs appear in sequence like explore
, explore-2
, explore-3
.
So, here's how I fixed it:
class Thing < ActiveRecord::Base
extend FriendlyId
friendly_id :slug_candidates, use: :slugged
validates :name, presence: true, uniqueness: { case_sensitive: false }
validates :slug, uniqueness: true
def slug_candidates
[:name, [:name, :id_for_slug]]
end
def id_for_slug
generated_slug = normalize_friendly_id(name)
things = self.class.where('slug REGEXP :pattern', pattern: "#{generated_slug}(-[0-9]+)?$")
things = things.where.not(id: id) unless new_record?
things.count + 1
end
def should_generate_new_friendly_id?
name_changed? || super
end
end
I used uniqueness validation for :slug
just in case this model is being used in concurrent code.
Here you can see it working:
irb(main):001:0> Thing.create(name: 'New thing')
(0.1ms) begin transaction
(0.2ms) SELECT COUNT(*) FROM "things" WHERE (slug REGEXP 'new-thing(-[0-9]+)?$')
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE LOWER("things"."name") = LOWER('New thing') LIMIT 1
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE "things"."slug" = 'new-thing' LIMIT 1
SQL (0.4ms) INSERT INTO "things" ("name", "slug") VALUES (?, ?) [["name", "New thing"], ["slug", "new-thing"]]
(115.7ms) commit transaction
=> #<Thing id: 1, name: "New thing", slug: "new-thing">
irb(main):002:0> Thing.create(name: 'New thing')
(0.2ms) begin transaction
(0.9ms) SELECT COUNT(*) FROM "things" WHERE (slug REGEXP 'new-thing(-[0-9]+)?$')
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing-2"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE LOWER("things"."name") = LOWER('New thing') LIMIT 1
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE "things"."slug" = 'new-thing-2' LIMIT 1
(0.1ms) rollback transaction
=> #<Thing id: nil, name: "New thing", slug: "new-thing-2">
irb(main):003:0> Thing.create(name: 'New-thing')
(0.2ms) begin transaction
(0.5ms) SELECT COUNT(*) FROM "things" WHERE (slug REGEXP 'new-thing(-[0-9]+)?$')
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing-2"]]
Thing Exists (0.3ms) SELECT 1 AS one FROM "things" WHERE LOWER("things"."name") = LOWER('New-thing') LIMIT 1
Thing Exists (0.3ms) SELECT 1 AS one FROM "things" WHERE "things"."slug" = 'new-thing-2' LIMIT 1
SQL (0.4ms) INSERT INTO "things" ("name", "slug") VALUES (?, ?) [["name", "New-thing"], ["slug", "new-thing-2"]]
(108.9ms) commit transaction
=> #<Thing id: 2, name: "New-thing", slug: "new-thing-2">
irb(main):004:0> Thing.create(name: 'New!thing')
(0.2ms) begin transaction
(0.6ms) SELECT COUNT(*) FROM "things" WHERE (slug REGEXP 'new-thing(-[0-9]+)?$')
Thing Exists (0.0ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE ("things"."id" IS NOT NULL) AND "things"."slug" = ? LIMIT 1 [["slug", "new-thing-3"]]
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE LOWER("things"."name") = LOWER('New!thing') LIMIT 1
Thing Exists (0.1ms) SELECT 1 AS one FROM "things" WHERE "things"."slug" = 'new-thing-3' LIMIT 1
SQL (0.1ms) INSERT INTO "things" ("name", "slug") VALUES (?, ?) [["name", "New!thing"], ["slug", "new-thing-3"]]
(112.4ms) commit transaction
=> #<Thing id: 3, name: "New!thing", slug: "new-thing-3">
irb(main):005:0>
Also, if you use sqlite3 adapter you'll need to install sqlite3_ar_regexp
gem (it won't be very fast, because SQLite doesn't have REGEXP() and it evaluates Ruby code instead).
来源:https://stackoverflow.com/questions/24306391/rails4-friendly-id-unique-slug-formatting