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.



163
164
165
166
167
# File 'lib/signoff/model.rb', line 163

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)


139
140
141
142
# File 'lib/signoff/model.rb', line 139

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.



232
233
234
235
236
237
238
239
240
# File 'lib/signoff/model.rb', line 232

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)


194
195
196
197
198
199
200
201
202
# File 'lib/signoff/model.rb', line 194

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)


204
205
206
207
208
209
210
211
212
213
# File 'lib/signoff/model.rb', line 204

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.



132
133
134
135
136
137
# File 'lib/signoff/model.rb', line 132

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



223
224
225
# File 'lib/signoff/model.rb', line 223

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

#last_rejectionObject



227
228
229
# File 'lib/signoff/model.rb', line 227

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

#pending?Boolean

Returns:

  • (Boolean)


148
149
150
# File 'lib/signoff/model.rb', line 148

def pending?
  !self.class.signoff_definition!.finalized_states.include?(current_state)
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.



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/signoff/model.rb', line 171

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)


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

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.



156
157
158
159
160
# File 'lib/signoff/model.rb', line 156

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).



219
220
221
# File 'lib/signoff/model.rb', line 219

def workflow_history
  signoff_events
end