¡Hola! ¡Muy buenas a todos! La semana pasada os presenté la extensión Spree It is a present que creé para Be Bellón y os prometí que esta semana os contaría cómo se hacen estas extensiones. Como lo prometido es deuda, y tomando como ejemplo esta extensión, se avecina un post técnico 😉
Crear una extensión para Spree Commerce
Para crear la extensión Spree It is a present yo me basé en la guía que tiene Spree (en inglés) para crear extensiones. Lógicamente esta guía, aunque es muy completa y está muy bien redactada, no puede cubrir todos los supuestos, por lo que para las cosas que no venían en la guía oficial eché un vistazo a otras extensiones de Spree ya existentes. Y para que tú lo tengas todo en un único sitio te describo aquí el proceso que yo seguí. ¡Vamos allá!
1. Instala Spree Commerce
Lo primero que tienes que hacer es asegurarte de que tienes instalada la gema spree para poder utilizar su comando de creación de extensiones. Presuponiendo que tienes ruby instalado, en un terminal ejecuta:
gem install spree
Este comando ya debería de encargarse de instalar spree y todas sus dependencias.
2. Crea el esqueleto de tu extensión con el generador
La gema de spree incluye un generador de extensiones que te deja prácticamente todo listo para que puedas empezar, de hecho te crea la estructura de la extensión, te incluye una licencia open-source (BSD 3), que básicamente permite todo uso de tu software eximiéndote de responsabilidades en caso de que el mismo cause algún perjuicio, y añade un README muy decente, ¡sólo te harán falta unos retoques en el Gemspec y a programar!
Para utilizar el generador simplemente ejecuta el siguiente comando un un terminal indicando el nombre que quieres darle a la extensión. El caso de spree_it_is a present el comando fue:
spree extension it_is_a_present
El resultado debería de ser algo similar a esto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
create spree_it_is_a_present create spree_it_is_a_present/app create spree_it_is_a_present/app/.gitkeep create spree_it_is_a_present/app/controllers/.gitkeep create spree_it_is_a_present/app/models/spree_it_is_a_present/configuration.rb create spree_it_is_a_present/app/models/.gitkeep create spree_it_is_a_present/app/services/.gitkeep create spree_it_is_a_present/app/views/.gitkeep create spree_it_is_a_present/lib create spree_it_is_a_present/lib/spree_it_is_a_present.rb create spree_it_is_a_present/lib/spree_it_is_a_present/engine.rb create spree_it_is_a_present/lib/spree_it_is_a_present/factories.rb create spree_it_is_a_present/lib/spree_it_is_a_present/version.rb create spree_it_is_a_present/lib/generators/spree_it_is_a_present/install/install_generator.rb create spree_it_is_a_present/bin create spree_it_is_a_present/bin/rails create spree_it_is_a_present/spec create spree_it_is_a_present/spec/.rubocop.yml create spree_it_is_a_present/spec/spec_helper.rb create spree_it_is_a_present/gemfiles create spree_it_is_a_present/gemfiles/spree_4_2.gemfile create spree_it_is_a_present/gemfiles/spree_master.gemfile chmod spree_it_is_a_present/bin/rails create spree_it_is_a_present/spree_it_is_a_present.gemspec create spree_it_is_a_present/Gemfile create spree_it_is_a_present/.gitignore create spree_it_is_a_present/LICENSE create spree_it_is_a_present/Rakefile create spree_it_is_a_present/README.md create spree_it_is_a_present/config/routes.rb create spree_it_is_a_present/config/locales/en.yml create spree_it_is_a_present/.rspec create spree_it_is_a_present/.travis.yml create spree_it_is_a_present/Appraisals create spree_it_is_a_present/.rubocop.yml Your extension has been generated with a gemspec dependency on Spree 4.2.3.1. |
Como podrás comprobar el generador crea un directorio con el nombre que has indicado, añadiéndole el prefijo spree_. En nuestro caso spree_it_is_a_present. Entra a tu directorio y abre tu editor favorito:
cd spree_it_is_a_present
E instalamos las dependencias:
bundle install
3. Crea migraciones
Si necesitas que tu extensión persista información en la base de datos, tendrás que añadir una migración.
La guía de extensiones oficial de Spree muestra cómo añadir un campo a un modelo ya existente en Spree.
En nuestro caso, necesitábamos añadir un modelo completamente nuevo que bautizamos como Spree::PresentNote
. Puedes añadir una migración como harías en otros proyectos en Rails, en nuestro caso ejecutando el comando:
bundle exec rails g migration CreateSpreePresentNotes recipient_name dedication
Y una vez nuestro fichero de migración ha sido generado podemos retocarlo a nuestro gusto:
1 2 3 4 5 6 7 8 9 10 11 12 |
require 'spree_extension/migration' class CreateSpreePresentNotes < SpreeExtension::Migration[6.1] def change create_table :spree_present_notes do |t| t.belongs_to :order, index: { unique: true }, null: false t.string :recipient_name t.text :dedication t.timestamps end end end |
Presta atención a un par de detalles aquí. Primero fíjate cómo spree_present_notes
referencia a un modelo ya existente de Spree (Spree::Order
), aunque aquí se referencia como :order
, más adelante veremos qué debemos añadir en el modelo para que Rails sepa a qué modelo nos estamos refiriendo. Y segundo, en otros proyectos de Rails estarás acostumbrado a que tus migraciones hereden de ActiveRecord::Migration
. En lugar de eso, haz require 'spree_extension/migration'
y haz que tus migraciones hereden de SpreeExtensionMigration
. Esto hará que tu extensión funcione en versiones de Rails anteriores a Rails 5.x, (Ver SpreeExtension::Migration).
Estas migraciones se copiaran en la aplicación de Spree que use nuestra extensión cuando ejecuten el comando para instalarla, vamos a ver cómo se hace.
4. Añade e instala tu extensión en tu aplicación de Spree
Cuando antes empieces a probar tu extensión antes sabrás cómo se comporta, además mola ver cómo poco a poco se va haciendo realidad, así que llegados a este punto toca añadir tu extensión a tu aplicación de Spree. En el fichero Gemfile, añade tu extensión indicando el path en el que se encuentra en tu ordenador (Nota: cambia el path según tus necesidades)
gem ‘spree_it_is_a_present’, path: ‘../path/spree_it_is_a_present’ # Use the path where your gem is located in your computer
E instalamos las dependencias
bundle install
Una vez hecho esto instala tu extensión ejecutando el siguiente comando y confirma que quieres ejecutar las migraciones tecleando Yes cuando te lo indique:
bundle exec rails g spree_it_is_a_present:install
5. Añade tus propios modelos…
Esta parte es bastante sencilla, ya que simplemente tienes que añadir el código para tu nuevo modelo y ya está. En este caso añadimos el modelo Spree::PresentNote
(app/models/spree/present_note.rb).
1 2 3 4 5 |
module Spree class PresentNote < Spree::Base belongs_to :order, class_name: 'Spree::Order' end end |
Como detalle que no aparece en la guía oficial y que te puede ser de utilidad, fíjate en cómo relaciono Spree::PresentNote
con el modelo ya existente Spree::Order
.
En este punto puede que quieras añadir factories o fixtures para tus tests, si quieres echarle un vistazo a cómo lo hice yo te dejo por aquí el link al commit.
…y/o extiende otros ya existentes
Una vez que hemos creado el modelo Spree::PresentNot
e necesitamos asociarlo con Spree::Order
desde esta última para permitir la navegación entre objetos. Como Spree::Order
es un modelo ya existente, tenemos que extenderlo, para ello crearemos un decorador para Spree::Order
utilizando ActiveSupport::Concern
en app/models/spree_it_is_a_present/spree/order_decorator.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
module SpreeItIsAPresent module Spree module OrderDecorator extend ActiveSupport::Concern prepended do has_one :present_note, class_name: 'Spree::PresentNote', dependent: :destroy accepts_nested_attributes_for :present_note, reject_if: :all_blank end def present_note_with_default present_note || build_present_note end end end end Spree::Order.prepend SpreeItIsAPresent::Spree::OrderDecorator |
Como puedes ver, además de añadir la relación, también indicamos que desde ahora Spree::Order
aceptará atributos para :present_note
. Esto es importante para que podamos persistir la información en el formulario de checkout posteriormente.
Llegados a este punto sería conveniente añadir algunos tests. En mi caso he utilizado RSpec junto con FactoryBot, pero también puedes utilizar otros frameworks.
Lo que más nos interesa es saber si cuando persistimos una Spree::Order
esta es capaz de persistir también datos relacionados con una Spree::PresentNote
.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
require 'spec_helper' describe Spree::Order do describe 'Spree::PresentNote nested attributes' do it 'accepts nested attributes for Spree::PresentNote' do attributes = attributes_for(:order, user: create(:user)).merge({ present_note_attributes: attributes_for(:present_note) }) expect(described_class.new(attributes)).to be_valid end end end |
Además de esto, también es interesante saber si nuestro nuevo modelo funciona correctamente, sobretodo centrándonos en añadir tests que aunque en otros proyectos me podrían parecer superfluos, en este caso al ser una gema compartida con muchas personas considero que servirán para prevenir futuras regresiones.
1 2 3 4 5 6 7 8 9 10 11 12 |
require 'spec_helper' describe Spree::PresentNote do describe 'attributes' do it { is_expected.to respond_to(:recipient_name) } it { is_expected.to respond_to(:dedication) } end describe 'relationships' do it { is_expected.to respond_to(:order) } end end |
6. Muestra tu formulario en el frontend
Lo primero para mostrar algo es tener algo que mostrar. Para ello, creamos un partial que contenga nuestro formulario y lo incrustamos con Deface. El partial sería tal que así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<div class="row"> <div class="col-12 mb-4"> <div id="present_note"> <h5 class="text-uppercase checkout-content-header"> <%= Spree.t('it_is_a_present.form.header') %> </h5> <%= form.fields_for :present_note, order.present_note_with_default do |present_note_form| %> <div class="inner checkout-content-inner" data-hook='present_note_inner'> <div lass="form-group checkout-content-inner-field has-float-label"> <%= present_note_form.text_field :recipient_name, class: 'required spree-flat-input', placeholder: Spree.t('it_is_a_present.form.recipient_name') %> <%= present_note_form.label :name, Spree.t('it_is_a_present.form.recipient_name'), class: 'text-uppercase' %> </div> <div class="form-group checkout-content-inner-field has-float-label"> <%= present_note_form.text_area :dedication, class: 'required spree-flat-input', placeholder: Spree.t('it_is_a_present.form.dedication') %> <%= present_note_form.label :dedication, Spree.t('it_is_a_present.form.dedication'), class: 'text-uppercase' %> </div> </div> <% end %> </div> </div> </div> |
Modificar vistas con Deface es muy sencillo gracias a que en Spree Commerce se han tomado la molestia de añadir IDs a la mayoría de elementos HTML. Si bien no sé hasta que punto el utilizar Deface seguirá siendo una opción viable en el futuro dado que el equipo que mantiene Spree decidió dejar de incluirla como dependencia. De momento las principales gemas de spree-contrib
siguen utilizándola así que yo hice lo mismo. Habrá que estar atentos.
A continuación puedes ver cómo se hace para añadir nuestro partial al formulario de checkout:
1 2 3 4 5 |
Deface::Override.new( virtual_path: 'spree/checkout/_address', text: "<%= render partial: 'spree/present_notes/form', locals: { form: form, order: @order } %>", name: 'present_notes_form', insert_before: '#delete-address-popup') |
7. Persiste los datos del formulario
Muy bien, ya tenemos por un lado los modelos que hemos visto en el paso 5 y también tenemos la vista que acabamos de ver en el paso anterior, ¿Qué nos falta? Efectivamente, el pegamento, el controlador.
El controlador de checkout que persiste instancias de Spree::Order
no tiene ni idea de cómo persistir nuestro nuevo modelo Spree::PresentNote
. En el paso 5 ya indicamos a Spree::Order
que debía aceptar atributos para Spree::PresentNote
, pero en algún lugar deberemos pasárselos. Ese lugar será Spree::CheckoutController
que es dónde Spree recibe todos los parámetros y persiste los pedidos. Para ello utilizaremos de nuevo el patrón Decorator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
module SpreeItIsAPresent module Spree module CheckoutControllerDecorator extend ActiveSupport::Concern private def permitted_checkout_attributes super + [ present_note_attributes: present_note_attributes ] end def present_note_attributes %i[recipient_name dedication] end end end end Spree::CheckoutController.prepend SpreeItIsAPresent::Spree::CheckoutControllerDecorator |
¿Y ahora qué?
Ahora tocaría los últimos pasos, que serían añadir un formulario para el backend y un controlador. Este punto es análogo a los puntos 6 y 7, así que para no extender más este post creo que no merece la pena explicarlo en detalle. Si quieres echarle un vistazo te dejo aquí el enlace al commit de Spree It is a commerce en el que añado esto. El resto es ya simplemente pulir un poco la gema y compartirla con el mundo.
Si has trabajado conmigo ya sabrás que me gusta mantener una historia de commits lo más clara y limpia posible, así que si te interesa ver paso a paso qué fui añadiendo a la gema puedes echarle un vistazo a todo el proceso commit por commit aquí.
¡Y hasta aquí llega el post de hoy! ¡Hasta la semana que viene! ¡Chao!