Plugin Rails: sluggable_finder

Visita este artí­culo en http://www.estadobeta.com/2007/07/27/plugin-rails-sluggable_finder/

Por Ismael en Desarrollo, Proyectos, Ruby & Rails, artículos

Plugin Ruby on Rails para obtener URL’s bonitas para tus modelos.

Sluggable_finder, el segundo plugin oficial de este servidor, es posiblemente más útil que el primero.

Actualización 19 de Marzo 2008

Bastante gente parece estar teniendo problemas con la carga del plugin en Rails 2+.

Aunque aún no descubro la causa específica (yo lo uso en Rails 2.0.2 junto a varios otros plugins), en algunos casos el problema desaparece al definir el orden de carga del plugin (gracias Fernan2!).

Code (ruby)
  1.  
  2. # environment.rb
  3. config.plugins = [:sluggable_finder, :all]
  4.  

Además, he actualizado el plugin (revisión 17 19) para corregir un problema de doble carga usando ActiveRecord en procesos asincrónicos (usando procesos Ruby separados como Reliable Message o Backgroundrb). Aunque no creo que el problema esté relacionado, les pido que actualicen sus copias y me hagan llegar sus observaciones. La forma de uso es la misma por lo que sus aplicaciones no debieran verse afectadas.

Además he incluido el parámetro opcional :reserved_slugs, al que puedes pasar un array de permalinks reservadas que no quieres disponibles para tus usuarios.

Code (ruby)
  1.  
  2. sluggable_finder :title, :reserved_slugs => [‘admin’, ’settings’, ‘users’]
  3.  

El problema

¿La idea? En la mayoría de las aplicaciones que he hecho en Rails, necesito URL’s “bonitas”, ojalá conteniendo el nombre del objeto que quiero recuperar de la base de datos (como las URL’s de este blog). En Rails, por defecto, puedes usar la ID de un objeto en la URL, y el método ActiveRecord::Base#find para encontrar ese objeto único.

Code (ruby)
  1.  
  2. # url: /posts/20
  3. @post = Post.find( params[:id] )
  4.  

Las ID’s en una base de datos debieran ser únicas; Rails aprovecha esto y por convención emite una excepción ActiveRecord::RecordNotFound si el objeto con esa ID no existe. Esto es útil porque podemos usar esa particularidad del método find para normalizar nuestro manejo de errores.

Pero usar ID’s en las URL’s de una aplicación en producción es mala práctica. No sólo se presta para abusos de los datos (es fácil obtener todos los datos si las ID’s son consecutivas) sino que es poco descriptivo y agrega poco y nada de valor para la SEO. Lo ideal es usar nombres completos codificados para URL’s o, como se los conoce informalmente, “slugs”.

El plugin

sluggable_finder hace posible, en una línea, agregar esta funcionalidad a tus modelos. Sigue leyendo para saber más…

Code (ruby)
  1. class Post < ActiveRecord::Base
  2.   sluggable_finder :title
  3. end

Asumiendo que nuestros Posts tienen un campo “title”, sluggable_finder normalizará el contenido de este campo y lo guardará en un campo “slug”, que debe existir en la tabla. Un post de título “Este es mi primer Post!” obtiene un slug “este-es-mi-primer-post”. Con un poco de trabajo en tus rutas puedes usar este campo como prefieras.

El slug se crea la primera vez. En las actualizaciones subsiguientes del título el slug NO CAMBIA, a menos que lo vacíes explícitamente con algo como @post.slug = nil. Esto, por que las URL’s no debieran cambiar en el tiempo.

Cada slug es único: si creas otro Post con el título “Este es mi primer Post!”, este segundo slug será “este-es-mi-primer-post-2″, automáticamente.

Configuración

El plugin acepta parámetros opcionales. Si quieres guardar los slugs en un campo llamado “permalink” en lugar de “slug”:

Code (ruby)
  1.  
  2. sluggable_finder :title, :to => :permalink
  3.  

También puedes “slugear” campos virtuales o compuestos:

Code (ruby)
  1. class Post < ActiveRecord::Base
  2.   belongs_to :author
  3.   sluggable_finder :get_slug
  4.  
  5.   def get_slug
  6.     "#{author.name} #{title}"
  7.   end
  8. end

Además, el plugin acepta un parámetro :scope, por si quieres que tus slugs sean únicos con respecto a otro objeto.

Code (ruby)
  1. class Post < ActiveRecord::Base
  2.   belongs_to :category
  3.   sluggable_finder :title, :scope => :category_id
  4. end

Pero… ¡Este plugin ya existe!

Cierto, están acts_as_slugable, acts_as_permalink y acts_as_urlnameable, pero éstos no trabajan correctamente con caracteres acentuados (como la é o la ñ), no modifican el slug si es que éste ya existe y otro detalle que explico a continuación. Yo quería un plugin sencillo que hiciera todo lo que necesito.

find_sluggable find

Necesitaba “slugs” que funcionaran de la misma forma que ActiveRecord::Base#find. Al buscar un objeto por su slug, el plugin me entrega un objeto único o emite la excepción ActiveRecord::RecordNotFound si es que éste no existe. Para esto, el plugin reescribe en los modelos el método find, que puedes usar en tus controladores de la manera usual.

Code (ruby)
  1. class PostsController < ApplicationController
  2.   def show
  3.     @post = Post.find( params[:post_slug] ) #emite ActiveRecord::RecordNotFound si no existe
  4.   end
  5. end

Si el primer parámetro es de tipo String, find busca en el campo “slug”, o cualquiera que hayas definido*. Ideal para combinarlo con rescue_action_in_public para manejar tus errores en el controlador de aplicación.

Si el primer parámetro es un símbolo o número, ActiveRecord usa el find tradicional. Esto es poderoso pero tiene un pequeño inconveniente: si en tu aplicación tenías controladores donde aún quieres usar las IDs en find directamente desde la URL:

Code (ruby)
  1.  
  2. @post = Post.find( params[:id] )
  3.  

sluggable_finder tratará de encontrar un “slug” similar a la ID y no encontrará nada. Esto es porque las ID de la URL vienen como simples Strings en el array params.

Esto sólo ocurrirá en los modelos que declaren sluggable_finder y la solución es forzar el parámetro ID al tipo Integer en los controladores.

Code (ruby)
  1.  
  2. @post = Post.find( params[:id].to_i )
  3.  

No es lo más bonito del mundo pero es necesario hasta que encuentre una manera confiable de distinguir entre un número, un String representando a un número y un String normal (¿a alguien se le ocurre?).

Más info en el README del plugin (en inglés).

Instalación

Code (ruby)
  1. ./script/plugin install  http://code.estadobeta.com/plugins/sluggable_finder

* Igual que find, puedes usar find_sluggable en el contexto de una colección: ciudad = @producto.ciudades.find_sluggable('santiago')

21 comentarios para “Plugin Rails: sluggable_finder”

  1. GravatarNicolás Orellana, Entre viajes y Orelworks! » Blog Archive » Creando plugins en Rails Dice:

    […] No olviden que tu problema no sólo lo tienes tú. Isamel desarrollo un plugin notable, que justamente era un problema que yo habia tenido… y habia sido bastante poco Railero para […]

  2. Gravatarmiguel Dice:

    Hola se ve muy bien el plugin,
    mi consulta es que tengo una tabla que tiene muchos registros ya, y queria implementar el plugin, pero no se como actualizar los registros para que en cada registro se guarde el slug del campo titulo ¿es posible hacer eso?

    Saludos Cordiales

  3. GravatarIsmael Dice:

    Miguel, claro que se puede. Una vez que hayas creado el campo “slug” en tu tabla, metete a la consola de Rails;

    .script/console

    ..Y ahi mismo cargas todos los registros y los vas salvando para que se actualicen. El plugin crea los slugs si es que estos aun no existe.

    MiObjeto.find(:all).each{|o| o.save}

    Si tienes muchos registros esto puede tomar tiempo.

  4. Gravatarmiguel Dice:

    gracias Ismael por la respuesta, el plugin funciona a la perfección :)

    Saludos

  5. GravatarBallenato Dice:

    Hola, tengo un problemilla al utilizar tu plugin.

    No consigo que sustituya correctamente los caracteres no ascii, en lugar de eso lo que hace es eliminarlos directamente. El caso es que, probándolo desde la consola, el método to_slug funciona perfectamente, pero no es así cuando lo ejecuto desde el navegador.

    Pienso que puede ser problema de la codificación, pero tengo los ficheros de texto en UTF-8, la base de datos también, y he añadido la linea “encoding: utf8″ en database.yml.

    ¿Se te ocurre alguna solución? Muchas gracias de antemano.

  6. GravatarIsmael Dice:

    Lo voy a revisar.
    Si encuentras la causa por favor me avisas.
    Gracias por el bug report!

    Que navegador/OS estas usando?

    Con que palabras probaste?

  7. GravatarBallenato Dice:

    Uso Firefox 2 sobre Ubuntu Gutsy. Versión de Rails 1.2.3.

    Me pasa con todos los caracteres no ASCII, por ejemplo las vocales acentuadas y la ‘ñ’.

    Si descubro algo más no dudes que te avisaré.

  8. Gravatarcarakan Dice:

    Hola Ismael, Ademas de los problemas de Ballenato, (mi codificación de caracteres para la tabla es latin1_spanish_ci), al momento de actualizar un campo me produce un error cuando el id de la tabla es diferente de id, mi id de tabla es idArticulo.

  9. GravatarIsmael Dice:

    Mmm… Lo de la ID no debiera estar relacionado a mi plugin… Si tu tabla tiene una primary key distinta a “id”, debes declaralo en tu modelo, con set_primary_key ‘idArticulo’

  10. Gravatarcarakan Dice:

    Hola Isamael si ya realice eso de set_primare_key, pero me lanza un error por que la consulta que genera al momento de actualizar la genera con un campo id ¿? muy raro.

  11. GravatarIsmael Dice:

    Carakan… No entiendo mucho el problema. Estas seguro de que lo de la ID esta relacionado con mi plugin?

    En unos dias voy a transformar el plugin en una Gema (para poderlo usar en Merb y otras aplicaciones ademas de Rails). Ahi aprovechare de revisar esos casos.

  12. GravatarFernan2 Dice:

    Tengo una aplicación en Rails 2.0.2, con un modelo Subtipo sluggable (va de maravilla!!) y un modelo Producto, que Belongs_to Subtipo (vía subtipo_id), que no es sluggable (al menos de momento).

    Y me falla al hacer una nueva migración según el estilo Rails 2 (ver http://railscasts.com/episodes/83), con un error “undefined method `sluggable_finder’”:

    script/generate migration add_plancompleto_to_productos plancompleto:boolean

    /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:1532:in `method_missing’: undefined method `sluggable_finder’ for # (NoMethodError)
    from /Users/fj2c/verema/trunk/app/models/subtipo.rb:3
    from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:203:in `load_without_new_constant_marking’
    from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:203:in `load_file’
    from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:342:in `new_constants_in’
    from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:202:in `load_file’
    from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:94:in `require_or_load’
    from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:248:in `load_missing_constant’
    from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:453:in `const_missing’
    … 34 levels…
    from /opt/local/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/commands/generate.rb:1
    from /opt/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require’
    from /opt/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require’
    from script/generate:3

    ¿Os pasa lo mismo?

  13. GravatarFernan2 Dice:

    Por cierto, el tema no es grave: quito el sluggable_finder de Subtipo, hago la migración y vuelvo a poner el sluggable_finder, y arreglado.

    Y por cierto, la línea es:
    sluggable_finder :nombre_corto, :to => :url_subtipo

    s2

  14. GravatarIsmael Dice:

    Que raro. Yo lo uso en este momento en Rails 2.0.2 sin problemas con las migraciones. De todas maneras voy a hacer un par de pruebas.

    Por supuesto si encuentras la causa del problema te ruego me informes.

    Muchas gracias por el bug report.

  15. GravatarFernan2 Dice:

    El problema es peor de lo que pensaba, porque también afecta al reinicio de mongrel: no deja arrancar con slugabble_finder (y obviamente, sin sluggable_finder arranca pero no va bien).

    En cuanto sepa algo, lo diré; pero el problema no es, o no es sólo, del sluggable_finder, porque llevo utilizándolo más de una semana y ha empezado a hacer esto ahora… raroraroraro…

  16. GravatarFernan2 Dice:

    parece ser que influye el orden de carga de los plugins, se ha arreglado poniendo en environment.rb la línea

    config.plugins = [:sluggable_finder, :all]

    dentro del bloque

    Rails::Initializer.run do |config|

    s2

  17. GravatarEstadoBeta » Archivo » Sluggable Finder rev. 17 Dice:

    […] Más información y comentarios en el artículo original. […]

  18. GravatarIsmael Dice:

    @Ballenato:

    sluggable_finder usa la librería Iconv para conversión de caracteres. Iconv es parte de la Standard Library de Ruby, pero depende de las herramientas iconv que tengas instaladas en tu plataforma. Primero trataría de verificar que esas funcionan correctamente en tu versión de Linux / Ruby.

  19. GravatarEstadoBeta » Archivo » Sluggable Finder rev. 20 Dice:

    […] … Y hay más mejoras para mi plugin sluggable_finder. […]

  20. GravatarJaime Iniesta Dice:

    Genial el plugin, lo he estado probando y funciona muy bien (estoy en Rails 2).

    Lo usaré en mis próximos proyectos.

  21. GravatarMis plugins habituales (II) — Ceritium.net Dice:

    […] Sluggable-finder: Con este plugin te será mas sencillo aún crear permalinks basados en texto y no en el id, para usar este plugin solo tendrás que añadir una linea más en el modelo que necesites y crear un campo más en la tabla a la que se refiera. […]

Deja un comentario

XHTML: puedes usar estas etiquetas: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>