Module: Controllers::Destroyable

Overview

Shared destroy action for CRM controllers: finds the record, checks
authorization, destroys it, and redirects with a success/error flash.
Aborted destroys (dependent records, FK restrictions) are surfaced as
friendly error messages instead of 500s.

See Also:

Instance Method Summary collapse

Instance Method Details

#destroyvoid

This method returns an undefined value.

Standard destroy action — delegates to #perform_destroy with the
controller's model class and any redirect options from params[:destroy].



18
19
20
# File 'app/concerns/controllers/destroyable.rb', line 18

def destroy
  perform_destroy controller_name.classify.constantize, params[:destroy]
end

#perform_destroy(record_class, options = nil) ⇒ void

This method returns an undefined value.

Destroy the record identified by params[:id] and redirect.

Parameters:

  • record_class (Class)

    the ActiveRecord class of the record to destroy

  • options (Hash, nil) (defaults to: nil)

    redirect targets

Options Hash (options):

  • success_url (String)

    redirect target after a successful destroy

  • error_url (String)

    redirect target after a failed destroy

  • redir_url (String)

    fallback redirect target for either outcome



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'app/concerns/controllers/destroyable.rb', line 30

def perform_destroy(record_class, options = nil)
  options ||= {}
  @error_msg = []
  record_identifier = "#{record_class.name.titleize} record id #{params[:id]}"
  record_instance_name = "@#{record_class.name.tableize.singularize}"
  begin
    record = instance_variable_get(record_instance_name) || record_class.find(params[:id])
    authorize! :destroy, record
    destroyed = record.destroy
    @error_msg += record.errors.full_messages
    # A `dependent: :restrict_with_error` in the cascade aborts the destroy and
    # adds its error to the *dependent* record, not to `record` — so without this
    # guard an aborted destroy (returns false, no error on `record`) would be
    # reported as a success. (AppSignal #6138.)
    @error_msg << "It has dependent records that must be removed first." if !destroyed && @error_msg.empty?
    instance_variable_set(record_instance_name, record)
  rescue ActiveRecord::RecordNotFound
    @error_msg << "#{record_identifier} not found."
  rescue CanCan::AccessDenied
    @error_msg << "Insufficient permissions to destroy #{record_identifier}"
  rescue ActiveRecord::DeleteRestrictionError => e
    # A dependent association declared `restrict_with_exception` still has rows.
    # Surface a friendly message instead of a 500. (AppSignal #6138.)
    @error_msg << "It has dependent #{e.message[/dependent (.+)\z/, 1] || 'records'}."
  rescue ActiveRecord::InvalidForeignKey
    # A database-level restrictive foreign key still has child rows. Surface
    # the same friendly failure as model-level delete restrictions.
    @error_msg << "It has dependent records that must be removed first."
  end
  if @error_msg.empty?
    flash[:info] = "#{record_identifier} was deleted successfully."
    redir_url = options[:success_url]
  else
    flash[:error] = "#{record_identifier} could not be deleted. #{@error_msg.join(',')}"
    redir_url = options[:error_url]
  end
  redir_url ||= options[:redir_url]
  redir_url ||= url_for(action: :index)
  # Redirection will be either succes/error url, or a generalized redir url option, or the index action
  redirect_to_return_path_or_default redir_url
end