Skip to content

How to create new Controller

This section provide basic information what needs to be implemented for new working controller.

Controller

Controllers source codes are located in directory app/controllers. Controller must be inherited from ApplicationController. Usual controller name is pluralized name of model, which is used in controller, with suffix Controller. For example MyModelsController for model MyModel. There are some special occasions, when controller doesn't work with model, but some other object instead. In this case, controller name is name of object with suffix Controller. For example EasyPages.

Namespacing of controllers does not work well with routes in redmine. If controller works with model in namespace, prefix should be used. For example MyNamespaceMyModelsController for model MyNamespace::MyModel.

Base controller definition
class MyModelsController < ApplicationController
  before_action :authorize_global
  before_action :custom_before_action, only: %i[index show]

  helper :attachments
  include AttachmentsHelper
  include_query_helpers

  def index
    index_for_easy_query MyModelQuery
  end

  def show
    my_model
    respond_to do |format|
      format.html
    end
  end

  def new
    @my_model = MyModel.new
    @my_model.safe_attributes = params[:my_model]
    respond_to do |format|
      format.html
    end
  end

  def create
    @my_model = MyModel.new
    @my_model.safe_attributes = params[:my_model]
    respond_to do |format|
      format.html do
        if @my_model.save
          flash[:notice] = l(:notice_successful_create)
          redirect_back_or_default my_model_path(@my_model)
        else
          render action: :new
        end
      end
    end
  end

  def edit
    # implementation
  end

  def update
    my_model.safe_attributes = params[:my_model]
    # implementation
  end

  def destroy
    # implementation
  end

  # custom actions

  private

  def my_model
    @my_model ||= MyModel.visible.find(params[:id])
  end

  def custom_before_action
    # implementation
  end
end

Controller structure

First properties to define within a new controller are before actions. Most common before actions for easy controllers are authorize_global and a custom action guard. Definition of before_action contains name of method to be executed and options to restrict execution to specific actions. Possible options are only and except.

before_action :authorize_global
before_action :custom_before_action, only: %i[index show]

Entity retrieval — prefer memoized getters over find_* before-actions

ApplicationController in Easy8 already registers a global rescue_from for ActiveRecord::RecordNotFound that renders a 404 page. This means you do not need to rescue locally around .find calls.

Instead of a find_* before-action with a local rescue:

# Avoid this pattern
before_action :find_my_model

def find_my_model
  @my_model = MyModel.visible.find(params[:id])
rescue ActiveRecord::RecordNotFound
  render_404
end

Use a private memoized getter and call it directly from actions:

def show
  my_model          # raises RecordNotFound → global handler → 404
  respond_to { |f| f.html }
end

private

# @return [MyModel]
# @raise [ActiveRecord::RecordNotFound] handled globally → 404
def my_model
  @my_model ||= MyModel.visible.find(params[:id])
end

If the record is optional (not finding it is not an error), use .find_by and handle nil explicitly:

def my_optional_model
  @my_optional_model ||= MyModel.visible.find_by(id: params[:id])
end

If a record has no dedicated AR table (e.g. active_quote derived from an association), raise ActiveRecord::RecordNotFound manually so the global handler still produces a 404:

# @raise [ActiveRecord::RecordNotFound] when no active quote exists
def active_quote
  @active_quote ||= my_model.active_quote || raise(ActiveRecord::RecordNotFound)
end

After that, there are helpers to be included. There are two ways to include helper. First is to include helper module (include AttachmentsHelper) directly into controller. Second is to use helper method. This allow to use helper methods within views. If controller uses queries, is good to include query helper.

helper :attachments
include AttachmentsHelper
include_query_helpers

Next to define are controller actions. Each action is represented by public controller method. Each method should be ended by block respond_to, which defines various response formats.

def my_action
  # request process implementation
  respond_to do |format|
    format.html do
      flash[:notice] = l(:notice)
      redirect_to path
    end
    format.js { render_api_ok }
  end
end

Last to define are private methods. These methods usually serves as before_actions or define logic necessary to request handling.

Views

Action views are defined in app/views/my_models directory. Each action view name must correspond to action name. Each GET action use action view by default. To achieve otherwise, render or redirect_to methods must be used within action.

Routes

One way how to define endpoints for actions are routes. Routes are defined in config/routes.rb file. Default actions routes for controller are defined by resources method, which should correspond with name of object/model_plural within controller name. To restrict default routes, only or except options can be used. There are several ways how to define custom routes. Within resources block is possible to use member or collection block. Each custom route must have defined REST method and name. If action is using multiple REST methods, match method in combination with option via: %i[get put] can be used.

It is also possible to define custom routes outside mentioned blocks. This kind of definition follows template method + path + options. Method is a REST method or match. Options can be:

  • to - controller#action
  • as - route name
  • via - additional REST methods
  • controller - controller name
  • action - action name
routes.rb
resources :my_models do
  member do
    match :my_model_action, via: %i[get put]
  end
  collection do
    get :my_models_action
  end
end

match 'my_models/:id/my_model_action', to: 'my_models#my_model_action', via: %i[get put]
get 'my_models/my_models_action', controller: 'my_models', action: 'my_models_action', via: %i[get put]

Tests

Our best-practice is to describe controller tests in request specs. These are defined in spec/requests directory. Within request spec should be tested each endpoint, handled by controller under test including "render".

request spec example
RSpec.describe "MyModels", type: :request, logged: true do
  subject { response }

  describe "GET index" do
    let!(:request) { get my_models_path }

    it { is_expected.to have_http_status(:success) }
  end

  describe "GET show" do
    let(:my_model) { FactoryBot.create(:my_model) }
    let!(:request) { get my_model_path(my_model) }

    it { is_expected.to have_http_status(:success) }
  end

  describe "GET new" do
    # implementation
  end

  describe "POST create" do
    let(:my_model_params) { { name: "new model name" } }
    let!(:request) { post my_models_path(my_model: my_model_params) }

    it { is_expected.to redirect_to(/my_models\/\d+/) }

    context "with invalid request" do
      let(:my_model_params) { { name: "" } }

      it "Handle validation error" do
        expect(response).to have_http_status(:success)
        expect(response).to render_template(:new)
      end
    end

  end

  describe "GET edit" do
    # implementation
  end

  describe "PUT update" do
    let(:my_model) { FactoryBot.create(:my_model, name: "Super name") }
    let(:my_model_params) { { name: "changed model name" } }
    let!(:request) { patch my_model_path(my_model, my_model: my_model_params) }

    it { is_expected.to redirect_to(my_model_path(my_model)) }

    it "name was changed" do
      expect { my_model.reload }.to change { my_model.name }.from("Super name").to("changed model name")
    end

    context "with invalid name" do
      let(:my_model_params) { { name: "" } }

      it "Handle validation error" do
        expect(response).to render_template(:edit)
      end
    end
  end

  describe "DELETE destroy" do
    let(:my_model) { FactoryBot.create(:my_model) }
    let!(:request) { delete my_model_path(my_model) }

    it { is_expected.to redirect_to(my_models_path) }

    it "record was really deleted" do
      expect { my_model.reload }.to raise_error(ActiveRecord::RecordNotFound)
    end
  end
end