God dammit past me - why didn't you bookmark the "how to use django and a deleted_items table" article to do easy-to-manage soft deletes in a system?

The general idea was to have a deleted_items table, available in django admin, that items are copied to when you call delete() on an object. It's easy to administer and you don't need loads of special edge cases in code.

This ring any bells, internet ppl?

P.S. Yes this is not perfect. The article goes into detail about these tradeoffs too.

@mrchrisadams I like this idea. I've just been adding `trashed` properties to models where I want them to be recoverable by users, but that always involves remembering to alter your queries everywhere to make sure they exclude trashed items!

@mark exactly. It also makes it easy to tell when you really really definitelty have deleted something, because there's only one table to look inside.

I think the article mentioned using a JSON binary field in the table to make it easy to dump serialised versions of an object in there too.

@mrchrisadams @mark Off the top of my head but: Create a view of the model table that explicitly excludes rows where "trashed" is true. Use that view everywhere, rather than the table. Now you never have to modify other queries to know about the underlying model table and trashed state (except when trashing a row).
@mrchrisadams @mark I don't think I'd want to persist deleted rows in some other form to a different table, because it might break referential integrity unless you do a lot of special-case handling. But I haven't thought much about it beyond that gut feeling, so maybe there's a reason that wouldn't be an issue.
@mrchrisadams @mark I guess this might be a good case for replacing refs to a deleted row with refs to a sentinel row in the same table. That way referential integrity is maintained and you can say "deleted item" in results and UI without special cases. Also you can _really_ delete the row rather than doing a soft delete. 🤔
@mrchrisadams I came across this approach yesterday, looks simple enough (with some performance caveats). This is
Elixir code, but the concept should transfer easily.
https://www.ash-hq.org/docs/guides/ash_archival/latest/topics/archival
Guide: Archival

Read the "Archival" guide on Ash HQ

Ash HQ
@mrchrisadams There's a library for it in Ruby, dunno about Python though. The Ruby thing was called acts_as_paranoid, I think... maybe having a fish around that search term might help.