mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-05-01 11:19:32 -05:00
Merge branch 'dev' into dev
This commit is contained in:
commit
2989d2cfb4
@ -556,3 +556,11 @@ Initial API version.
|
||||
|
||||
- Consumption templates were refactored to workflows and API endpoints
|
||||
changed as such.
|
||||
|
||||
#### Version 5
|
||||
|
||||
- Added bulk deletion methods for documents and objects.
|
||||
|
||||
#### Version 6
|
||||
|
||||
- Moved acknowledge tasks endpoint to be under `/api/tasks/acknowledge/`.
|
||||
|
@ -253,6 +253,10 @@
|
||||
<context context-type="sourcefile">src/app/app.component.ts</context>
|
||||
<context context-type="linenumber">87</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">118</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html</context>
|
||||
<context context-type="linenumber">37</context>
|
||||
@ -1480,11 +1484,11 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">57</context>
|
||||
<context context-type="linenumber">59</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">86</context>
|
||||
<context context-type="linenumber">88</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.html</context>
|
||||
@ -2216,11 +2220,11 @@
|
||||
<source>Confirm delete</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">53</context>
|
||||
<context context-type="linenumber">55</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">80</context>
|
||||
<context context-type="linenumber">82</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
|
||||
@ -2235,18 +2239,18 @@
|
||||
<source>This operation will permanently delete this document.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">54</context>
|
||||
<context context-type="linenumber">56</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5641451190833696892" datatype="html">
|
||||
<source>This operation cannot be undone.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">55</context>
|
||||
<context context-type="linenumber">57</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">84</context>
|
||||
<context context-type="linenumber">86</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
|
||||
@ -2281,14 +2285,14 @@
|
||||
<source>Document deleted</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">64</context>
|
||||
<context context-type="linenumber">66</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7295637485862454066" datatype="html">
|
||||
<source>Error deleting document</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">69</context>
|
||||
<context context-type="linenumber">71</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
@ -2299,56 +2303,56 @@
|
||||
<source>This operation will permanently delete the selected documents.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">82</context>
|
||||
<context context-type="linenumber">84</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6804051092296228130" datatype="html">
|
||||
<source>This operation will permanently delete all documents in the trash.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">83</context>
|
||||
<context context-type="linenumber">85</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6996183233986182894" datatype="html">
|
||||
<source>Document(s) deleted</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">94</context>
|
||||
<context context-type="linenumber">96</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6962724852893361467" datatype="html">
|
||||
<source>Error deleting document(s)</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">101</context>
|
||||
<context context-type="linenumber">103</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7534569062269274401" datatype="html">
|
||||
<source>Document restored</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">113</context>
|
||||
<context context-type="linenumber">116</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="9136016619414048201" datatype="html">
|
||||
<source>Error restoring document</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">117</context>
|
||||
<context context-type="linenumber">126</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="960063472770266304" datatype="html">
|
||||
<source>Document(s) restored</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">127</context>
|
||||
<context context-type="linenumber">136</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8405416976953346141" datatype="html">
|
||||
<source>Error restoring document(s)</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
|
||||
<context context-type="linenumber">133</context>
|
||||
<context context-type="linenumber">142</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8119815638230251386" datatype="html">
|
||||
@ -5437,36 +5441,36 @@
|
||||
<source>TOTP activated successfully</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">263</context>
|
||||
<context context-type="linenumber">264</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3755006064892435830" datatype="html">
|
||||
<source>Error activating TOTP</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">265</context>
|
||||
<context context-type="linenumber">266</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">271</context>
|
||||
<context context-type="linenumber">272</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5919827473541889422" datatype="html">
|
||||
<source>TOTP deactivated successfully</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">287</context>
|
||||
<context context-type="linenumber">288</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6214722303383624015" datatype="html">
|
||||
<source>Error deactivating TOTP</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">289</context>
|
||||
<context context-type="linenumber">290</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">294</context>
|
||||
<context context-type="linenumber">295</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3797570084942068182" datatype="html">
|
||||
|
@ -16,6 +16,7 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { Router } from '@angular/router'
|
||||
|
||||
const documentsInTrash = [
|
||||
{
|
||||
@ -38,6 +39,7 @@ describe('TrashComponent', () => {
|
||||
let trashService: TrashService
|
||||
let modalService: NgbModal
|
||||
let toastService: ToastService
|
||||
let router: Router
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
@ -61,6 +63,7 @@ describe('TrashComponent', () => {
|
||||
trashService = TestBed.inject(TrashService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
router = TestBed.inject(Router)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
@ -161,6 +164,22 @@ describe('TrashComponent', () => {
|
||||
expect(restoreSpy).toHaveBeenCalledWith([1, 2])
|
||||
})
|
||||
|
||||
it('should offer link to restored document', () => {
|
||||
let toasts
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
toastService.getToasts().subscribe((allToasts) => {
|
||||
toasts = [...allToasts]
|
||||
})
|
||||
jest.spyOn(trashService, 'restoreDocuments').mockReturnValue(of('OK'))
|
||||
component.restore(documentsInTrash[0])
|
||||
expect(toasts.length).toEqual(1)
|
||||
toasts[0].action()
|
||||
expect(navigateSpy).toHaveBeenCalledWith([
|
||||
'documents',
|
||||
documentsInTrash[0].id,
|
||||
])
|
||||
})
|
||||
|
||||
it('should support toggle all items in view', () => {
|
||||
component.documentsInTrash = documentsInTrash
|
||||
expect(component.selectedDocuments.size).toEqual(0)
|
||||
|
@ -7,6 +7,7 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { Router } from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-trash',
|
||||
@ -26,7 +27,8 @@ export class TrashComponent implements OnDestroy {
|
||||
private trashService: TrashService,
|
||||
private toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
private settingsService: SettingsService
|
||||
private settingsService: SettingsService,
|
||||
private router: Router
|
||||
) {
|
||||
this.reload()
|
||||
}
|
||||
@ -110,7 +112,14 @@ export class TrashComponent implements OnDestroy {
|
||||
restore(document: Document) {
|
||||
this.trashService.restoreDocuments([document.id]).subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Document restored`)
|
||||
this.toastService.show({
|
||||
content: $localize`Document restored`,
|
||||
delay: 5000,
|
||||
actionName: $localize`Open document`,
|
||||
action: () => {
|
||||
this.router.navigate(['documents', document.id])
|
||||
},
|
||||
})
|
||||
this.reload()
|
||||
},
|
||||
error: (err) => {
|
||||
|
@ -30,4 +30,6 @@ export interface PaperlessTask extends ObjectWithId {
|
||||
result?: string
|
||||
|
||||
related_document?: number
|
||||
|
||||
owner?: number
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ describe('TasksService', () => {
|
||||
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
|
||||
tasksService.dismissTasks(new Set([1, 2, 3]))
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}acknowledge_tasks/`
|
||||
`${environment.apiBaseUrl}tasks/acknowledge/`
|
||||
)
|
||||
expect(req.request.method).toEqual('POST')
|
||||
expect(req.request.body).toEqual({
|
||||
|
@ -64,7 +64,7 @@ export class TasksService {
|
||||
|
||||
public dismissTasks(task_ids: Set<number>) {
|
||||
this.http
|
||||
.post(`${this.baseUrl}acknowledge_tasks/`, {
|
||||
.post(`${this.baseUrl}tasks/acknowledge/`, {
|
||||
tasks: [...task_ids],
|
||||
})
|
||||
.pipe(first())
|
||||
|
@ -3,7 +3,7 @@ const base_url = new URL(document.baseURI)
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiBaseUrl: document.baseURI + 'api/',
|
||||
apiVersion: '5',
|
||||
apiVersion: '6',
|
||||
appTitle: 'Paperless-ngx',
|
||||
version: '2.13.5',
|
||||
webSocketHost: window.location.host,
|
||||
|
@ -5,7 +5,7 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://localhost:8000/api/',
|
||||
apiVersion: '5',
|
||||
apiVersion: '6',
|
||||
appTitle: 'Paperless-ngx',
|
||||
version: 'DEVELOPMENT',
|
||||
webSocketHost: 'localhost:8000',
|
||||
|
@ -24,7 +24,7 @@ from documents.models import StoragePath
|
||||
from documents.permissions import set_permissions_for_object
|
||||
from documents.tasks import bulk_update_documents
|
||||
from documents.tasks import consume_file
|
||||
from documents.tasks import update_document_archive_file
|
||||
from documents.tasks import update_document_content_maybe_archive_file
|
||||
|
||||
logger: logging.Logger = logging.getLogger("paperless.bulk_edit")
|
||||
|
||||
@ -191,7 +191,7 @@ def delete(doc_ids: list[int]) -> Literal["OK"]:
|
||||
|
||||
def reprocess(doc_ids: list[int]) -> Literal["OK"]:
|
||||
for document_id in doc_ids:
|
||||
update_document_archive_file.delay(
|
||||
update_document_content_maybe_archive_file.delay(
|
||||
document_id=document_id,
|
||||
)
|
||||
|
||||
@ -245,7 +245,7 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]:
|
||||
doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
|
||||
doc.save()
|
||||
rotate_tasks.append(
|
||||
update_document_archive_file.s(
|
||||
update_document_content_maybe_archive_file.s(
|
||||
document_id=doc.id,
|
||||
),
|
||||
)
|
||||
@ -423,7 +423,7 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]:
|
||||
if doc.page_count is not None:
|
||||
doc.page_count = doc.page_count - len(pages)
|
||||
doc.save()
|
||||
update_document_archive_file.delay(document_id=doc.id)
|
||||
update_document_content_maybe_archive_file.delay(document_id=doc.id)
|
||||
logger.info(f"Deleted pages {pages} from document {doc.id}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error deleting pages from document {doc.id}: {e}")
|
||||
|
@ -9,7 +9,7 @@ from django.core.management.base import BaseCommand
|
||||
from documents.management.commands.mixins import MultiProcessMixin
|
||||
from documents.management.commands.mixins import ProgressBarMixin
|
||||
from documents.models import Document
|
||||
from documents.tasks import update_document_archive_file
|
||||
from documents.tasks import update_document_content_maybe_archive_file
|
||||
|
||||
logger = logging.getLogger("paperless.management.archiver")
|
||||
|
||||
@ -77,13 +77,13 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
|
||||
|
||||
if self.process_count == 1:
|
||||
for doc_id in document_ids:
|
||||
update_document_archive_file(doc_id)
|
||||
update_document_content_maybe_archive_file(doc_id)
|
||||
else: # pragma: no cover
|
||||
with multiprocessing.Pool(self.process_count) as pool:
|
||||
list(
|
||||
tqdm.tqdm(
|
||||
pool.imap_unordered(
|
||||
update_document_archive_file,
|
||||
update_document_content_maybe_archive_file,
|
||||
document_ids,
|
||||
),
|
||||
total=len(document_ids),
|
||||
|
28
src/documents/migrations/1057_paperlesstask_owner.py
Normal file
28
src/documents/migrations/1057_paperlesstask_owner.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-04 21:56
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1056_customfieldinstance_deleted_at_and_more"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="paperlesstask",
|
||||
name="owner",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="owner",
|
||||
),
|
||||
),
|
||||
]
|
@ -641,7 +641,7 @@ class UiSettings(models.Model):
|
||||
return self.user.username
|
||||
|
||||
|
||||
class PaperlessTask(models.Model):
|
||||
class PaperlessTask(ModelWithOwner):
|
||||
ALL_STATES = sorted(states.ALL_STATES)
|
||||
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
|
||||
|
||||
|
@ -1567,7 +1567,7 @@ class UiSettingsViewSerializer(serializers.ModelSerializer):
|
||||
return ui_settings
|
||||
|
||||
|
||||
class TasksViewSerializer(serializers.ModelSerializer):
|
||||
class TasksViewSerializer(OwnedObjectSerializer):
|
||||
class Meta:
|
||||
model = PaperlessTask
|
||||
depth = 1
|
||||
@ -1582,6 +1582,7 @@ class TasksViewSerializer(serializers.ModelSerializer):
|
||||
"result",
|
||||
"acknowledged",
|
||||
"related_document",
|
||||
"owner",
|
||||
)
|
||||
|
||||
type = serializers.SerializerMethodField()
|
||||
|
@ -940,9 +940,10 @@ def before_task_publish_handler(sender=None, headers=None, body=None, **kwargs):
|
||||
close_old_connections()
|
||||
|
||||
task_args = body[0]
|
||||
input_doc, _ = task_args
|
||||
input_doc, overrides = task_args
|
||||
|
||||
task_file_name = input_doc.original_file.name
|
||||
user_id = overrides.owner_id if overrides else None
|
||||
|
||||
PaperlessTask.objects.create(
|
||||
task_id=headers["id"],
|
||||
@ -953,6 +954,7 @@ def before_task_publish_handler(sender=None, headers=None, body=None, **kwargs):
|
||||
date_created=timezone.now(),
|
||||
date_started=None,
|
||||
date_done=None,
|
||||
owner_id=user_id,
|
||||
)
|
||||
except Exception: # pragma: no cover
|
||||
# Don't let an exception in the signal handlers prevent
|
||||
|
@ -206,9 +206,10 @@ def bulk_update_documents(document_ids):
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_document_archive_file(document_id):
|
||||
def update_document_content_maybe_archive_file(document_id):
|
||||
"""
|
||||
Re-creates the archive file of a document, including new OCR content and thumbnail
|
||||
Re-creates OCR content and thumbnail for a document, and archive file if
|
||||
it exists.
|
||||
"""
|
||||
document = Document.objects.get(id=document_id)
|
||||
|
||||
@ -234,8 +235,9 @@ def update_document_archive_file(document_id):
|
||||
document.get_public_filename(),
|
||||
)
|
||||
|
||||
if parser.get_archive_path():
|
||||
with transaction.atomic():
|
||||
with transaction.atomic():
|
||||
oldDocument = Document.objects.get(pk=document.pk)
|
||||
if parser.get_archive_path():
|
||||
with open(parser.get_archive_path(), "rb") as f:
|
||||
checksum = hashlib.md5(f.read()).hexdigest()
|
||||
# I'm going to save first so that in case the file move
|
||||
@ -246,7 +248,6 @@ def update_document_archive_file(document_id):
|
||||
document,
|
||||
archive_filename=True,
|
||||
)
|
||||
oldDocument = Document.objects.get(pk=document.pk)
|
||||
Document.objects.filter(pk=document.pk).update(
|
||||
archive_checksum=checksum,
|
||||
content=parser.get_text(),
|
||||
@ -268,24 +269,41 @@ def update_document_archive_file(document_id):
|
||||
],
|
||||
},
|
||||
additional_data={
|
||||
"reason": "Update document archive file",
|
||||
"reason": "Update document content",
|
||||
},
|
||||
action=LogEntry.Action.UPDATE,
|
||||
)
|
||||
else:
|
||||
Document.objects.filter(pk=document.pk).update(
|
||||
content=parser.get_text(),
|
||||
)
|
||||
|
||||
if settings.AUDIT_LOG_ENABLED:
|
||||
LogEntry.objects.log_create(
|
||||
instance=oldDocument,
|
||||
changes={
|
||||
"content": [oldDocument.content, parser.get_text()],
|
||||
},
|
||||
additional_data={
|
||||
"reason": "Update document content",
|
||||
},
|
||||
action=LogEntry.Action.UPDATE,
|
||||
)
|
||||
|
||||
with FileLock(settings.MEDIA_LOCK):
|
||||
with FileLock(settings.MEDIA_LOCK):
|
||||
if parser.get_archive_path():
|
||||
create_source_path_directory(document.archive_path)
|
||||
shutil.move(parser.get_archive_path(), document.archive_path)
|
||||
shutil.move(thumbnail, document.thumbnail_path)
|
||||
shutil.move(thumbnail, document.thumbnail_path)
|
||||
|
||||
document.refresh_from_db()
|
||||
logger.info(
|
||||
f"Updating index for document {document_id} ({document.archive_checksum})",
|
||||
)
|
||||
with index.open_index_writer() as writer:
|
||||
index.update_document(writer, document)
|
||||
document.refresh_from_db()
|
||||
logger.info(
|
||||
f"Updating index for document {document_id} ({document.archive_checksum})",
|
||||
)
|
||||
with index.open_index_writer() as writer:
|
||||
index.update_document(writer, document)
|
||||
|
||||
clear_document_caches(document.pk)
|
||||
clear_document_caches(document.pk)
|
||||
|
||||
except Exception:
|
||||
logger.exception(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import uuid
|
||||
|
||||
import celery
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
@ -11,7 +12,6 @@ from documents.tests.utils import DirectoriesMixin
|
||||
|
||||
class TestTasks(DirectoriesMixin, APITestCase):
|
||||
ENDPOINT = "/api/tasks/"
|
||||
ENDPOINT_ACKNOWLEDGE = "/api/acknowledge_tasks/"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@ -125,7 +125,7 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT_ACKNOWLEDGE,
|
||||
self.ENDPOINT + "acknowledge/",
|
||||
{"tasks": [task.id]},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@ -133,6 +133,52 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
def test_tasks_owner_aware(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing PaperlessTasks with owner and with no owner
|
||||
WHEN:
|
||||
- API call is made to get tasks
|
||||
THEN:
|
||||
- Only tasks with no owner or request user are returned
|
||||
"""
|
||||
|
||||
regular_user = User.objects.create_user(username="test")
|
||||
regular_user.user_permissions.add(*Permission.objects.all())
|
||||
self.client.logout()
|
||||
self.client.force_authenticate(user=regular_user)
|
||||
|
||||
task1 = PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_one.pdf",
|
||||
owner=self.user,
|
||||
)
|
||||
|
||||
task2 = PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_two.pdf",
|
||||
)
|
||||
|
||||
task3 = PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_three.pdf",
|
||||
owner=regular_user,
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
self.assertEqual(response.data[0]["task_id"], task3.task_id)
|
||||
self.assertEqual(response.data[1]["task_id"], task2.task_id)
|
||||
|
||||
acknowledge_response = self.client.post(
|
||||
self.ENDPOINT + "acknowledge/",
|
||||
{"tasks": [task1.id, task2.id, task3.id]},
|
||||
)
|
||||
self.assertEqual(acknowledge_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(acknowledge_response.data, {"result": 2})
|
||||
|
||||
def test_task_result_no_error(self):
|
||||
"""
|
||||
GIVEN:
|
||||
|
@ -607,7 +607,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
mock_consume_file.assert_not_called()
|
||||
|
||||
@mock.patch("documents.tasks.bulk_update_documents.si")
|
||||
@mock.patch("documents.tasks.update_document_archive_file.s")
|
||||
@mock.patch("documents.tasks.update_document_content_maybe_archive_file.s")
|
||||
@mock.patch("celery.chord.delay")
|
||||
def test_rotate(self, mock_chord, mock_update_document, mock_update_documents):
|
||||
"""
|
||||
@ -626,7 +626,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
self.assertEqual(result, "OK")
|
||||
|
||||
@mock.patch("documents.tasks.bulk_update_documents.si")
|
||||
@mock.patch("documents.tasks.update_document_archive_file.s")
|
||||
@mock.patch("documents.tasks.update_document_content_maybe_archive_file.s")
|
||||
@mock.patch("pikepdf.Pdf.save")
|
||||
def test_rotate_with_error(
|
||||
self,
|
||||
@ -654,7 +654,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
mock_update_archive_file.assert_not_called()
|
||||
|
||||
@mock.patch("documents.tasks.bulk_update_documents.si")
|
||||
@mock.patch("documents.tasks.update_document_archive_file.s")
|
||||
@mock.patch("documents.tasks.update_document_content_maybe_archive_file.s")
|
||||
@mock.patch("celery.chord.delay")
|
||||
def test_rotate_non_pdf(
|
||||
self,
|
||||
@ -680,7 +680,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
mock_chord.assert_called_once()
|
||||
self.assertEqual(result, "OK")
|
||||
|
||||
@mock.patch("documents.tasks.update_document_archive_file.delay")
|
||||
@mock.patch("documents.tasks.update_document_content_maybe_archive_file.delay")
|
||||
@mock.patch("pikepdf.Pdf.save")
|
||||
def test_delete_pages(self, mock_pdf_save, mock_update_archive_file):
|
||||
"""
|
||||
@ -705,7 +705,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
self.doc2.refresh_from_db()
|
||||
self.assertEqual(self.doc2.page_count, expected_page_count)
|
||||
|
||||
@mock.patch("documents.tasks.update_document_archive_file.delay")
|
||||
@mock.patch("documents.tasks.update_document_content_maybe_archive_file.delay")
|
||||
@mock.patch("pikepdf.Pdf.save")
|
||||
def test_delete_pages_with_error(self, mock_pdf_save, mock_update_archive_file):
|
||||
"""
|
||||
|
@ -13,7 +13,7 @@ from django.test import override_settings
|
||||
|
||||
from documents.file_handling import generate_filename
|
||||
from documents.models import Document
|
||||
from documents.tasks import update_document_archive_file
|
||||
from documents.tasks import update_document_content_maybe_archive_file
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
from documents.tests.utils import FileSystemAssertsMixin
|
||||
|
||||
@ -46,7 +46,7 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
os.path.join(self.dirs.originals_dir, f"{doc.id:07}.pdf"),
|
||||
)
|
||||
|
||||
update_document_archive_file(doc.pk)
|
||||
update_document_content_maybe_archive_file(doc.pk)
|
||||
|
||||
doc = Document.objects.get(id=doc.id)
|
||||
|
||||
@ -63,7 +63,7 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
doc.save()
|
||||
shutil.copy(sample_file, doc.source_path)
|
||||
|
||||
update_document_archive_file(doc.pk)
|
||||
update_document_content_maybe_archive_file(doc.pk)
|
||||
|
||||
doc = Document.objects.get(id=doc.id)
|
||||
|
||||
@ -94,8 +94,8 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
os.path.join(self.dirs.originals_dir, "document_01.pdf"),
|
||||
)
|
||||
|
||||
update_document_archive_file(doc2.pk)
|
||||
update_document_archive_file(doc1.pk)
|
||||
update_document_content_maybe_archive_file(doc2.pk)
|
||||
update_document_content_maybe_archive_file(doc1.pk)
|
||||
|
||||
doc1 = Document.objects.get(id=doc1.id)
|
||||
doc2 = Document.objects.get(id=doc2.id)
|
||||
|
@ -5,6 +5,7 @@ import celery
|
||||
from django.test import TestCase
|
||||
|
||||
from documents.data_models import ConsumableDocument
|
||||
from documents.data_models import DocumentMetadataOverrides
|
||||
from documents.data_models import DocumentSource
|
||||
from documents.models import PaperlessTask
|
||||
from documents.signals.handlers import before_task_publish_handler
|
||||
@ -48,7 +49,10 @@ class TestTaskSignalHandler(DirectoriesMixin, TestCase):
|
||||
source=DocumentSource.ConsumeFolder,
|
||||
original_file="/consume/hello-999.pdf",
|
||||
),
|
||||
None,
|
||||
DocumentMetadataOverrides(
|
||||
title="Hello world",
|
||||
owner_id=1,
|
||||
),
|
||||
),
|
||||
# kwargs
|
||||
{},
|
||||
@ -65,6 +69,7 @@ class TestTaskSignalHandler(DirectoriesMixin, TestCase):
|
||||
self.assertEqual(headers["id"], task.task_id)
|
||||
self.assertEqual("hello-999.pdf", task.task_file_name)
|
||||
self.assertEqual("documents.tasks.consume_file", task.task_name)
|
||||
self.assertEqual(1, task.owner_id)
|
||||
self.assertEqual(celery.states.PENDING, task.status)
|
||||
|
||||
def test_task_prerun_handler(self):
|
||||
|
@ -1,5 +1,7 @@
|
||||
import os
|
||||
import shutil
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
@ -184,3 +186,75 @@ class TestEmptyTrashTask(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
|
||||
tasks.empty_trash()
|
||||
self.assertEqual(Document.global_objects.count(), 0)
|
||||
|
||||
|
||||
class TestUpdateContent(DirectoriesMixin, TestCase):
|
||||
def test_update_content_maybe_archive_file(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing document with archive file
|
||||
WHEN:
|
||||
- Update content task is called
|
||||
THEN:
|
||||
- Document is reprocessed, content and checksum are updated
|
||||
"""
|
||||
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
||||
shutil.copy(
|
||||
Path(__file__).parent
|
||||
/ "samples"
|
||||
/ "documents"
|
||||
/ "originals"
|
||||
/ "0000001.pdf",
|
||||
sample1,
|
||||
)
|
||||
sample1_archive = self.dirs.archive_dir / "sample_archive.pdf"
|
||||
shutil.copy(
|
||||
Path(__file__).parent
|
||||
/ "samples"
|
||||
/ "documents"
|
||||
/ "originals"
|
||||
/ "0000001.pdf",
|
||||
sample1_archive,
|
||||
)
|
||||
doc = Document.objects.create(
|
||||
title="test",
|
||||
content="my document",
|
||||
checksum="wow",
|
||||
archive_checksum="wow",
|
||||
filename=sample1,
|
||||
mime_type="application/pdf",
|
||||
archive_filename=sample1_archive,
|
||||
)
|
||||
|
||||
tasks.update_document_content_maybe_archive_file(doc.pk)
|
||||
self.assertNotEqual(Document.objects.get(pk=doc.pk).content, "test")
|
||||
self.assertNotEqual(Document.objects.get(pk=doc.pk).archive_checksum, "wow")
|
||||
|
||||
def test_update_content_maybe_archive_file_no_archive(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing document without archive file
|
||||
WHEN:
|
||||
- Update content task is called
|
||||
THEN:
|
||||
- Document is reprocessed, content is updated
|
||||
"""
|
||||
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
||||
shutil.copy(
|
||||
Path(__file__).parent
|
||||
/ "samples"
|
||||
/ "documents"
|
||||
/ "originals"
|
||||
/ "0000001.pdf",
|
||||
sample1,
|
||||
)
|
||||
doc = Document.objects.create(
|
||||
title="test",
|
||||
content="my document",
|
||||
checksum="wow",
|
||||
filename=sample1,
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
tasks.update_document_content_maybe_archive_file(doc.pk)
|
||||
self.assertNotEqual(Document.objects.get(pk=doc.pk).content, "test")
|
||||
|
@ -1705,6 +1705,7 @@ class RemoteVersionView(GenericAPIView):
|
||||
class TasksViewSet(ReadOnlyModelViewSet):
|
||||
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||
serializer_class = TasksViewSerializer
|
||||
filter_backends = (ObjectOwnedOrGrantedPermissionsFilter,)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = (
|
||||
@ -1719,19 +1720,17 @@ class TasksViewSet(ReadOnlyModelViewSet):
|
||||
queryset = PaperlessTask.objects.filter(task_id=task_id)
|
||||
return queryset
|
||||
|
||||
|
||||
class AcknowledgeTasksView(GenericAPIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = AcknowledgeTasksViewSerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
@action(methods=["post"], detail=False)
|
||||
def acknowledge(self, request):
|
||||
serializer = AcknowledgeTasksViewSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
tasks = serializer.validated_data.get("tasks")
|
||||
task_ids = serializer.validated_data.get("tasks")
|
||||
|
||||
try:
|
||||
result = PaperlessTask.objects.filter(id__in=tasks).update(
|
||||
tasks = PaperlessTask.objects.filter(id__in=task_ids)
|
||||
if request.user is not None and not request.user.is_superuser:
|
||||
tasks = tasks.filter(owner=request.user) | tasks.filter(owner=None)
|
||||
result = tasks.update(
|
||||
acknowledged=True,
|
||||
)
|
||||
return Response({"result": result})
|
||||
|
@ -333,7 +333,7 @@ REST_FRAMEWORK = {
|
||||
"DEFAULT_VERSION": "1",
|
||||
# Make sure these are ordered and that the most recent version appears
|
||||
# last
|
||||
"ALLOWED_VERSIONS": ["1", "2", "3", "4", "5"],
|
||||
"ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6"],
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
|
@ -18,7 +18,6 @@ from django.views.static import serve
|
||||
from rest_framework.authtoken import views
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from documents.views import AcknowledgeTasksView
|
||||
from documents.views import BulkDownloadView
|
||||
from documents.views import BulkEditObjectsView
|
||||
from documents.views import BulkEditView
|
||||
@ -132,11 +131,6 @@ urlpatterns = [
|
||||
name="remoteversion",
|
||||
),
|
||||
re_path("^ui_settings/", UiSettingsView.as_view(), name="ui_settings"),
|
||||
re_path(
|
||||
"^acknowledge_tasks/",
|
||||
AcknowledgeTasksView.as_view(),
|
||||
name="acknowledge_tasks",
|
||||
),
|
||||
re_path(
|
||||
"^mail_accounts/test/",
|
||||
MailAccountTestView.as_view(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user