mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Merge branch 'feature-bulk-edit' into feature-bulk-editor
This commit is contained in:
		@@ -5,85 +5,6 @@ Advanced topics
 | 
				
			|||||||
Paperless offers a couple features that automate certain tasks and make your life
 | 
					Paperless offers a couple features that automate certain tasks and make your life
 | 
				
			||||||
easier.
 | 
					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:
 | 
					.. _advanced-matching:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Matching tags, correspondents and document types
 | 
					Matching tags, correspondents and document types
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -400,11 +400,6 @@ PAPERLESS_FILENAME_DATE_ORDER=<format>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Defaults to none, which disables this feature.
 | 
					    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
 | 
					Binaries
 | 
				
			||||||
########
 | 
					########
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
<div class="form-group paperless-input-select">
 | 
					<div class="form-group paperless-input-select">
 | 
				
			||||||
  <label [for]="inputId">{{title}}</label>
 | 
					  <label [for]="inputId">{{title}}</label>
 | 
				
			||||||
  <div [class.input-group]="showPlusButton()">
 | 
					  <div [class.input-group]="showPlusButton()">
 | 
				
			||||||
    <ng-select name="correspondent" [(ngModel)]="value"
 | 
					    <ng-select name="inputId" [(ngModel)]="value"
 | 
				
			||||||
      [disabled]="disabled"
 | 
					      [disabled]="disabled"
 | 
				
			||||||
      [style.color]="textColor"
 | 
					      [style.color]="textColor"
 | 
				
			||||||
      [style.background]="backgroundColor"
 | 
					      [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">
 | 
					  <img src="assets/logo.svg" height="80" class="m-2 d-none d-md-block">
 | 
				
			||||||
</app-page-header>
 | 
					</app-page-header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import { Component, OnInit } from '@angular/core';
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
 | 
					import { Meta } from '@angular/platform-browser';
 | 
				
			||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
 | 
					import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
 | 
				
			||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
 | 
					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 {
 | 
					export class DashboardComponent implements OnInit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  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[] = []
 | 
					  savedViews: PaperlessSavedView[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@ export class SavedViewWidgetComponent implements OnInit {
 | 
				
			|||||||
  documents: PaperlessDocument[] = []
 | 
					  documents: PaperlessDocument[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit(): void {
 | 
					  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
 | 
					      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="row no-gutters">
 | 
				
			||||||
    <div class="col-md-2 d-none d-lg-block">
 | 
					    <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">
 | 
					      <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>
 | 
				
			||||||
    <div class="col">
 | 
					    <div class="col">
 | 
				
			||||||
      <div class="card-body">
 | 
					      <div class="card-body">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
 | 
					@import "/src/theme";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.result-content {
 | 
					.result-content {
 | 
				
			||||||
  color: darkgray;
 | 
					 | 
				
			||||||
  overflow-wrap: anywhere;
 | 
					  overflow-wrap: anywhere;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -8,7 +9,7 @@
 | 
				
			|||||||
  object-position: top;
 | 
					  object-position: top;
 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  mix-blend-mode: multiply;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.search-score-bar {
 | 
					.search-score-bar {
 | 
				
			||||||
@@ -16,3 +17,23 @@
 | 
				
			|||||||
  height: 5px;
 | 
					  height: 5px;
 | 
				
			||||||
  margin-top: 2px;
 | 
					  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) { }
 | 
					  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()
 | 
					  @Input()
 | 
				
			||||||
  moreLikeThis: boolean = false
 | 
					  moreLikeThis: boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,6 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
 | 
					      <div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
 | 
				
			||||||
        <div *ngFor="let t of getTagsLimited$() | async">
 | 
					        <div *ngFor="let t of getTagsLimited$() | async">
 | 
				
			||||||
          <app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag>
 | 
					          <app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -100,7 +100,7 @@
 | 
				
			|||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div *ngIf="displayMode == 'largeCards'">
 | 
					<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>
 | 
					  </app-document-card-large>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -115,7 +115,7 @@
 | 
				
			|||||||
    <th class="d-none d-xl-table-cell">Added</th>
 | 
					    <th class="d-none d-xl-table-cell">Added</th>
 | 
				
			||||||
  </thead>
 | 
					  </thead>
 | 
				
			||||||
  <tbody>
 | 
					  <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>
 | 
					      <td>
 | 
				
			||||||
        <div class="custom-control custom-checkbox">
 | 
					        <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)">
 | 
					          <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>
 | 
					  </tbody>
 | 
				
			||||||
</table>
 | 
					</table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
 | 
				
			||||||
<div class="m-n2 row 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>
 | 
				
			||||||
  <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>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router';
 | 
				
			|||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
 | 
					import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
 | 
				
			||||||
import { Observable } from 'rxjs';
 | 
					import { Observable } from 'rxjs';
 | 
				
			||||||
import { map } from 'rxjs/operators';
 | 
					import { map } from 'rxjs/operators';
 | 
				
			||||||
 | 
					import { PaperlessDocument } from 'src/app/data/paperless-document';
 | 
				
			||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
 | 
					import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
 | 
				
			||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
 | 
					import { DocumentListViewService } from 'src/app/services/document-list-view.service';
 | 
				
			||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.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> {
 | 
					  private executeBulkOperation(method: string, args): Observable<any> {
 | 
				
			||||||
    return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
 | 
					    return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
 | 
				
			||||||
      map(r => {
 | 
					      map(r => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,10 +8,9 @@
 | 
				
			|||||||
  <div class="modal-body">
 | 
					  <div class="modal-body">
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    <app-input-text title="Name" formControlName="name"></app-input-text>
 | 
					    <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-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>
 | 
				
			||||||
  <div class="modal-footer">
 | 
					  <div class="modal-footer">
 | 
				
			||||||
    <button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
 | 
					    <button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,9 +8,9 @@
 | 
				
			|||||||
    <div class="modal-body">
 | 
					    <div class="modal-body">
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      <app-input-text title="Name" formControlName="name"></app-input-text>
 | 
					      <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-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>
 | 
				
			||||||
    <div class="modal-footer">
 | 
					    <div class="modal-footer">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,11 +7,21 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="modal-body">
 | 
					    <div class="modal-body">
 | 
				
			||||||
      <app-input-text title="Name" formControlName="name"></app-input-text>
 | 
					      <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-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-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>
 | 
				
			||||||
    <div class="modal-footer">
 | 
					    <div class="modal-footer">
 | 
				
			||||||
      <button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
 | 
					      <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: 1, value: "#a6cee3", name: "Light Blue", textColor: "#000000"},
 | 
				
			||||||
    {id: 2, value: "#1f78b4", name: "Blue", textColor: "#ffffff"},
 | 
					    {id: 2, value: "#1f78b4", name: "Blue", textColor: "#ffffff"},
 | 
				
			||||||
    {id: 3, value: "#b2df8a", name: "Light Green", textColor: "#000000"},
 | 
					    {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: 5, value: "#fb9a99", name: "Light Red", textColor: "#000000"},
 | 
				
			||||||
    {id: 6, value: "#e31a1c", name: "Red ", textColor: "#ffffff"},
 | 
					    {id: 6, value: "#e31a1c", name: "Red ", textColor: "#ffffff"},
 | 
				
			||||||
    {id: 7, value: "#fdbf6f", name: "Light Orange", textColor: "#000000"},
 | 
					    {id: 7, value: "#fdbf6f", name: "Light Orange", textColor: "#000000"},
 | 
				
			||||||
    {id: 8, value: "#ff7f00", name: "Orange", textColor: "#000000"},
 | 
					    {id: 8, value: "#ff7f00", name: "Orange", textColor: "#000000"},
 | 
				
			||||||
    {id: 9, value: "#cab2d6", name: "Light Violet", textColor: "#000000"},
 | 
					    {id: 9, value: "#cab2d6", name: "Light Violet", textColor: "#000000"},
 | 
				
			||||||
    {id: 10, value: "#6a3d9a", name: "Violet", textColor: "#ffffff"},
 | 
					    {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: 12, value: "#000000", name: "Black", textColor: "#ffffff"},
 | 
				
			||||||
    {id: 13, value: "#cccccc", name: "Light Grey", textColor: "#000000"}
 | 
					    {id: 13, value: "#cccccc", name: "Light Grey", textColor: "#000000"}
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,29 @@
 | 
				
			|||||||
import json
 | 
					import json
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import shutil
 | 
					import shutil
 | 
				
			||||||
 | 
					from contextlib import contextmanager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.core.management import call_command
 | 
					from django.core.management import call_command
 | 
				
			||||||
from django.core.management.base import BaseCommand, CommandError
 | 
					from django.core.management.base import BaseCommand, CommandError
 | 
				
			||||||
 | 
					from django.db.models.signals import post_save, m2m_changed
 | 
				
			||||||
from filelock import FileLock
 | 
					from filelock import FileLock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from documents.models import Document
 | 
					from documents.models import Document
 | 
				
			||||||
from documents.settings import EXPORTER_FILE_NAME, EXPORTER_THUMBNAIL_NAME, \
 | 
					from documents.settings import EXPORTER_FILE_NAME, EXPORTER_THUMBNAIL_NAME, \
 | 
				
			||||||
    EXPORTER_ARCHIVE_NAME
 | 
					    EXPORTER_ARCHIVE_NAME
 | 
				
			||||||
from ...file_handling import create_source_path_directory, \
 | 
					from ...file_handling import create_source_path_directory
 | 
				
			||||||
    generate_unique_filename
 | 
					 | 
				
			||||||
from ...mixins import Renderable
 | 
					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):
 | 
					class Command(Renderable, BaseCommand):
 | 
				
			||||||
@@ -47,7 +58,12 @@ class Command(Renderable, BaseCommand):
 | 
				
			|||||||
            self.manifest = json.load(f)
 | 
					            self.manifest = json.load(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self._check_manifest()
 | 
					        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
 | 
					                # Fill up the database with whatever is in the manifest
 | 
				
			||||||
                call_command("loaddata", manifest_path)
 | 
					                call_command("loaddata", manifest_path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -117,9 +133,6 @@ class Command(Renderable, BaseCommand):
 | 
				
			|||||||
            document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
 | 
					            document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            with FileLock(settings.MEDIA_LOCK):
 | 
					            with FileLock(settings.MEDIA_LOCK):
 | 
				
			||||||
                document.filename = generate_unique_filename(
 | 
					 | 
				
			||||||
                    document, settings.ORIGINALS_DIR)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if os.path.isfile(document.source_path):
 | 
					                if os.path.isfile(document.source_path):
 | 
				
			||||||
                    raise FileExistsError(document.source_path)
 | 
					                    raise FileExistsError(document.source_path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,8 @@
 | 
				
			|||||||
  <title>Paperless-ng</title>
 | 
					  <title>Paperless-ng</title>
 | 
				
			||||||
  <base href="/">
 | 
					  <base href="/">
 | 
				
			||||||
  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
					  <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}}">
 | 
						<meta name="cookie_prefix" content="{{cookie_prefix}}">
 | 
				
			||||||
  <link rel="icon" type="image/x-icon" href="favicon.ico">
 | 
					  <link rel="icon" type="image/x-icon" href="favicon.ico">
 | 
				
			||||||
<link rel="stylesheet" href="{% static 'frontend/styles.css' %}"></head>
 | 
					<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 rest_framework.test import APITestCase
 | 
				
			||||||
from whoosh.writing import AsyncWriter
 | 
					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.models import Document, Correspondent, DocumentType, Tag, SavedView
 | 
				
			||||||
from documents.tests.utils import DirectoriesMixin
 | 
					from documents.tests.utils import DirectoriesMixin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -615,3 +615,115 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        v1 = SavedView.objects.get(id=v1.id)
 | 
					        v1 = SavedView.objects.get(id=v1.id)
 | 
				
			||||||
        self.assertEqual(v1.filter_rules.count(), 0)
 | 
					        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")
 | 
					        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")
 | 
					        d1 = 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)
 | 
					        d2 = 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")
 | 
					        t1 = Tag.objects.create(name="t")
 | 
				
			||||||
        DocumentType.objects.create(name="dt")
 | 
					        dt1 = DocumentType.objects.create(name="dt")
 | 
				
			||||||
        Correspondent.objects.create(name="c")
 | 
					        c1 = Correspondent.objects.create(name="c")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        d1.tags.add(t1)
 | 
				
			||||||
 | 
					        d1.correspondents = c1
 | 
				
			||||||
 | 
					        d1.document_type = dt1
 | 
				
			||||||
 | 
					        d1.save()
 | 
				
			||||||
 | 
					        d2.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        target = tempfile.mkdtemp()
 | 
					        target = tempfile.mkdtemp()
 | 
				
			||||||
        self.addCleanup(shutil.rmtree, target)
 | 
					        self.addCleanup(shutil.rmtree, target)
 | 
				
			||||||
@@ -59,11 +65,25 @@ class TestExportImport(DirectoriesMixin, TestCase):
 | 
				
			|||||||
                    self.assertEqual(checksum, element['fields']['archive_checksum'])
 | 
					                    self.assertEqual(checksum, element['fields']['archive_checksum'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with paperless_environment() as dirs:
 | 
					        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)
 | 
					            call_command('document_importer', target)
 | 
				
			||||||
 | 
					            self.assertEqual(Document.objects.count(), 2)
 | 
				
			||||||
            messages = check_sanity()
 | 
					            messages = check_sanity()
 | 
				
			||||||
            # everything is alright after the test
 | 
					            # everything is alright after the test
 | 
				
			||||||
            self.assertEqual(len(messages), 0, str([str(m) for m in messages]))
 | 
					            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):
 | 
					    def test_export_missing_files(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        target = tempfile.mkdtemp()
 | 
					        target = tempfile.mkdtemp()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,6 +58,8 @@ class IndexView(TemplateView):
 | 
				
			|||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        context = super().get_context_data(**kwargs)
 | 
					        context = super().get_context_data(**kwargs)
 | 
				
			||||||
        context['cookie_prefix'] = settings.COOKIE_PREFIX
 | 
					        context['cookie_prefix'] = settings.COOKIE_PREFIX
 | 
				
			||||||
 | 
					        context['username'] = self.request.user.username
 | 
				
			||||||
 | 
					        context['full_name'] = self.request.user.get_full_name()
 | 
				
			||||||
        return context
 | 
					        return context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user