Link Search Menu Expand Document

Customizing your admin pages

Customers

We won’t make any customizations in the admin page, but let’s add a validation to the Customer model.

# app/models/customer.rb
class Customer < ApplicationRecord
  validates :name, presence: true
end

Products

Let’s format the prices, and on the show page, let’s add the number of current subscriptions.

# In app/models/product.rb
class Product < ApplicationRecord
  has_many :subscriptions
  monetize :price_cents # this is a feature of rails-money
end

The interesting thing to keep in mind here are the calls to type.computed. The call to computed tells Super that you’re defining a field that isn’t a column. The first argument, :column yields the value of that field/method, while :record yields the entire record. The value of the block is what is shown to the user.

The call to type.string might seem a little incorrect, but you can think of it like calling #to_s on the field to get it ready for display.

# In app/controllers/admin/products_controller.rb
class Admin::ProductsController < AdminController
  class Controls < AdminControls
    def model
      Product
    end

    def display_schema(action:)
      Super::Display.new do |f, type|
        f[:id] = type.string
        f[:name] = type.string
        f[:price] = type.computed(:column, &:format)
        if action.show?
          time = Time.current
          f[:current_subscriptions] = type.computed(:record) do |record|
            record.subscriptions.where("starts_at < ? AND ends_at > ?", time, time).size
          end
          f[:created_at] = type.timestamp
          f[:updated_at] = type.timestamp
        end
      end
    end
  end
end

Note that this doesn’t change the filtering or sorting sections. That’s customizable too, but we’ll get to that later.

The index view:

The show view:

Receipts

Let’s format the prices here too, but for good measure, we’ll do it a little more manually without editing the model. We’ll also add a link to the associated subscription.

Note the type.real here. It’s works identically to type.computed except that it tells Super that it is a real database column.

# app/controllers/admin/receipts_controller.rb
class Admin::ReceiptsController < AdminController
  class Controls < AdminControls
    def model
      Receipt
    end

    def display_schema(action:)
      Super::Display.new do |f, type|
        f[:id] = type.string
        f[:total] = type.computed(:record) do |record|
          Money.new(record.total_cents, record.total_currency).format
        end
        f[:subscription_id] = type.real(:column) do |value|
          AdminController.helpers.link_to(
            "Subscription ##{value}",
            Rails.application.routes.url_helpers.admin_subscription_path(value)
          )
        end
        f[:created_at] = type.timestamp
        if action.show?
          f[:updated_at] = type.timestamp
        end
      end
    end
  end
end

The index view:

The show view:

Subscriptions

We’ll do a little more here. In addition to showing the related Product and Customer, let’s show the if the subscription is currently active.

We’ll also update the forms and turn the product field into a dropdown. And while we’re at it, let’s use nested attributes so that we can edit or create a customer here.

# app/models/subscription.rb
class Subscription < ApplicationRecord
  belongs_to :product
  belongs_to :customer

  accepts_nested_attributes_for :customer
  validates_associated :customer
end

The interesting thing to note here is the nested attributes. Note that the f[:customer_attributes] is what’ll be set, while the type.has_one(:customer) is what’ll be read. Also of note, type.has_one and type.belongs_to are aliases.

# app/controllers/admin/subscriptions_controller.rb
class Admin::SubscriptionsController < AdminController
  class Controls < Super::Controls
    def model
      Subscription
    end

    def scope(action:)
      model.includes(:customer, :product)
    end

    def display_schema(action:)
      current_time = Time.current
      Super::Display.new do |f, type|
        f[:id] = type.string
        f[:customer_id] = type.real(:record) do |record|
          customer = record.customer
          AdminController.helpers.link_to(
            customer.name,
            Rails.application.routes.url_helpers.admin_customer_path(customer.id)
          )
        end
        f[:product_id] = type.real(:record) do |record|
          product = record.product
          AdminController.helpers.link_to(
            product.name,
            Rails.application.routes.url_helpers.admin_product_path(product.id)
          )
        end
        f[:status] = type.computed(:record) do |record|
          if record.starts_at.nil? || record.ends_at.nil?
            "Unknown"
          elsif record.starts_at <= current_time && record.ends_at >= current_time
            "Active"
          elsif action.show?
            "Inactive"
          end
        end
        if action.index?
          f[:created_at] = type.timestamp
        else
          f[:starts_at] = type.timestamp
          f[:ends_at] = type.timestamp
          f[:created_at] = type.timestamp
          f[:updated_at] = type.timestamp
        end
      end
    end

    def form_schema(action:)
      Super::Form.new do |f, type|
        f[:customer_attributes] = type.has_one(:customer) do
          f[:name] = type.string
        end
        f[:product_id] = type.select(collection: Product.all.map { |product| [product.name, product.id] })
        f[:starts_at] = type.string
        f[:ends_at] = type.string
      end
    end
  end
end

The index view:

The show view:

The edit view:

The new view (with a validation error):