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).