Search code examples
ruby-on-railsruby-on-rails-4friendly-id

Friendly_id creating duplicate slugs for multiple objects in Rails


My Rails build contains no pathnames. The application consists of two main objects: collections and items. So if I have a link: https://foo.com/foo, this could potentially identify either a collection or an item. The difference is made clear within the context and UI flow throughout the application.

My question: is there a way to have the Friendly ID gem generate unique slugs by seeing if a slug has already been taken by another object? I understand you can generate candidates so that friendly ID will not duplicate a slug for a given object, but I need friendly ID to check both existing collection slugs and item slugs before generating a new slug.

I hope that didn't sound too confusing. To re-word more concisely: is there a method available for friendly ID to check for slugs in multiple objects before generating a new slug?


Solution

  • NOTE This is all untested, just working from docs and reading the source code.

    You could inherit the FriendlyId::SlugGenerator class, and override the available? method to check for the existing records in the opposing model:

    class CrossModelSlugGenerator << FriendlyId::SlugGenerator
    
        def available?(slug)
          if (@scope.class == "Item::ActiveRecord_Relation")
             # Search for collections with this slug and return false if they exist.
          elsif (@scope.class == "Collection::ActiveRecord_Relation")
             # Search for items with the this slug and return false if they exist.
          end
    
          # Otherwise do a normal slug check
          !@scope.exists_by_friendly_id?(slug)
        end
    
    end
    

    You can see the full code of the SlugGenerator class here:

    https://github.com/norman/friendly_id/blob/master/lib/friendly_id/slug_generator.rb

    Then you would have to tell the Friendly ID configuration to use that class instead. Create an initializer in config/intitializers/friendly_id.rb:

    FriendlyId.defaults do |config|
        config.slug_generator_class = "CrossModelSlugGenerator"
    end
    

    Try that out and see if it works out for you. Again, I haven't tested any of it, but it seems like it should work out.

    EDIT - You may need to wrap the class in the FriendlyId module like this:

    You might need an include somewhere, possibly in your class definition. Also, try wrapping the class into the FriendlyId module, so maybe something like this:

    include "friendly_id"
    
    module FriendlyId
      class CrossModelSlugGenerator << SlugGenerator
        ...
      end
    end
    

    With this change, you may also need to explicitly specify the module in the config class name:

    FriendlyId.defaults do |config|
        config.slug_generator_class = "FriendlyId::CrossModelSlugGenerator"
    end