sábado, 26 de noviembre de 2011

Rails legacy

¿Que tipos de legacy tenemos?
  1. El código que hice hace seis meses, cuando mis conocimientos eran inferiores a los actuales [fácil]
  2. El código que no he echo yo, sino un compañero, otro equipo o una empresa de terceros (en este orden) [moderado]
  3. El código o aplicación que ha estado o está funcionando desde hace "tiempo" con una tecnología distinta a rails, distinguiendo:
    1. Aplicación a recaer [moderado]
    2. Aplicación a heredar (los datos, el funcionamiento) o convivir [difícil]
Podemos entrar en muchos matices y seguramente definir una lista más completa, en todo caso para los dos primeros puntos, dos conejos rápidos:
  1. Si no tengo test añadirlos, si tengo entenderlos, añadir más tests... he dicho test? ¡Pues eso, test, test, test!
  2. Refactoring a demanda, cuando se tenga que modificar algo se aprovecha y se corrige.
Para el tercer punto, sigue leyendo ya que es el motivo principal de este articulo.

¿Rails es legacy?

Si!

¿Rails está pensado para legacy?

En mi opinión, no! La filosofía core de rails es "Convention over Configuration (CoC)". Una aplicación legacy raramente seguirá el convenio de rails, con lo que a priori ya perdemos o tenemos que añadir el coste de ir especificando todo lo que no siga el convenio, nombres de tablas, tipos, joins, etc... Lo segundo y derivado de este problema, es que al salirnos del core de rails y ampliar con gems, nos encontraremos que algunas de ellas están solo pensadas para aplicaciones que sigan el convenio y no tienen toda la especificación rails implementada. Si no tienes alternativa sigue leyendo.


Primera solución: mapeando con vistas

Como: creando vistas que renombren la tabla, las columna y los tipos al convenio de rails y luego creando modelos rails que nos permitan acceder a ellos.

Limitaciones: solo lectura (* dependiendo del SGBD).

Problemas habituales y posibles soluciones / alternativas: 
  • Indice compuesto por strings:
    1. Mantenemos el string como indice (nos saltamos el convenio)
    2. Creamos una tabla intermedia que traduzca de strings a integer y ligamos su mantenimiento con trigers.
    3. Creamos una función que traduzca de strings a integers (afecta al rendimiento y puedes tener claves muy largas)
  • Indice compuesto por múltiples columnas: juntamos las columans en la vista

Segunda solución: mapeado en modelos las cuatro coas más importantes

En el modelo:

nombre de la tabla personalizado:
        set_table_name "client"

nombre del identificador principal o clave primaria (sin clave compuesta)
set_primary_key "client_id"
* Tener en cuenta que la opción "find_by_id" deja de funcionar, podemos utilizar "find_by"o "find_by_client_id"
 nombre del identificador principal o clave primaria (con clave compuesta)
Instal·lamos la gem composite_primary_keys
Especificamos: "set_primary_keys :person_id, :group_id"
 renombrado columnas puntuales
alias_attibute "person_name_full", "name"
* Aquí los find funcionan para el alias.
definiendo relaciones con otros modelos
has_many :comments,
 :class_name => "WordpressComment"
 :foreign_key => "comment_post_ID"
* Ejemplo real: tabla "post" con la columna "comment_post_ID" que mapeamos contra la tabla del modelo "WordpressComment" con el nombre de tabla y el id que tenga definidos.
definiendo relaciones con otros modelos y claves compuestas:
Si la cosa se complica mucho, mi recomendación es utilizar métodos dentro del modelo que nos permitan realizar las búsquedas:
def comments
    WordpressComment.find_by_post_id(self.coment_post_id)
end
 Forzar los plurales "config/initializers/inflections.rb":

* Partiendo del nombre de la tabla y añadiendo una "s"
inflect.irregular 'imp_person', 'imp_persons'
inflect.irregular 'imp_comments', 'imp_commentss'
 En las migrations:

Siempre que quieras crearlas (lo normal es que venga hechas).

si el campo id no existe, especificarlo
def change
   create_table :imp_person, {:id => false} do |t|

...
si no hay fechas de control (eliminarlas)

t.timestamps
si tenemos clave primaria añadirle el indice con execute
def up
  execute "ALTER TABLE person ADD PRIMARY KEY (national_document);"
end
def down
  execute "ALTER TABLE person DROP PRIMARY KEY (national_document);"
end

Un par de recomendacoines más:
  1. Valorar el añadir un prefijo a los modelos, de esta forma juntamos toda la lógica.
  2. Dado el coste de definir, lo definimos todo, aún siguiendo el convenio de rails, té evitarás problemas raros.
  3. Si has generado el modelo con scaffold reviste las fixtures, es posible que no te sirvan (y escribe test!).

Otro escenario habitual en legacy: nueva aplicación que tiene que heredar los datos del sistema anterior. Aquí es habitual encontrarse con los mismos problemas de convenio, más las limitaciones derivadas de los nuevos modelos de datos, campos que se añaden, campos que se quitan, nuevas reglas de negocio que pueden provocar que nuestros datos no se validen, nuevas columnas que no podemos rellenar etc... La solución dependerá mucho de nuestro escenario, en general suele dar muy buen resultado crear modelos paralelos más o menos ligados con el principal, por ejemplo podemos tener el modelo "Client" y "ClientLegacy", aquí ya es la habilidad de cada uno para reducir código, solo una nota, en mi opinión es mejor mantener los dos modelos con dos tablas distintas (para mi la validación en BD es igual de importante que en el modelo y claramente ambos tendrán validaciones distintas).



Hasta aquí esta primera aproximación al mundo legacy, en futuros artículos más escenarios, soluciones y experiencias.

sábado, 19 de noviembre de 2011

¿Como evitar problemas en las migrations?

Primero de todo teniendo en cuenta para que sirven:
  1. Actualizar el esquema de base de datos (añadir tablas, campos, índices, eliminar...) [50% del valor total]
  2. Automatizar transformaciones o scripts para mover datos en el despliegue de producción (típico caso de una tabla que se divide en dos y tenemos que recolocar los datos con cierta lógica). [30% del valor total]
  3. Poder movernos en la línea temporal y en caso de necesidad poder bajar a una versión de código inferior con una base de datos acorde. [20% del valor total]
Estos tres puntos son lo más importante y lo que más dolores de cabeza trae en entornos donde no hay una solución equivalente a las migrations (que muchas veces termina siendo un fichero leame o muchos scripts que la gente tiene que ir haciendo y que la persona que despliega tiene que acordarse de ejecutar en en desarrollo, producción, test, ...) todo lo demás son añadidos o como dirían los yankis "sugar", y me refiero a todo, des de los métodos del "Active Record" hasta el potencial derivado de utilizar código rails. Y el azúcar en exceso es malo (cita de mi médico).

Cuando aparecen los problemas:
  1. Al intentar hacer las migrations des de zero [ahora] hay algún paso que falla.
  2. Al desplegar en el entorno de producción/test falla.
Las causas de los problemas:

Modificación a posteriori de una migration subida al repositorio (GIT): típico caso donde alguien se ha dejado de añadir un campo, el nombre estaba mal escrito, el tipo de datos no era el necesario y en lugar de hacer una nueva migration se decide sobrescribir la original y subirlo al repositorio GIT. En que escenarios falla:
  • Si en lapos de tiempo entre la primera y la segunda versión, alguien había bajado los cambios del repositorio y había ejecutado "rake db:migrate". Aún bajando la nueva versión que podamos subir esa migration no volverá a ejectuarse y nuestro compañero quedará tocado.
  • Si tenemos sistemas automatizados como el servidor de integración continua, van a sufrir los mismos problemas. (Siempre y cuando este servidor rake db:migrate)
  • Si queremos movernos en la línea temporal de nuestro código o entre versiones (commits) es posible que nos encontremos con inconsistencias.
  • Favorece la aparición de condicionales, ejemplo añade una columna si no existe....
Esté caso se da con mucha frecuencia cuando tenemos un programador que percibe las migrations como parte del código a mantener "limpio" y ordenado, en lugar de ser una secuencia cronológica de cambios y mejoras, tener muchas migrations no es malo.

Haber implementado solo "up" si solo nos preocupamos de migrar para adelante estamos perdiendo el 20% del potencial y nos exponemos a:
  • En caso de error en producción si tenemos que retroceder de versión nos quedaremos con un código inconsistente ya que la base de datos no habrá bajado de versión (a no ser que recuperemos la copia de seguridad). Esto puede suceder tanto en el momento del deploy como al cabo de unos días que se decida volver a la versión antigua.
  • No poder probar el código entre versiones, solo podremos trabajar en la última versión y si tenemos que hacer un "rake db:migrate:redo" tendremos que ayudarnos de sql a mano.
  • Favorecer los condicionales en las migrations, mismo caso, añade la columna si no existe (porqué antes no la hemos borrado).
Por suerte el generador de scaffold ya nos ayuda cuando somo novatos y en rails 3.1 ha aparecido la opción "change" que tiene un número limitado de métodos (add_column, add_index, add_timestamp, create_table, remove_timestamps, rename_column, rename_index, rename_table) que no hace falta escribir "up" y "down"  y el solo sabe resolver. Para todo lo demás valorar

Pasar del "active record" y utilizar execute, a no ser que tengamos muy claro y pactado con el equipo y el cliente que vamos a utilizar un SGBD concreto, añadir "execute" nos limita a la hora de cambiar de base de datos, nos obliga a aprender y entender SQL (eso no quita que como programador debas conocerlo) y en general favorece el espagueti coding (sentencias de más de una linea con mega construcciones de tildes)

Utilizar modelos en nuestras migrations, es hambre para hoy y pan para mañana, a la larga cuando añadamos complejidad a nuestros modelos terminará generando problemas, el ejemplo:
Tenemos una migration que crea una tabla de usuarios con el campo correo (como login) y contraseña y utilizamos la misma migration para poner el usuario admin y de esta forma asegurar que siempre tenemos un primer usuario, más adelante decidimos ampliar la tabla con el campo nombre de usuario y lo hacemos obligatorio, des de este momento no podremos volver a hacer el up de esta migratio, ya que nos fallará al no tener informado el nombre de usuario.
Situaciones que lo promueven:
  • Populamos la base de datos des de las migrations. Es un error, debemos popular des del seed (prometo un articulo dedicado al seed y como popular).
  • Hacemos cambios de estructura (el ejemplo de pasar de una tabla a dos y tener que repartir la información). Ya de por si es un caso complejo, porqué nos encontramos con el código y los modelos en una versión superior, seguramente estaremos utilizando execute para acceder a la información antigua, con lo que mi recomendación es tratarlo como un caso excepcional y hacerlo todo con execute y sql directo, intentando ser lo más curiosos y encapsulando todo lo que podamos.


En caso de pánico cargar el fichero schema.rb con "db:schema:load" or "rake db:setup", el segundo incluye la carga del seed. Warning: ambas ordenes recrean la base de datos de zero, con lo que perderemos todos los registros, con lo que esto nos puede servir para un desarrollador que se une al equipo o un entorno de test / preproducción.

El resumen de consejos es:
  • Si se ha versionado (en git commit) no se puede modificar. Cremos una nueva migration y corregimos. [riesgo alto]
  • Siempre implementar la opción de up y down conjuntamente. [riesgo moderado]
  • Utilizar "active record" y evitar execute siempre que se pueda. [limitación al cambiar de SGBD]
  • Utilizar modelos en nuestras migrations. [riesgo alto]
  • Tener muchas migrations no es malo. [desmintiendo el mito]

domingo, 13 de noviembre de 2011

Git workflow

Ligado con el proyecto de infraestructura es necesario tener un plan de trabajo que nos permita trabajar de forma cómoda a la vez que resolver nuestros problemas con la mayor brevedad y menor incidencia.

En un ciclo normal de desarrollo, el equipo nos reunimos con el cliente, revisamos el estado del proyecto y definimos una serie de historias de usuario / funcionalidades / necesidades que creemos podemos desarrollar en un tiempo corto (idealmente para la siguiente itreación), una vez terminadas o alcanzado el plazo pactado, pasamos nuestras funcionalidades nuevas a release (no tiene porqué ser al final, puede ser progresivo), las validamos con el cliente y si todo va bien las ponemos en producción.
¿Que sucede si no se aprueba la release? Tenemos dos escenarios:

Idealmente tendríamos que corregirlo en develop y luego volver a actualizar release, con lo que si algun otro programador está trabajando en develop, podremos no solo corregir el problema sino también añadir nuevo valor al cliente (siguiendo el flujo de mejora continuada).

Si por alguna razón develop no está estable (se ha perdido funcionalidad, se ha dejado algo que antes funcionaba a medias, etc, ...) tenemos un problema serio. No obstante podemos hacer una copia de la branca release, corregir el problema, hacer merge en release, validarlo con el usuario y hacer merge en develop.
Como vemos a la práctica es más costoso, requiere de más merges, tenemos cosas nuevas que el usuario final aun no puede ver.

¿Que sucede si aparece un error en producción?

Primero de todo analizar el nivel de criticidad, es realmente un error, cumple nuestro requisito de hotfix, si es que sí hacemos lo siguiente:
  1. Copia de master en local 
  2. Realizamos un test que reproduzca el problema
  3. Corregimos, merge a master, validamos con el usuario.
  4. Merge a develop


El segundo merge lo hacemos en develop, para tener nuestra branca "principal" de desarrollo actualizada, no obstante tenemos un escenario donde podemos valorarlo:
  • Tenemos entorno de pre-producción y no podemos / queremos que el error se pueda reproduzir allí.
Típicamente en este punto queda la duda si se puede perder un hotfix con algún merge, por ejemplo con uno de release (sin hotfix) a master (con hotfix), en este caso si el merge funciona bien (que es lo normal) se mantiene el hotfix en master.

Un pequeño resumen de las brancas que tenemos:

Origin / Master (M):
  • Branca estable (en producción)
  • Solo HOTFIX (error grave, requerimiento legal, ..), con copia a develop.
Origin / Release (R):
  • Futura versión a desplegar
  • Funcionalidades completas y terminadas
  • Solo correcciones menores que no se pueden hacer en develop, con copia a develop.
Origin / Develop (D):
  • Las nuevas funcionalidades van aquí
  • Branca compartida entre programadores
  • Si no está aquí no existe (no se mantiene, prueba, etc..)
  • No hay funcionalidades a medias, subimos cuando está listo para validar (por otros programadores, el cliente, …) [idealmente]

Un árbol resumen de que atacar y en que orden:

Y como siempre el resumen final (click para ampliar):

Agradecer el articulo de Vincent Driessen en http://nvie.com/posts/a-successful-git-branching-model/ que en su día nos ayudo a tener una idea clara de como trabajar con GIT.

sábado, 5 de noviembre de 2011

ActiveRecord CallBacks

Cuando los modelos se empiezan a complicar es bastante útil conocer los distintos métodos que tenemos para interferir en los procesos de transferencia entre la base de datos y nuestros modelos, aquí viene un pequeño resumen:


Cargando objetos:

Nos permiten alterar los datos o intervenir en el proceso de carga en memoria.
  • after_initialize: cuando queremos interferir al crear / cargar (instanciar) un objeto.
  • after_find: cuando se carga en memoria un objeto de la base de datos (model.find, model.first..). 
Tener en cuenta que primero se lanza "after_find" y luego "after_initialize".

def after_initialize
    user.name = self.name.capitalize
end

user = User.new(:name => 'ADMIN') 
puts user.name 
=> Admin

Si queremos que algo solo se aplique a los modelos que provienen de la base de datos, debemos utilizar el "after_find" (para marcar que es un modelo con A.R.) y si queremos que se aplique a todo (creados en memoria y leídos de la base de datos), tenemos que utilizar el "after_initialize".


Guardando objetos (nuevos o existentes):

Están disponible al invocar save (user.save)
  • before_validation i [before_validation_on_create | before_validation_on_update]: típicamente para automatizar campos (calculados, assegurar-se que un valor no es nulo...).
  • after_validation, [after_validation_on_create | after_validation_on_update], before_save: típicamente para validar la lógica del modelo y de la aplicación en conjunto.
  • [arround_insert | arround_update]: guardado físico y validación por parte de la base de datos.
  • [after_create | after_update] y after_save: desencadenadores que nos permiten actualizar contadonres, actualitzar dependencias, comunicar acciones o eventos por correo o servicios web, etz..


Eliminando objetos (existentes en la base de datos):

Están disponibles al invocar destroy (user.destroy)
  • before_destroy: validar la lógica del modelo y de la aplicación en conjunto, por ejemplo dependencias.
  • arround_destroy: guardado físico y validación por parte de la base de datos.
  • after_destroy: desencadenadores que nos permiten actualizar contadonres, actualitzar dependencias, comunicar acciones o eventos por correo o servicios web, etz..


Guardado conjunto con transacciones:

Si tenemos una transaccion definida (ejemplo típico de transferir dinero de una cuenta a otra)

SendMoney.transaction do
    customer_1.save
    customer_2.save
end

Tenemos disponible:
  • after_commit: cuando la transacción y todos sus pasos se han realizado correctamente
  • after_rollback: cuando la transacción a fallado, porqué uno de los pasos a fallado.


Guardado con relaciones o anidado (relations and nested atributes):

Si tenemos modelos con relaciones definidas (has_many, belongs_to, ...) podemos aprovechar los métodos siguiente métodos para interferir en su creación:

  • before_add y after_add: al crear
  • before_remove y after_remove: al eliminar

class Customer < ActiveRecord::Base
    has_many :orders, :before_add => :check_credit_limit
    ...


Cuadro resumen:



Más información en:


martes, 25 de octubre de 2011

Creando PDF's con PRAWN

Si quieres crear informes o documents PDF's a demanda (dinamicamente) y aún no has escogido ninguna solución, no dejes de echarle un vistazo a PRAWN.

Simples pasos para la instalación:
  1. Editar el fichero "gemfile" y añadir "gem 'prawn' " 
  2. Linea de comandos y "bundle install"
  3. En "app" creamos una carpeta "app/reports" donde guardaremos nuestros generadores de PDF's
  4. En "app/reports" creamos una caperta "app/reports/images" donde guardaremos las imágenes que añadamos a nuestros PDF's
  5. Editamos nuestro fichero "application.rb" y añadimos la carpeta que hemos creado.
    config.autoload_paths << "#(Rails.root)/app/reports" #PDF custom generators
  6. Bajarnos el manual de http://prawn.majesticseacreature.com/manual.pdf y ponerlo en "doc/prawn_manual"
¿Cómo creamos nuestro primer report? 
  1. Creamos nuestro generador PDF en "app/reports/products_report1.rb" y "app/reports/products_report2.rb"
  2. Lo rellenamos con algún ejemplo dumy
    Ejemplo1:

    Ejemplo2:
  3. Creamos el punto de entrada en el controlador "app/controller/products_controller.rb"
  4. Añadimos la ruta en el "config/routes.rb"
  5. Añadimos un botón o link al informe en "app/views/products/index.html.erb"
    <%=link_to 'report1',  print1_products_url%> -
    <%=link_to 'report2',  print2_products_url%> <br />
Des de aquí es solo ampliar los generadores que hemos creado, para que mediante el controlador vaya renderizando la información que nos interese.

Para revisar el proyecto de ejemplo en profundidad puedes ir a https://github.com/micues/prawn_test

También te será útil revisar-te el manual que hemos adjuntado en la documentación para ver más en detalle que objectos y funciones puedes utilizar con prawn.

    viernes, 21 de octubre de 2011

    Ubuntu RoR + postgresSQL 9 -> sobre windows

    Si tu entorno "natural" es windows, pero tu desarrollo es rails, hay algunas situaciones donde te puede interesar tener una ubuntu con rails funcionando:
    • Para probar el sistema en un entorno parecido al de producción (en producción windows + rails está prohibido).
    • Para las gem que no se pueden compilar con el DevKit o no tienen versión "-x86-mingw32"(pre-compilada).
    • Para aumentar la velocidad en las pruebas en el servidor de desarrollo (sobretodo arranque)
    • Para aumentar la velocidad en los test (esta es mi principal)
    Para conseguirlo unos simples pasos:
    1. Descargamos virtual box (porqué es gratuito multi plataforma)
      https://www.virtualbox.org/wiki/Downloads
      I escogemos VirtualBox 4.1.x for Windows hosts x86/amd64
    2. Instalación dummy de virtual box (siguiente-siguiente)
    3. Descargamos la última versión de ubuntu
      1. Vamos a http://www.ubuntu.com/download/server/download
      2. Seleccionamos la versión a descargar. Mi recomendación la última LTS (Long-time-suport) que seguramente será la que tengamos en el servidor.
        Las versiones LTS (Long Term Support), que se liberan cada dos años, reciben soporte durante tres años en los sistemas de escritorio y cinco para la edición orientada a servidores
        Fuente: http://es.wikipedia.org/wiki/Ubuntu
      3. Guardamos la iso en un lugar conocido.
    4. Creamos la máquina virtual. Inicio -> Ejecutar-> "Oracle VM VirtualBox" y le damos a "New" o CTRL+N. A partir de aquí es un wizzard, nombre, versión... ram, disco, que tener en cuenta:
      1. Sistema operativo igual al que vamos a instalar Linux y ubuntu amb64 o ubunty 32 bits i386
      2. Disco tipo VDI (virtual box disk image) y "fixed size" en lugar de "dynamically allocated", para mejorar la velocidad.[recomendación]



        Si por equivocación le damos a cancelar al wizzard siempre podemos añadir la imagen des de "Devices" -> "CD/DVD's"

    5. Instalar el sistema operativo. Seleccionamos la máquina instalada y le damos al botón "Start" o "Run" y la primera vez no salé un wizzard. En la segunda pantalla del wizzard podemos escoger montar una unidad con la imagen que hemos bajado y de esta forma nos arranca la máquina con el wizzard de instalación de ubuntu.



    6. Instalación dummy de ubuntu (siguiente-siguiente)



      Del wizzard solo hace falta cambiar el teclado al que tengamos y escoger un nombre de usuario y contraseña.

    7. Para instalar rails (tiene que ser en una instalación limpia), linea de comandos y
      wget --no-check-certificate https://raw.github.com/joshfng/railsready/master/railsready.sh && bash railsready.sh
      + información en https://github.com/joshfng/railsready

      El script nos pregunta si queremos RVM (Ruby Version Manager) que nos permitiría en un futuro instalar en la misma máquina otra version de ruby que no sea la 1.9.2. Es muy recomendable trabajar en RVM pero si no lo ves claro (o quieres la máquina lo más simple posible) siempre puedes crear otra máquina virtual y instalar la versión de ruby superior cuando la necesites.

      Para que funcione necesitamos tener internet accesible des de la máquina virtual.

    8. Para instalar postgresSQL, linea de comandos y
      1. sudo apt-get install python-software-properties
      2. sudo add-apt-repository ppa:pitti/postgresql
      3. sudo apt-get update
      4. sudo apt-get install postgresql

      5. ¿Por qué? Porqué ubutnu no puede dar soporte a todas la versiones de PostgresSQL y la oficial es la 8.2 (a noviembre 2011), en futuras versiones se irá actualizando. Entonces para evitar tener que bajarnos la versión de la página oficial http://www.postgresql.org/download/ y compilarla por nosotros mismos, lo que hacemos es añadir un segundo respositorio de software (market / app store) y bajarnos la versión des de allí. 
        (http://www.dctrwatson.com/2010/09/installing-postgresql-9-0-on-ubuntu-10-04/)

    9. Des de aquí la instalacón es igual que en windows 
      1. linea de comandos y "gem install rails"

    En este punto deberías tener una máquina preparada para desarrollar, recuerda de realizar la performance / tuning básica al postgres (de serie viene preparada para un 386) y instalar GIT.

    Una buena solución que se integra muy bien con Windows es utilizar las "shared folders" de la virtual box, las encontrarás en "Devices" -> "Shared Folder" y te permiten mapear tus carpetas de windwos en ubuntu.

    Una última recomendación para VirtualBox es cambiar en "Devices" -> "Network Adapters" la conexión de "Nat" a "Bridge" (para que la máquina sea visible des de fuera) y instalar la "Guest additions" que encontramos en "Devices".

      miércoles, 19 de octubre de 2011

      Zona horaria en rails

      Si trabajas con fechas y horas y tienes configurada una zona horaria esto te puede interesar.

      Cuando inicies un proyecto en rails recuerda modificar:
      /config/application.rb

      Y substituir
      # config.time_zone = 'Central Time (US & Canada)'
      Por
      # More timezones on http://apidock.com/rails/TimeZone
      config.time_zone = 'Madrid'


      ¿Por qué no sumar +2UTC?

      Porqué en la zona horaria de Madrid el más n varía dependiendo del horario de verano y invierno, con lo que no podemos solo sumar n horas, tenemos que especificar la zona horaria, la cual ya gestiona la diferencia dependiendo del día del año.

      ¿Y si vivo fuera de España?

      Revisa http://apidock.com/rails/TimeZone y busca tu zona horaria.

      ¿Como detecto que la hora está mal?

      Dependiendo de la configuración del equipo o servidor, y de como nuestros usuarios entre las fechas, es posible que no nos demos cuenta, con lo que la forma más fácil es crear un registro y revisar las columnas "created_at" y "updated_at" y ver si coinciden con las actuales.

      Infraestructura con GIT y varios entornos

      De la creación a la actualidad.

      En la primera iteración, dios creó la programación y el primer programador programaba "a pelo" contra el entorno de producción.
      Todo funcionaba bien, hasta que un lunes, después de tener a nuestro programador trabajando dos días seguidos sin descanso, los usuarios vieron les cambios y le pidieron que los deshiciera. El pobre programador no sabía que hacer, porqué no recordaba que "coño" había tocado y el chico de sistemas estaba de vacaciones, con lo que no nadie cambiaba la cinta de la copia de seguridad, ergo no había copia.

      En la segunda iteración, dios se apiadó de el, y creó el entorno de producción, donde el desarrollador pudiera trabajar durante meses sin afectar a producción, como era un dios generoso, le facilito también un sistema de control de versiones, entonces creo GIT (subversión, SVN...), de esta forma podía hacer prototipos, desplegar en producción, subir y bajar de versión, cuando quisiera.

      Todo funcionaba bien y había mucho trabajo, pero a veces nuestro cowboy solitario tocaba partes del código y otras partes dejaban de funcionar.


      En la tercera iteración dios creo los test automatizados y el entorno de test.
      Todo funcionaba bien, y había mucho trabajo con lo que decidieron coger un segundo programador. Todo iba fenomenal, estaban compenetrados, bueno casi, excepto cuando los dos modificaban partes comunes, o cuando actualizaban el software de desarrollo, eran pequeños problemas que pasaban los test en la máquina de cada programador (cuando todos se acordaban de pasarlos), pero que al unirse en desarrollo no funcionaban.

      Dios lo vio y en la cuarta iteración decidió crear el ángel Jenkins, dueño y amo del servidor de integración continua, el cual siempre estaba atento a los nuevos cambios del GIT, los bajaba y volvía ha ejecutar los test y en caso de error se lo comunicaba al equipo de desarrollo.
      Todo funcionaba bien, hasta que llego el día en que su aplicación empezó ha comunicarse con otras aplicaciones y sistemas, los usuarios estaban integrados con el sistema de recursos humanos, la comunicación con el servidor de correo que sistemas había instalado, la explotación de datos con el DWH corporativo que se había comprado, había muchas cosas que estaban fuera de la lógica y los test de programación.

      Nuestro buen dios en su quinta iteración lo vio y decidió crear los MOCKs (objetos simulados) para que los desarrolladores y el ángel Jenkins pudieran probar el funcionamiento, así como también creó el entorno de pre-producción para que sistemas y el usuario final pudieran probar las funcionalidades en un entorno separado del de producción.

      Todo funcionaba bien, la empresa era un éxito y nuestro software estaba triunfando, con lo que un día decidieron contratar a un comercial para vender "la solución" a otros clientes, pero había un problema que le íbamos a enseñar a nuestros clientes?

      Era sábado y dios estaba de compras en su sexta iteración, con lo que decidió comprar un servidor, el servidor DEMO, donde el comercial podría hacer sus demos sin datos de los clientes y los desarrolladores podrían ver la última versión estable de todo el software (no solo del producto donde ellos trabajaban), de esta forma podría proponer mejoras y coger ideas de otros equipos.


      Era la séptima iteración y dio decidió descansar y contemplar su obra.

      sábado, 15 de octubre de 2011

      Como: crear un nuevo entorno en ROR?

      Hay varios escenarios donde te puede interesar crear un nuevo entorno (environment):
      1. Crear un entorno de pre-producción con variables a otros sitems de pre-producción.
      2. Tener un servidor Demo constantamente operativo.
      3. Tener dos entornos de producción distintos (dos clientes).
      Para hacerlo: ejemplo "pre_production"
      1. Copiamos de "config\environments\production.rb" a "config\environments\pre_production.rb", editamos el fichero y modificamos la variables que sean distintas.
      2. Editamos el fichero "config\database.yml", copiamos el bloque "production" y lo renombramos a pre_produciton
      3. Subimos al GIT
      Para probarlo
      1. Tenemos que ejecutar los trabajos rake con en el nuevo enriorment, ejemplo para migrate
        rake db:migrate RAILS_ENV=pre_production
      2. Para iniciar el servidor:
        rails server -e pre_production
      Extra (que tener en GIT y que no).
      1. Como parte de la configuración del entorno, recomiendo quitar del GIT el "database.yml" y en su lugar añadir el "database.yml.example" (que deber ser una copia del database.yml") de esta forma cada desarrollador puede tener una base de datos distinta y/o un usuario y contraseña específico.
      2. Los mismo que en el punto anterior se aplica a "config\environments\development.rb"
      3. En el caso de tener de "config\environments\production.rb" si tenéis dos entornos de producción y vais a tomar otras estrategias, pensad en mantener el proceso lo más automatizado posible y evitar las modificaciones a mano para el despliegue.

      Datepicker and timepicker more friendly

      Si tu usuarios se quejan de tener estás cajas:
       Y te reclaman un sistema más "moderno" esto te puede interesar.

      Problemas identificado de estás cajas:
      1. No hay garantía que la fecha se haya introducido correctamente. Y si no se introduce correctamente (ejemplo 30/02/2011) la base de datos la guardará como nulo (por defecto sin avisar).
      2. Es lento (desplegar y seleccionar requiere de 5 a 10 segundos, hacer la prueba con vuestros usuarios) 
      3. No tenemos forma de orientarnos,  la fecha seleccionada es hoy?... el martes que viene? ...
      4. Estamos informando un solo campo (fecha o fecha con hora) pero tenemos de 3 a 6 desplegables, con lo que  inconscientemente le estamos dando un peso (o importancia) mayor del que debería.
      5. Las 3-6 cajitas con sus separadores, sus flechas desplegables y su contenido ocupan un espacio que podríamos simplificar o ocupar con otro contenido.
      Solución propuesta:
      1. Convertimos los campos de "form_field_on" o "form_field_at" a "text_field".
      2. Añadimos un selector que nos facilite la vida.
      Resultado (desplegado):


      Como lo conseguimos:
      Si vas a trabajar con formatos de fecha no americanos, es posible que antes de empezar te interese adaptarlo (http://railsdynamics.blogspot.com/2011/09/cambiando-el-formato-de-fecha.html).
      Para los objectos date (de aquí en adelante "_on") tenemos el DatePicker de JQueryUI http://jqueryui.com/demos/datepicker/
      Para los objetos time (de aquí en adelante "_at") tenemos  el addon TimePikcer http://trentrichardson.com/examples/timepicker/ o https://github.com/trentrichardson/jQuery-Timepicker-Addon

      Pasos:
      1. Colocamos los javascript
        app/assets/javascripts/jquery-ui-1.8.16.custom.min.js
        app/assets/javascripts/jquery-ui-timepicker-addon.js
      2. Si queremos internacionalizar también añadimos los javascript de I18N
        app/assets/javascripts/jquery-ui-i18n.js
        app/assets/javascripts/jquery-ui-timepicker-addon.js
      3. Añadimos el CSS para que se vea correctament
        app/assets/stylesheets/jquery-ui-timepicker-addon.css
      4. Extendemos la clase form helper para añadir dos nuevos métodos: form_field_at y form_field_on
        app/helpers/form_helper.rb
        module FormHelper
          def self.included(base)
            ActionView::Helpers::FormBuilder.instance_eval do
              include FormBuilderMethods
            end
          end

          module FormBuilderMethods  
            def form_field_at(method, default_value = nil)
              script = '<script>'
              script << ' $(function(){$( "#' + @object_name.to_s + '_' + method.to_s + '" ).datetimepicker({ showOn: \'button\' });});'
              script << '</script>' + "\n"
                  
              value = @object.send(method).to_s
              value = default_value.to_s if value.blank?
            
              form_field_at = @template.text_field @object_name, method,
                    :value => value , :size => 16, :ondblclick => 'this.value="' + Time.now.to_s + '"'
            
              script.to_s.html_safe + '  ' + form_field_at.to_s.html_safe
            end

            def form_field_on(method, default_value = nil)
              script = '<script>'
              script << ' $(function(){$( "#' + @object_name.to_s + '_' + method.to_s + '" ).datepicker({ showOn: \'button\' });});'
              script << '</script>' + "\n"
                  
              value = @object.send(method).to_s
              value = default_value.to_s if value.blank?
            
              form_field_on = @template.text_field @object_name, method,
                    :value => value , :size => 10, :ondblclick => 'this.value="' + Time.now.to_date.to_s + '"'
            
              script.to_s.html_safe + '  ' + form_field_on.to_s.html_safe
            end  

          end
         
        end
      5. Cambiamos en la vista las cajas de texto de text_field a form_field_at y form_field_on
        app/views/events/_form.html.erb
          <div class="field">
            <%= f.label :start_at %><br />
            <%= f.form_field_at :start_at %>
          </div>
          <div class="field">
            <%= f.label :finish_on %><br />
            <%= f.form_field_on :finish_on %>
          </div>
      Con esto tendremos los date y time picker funcionando, ahora si queremos añadir validación también tenemos que:
      1. Añadir un helper para la fecha
        app/helpers/validation_helper.rb
        module ValidationHelper
          def conversion_to_date_at(method,field_at_before_type_cast)
            errors.add(method, ' (' + field_at_before_type_cast + ') formato incorrecto ' + DATE_AT_FORMAT_EXPLANATION + ' o valor fuera de rango') if is_invalid_date_at(field_at_before_type_cast)
          end

          def is_invalid_date_at(field)    
            if field == nil or field == ''
              false   #Null is not an invalid date. Valid date!
            else
              begin
                if defined?(DATE_AT_FORMAT)
                  format_to_validate = DATE_AT_FORMAT
                else
                  format_to_validate = I18n.t("time.formats.default")
                end
                DateTime.strptime(field, format_to_validate)              
                false  #Converstion without problems! Valid date!
              rescue
                true  #Converstion with problems! Invalid date!
              end
            end
          
          end

          def conversion_to_date_on(method,field_on_before_type_cast)
            errors.add(method, ' (' + field_on_before_type_cast + ') formato incorrecto ' + DATE_ON_FORMAT_EXPLANATION + ' o valor fuera de rango') if is_invalid_date_on(field_on_before_type_cast)
          end

          def is_invalid_date_on(field)    
            if field == nil or field == ''
              false   #Null is not an invalid date. Valid date!
            else
              begin
                if defined?(DATE_ON_FORMAT)
                  format_to_validate = DATE_ON_FORMAT
                else
                  format_to_validate = I18n.t("date.formats.default")
                end      
                DateTime.strptime(field, format_to_validate)              
                false  #Converstion without problems! Valid date!
              rescue
                true  #Converstion with problems! Invalid date!
              end
            end
          end  
        end
      2. Añadir la validación en el modelo
        app/models/event.rb
        class Event < ActiveRecord::Base
          include ValidationHelper
          require 'date'
          validate {conversion_to_date_at(:start_at, start_at_before_type_cast)}
          validate {conversion_to_date_on(:finish_on, finish_on_before_type_cast)}
        end



      martes, 20 de septiembre de 2011

      Cambiando el formato de fecha

      Si tu formato de fecha no es YYYY-MM-DD esto te puede interesar.

      Por defecto en rails tenemos dos tipos de objectos para gestionar fechas los objectos date (de aquí en adelante "_on") que permiten guardar una fecha con año, mes y día y los objetos timestamp (de aquí en adelante "_at") que permiten guardar una fecha con año, mes, día, hora, minuto y segundo (sin zona horaria). Sus formato de salida por defecto son:

      Fecha (output):

      Fecha (input):

      Primer solución (y la oficial):

      Utilizar la API de internacionalización de RAILS, resumo los pasos de forma rápida:
      1. Descargar los "locales" que no interesen de https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale
      2. Copiarlos en "config\locales\" los que vayamos a utilizar.
      3. En el fichero "config\aplication.rb" descomentar la siguientes línias
        # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
        # config.i18n.default_locale = :de
      4. Y establecer nuestro idioma por defecto, para español sería:
        config.i18n.default_locale = :es
      5. Reiniciar el servidor web
      Con lo que tendremos:
      Con lo que hemos solucionado las entradas de fecha (input), pero no las salidas (output). Para solucionar las salidas tenemos que:
      1. Crear el fichero "initializers\date_formats.rb"
      2. Añadir:
        Time::DATE_FORMATS[:default] = lambda { |time| I18n.l(time) }
        Date::DATE_FORMATS[:default] = lambda { |date| I18n.l(date) }
      3. Reiniciar el servidor web
      Con lo que tendremos:
      En mi caso el formato por defecto para el locale ca (catalán) no era el deseado, con lo que abrí "config\locales\ca.yml" y modifiqué "default: "%A, %d de %B de %Y %H:%M:%S %z"" por "default: "%d de %b %H:%M""

      Solución alternativa (sin internacionalizar):

      Una solución única:
      1. Crear el fichero "initializers\date_formats.rb"
      2. Añadir nuestra definición de formatos. Ejemplo:

        DATE_AT_FORMAT = "%d/%m/%Y %H:%M"
        DATE_ON_FORMAT = "%d/%m/%Y"

        Time::DATE_FORMATS.merge!(
        :standard => DATE_AT_FORMAT,
        :default => DATE_AT_FORMAT,
        :standard_time => DATE_AT_FORMAT
        )

        Date::DATE_FORMATS.merge!(
        :standard => DATE_ON_FORMAT,
        :default => DATE_ON_FORMAT,
        :standard_time => DATE_ON_FORMAT
        )

        La siguiente alternativa también funciona:

        DATE_AT_FORMAT = "%d/%m/%Y %H:%M"
        DATE_ON_FORMAT = "%d/%m/%Y"

        Time::DATE_FORMATS[:default] = DATE_AT_FORMAT
        Date::DATE_FORMATS[:default] = DATE_ON_FORMAT

      Con esto habremos conseguido corregir los outputs, ahora si queremos corregir los inputs tendremos que ir uno por uno y añadir la línia ":order => [:day, :month, :year]", ejemplos:

          <%= f.datetime_select :start_at, :order => [:day, :month, :year]%>
          <%= f.date_select :start_on, :order => [:day, :month, :year] %>

      domingo, 18 de septiembre de 2011

      Double submit -> Double post -> Duplicated data

      Si tus usuarios hacen esto:
      1. Pulsar dos veces la tecla enter/intro
      2. Doble click en "guardar"
      Y te has encontrado con:
      1. Registros duplicados aleatorios inexplicables.
      2. Usuarios que intentan sabotear / automatizar la aplicación mandando múltiples submits.
      3. Una lógica de modelo que no gestiona los duplicados.
      Esto te puede interesar:

      En algunos casos, bien por la acción del mismo usuario o por algún error derivado del envió del post, el servidor recibe dos peticiones de post idénticas, entonces se puede dar el caso que acepte las dos y se generen registros duplicados o que se guarde un registro y se devuelva un mensaje de error para el segundo, con lo que confundiremos al usuario.

      Aproximaciones al problema:
      1. Implementación javascript del tipo "<form onSubmit="doublePostCheck()"> donde la función solo devuelve verdadero la primera vez.
      2. Token / Flag validation: de un variable tipo "hidden" en el cliente y validación en el servido.
      3. Hash de les variables del post y comparación de este con el último.
      En el punto 1 dependemos del usuario (que no desactive el Javascript o lo salte), en los puntos 2 y 3 no queda claro como gestionar el segundo submit.

      La solución ':disable_with => 'Saving...' :

      Modificar la vista y añadir "<%=  f.submit 'Save', :disable_with => 'Saving...' %>".

      La solución cumple con:
      • Evita el problema del "doble enter", "doble click".
      • Evita el problema del doble envio del post
      • KISS (Keep it simple stupid).
      • SPOT (Single point of thrut).

      BluePrint y CSS from rails 3.0 to rails 3.1

      Si utilizas el framewrok Blueprint y has migrado una aplicación de rails 3.0 a 3.1 esto te puede interesar, experiencias:

      >> La solución oficial (Estilos dentro de assets):
      1. Mover las hojas de estilo de "public\stylesheets" a "app\assets\stylesheets"
      2. Mover las imágenes de "\public\images" a "app\assets\images"
      3. Eliminar las llamadas a CSS "stylesheet_link_tag" de los layouts

      Problemas:
      1. Perdemos el condicional de "print, :media => 'print'" y "screen, :media => 'screen, projection'", con lo que si queremos utilizar BluePrint tendremos que borrar uno de los dos (app\views\layouts\*.erb)
      2. Si tenemos alguno estilo condicional como en nuestro caso "devise.css" lo tenemos que aplicar a todas las hojas.

      >> Migrar blueprint a SASS:

      Si migrar a SASS es una condición sinecuanum os puede interesar el siguiente proyecto "https://github.com/chriseppstein/blueprint-sass", nosotros no lo hemos probado.

      >> Nuestra solución mixta:
      1. Mover las hojas de estilo propias (por ejemplo devise.css) y el framework Blueprint a "vendor\assets\stylesheets".
      2. Mover las hojas de estilo comunes (típicamente proyecto.css) de "public\stylesheets" a "app\assets\stylesheets"
      3. Mover las imágenes de "\public\images" a "app\assets\images"
      4. Corregir las rutas de las imágenes en los hojas estilos, típicamente "proyecto.css". Ejemplo
        • Original: "background-image: url(/images/logo.png);"
        • Final: "background-image: url(/assets/logo.png);"
        • Este paso solo hace falta si queremos mantener todas nuestras imágenes en la carpeta "app\assets\images" si las dejamos en "\public\images" no hará falta hacer el cambio, pero tendremos que acordarnos que tenemos imágenes en dos carpetas distintas.
      Ventajas:
      1. Nos permite migrar la aplicación a 3.1 con pocos cambios
      2. Nos permite en un futuro empezar a utilizar SASS
      3. Nos permite mantener estilos condicionales.

      Hello world

      Hello world... o como decía mi profesor de programación Hola Manola.