Las cargas de archivos son generalmente un área difícil en el desarrollo web. En este tutorial, aprenderemos a usar Dragonfly, una poderosa gema de Ruby que hace que sea fácil y eficiente agregar cualquier tipo de funcionalidad de carga a un proyecto de Rails..
Nuestra aplicación de muestra mostrará una lista de usuarios, y para cada uno de ellos, podremos cargar un avatar y tenerlo almacenado. Además, Dragonfly nos permitirá:
En esta lección, seguiremos un enfoque de BDD [Behavior Driven Development], utilizando Cucumber y RSpec.
Necesitará tener instalado Imagemagick: puede consultar esta página para ver los binarios para instalar. Como estoy basado en una plataforma Mac, use Homebrew, simplemente puedo escribir brew install imagemagick
.
También necesitarás clonar una aplicación básica de Rails que usaremos como punto de partida..
Comenzaremos clonando el repositorio inicial y configurando nuestras dependencias:
git clone http: //[email protected]/cloud8421/tutorial_dragonfly_template.git cd tutorial_dragonfly_template
Esta aplicación requiere al menos Ruby 1.9.2 para ejecutarse, sin embargo, te animo a que uses 1.9.3. La versión de Rails es 3.2.1. El proyecto no incluye un .rvmrc
o un .rbenv
expediente.
A continuación, corremos:
paquete de instalación paquete exec rake db: configuración db: prueba: preparar db: semilla
Esto se encargará de las dependencias de gemas y la configuración de la base de datos (utilizaremos sqlite, por lo que no hay que preocuparse por la configuración de la base de datos).
Para probar que todo funciona como se espera, podemos ejecutar:
paquete exec rspec paquete exec pepino
Debes encontrar que todas las pruebas han pasado. Repasemos la salida de Pepino:
Característica: administrar el perfil de usuario Como usuario Para administrar mis datos, quiero acceder a mi página de perfil de usuario. Antecedentes: Dado que un usuario existe con el correo electrónico "[email protected]" Escenario: ver mi perfil Dado que estoy en la página de inicio Cuándo Sigo "Perfil" para "[email protected]" Luego debería estar en la página de perfil para "[email protected]" Escenario: editar mi perfil Dado que estoy en la página de perfil para "[email protected]" Cuando Sigo "Editar" Y cambio mi correo electrónico con "[email protected]" Y hago clic en "Guardar". Debería estar en la página de perfil de "[email protected]" Y debería ver "Escenarios actualizados por el usuario" 2 escenarios (2 pasados) 11 pasos (11 pasados) 0m0.710s
Como puede ver, estas características describen un flujo de trabajo de usuario típico: abrimos una página de usuario de una lista, presionamos "Editar" para editar los datos del usuario, cambiamos el correo electrónico y guardamos.
Ahora, intenta ejecutar la aplicación:
rieles
Si abres http :: // localhost: 3000
en el navegador, encontrará una lista de usuarios (completamos previamente la base de datos con 40 registros aleatorios gracias a la gema Faker).
Por ahora, cada uno de los usuarios tendrá un pequeño avatar de 16x16px y un gran avatar de marcador de posición en su página de perfil. Si edita al usuario, podrá cambiar sus detalles (nombre, apellido y contraseña), pero si intenta cargar un avatar, no se guardará..
Siéntase libre de navegar en la base de código: la aplicación utiliza Simple Form para generar vistas de formulario y Twitter Bootstrap para CSS y diseño, ya que se integran perfectamente y ayudan mucho a acelerar el proceso de creación de prototipos..
Comenzaremos agregando un nuevo escenario a características / administrador_perfil.featura
:
... Escenario: agregar un avatar Dado que estoy en la página de perfil para "[email protected]" Cuando sigo "Editar" y subo el avatar de bigote Y hago clic en "Guardar". Luego debería estar en la página de perfil para "correo electrónico @ example.com "Y el perfil debe mostrar" el avatar del bigote "
Esta característica es bastante autoexplicativa, pero requiere algunos pasos adicionales para agregar a características / step_definitions / user_steps.rb
:
... Cuando / ^ Subo el avatar del bigote $ / do attach_file 'user [avatar_image]', Rails.root + 'spec / fixtures / mustache_avatar.jpg' end Luego / ^ el perfil debería mostrar "([^"] *) " $ / do | imagen | patrón = imagen de caso cuando 'el avatar del bigote' / mustache_avatar / end n = Nokogiri :: HTML (page.body) n.xpath (".// img [@ class = 'thumbnail']") .first ['src']. should = ~ end pattern
Este paso asume que tienes una imagen, llamada mustache_avatar.jpg
dentro especificaciones / accesorios
. Como puedes imaginar, esto es solo un ejemplo; puede ser lo que quieras.
El primer paso utiliza Capibara para encontrar el usuario [avatar_image]
campo de archivo y subir el archivo. Tenga en cuenta que estamos asumiendo que tendremos una avatar_imagen
atributo en el Usuario
modelo.
El segundo paso utiliza Nokogiri (una potente biblioteca de análisis HTML / XML) y XPath para analizar el contenido de la página de perfil resultante y buscar los primeros img
etiqueta con un miniatura
clase y prueba de que la src
atributo contiene bigote_avatar
.
Si tu corres Pepino
ahora, este escenario generará un error, ya que no hay ningún campo de archivo con el nombre que especificamos. Ahora es el momento de centrarse en el Usuario
modelo.
Antes de integrar Dragonfly con el Usuario
modelo, vamos a añadir un par de especificaciones a usuario_spec.rb
.
Podemos añadir un nuevo bloque justo después de la atributos
contexto:
contexto "atributos de avatar" hágalo debería responder_a (: avatar_imagen) esto debería permitir_mass_assignment_of (: avatar_image) fin
Probamos que el usuario tenga un avatar_imagen
atributo y, como vamos a actualizar este atributo a través de un formulario, debe ser accesible (segunda especificación).
Ahora podemos instalar Dragonfly: al hacer esto, conseguiremos que estas especificaciones se vuelvan ecológicas.
Agreguemos las siguientes líneas al Gemfile:
gema 'rack-cache', requiere: 'rack / cache' gema 'dragonfly', '~> 0.9.10'
A continuación, podemos correr instalación de paquete
. Rack-cache es necesario para el desarrollo, ya que es la opción más simple para tener almacenamiento en caché HTTP. También se puede utilizar en la producción, incluso si las soluciones más robustas (como Varnish o Squid) serían mejores.
También necesitamos agregar el inicializador Dragonfly. Vamos a crear el config / initializers / dragonfly.rb
archivo y añadir lo siguiente:
requiere la aplicación 'dragonfly' = Dragonfly [: images] app.configure_with (: imagemagick) app.configure_with (: rails) app.define_macro (ActiveRecord :: Base,: image_accessor)
Esta es la configuración de Dragonfly de vainilla: configura una aplicación Dragonfly y la configura con el módulo necesario. También agrega una nueva macro a ActiveRecord
que podremos utilizar para ampliar nuestra Usuario
modelo.
Necesitamos actualizar config / application.rb
, y añada una nueva directiva a la configuración (justo antes de la config.generators
bloquear):
config.middleware.insert 0, 'Rack :: Cache', verbose: true, metastore: URI.encode ("file: # Rails.root / tmp / dragonfly / cache / meta"), entitystore: URI.encode ("file: # Rails.root / tmp / dragonfly / cache / body") a menos que Rails.env.production? config.middleware.insert_after 'Rack :: Cache', 'Dragonfly :: Middleware',: images
Sin entrar en mucho detalle, estamos configurando Rack :: Caché
(excepto para producción, donde está habilitado de forma predeterminada) y configurar Dragonfly para usarlo.
Almacenaremos nuestras imágenes en el disco, sin embargo, necesitamos una forma de rastrear la asociación con un usuario. La opción más sencilla es agregar dos columnas a la tabla de usuarios con una migración:
rieles g migración add_avatar_to_users avatar_image_uid: cadena avatar_image_name: cadena paquete exec rake db: migrate db: test: prepare
Una vez más, esto es directamente de la documentación de Dragonfly: necesitamos tener un avatar_image_uid
columna para identificar de forma única el archivo avatar y una avatar_image_name
para almacenar su nombre de archivo original (la última columna no es estrictamente necesaria, pero permite la generación de urls de imágenes que terminan con el nombre de archivo original).
Finalmente, podemos actualizar el Usuario
modelo:
Usuario de clase < ActiveRecord::Base image_accessor :avatar_image attr_accessible :email, :first_name, :last_name, :avatar_image…
los image_accessor
El inicializador de Dragonfly pone a disposición el método y solo requiere un nombre de atributo. También hacemos el mismo atributo accesible en la línea de abajo..
Corriendo rspec
Ahora debería mostrar todas las especificaciones verde.
Para probar la función de carga, podemos agregar un contexto a users_controller_spec.rb
en el Actualización PUT
bloquear:
context "avatar image" do let! (: image_file) fixture_file_upload ('/ mustache_avatar.jpg', 'image / jpg') context "cargando un avatar" do antes de poner: update, id: user.id, user: avatar_image: image_file end it "debería guardar efectivamente el registro de la imagen en el usuario" do user.reload user.avatar_image_name.should = ~ / mustache_avatar / end end end end
Reutilizaremos el mismo dispositivo y crearemos un simulacro para la carga con fixture_file_upload
.
Como esta funcionalidad se aprovecha de Dragonfly, no necesitamos escribir código para que pase..
Ahora tenemos que actualizar nuestras vistas para mostrar el avatar. Vamos a empezar desde la página de presentación del usuario y abrir app / views / users / show.html.erb
y actualízalo con el siguiente contenido:
<% if @user.avatar_image.present? %> <%= image_tag @user.avatar_image.url, class: 'thumbnail' %> <% else %> <% end %>
<%= @user.name %>
<%= @user.email %>
<%= link_to 'Edit', edit_user_path(@user), class: "btn" %>
A continuación, podemos actualizar app / views / users / edit.html.erb
:
<%= simple_form_for @user, multipart: true do |f| %><% end %><% if @user.avatar_image.present? %> <%= image_tag @user.avatar_image.url, class: 'thumbnail' %> <% else %> <% end %><%= f.input :avatar_image, as: :file %><%= f.input :first_name %> <%= f.input :last_name %> <%= f.input :email %><%= f.submit 'Save', class: "btn btn-primary" %>
Podemos mostrar el avatar de usuario con una simple llamada a @ user.avatar_image.url
. Esto devolverá un url a una versión no modificada del avatar cargado por el usuario.
Si tu corres Pepino
Ahora, verás la característica verde. Siéntete libre de probarlo en el navegador también!
Estamos confiando implícitamente en CSS para cambiar el tamaño de la imagen si es demasiado grande para su contenedor. Es un enfoque inestable: nuestro usuario podría cargar avatares no cuadrados o una imagen muy pequeña. Además, siempre estamos sirviendo la misma imagen, sin preocuparnos demasiado por el tamaño de la página o el ancho de banda.
Necesitamos trabajar en dos áreas diferentes: agregar algunas reglas de validación a la carga del avatar y especificar el tamaño y la proporción de la imagen con Dragonfly.
Comenzaremos por abrir el usuario_spec.rb
archivo y añadiendo un nuevo bloque de especificaciones:
contexto "atributos de avatar" do% w (avatar_imagen retained_avatar_image remove_avatar_image) .each do | attr | it debe responder_to (attr.to_sym) final% w (avatar_imagen retained_avatar_image remove_avatar_image) .each do | attr | it debe permitir_mass_assignment_of (attr.to_sym) end it it "debería validar el tamaño del archivo del avatar" do user.avatar_image = Rails.root + 'spec / fixtures / huge_size_avatar.jpg' user.should_not be_valid # size is> 100 KB end it "debería validar el formato del avatar" do user.avatar_image = Rails.root + 'spec / fixtures / dummy.txt' user.should_not be_valid end end
Estamos probando la presencia y estamos permitiendo la "asignación masiva" de atributos adicionales que utilizaremos para mejorar el formulario de usuario (: retained_avatar_image
y : remove_avatar_image
).
Además, también estamos probando que nuestro modelo de usuario no acepte grandes subidas (más de 200 KB) y archivos que no sean imágenes. Para ambos casos, debemos agregar dos archivos de aparatos (una imagen con el nombre especificado y cuyo tamaño es más de 200 KB y un archivo de texto con cualquier contenido).
Como de costumbre, ejecutar estas especificaciones no nos pondrá verdes. Vamos a actualizar el modelo de usuario para agregar esas reglas de validación:
... attr_accessible: email,: first_name,: last_name, avatar_image, retained_avatar_image,: remove_avatar_image ... validates_size_of: avatar_image, maximum: 100.kilobytes validates_property: format, of: avatar_image, in: [: jpeg,> : jpg] validates_property: mime_type, of:: avatar_image, en: ['image / jpg', 'image / jpeg', 'image / png', 'image / gif'], case_sensitive: false
Estas reglas son bastante efectivas: tenga en cuenta que, además de verificar el formato, también verificamos el tipo mime para una mejor seguridad. Al ser una imagen, permitimos archivos jpg, png y gif..
Nuestras especificaciones deberían pasar ahora, por lo que es hora de actualizar las vistas para optimizar la carga de la imagen.
De forma predeterminada, Dragonfly utiliza ImageMagick para procesar dinámicamente las imágenes cuando se solicita. Suponiendo que tenemos un usuario
instancia en uno de nuestros puntos de vista, entonces podríamos:
user.avatar_image.thumb ('100x100'). url user.avatar_image.process (: greyscale) .url
Estos métodos crearán una versión procesada de esta imagen con un hash único y, gracias a nuestra capa de almacenamiento en caché, se llamará a ImageMagick solo una vez por imagen. Después de eso, la imagen será servida directamente desde el caché..
Puedes usar muchos métodos incorporados o simplemente construir los tuyos, la documentación de Dragonfly tiene muchos ejemplos..
Revisemos nuestro usuario editar
página y actualizar el código de vista:
... <% if @user.avatar_image.present? %> <%= image_tag @user.avatar_image.thumb('400x400#').url, class: 'thumbnail' %> <% else %>...
Haremos lo mismo para el usuario. espectáculo
página:
... <% if @user.avatar_image.present? %> <%= image_tag @user.avatar_image.thumb('400x400#').url, class: 'thumbnail' %> <% else %>...
Estamos forzando el tamaño de la imagen a 400 x 400 píxeles. los #
El parámetro también le indica a ImageMagick que recorte la imagen manteniendo una gravedad central. Puede ver que tenemos el mismo código en dos lugares, por lo que vamos a reformular esto en un parcial llamado views / users / _avatar_image.html.erb
<% if @user.avatar_image.present? %> <%= image_tag @user.avatar_image.thumb('400x400#').url, class: 'thumbnail' %> <% else %> <% end %>
Entonces podemos reemplazar el contenido de la .miniatura
Contenedor con una simple llamada a:
<%= render 'avatar_image' %>
Podemos hacerlo aún mejor moviendo el argumento de pulgar
Fuera de lo parcial. Vamos a actualizar _avatar_image.html.erb
:
<% if user.avatar_image.present? %> <%= image_tag user.avatar_image.thumb(args).url %> <% else %> & text = Super + cool + avatar "alt =" Super cool avatar "> <% end %>
Ahora podemos llamar a nuestro parcial con dos argumentos: uno para el aspecto deseado y otro para el usuario:
<%= render 'avatar_image', args: '400x400#', user: @user %>
Podemos usar el fragmento de arriba en editar
y espectáculo
vistas, mientras que podemos llamarlo de la siguiente manera dentro de views / users / _user_table.html.erb
, donde estamos mostrando las miniaturas pequeñas.
...<%= link_to 'Profile', user_path(user) %> <%= render 'avatar_image', args: '16x16#', user: user %> <%= user.first_name %> ...
En ambos casos, también realizamos una expresión regular en el aspecto para extraer una cadena compatible con el servicio placehold.it (es decir, eliminar caracteres no alfanuméricos).
Dragonfly crea dos atributos adicionales que podemos usar en un formulario:
retenido_avatar_imagen
: esto almacena la imagen cargada entre recargas. Si las validaciones para otro campo de formulario (por ejemplo, correo electrónico) fallan y la página se vuelve a cargar, la imagen cargada todavía está disponible sin necesidad de volver a cargarla. Lo utilizaremos directamente en el formulario..remove_avatar_image
: cuando sea cierto, la imagen del avatar actual se eliminará tanto del registro del usuario como del disco.Podemos probar la eliminación de avatar agregando una especificación adicional a users_controller_spec.rb
, en el imagen de avatar
bloquear:
… El contexto "eliminando un avatar" se debe hacer antes de user.avatar_image = Rails.root + 'spec / fixtures / mustache_avatar.jpg' user.save end it "debería eliminar el avatar del usuario" do put: update, id: user. id, usuario: remove_avatar_image: "1" user.reload user.avatar_image_name.should be_nil end end ...
Una vez más, Dragonfly conseguirá que esta especificación pase automáticamente ya que ya tenemos el remove_avatar_image
atributo disponible para la instancia de usuario.
Añadamos otra característica a gestión_perfil.featura
:
Escenario: eliminar un avatar Dado al usuario con el correo electrónico "[email protected]" tiene el avatar de bigote Y estoy en la página de perfil para "[email protected]" Cuando sigo "Editar" Y reviso "Eliminar imagen de avatar" Y hago clic en "Guardar". Luego debería estar en la página de perfil para "[email protected]" Y el perfil debería mostrar "el avatar del marcador de posición"
Como de costumbre, necesitamos agregar algunos pasos a user_steps.rb
y actualiza uno para agregar un Regex para el avatar de marcador de posición:
Dado / ^ el usuario con el correo electrónico "([^"] *) "tiene el avatar bigote $ / do | email | u = User.find_by_email (email) u.avatar_image = Rails.root + 'spec / fixtures / mustache_avatar.jpg 'u.save end When / ^ Reviso "([^"] *) "$ / do | checkbox | check check end end Entonces / ^ el perfil debe mostrar "([^"] *) "$ / do | image | pattern = case image cuando 'el avatar del marcador de posición' /placehold.it/ cuando 'el avatar del bigote' / mustache_avatar / end n = Nokogiri :: HTML (page.body) n.xpath (".// img [@ class = 'thumbnail']"). primero ['src']. should = ~ end pattern
También necesitamos agregar dos campos adicionales a la editar
formar:
...<%= f.input :retained_avatar_image, as: :hidden %> <%= f.input :avatar_image, as: :file, label: false %> <%= f.input :remove_avatar_image, as: :boolean %>...
Esto hará que nuestra función pase.
Para evitar tener una característica grande y demasiado detallada, podemos probar la misma funcionalidad en una especificación de solicitud.
Vamos a crear un nuevo archivo llamado especificación / solicitudes / user_flow_spec.rb
y añádele este contenido:
requiera 'spec_helper' describa "Flujo de usuario" do let! (: user) Factory (: user, email: "[email protected]") describe "viendo el perfil" do it "debería mostrar el perfil para el usuario" visita '/' page.find ('tr', text: user.email) .click_link ("Profile") current_path = URI.parse (current_url) .path current_path.should == user_path (usuario) final fin describe "actualización los datos del perfil "hazlo" deberían guardar los cambios "do visit '/' page.find ('tr', text: user.email) .click_link (" Profile ") click_link 'Edit' fill_in: email, con:" new_email @ example.com "click_button 'Save' current_path.should == user_path (user) page.should have_content" User updated "end end describe" administrando el avatar "hazlo" debería guardar el avatar cargado "do user.avatar_image = Rails.root + 'spec / fixtures / mustache_avatar.jpg' user.save visit user_path (usuario) click_link 'Editar' attach_file 'user [avatar_image]', Rails.root + 'spec / fixtures / mustache_avatar.jpg' click_button 'Guardar' current_path.should == user_path (usuario) page.should have_con carpa "User updated" n = Nokogiri :: HTML (page.body) n.xpath (".// img [@ class = 'thumbnail']"). first ['src']. should = ~ / mustache_avatar / end "debe eliminar el avatar si se solicita" do user.avatar_image = Rails.root + 'spec / fixtures / mustache_avatar.jpg' user.save visite user_path (usuario) click_link 'Editar' marque "Eliminar imagen de avatar" click_button 'Save' current_path .should == user_path (user) page.should have_content "User updated" n = Nokogiri :: HTML (page.body) n.xpath (".// img [@ class = 'thumbnail']"). first [' src ']. should = ~ /placehold.it/ end end end
La especificación encapsula todos los pasos que utilizamos para definir nuestra característica principal. Prueba exhaustivamente el marcado y el flujo, para que podamos asegurarnos de que todo funcione correctamente a un nivel granular.
Ahora podemos acortar el gestión_perfil.featura
:
Característica: administrar el perfil de usuario Como usuario Para administrar mis datos quiero acceder a mi página de perfil de usuario. Antecedentes: Dado que un usuario existe con el correo electrónico "[email protected]" Escenario: editar mi perfil Dado que cambio el correo electrónico con "new_mail @ example.com "para" [email protected] "Luego debería ver" Usuario actualizado "Escenario: agregar un avatar Dado que cargué el avatar de bigote para" [email protected] "Luego el perfil debería mostrar" el avatar de bigote " Escenario: eliminar un avatar Dado que el usuario "[email protected]" tiene el avatar de bigote y lo quito, entonces el usuario "[email protected]" debe tener "el avatar de marcador de posición"
Actualizado user_steps.rb
:
Dado que ^ ^ existe un usuario con el correo electrónico "([^"] *) "$ / do | email | Factory (: user, email: email) final Dado / ^ el usuario con el correo electrónico" ([^ "] *)" tiene el bigote avatar $ / do | email | u = User.find_by_email (email) u.avatar_image = Rails.root + 'spec / fixtures / mustache_avatar.jpg' u.save end Then / ^ Debería ver "([^"] *) "$ / do | content | page.should have_content (content) end Entonces / ^ el perfil debería mostrar "([^"] *) "$ / do | image | n = Nokogiri :: HTML (page.body) n.xpath (".// img [@ class = 'thumbnail']"). first ['src']. should = ~ pattern_for (image) end Dado / ^ I cambie el correo electrónico con "([^"] *) "para" ([^ "] *)" $ / do | new_email, old_email | u = User.find_by_email (old_email) visita edit_user_path (u) fill_in: email, con: new_email click_button 'Save' end Dado / ^ Subo el avatar del bigote para "([^"] *) "$ / do | email | u = User.find_by_email (email) visita edit_user_path (u) attach_file 'user [avatar_image]', Rails.root + 'spec / fixtures / mustache_avatar.jpg' click_button 'Save' end Dado / ^ el usuario "([^"] * ) "tiene el avatar del bigote y lo quito $ / do | email | u = Usuario.find_by_email (correo electrónico) u.avatar_image = Rails.root + 'spec / fixtures / mustache_avatar.jpg' u. guarda edit_user_path (u) marca "Eliminar imagen de avatar" click_button 'Save' end Entonces / ^ el usuario " ([^ "] *)" debería tener "([^"] *) "$ / do | email, imagen | u = User.find_by_email (email) visita user_path (u) n = Nokogiri :: HTML (page.body) n.xpath (".// img [@ class = 'thumbnail']"). first ['src'] .should = ~ pattern_for (image) end def pattern_for (image_name) caso image_name cuando 'el avatar del marcador de posición' /placehold.it/ cuando 'el avatar del bigote' / mustache_avatar / end end
Como último paso, podemos agregar fácilmente el soporte de S3 para almacenar los archivos de avatar. Vamos a reabrir config / initializers / dragonfly.rb
y actualizar el bloque de configuración:
Dragonfly :: App [: images] .configure do | c | c.datastore = Dragonfly :: DataStorage :: S3DataStore.new c.datastore.configure do | d | d.bucket_name = 'dragonfly_tutorial' d.access_key_id = 'some_access_key_id' d.secret_access_key = 'some_secret_access_key' end end end menos que% (prueba de desarrollo pepino) .include? Rails.env
Esto funcionará de manera inmediata y solo afectará la producción (o cualquier otro entorno que no esté especificado en el archivo). Dragonfly usará de forma predeterminada el almacenamiento del sistema de archivos para todos los demás casos.
Espero que hayas encontrado este tutorial interesante y que hayas podido recopilar algunos datos interesantes..
Lo invito a consultar la página de Dragonfly GitHub para obtener documentación extensa y otros ejemplos de casos de uso, incluso fuera de una aplicación de Rails..