mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge branch 'feature-bulk-edit' into feature-bulk-editor
This commit is contained in:
commit
4cae338479
@ -5,85 +5,6 @@ Advanced topics
|
||||
Paperless offers a couple features that automate certain tasks and make your life
|
||||
easier.
|
||||
|
||||
Guesswork
|
||||
#########
|
||||
|
||||
|
||||
Any document you put into the consumption directory will be consumed, but if
|
||||
you name the file right, it'll automatically set some values in the database
|
||||
for you. This is is the logic the consumer follows:
|
||||
|
||||
1. Try to find the correspondent, title, and tags in the file name following
|
||||
the pattern: ``Date - Correspondent - Title - tag,tag,tag.pdf``. Note that
|
||||
the format of the date is **rigidly defined** as ``YYYYMMDDHHMMSSZ`` or
|
||||
``YYYYMMDDZ``. The ``Z`` refers "Zulu time" AKA "UTC".
|
||||
The tags are optional, so the format ``Date - Correspondent - Title.pdf``
|
||||
works as well.
|
||||
2. If that doesn't work, we skip the date and try this pattern:
|
||||
``Correspondent - Title - tag,tag,tag.pdf``.
|
||||
3. If that doesn't work, we try to find the correspondent and title in the file
|
||||
name following the pattern: ``Correspondent - Title.pdf``.
|
||||
4. If that doesn't work, just assume that the name of the file is the title.
|
||||
|
||||
So given the above, the following examples would work as you'd expect:
|
||||
|
||||
* ``20150314000700Z - Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
|
||||
* ``20150314Z - Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
|
||||
* ``Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
|
||||
* ``Another Company - Letter of Reference.jpg``
|
||||
* ``Dad's Recipe for Pancakes.png``
|
||||
|
||||
These however wouldn't work:
|
||||
|
||||
* ``2015-03-14 00:07:00 UTC - Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
|
||||
* ``2015-03-14 - Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
|
||||
* ``Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
|
||||
* ``Another Company- Letter of Reference.jpg``
|
||||
|
||||
Do I have to be so strict about naming?
|
||||
=======================================
|
||||
|
||||
Rather than using the strict document naming rules, one can also set the option
|
||||
``PAPERLESS_FILENAME_DATE_ORDER`` in ``paperless.conf`` to any date order
|
||||
that is accepted by dateparser_. Doing so will cause ``paperless`` to default
|
||||
to any date format that is found in the title, instead of a date pulled from
|
||||
the document's text, without requiring the strict formatting of the document
|
||||
filename as described above.
|
||||
|
||||
.. _dateparser: https://github.com/scrapinghub/dateparser/blob/v0.7.0/docs/usage.rst#settings
|
||||
|
||||
.. _advanced-transforming_filenames:
|
||||
|
||||
Transforming filenames for parsing
|
||||
==================================
|
||||
|
||||
Some devices can't produce filenames that can be parsed by the default
|
||||
parser. By configuring the option ``PAPERLESS_FILENAME_PARSE_TRANSFORMS`` in
|
||||
``paperless.conf`` one can add transformations that are applied to the filename
|
||||
before it's parsed.
|
||||
|
||||
The option contains a list of dictionaries of regular expressions (key:
|
||||
``pattern``) and replacements (key: ``repl``) in JSON format, which are
|
||||
applied in order by passing them to ``re.subn``. Transformation stops
|
||||
after the first match, so at most one transformation is applied. The general
|
||||
syntax is
|
||||
|
||||
.. code:: python
|
||||
|
||||
[{"pattern":"pattern1", "repl":"repl1"}, {"pattern":"pattern2", "repl":"repl2"}, ..., {"pattern":"patternN", "repl":"replN"}]
|
||||
|
||||
The example below is for a Brother ADS-2400N, a scanner that allows
|
||||
different names to different hardware buttons (useful for handling
|
||||
multiple entities in one instance), but insists on adding ``_<count>``
|
||||
to the filename.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Brother profile configuration, support "Name_Date_Count" (the default
|
||||
# setting) and "Name_Count" (use "Name" as tag and "Count" as title).
|
||||
PAPERLESS_FILENAME_PARSE_TRANSFORMS=[{"pattern":"^([a-z]+)_(\\d{8})_(\\d{6})_([0-9]+)\\.", "repl":"\\2\\3Z - \\4 - \\1."}, {"pattern":"^([a-z]+)_([0-9]+)\\.", "repl":" - \\2 - \\1."}]
|
||||
|
||||
|
||||
.. _advanced-matching:
|
||||
|
||||
Matching tags, correspondents and document types
|
||||
|
@ -400,11 +400,6 @@ PAPERLESS_FILENAME_DATE_ORDER=<format>
|
||||
|
||||
Defaults to none, which disables this feature.
|
||||
|
||||
PAPERLESS_FILENAME_PARSE_TRANSFORMS
|
||||
Transforms filenames before they are processed by paperless. See
|
||||
:ref:`advanced-transforming_filenames` for details.
|
||||
|
||||
Defaults to none, which disables this feature.
|
||||
|
||||
Binaries
|
||||
########
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="form-group paperless-input-select">
|
||||
<label [for]="inputId">{{title}}</label>
|
||||
<div [class.input-group]="showPlusButton()">
|
||||
<ng-select name="correspondent" [(ngModel)]="value"
|
||||
<ng-select name="inputId" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
[style.color]="textColor"
|
||||
[style.background]="backgroundColor"
|
||||
|
@ -1,4 +1,4 @@
|
||||
<app-page-header title="Dashboard" subTitle="Welcome to paperless-ng!">
|
||||
<app-page-header title="Dashboard" [subTitle]="subtitle">
|
||||
<img src="assets/logo.svg" height="80" class="m-2 d-none d-md-block">
|
||||
</app-page-header>
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Meta } from '@angular/platform-browser';
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
|
||||
@ -11,8 +12,29 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private savedViewService: SavedViewService) { }
|
||||
private savedViewService: SavedViewService,
|
||||
private meta: Meta
|
||||
) { }
|
||||
|
||||
get displayName() {
|
||||
let tagFullName = this.meta.getTag('name=full_name')
|
||||
let tagUsername = this.meta.getTag('name=username')
|
||||
if (tagFullName && tagFullName.content) {
|
||||
return tagFullName.content
|
||||
} else if (tagUsername && tagUsername.content) {
|
||||
return tagUsername.content
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
get subtitle() {
|
||||
if (this.displayName) {
|
||||
return `Hello ${this.displayName}, welcome to Paperless-ng!`
|
||||
} else {
|
||||
return `Welcome to Paperless-ng!`
|
||||
}
|
||||
}
|
||||
|
||||
savedViews: PaperlessSavedView[] = []
|
||||
|
||||
|
@ -23,7 +23,7 @@ export class SavedViewWidgetComponent implements OnInit {
|
||||
documents: PaperlessDocument[] = []
|
||||
|
||||
ngOnInit(): void {
|
||||
this.documentService.list(1,10,this.savedView.sort_field, this.savedView.sort_reverse, this.savedView.filter_rules).subscribe(result => {
|
||||
this.documentService.listFiltered(1,10,this.savedView.sort_field, this.savedView.sort_reverse, this.savedView.filter_rules).subscribe(result => {
|
||||
this.documents = result.results
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,15 @@
|
||||
<div class="card mb-3 bg-light shadow-sm">
|
||||
<div class="card mb-3 bg-light shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-2 d-none d-lg-block">
|
||||
<img [src]="getThumbUrl()" class="card-img doc-img border-right">
|
||||
<div class="col-md-2 d-none d-lg-block doc-img-background" [class.doc-img-background-selected]="selected">
|
||||
<img [src]="getThumbUrl()" class="card-img doc-img border-right" (click)="selected = selectable ? !selected : false">
|
||||
|
||||
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="selected = $event.target.checked">
|
||||
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card-body">
|
||||
|
@ -1,5 +1,6 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.result-content {
|
||||
color: darkgray;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@ -8,11 +9,31 @@
|
||||
object-position: top;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.search-score-bar {
|
||||
width: 100px;
|
||||
height: 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.document-card-check {
|
||||
display: none
|
||||
}
|
||||
|
||||
.document-card:hover .document-card-check {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-selected {
|
||||
border-color: $primary;
|
||||
}
|
||||
|
||||
.doc-img-background {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.doc-img-background-selected {
|
||||
background-color: $primaryFaded;
|
||||
}
|
@ -12,6 +12,25 @@ export class DocumentCardLargeComponent implements OnInit {
|
||||
|
||||
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
|
||||
|
||||
_selected = false
|
||||
|
||||
get selected() {
|
||||
return this._selected
|
||||
}
|
||||
|
||||
@Input()
|
||||
set selected(value: boolean) {
|
||||
this._selected = value
|
||||
this.selectedChange.emit(value)
|
||||
}
|
||||
|
||||
@Output()
|
||||
selectedChange = new EventEmitter<boolean>()
|
||||
|
||||
get selectable() {
|
||||
return this.selectedChange.observers.length > 0
|
||||
}
|
||||
|
||||
@Input()
|
||||
moreLikeThis: boolean = false
|
||||
|
||||
|
@ -10,7 +10,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
||||
<div *ngFor="let t of getTagsLimited$() | async">
|
||||
<app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag>
|
||||
|
@ -100,7 +100,7 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="displayMode == 'largeCards'">
|
||||
<app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)">
|
||||
<app-document-card-large [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)">
|
||||
</app-document-card-large>
|
||||
</div>
|
||||
|
||||
@ -115,7 +115,7 @@
|
||||
<th class="d-none d-xl-table-cell">Added</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let d of list.documents" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
|
||||
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
|
||||
<td>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (change)="list.setSelected(d, $event.target.checked)">
|
||||
@ -149,7 +149,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="m-n2 row m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
||||
<app-document-card-small [document]="d" [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" *ngFor="let d of list.documents" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
|
||||
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
||||
<app-document-card-small [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
||||
@ -139,6 +140,10 @@ export class DocumentListComponent implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
trackByDocumentId(index, item: PaperlessDocument) {
|
||||
return item.id
|
||||
}
|
||||
|
||||
private executeBulkOperation(method: string, args): Observable<any> {
|
||||
return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
|
||||
map(r => {
|
||||
|
@ -8,10 +8,9 @@
|
||||
<div class="modal-body">
|
||||
|
||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
||||
<app-input-text title="Match" formControlName="match"></app-input-text>
|
||||
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||
<app-input-check title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||
|
||||
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
|
||||
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
||||
|
@ -8,9 +8,9 @@
|
||||
<div class="modal-body">
|
||||
|
||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
||||
<app-input-text title="Match" formControlName="match"></app-input-text>
|
||||
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||
<app-input-check title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
|
||||
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@ -7,11 +7,21 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
||||
<app-input-select title="Colour" [items]="getColours()" formControlName="colour" [textColor]="getColor(objectForm.value.colour).textColor" [backgroundColor]="getColor(objectForm.value.colour).value"></app-input-select>
|
||||
|
||||
|
||||
<div class="form-group paperless-input-select">
|
||||
<label for="colour">Colour</label>
|
||||
<ng-select name="colour" formControlName="colour" [items]="getColours()" bindValue="id" bindLabel="name" [clearable]="false">
|
||||
<ng-template ng-option-tmp ng-label-tmp let-item="item">
|
||||
<span class="badge" [style.background]="item.value" [style.color]="item.textColor">{{item.name}}</span>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
</div>
|
||||
|
||||
<app-input-check title="Inbox tag" formControlName="is_inbox_tag" hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
|
||||
<app-input-text title="Match" formControlName="match"></app-input-text>
|
||||
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||
<app-input-check title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
|
||||
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
||||
|
@ -6,14 +6,14 @@ export const TAG_COLOURS = [
|
||||
{id: 1, value: "#a6cee3", name: "Light Blue", textColor: "#000000"},
|
||||
{id: 2, value: "#1f78b4", name: "Blue", textColor: "#ffffff"},
|
||||
{id: 3, value: "#b2df8a", name: "Light Green", textColor: "#000000"},
|
||||
{id: 4, value: "#33a02c", name: "Green", textColor: "#000000"},
|
||||
{id: 4, value: "#33a02c", name: "Green", textColor: "#ffffff"},
|
||||
{id: 5, value: "#fb9a99", name: "Light Red", textColor: "#000000"},
|
||||
{id: 6, value: "#e31a1c", name: "Red ", textColor: "#ffffff"},
|
||||
{id: 7, value: "#fdbf6f", name: "Light Orange", textColor: "#000000"},
|
||||
{id: 8, value: "#ff7f00", name: "Orange", textColor: "#000000"},
|
||||
{id: 9, value: "#cab2d6", name: "Light Violet", textColor: "#000000"},
|
||||
{id: 10, value: "#6a3d9a", name: "Violet", textColor: "#ffffff"},
|
||||
{id: 11, value: "#b15928", name: "Brown", textColor: "#000000"},
|
||||
{id: 11, value: "#b15928", name: "Brown", textColor: "#ffffff"},
|
||||
{id: 12, value: "#000000", name: "Black", textColor: "#ffffff"},
|
||||
{id: 13, value: "#cccccc", name: "Light Grey", textColor: "#000000"}
|
||||
]
|
||||
|
@ -1,18 +1,29 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import contextmanager
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.models.signals import post_save, m2m_changed
|
||||
from filelock import FileLock
|
||||
|
||||
from documents.models import Document
|
||||
from documents.settings import EXPORTER_FILE_NAME, EXPORTER_THUMBNAIL_NAME, \
|
||||
EXPORTER_ARCHIVE_NAME
|
||||
from ...file_handling import create_source_path_directory, \
|
||||
generate_unique_filename
|
||||
from ...file_handling import create_source_path_directory
|
||||
from ...mixins import Renderable
|
||||
from ...signals.handlers import update_filename_and_move_files
|
||||
|
||||
|
||||
@contextmanager
|
||||
def disable_signal(sig, receiver, sender):
|
||||
try:
|
||||
sig.disconnect(receiver=receiver, sender=sender)
|
||||
yield
|
||||
finally:
|
||||
sig.connect(receiver=receiver, sender=sender)
|
||||
|
||||
|
||||
class Command(Renderable, BaseCommand):
|
||||
@ -47,11 +58,16 @@ class Command(Renderable, BaseCommand):
|
||||
self.manifest = json.load(f)
|
||||
|
||||
self._check_manifest()
|
||||
with disable_signal(post_save,
|
||||
receiver=update_filename_and_move_files,
|
||||
sender=Document):
|
||||
with disable_signal(m2m_changed,
|
||||
receiver=update_filename_and_move_files,
|
||||
sender=Document.tags.through):
|
||||
# Fill up the database with whatever is in the manifest
|
||||
call_command("loaddata", manifest_path)
|
||||
|
||||
# Fill up the database with whatever is in the manifest
|
||||
call_command("loaddata", manifest_path)
|
||||
|
||||
self._import_files_from_manifest()
|
||||
self._import_files_from_manifest()
|
||||
|
||||
@staticmethod
|
||||
def _check_manifest_exists(path):
|
||||
@ -117,9 +133,6 @@ class Command(Renderable, BaseCommand):
|
||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||
|
||||
with FileLock(settings.MEDIA_LOCK):
|
||||
document.filename = generate_unique_filename(
|
||||
document, settings.ORIGINALS_DIR)
|
||||
|
||||
if os.path.isfile(document.source_path):
|
||||
raise FileExistsError(document.source_path)
|
||||
|
||||
|
@ -8,6 +8,8 @@
|
||||
<title>Paperless-ng</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="username" content="{{username}}">
|
||||
<meta name="full_name" content="{{full_name}}">
|
||||
<meta name="cookie_prefix" content="{{cookie_prefix}}">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="{% static 'frontend/styles.css' %}"></head>
|
||||
|
@ -8,7 +8,7 @@ from django.contrib.auth.models import User
|
||||
from rest_framework.test import APITestCase
|
||||
from whoosh.writing import AsyncWriter
|
||||
|
||||
from documents import index
|
||||
from documents import index, bulk_edit
|
||||
from documents.models import Document, Correspondent, DocumentType, Tag, SavedView
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
|
||||
@ -615,3 +615,115 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
||||
|
||||
v1 = SavedView.objects.get(id=v1.id)
|
||||
self.assertEqual(v1.filter_rules.count(), 0)
|
||||
|
||||
|
||||
class TestBulkEdit(DirectoriesMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBulkEdit, self).setUp()
|
||||
|
||||
user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_login(user=user)
|
||||
|
||||
patcher = mock.patch('documents.bulk_edit.async_task')
|
||||
self.async_task = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.c1 = Correspondent.objects.create(name="c1")
|
||||
self.c2 = Correspondent.objects.create(name="c2")
|
||||
self.dt1 = DocumentType.objects.create(name="dt1")
|
||||
self.dt2 = DocumentType.objects.create(name="dt2")
|
||||
self.t1 = Tag.objects.create(name="t1")
|
||||
self.t2 = Tag.objects.create(name="t2")
|
||||
self.doc1 = Document.objects.create(checksum="A", title="A")
|
||||
self.doc2 = Document.objects.create(checksum="B", title="B", correspondent=self.c1, document_type=self.dt1)
|
||||
self.doc3 = Document.objects.create(checksum="C", title="C", correspondent=self.c2, document_type=self.dt2)
|
||||
self.doc4 = Document.objects.create(checksum="D", title="D")
|
||||
self.doc5 = Document.objects.create(checksum="E", title="E")
|
||||
self.doc2.tags.add(self.t1)
|
||||
self.doc3.tags.add(self.t2)
|
||||
self.doc4.tags.add(self.t1, self.t2)
|
||||
|
||||
def test_set_correspondent(self):
|
||||
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
|
||||
bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], self.c2.id)
|
||||
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 3)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc2.id])
|
||||
|
||||
def test_unset_correspondent(self):
|
||||
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
|
||||
bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], None)
|
||||
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 0)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs['document_ids'], [self.doc2.id, self.doc3.id])
|
||||
|
||||
def test_set_document_type(self):
|
||||
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
|
||||
bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], self.dt2.id)
|
||||
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 3)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc2.id])
|
||||
|
||||
def test_unset_document_type(self):
|
||||
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
|
||||
bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], None)
|
||||
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 0)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs['document_ids'], [self.doc2.id, self.doc3.id])
|
||||
|
||||
def test_add_tag(self):
|
||||
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
|
||||
bulk_edit.add_tag([self.doc1.id, self.doc2.id, self.doc3.id, self.doc4.id], self.t1.id)
|
||||
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 4)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc3.id])
|
||||
|
||||
|
||||
def test_remove_tag(self):
|
||||
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
|
||||
bulk_edit.remove_tag([self.doc1.id, self.doc3.id, self.doc4.id], self.t1.id)
|
||||
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 1)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs['document_ids'], [self.doc4.id])
|
||||
|
||||
def test_delete(self):
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
bulk_edit.delete([self.doc1.id, self.doc2.id])
|
||||
self.assertEqual(Document.objects.count(), 3)
|
||||
self.assertCountEqual([doc.id for doc in Document.objects.all()], [self.doc3.id, self.doc4.id, self.doc5.id])
|
||||
|
||||
def test_api(self):
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
"documents": [self.doc1.id],
|
||||
"method": "delete",
|
||||
"parameters": {}
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(Document.objects.count(), 4)
|
||||
|
||||
def test_api_invalid_doc(self):
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
"documents": [-235],
|
||||
"method": "delete",
|
||||
"parameters": {}
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
|
||||
def test_api_invalid_method(self):
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
"documents": [self.doc2.id],
|
||||
"method": "exterminate",
|
||||
"parameters": {}
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
|
@ -24,11 +24,17 @@ class TestExportImport(DirectoriesMixin, TestCase):
|
||||
|
||||
file = os.path.join(self.dirs.originals_dir, "0000001.pdf")
|
||||
|
||||
Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow", filename="0000001.pdf", mime_type="application/pdf")
|
||||
Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG)
|
||||
Tag.objects.create(name="t")
|
||||
DocumentType.objects.create(name="dt")
|
||||
Correspondent.objects.create(name="c")
|
||||
d1 = Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow", filename="0000001.pdf", mime_type="application/pdf")
|
||||
d2 = Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG)
|
||||
t1 = Tag.objects.create(name="t")
|
||||
dt1 = DocumentType.objects.create(name="dt")
|
||||
c1 = Correspondent.objects.create(name="c")
|
||||
|
||||
d1.tags.add(t1)
|
||||
d1.correspondents = c1
|
||||
d1.document_type = dt1
|
||||
d1.save()
|
||||
d2.save()
|
||||
|
||||
target = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, target)
|
||||
@ -59,11 +65,25 @@ class TestExportImport(DirectoriesMixin, TestCase):
|
||||
self.assertEqual(checksum, element['fields']['archive_checksum'])
|
||||
|
||||
with paperless_environment() as dirs:
|
||||
self.assertEqual(Document.objects.count(), 2)
|
||||
Document.objects.all().delete()
|
||||
Correspondent.objects.all().delete()
|
||||
DocumentType.objects.all().delete()
|
||||
Tag.objects.all().delete()
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
|
||||
call_command('document_importer', target)
|
||||
self.assertEqual(Document.objects.count(), 2)
|
||||
messages = check_sanity()
|
||||
# everything is alright after the test
|
||||
self.assertEqual(len(messages), 0, str([str(m) for m in messages]))
|
||||
|
||||
@override_settings(
|
||||
PAPERLESS_FILENAME_FORMAT="{title}"
|
||||
)
|
||||
def test_exporter_with_filename_format(self):
|
||||
self.test_exporter()
|
||||
|
||||
def test_export_missing_files(self):
|
||||
|
||||
target = tempfile.mkdtemp()
|
||||
|
@ -58,6 +58,8 @@ class IndexView(TemplateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['cookie_prefix'] = settings.COOKIE_PREFIX
|
||||
context['username'] = self.request.user.username
|
||||
context['full_name'] = self.request.user.get_full_name()
|
||||
return context
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user