Segunda parte del diario de Ricardo Vila, ingeniero de software en TEIMAS, actualizando Teixo a la última versión de Rails.
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.
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:
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
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.
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.
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
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.
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:
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
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 objects no longer support add_to_base. We had to change these calls to errors.add :base.
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.
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');
});
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 ...”}
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
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.
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
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
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 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.
This method no longer exist so whe changed part of the error control to use config/routes.rb instead of this mechanism.
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.
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
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.
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
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.
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.
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.