Skip to content

Instantly share code, notes, and snippets.

@dabit
Last active November 2, 2022 15:29
Show Gist options
  • Save dabit/c8c06f0b2fc51219dd6691bf6306a6e0 to your computer and use it in GitHub Desktop.
Save dabit/c8c06f0b2fc51219dd6691bf6306a6e0 to your computer and use it in GitHub Desktop.
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
def self.inherited(subclass)
super
return unless subclass.has_attribute?(:deleted_at)
setup_for_soft_delete(subclass)
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
nil
end
def self.setup_for_soft_delete(subclass)
subclass.send(:default_scope, -> { where(deleted_at: nil) })
class << subclass
def archived
where.not(deleted_at: nil)
end
end
subclass.define_method(:destroy) do
touch(:deleted_at)
end
end
def self.human_enum_name(enum_name, enum_value)
I18n.t("activerecord.attributes.#{model_name.i18n_key}.#{enum_name.to_s.pluralize}.#{enum_value}")
end
def self.plural_name
model_name.human(count: 2)
end
def self.enum_collection_for_select(enum_plural_name)
send(enum_plural_name).collect { |k, _v| [k, human_enum_name(enum_plural_name, k)] }
end
end
@silva96
Copy link

silva96 commented Oct 26, 2022

I always add this so I can easily create select tags for "statuses" or "types" of a model (wherever you use enums)

class ApplicationRecord < ActiveRecord::Base
  def self.human_enum_collection(enum_name, except: [])
    send(enum_name.to_s.pluralize).keys.filter_map do |val|
      [human_enum_name(enum_name, val), val] unless except.include?(val)
    end.compact
  end
end
# Any ApplicationRecord using enums
class Payout < ApplicationRecord
  enum status: { requested: 0, in_transit: 1, completed: 2, rejected: 3 }
  enum account_type: { checking: 0, vista: 1, savings: 2 }
  # ...
end
en:
  activerecord:
    attributes:
      payout:
        statuses:
          requested: "Solicited"
          in_transit: "In transit"
          completed: "Completed"
          rejected: "Rejected"
        account_types:
          checking: "Checking acc."
          savings: "Savings acc."
          vista: "Vista acc."

Use it in selects

<%= f.select :account_type, Payout.human_enum_collection(:account_type) %>
<%= f.select :status, Payout.human_enum_collection(:status, except: [:rejected]) %>

@silva96
Copy link

silva96 commented Oct 26, 2022

@alejandrodevs I usually prefer to be more explicit and implement a soft_delete method. I do this in a general purpose concern that I include in all soft_deletable models.

@dabit
Copy link
Author

dabit commented Oct 26, 2022

@alejandrodevs My use case is very simple and straightforward; I just want to keep a small selection of tables where I want the data to be deleted but still keep it handy in case someone needs it back. I have been in several situations where the user deletes a record by mistake or simply because they thought they would not need it in the future and then realize that they do, in fact, need it later.

I use the default scope because I don't expect this data to show up anywhere within the app, and I want it to behave as if it's gone forever. I rarely add the functionality to browse this data. I include an archived scope for my convenience, just in case I want to dig through it via the console.

Adding a deleted_at column is explicit enough to tell me if a table is soft deletable or not (I religiously use annotate, so I can always see what my tables look like).

I override destroy because it makes it simpler to turn it on and off just by adding or removing the deleted_at column.

By the way, I will include your scope-based refactoring in a future iteration; thank you for that. I agree that using scope is better.

Lastly, I understand there are gems for everything these days, but I personally never add a dependency for something I can fix with a few lines of code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment