Using an observer on a controller in Rails 4

Observers are not used anymore in Rails 4, although you can use the rails-observers gem. But in one of my controllers I have some actions that need to be performed, for which an observer would be ideal in my opinion:

def approve
    authorize! :approve, :timesheet_assessment

    @timesheet = Timesheet.find(params[:timesheet_assessment_id])
    @timesheet.update_attributes(status: "approved")

    @notification = Notification.where(subject_id: @timesheet.id).first
    unless @notification.blank?
      @notification.status = "approved"
      @notification.save
    end

    Notifier.new(@timesheet.user).timesheet_approved(@timesheet, current_user)

    Aggregator.new(@timesheet).store_aggregated_activities

    redirect_to admin_timesheet_assessments_path, notice: I18n.t('.timesheet.message_approve')
  end

The creation of the notification and storing the aggregated activities should not belong in this controller method, but can be moved to an observer. What would be a good solution in your opinion?

First move the logic to “mark a notification as approved” into Notification and Timesheet.

def approve
  authorize! :approve, :timesheet_assessment

  @timesheet = Timesheet.find(params[:timesheet_assessment_id])
  @timesheet.approve!

  Notification.approve_subject(@timesheet)
  Notifier.new(@timesheet.user).timesheet_approved(@timesheet, current_user)

  Aggregator.new(@timesheet).store_aggregated_activities

  redirect_to admin_timesheet_assessments_path, notice: I18n.t('.timesheet.message_approve')
end

class Timesheet < ActiveRecord::Base
  # TODO: Probably could be an `ApprovalConcern` mix-in
  def approve!
    update_attribute(:status, "approved")
  end
end

class Notification < ActiveRecord::Base
  def self.approve_subject(subject)
    notification = where(subject_id: subject.id).first
    notification.approve! unless notification.blank?
  end

  # TODO: Probably could be an `ApprovalConcern` mix-in
  def approve!
    update_attribute(:status, "approved")
  end
end

Then, you can create a TimesheetApprovalService object to encapsulate the extra logic.

def approve
  authorize! :approve, :timesheet_assessment

  @timesheet = Timesheet.find(params[:timesheet_assessment_id])
  @timesheet.approve!

  TimesheetApprovalService.new(@timesheet, current_user).approve!

  redirect_to admin_timesheet_assessments_path, notice: I18n.t('.timesheet.message_approve')
end

class TimesheetApprovalService
  def initialize(timesheet, approved_by)
    @timesheet   = timesheet
    @approved_by = approved_by
  end

  def approve!
    approve_notification
    send_notifier
    store_aggregated_activities
  end

  private

  def approve_notification
    Notification.approve_subject(@timesheet)
  end

  def send_notifier
    Notifier.new(@timesheet.user).timesheet_approved(@timesheet, @approved_by)
  end

  def store_aggregated_activities
    Aggregator.new(@timesheet).store_aggregated_activities
  end
end

I don’t know much about your domain needs, so take the naming with a grain of salt. But, overall the idea to encapsulate that logic so that if anything changes you can update it in one place. Also, you now have the benefit of being able to use the approval service in places other than the controller such as background jobs or rake tasks.

Additionally, it helps testing. Write unit test for the service and stub it out in the controller test. This saves you from stubbing out three collaborators when testing the approve controller action.

2 Likes

Thanks, that is a solution direction I was looking for. I will implement this and see if it works out for me.

Recently I saw a nice solution for extracting business logic in Rails applications: GitHub - AaronLasseigne/active_interaction: Manage application specific business logic.

Maybe it can help you.