discard-1.4.0/0000755000004100000410000000000015145704354013165 5ustar www-datawww-datadiscard-1.4.0/bin/0000755000004100000410000000000015145704354013735 5ustar www-datawww-datadiscard-1.4.0/bin/setup0000755000004100000410000000020315145704354015016 0ustar www-datawww-data#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here discard-1.4.0/bin/console0000755000004100000410000000052615145704354015330 0ustar www-datawww-data#!/usr/bin/env ruby require "bundler/setup" require "discard" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start(__FILE__) discard-1.4.0/.gitignore0000644000004100000410000000015215145704354015153 0ustar www-datawww-data/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ /spec/examples.txt discard-1.4.0/.github/0000755000004100000410000000000015145704354014525 5ustar www-datawww-datadiscard-1.4.0/.github/dependabot.yml0000644000004100000410000000016615145704354017360 0ustar www-datawww-dataversion: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" discard-1.4.0/.github/workflows/0000755000004100000410000000000015145704354016562 5ustar www-datawww-datadiscard-1.4.0/.github/workflows/test.yml0000644000004100000410000000251615145704354020270 0ustar www-datawww-dataname: Test on: push: branches: - master pull_request: branches: - master jobs: build: runs-on: ubuntu-latest name: Test on Rails ${{ matrix.rails_version }} and Ruby ${{ matrix.ruby_version }} strategy: fail-fast: false matrix: include: - rails_version: 8.0.0.rc1 ruby_version: '3.3' sqlite_version: ~> 2.0 - rails_version: ~> 7.2.0 ruby_version: '3.3' sqlite_version: ~> 2.0 - rails_version: ~> 7.2.0 ruby_version: '3.2' sqlite_version: ~> 2.0 - rails_version: ~> 7.2.0 ruby_version: '3.1' sqlite_version: ~> 2.0 - rails_version: ~> 7.1.0 ruby_version: '3.3' sqlite_version: ~> 1.0 - rails_version: ~> 7.0.0 ruby_version: '3.2' sqlite_version: ~> 1.0 - rails_version: ~> 6.1.0 ruby_version: '3.0' sqlite_version: ~> 1.0 env: RAILS_VERSION: ${{ matrix.rails_version }} SQLITE_VERSION: ${{ matrix.sqlite_version }} steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby_version }} - name: Bundle install run: bundle install - name: Test run: bundle exec rake discard-1.4.0/.github/ISSUE_TEMPLATE/0000755000004100000410000000000015145704354016710 5ustar www-datawww-datadiscard-1.4.0/.github/ISSUE_TEMPLATE/bug-report.md0000644000004100000410000000106615145704354021323 0ustar www-datawww-data--- name: Bug Report about: Is Discard not working correctly for you? Let us know! title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Additional context** Please tells us your Rails and Ruby versions, and anything else that might be helpful about the environment you encountered the issue. discard-1.4.0/.github/ISSUE_TEMPLATE/feature-proposal.md0000644000004100000410000000074115145704354022524 0ustar www-datawww-data--- name: Feature Proposal about: Discard is feature-complete, but we're happy to hear you out! title: '' labels: '' assignees: '' --- **Discard is feature-complete, but if you have an idea for a feature that will benefit all discard users and that won't offer a significant maintenance burden, we're happy to listen. Explain it as best you can and we'll let you know if it's something we'd like to have or if it's something that might be a better off as an extension or fork.** discard-1.4.0/lib/0000755000004100000410000000000015145704354013733 5ustar www-datawww-datadiscard-1.4.0/lib/discard/0000755000004100000410000000000015145704354015344 5ustar www-datawww-datadiscard-1.4.0/lib/discard/model.rb0000644000004100000410000001411015145704354016766 0ustar www-datawww-data# frozen_string_literal: true module Discard # Handles soft deletes of records. # # Options: # # - :discard_column - The columns used to track soft delete, defaults to `:discarded_at`. module Model extend ActiveSupport::Concern included do class_attribute :discard_column self.discard_column = :discarded_at scope :kept, ->{ undiscarded } scope :undiscarded, ->{ where(discard_column => nil) } scope :discarded, ->{ where.not(discard_column => nil) } scope :with_discarded, ->{ unscope(where: discard_column) } define_model_callbacks :discard define_model_callbacks :undiscard end # :nodoc: module ClassMethods # Discards the records by instantiating each # record and calling its {#discard} method. # Each object's callbacks are executed. # Returns the collection of objects that were discarded. # # Note: Instantiation, callback execution, and update of each # record can be time consuming when you're discarding many records at # once. It generates at least one SQL +UPDATE+ query per record (or # possibly more, to enforce your callbacks). If you want to discard many # rows quickly, without concern for their associations or callbacks, use # #update_all(discarded_at: Time.current) instead. # # ==== Examples # # Person.where(age: 0..18).discard_all def discard_all kept.each(&:discard) end # Discards the records by instantiating each # record and calling its {#discard!} method. # Each object's callbacks are executed. # Returns the collection of objects that were discarded. # # Note: Instantiation, callback execution, and update of each # record can be time consuming when you're discarding many records at # once. It generates at least one SQL +UPDATE+ query per record (or # possibly more, to enforce your callbacks). If you want to discard many # rows quickly, without concern for their associations or callbacks, use # #update_all!(discarded_at: Time.current) instead. # # ==== Examples # # Person.where(age: 0..18).discard_all! def discard_all! kept.each(&:discard!) end # Undiscards the records by instantiating each # record and calling its {#undiscard} method. # Each object's callbacks are executed. # Returns the collection of objects that were undiscarded. # # Note: Instantiation, callback execution, and update of each # record can be time consuming when you're undiscarding many records at # once. It generates at least one SQL +UPDATE+ query per record (or # possibly more, to enforce your callbacks). If you want to undiscard many # rows quickly, without concern for their associations or callbacks, use # #update_all(discarded_at: nil) instead. # # ==== Examples # # Person.where(age: 0..18).undiscard_all def undiscard_all discarded.each(&:undiscard) end # Undiscards the records by instantiating each # record and calling its {#undiscard!} method. # Each object's callbacks are executed. # Returns the collection of objects that were undiscarded. # # Note: Instantiation, callback execution, and update of each # record can be time consuming when you're undiscarding many records at # once. It generates at least one SQL +UPDATE+ query per record (or # possibly more, to enforce your callbacks). If you want to undiscard many # rows quickly, without concern for their associations or callbacks, use # #update_all!(discarded_at: nil) instead. # # ==== Examples # # Person.where(age: 0..18).undiscard_all! def undiscard_all! discarded.each(&:undiscard!) end end # @return [Boolean] true if this record has been discarded, otherwise false def discarded? self[self.class.discard_column].present? end # @return [Boolean] false if this record has been discarded, otherwise true def undiscarded? !discarded? end alias kept? undiscarded? # Discard the record in the database # # @return [Boolean] true if successful, otherwise false def discard return false if discarded? run_callbacks(:discard) do update_attribute(self.class.discard_column, Time.current) end end # Discard the record in the database # # There's a series of callbacks associated with #discard!. If the # before_discard callback throws +:abort+ the action is cancelled # and #discard! raises {Discard::RecordNotDiscarded}. # # @return [Boolean] true if successful # @raise {Discard::RecordNotDiscarded} def discard! discard || _raise_record_not_discarded end # Undiscard the record in the database # # @return [Boolean] true if successful, otherwise false def undiscard return false unless discarded? run_callbacks(:undiscard) do update_attribute(self.class.discard_column, nil) end end # Undiscard the record in the database # # There's a series of callbacks associated with #undiscard!. If the # before_undiscard callback throws +:abort+ the action is cancelled # and #undiscard! raises {Discard::RecordNotUndiscarded}. # # @return [Boolean] true if successful # @raise {Discard::RecordNotUndiscarded} def undiscard! undiscard || _raise_record_not_undiscarded end private def _raise_record_not_discarded raise ::Discard::RecordNotDiscarded.new(discarded_fail_message, self) end def _raise_record_not_undiscarded raise ::Discard::RecordNotUndiscarded.new(undiscarded_fail_message, self) end def discarded_fail_message return "A discarded record cannot be discarded" if discarded? "Failed to discard the record" end def undiscarded_fail_message return "An undiscarded record cannot be undiscarded" if undiscarded? "Failed to undiscard the record" end end end discard-1.4.0/lib/discard/version.rb0000644000004100000410000000014115145704354017352 0ustar www-datawww-data# frozen_string_literal: true module Discard # Discard version VERSION = "1.4.0".freeze end discard-1.4.0/lib/discard/errors.rb0000644000004100000410000000110315145704354017200 0ustar www-datawww-data# frozen_string_literal: true module Discard # = Discard Errors # # Generic exception class. class DiscardError < StandardError end # Raised by {Discard::Model#discard!} class RecordNotDiscarded < DiscardError attr_reader :record def initialize(message = nil, record = nil) @record = record super(message) end end # Raised by {Discard::Model#undiscard!} class RecordNotUndiscarded < DiscardError attr_reader :record def initialize(message = nil, record = nil) @record = record super(message) end end end discard-1.4.0/lib/discard.rb0000644000004100000410000000020315145704354015664 0ustar www-datawww-data# frozen_string_literal: true require "active_record" require "discard/version" require "discard/errors" require "discard/model" discard-1.4.0/discard.gemspec0000644000004100000410000000234015145704354016142 0ustar www-datawww-data# frozen_string_literal: true lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'discard/version' Gem::Specification.new do |spec| spec.name = "discard" spec.version = Discard::VERSION spec.authors = ["John Hawthorn"] spec.email = ["john.hawthorn@gmail.com"] spec.summary = %q{ActiveRecord soft-deletes done right} spec.description = %q{Allows marking ActiveRecord objects as discarded, and provides scopes for filtering.} spec.homepage = "https://github.com/jhawthorn/discard" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_dependency "activerecord", ">= 4.2", "< 9.0" spec.add_development_dependency "bundler" spec.add_development_dependency "rake", ">= 10.0" spec.add_development_dependency "rspec", "~> 3.5.0" spec.add_development_dependency "database_cleaner", "~> 1.5" spec.add_development_dependency "with_model", "~> 2.0" spec.add_development_dependency "sqlite3" end discard-1.4.0/LICENSE.txt0000644000004100000410000000207015145704354015007 0ustar www-datawww-dataThe MIT License (MIT) Copyright (c) 2017 John Hawthorn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. discard-1.4.0/.yardopts0000644000004100000410000000015415145704354015033 0ustar www-datawww-data--protected --no-private --embed-mixin ClassMethods - README.md CHANGELOG.md CODE_OF_CONDUCT.md LICENSE.txt discard-1.4.0/.rspec0000644000004100000410000000003615145704354014301 0ustar www-datawww-data--color --require spec_helper discard-1.4.0/Rakefile0000644000004100000410000000024615145704354014634 0ustar www-datawww-datarequire 'bundler/setup' require 'bundler/gem_tasks' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:rspec) desc 'Run the test suite' task default: :rspec discard-1.4.0/CODE_OF_CONDUCT.md0000644000004100000410000000623715145704354015774 0ustar www-datawww-data# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at john.hawthorn@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ discard-1.4.0/Gemfile0000644000004100000410000000036315145704354014462 0ustar www-datawww-datasource 'https://rubygems.org' rails_version = ENV['RAILS_VERSION'] gem 'activerecord', rails_version if sqlite_version = ENV['SQLITE_VERSION'] gem 'sqlite3', sqlite_version end # Specify your gem's dependencies in discard.gemspec gemspec discard-1.4.0/README.md0000644000004100000410000002316015145704354014446 0ustar www-datawww-data# Discard [![Test](https://github.com/jhawthorn/discard/actions/workflows/test.yml/badge.svg)](https://github.com/jhawthorn/discard/actions/workflows/test.yml) Soft deletes for ActiveRecord done right. ## What does this do? A simple ActiveRecord mixin to add conventions for flagging records as discarded. ## Installation Add this line to your application's Gemfile: ```ruby gem 'discard', '~> 1.4' ``` And then execute: $ bundle ## Usage **Declare a record as discardable** Declare the record as being discardable ``` ruby class Post < ActiveRecord::Base include Discard::Model end ``` You can either generate a migration using: ``` rails generate migration add_discarded_at_to_posts discarded_at:datetime:index ``` or create one yourself like the one below: ``` ruby class AddDiscardToPosts < ActiveRecord::Migration[5.0] def change add_column :posts, :discarded_at, :datetime add_index :posts, :discarded_at end end ``` #### Discard a record ```ruby Post.all # => [#] Post.kept # => [#] Post.discarded # => [] post = Post.first # => # post.discard # => true post.discard! # => Discard::RecordNotDiscarded: Failed to discard the record post.discarded? # => true post.undiscarded? # => false post.kept? # => false post.discarded_at # => 2017-04-18 18:49:49 -0700 Post.all # => [#] Post.kept # => [] Post.discarded # => [#] ``` ***From a controller*** Controller actions need a small modification to discard records instead of deleting them. Just replace `destroy` with `discard`. ``` ruby def destroy @post.discard redirect_to users_url, notice: "Post removed" end ``` #### Undiscard a record ```ruby post = Post.first # => # post.undiscard # => true post.undiscard! # => Discard::RecordNotUndiscarded: Failed to undiscard the record post.discarded_at # => nil ``` ***From a controller*** ```ruby def update @post.undiscard redirect_to users_url, notice: "Post undiscarded" end ``` #### Working with associations Under paranoia, soft deleting a record will destroy any `dependent: :destroy` associations. Probably not what you want! This leads to all dependent records also needing to be `acts_as_paranoid`, which makes restoring awkward: paranoia handles this by restoring any records which have their deleted_at set to a similar timestamp. Also, it doesn't always make sense to mark these records as deleted, it depends on the application. A better approach is to simply mark the one record as discarded, and use SQL joins to restrict finding these if that's desired. For example, in a blog comment system, with `Post`s and `Comment`s, you might want to discard the records independently. A user's comment history could include comments on deleted posts. ``` ruby Post.kept # SELECT * FROM posts WHERE discarded_at IS NULL Comment.kept # SELECT * FROM comments WHERE discarded_at IS NULL ``` Or you could decide that comments are dependent on their posts not being discarded. Just override the `kept` scope on the Comment model. ``` ruby class Comment < ActiveRecord::Base belongs_to :post include Discard::Model scope :kept, -> { undiscarded.joins(:post).merge(Post.kept) } def kept? undiscarded? && post.kept? end end Comment.kept # SELECT * FROM comments # INNER JOIN posts ON comments.post_id = posts.id # WHERE # comments.discarded_at IS NULL AND # posts.discarded_at IS NULL ``` SQL databases are very good at this, and performance should not be an issue. In both of these cases restoring either of these records will do right thing! #### Default scope It's usually undesirable to add a default scope. It will take more effort to work around and will cause more headaches. If you know you need a default scope, it's easy to add yourself ❤. ``` ruby class Post < ActiveRecord::Base include Discard::Model default_scope -> { kept } end Post.all # Only kept posts Post.with_discarded # All Posts Post.with_discarded.discarded # Only discarded posts ``` #### Custom column If you're migrating from paranoia, you might want to continue using the same column. ``` ruby class Post < ActiveRecord::Base include Discard::Model self.discard_column = :deleted_at end ``` #### Callbacks Callbacks can be run before, after, or around the discard and undiscard operations. A likely use is discarding or deleting associated records (but see "Working with associations" for an alternative). ``` ruby class Comment < ActiveRecord::Base include Discard::Model end class Post < ActiveRecord::Base include Discard::Model has_many :comments after_discard do comments.discard_all end after_undiscard do comments.undiscard_all end end ``` *Warning:* Please note that callbacks for save and update are run when discarding/undiscarding a record #### Performance tuning `discard_all` and `undiscard_all` is intended to behave like `destroy_all` which has callbacks, validations, and does one query per record. If performance is a big concern, you may consider replacing it with: `scope.update_all(discarded_at: Time.current)` or `scope.update_all(discarded_at: nil)` #### Working with Devise A common use case is to apply discard to a User record. Even though a user has been discarded they can still login and continue their session. If you are using Devise and wish for discarded users to be unable to login and stop their session you can override Devise's method. ```ruby class User < ActiveRecord::Base def active_for_authentication? super && !discarded? end end ``` ## Non-features * Special handling of AR counter cache columns - The counter cache counts the total number of records, both kept and discarded. * Recursive discards (like AR's dependent: destroy) - This can be avoided using queries (See "Working with associations") or emulated using callbacks. * Recursive restores - This concept is fundamentally broken, but not necessary if the recursive discards are avoided. ## Extensions Discard provides the smallest subset of soft-deletion features that we think are useful to all users of the gem. We welcome the addition of gems that work with Discard to provide additional features. - [discard-rails-observers](https://github.com/pelargir/discard-rails-observers) integrates discard with the [rails-observers gem](https://github.com/rails/rails-observers) ## Why not paranoia or acts_as_paranoid? I've worked with and have helped maintain [paranoia](https://github.com/rubysherpas/paranoia) for a while. I'm convinced it does the wrong thing for most cases. Paranoia and [acts_as_paranoid](https://github.com/ActsAsParanoid/acts_as_paranoid) both attempt to emulate deletes by setting a column and adding a default scope on the model. This requires some ActiveRecord hackery, and leads to some surprising and awkward behaviour. * A default scope is added to hide soft-deleted records, which necessitates adding `.with_deleted` to associations or anywhere soft-deleted records should be found. :disappointed: * Adding `belongs_to :child, -> { with_deleted }` helps, but doesn't work for joins and eager-loading [before Rails 5.2](https://github.com/rubysherpas/paranoia/issues/355) * `delete` is overridden (`really_delete` will actually delete the record) :unamused: * `destroy` is overridden (`really_destroy` will actually delete the record) :pensive: * `dependent: :destroy` associations are deleted when performing soft-destroys :scream: * requiring any dependent records to also be `acts_as_paranoid` to avoid losing data. :grimacing: There are some use cases where these behaviours make sense: if you really did want to _almost_ delete the record. More often developers are just looking to hide some records, or mark them as inactive. Discard takes a different approach. It doesn't override any ActiveRecord methods and instead simply provides convenience methods and scopes for discarding (hiding), restoring, and querying records. You can find more information about the history and purpose of Discard in [this blog post](https://supergood.software/introduction-to-discard/). ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. ## Contributing Please consider filing an issue with the details of any features you'd like to see before implementing them. Discard is feature-complete and we are only interested in adding additional features that won't require substantial maintenance burden and that will benefit all users of the gem. We encourage anyone that needs additional or different behaviour to either create their own gem that builds off of discard or implement a new package with the different behaviour. Discard is very simple and we like it that way. Creating your own clone or fork with slightly different behaviour may not be that much work! If you find a bug in discard, please report it! We try to keep up with any issues and keep the gem running smoothly for everyone! You can report issues [here](https://github.com/jhawthorn/discard/issues). ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). ## Acknowledgments * [Ben Morgan](https://github.com/BenMorganIO) who has done a great job maintaining paranoia * [Ryan Bigg](http://github.com/radar), the original author of paranoia (and many things), as a simpler replacement of acts_as_paranoid * All paranoia users and contributors discard-1.4.0/CHANGELOG.md0000644000004100000410000000167215145704354015004 0ustar www-datawww-data### Unreleased ### Version 1.4.0 Release date: 2024-11-05 * Support Rails 8.0 and 8.1 (#110, #111) * More descriptive error messages (#108) ### Version 1.3.0 Release date: 2023-08-17 * Fix `undiscard` so it returns false instead of nil when the record isn't discarded (#95, #96) ### Version 1.2.1 Release date: 2021-12-16 * Support for ActiveRecord 7 ### Version 1.2.0 Release date: 2020-02-17 * Add `discard_all!` and `undiscard_all!` * Add `undiscarded?` and `kept?` to match the scopes of the same names ### Version 1.1.0 Release date: 2019-05-03 * Support for ActiveRecord 6 * `discard_all` and `undiscard_all` now return affected records * Add `discard!` and `undiscard!` ### Version 1.0.0 Release date: 2018-03-16 * Add undiscard callbacks and `.undiscard_all` ### Version 0.2.0 Release date: 2017-11-22 * Add `.discard_all` * Add `undiscarded` scope * Add callbacks ### Version 0.1.0 Release date: 2017-04-28 * Initial version!