Guru on Rails

if you don’t sacrifice for your dream then your dream becomes your sacrifice.
Sydney, Sat 01 Jun 2019
How to manage model's statuses efficiently
Wed 27 Jun 2018

Challenge: We do want to manage Status of model efficiently. Statuses will automatically changed whenever the data of model changed. However, we do want to define the condition for every status and the event allows statuses changed.

Solution: We use a gem names `AASM`. We set up callback function before_save to listen for events and change status if it meets the condition.

module EvaluationStatusConcern
  extend ActiveSupport::Concern

  included do
    aasm :column => 'status' do
      state :candidate, initial: true
      state :scheduled, :ready_to_go, :completed, :approved, :disapproved, :closed

      event :scheduling do
        transitions from: :candidate, to: :scheduled
      end

      event :mark_as_ready_to_go do
        transitions from: :scheduled, to: :ready_to_go
      end

      event :mark_as_scheduled do
        transitions from: :ready_to_go, to: :scheduled
      end

      event :completing do
        transitions from: [:scheduled, :ready_to_go], to: :completed
      end

      event :approve do
        transitions from: :completed, to: :approved
      end

      event :disapprove do
        transitions from: :completed, to: :disapproved
      end

      event :reapprove do
        transitions from: :disapproved, to: :approved
      end

      event :unapprove do
        transitions from: [:approved, :closed], to: :completed
      end

      event :undisapprove do
        transitions from: :disapproved, to: :completed
      end

      event :closing do
        transitions from: :approved, to: :closed
      end

      event :reopen do
        transitions from: :closed, to: :approved
      end
    end

    validates :scheduled_date,
             presence: true, unless: :candidate?

    validates :primary_auditor_id,
             presence: true, if: Proc.new { |object| object.secondary_auditor_id.blank? && !object.candidate? }

    validates :secondary_auditor_id,
             presence: true, if: Proc.new { |object| object.primary_auditor_id.blank? && !object.candidate? }

    validate do
      errors.add(:score, 'must be filled.') if (completed? || approved? || disapproved? || closed?) && score.nil?
    end

    validate do
      errors.add(:approver_id, 'must be blanked.') if !(approved? || disapproved? || closed?) && approver_id.present?
    end

    validate do
      errors.add(:approver_id, 'must be filled.') if (approved? ||disapproved? || closed?) && (approver_id.nil? || (approver.role.Admin? || approver.role.MSQ?) == false)
    end

    before_save do
      # Change status based on some conditions
      self.scheduling if may_scheduling? && scheduled_date.present? && (primary_auditor_id.present? || secondary_auditor_id.present?)

      self.completing if may_completing? && software_type.present? && hardware_type.present? && score.present?

      self.closing if may_closing? && count_car_rectified_items == count_observation_item
    end

    scope :scheduled, -> {
      where(status: ['scheduled', 'ready_to_go'])
    }

    scope :candidate, -> {
      where(status: 'candidate')
    }
  end
end

As we can see that wherever users do want to make change in the source code, it's not a matter. Because we implement EVENTs for changes. According the above code, we involve changing status by callback function "before_save" so that every time the instance saved, system involves changing status before saving. System always check if the condition for changing is met. We also can easily change the condition if business logic changed. Applying the single responsibility pattern we implement managing status's change in a module called Status Concerns. That's it.