Tag: Ruby on Rails

  • I just figured out how to use Font Awesome 6 in a Rails 7 project that uses importmaps. I’m not entirely sure why this works and why some of the workarounds are needed, but my googling yielded no results when I was searching so hopefully here I’ll be saving the next person some time.

    If you search Rubygems for gems with the name “font awesome” you’ll find quite a few but I didn’t like any of them. They all use the font version of the icons, instead of the SVG, or they are very outdated, or they expect you to use SCSS, which I’m not using at the moment. But ultimately, the team at Font Awesome maintains the NPM packages and we should use those directly, not re-wrap packages that will always be out of date.

    For me, using NPM packages directly was higher priority than using importmaps. That’s how strongly I feel about it. I would have installed Webpacker to use Font Awesome’s main package.

    I managed to make this work, but if I’m frank, I’m not 100% sure why the workarounds are needed, so if you have any insights about it or how to improve this, please drop a comment.

    Font Awesome’s documentation says you should install the fontawesome-free package:

    npm install --save @fortawesome/fontawesome-free

    Instead we are going to pin that package, but also some of the dependencies we need later:

    ./bin/importmap pin @fortawesome/fontawesome-free \
                        @fortawesome/fontawesome-svg-core \
                        @fortawesome/free-brands-svg-icons \
                        @fortawesome/free-regular-svg-icons \
                        @fortawesome/free-solid-svg-icons

    This adds the following lines to your importmap.rb:

    pin "@fortawesome/fontawesome-free", to: "https://ga.jspm.io/npm:@fortawesome/[email protected]/js/fontawesome.js"
    pin "@fortawesome/fontawesome-svg-core", to: "https://ga.jspm.io/npm:@fortawesome/[email protected]/index.es.js"
    pin "@fortawesome/free-brands-svg-icons", to: "https://ga.jspm.io/npm:@fortawesome/[email protected]/index.es.js"
    pin "@fortawesome/free-regular-svg-icons", to: "https://ga.jspm.io/npm:@fortawesome/[email protected]/index.es.js"
    pin "@fortawesome/free-solid-svg-icons", to: "https://ga.jspm.io/npm:@fortawesome/[email protected]/index.es.js"

    Then Font Awesome’s documentation says you should add these lines to your code:

    <script defer src="/your-path-to-fontawesome/js/brands.js"></script>
    <script defer src="/your-path-to-fontawesome/js/solid.js"></script>
    <script defer src="/your-path-to-fontawesome/js/fontawesome.js"></script>

    Which might make you think this is a good idea:

    <script defer src="https://ga.jspm.io/npm:@fortawesome/[email protected]/js/brands.js"></script>
    <script defer src="https://ga.jspm.io/npm:@fortawesome/[email protected]/js/solid.js"></script>
    <script defer src="https://ga.jspm.io/npm:@fortawesome/[email protected]/js/fontawesome.js"></script>

    But it doesn’t work. It fails with this error:

    Here I’m a bit confused. How come it fails with that error? Any ideas?

    What did work was editing app/javascript/application.js and adding::

    import {far} from "@fortawesome/free-regular-svg-icons"
    import {fas} from "@fortawesome/free-solid-svg-icons"
    import {fab} from "@fortawesome/free-brands-svg-icons"
    import {library} from "@fortawesome/fontawesome-svg-core"
    import "@fortawesome/fontawesome-free"
    library.add(far, fas, fab)

    I can’t help but feel that there’s a function or method in fontawesome-free that I could call that would do all the setup automatically with less imports and less library building, but I couldn’t find it yet.

  • When I create a new Rails project I like to have a robust seeds that can be used to quickly bootstrap development, testing and staging environments to interact with the application. I think this is critical for development speed.

    If a developer creates a feature to, for example, connect two records together, you just want them to fire up the application and connect two records to see it work. You don’t want them spending time creating the records because that’s a waste of time, but also, because all developers end up having different subsets of testing data and generally ignoring everything that’s not included in them. It’s better to grow a set of testing data that’s good and complete.

    One of the problem I run into is that generating testing data, or sample data, doesn’t happen often enough and changes in the application often break it. Because of that, I wrote this simple test:

    RSpec.describe "rake db:seed" do
      it "runs" do
        Imok::Application.load_tasks if Rake::Task.tasks.none? { |t| t.name == "db:seed" }
        ENV["verbose"] = "false"
        Rake::Task["db:seed"].invoke
      end
    end

    It doesn’t have any assertions, but with just a few lines of code it probably covers 90% of seed data creation problems, that generally result in a crash. I would advice against having assertions here, as they may cost more time than the time they’ll save because sample data evolves a lot and it’s not production critical.

  • I like my models to be printed nicely, to make the class of the model as well as the id and other data available, so, when they end up in a log or console, I can now exactly what it is. I’ve been doing this since before Rails 3 and since Rails projects now have an ApplicationRecord class, it’s even easier.

    On my global parent for all model classes, ApplicationRecord I add this:

    def to_s(extra = nil)
      if extra
        extra = ":#{extra}"
      end
      "#<#{self.class.name}:#{id}#{extra}>"
    end

    That makes all records automatically print as:

    <ModelName:id>

    For example:

    <User:123>

    which makes the record findable.

    But also, allows the sub-classes to add a bit of extra information without having to re-specify the whole format. For example, for the User class, it might be:

      def to_s
        super(email)
      end

    so that when the user gets printed, it ends up being:

    <User:123:[email protected]>

    which I found helps a lot with quickly debugging issues, in production as well as development environments.

  • Rails 6 shipped with a very nice feature to keep encrypted credentials on the repo but separate them by environment, so you can have the credentials for development, staging and production, encrypted with different keys, that you keep safe at different levels.

    For example, you might give the development key to all developers, but the production keys are kept very secret and only accessible to a small set of devops people.

    You edit these credentials by running:

    bundle exec rails credentials:edit --environment development

    for the development credentials, or

    bundle exec rails credentials:edit --environment production

    for production ones. You get the idea.

    When you run it, if the credentials don’t exist, it generates a key. If they exist, you need to have the keys. After decrypting, it runs your default editor and on Windows, this is the error I was getting:

    No $EDITOR to open file in. Assign one like this:
    
    EDITOR="mate --wait" bin/rails credentials:edit
    
    For editors that fork and exit immediately, it's important to pass a wait flag,
    otherwise the credentials will be saved immediately with no chance to edit.

    It took me a surprisingly long time to figure out how to set the editor on Windows, so, for me and others, I’m documenting it in this post:

    $env:EDITOR="notepad"

    After that, running the credentials:edit command works and opens Notepad. Not the best editor by far, but for this quick changes, it works. Oh, and I’m using Powershell. I haven’t run cmd in ages.

  • For those cases in which there can be one and only one record on the database with certain fields and I don’t just want to get the first one and silently get the wrong one. I want to make sure there’s one and only one, so, I wrote this little extension to ActiveRecord that does exactly that:

    [code lang=”ruby”]
    module ActiveRecordExtension
    extend ActiveSupport::Concern

    class_methods do
    def one_and_only
    records = limit(2).all.to_a
    if records.count &gt; 1
    raise "#{self} generated more than one record when expecting only one."
    else
    records.first
    end
    end

    def one_and_only!
    one_and_only.tap do |record|
    if record.nil?
    raise "#{self} didn’t generate any records."
    end
    end
    end
    end
    end

    ActiveRecord::Base.send(:include, ActiveRecordExtension)
    [/code]
    The first method, one_and_only, will raise an exception if there’s more than one item but it’ll return null if there aren’t any. one_and_only! will fail if there isn’t exactly one and only one record in the database.

    If you don’t know why I’m calling this The Highlander query, you should go and watch Christopher Lambert’s masterpiece.

  • Call to Buzz, like many applications I developed before, sends emails. Lot’s of emails. To avoid accidentally emailing a customer or random person I use mail_safe. It’s one of the first gems I install on a Rails project and you should too. mail_safe re-writes the to-header so you end up receiving all the emails that you sent.

    Once you are receiving all these emails, there’s another problem. They are likely to have exactly the same subject, so, mail clients are likely to group them in threads, which can be annoying. To avoid that, I added this to my ApplicationMailer class and voila! all emails have a unique subject:

    [code language=”ruby”]
    if Rails.env.development?
    after_action :uniq_subjects_in_development

    def uniq_subjects_in_development
    mail.subject += " #{SecureRandom.uuid}"
    end
    end
    [/code]

  • I’ve been using both Active Admin and Delayed::Job for years now and both have served me very well. It’s very common for me to want to display job records in the admin tool and have some extra tools around them such as:

    • the ability to mark them all for re-run
    • the ability to run one manually (not recommended for production)
    • the ability to run them all manually (also not recommended for production)
    • an easy way to delete them all
    • a good view of the data inside the job

    To achieve this, over the years, my admin/delayed_job.rb grew and I took it from project to project. Today I want to share it with you in case you are doing the same:

    ActiveAdmin.register Delayed::Job, as: "DelayedJob" do
      collection_action :run_all, method: :post do
        successes, failures = Delayed::Worker.new.work_off
        if failures > 0
          flash[:alert] = "#{failures} failed delayed jobs when running them."
        end
        if successes > 0
          flash[:notice] = "#{successes} delayed jobs run successfully"
        end
        if failures == 0 && successes == 0
          flash[:notice] = "Yawn... no delayed jobs run"
        end
        redirect_to admin_delayed_jobs_url
        I18n.locale = :en # Running delayed jobs can mess up the locale.
      end
    
      if Rails.env.development?
        collection_action :delete_all, method: :post do
          n = Delayed::Job.delete_all
          redirect_to admin_delayed_jobs_url, notice: "#{n} jobs deleted."
        end
      end
    
      collection_action :mark_all_for_re_run, method: :post do
        n = Delayed::Job.update_all("run_at = created_at")
        redirect_to admin_delayed_jobs_url, notice: "Marked all jobs (#{n}) for re-running."
      end
    
      member_action :run, method: :post do
        delayed_job = Delayed::Job.find(params[:id])
        begin
          delayed_job.invoke_job
          delayed_job.destroy
          redirect_to admin_delayed_jobs_url, notice: "1 delayed job run, apparently successfully, but who knows!"
        rescue => e
          redirect_to admin_delayed_jobs_url, alert: "Failed to run a delayed job: #{e.to_s}"
        end
        I18n.locale = :en # Running delayed jobs can mess up the locale.
      end
    
      action_item do
        links = link_to("Run All Delayed Jobs", run_all_admin_delayed_jobs_url, method: :post)
        links += (" " + link_to("Mark all for re-run", mark_all_for_re_run_admin_delayed_jobs_url, method: :post)).html_safe
        links += (" " + link_to("Delete All Delayed Jobs", delete_all_admin_delayed_jobs_url, method: :post)).html_safe if Rails.env.development?
        links
      end
    
      index do
        selectable_column
        id_column
        column "P", :priority
        column "A", :attempts
        column("Error", :last_error, sortable: :last_error) { |post| post.last_error.present? ? post.last_error.split("\n").first : "" }
        column(:created_at, sortable: :created_at) { |job| job.created_at.iso8601.gsub("T", " ") }
        column(:run_at, sortable: :run_at) { |post| post.run_at.present? ? post.run_at.iso8601.gsub("T", " ") : nil }
        column :queue
        column("Running", sortable: :locked_at) { |dj| dj.locked_at.present? ? "#{(Time.now - dj.locked_at).round(1)}s by #{dj.locked_by}" : "" }
        actions
      end
    
      show title: ->(dj) { "Delayed job #{dj.id}" } do |delayed_job|
        panel "Delayed job" do
          attributes_table_for delayed_job do
            row :id
            row :priority
            row :attempts
            row :queue
            row :run_at
            row :locked_at
            row :failed_at
            row :locked_by
            row :created_at
            row :updated_at
          end
        end
    
        panel "Handler" do
          begin
            pre delayed_job.handler
          rescue => e
            div "Couldn't render handler: #{e.message}"
          end
        end
    
        panel "Last error" do
          begin
            pre delayed_job.last_error
          rescue => e
            div "Couldn't render last error: #{e.message}"
          end
        end
    
        active_admin_comments
      end
    
      form do |f|
        f.inputs("Delayed job") do
          f.input :id, input_html: { readonly: true }
          f.input :priority
          f.input :attempts
          f.input :queue
          f.input :created_at, input_html: { readonly: true }, as: :string
          f.input :updated_at, input_html: { readonly: true }, as: :string
          f.input :run_at, input_html: { readonly: true }, as: :string
          f.input :locked_at, input_html: { readonly: true }, as: :string
          f.input :failed_at, input_html: { readonly: true }, as: :string
          f.input :locked_by, input_html: { readonly: true }
        end
    
        f.buttons
      end
    
      controller do
        def update
          @delayed_job = Delayed::Job.find(params[:id])
          @delayed_job.assign_attributes(params[:delayed_job], without_protection: true)
          if @delayed_job.save
            redirect_to admin_delayed_jobs_url, notice: "Delayed job #{@delayed_job} saved successfully"
          else
            render :edit
          end
          I18n.locale = :en # Running delayed jobs can mess up the locale.
        end
      end
    end
  • Ruby on Rails has a very convenient way of presenting a dialog for potentially dangerous tasks that require some confirmation before proceeding. All you have to do is add

    [code language=”ruby”]
    data: {confirm: "Are you sure?"}
    [/code]

    A problem with those, though, is that most (all?) browsers make the dialogs quite ugly:

    Call to Buzz - ugly dialog

    For Call to Buzz, I wanted to finally have something better. Thankfully, there’s a very simple solution, as long as you are using Bootstrap: Twitter::Bootstrap::Rails::Confirm. You just add it to your project and the dialog now looks like this:

    Call to Buzz - bootstrap dialog

    That looks much better, but it’d be nice if it had a nice title, better matching buttons, etc. It’s easy to do by adding some data attributes to the link and the documentation for this gem recommends creating a new method to use instead of link_to when deleting something. I wasn’t very happy with this approach so I resolved it with pure JavaScript so my links remain oblivious to the fact that I’m using this gem:

    [code language=”javascript”]
    $(document).ready(function () {
    $("a[data-confirm]").data({
    "confirm-fade": true,
    "confirm-title": "Call to Buzz"
    });
    $("a[data-method=delete]").data({
    "confirm-title": "Warning",
    "confirm-cancel": "Cancel",
    "confirm-cancel-class": "btn-cancel",
    "confirm-proceed": "Delete",
    "confirm-proceed-class": "btn-danger"
    });
    });
    [/code]

    And with that, the dialog now looks like this:

    Call to Buzz - much better bootstrap dialog

    Update: Later on I wanted to be able to define some extra parameters on a link, such as:

    [code language=”ruby”]
    data: {confirm: "Are you sure you want to disconnect?", confirm_proceed: "Disconnect"}
    [/code]

    To achieve that I re-wrote the code that dynamically adds the extra confirm parameters to look like this (this uses jQuery 3, on earlier version you’d have to do confirm-fade instead of confirmFade):

    [code language=”javascript”]
    $("a[data-confirm]").each(function (_, link) {
    var $link = $(link);
    var data = $link.data();
    data = $.extend({}, {
    "confirmFade": true,
    "confirmTitle": "Call to Buzz"
    }, data);
    if ($link.data("method") == "delete") {
    data = $.extend({}, {
    "confirmTitle": "Warning",
    "confirmCancel": "Cancel",
    "confirmCancelClass": "btn-cancel",
    "confirmProceed": "Delete",
    "confirmProceedClass": "btn-danger"
    }, data);

    }
    $link.data(data);
    });
    [/code]

  • If you are using Simple Form and Bootstrap 3, you probably initialized your app with something similar to:

    [code language=”bash”]
    rails generate simple_form:install –bootstrap
    [/code]

    which adds a file called simple_form_bootstrap.rb to your application with many customizations that make Simple Form output nicely formatted Bootstrap 3 forms.

    For Call to Buzz, an app I’m building, I had several booleans to display, like this:

    Call to Buzz booleans

    My problem was that I wanted a label to explain what those booleans are, something like this:

    Call to Buzz booleans with label

    I achieved that by creating a wrapper that’s a mix between :horizontal_boolean and horizontal_radio_and_checkboxes which I called horizontal_boolean_with_label and looks like this:

    [code language=”ruby”]
    config.wrappers :horizontal_boolean_with_label, tag: &quot;div&quot;, class: &quot;form-group&quot;, error_class: &quot;has-error&quot; do |b|
    b.use :html5
    b.optional :readonly

    b.use :label, class: &quot;col-sm-3 control-label&quot;

    b.wrapper tag: &quot;div&quot;, class: &quot;col-sm-9&quot; do |wr|
    wr.wrapper tag: &quot;div&quot;, class: &quot;checkbox&quot; do |ba|
    ba.use :label_input
    end

    wr.use :error, wrap_with: {tag: &quot;span&quot;, class: &quot;help-block&quot;}
    wr.use :hint, wrap_with: {tag: &quot;p&quot;, class: &quot;help-block&quot;}
    end
    end[/code]

  • Rails 4.1 introduced the concept of secrets.yml, a file in which you store all the credentials for your app, separated by environment, so for example, development can talk to Test Stripe and production to Live Stripe. Furthermore, this file is capable of picking up environment variables which allows you to divorce credentials from code. Not properly separating credentials from code recently cost Uber the leakage of 50,000 driver names and license numbers.

    At Qredo we are very strict about handling credentials, including the ones for SMTP, which in Rails projects are normally stored in config/environments/development.rbconfig/environments/production.rb, etc. Trying to read Rails.application.secrets from those files doesn’t work, because they are loaded before the secrets, so, we came up with this alternative solution.

    The environment files that need to use SMTP for delivering email have this common configuration:

    config.action_mailer.delivery_method = :smtp
    config.action_mailer.smtp_settings = {
      address: "smtp.example.com",
      port: 587,
      authentication: "plain",
      enable_starttls_auto: true
    }

    and then, on config/initializers/email.rb we finish configuring our SMTP credentials by reading it from secrets.yml:

    if ActionMailer::Base.delivery_method == :smtp
      ActionMailer::Base.smtp_settings[:domain]    = Rails.application.secrets.smtp_domain
      ActionMailer::Base.smtp_settings[:user_name] = Rails.application.secrets.smtp_user_name
      ActionMailer::Base.smtp_settings[:password]  = Rails.application.secrets.smtp_password
    end
    

    In config/secrets.yml you need set those credentials:

    development:
      secret_key_base: 123...
      smtp_domain: example.org
      smtp_user_name: [email protected]
      smtp_password: <%= ENV["SMTP_PASSWORD"] %>
    
    test:
      secret_key_base: c7f3f62597b14c7287d75c239168fd3a89d3a6cb51beb909679c2609e912aaa3ca0a0aa5df2e191648baed6fe49698527a75dd265d222697576405971f478c98
    
    staging:
      secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
      smtp_domain: example.net
      smtp_user_name: [email protected]
      password: <%= ENV["SMTP_PASSWORD"] %>
    
    production:
      secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
      smtp_domain: example.com
      smtp_user_name: [email protected]
      smtp_password: <%= ENV["SMTP_PASSWORD"] %>

    And that’s it! Enjoy bringing SMTP credentials into Rails >4.0 secrets management.