Maintaining custom application tasks in Rails
Here are some hints on best practices to maintain your tasks in larger projects.
Rake Tasks vs. Scripts
- The Rails default is using rake tasks for your application tasks. These live in
lib/tasks/*
. - In case you want to avoid rake for your tasks and just use plain ruby scripts, consider
lib/scripts/*
as folder.
Keeping tasks slim
For readability and testing it's easier to keep your tasks slim. We suggest to use folders inside the tasks
or scripts
folder.
Example for a task:
The slim task lib/tasks/gitlab.rb
:
Unpoly 3.7.1, 3.7.2 and 3.7.3 released
Version 3.7.0 broke some things in complex forms. Sorry for that. Concurrent user input is hard.
3.7.1
This change fixes two regressions for form field watchers, introduced by 3.7.0:
- When a change is detected while waiting for an async callback, prevent the new callback from crashing with
Cannot destructure property { disable } of null
. - When a change is detected while waiting for an async callback, the full debounce delay of that new change is honored.
...
CSS: Letting text flow around a round element
If you have an element with significant border-radius (e.g. 50%
for a circle) and you want inline content (i.e. text) to flow around it, do it like this:
- Place the element right before the text and
float: right
orfloat: left
- Tell the browser to take the
content-box
for the element's shape, i.e. without margin, padding and border.shape-outside: content-box
- Set the
margin
where you want it, e.g. 10px left and bottom - Set the
shape-margin
to the same size ...
Cucumber step to match table rows with Capybara
These steps are now part of Spreewald.
This note describes a Cucumber step that lets you write this:
Then I should see a table with the following rows:
| Bruce Wayne | Employee | 1972 |
| Harleen Quinzel | HR | 1982 |
| Alfred Pennyworth | Engineering | 1943 |
If there are additional columns or rows in the table that are not explicitely expected, the step won't complain. It does however expect the rows to be ordered as stat...
Pitfall: ActiveRecord callbacks: Method call with multiple conditions
In the following example the method update_offices_people_count
won't be called when office_id
changes, because it gets overwritten by the second line:
after_save :update_offices_people_count, :if => :office_id_changed? # is overwritten …
after_save :update_offices_people_count, :if => :trashed_changed? # … by this line
Instead write:
after_save :update_offices_people_count, :if => :office_people_count_needs_update?
private
def office_people_count_needs_update?
office_id_changed? || trashed_changed?
end
Or...
Copying validation errors from one attribute to another
When using virtual attributes, the attached trait can be useful to automatically copy errors from one attribute to another.
Here is a typical use case where Paperclip creates a virtual attribute :attachment
, but there are validations on both :attachment
and :attachment_file_name
. If the form has a file picker on :attachment
, you would like to highlight it with errors from any attribute:
class Note < ActiveRecord::Base
has_attached_file :attachment
validates_attachment_presence :a...
Rails: Using custom config files with the config_for method
You can use the config.x configuration in combination with config_for
to configure global settings for your Rails 4.2+ application.
Example
In your config/application.rb
assign the settings from e.g. config/settings.yml
as follows:
module FooApplication
class Application < Rails::Application
config.x.settings = config_for(:settings)
end
end
The config/settings.yml
might look as follows:
shared: &shared
email: info@example.com
...
Dockerfile and Heredocs
I recently did a quick research on how to better write down multiline statements like this:
# Dockerfile
RUN export DEBIAN_FRONTEND=noninteractive \
&& apt-get update \
&& apt-get dist-upgrade -y \
&& apt-get install --no-install-recommends --no-install-suggests -y \
ca-certificates \
curl \
unzip
It turns out, there is! Buildkit, the tool behind docker build
added support for heredoc strings in May 2021.
Now we could refactor the code above like this:
Browser debugging tricks
A list of clever debugging tricks. TOC:
- Advanced Conditional Breakpoints
- monitor() class Calls
- Call and Debug a Function
- Pause Execution on URL Change
- Debugging Property Reads
- Use copy()
- Debugging HTML/CSS
RubyMine: Real-time Collaborating in the IDE
RubyMine has a collaboration feature called "Code With Me". Using it, you can invite someone into your local editor to work together. This is nicer to the eyes and much more powerful than sharing code through some video chat.
How to
Getting started is really simple:
- Click the "add person" icon in the top-right editor corner (or hit Ctrl + Shift + Y) and select "Start Session".
- Choose permissions:
- "Read-only" lets others only watch you.
- "Edit files" is needed for pairing. Note that this allows remote partners to, well, ...
Ignore commits when git blaming
You can ignore certain commits when using git blame with the --ignore-revs-file
option. This is handy to ignore large rubocop commits or big renamings in your project. You can add and commit a .git-blame-ignore-revs
file in your project to track a list of commits that should be ignored.
# a list of commit shas
123...
456...
Use git blame with the --ignore-revs-file
option and ignore the SHAs specified in .git-blame-ignore-revs
.
git blame --ignore-revs-file .git-blame-ignore-revs
If you want to use this flag by def...
You should probably load your JavaScript with <script defer>
It is generally discouraged to load your JavaScript by a <script src>
tag in the <head>
:
<head>
<script src="app.js"></script>
</head>
The reason is that a <script src>
tag will pause the DOM parser until the script has loaded and executed. This will delay the browser's first contentful paint.
A much better default is to load your scripts with a <script src defer>
tag:
<head>
<script src="app.js" defer></script>
</head>
A deferred script has many useful properties:
- I...
How to: Context-dependent word expansion in RubyMine
One of the many useful features of TextMate is autocompletion of words. If I were in TextMate right now, I could write "au[tab]", and it would complete it to "autocompletion". RubyMine can do this, too. When you write a word (e.g. a variable name), just hit ALT + / repeatedly and it will offer all completions for the letters you typed. This action is called Cyclic Expand Word in RubyMine / IntelliJ IDEA.
This feature keeps you from mistyping variable names, saves you keystrokes and speeds up development. ~10 keystrokes to the price ...
Disable PostgreSQL's Write-Ahead Log to speed up tests
The linked article suggests an interesting way to speed up tests of Rails + Postgres apps:
PostgreSQL allows the creation of “unlogged” tables, which do not record data in the PostgreSQL Write-Ahead Log. This can make the tables faster, but significantly increases the risk of data loss if the database crashes. As a result, this should not be used in production environments. If you would like all created tables to be unlogged in the test environment you can add the following to your...
RSpec: be_true does not actually check if a value is true
Don't use be_true
to check if a value is true
. It actually checks if it anything other than nil
or false
. That's why it has been renamed to be_truthy
in recent RSpec versions.
The same thing holds for be_false
, which actually checks if a value is not "truthy".
If you want to check for true
or false
in RSpec 2, write this instead:
value.should == true
value.should == false
If you want to check for true
or false
in RSpec 3+, write this instead:
e...
ActiveRecord: When aggregating nested children, always exclude children marked for destruction
When your model is using a callback like before_save
or before_validation
to calculate an aggregated value from its children, it needs to skip those children that are #marked_for_destruction?
. Otherwise you will include children that have been ticked for deletion in a nested form.
Wrong way
class Invoice < ApplicationRecord
has_many :invoice_items
accepts_nested_attributes_for :invoice_items, :allow_destroy => true # the critical code 1/2
before_save :calculate_and_store_amount # the crit...
open-next-failure: An alias to speed up test debugging
Getting an entire test suite green can be a tedious task which involves frequent switches between the CLI that is running tests back to the IDE where its cause can be fixed.
The following bash aliases helped me speed up that process:
alias show-next-failure="bundle exec rspec --next-failure"
alias open-next-failure="show-next-failure || show-next-failure --format json | jq -r '.examples[0]' | jq '\"--line \" + (.line_number|tostring) + \" \" + .file_path' | xargs echo | xargs rubymine"
There is a lot going on above but the gist...
Capistrano 3: Running a command on all servers
This Capistrano task runs a command on all servers.
bundle exec cap production app:run cmd='zgrep -P "..." RAILS_ROOT/log/production.log'
Code
# lib/capistrano/tasks/app.rake
namespace :app do
# Use e.g. to grep logs on all servers:
# b cap production app:run_cmd cmd='zgrep -P "..." RAILS_ROOT/log/production.log'
#
# * Use RAILS_ROOT as a placeholder for the remote Rails root directory.
# * Append ` || test $? =1;` to grep calls in order to avoid exit code 1 (= "nothing found")
# * To be able to process ...
Regular tasks for long-running projects
When projects run for many years, they require special regular maintenance to stay fresh. This kind of maintenance is usually not necessary for small or quick projects, but required to keep long-running projects from getting stale.
You should be able to fit this into a regular development block.
Quarterly
Check which libraries need updating
As time goes by, libraries outdate. Check your software components and decide if any of it needs an update. Your main components (e.g. Ruby, Rails, Unpoly) should always be reasonably up to d...
Summarizing heredoc in Ruby and Rails
This card tries to summarize by example the different uses of heredoc.
- In Ruby
<<
vs.<<-
vs.<<~
- In Rails
strip_heredoc
vs.squish
strip_heredoc
should be used for a text, where you want to preserve newlines. (multi-line -> multi-line)
squish
should be used for a text, where you want to squish newlines. (multi-line -> one-line)
Ruby 2.3+
def foo
bar = <<~TEXT
line1
line2
line3
TEXT
puts bar.inspect
end
foo => "line1\nline2\nline3\n"
Read more: [Unindent HEREDOCs in Ruby 2.3](/m...
Databases don't order rows unless you tell them so
There is no such thing as a "default order" of rows in database tables.
For instance, when you paginate a result set: When using LIMIT
, it is important to use an ORDER BY
clause that constrains the result rows into a unique order. Otherwise you will get an unpredictable subset of the query's rows. You might be asking for the tenth through twentieth rows, but tenth through twentieth in what ordering? The ordering is unknown, unless you specified ORDER BY
.
In Rails, if you use Record.first
or Record.last
, it will default to orderin...
Gitlab: How to cancel redundant pipelines
In the Gitlab settings the flag Auto-cancel redundant pipelines is enabled by default. This auto-cancels jobs that have the interruptible
setting set to true
(defaults to false
e.g. to not cancel deploys by accident).
Consider to set the interruptible
flag for test jobs to reduce the load on your runners like in the following example .gitlab-ci.yml
:
rubocop:
interruptible: true
script:
- 'bundle exec rubocop'
rspec:
int...
CSS has a built-in clearfix
The need for clearfix hacks has been greatly reduced since we could layout with Flexbox or CSS Grid.
However, when you do need a clearfix, there's no reason to use a hack anymore. You can just give the clearing container display: flow-root
.
Difference between respond_to/format and params[:format]
To return non-HTML responses (like XLS spreadsheets), we usually use the
respond_to do |format|
format.xls do
# send spreadsheet
end
end
This is often, but not always the same as checking for params[:format] == :xls
, so don't rely on this when e.g. one format checks for authorization and the other doesn't.
params[:format]
is only set when a user explicitly puts a .xls
at the end of the URL.
The format.xls
block also responds when the user's browser requests the application/excel
MIME type.
If Internet Explo...