Rails: Enseñame esas fotos.

26
Julio
2006

En una aplicación web que tengo que programar decidí implementar, sin que explícitamente me lo pidieran (primera regla de la ingeniería del software: no hagas nada por lo que no te paguen o no te hayan pedido hacer), un pequeño sistema para subir fotos y poder mostrarlas en las páginas (un mini-flickr muy limitado, para entendernos).

El proceso no es complicado, pero la información está muy diseminada a lo largo de todo Internet (o yo no se utilizar Google correctamente) por lo que voy a intentar resumir la implementación que he conseguido juntar a base de pequeños trozos.

Yo he decidido guardar los datos de la fotografía en la base de datos, si se quieren almacenar directamente en el disco duro sería fácil utilizar mi código con las ideas explicadas en Basic File Uploads With Rails (que sea un artículo de ayer no es casualidad).

Lo primero es la migración de nuestro modelo (al que queramos asociarle las fotos):

class AddPhotoToModel < ActiveRecord::Migration
  def self.up
    add_column :model, :photo_data, :binary, :limit => 100.kilobytes # foto
    add_column :model, :photo_content_type, :string # tipo MIME de la fotografía
  end

  def self.down
    remove_column :model, :photo_data
    remove_column :model, :photo_content_type
  end
end

He puesto un límite de 100 kilobytes para cada fotografía simplemente porque no quería fotografías muy grandes y el campo del content-type es importante para luego poder enviar los datos de vuelta al navegador.

Sería ideal que en el modelo los campos photo_data y photo_content_type fueran parte de una agregación, pero no lo tengo implementado así y prefiero describir una versión que se que funciona.

Ahora tenemos que escribir un método en el modelo para facilitar el uso de la fotografía (partes de este código están descritas en el libro Agile Web Development with Rails, páginas 350 y siguientes de la primera edición).

class Model < ActiveRecord::Base
  validates_format_of :photo_content_type, :with => /^image/,
    :if => Proc.new { |model| not model.photo_content_type.blank?}

  def photo=(photo_field)
    self.photo_content_type = photo_field.content_type.chomp
    self.photo_data = photo_field.read
  end
end

Este pequeño método permite que en nuestras vistas podamos utilizar un campo de formulario llamado model[photo] (lo de model es un ejemplo) y que la foto se incluya automáticamente en la base de datos. El “validador” de más arriba simplemente sirve para que sólo nos suban imágenes y no su mp3 favorito (el if del “validador” está para el caso de que no exista fotografía, una opción que se permite en mi modelo).

Ahora toca el controlador que es el que se encargará de enviar la foto cuando se lo pidan o de guardarla en la base de datos, además de realizar un caching en el disco duro para no hacer sufrir a la pobre base de datos (esta vez el código viene de Caching images in Rails).

Primero el método que recupera la foto:

class ModelsController < ApplicationController
  caches_page :photo
  def photo
    model = Model.find(@params[:id])
    send_data model.photo_data,
      :type => model.photo_content_type,
      :disposition => ‘inline’
  end
end

La primera línea indica a Rails que debe “cachear” la acción photo, que es el método que viene a continuación. El método recupera los datos del modelo utilizando el id que traen los parámetros y utiliza el método de Rails send_data para enviar los datos de la fotografía. Son importantes los parámetros type y disposition ya que si no los navegadores no sabrían mostrar la imagen.

En el código para subir las fotos debo explicar un par de cosas. Mi idea era que en el formulario para crear o editar el modelo apareciera la posibilidad de subir una foto nueva, mostrando también la anterior si la hubiera. La implementación naïve me demostró que no puedes esperar que los navegadores no manden los “contenidos” de un campo vacío. A continuación muestro el código teniendo este hecho en cuenta (muestro sólo el create, el update sería similar):

class ModelsController < ApplicationController
  def create
    check_for_photo(params)
    @model = Model.new(params[:model])
    if @model.save
      redirect_to :action => ‘list’
    else
      render :action => ‘new’
    end
  end

protected
  def upload_photo(id)
    expire_page :controller => ‘models’,
      :action => ‘photo’,
      :id => id
  end

  def check_for_photo(params)
    # Si nos proporciona foto la descacheamos
    if params[:person][:photo].class == File
      upload_photo(params[:id])
    else # si no las proporcionan la eliminamos del hash
      params[:person].delete(:photo)
    end
  end
end

El método create es muy similar al método que crearía el scaffolding pero le hemos incluido la llamada al método check_for_photo antes de nada.

Este método comprueba que la clase del parámetro photo es File (en el caso de enviar el formulario sin haber elegido un archivo la clase del parámetro sería StringIO, algo que tendrá alguna razón, pero que me constó un montón adivinar y entender) y en esa caso llama a otro método upload_photo (que está fatal llamado porque no hace lo que dice, pero bueno, “razones históricas”). En el caso de que la clase no sea File el método elimina el parámetro photo del hash de parámetros para que no sea salvado al modelo (al no haber nada el guardado del modelo fallaría).

Finalmente el método upload_photo indica a Rails que debe considerar cualquier información cacheada de la foto inválida, lo que hará que la próxima vez que se pregunte por ella se regenere desde la base de datos.

El código asociado a las vistas es seguramente el más sencillo de todos. Este primer trozo de código se encargará de mostrar la fotografía del modelo y en el caso de no existir utilizará una fotografía “falsa” en su lugar (esto, por tonto que parezca, me empeñé en hacerlo desde el modelo y obviamente fue imposible, porque es parte de la vista, no del modelo).

<%= if not @model.photo_content_type.blank?
  image_tag(url_for(:controller => ‘model’, :action => ‘photo’, :id => @model))
else
  image_tag(’no-photo’)
end %>

Este otro trozo es el código que permite incluir un formulario con el cual subir las imágenes (el parámetro multipart es muy importante, sino nada funciona):

<%= start_form_tag({ :action => 'create' }, :multipart => true) %>
<%= file_field ‘model’, ‘photo’ %>
<%= submit_tag “Crear” %>
<%= end_form_tag %>

En este ejemplo no se observa muy bien que si no se envía una foto los demás datos sería guardados, aunque si no se envía una foto pero se envía el formulario no debería haber errores, al ser el envío de la foto opcional.

Bueno, creo que eso es más o menos todo. El siguiente paso que quiero realizar es utilizar RMagick para reducir a un tamaño común todas las fotografías que se envían o quizá implementar algo parecido a lo que tiene digg al subir el avatar que dejan que elijas la parte que quieres… eso sí que estaría bien.


16 comentarios a “Rails: Enseñame esas fotos.”

  1. Gravatar deigote dice:

    Luego me dices que yo escribo mucho en mi wiki… justo estos días quería hacer algo parecido para subir mis fotos, es posible que me venga bien esto. Así que posiblemente gracias x-D

  2. Gravatar Daniel dice:

    Ya bueno, es que tengo la sensación de que no hago nada útil y me gusta ver con que búsquedas llega la gente de Google. Si alguien encuentra esto y le sirve para cualquier cosa que esté haciendo, aunque yo no sepa que lo ha utilizado de alguna manera me sentiré feliz.

    Ya sabes, ese rollo de cultura libre, open source y “Creative Commies”… sueños de libertad y un mundo mejor (con mejor software y hardware grrrrr si se puede).

  3. Gravatar Gandalfj dice:

    ¡Monta una encuesta para que propongamos nombres para la criatura!

  4. Gravatar Daniel dice:

    ¿Criatura? Si te refieres a la aplicación ya tiene nombre porque es un encargo… no es pública aún, pero cuando lo sea quizá la publique por aquí (depende de lo orgulloso que me sienta de ella y cuantos client want this acabe aceptando).

    Por cierto, normalmente escojo el nombre de los proyectos bastante pronto en su línea de vida, creo que el nombre define bastante el espíritu de un proyecto.

  5. Gravatar Nood dice:

    Soy muy nuevo en todo esto de programación…por lo que pido si algn xfavor me lo puede juntar todo en un archivo o q’ lo compriman y q’ me qde slo para bajarlo….si son tan amables….GRACIAS!!!!

  6. Gravatar Daniel dice:

    No creo que se pueda juntar y comprimir ya que el artículo es una “receta” para que cada uno lo pueda aplicar a su problema específico, de cualquier forma siempre puedes cortar y pegar en lo archivos adecuados.

  7. Gravatar deigote dice:

    Espero que no corte y pegue, que nos quedamos los demás sin la receta ;-)

  8. Gravatar Daniel dice:

    Bueno… pues que “copie”

    (Oye, en serio, ¿revisas todas las entradas por nuevos comentarios o qué?)

  9. Gravatar deigote dice:

    Jajaja. Estoy suscrito a los comentarios. Lo siento Dani, eres mi blog más leido, y ahora en el trabajo tengo algunos ratos libres que llenar. Si quieres dejo de comentar tanto para que no parezca que soy tú :P (aunque cuando tú escribes en el mío ya lo dejas claro con tu nombre), pero es que tienes unos comentadores muy buenos y me quedo con las ganas.

  10. Gravatar Daniel dice:

    Para nada me molesta… a tu supervisor quizá, pero a mi no.

  11. Gravatar Nood dice:

    aish…lo q’ daría por tener esos códigos ya puestos! PERO NO ENTIENDO!!!!!!

    pero osea…seguros q’ no se puede comprimir? xq yo he bajado aplicaciones completas hechas de varias páginas!! hasta sitios enteros!! algn me podría ayudar???

    no se…tal vez yo les pueda ayudar en algo de vuelta…

  12. Gravatar Daniel dice:

    Primero: los códigos están ahí ya puestos.

    Segundo: si no entiendes (y no grites si no hay razón para hacerlo) quizá necesites leer un par de libros antes que esta página.

    Tercero: seguro que se puede comprimir, pero 60 líneas me parece suficientemente comprimido.

    Cuarto: si obviamos el segundo punto, comprenderás que esto no es una aplicación completa ni un sitio entero. Son unos consejos para que si alguien quiere realizar algo parecido en su propia aplicación tenga unos primeros pasos en los que fijarse.

    Quinto: escribe bien.

    Sexto: Por que soy un pedazo de pan aquí tienes tu archivo comprimido (que me hagas trabajar a mí por algo que deberías haber hecho tú debería comerte la conciencia).

  13. Gravatar jose dice:

    Buenas,soy jose ,y soy novato en esto, hize un curso básico de paginas web porque me gusta este mundo, aunque es dificilisimo y ya soy mayorcito para esto, pero bueno, hay estoy intentando hacer una web dedicada al mundo de la bicicleta.
    En ella tenia pensado que me pudiesen enviar fotos de carreras o de entrenamiento de ciclismo y subirlas, me gusto mucho la idea de la web http://www.sosmtb.com que comparte las fotos,y se pueden ver en un apartado.
    ¿ como se hace esto? creo que es lo que tu publicas y nos quieres enseñar. Pero ¿como lo hago?Que carpetas tengo que crear en mi index. Donde pongo las fotos?
    Ya se copio tu código ..¿y que mAs hago?
    Si me puedes ayudar te lo agradeceria, y si soy demasiado novato y no me puedes ayudarme lo agradezco de todas maneras. Gracias

    Nota del editor: Enlace arreglado.

  14. Gravatar Daniel dice:

    Lo que explico en el artículo está un poco más allá de un “curso básico de páginas web” y requiere algunos conocimientos de programación y administración de servidores.

    Lo mejor para no complicarse la vida es utilizar una aplicación web ya hecha, como por ejemplo Gallery, que permite crear álbumnes de fotos y compartirlos con otros usuarios. Pon un enlace a tu instalación de Gallery desde tus páginas y ya tienes un sistema para compartir fotografías.

  15. Gravatar jose dice:

    Tienes razón, pondré un enlace a esta aplicación y haber como me sale.Muchas gracias.

  16. Gravatar elrailero dice:

    hola, tengo una duda y espero que me puedas responderla.a lo mejor es un poco tonta pero bueno, A que haces referencia con :person en el método
    def check_for_photo(params)
    # Si nos proporciona foto la descacheamos
    if params[:person][:photo].class == File
    upload_photo(params[:id])
    else # si no las proporcionan la eliminamos del hash
    params[:person].delete(:photo)
    end
    Muchas gracias

Deja un comentario

Puedes enterarte de las respuestas a tus comentarios de esta entrada mediante myComments.

XHTML: Puedes utilizar las siguientes etiquetas: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Tu servidor sin límites: 20GB de espacio, 1TB de transferencia, 1 dominio gratuito. Por 1.5€ al mes utilizando el código "RUIDOBLANCO" en DreamHost. Más información.