Rails: Enseñame esas fotos.
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.


26 de Julio de 2006 a las 13:46
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
26 de Julio de 2006 a las 15:32
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).
26 de Julio de 2006 a las 17:29
¡Monta una encuesta para que propongamos nombres para la criatura!
26 de Julio de 2006 a las 22:03
¿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.
20 de Agosto de 2006 a las 03:59
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!!!!
31 de Agosto de 2006 a las 02:03
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.
31 de Agosto de 2006 a las 10:35
Espero que no corte y pegue, que nos quedamos los demás sin la receta
31 de Agosto de 2006 a las 14:37
Bueno… pues que “copie”
(Oye, en serio, ¿revisas todas las entradas por nuevos comentarios o qué?)
1 de Septiembre de 2006 a las 08:44
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ú
(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.
1 de Septiembre de 2006 a las 10:51
Para nada me molesta… a tu supervisor quizá, pero a mi no.
3 de Septiembre de 2006 a las 00:47
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…
3 de Septiembre de 2006 a las 13:26
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).
31 de Octubre de 2006 a las 16:20
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.
31 de Octubre de 2006 a las 21:39
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.
5 de Noviembre de 2006 a las 17:37
Tienes razón, pondré un enlace a esta aplicación y haber como me sale.Muchas gracias.
29 de Enero de 2008 a las 23:56
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