Rails 3.2 | How we updated our main product’s core without downtime

Segunda parte del diario de Ricardo Vila, ingeniero de software en TEIMAS, actualizando Teixo a la última versión de Rails.

This is some text inside of a div block.

Este artículo está únicamente disponible en inglés. Es la continuación de How we updated our main product’s core without downtime - Rails 3.2 (I of II): First steps.

2.3.2. Other changes and fixes

Once the app was running we continued to work making many changes, most of them on the first phase of this update while trying to get to 0 errors on our CI environment. But many other problems were detected and fixed when the app was released on our staging environment, thanks to the Customer Experience department.

Sometimes we faced problems that would require a lot of work to fix, so we first tried to workaround it with Monkey Patchings, and scheduled the right fix work in the future so we could bypass this error and go on with the process. Later we try to do the right fix but not after assessing if it is worth the work (sometimes it wasn’t).

Many of the problems we faced where those ones:

html_safe?

Teixo has a lot of helpers that return strings with embedded html, css, or even js. Since the arrival of xss protection with Rails 3 the use of those helpers and other variables on the Erb and Haml templates returned escaped html making Teixo unusable. The right fix would have been to review all the partials and use .html_safe on every ‘unsafe’ string use. This approach was impossible due the size of the project. We knew that our helpers and rest of code on partials were safe (we filter the user input) so we workaround this with a Monkey Patch:

We defined a module CustomHtmlSafe:

          module CustomHtmlSafe

            def html_safe?

          true

            end

          end

And monkey patched several classes. ActiveSupport::SafeBuffer also had to overwrite to_s to avoid rendering problems:

          class ActionView::OutputBuffer

            include CustomHtmlSafe

          end

          class ActiveSupport::SafeBuffer

            include CustomHtmlSafe

            def to_s

          "#{self}"

            end

          end

          class ActionView::SafeBuffer

            include CustomHtmlSafe

          end

          class String

            include CustomHtmlSafe

          end

form_for, remote_form_for and link_to_remote

The method form_for no longer received a symbol as the first param, whe had to change all the form_for from this:

          =form_for :document, @document, :url => url do |f|

To this

          =form_for @document, :url => url do |f|

Also, on the old form_for the symbol of first param should be added as an option with an :as label (when the name of the form param doesn’t match the field of the object).

          =form_for @document, :as => :document, :url => url do |f|

Teixo had a lot of partials which used remote_form_for that no longer exists. The Rails guide recommends to use form_for with the :remote option instead. We have to rewrite all those remote_forms.

Routes helper method changes

Several route helpers methods changed from Rails 2 so we had to review and rewrite them, specially in create and update methods:

          update_api_v2_devices_waste_collection_path(wc.id)

To:

          api_v2_devices_waste_collection_path(wc.id),

Also on remote routes helper methods like create_xxxx_remote_path or update_xxxx_remote_path.

After initialize

The after_initialize callbacks on Rails 2 were declared as a method, on Rails 3 this changed to the standard macro style:

          def after_initialize

          self.effective_company_type ||= DEFAULT_COMPANY_TYPE

          end

To:

after_initialize do |init_object|

init_object.effective_company_type ||= DEFAULT_COMPANY_TYPE

          end

Replace_html and render :update

This was one of the most complex changes we had to make. The replace_html method is no longer supported on Rails 3. Most of the dynamic behavior in Teixo’s frontend comes from this Rails feature so we cannot go without it. We can’t afford changing Teixo’s frontend behavior at this moment (it will be a huge project in a later phase of the update) so we implemented our own replace_html based on jQuery (the js library we already had in Teixo).

Our definition was included in jquery_helper.rb and looks like this:

            def replace_html(element_id, html)

          insert_html(:html, element_id, html)

            end

            def insert_html(position, element_id, html)

          insertion = position.to_s.downcase

          insertion = 'append' if insertion == 'bottom'

          insertion = 'prepend' if insertion == 'top'

              # Adds immediate timeout to execute complete and success callbacks

          %Q(

             setTimeout(function () {

               jQuery('##{element_id}').#{insertion}('#{escape_javascript(html)}');

               $(document).trigger('ajax:replaced', jQuery('##{element_id}'));

             });

          )

            end

So replace_html receives the same parameters as before, a dom element id and the html to change the content of the dom element with. But we should also change how this response was sent to the browser, no more render :update were useful, we have to develop a new way, our generate_js_response that simply renders the js passed as parameter:

          def generate_js_response(&block)

            render "shared/js_response.js", :locals => {:js_content => block}

          end

And the shared/js_response.js 

          <% content = [] %>

          <% self.instance_exec(content, &js_content)%>

          <%= content.join %>

So, finally we changed our uses of replace_html from this:

   render :update do |page|

       page.replace_html('link_add_bank_account', render(:partial => 'remote_form'))

   end

To this:

   generate_js_response do |page|

       page << replace_html('link_add_bank_account', render(:partial => 'remote_form'))

   end

Only render :update has to be changed, not the replace_html, so the work to change the 300 occurrences of replace_html was more affordable.

Mailers

Rails 3 made several changes on mailing system so we had to make some relevant changes, and not all of them were on the documentation:

  • No more Mailer.deliver_xxxxx() methods. We should change to Mailer.xxxx().deliver
  • The way to compose and use partials and templates changed. We had to fix issues related to:
  1. How to name partials (in our case from .text.html.haml to .html.haml) used in mailing composition.
  2. All the locals used in a mailer partial should be passed, even they are nil.
  • Composing emails with attachments changed even more. It was needed to attach de documents and define the email as one with mixed content.

            def set_multipart_structure(mixed_mail)

          if attachments.any?

               # Set the message content-type to be 'multipart/mixed'

               mixed_mail.content_type 'multipart/mixed'

               mixed_mail.header['content-type'].parameters[:boundary] = mixed_mail.body.boundary

               # Set Content-Disposition to nil to remove it - fixes iOS attachment viewing

               mixed_mail.content_disposition = nil

          end

            end

Clone and dup

Rails clone method does not exist in Rails 3 (it comes back in Rails 4) so we changed call to it with dup method.

errors.add_to_base

Errors objects no longer support add_to_base. We had to change these calls to errors.add :base.

Boolean params

Params with boolean values inside where magically managed in Rails 2, The string ‘true’ was automatically parsed to true, and ‘false’ to false. With the arrival of Rails 3 this behaviour changed so  we had to define a parser and use it on the different params needed:

          def parse_boolean(str_bool)  

          ActiveRecord::ConnectionAdapters::Column.value_to_boolean(str_bool)

          end

Another option would be to review al the forms using boolean values but this was an easier way.

Link_to_remote

In Rails 2, it was possible to use “link_to_remote ... :update => 'id'” for replacing the content of $('#id') automatically. It’s not possible within Rails 3 so we had to adapt our wrapper of link_to_remote on actions_link_helper.rb:

            def link_to_remote(name, options = {}, html_options = nil)

          .... // Custom implementation 

               //adding the relevant part for making the html replacement work

          data_replace = options.delete(:update)

          html_options = html_options.merge(:"data-replace" => "##{data_replace}") if data_replace.present?

          data_complete = options.delete(:complete)

          html_options = html_options.merge(:"data-complete" => "#{data_complete}") if data_complete.present?

          data_before = options.delete(:before)

          html_options = html_options.merge(:"data-before" => "#{data_before}") if data_before.present?

          data_error = options.delete(:error)

          html_options = html_options.merge(:"data-error" => "#{data_error}") if data_error.present?

          link_to(name, path, options.merge(:remote => true).merge(html_options))

            end

and define this helper in our application.js to make it work:

          $('[data-remote][data-replace]')

            .data('type', 'html')

            .live('ajax:success', function(event, data) {

          var $this = $(this);

          $($this.data('replace')).html(data);

          $this.trigger('ajax:replaced');

            });

Finder_sql

Relations with a finder_sql had to be changed by the syntax change to a proc on Rails 3. 

From:

          has_many :formations, :class_name => "Formation",

            :finder_sql => %q(SELECT DISTINCT ...)

To:

          has_many :formations, :class_name => "Formation",

            :finder_sql => proc {"SELECT DISTINCT ...”}

attributes=

The behavior of this method changed from Rails 2 to 3. We had a several uses of this method where the hash used had virtual params or non existing attributes, this raises an error on Rails 3. So we had to monkeypatch this method, for filtering the received hash and allow only existing attributes. The monkeypatch looks like this:

           class ActiveRecord::Base

             alias_method :super_attributes=, :attributes=

             def attributes=(hash = {})

            hash ||= {}

            self.super_attributes = hash.select{|k,v|

          self.class.column_names.member?(k.to_s) || k.to_s.match(/_attributes\z/) || self.respond_to?(:"{k}=")}

             end

           end

Submit_to_remote

The helper method submit_to_remote is no longer available on Rails 3. So we had to define one own in application_helper.rb

            def submit_to_remote(name, value, options = {})

          html_options = options.delete(:html) || {}

          submit_tag value, options.merge(html_options).merge(:id => name)

            end

In some cases this was not enough so we had to replace it with a specific link_to_remote.

Errors.full_message

Teixo use the errors.full_message to display the problems a page form has (required fields, wrong format, length issues, etc.). But the behavior has changed in Rails 3 and the nested objects errors are not included in the full_message. So we had to monkeypatch it, as you can see:

          class ActiveModel::Errors

            alias_method :old_full_message, :full_message

            def full_message(attribute, message)

          if (splitted_attribute = attribute.to_s.split(".")).count > 1

             translated_attribute = if @base.send(splitted_attribute.first).respond_to?(:any?)

               @base.send(splitted_attribute.first).first.class.human_attribute_name(splitted_attribute.second)

             else

               @base.send(splitted_attribute.first).class.human_attribute_name(splitted_attribute.second)

             end

             old_full_message(translated_attribute, message)

          else

             old_full_message(attribute, message)

          end

            end

          end

Caching views

The use of cache in views changed slightly and we had to struggle a lot to find out what was happening. We had some partials which used cache defined in a special controller, with code like this (action_cache_key is a method to get current partial cache key):

          Rails.cache.fetch(action_cache_key(opts)) do

          render opts[:action]

          end

If the partial is not cached the result was not rendered, but the second time we accessed this page/partial, it was cached fine and was rendered fine.

Finally we notice that we have to return always a String inside the Rails.cache.fetch block:

          Rails.cache.fetch(action_cache_key(opts)) do

            block.call if block_given?

            render(opts[:action]).join("")

          end

readonly(false)

Many object relations in Teixo were loaded with attributes chaining and later updated somehow. With the update to Rails 3 those loads were by default marked as readonly, So further attempts to update those related objects were failing. We had to manually check those relations and mark these loads as readonly(false). For example:

          outgoing_line.outgoing.update_me

Changed to

          outgoing_line.outgoing(:readonly => false).update_me

Reload

Reload method if called on a deleted object on Rails 2 simply returned nil, on Rails 3 it raises an Exception. We had to review some callbacks that did reload and failed when runned after a delete.

render_optional_error_file

This method no longer exist so whe changed part of the error control to use config/routes.rb instead of this mechanism.

Flash errors

The behavior of flash messages feature had changed between Rails 2.3 and 3.2 had  changed. The messages are no longer available through redirects so we had to do some flash.keep on certain callbacks and filters.

Log_error

Rails 2 has a default mechanism to handle errors on controllers. Whenever an error is raised Rails 2 controllers called to a method named log_error. Since Rails 3 that is no longer true. We had to configure on our base controller a explicit call on this method whenever a error is raised:

          class ApplicationController < ActionController::Base

            rescue_from StandardError, with: :log_error

Time.zone.parse

In some places throughout the app (mostly on reports) users can choose to filter data by dates, usually for full month length, so we used Time.zone.parse to parse partial date param strings to get data time boundaries. So users who wanted a report of certain data for the month of July sent params like date1_str: "/07/2022" and date2_str: "/08/2022". In Rails 2 it worked like this:

          > date1 = Time.zone.parse("/07/2022")

          Fri, 01 Jul 2022 00:00:00 CEST +02:00

But when we changed to Rails 3 the behavior was:

          > date1 = Time.zone.parse("/07/2022")

          Sun, 31 Jul 2022 00:00:00 CEST +02:00

So un Rails 3 users had data for the month of august. We fixed it adding a call to beginning_of_month when needed.

Respond_to

In Rails 3, respond_to works differently than Rails 2 so we had to review and test different occurrences. 

Also this behavior changes when no Accept header is attached to the request and this was affecting several clients. In Rails 2 default response type in this case was the same as the content-type on the request. We had to implement a before_filter on API controllers to add a default Accept header if none was declared, so that Teixo will work as in Rails 2.

            def add_accept_header_if_necesary

          if request.headers['HTTP_ACCEPT'].blank?

               Rails.logger.info("ApplicationController: Accept header empty for #{request.host}/#{request.path}")

                 if request.headers['CONTENT_TYPE'].present?

                     new_format = Mime::Type.lookup(request.headers['CONTENT_TYPE'])

               if new_format.present?

                 request.format = new_format.ref

               end

               end

          end

            end

Render with a proc {}

In Rails 3.2 it is not possible to use a render text with a proc as an argument. Fortunately we had only a few uses of this behavior.

Disabled form fields

On Rails 2 disabled form fields act as readonly fields, so data is sent to the server when the form is submitted. From Rails 3 onwards disabled fields are not sent to the server. So we had to review those disabled fields and mark it as readonly instead, or keep them disabled, depending on the case.

BigDecimal gem

We had to add the gem BigDecimal (to continue using this type). We also had to keep it in a compatible version with Rails 3  because it uses  BigDecimal.new for initializing attributes of this class and recent versions of the gem do not support this behavior.

Fecha
16/6/23
Categoría
Tecnología
Etiquetas
Compartir en
NOTICIAS

Suscríbete a la newsletter

¿Quieres recibir nuestras noticias en tu bandeja de entrada?