From 872fc206b4bf1af2126037c7ef405031d7debaf4 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 23 Apr 2024 01:14:46 -0700 Subject: [PATCH] Docs only [skip ci] --- .../admin/trash/trash.component.html | 42 ++--- .../components/admin/trash/trash.component.ts | 98 +++++++---- .../confirm-dialog.component.spec.ts | 10 -- .../confirm-dialog.component.ts | 20 --- .../document-detail.component.ts | 8 +- .../bulk-editor/bulk-editor.component.ts | 3 +- src-ui/src/app/services/trash.service.ts | 10 +- ...d_at_correspondent_restored_at_and_more.py | 154 ------------------ ...ocument_deleted_at_document_restored_at.py | 24 +++ src/documents/models.py | 20 +-- src/documents/signals/handlers.py | 2 - src/documents/tasks.py | 17 -- src/documents/views.py | 3 - ...ted_at_mailaccount_restored_at_and_more.py | 44 ----- 14 files changed, 132 insertions(+), 323 deletions(-) delete mode 100644 src/documents/migrations/1047_correspondent_deleted_at_correspondent_restored_at_and_more.py create mode 100644 src/documents/migrations/1049_document_deleted_at_document_restored_at.py delete mode 100644 src/paperless_mail/migrations/0024_mailaccount_deleted_at_mailaccount_restored_at_and_more.py diff --git a/src-ui/src/app/components/admin/trash/trash.component.html b/src-ui/src/app/components/admin/trash/trash.component.html index 2a2040126..451f22895 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.html +++ b/src-ui/src/app/components/admin/trash/trash.component.html @@ -3,19 +3,19 @@ title="Trash" i18n-title info="Manage trashed items." > - - -
- +
@@ -24,7 +24,7 @@ info="Manage trashed items."
- +
@@ -42,16 +42,16 @@ info="Manage trashed items." } - @for (object of trashedObjects; track object.id) { - + @for (document of documentsInTrash; track document.id) { +
- - + +
- {{ object['name'] ?? object['title'] }} - {{ object['deleted_at'] | customDate }} + {{ document.title }} + {{ document.deleted_at | customDate }}
@@ -59,16 +59,16 @@ info="Manage trashed items."
- - + +
- -
@@ -82,13 +82,13 @@ info="Manage trashed items." @if (!isLoading) {
- {trashedObjects.length, plural, =1 {One object in trash} other {{{trashedObjects.length || 0}} total objects in trash}} - @if (selectedObjects.size > 0) { -  ({{selectedObjects.size}} selected) + {documentsInTrash.length, plural, =1 {One object in trash} other {{{documentsInTrash.length || 0}} total documents in trash}} + @if (selectedDocuments.size > 0) { +  ({{selectedDocuments.size}} selected) }
- @if (trashedObjects.length > 20) { - + @if (documentsInTrash.length > 20) { + }
} diff --git a/src-ui/src/app/components/admin/trash/trash.component.ts b/src-ui/src/app/components/admin/trash/trash.component.ts index 89eb92615..c5e53bfc8 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.ts +++ b/src-ui/src/app/components/admin/trash/trash.component.ts @@ -1,57 +1,91 @@ -import { HttpClient } from '@angular/common/http' -import { Component } from '@angular/core' -import { ObjectWithId } from 'src/app/data/object-with-id' +import { Component, OnDestroy } from '@angular/core' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { Document } from 'src/app/data/document' import { ToastService } from 'src/app/services/toast.service' import { TrashService } from 'src/app/services/trash.service' -import { environment } from 'src/environments/environment' +import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' +import { Subject, takeUntil } from 'rxjs' @Component({ selector: 'pngx-trash', templateUrl: './trash.component.html', styleUrl: './trash.component.scss', }) -export class TrashComponent { - public trashedObjects: ObjectWithId[] = [] - public selectedObjects: Set = new Set() +export class TrashComponent implements OnDestroy { + public documentsInTrash: Document[] = [] + public selectedDocuments: Set = new Set() public togggleAll: boolean = false public page: number = 1 public isLoading: boolean = false + unsubscribeNotifier: Subject = new Subject() constructor( private trashService: TrashService, - private toastService: ToastService + private toastService: ToastService, + private modalService: NgbModal ) { this.reload() } + ngOnDestroy() { + this.unsubscribeNotifier.next() + this.unsubscribeNotifier.complete() + } + reload() { this.isLoading = true - this.trashService.getTrash().subscribe((trash) => { - this.trashedObjects = trash + this.trashService.getTrash().subscribe((documentsInTrash) => { + this.documentsInTrash = documentsInTrash this.isLoading = false - console.log('Trash:', trash) + console.log('Trash:', documentsInTrash) }) } - deleteObject(object: ObjectWithId) { - this.trashService.emptyTrash([object.id]).subscribe(() => { - this.toastService.showInfo($localize`Object deleted`) - this.reload() + delete(document: Document) { + let modal = this.modalService.open(ConfirmDialogComponent, { + backdrop: 'static', }) - } - - emptyTrash(objects: Set = null) { - console.log('Emptying trash') - this.trashService - .emptyTrash(objects ? Array.from(objects) : []) + modal.componentInstance.title = $localize`Confirm delete` + modal.componentInstance.messageBold = $localize`This operation will permanently delete this document.` + modal.componentInstance.message = $localize`This operation cannot be undone.` + modal.componentInstance.btnClass = 'btn-danger' + modal.componentInstance.btnCaption = $localize`Delete` + modal.componentInstance.confirmClicked + .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe(() => { - this.toastService.showInfo($localize`Object(s) deleted`) - this.reload() + modal.componentInstance.buttonsEnabled = false + this.trashService.emptyTrash([document.id]).subscribe(() => { + this.toastService.showInfo($localize`Document deleted`) + this.reload() + }) }) } - restoreObject(object: ObjectWithId) { - this.trashService.restoreObjects([object.id]).subscribe(() => { + emptyTrash(documents: Set = null) { + let modal = this.modalService.open(ConfirmDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.title = $localize`Confirm delete` + modal.componentInstance.messageBold = $localize`This operation will permanently delete ${ + documents?.size ?? $localize`all` + } documents.` + modal.componentInstance.message = $localize`This operation cannot be undone.` + modal.componentInstance.btnClass = 'btn-danger' + modal.componentInstance.btnCaption = $localize`Delete` + modal.componentInstance.confirmClicked + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => { + this.trashService + .emptyTrash(documents ? Array.from(documents) : []) + .subscribe(() => { + this.toastService.showInfo($localize`Document(s) deleted`) + this.reload() + }) + }) + } + + restore(document: Document) { + this.trashService.restoreDocuments([document.id]).subscribe(() => { this.toastService.showInfo($localize`Object restored`) this.reload() }) @@ -59,7 +93,7 @@ export class TrashComponent { restoreAll(objects: Set = null) { this.trashService - .restoreObjects(objects ? Array.from(this.selectedObjects) : []) + .restoreDocuments(objects ? Array.from(this.selectedDocuments) : []) .subscribe(() => { this.toastService.showInfo($localize`Object(s) restored`) this.reload() @@ -68,20 +102,20 @@ export class TrashComponent { toggleAll(event: PointerEvent) { if ((event.target as HTMLInputElement).checked) { - this.selectedObjects = new Set(this.trashedObjects.map((t) => t.id)) + this.selectedDocuments = new Set(this.documentsInTrash.map((t) => t.id)) } else { this.clearSelection() } } - toggleSelected(object: ObjectWithId) { - this.selectedObjects.has(object.id) - ? this.selectedObjects.delete(object.id) - : this.selectedObjects.add(object.id) + toggleSelected(object: Document) { + this.selectedDocuments.has(object.id) + ? this.selectedDocuments.delete(object.id) + : this.selectedDocuments.add(object.id) } clearSelection() { this.togggleAll = false - this.selectedObjects.clear() + this.selectedDocuments.clear() } } diff --git a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts index e2c16dbf8..0a15659aa 100644 --- a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts @@ -86,14 +86,4 @@ describe('ConfirmDialogComponent', () => { expect(closeModalSpy).toHaveBeenCalled() expect(confirmSubjectResult).toBeFalsy() }) - - it('should support delay confirm', fakeAsync(() => { - component.confirmButtonEnabled = false - component.delayConfirm(1) - expect(component.confirmButtonEnabled).toBeFalsy() - tick(1500) - fixture.detectChanges() - expect(component.confirmButtonEnabled).toBeTruthy() - discardPeriodicTasks() - })) }) diff --git a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts index 9c627ead0..2dfb6a15d 100644 --- a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts +++ b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts @@ -54,26 +54,6 @@ export class ConfirmDialogComponent { confirmSubject: Subject alternativeSubject: Subject - delayConfirm(seconds: number) { - const refreshInterval = 0.15 // s - - this.secondsTotal = seconds - this.seconds = seconds - - interval(refreshInterval * 1000) - .pipe( - take(this.secondsTotal / refreshInterval + 2) // need 2 more for animation to complete after 0 - ) - .subscribe((count) => { - this.seconds = Math.max( - 0, - this.secondsTotal - refreshInterval * (count + 1) - ) - this.confirmButtonEnabled = - this.secondsTotal - refreshInterval * count < 0 - }) - } - cancel() { this.confirmSubject?.next(false) this.confirmSubject?.complete() diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 339d35587..d6cbdd57e 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -773,11 +773,11 @@ export class DocumentDetailComponent let modal = this.modalService.open(ConfirmDialogComponent, { backdrop: 'static', }) - modal.componentInstance.title = $localize`Confirm delete` - modal.componentInstance.messageBold = $localize`Do you really want to delete document "${this.document.title}"?` - modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.` + modal.componentInstance.title = $localize`Confirm` + modal.componentInstance.messageBold = $localize`Do you really want to move the document "${this.document.title}" to the trash?` + modal.componentInstance.message = $localize`Documents can be restored prior to permanent deletion.` modal.componentInstance.btnClass = 'btn-danger' - modal.componentInstance.btnCaption = $localize`Delete document` + modal.componentInstance.btnCaption = $localize`Move to trash` this.subscribeModalDelete(modal) // so can be re-subscribed if error } diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 5bdb4df9b..763e69b0e 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -708,8 +708,9 @@ export class BulkEditorComponent let modal = this.modalService.open(ConfirmDialogComponent, { backdrop: 'static', }) - modal.componentInstance.title = $localize`Delete confirm` + modal.componentInstance.title = $localize`Confirm` modal.componentInstance.messageBold = $localize`Move ${this.list.selected.size} selected document(s) to the trash?` + modal.componentInstance.message = $localize`Documents can be restored prior to permanent deletion.` modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnCaption = $localize`Move to trash` modal.componentInstance.confirmClicked diff --git a/src-ui/src/app/services/trash.service.ts b/src-ui/src/app/services/trash.service.ts index 1df3abd5c..e1a310621 100644 --- a/src-ui/src/app/services/trash.service.ts +++ b/src-ui/src/app/services/trash.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http' import { Injectable } from '@angular/core' import { Observable } from 'rxjs' import { environment } from 'src/environments/environment' -import { ObjectWithId } from '../data/object-with-id' +import { Document } from '../data/document' @Injectable({ providedIn: 'root', @@ -10,18 +10,18 @@ import { ObjectWithId } from '../data/object-with-id' export class TrashService { constructor(private http: HttpClient) {} - public getTrash(): Observable { - return this.http.get(`${environment.apiBaseUrl}trash/`) + public getTrash(): Observable { + return this.http.get(`${environment.apiBaseUrl}trash/`) } - public emptyTrash(documents: number[] = []) { + public emptyTrash(documents: number[] = []): Observable { return this.http.post(`${environment.apiBaseUrl}trash/`, { action: 'empty', documents, }) } - public restoreObjects(documents: number[]): Observable { + public restoreDocuments(documents: number[]): Observable { return this.http.post(`${environment.apiBaseUrl}trash/`, { action: 'restore', documents, diff --git a/src/documents/migrations/1047_correspondent_deleted_at_correspondent_restored_at_and_more.py b/src/documents/migrations/1047_correspondent_deleted_at_correspondent_restored_at_and_more.py deleted file mode 100644 index 77f641e4b..000000000 --- a/src/documents/migrations/1047_correspondent_deleted_at_correspondent_restored_at_and_more.py +++ /dev/null @@ -1,154 +0,0 @@ -# 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), - ), - migrations.AddField( - model_name="note", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="note", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="customfield", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="customfield", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="customfieldinstance", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="customfieldinstance", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="savedviewfilterrule", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="savedviewfilterrule", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="sharelink", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="sharelink", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="workflowaction", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="workflowaction", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="workflowtrigger", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name="workflowtrigger", - name="restored_at", - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/src/documents/migrations/1049_document_deleted_at_document_restored_at.py b/src/documents/migrations/1049_document_deleted_at_document_restored_at.py new file mode 100644 index 000000000..80006824e --- /dev/null +++ b/src/documents/migrations/1049_document_deleted_at_document_restored_at.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.11 on 2024-04-23 07:56 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("documents", "1048_alter_savedviewfilterrule_rule_type"), + ] + + operations = [ + 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), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 21db78517..8ce038600 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -29,7 +29,7 @@ from documents.data_models import DocumentSource from documents.parsers import get_default_file_extension -class ModelWithOwner(SoftDeleteModel): +class ModelWithOwner(models.Model): owner = models.ForeignKey( User, blank=True, @@ -132,7 +132,7 @@ class StoragePath(MatchingModel): verbose_name_plural = _("storage paths") -class Document(ModelWithOwner): +class Document(SoftDeleteModel, ModelWithOwner): STORAGE_TYPE_UNENCRYPTED = "unencrypted" STORAGE_TYPE_GPG = "gpg" STORAGE_TYPES = ( @@ -462,7 +462,7 @@ class SavedView(ModelWithOwner): return f"SavedView {self.name}" -class SavedViewFilterRule(SoftDeleteModel): +class SavedViewFilterRule(models.Model): RULE_TYPES = [ (0, _("title contains")), (1, _("content contains")), @@ -694,7 +694,7 @@ class PaperlessTask(models.Model): return f"Task {self.task_id}" -class Note(SoftDeleteModel): +class Note(models.Model): note = models.TextField( _("content"), blank=True, @@ -734,7 +734,7 @@ class Note(SoftDeleteModel): return self.note -class ShareLink(SoftDeleteModel): +class ShareLink(models.Model): class FileVersion(models.TextChoices): ARCHIVE = ("archive", _("Archive")) ORIGINAL = ("original", _("Original")) @@ -794,7 +794,7 @@ class ShareLink(SoftDeleteModel): return f"Share Link for {self.document.title}" -class CustomField(SoftDeleteModel): +class CustomField(models.Model): """ Defines the name and type of a custom field """ @@ -840,7 +840,7 @@ class CustomField(SoftDeleteModel): return f"{self.name} : {self.data_type}" -class CustomFieldInstance(SoftDeleteModel): +class CustomFieldInstance(models.Model): """ A single instance of a field, attached to a CustomField for the name and type and attached to a single Document to be metadata for it @@ -941,7 +941,7 @@ if settings.AUDIT_LOG_ENABLED: auditlog.register(CustomFieldInstance) -class WorkflowTrigger(SoftDeleteModel): +class WorkflowTrigger(models.Model): class WorkflowTriggerMatching(models.IntegerChoices): # No auto matching NONE = MatchingModel.MATCH_NONE, _("None") @@ -1045,7 +1045,7 @@ class WorkflowTrigger(SoftDeleteModel): return f"WorkflowTrigger {self.pk}" -class WorkflowAction(SoftDeleteModel): +class WorkflowAction(models.Model): class WorkflowActionType(models.IntegerChoices): ASSIGNMENT = ( 1, @@ -1264,7 +1264,7 @@ class WorkflowAction(SoftDeleteModel): return f"WorkflowAction {self.pk}" -class Workflow(SoftDeleteModel): +class Workflow(models.Model): name = models.CharField(_("name"), max_length=256, unique=True) order = models.IntegerField(_("order"), default=0) diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 8113077ea..236a1705a 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -308,8 +308,6 @@ def cleanup_document_deletion(sender, instance, force=False, **kwargs): now = timezone.localtime(timezone.now()) if now - instance.deleted_at < timedelta(days=settings.EMPTY_TRASH_DELAY): return - # print(instance.pk, force, kwargs) - return with FileLock(settings.MEDIA_LOCK): if settings.TRASH_DIR: # Find a non-conflicting filename in case a document with the same diff --git a/src/documents/tasks.py b/src/documents/tasks.py index 475021328..ead3b7a35 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -307,27 +307,10 @@ def empty_trash(doc_ids=None): if doc_ids is not None else Document.deleted_objects.filter(deleted_at__gt=cutoff) ) - # print(documents, doc_ids) for doc in documents: - # with disable_signal( - # post_delete, - # receiver=cleanup_document_deletion, - # sender=Document, - # ): doc.delete() post_delete.send( sender=Document, instance=doc, force=True, ) - - # messages.log_messages() - - # if messages.has_error: - # raise SanityCheckFailedException("Sanity check failed with errors. See log.") - # elif messages.has_warning: - # return "Sanity check exited with warnings. See log." - # elif len(messages) > 0: - # return "Sanity check exited with infos. See log." - # else: - # return "No issues detected." diff --git a/src/documents/views.py b/src/documents/views.py index 50edc45bf..916a376e8 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -2077,9 +2077,6 @@ class TrashView(PassUserMixin): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - # user = self.request.user - # method = serializer.validated_data.get("method") - # parameters = serializer.validated_data.get("parameters") doc_ids = serializer.validated_data.get("documents") action = serializer.validated_data.get("action") if action == "restore": diff --git a/src/paperless_mail/migrations/0024_mailaccount_deleted_at_mailaccount_restored_at_and_more.py b/src/paperless_mail/migrations/0024_mailaccount_deleted_at_mailaccount_restored_at_and_more.py deleted file mode 100644 index 671971695..000000000 --- a/src/paperless_mail/migrations/0024_mailaccount_deleted_at_mailaccount_restored_at_and_more.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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), - ), - ]