Rails 5.1 added :default option to belongs_to
From the changelog
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.
From the changelog
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.
Hat tip to my editor Cynthia for pointing out why I should never use the word impactful in my writing.
I laughed out loud when I got to that last part (highlight mine.)
Full explanation here
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.
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.
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.)
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.
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.
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'"
},
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.
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.
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...
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"
.
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.
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
This one caught me by surprise today. Luckily, it's relatively simple to detect the missing functionality using Modernizr.js and use Datepickr instead.
$(function(){
if (!Modernizr.inputtypes.date) {
$('input[type=date]').datepicker({
dateFormat : 'yy-mm-dd'
});
}
});
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.
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
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: {}
Wow, how did I miss this memo? Ruby 2.3 introduced a safe operator
Instead of
current_user.try(:profile).try(:bio)
you can now do
current_user&.profile&.bio