V2 - Ruby on Rails Tips and Tricks
Introduction
This pages includes tips and tricks for Ruby on Rails Development.
Decreasing Startup Times
It is very annoying that rake, rails c, etc, ar starting very slowly. Use rails-sh to solve this problem. Install it as root:
$ gem install rails-sh --no-ri
And use it as a shell:
$ rails-sh rails-sh(v2p0)> rake db:reset rails-sh(v2p0)> rake db:load_tree model=Folder file=/tmp/v2/2014.08.19.Folder.txt
Asset Pipeline
General documentation o the Asset Pipeline is available at:
The following applies to applications running in the production environment
Icon Display
After a production deployment, you need to check to ensure that icons are displayed correctly.
TODO: This should be part of a basic post-install smoke test. A smoke test should probably also look for fatal errors and missing file errors in the log.
Rails Applications Deployed Sub-DIrectories
If a Rails application is served from a sub-directory (e.g. v2.softxs.ch/v2p0) then you need a line like the following in config/environments/production.rb
config.action_controller.relative_url_root = '/v2p0'
If this line is missing, then image asset urls will be like :
http://v2.softxs.ch/assets/glyphicons-halflings-ab3144065a860d198f1d7d9a4882640c.png
Instead of:
http://v2.softxs.ch/v2p0/assets/glyphicons-halflings-ab3144065a860d198f1d7d9a4882640c.png
The following may be an additional way of setting a relative path (not tested):
RAILS_RELATIVE_URL_ROOT="/v2p0" bundle exec rake assets:precompile
Getting a Production Rails System to Recognize Code Changes
Due to caching, nothing happens when you change .erb (and probably other) files. In order to get the rails server to recognize changes run the following command:
touch tmp/restart.txt
You have to run the following command to get Rails to rebuild it's cached assets (stored in public/assets):
rake assets:precompile
To delete all precompiled assets: (this basically does a 'rm -rf public/assets')
rake assets:clean
In addition, the cache, located in tmp/cache, can be cleared with the folloring command (this basically does a 'rm -rf /tmp/cache/*'):
rake tmp:cache:clear
It is unlikely that you will need to run this command.
After running commands that pre-compile or clear assets, or clear the cache, don't forget to run the touch tmp/restart.txt command.
Production Asset Processing
You have two choices for asset compilation, which is defined in the file config/environments/{development|test|production}.rb
config.assets.compile = true # The setting for development and test config.assets.compile = false # The setting for production
Note that if you set production to compile assets automatically (the non-default true setting above), then you also have to change config/application.rb:
For config.assets.compile = true, e.g. 'lazy' compilation
# If you precompile assets before deploying to production, use this line # Bundler.require(*Rails.groups(:assets => %w(development test))) # If you want your assets lazily compiled in production, use this line Bundler.require(:default, :assets, Rails.env)
For config.assets.compile = false, e.g. pre-compilation
# If you precompile assets before deploying to production, use this line Bundler.require(*Rails.groups(:assets => %w(development test))) # If you want your assets lazily compiled in production, use this line # Bundler.require(:default, :assets, Rails.env)
If you select fase, then you need to pre-compile assets. Also if you empty the public/assets directory you need to pre-compile assets again. This is explained in the following article:
You pre-compile assets with the following command:
rake assets:precompile
Note that in 'real production' we do not want config.assets.compile = true, as it causes a 'Sprockets call' for each requested assets file to check if the cache is up to date. See:
You can display the asset paths, the list of all directories (including images, javascripts and stylesheets directories) from which assets will be compiled in the Rails console, using the following command:
irb(main):003:0> Rails.application.config.assets.paths => ["/v01/local/www/rails/v2p0-app/app/assets/images", "/v01/local/www/rails/v2p0-app/app/assets/javascripts", "/v01/local/www/rails/v2p0-app/app/assets/stylesheets", "/v01/local/www/rails/v2p0-app/lib/assets/images", "/v01/local/www/rails/v2p0-app/vendor/assets/javascripts", "/usr/local/lib/ruby/gems/1.9/gems/jquery-rails-2.1.4/vendor/assets/javascripts", "/usr/local/lib/ruby/gems/1.9/gems/bootstrap-sass-2.2.2.0/vendor/assets/images", "/usr/local/lib/ruby/gems/1.9/gems/bootstrap-sass-2.2.2.0/vendor/assets/javascripts", "/usr/local/lib/ruby/gems/1.9/gems/bootstrap-sass-2.2.2.0/vendor/assets/stylesheets", "/usr/local/lib/ruby/gems/1.9/gems/coffee-rails-3.2.2/lib/assets/javascripts", "/usr/local/lib/ruby/gems/1.9/gems/the_sortable_tree-2.3.0/app/assets/images", "/usr/local/lib/ruby/gems/1.9/gems/the_sortable_tree-2.3.0/app/assets/javascripts", "/usr/local/lib/ruby/gems/1.9/gems/the_sortable_tree-2.3.0/app/assets/stylesheets"]
Note that assets are taken from all the gems that are used by the application.
Bootstrap CSS Files
When using the bootstrap-sass gem, the bootstrap.css and bootstrap-responsive.css files are automatically compiled as assets, provided that app/assets/stylesheets/custom.css.scss contains:
@import "bootstrap"; @import "bootstrap-responsive";
You don't need derectives in app/assets/stylesheets/application.css. E.g. the following is unnecessary:
*= require bootstrap *= require bootstrap-responsive
Note that you don't need a copy of bootstrap-responsive.css in your codebase in app/assets (or lib/assets or vendor/assets) it is automatically included from the gem.
Testing
Testing with RSpec, Capybara and Selenium
Transaction Problems
- Rspec runs all test examples in a database transaction as default, i.e. we can change in the database everything, it will not disturb any other tests, the database is
rolled back after all test example. So we can be sure that every data prepared in the before :all block is available for al test unchanged.
However if we are using the Selenium WebDriver, because we need JavaScript in the test, the WEB server of the Selenium WebDriver is running in different thread as the rspec. This causes two problems:
Any data preparation in the before block (e.g. ba calling FactoryGirl.create) is not seen from the Selenium WebDriver.
Any data changes in the test data base, caused by activity in the Selenium WebDriver remains in the database and will not rolled back.
There is a solution, mentioned on Capybara's github page,
- to force to use the same transaction for all threads. I tried, it really solves both problems above, but the following error message comes up irregularly during
the test: ActiveRecord::StatementInvalid: Mysql2::Error: This connection is still waiting for a result, try again once you have the result. So this solution is unusable,as mentioned also Capybara's page: "This may have thread safety implications and could cause strange failures, so use caution with this approach". The other problem with this, that Spork doesn't run with it at all.
- to force to use the same transaction for all threads. I tried, it really solves both problems above, but the following error message comes up irregularly during
There is a DatabaseCleaner gem mentioned often on the Net, but it is not a real solution. As default it does the same as rspec does itself, i.e. test can be
surrounded by database transactions, it doesn't help anything. If the DatabaseCleaner is used in truncate or delete mode, it will remove also all data prepared in the before :all block, so it can not be used again.
- There is a solution for the first problem (but not not for the second one). It is possible to switch off transaction handling around the tests. Use
self.use_transactional_fixtures for it. But it means, that the database should be cleaned-up in the after block of that test, i.e. change back everything what can disturb other tests, first of all, all data set in the before all block. This partial solution what we can use, and see the example here (a note file is detached in the test and attached again at the end):
describe "Detach Task-Note File", :js => true do describe "Without Authorizations" do self.use_transactional_fixtures = false before do visit local_login_path login_as_user @user_root find '#v2_login_completed' # waits for login visit task_path @root_task open_all_blocks #### opens all closable blocks on the GUI (see spec/support/open_block.rb) # press detach button find("a.btn[href^='#{file_asset_path @root_note_file1}']", text: I18n.t(:button_detach)).click wait_for_alert.accept #### accept the alert box (see spec/support/wait_for_alert.rb) wait_for_ajax #### waits for an AJAX call completion (see spec/support/wait_for_ajax.rb) end after do without_access_control do file_asset = FileAsset.find(@root_note_file1.id) file_asset.resource = @root_note file_asset.save! end end # Links to @root_note_file1 should disappear it { should_not have_css("a.btn[href^='#{file_asset_path @root_note_file1}']") } end # Without Authorizations end # Detach Task-Note File
Tips
- See all tips as comments (####) in the example code below, and above.
#### use :js => true to start the web driver by rspec, default is Selenium describe "Create with all required fields filled", :js => true do before do visit local_login_path #### login login_as_user @user_root #### see spec/support/request_macros.rb find '#v2_login_completed' #### waits for completion of ajax login visit new_revision_file_asset_path @root_rev choose "Revision File" select "Publish", from: "file_asset[file_type_ref_id]" click_button "Next" login_as_user other_user #### change user without leaving the page find '#v2_login_completed' #### waits for completion of ajax login end it "redirects back to the revision" do page.find("#upload_upload", visible:false).set(path1) should_not have_error_message() should have_selector("div#rev-#{@root_rev.id}-detail") #### "current_path" doesn't wait, so insert it after other "have..." test current_path.should == revision_path( @root_rev ) end describe "Save should create a FileAsset" do #### Selenium runs in a different thread, do not use transaction, otherwise the "change" below will not recognize any change in count self.use_transactional_fixtures = false #### Unfortunately "sleep 1" is required subject { lambda { page.find("#upload_upload", visible:false).set(path1); sleep 1 } } it { should change(@root_rev.file_assets, :count).by(1) } end end
Testing JQuery File Upload with Capybara
To achieve, that capybara allows to set file in the file input field the copy the #{GEMDIR}/jquery-fileupload-rails-0.4.1/vendor/assets/stylesheets/jquery.fileupload-ui.scss file into app/assets/stylesheets/jquery.fileupload-ui.scss.erb and change two lines, i.e. the translate will not be effective in test mode:
filter: alpha(opacity=0);<% unless Rails.env.test? %> -moz-transform: translate(-300px, 0) scale(4);<% end %>
Otherwise the following line will report "NonVisibleError":
subject { lambda { page.find("#upload_upload", visible:false).set(path1); sleep 1 } }
Testing and Folder Select
The folder select input in test mode can be set to contain generally all folders to allow to test authorizations, even with simulating hacking. E.g. app/views/file_assets/_fields.html.erb
<% if Rails.env.test? %> <% old_ignore_access_control = Authorization.ignore_access_control %> <% Authorization.ignore_access_control true %> <% end %> <%= render 'folders/folder_select', { f: f, object: @file_asset, roles: [:root, :admin, :update ] } %> <% if Rails.env.test? %> <% Authorization.ignore_access_control old_ignore_access_control %> <% end %>
Internationalization
The localization is set per session in app/controllers/application_controller.rb in method set_user_localization. The setting is taken currently from the configuration, but could be taken from the user's setting later on.
Date, Date Format, JQuery Date-Picker
The gem 'bootstrap-datepicker-rails' is used for all date fields in the system. For simple_form fields the DatePickerInput class is defined in app/inputs/date_picker_input.rb. It can be utilized with the "as: :date_picker" option, e.g.:
<%= f.input :min_date, input_html: { :class => 'input-small' }, :as => :date_picker %>
For ActiveForm::FormBuilder fields the method datepicker_field is defined, e.g.:
<%= f.datepicker_field :min_date, { class: 'input-small', id: nil } %>
These methods add "datepicker" the HTML class of the input field and set the HTML value to the localized value of the current attribute if any.
The global javascript function v2_set_datepicker (defined in app/views/shared/_datepicker.js.erb and ) sets the HTML spaceholder value to the 'date.datepicker.format' and sets all datepicker options. This is the place where to change datepicker options. This function can be used also in unobtrusive javascripts also to set datepicker behaviour for dinamically added fields.
If a localized date string must be used in SQL, e.g in AREL where clause, it must be converted to ISO date format. Use Util.i18n_date_to_iso for is.
The format of the date is defined in the locale file, e.g. config/locales/en.yml (currentl the ISO format is defined, but it can be changed):
date: # datepicker.format and formats.default must correspond to each other, # see app/inputs/date_picker_input.rb datepicker: format: 'yyyy-mm-dd' formats: default: '%Y-%m-%d'
Delayed Jobs
Using ''render_to_string'' in Delayed Jobs
Delayed jobs can schedule model methods, while render_to_string is available in controllers and views. The trick is: create a controller instance in the model and call a controller method to render the view. E.g.
app/model/transmittal.rb:
def transmit # prepare cover letter as HTML attachment letter_string = TransmittalsController.new.create_cover_letter self ... end handle_asynchronously :transmit
app/controller/transmittals_controller.rb
# Creates transmittal cover letter, called from Transmittal.transmit because # render_to_string is not available in model def create_cover_letter transmittal render_to_string "cover_letter", formats: [:html], layout: false, locals: { transmittal: transmittal } end
Per Request Variables
Using of class variables (@@variable) in Ruby on Rails can cause surprises because they will live forever, until the server is restarted or the garbage collector removes the class. If we need per request storage, e.g. to save same data created many time in a single request, we can use RequestStore from the gem request_store. We can store everything in the hash RequestStore.store and it's life time will be guaranteed only the current request. See examples in app/lib/dyn_classifications_associations.rb, app/model/folder.rb.
Mixin Class Methods
A module can be used the define instance and even class methods:
module Foo def self.included base base.send :include, InstanceMethods base.extend ClassMethods end module InstanceMethods def bar1 'bar1' end end module ClassMethods def bar2 'bar2' end end end class Test include Foo end Test.new.bar1 # => "bar1" Test.bar2 # => "bar2"
See module DynClassificationsAssociations defined in app/lib/dyn_classifications_associations.rb and used in more models.