Module: Signoff::Model

Extended by:
ActiveSupport::Concern
Included in:
Signoff
Defined in:
lib/signoff/model.rb

Overview

The behaviour mixed into ActiveRecord models via include Signoff. It provides the signoff class macro, the transition methods (submit!, approve!, reject!), state predicates, query scopes and the audit-trail helpers.

Instance Method Summary collapse

Instance Method Details

#approve!(user: nil, comment: nil, metadata: {}, to: nil, ip_address: nil, user_agent: nil) ⇒ Object

Advance the record forward and record an “approve” event.



142
143
144
145
146
# File 'lib/signoff/model.rb', line 142

def approve!(user: nil, comment: nil, metadata: {}, to: nil,
             ip_address: nil, user_agent: nil)
  _advance!(action: "approve", to: to, user: user, comment: comment,
            metadata: , ip_address: ip_address, user_agent: user_agent)
end

#approved?Boolean

Returns:

  • (Boolean)


118
119
120
121
# File 'lib/signoff/model.rb', line 118

def approved?
  self.class.signoff_definition!
      .approval_terminal_states.include?(current_state)
end

#approved_byObject

The user that moved the record into a successful terminal state.



211
212
213
214
215
216
217
218
219
# File 'lib/signoff/model.rb', line 211

def approved_by
  states = self.class.signoff_definition!
               .approval_terminal_states.map(&:to_s)
  return nil if states.empty?

  _latest_event(conditions: { to_state: states }) do |e|
    states.include?(e.to_state)
  end&.user
end

#can_approve?(user = nil) ⇒ Boolean

— authorization predicates —————————————

Returns:

  • (Boolean)


173
174
175
176
177
178
179
180
181
# File 'lib/signoff/model.rb', line 173

def can_approve?(user = nil)
  definition = self.class.signoff_definition!
  from = current_state
  # A no-argument approve! only proceeds when exactly one forward
  # transition exists (zero => terminal, many => ambiguous).
  return false unless definition.forward_targets(from).size == 1

  _guard_satisfied?(from, user || Signoff::Current.user)
end

#can_reject?(user = nil) ⇒ Boolean

Returns:

  • (Boolean)


183
184
185
186
187
188
189
190
191
192
# File 'lib/signoff/model.rb', line 183

def can_reject?(user = nil)
  definition = self.class.signoff_definition!
  return false unless definition.reject_state

  from = current_state
  return false if from == definition.reject_state ||
                  definition.approval_terminal_states.include?(from)

  _guard_satisfied?(from, user || Signoff::Current.user)
end

#current_stateObject

The current workflow state as a Symbol.



111
112
113
114
115
116
# File 'lib/signoff/model.rb', line 111

def current_state
  definition = self.class.signoff_definition!
  _ensure_state_column!(definition)
  value = self[definition.state_column]
  (value.presence || definition.initial_state).to_sym
end

#last_approvalObject



202
203
204
# File 'lib/signoff/model.rb', line 202

def last_approval
  _latest_event(conditions: { action: "approve" }) { |e| e.action == "approve" }
end

#last_rejectionObject



206
207
208
# File 'lib/signoff/model.rb', line 206

def last_rejection
  _latest_event(conditions: { action: "reject" }) { |e| e.action == "reject" }
end

#pending?Boolean

Returns:

  • (Boolean)


127
128
129
# File 'lib/signoff/model.rb', line 127

def pending?
  !approved? && !rejected?
end

#reject!(user: nil, comment: nil, metadata: {}, ip_address: nil, user_agent: nil) ⇒ Object

Move the record to the configured reject state and record a “reject” event.



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/signoff/model.rb', line 150

def reject!(user: nil, comment: nil, metadata: {},
            ip_address: nil, user_agent: nil)
  definition = self.class.signoff_definition!
  unless definition.reject_state
    raise InvalidTransitionError,
          "no reject state configured for #{self.class.name}"
  end

  from = current_state
  raise InvalidTransitionError, "record is already rejected" if from == definition.reject_state
  if definition.approval_terminal_states.include?(from)
    raise InvalidTransitionError,
          "cannot reject a finalized record (state: #{from})"
  end

  _perform_transition!(action: "reject", from: from,
                       to: definition.reject_state, user: user,
                       comment: comment, metadata: ,
                       ip_address: ip_address, user_agent: user_agent)
end

#rejected?Boolean

Returns:

  • (Boolean)


123
124
125
# File 'lib/signoff/model.rb', line 123

def rejected?
  current_state == self.class.signoff_definition!.reject_state
end

#submit!(user: nil, comment: nil, metadata: {}, to: nil, ip_address: nil, user_agent: nil) ⇒ Object

Advance the record forward and record a “submit” event. Conventionally used for the initial submission out of the draft state.



135
136
137
138
139
# File 'lib/signoff/model.rb', line 135

def submit!(user: nil, comment: nil, metadata: {}, to: nil,
            ip_address: nil, user_agent: nil)
  _advance!(action: "submit", to: to, user: user, comment: comment,
            metadata: , ip_address: ip_address, user_agent: user_agent)
end

#workflow_historyObject

The full, chronologically ordered event history (preloadable via includes(:signoff_events) to avoid N+1 queries).



198
199
200
# File 'lib/signoff/model.rb', line 198

def workflow_history
  signoff_events
end