Today I Learned

20 posts about #rails

Rails 5.1 added :default option to belongs_to

From the changelog

image

Now if only someone would add it to has_one relationships. Tim says it's harder because then you're setting attributes on another object.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Getting Heroku Review Apps to Work

A few years ago I heard about a project called Fourchette, which facilitated setting up one Heroku app per pull request on a project (aka review apps). I remember being all like THAT'S FREAKING BRILLIANT! Then I went back to whatever I was doing and never did anything about it.

Well, this week I finally had the time and inclination to get review apps working on Heroku. The instructions are out there, but they gave me enough trouble that I figured I'd document the gotchas for posterity.

#1. Understand the app.json file, really

We already had a tiny app.json file that we had created in connection with getting Heroku CI to run our test suite. All it had was an environments section that looked like this:

"environments": {
  "test": {
     "env": {
      "DEBUG_MAIL": "true",
      "OK_TO_SEED": "true"
    },
    "addons":[
      "heroku-postgresql:hobby-basic",
      "heroku-redis:hobby-dev"
    ]

When I started trying to get review apps to work, I simply created a pull request, and followed the dashboard instructions for creating review apps, assuming that since we already had an app.json file that it would just work. Nope, not at all.

After much thrashing, what finally got me over the hump was understanding the purpose of app.json from first principles, which didn't happen until I read this description of the Heroku Platform API. App.json originally came about as a way to automate the creation of an entire Heroku project, not just a CI or Review configuration. It predates CI and Review Apps and has been in essence repurposed.

#2. Add all your ENV variables

The concept of ENV variables being inherited from the designated parent app really threw me for a loop at first. I figured that the only ENV variables needed to be declared in the env section of app.json would be the ones I was overriding with a fixed value. Wrong again.

After much trial-and-error, I ended up with a list of all the same ENV variables as my staging environment. Some with fixed values, but most just marked as required.

"env": {
    "AWS_ACCESS_KEY_ID": {
      "required": true
    },
    "AWS_SECRET_ACCESS_KEY": {
      "required": true
    },

This won't make sense if you're thinking that app.json is specifically for setting up Review Apps (see #1 above.)

#3. Understand the lifecycle, especially with regards to add-ons

After everything was mostly working (meaning that I was able to get past the build stage and actually access my web app via the browser) I still kept getting errors related to the Redis server being missing. To make a long story short, not only did I have to add it to the addons section, but I also had to delete the review app altogether and create it again, so that addons would be created. (Addons are not affected by redeployment.)

"addons":[
  "heroku-postgresql:hobby-basic",
  "heroku-redis:hobby-dev",
  "memcachier:dev"
],

In retrospect, I realize that the reason that was totally unclear is that my review apps Postgres add-on was automatically created, even before I added an addons section to app.json. (Initially I thought it was coming from the test environment.)

I still don't know if Postgres is added by default to all review apps, or inherited from the parent app.

4. Post deploy to the rescue

There's at least one thing you want to do once, every time a new review app is created, and that is to load your database schema. You probably want to seed data also.

"scripts": {
  "postdeploy": "OK_TO_SEED=true bundle exec rails db:schema:load db:seed"
}

As an aside, I have learned to put an OK_TO_SEED conditional check around destructive seed operations to help prevent running in production. This is especially important if you run your staging instances in production mode, like you should.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

How to setup Heroku Rails app to handle yarn.lock

One of the nicest features of Rails 5 is its integration with Yarn, the latest and greatest package manager for Node.js. Using it means you can install JavaScript dependencies for your app just as easily as you use Bundler to install Ruby gems.

Now one of the biggest problems you face when using any sort of Node package management is that the combinatorial explosion of libraries downloaded in order to do anything of significance.

Given that reality, you really do not want to add node_modules to your project's git repository, no more than you would want to add all the source code of your gems. Instead, you add node_modules to your .gitignore file.

Yarn adds a file to the root of your Rails app called yarn.lock. Today I learned that if you include the Node.js buildpack to your project on Heroku, it will recognize yarn.lock and install any required node modules for you. You just have to make sure that it runs first in the build chain.

heroku buildpacks:add --index 1 heroku/nodejs

Side note: If you use Heroku CI then you'll need to setup your test environment with the extra buildpack also by adding a new section to app.json.

"buildpacks": [
{ "url": "heroku/nodejs" },
{ "url": "heroku/ruby" }
]

Note that the nodejs buildpack expects a test script to be present in package.json. If you don't have one already, just add a dummy directive there. Almost anything will work; I just put an echo statement.

"scripts": {
    "test": "echo 'no tests in js'"
  },
Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Cloudflare Flexible SSL mode breaks Rails 5 CSRF

Putting this out there since I didn't find anything on StackOverflow or other places concerning this problem, which I'm sure I'm not the first to run into. CloudFlare is great, especially as a way to set-and-forget SSL on your site, along with all the other benefits you get. It acts as a proxy to your app, and if you set its SSL mode to Flexible then you don't have to have an SSL certificate setup on your server. This used to be a big deal when SSL certificates were expensive. (You could argue that since Let's Encrypt and free SSL certificates it's not worth using Flexible mode anymore.)

Anyway, I digress. The point of this TIL is that if you proxy https requests to http endpoint in Rails 5, you'll get the dreaded InvalidAuthenticityToken exception whenever you try to submit any forms. It has nothing to do with the forgery_protection_origin_check before action in ApplicationController.

The dead giveaway that you're having this problem is in your logs. Look for the following two lines near each other.

WARN -- : [c2992f72-f8cc-49a2-bc16-b0d429cdef20] HTTP Origin header (https://www.example.com) didn't match request.base_url (http://www.example.com)  
...
FATAL -- : [c2992f72-f8cc-49a2-bc16-b0d429cdef20] ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken): 
Aug 13 18:08:48 pb2-production app/web.1: F, [2017-08-14T01:08:48.226341 #4] FATAL -- : [c2992f72-f8cc-49a2-bc16-b0d429cdef20]    

The solution is simple. Make sure you have working SSL and HTTPS on Heroku (or wherever you're serving your Rails application.) Turn Cloudflare SSL to Full mode. Problem solved.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Run Puma in Single mode for development

Turns out how to switch between single and clustered modes of Puma is super unclear in the (little to non-existent) documentation. You'd think that setting WEB_CONCURRENCY to 1 would do it, but you actually have to set it to zero. Meaning you don't want to spin up any child processes.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

return while rendering partial collections

Ruby's next keyword only works in the context of a loop or enumerator method.

So if you're rendering a collection of objects using Rails render partial: collection, how do you skip to the next item?

Since partials are compiled into methods in a dynamically generated view class, you can simulate next by using an explicit return statement. It will short-circuit the rendering of your partial template and iteration will continue with the next element of the collection.

For example

# app/views/users/_user.haml
- return if user.disabled?
  %li[user]
    rest of your template...
Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Using default_url_options in RSpec with Rails 5

There's a long-standing bug in the integration of controller testing into RSpec that prevents you from easily setting default_url_options for your controller specs. As far as I can tell, it doesn't get fixed because the RSpec teams considers the problem a bug in Rails, and the Rails team does not care if RSpec breaks.

I'm talking about the issue you run into when you're trying to work with locale settings passed into your application as namespace variables in routes.rb like this:

scope "/:locale" do
    devise_for :users,  #...and so on

Today I learned that the inability to set a default :locale parameter can be maddening. Your specs will fail with ActionView::Template::Error: No route matches errors:

1) Devise::RegistrationsController POST /users should allow registration
     Failure/Error: %p= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token)

     ActionView::Template::Error:
       No route matches {"action":"show","confirmation_token":"pcyw_izS8GchnT-R3EGz","controller":"devise/confirmations"} missing required keys: [:locale]

The reason is that ActionController::TestCase ignores normal settings of default_url_options in ApplicationController or your config/environments/test.rb. No other intuitive attempt at a workaround worked either. Frustratingly, it took me around an hour to debug and come up with a monkeypatch-style workaround. The existing workarounds that I could find online are all broken in Rails 5.

So here it is:

# spec/support/fix_locales.rb
ActionController::TestCase::Behavior.module_eval do
  alias_method :process_old, :process

  def process(action, *args)
    if params = args.first[:params]
      params["locale"] = I18n.default_locale
    end
    process_old(action, *args)
  end
end

Note the assumption that you are passing params in your spec using a symbol key and not the string "params".

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

CSS Autoprefixer OMG!!!

Posting on Rails channel, since there is a gem for using this amazing tool with your Rails apps. Using Autoprefixer, you no longer have to worry about writing or maintaining vendor-specific CSS properties. (The ones with the dash prefixes.) You just use the latest W3C standards, and the rest is taken care of for you with post-processing.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

FactoryGirl, WebMock, VCR, Fog and CarrierWave

In the interest of fast suite runs (amongst other reasons) you want to make sure that your specs are not dependent on remote servers as they do their thing. One of the more popular ways of achieving this noble aim is by using a gem called WebMock, a library for stubbing and setting expectations on HTTP requests in Ruby.

The first time you use WebMock, code that calls external servers will break.

WebMock::NetConnectNotAllowedError:
       Real HTTP connections are disabled. Unregistered request: GET https://nueprops.s3.amazonaws.com/test...

       You can stub this request with the following snippet:

       stub_request(:get, "https://nueprops.s3.amazonaws.com...

Now maintaining that stub code is often painful, so you probably want to use a gem called VCR to automate the process. VCR works really well. After instrumenting your spec correctly, you run it once to generate a cassette, which is basically a YAML file that captures the HTTP interaction(s) of your spec with the external servers. Subsequent test runs use the cassette file instead of issuing real network calls.

Creation and maintenance of cassettes that mock interaction with JSON-based web services is easy. Services that talk binary? Not so much. And almost every modern Rails project I've ever worked on uses CarrierWave (or Paperclip) to handle uploads to AWS. If you try to use VCR on those requests, you're in for a world of annoyance.

Enter Fog, the cloud-abstraction library that undergirds those uploader's interactions with AWS S3. It has a somewhat poorly documented, yet useful mock mode. Using this mode, I was able to make WebMock stop complaining about CarrierWave trying to upload fixture files to S3.

However, the GET requests generated in my specs were still failing. Given that I'm using the venerable FactoryGirl gem to generate my test data, I was able to eventually move the stub_request calls out of my spec and into a better abstraction level.

factory :standard_star do
  sequence(:name) { |n| "Cat Wrangler #{n}" }
  description "Excellence in project management of ADD people"
  icon { Rack::Test::UploadedFile.new('spec/support/stars/cat-wrangler.jpg') }
  image { Rack::Test::UploadedFile.new('spec/support/stars/cat-wrangler.jpg') }
  after(:create) do |s, e|
    WebMock.stub_request(:get, "https://nueprops.s3.amazonaws.com/test/uploads/standard_star/image/#{s.name.parameterize}/cat-wrangler.jpg").
             to_return(:status => 200, :body => s.image.file.read)

    WebMock.stub_request(:get, "https://nueprops.s3.amazonaws.com/test/uploads/standard_star/icon/#{s.name.parameterize}/cat-wrangler.jpg").
             to_return(:status => 200, :body => s.icon.file.read)

  end
end
Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

When counter_cache on wrong side of association

Absentmindedly put a counter_cache declaration on the has_many instead of where it belongs (pun intended.)

Rails 5 will complain in the most cryptic way it possibly can, which is to raise the following exception

ActiveModel::MissingAttributeError: can't write unknown attribute `true`

If you get that error, now you know how to fix it. Good luck and godspeed.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Change Rails default generators to Sass

Rails inexplicably defaults to SCSS when generating stylesheets. Maybe for the same reasons that DHH doesn't like Haml?

Anyway, to fix it just add the following directive to config/environments/development.rb:

config.sass.preferred_syntax = :sass
Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Rails 5 Attributes API + JSONb Postgres columns

As of when I'm writing this (Jan 2017), support for using ActiveRecord store with Postgres JSONb columns is a bit of shit-show. I'm planning to help fix it as soon as I have some time to spare, but for the moment if you want a better way of supporting these valuable column types in your Rails 5 app, use the new Attributes API. Plus get much improved performance with the Oj gem.

Here's how to make it work. First, define a :jsonb type to replace the native one.

class JsonbType < ActiveModel::Type::Value
  include ActiveModel::Type::Helpers::Mutable

  def type
    :jsonb
  end

  def deserialize(value)
    if value.is_a?(::String)
      Oj.load(value) rescue nil
    else
      value
    end
  end

  def serialize(value)
    if value.nil?
      nil
    else
      Oj.dump(value)
    end
  end

  def accessor
    ActiveRecord::Store::StringKeyedHashAccessor
  end
end

Next, register it in an initializer.

ActiveRecord::Type.register(:jsonb, JsonbType, override: true)

Note that the JsonbType class will need to be somewhere in your loadpath.

Now just declare the attribute at the top of your ActiveRecord model like this:

class User < ApplicationRecord
  attribute :preferences, :jsonb, default: {}
Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

ActiveRecord test objects made easy

If you're testing a ActiveRecord model mixin in your application, you might be tempted to unit test it in the context of one of your app's models. However, that would violate your test isolation and introduce complexities related to the behavior of the model.

Better solution is to make an Active Record class just for your test, and the fact that you can invoke schema definitions on the fly makes it super easy. Here's the top of one of my specs, illustrating the technique.

require 'rails_helper'

ActiveRecord::Schema.define do
  create_table :test_objects, force: true do |t|
    t.jsonb :jobs, null: false, default: {}
  end
end

class TestObject < ApplicationRecord
  include WorkerRegistry
end

RSpec.describe WorkerRegistry do
  let(:test_object) { TestObject.create }

  ...
Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Adding your own datetime formats to Rails

An aspect of Rails that I adore is how it has a place for nearly everything you need to do. One of those things is to format dates/times using the strftime method. Instead of tucking away custom strftime patterns in constants, you can configure them onto the native Rails formatter, accessed via time.to_s(:format_name)

DateTime formats are shared with Time and stored in the Time::DATE_FORMATS hash. Use your desired format name as the hash key and either a strftime string or Proc instance that takes a time or datetime argument as the value.

# config/initializers/time_formats.rb
Time::DATE_FORMATS[:month_and_year] = '%B %Y'
Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }

Here's one of the formats that I've been using lately, to get my times into a more familiar form.

Time::DATE_FORMATS[:short_time] =
   lambda { |time| time.strftime('%I:%M%p').gsub('AM','am').gsub('PM','pm').gsub(':00','') }
Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Easily add Toastr flash notices to Rails apps

I learned about Toastr JavaScript library last week and have been delighted to use it instead of more traditional flash messaging.

First of all get the Toastr sources. I opted to link to them on CDNJS:

= stylesheet_link_tag 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.css'
= javascript_include_tag 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js'

Next I defined some extra flash types in my application_controller.rb file to match Toastr's native notification types and enable use of the built-in styling.

class ApplicationController < ActionController::Base
  add_flash_types :success, :info, :warning, :error
  ...

Finally, add the following block of JavaScript to the bottom of a layout template (or whatever shared partial that contains your JS and CSS includes.

- flash.keys.each do |key|
  - toastr_key = key
  - toastr_key = 'info' if key == 'notice'
  - toastr_key = 'warning' if key == 'alert'
  :javascript
    $(function() {
      toastr["#{toastr_key}"]("#{flash[key]}");
    });

Lines 2 and 3 establish a mapping from conventional Rails notice and alert so that I don't have to hack libraries like Devise which rely on them.

Easy.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Don't roll your own slug code, use FriendlyId

This finding was a pleasant surprise. For years, I've been writing the same kind of boilerplate code to override to_param on my model classes and generate unique slugs. Turns out there's a really well-written library that does that, with some worthwhile additional functionality. Check out FriendlyId for easy slug generation and even the ability to preserve history of slugs after changes, so that it's possible to do 301 redirects with just a couple lines of code.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Set defaults for JSONb postgres columns in Rails

Make sure to pass the migration a native Ruby hash as the default value. DO NOT pass it a string representation of an hash, thinking that it'll work (as valid JSON).

DO THIS

t.jsonb :preferences, default: {}, null: false

NOT

t.jsonb :preferences, default: '{}', null: false

It'll break in a maddeningly non-obvious way. Take my word for it. Also there is this relevant StackOverflow post which saved my ass.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Use Nodemon to auto-restart Rails server

The handy-dandy Nodemon tool is not just for Node. Today I whipped up an invocation that can restart my Rails server whenever there are changes in the config directory tree. Super useful when working heavily with i18n, since changing translation files requires bouncing the server to see changes reflected in the view.

$ nodemon --watch config -e rb,yml --exec "rails server"
Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

Better enumerated types in Active Record

Rails 4 added support for enumerations in Active Record classes. That's cool, but what's cooler is how it has been reimagined by Foraker Labs in Denver, based on the seriously underrated gem Enumerated Type.

Please go read the blog post about it right now, it'll take 5-10 minutes and I promise you won't regret it.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way

How Turbolinks handles redirects

When you visit location /one and the server redirects you to location /two, you expect the browser’s address bar to display the redirected URL. However, Turbolinks makes requests using XMLHttpRequest, which transparently follows redirects. There’s no way for Turbolinks to tell whether a request resulted in a redirect without additional cooperation from the server.

To work around this problem, Rails sends a Turbolinks-Location header in response to a visit that was redirected using redirect_to, and Turbolinks will replace the browser’s topmost history entry with the value provided. If for some reason you are performing redirects manually (so-to-speak, without using the redirect_to helper method), then you'll have to take care of adding the header yourself.

Hero?1484762197

The "Bible" of Ruby on Rails is better than ever

For a limited time, get half off the ultimate The Rails 5 Way package

The "Future Proof Package" is available to TIL readers for only $20. It includes The Rails 5 Way, plus early access to Obie's next two books: Mastering The Rails Way and Testing The Rails Way