Month: August 2016

  • 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: "div", class: "form-group", error_class: "has-error" do |b|
    b.use :html5
    b.optional :readonly

    b.use :label, class: "col-sm-3 control-label"

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

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