Popular mistakes when using nested forms
Here are some popular mistakes when using nested forms:
- You are using
fields_forinstead ofform.fields_for. - You forgot to use
accepts_nested_attributesin the containing model. Rails won't complain, but nothing will work. In particular,nested_form.objectwill benil. - The
:reject_ifoption lambda in youraccepts_nested_attributescall is defined incorrectly. Raise the attributes hash given to your:reject_iflambda to see if it looks like you expect. - If you are nesting forms into nested forms, each model involved ne...
Writing a README for a project
Rails applications and ruby gems should have a README that gives the reader a quick overview of the project. Its size will vary as projects differ in complexity, but there should always be some introductory prose for a developer to read when starting on it.
Purpose
That's already the main purpose of a project README: Give a new developer a quick overview of the project. In sketching this outline, the README should notify the reader of any peculiarity he needs to know of.
Remember that in a few months, you'll be a kind of "new ...
Migrating from Elasticsearch to Opensearch: searchkick instructions (without downtime!)
General
A general overview about why and how we migrate can be found under Migrating from Elasticsearch to Opensearch
This card deals with specifics concerning the use of searchkick.
Step 1: Make Opensearch available for Searchkick
In your Gemfile
# Search
gem 'searchkick' # needs to be > 5, to use Opensearch 2
gem 'elasticsearch'
gem 'opensearch-ruby'
in config/initializers/searchkick.rb (or wherever you have configured your Searchkick settings) add:
SEARCHKICK_CLIENT_T...
When you want to format only line breaks, you probably do not want `simple_format`
For outputting a given String in HTML, you mostly want to replace line breaks with <br> or <p> tags.
You can use simple_format, but it has side effects like keeping some HTML.
If you only care about line breaks, you might be better off using a small, specialized helper method:
def format_linebreaks(text)
safe_text = h(text)
paragraphs = split_paragraphs(safe_text).map(&:html_safe)
html = ''.html_safe
paragraphs.each do |paragraph|
html << content_tag(:p, paragraph)
end
html
end
Full di...
Clean up application servers when deploying
Our development process makes us deploy very often. As the number of releases grows, junk clogs up the hard drive of our application servers:
- Old release code
- Old
tmpfolders with compiled view templates etc. - Precompiled assets (Javascripts, images...) that no longer exist. When using the asset pipeline, Capistrano will symlink the
public/assetsdirectory toshared/assets. This is cool since we can still serve previous assets after a new release, in the window where browser caches might still have references to old assets. But i...
ActiveRecord: String and text fields should always validate their length
If you have a :string or :text field, you should pair it with a model validation that restricts its length.
There are two motivations for this:
- In modern Rails, database types
:stringand:textno longer have a relevant size limit. Without a validation a malicious user can quickly exhaust the hard drive of your database server. - In legacy Rails (or database schemas migrated from legacy Rails), database types
:stringand:texthad a database-side length constraint. When the user enters a longer string, the ActiveRecord valida...
Eager-loading polymorphic associations
To avoid n+1 queries, you want to eager-load associated records if you know you need to access them later on.
The Rails docs say:
Eager loading is supported with polymorphic associations.
This is true, but has some caveats.
Example
Consider the following models:
class Image < ActiveRecord::Base; end
class Video < ActiveRecord::Base; end
class PageVersion < ActiveRecord::Base
belongs_to :primary_medium, polymorphic: true # may be Image or Video
end
class Page < ActiveRecord::Base
belongs_to ...
Beware of params with non-string values (nil, array, hash)
Recent rails security updates have shown that people make incorrect assumptions about the possible contents of the params hash.
Just don't make any! Treat it as what it is: potentially unsafe user input. For example:
/pages/edit?foo= --> params == {:foo => ""}
/pages/edit?foo --> params == {:foo => nil}
/pages/edit?foo[] --> params == {:foo => [nil]} # at least in older rails 3 and in rails 2.x
Be especially wary about stuff like
User.find_by_password_reset_token(params[:password_reset_token])
I...
The many gotchas of Ruby class variables
TLDR: Ruby class variables (@@foo) are dangerous in many ways. You should avoid them at all cost. See bottom of this card for alternatives.
Class variables are shared between a class hierarchy
When you declare a class variable, it is shared between this and all descending (inheriting) classes. This is rarely what you want.
Class variables are bound at compile-time
Like unqualified constants, class variables are bound to your current scope *whe...
What we know about PDFKit
What PDFKit is
- PDFKit converts a web page to a PDF document. It uses a Webkit engine under the hood.
- For you as a web developer this means you can keep using the technology you are familar with and don't need to learn LaTeX. All you need is a pretty print-stylesheet.
How to use it from your Rails application
- You can have PDFKit render a website by simply calling
PDFKit.new('http://google.com').to_file('google.pdf'). You can then send the...
Handy: A regex that validates all valid email addresses (give or take) - Axon Flux // A Ruby on Rails Blog
/^([\w!#$%&'*+-/=?^`{|}~]+.)*[\w!#$%&'*+-/=?^`{|}~]+@((((([a-z0-9]{1}[a-z0-9-]{0,62}[a-z0-9]{1})|[a-z]).)+[a-z]{2,6})|(\d{1,3}.){3}\d{1,3}(:\d{1,5})?)$/i
Escape a string for transportation in a URL
To safely transport an arbitrary string within a URL, you need to percent-encode characters that have a particular meaning in URLs, like & or =.
If you are using Rails URL helpers like movies_path(:query => ARBITRARY_STRING_HERE), Rails will take care of the encoding for you. If you are building URLs manually, you need to follow this guide.
Ruby
In Ruby, use CGI.escape:
# ✅ good
CGI.escape('foo=foo&bar=bar')
=> "foo%3Dfoo%26bar%3Dbar"
Do not ever use `URI.en...
RSpec: Increase readability with super_diff
When handling nested hashes the RSpec output is often hard to read. Here the gem super_diff could help.
Add super_diff to your project
- Add
super_diffto your Gemfile:
gem 'super_diff'
- Require it in your
spec_helper.rb
require 'super_diff/rspec' # For Rails applications you can replace this with 'super_diff/rspec-rails'
-
Customize colors in
spec/support/super_diff.rb
SuperDiff.configure do |config|
config.ac...
Preloaded associations are filtered by conditions on the same table
When you eagerly load an association list using the .include option, and at the same time have a .where on an included table, two things happen:
- Rails tries to load all involved records in a huge single query spanning multiple database tables.
- The preloaded association list is filtered by the
wherecondition, even though you only wanted to use thewherecondition to filter the containing model.
The second case's behavior is mostly unexpected, because pre-loaded associations usually don't care about the circumstances under whi...
RSpec: Applying stubs only within a block
When you mocked method calls in RSpec, they are mocked until the end of a spec, or until you explicitly release them.
You can use RSpec::Mocks.with_temporary_scope to have all mocks applied inside a block to be released when the block ends.
Example:
RSpec::Mocks.with_temporary_scope do
allow(Rails).to receive(:env).and_return('production'.inquiry)
puts Rails.env # prints "production"
end
puts Rails.env # prints "test"
Note that, when overriding pre-existing mocks inside the block, they are not reverted to the previously ...
Livereload + esbuild
Getting CSS (and JS) live reloading to work in a esbuild / Rails project is a bit of a hassle, but the following seems to work decently well.
We assume that you already use a standard "esbuild in Rails" setup, and have an esbuild watcher running that picks up your source code in app/assets and compiles to public/assets; if not change the paths below accordingly.
Basic idea
We will
- use the
guard-livereloadgem as the livereload server (which send updates to the browser), - use the
livereload-jsnpm package in the browser to con...
Using FactoryBot in Development
If you need dummy data to play around with in development, it's often faster to reuse your existing factories instead of using the UI or creating records in the Rails console. This approach saves time and gives you useful defaults and associations right out of the box.
You can use FactoryBot directly in the Rails console like this:
require 'factory_bot_rails' # Not needed if the factory_bot_rails gem is in the :development group
FactoryBot.create(:user)
You can also apply traits or override attributes:
FactoryBot.create...
Tod: A Gem for handling daytime without a date
Tod is a gem for working with daytimes. That's a tuple of (hour, minute second) without a day, month or year.
Another additional gem?
Thus SQL has a time datatype for storing time of day in the format hh:mm:ss, neither Ruby nor Rails themselves offer an elegant way to deal with day times.
Time and DateTime both handle daytime values AND calendar date, using them to only store the time of day will end in inconsistent and thus confusing data, e. g. Time.new will initialize with the current Time in your Timezone, `DateTime.n...
Don't mix Array#join and String#html_safe
You cannot use Array#join on an array of strings where some strings are html_safe and others are not. The result will be an unsafe string and will thus be escaped when rendered in a view:
unsafe_string = '<span>foo</span>'
safe_string = '<span>bar</span>'.html_safe
[unsafe_string, safe_string].join(' ') # will incorrectly render as '<span>foo</span><span&t;bar</span>'
Bad
The solution is not to call html_safe on the joined array and if you thought it would be, you [don't understand how XSS prot...
Enabling YJIT
YJIT is Ruby's default just-in-time compiler. It is considered production-ready since Ruby 3.2 (source).
To activate YJIT you need two steps:
- Your
rubybinary needs to be compiled with YJIT support. - You need to enable YJIT.
Getting a Ruby with YJIT support
We usually install Ruby with tools like rbenv or asdf. This compiles the ruby binary from the source code. Support for YJIT will be automatically added during this compilation...
Capistrano task to edit staging / production credentials
When using Rails credentials, you will edit the encrypted credentials for staging or production environments from time to time. To do that you need the secret key which should only live on the servers.
Do not download these key files to your local dev environment. They are sensitive and must not be stored on your machine.
Instead, put the attached capistrano task into lib/capistrano/tasks/ of your application. It expects environment specific keys to live in :shared_path/config/credentials/:stage.key. If you have a single master.key...
Carrierwave: How to attach files in tests
Attaching files to a field that is handled by Carrierwave uploaders (or maybe any other attachment solution for Rails) in tests allows different approaches. Here is a short summary of the most common methods.
You might also be interested in this card if you see the following error in your test environment:
CarrierWave::FormNotMultipart:
You tried to assign a String or a Pathname to an uploader, for security reasons, this is not allowed.
If this is a file upload, please check that your upload form is multipart encoded.
Factor...
RSpec: Where to put shared example groups
Shared example groups are a useful RSpec feature. Unfortunately the default directory structure generated by rspec-rails has no obvious place to put them.
I recommend storing them like this:
spec/models/shared_examples/foo.rb
spec/models/shared_examples/bar.rb
spec/models/shared_examples/baz.rb
spec/controllers/shared_examples/foo.rb
spec/controllers/shared_examples/bar.rb
spec/controllers/shared_examples/baz.rb
To ma...
Use find_in_batches or find_each to deal with many records efficiently
Occasionally you need to do something directly on the server -- like having all records recalculate something that cannot be done in a migration because it takes a long time.
Let's say you do something like this:
Project.all.each(&:recalculate_statistics!)
Even though you may have been successful with this on your development machine or the staging server, keep in mind that production machines often hold a lot more records. Using all may just work, even with lots of records, but when you iterate over such records and fetch a...