Basic start of soft delete

Currently blocked by https://github.com/san4ezy/django_softdelete/pull/30
This commit is contained in:
shamoon 2024-04-22 15:19:05 -07:00
parent 9d4e2d4652
commit 06bb218c71
8 changed files with 183 additions and 3 deletions

View File

@ -17,6 +17,7 @@ django-extensions = "*"
django-filter = "~=24.2" django-filter = "~=24.2"
django-guardian = "*" django-guardian = "*"
django-multiselectfield = "*" django-multiselectfield = "*"
django-soft-delete = "*"
djangorestframework = "==3.14.0" djangorestframework = "==3.14.0"
djangorestframework-guardian = "*" djangorestframework-guardian = "*"
drf-writable-nested = "*" drf-writable-nested = "*"

9
Pipfile.lock generated
View File

@ -540,6 +540,15 @@
"index": "pypi", "index": "pypi",
"version": "==0.1.12" "version": "==0.1.12"
}, },
"django-soft-delete": {
"hashes": [
"sha256:2b28ab95d18847ea301c46f6f172ea878490fdf12261c697ec7e44b9d652f866",
"sha256:5afe70c0f5ce066c0e287c39806fb949ab66e73305a11b88d4c533247f3b96f0"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==1.0.12"
},
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
"sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8", "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8",

View File

@ -1362,6 +1362,20 @@ processing. This only has an effect if
Defaults to false. Defaults to false.
## Trash
#### [`EMPTY_TRASH_DELAY=<num>`](#EMPTY_TRASH_DELAY) {#EMPTY_TRASH_DELAY}
: Sets how long in days objects remain in the 'trash' before they are permanently deleted.
Defaults to 30 days, minimum of 1 day.
#### [`PAPERLESS_EMPTY_TRASH_TASK_CRON=<cron expression>`](#PAPERLESS_EMPTY_TRASH_TASK_CRON) {#PAPERLESS_EMPTY_TRASH_TASK_CRON}
: Configures the schedule to empty the trash of expired deleted objects.
Defaults to `0 1 * * *`, once per day.
## Binaries ## Binaries
There are a few external software packages that Paperless expects to There are a few external software packages that Paperless expects to

View File

@ -0,0 +1,84 @@
# Generated by Django 4.2.11 on 2024-04-22 20:44
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "1046_workflowaction_remove_all_correspondents_and_more"),
]
operations = [
migrations.AddField(
model_name="correspondent",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="correspondent",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="document",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="document",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="documenttype",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="documenttype",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="savedview",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="savedview",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="storagepath",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="storagepath",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="tag",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="tag",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="workflow",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="workflow",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@ -23,11 +23,13 @@ from multiselectfield import MultiSelectField
if settings.AUDIT_LOG_ENABLED: if settings.AUDIT_LOG_ENABLED:
from auditlog.registry import auditlog from auditlog.registry import auditlog
from django_softdelete.models import SoftDeleteModel
from documents.data_models import DocumentSource from documents.data_models import DocumentSource
from documents.parsers import get_default_file_extension from documents.parsers import get_default_file_extension
class ModelWithOwner(models.Model): class ModelWithOwner(SoftDeleteModel):
owner = models.ForeignKey( owner = models.ForeignKey(
User, User,
blank=True, blank=True,
@ -1262,7 +1264,7 @@ class WorkflowAction(models.Model):
return f"WorkflowAction {self.pk}" return f"WorkflowAction {self.pk}"
class Workflow(models.Model): class Workflow(SoftDeleteModel):
name = models.CharField(_("name"), max_length=256, unique=True) name = models.CharField(_("name"), max_length=256, unique=True)
order = models.IntegerField(_("order"), default=0) order = models.IntegerField(_("order"), default=0)

View File

@ -1,6 +1,7 @@
import logging import logging
import os import os
import shutil import shutil
from datetime import timedelta
from typing import Optional from typing import Optional
from celery import states from celery import states
@ -302,7 +303,13 @@ def set_storage_path(
@receiver(models.signals.post_delete, sender=Document) @receiver(models.signals.post_delete, sender=Document)
def cleanup_document_deletion(sender, instance, using, **kwargs): def cleanup_document_deletion(sender, instance, **kwargs):
now = timezone.localtime(timezone.now())
if now - instance.deleted_at < timedelta(days=settings.EMPTY_TRASH_DELAY):
logger.info(
f"Detected soft delete of {instance!s}. Deferring cleanup.",
)
return
with FileLock(settings.MEDIA_LOCK): with FileLock(settings.MEDIA_LOCK):
if settings.TRASH_DIR: if settings.TRASH_DIR:
# Find a non-conflicting filename in case a document with the same # Find a non-conflicting filename in case a document with the same

View File

@ -207,6 +207,19 @@ def _parse_beat_schedule() -> dict:
"expires": ((7.0 * 24.0) - 1.0) * 60.0 * 60.0, "expires": ((7.0 * 24.0) - 1.0) * 60.0 * 60.0,
}, },
}, },
{
"name": "Empty trash",
"env_key": "PAPERLESS_EMPTY_TRASH_TASK_CRON",
# Default daily at 01:00
"env_default": "0 1 * * *",
"task": "documents.tasks.sanity_check",
"options": {
# 1 hour before default schedule sends again
"expires": 23.0
* 60.0
* 60.0,
},
},
] ]
for task in tasks: for task in tasks:
# Either get the environment setting or use the default # Either get the environment setting or use the default
@ -1148,3 +1161,9 @@ EMAIL_SUBJECT_PREFIX: Final[str] = "[Paperless-ngx] "
if DEBUG: # pragma: no cover if DEBUG: # pragma: no cover
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = BASE_DIR / "sent_emails" EMAIL_FILE_PATH = BASE_DIR / "sent_emails"
###############################################################################
# Soft Delete
###############################################################################
EMPTY_TRASH_DELAY = max(__get_int("PAPERLESS_SOFT_DELETE_DELAY", 30), 1)

View File

@ -0,0 +1,44 @@
# Generated by Django 4.2.11 on 2024-04-22 20:44
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("paperless_mail", "0023_remove_mailrule_filter_attachment_filename_and_more"),
]
operations = [
migrations.AddField(
model_name="mailaccount",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="mailaccount",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="mailrule",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="mailrule",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="processedmail",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="processedmail",
name="restored_at",
field=models.DateTimeField(blank=True, null=True),
),
]