mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-09 09:58:20 -05:00
Chore: Convert the consumer to a plugin (#6361)
This commit is contained in:
parent
e837f1e85b
commit
b720aa3cd1
36
paperless-ngx.code-workspace
Normal file
36
paperless-ngx.code-workspace
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./src",
|
||||||
|
"name": "Backend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./src-ui",
|
||||||
|
"name": "Frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.github",
|
||||||
|
"name": "CI/CD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./docs",
|
||||||
|
"name": "Documentation"
|
||||||
|
}
|
||||||
|
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"files.exclude": {
|
||||||
|
"**/__pycache__": true,
|
||||||
|
"**/.mypy_cache": true,
|
||||||
|
"**/.ruff_cache": true,
|
||||||
|
"**/.pytest_cache": true,
|
||||||
|
"**/.idea": true,
|
||||||
|
"**/.venv": true,
|
||||||
|
"**/.coverage": true,
|
||||||
|
"**/coverage.json": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ import { environment } from 'src/environments/environment'
|
|||||||
import { WebsocketConsumerStatusMessage } from '../data/websocket-consumer-status-message'
|
import { WebsocketConsumerStatusMessage } from '../data/websocket-consumer-status-message'
|
||||||
import { SettingsService } from './settings.service'
|
import { SettingsService } from './settings.service'
|
||||||
|
|
||||||
// see ConsumerFilePhase in src/documents/consumer.py
|
// see ProgressStatusOptions in src/documents/plugins/helpers.py
|
||||||
export enum FileStatusPhase {
|
export enum FileStatusPhase {
|
||||||
STARTED = 0,
|
STARTED = 0,
|
||||||
UPLOADING = 1,
|
UPLOADING = 1,
|
||||||
|
@ -2,15 +2,13 @@ import datetime
|
|||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import magic
|
import magic
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from channels.layers import get_channel_layer
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@ -20,6 +18,7 @@ from filelock import FileLock
|
|||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from documents.classifier import load_classifier
|
from documents.classifier import load_classifier
|
||||||
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.file_handling import create_source_path_directory
|
from documents.file_handling import create_source_path_directory
|
||||||
from documents.file_handling import generate_unique_filename
|
from documents.file_handling import generate_unique_filename
|
||||||
@ -45,6 +44,8 @@ from documents.plugins.base import AlwaysRunPluginMixin
|
|||||||
from documents.plugins.base import ConsumeTaskPlugin
|
from documents.plugins.base import ConsumeTaskPlugin
|
||||||
from documents.plugins.base import NoCleanupPluginMixin
|
from documents.plugins.base import NoCleanupPluginMixin
|
||||||
from documents.plugins.base import NoSetupPluginMixin
|
from documents.plugins.base import NoSetupPluginMixin
|
||||||
|
from documents.plugins.helpers import ProgressManager
|
||||||
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
from documents.signals import document_consumption_finished
|
from documents.signals import document_consumption_finished
|
||||||
from documents.signals import document_consumption_started
|
from documents.signals import document_consumption_started
|
||||||
from documents.utils import copy_basic_file_stats
|
from documents.utils import copy_basic_file_stats
|
||||||
@ -247,88 +248,81 @@ class ConsumerStatusShortMessage(str, Enum):
|
|||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
class ConsumerFilePhase(str, Enum):
|
class ConsumerPlugin(
|
||||||
STARTED = "STARTED"
|
AlwaysRunPluginMixin,
|
||||||
WORKING = "WORKING"
|
NoSetupPluginMixin,
|
||||||
SUCCESS = "SUCCESS"
|
NoCleanupPluginMixin,
|
||||||
FAILED = "FAILED"
|
LoggingMixin,
|
||||||
|
ConsumeTaskPlugin,
|
||||||
|
):
|
||||||
class Consumer(LoggingMixin):
|
|
||||||
logging_name = "paperless.consumer"
|
logging_name = "paperless.consumer"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
input_doc: ConsumableDocument,
|
||||||
|
metadata: DocumentMetadataOverrides,
|
||||||
|
status_mgr: ProgressManager,
|
||||||
|
base_tmp_dir: Path,
|
||||||
|
task_id: str,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(input_doc, metadata, status_mgr, base_tmp_dir, task_id)
|
||||||
|
|
||||||
|
self.renew_logging_group()
|
||||||
|
|
||||||
|
self.filename = self.metadata.filename or self.input_doc.original_file.name
|
||||||
|
|
||||||
def _send_progress(
|
def _send_progress(
|
||||||
self,
|
self,
|
||||||
current_progress: int,
|
current_progress: int,
|
||||||
max_progress: int,
|
max_progress: int,
|
||||||
status: ConsumerFilePhase,
|
status: ProgressStatusOptions,
|
||||||
message: Optional[ConsumerStatusShortMessage] = None,
|
message: Optional[Union[ConsumerStatusShortMessage, str]] = None,
|
||||||
document_id=None,
|
document_id=None,
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
payload = {
|
self.status_mgr.send_progress(
|
||||||
"filename": os.path.basename(self.filename) if self.filename else None,
|
status,
|
||||||
"task_id": self.task_id,
|
message,
|
||||||
"current_progress": current_progress,
|
current_progress,
|
||||||
"max_progress": max_progress,
|
max_progress,
|
||||||
"status": status,
|
extra_args={
|
||||||
"message": message,
|
|
||||||
"document_id": document_id,
|
"document_id": document_id,
|
||||||
"owner_id": self.override_owner_id if self.override_owner_id else None,
|
"owner_id": self.metadata.owner_id if self.metadata.owner_id else None,
|
||||||
}
|
},
|
||||||
async_to_sync(self.channel_layer.group_send)(
|
|
||||||
"status_updates",
|
|
||||||
{"type": "status_update", "data": payload},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _fail(
|
def _fail(
|
||||||
self,
|
self,
|
||||||
message: ConsumerStatusShortMessage,
|
message: Union[ConsumerStatusShortMessage, str],
|
||||||
log_message: Optional[str] = None,
|
log_message: Optional[str] = None,
|
||||||
exc_info=None,
|
exc_info=None,
|
||||||
exception: Optional[Exception] = None,
|
exception: Optional[Exception] = None,
|
||||||
):
|
):
|
||||||
self._send_progress(100, 100, ConsumerFilePhase.FAILED, message)
|
self._send_progress(100, 100, ProgressStatusOptions.FAILED, message)
|
||||||
self.log.error(log_message or message, exc_info=exc_info)
|
self.log.error(log_message or message, exc_info=exc_info)
|
||||||
raise ConsumerError(f"{self.filename}: {log_message or message}") from exception
|
raise ConsumerError(f"{self.filename}: {log_message or message}") from exception
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.path: Optional[Path] = None
|
|
||||||
self.original_path: Optional[Path] = None
|
|
||||||
self.filename = None
|
|
||||||
self.override_title = None
|
|
||||||
self.override_correspondent_id = None
|
|
||||||
self.override_tag_ids = None
|
|
||||||
self.override_document_type_id = None
|
|
||||||
self.override_asn = None
|
|
||||||
self.task_id = None
|
|
||||||
self.override_owner_id = None
|
|
||||||
self.override_custom_field_ids = None
|
|
||||||
|
|
||||||
self.channel_layer = get_channel_layer()
|
|
||||||
|
|
||||||
def pre_check_file_exists(self):
|
def pre_check_file_exists(self):
|
||||||
"""
|
"""
|
||||||
Confirm the input file still exists where it should
|
Confirm the input file still exists where it should
|
||||||
"""
|
"""
|
||||||
if not os.path.isfile(self.original_path):
|
if not os.path.isfile(self.input_doc.original_file):
|
||||||
self._fail(
|
self._fail(
|
||||||
ConsumerStatusShortMessage.FILE_NOT_FOUND,
|
ConsumerStatusShortMessage.FILE_NOT_FOUND,
|
||||||
f"Cannot consume {self.original_path}: File not found.",
|
f"Cannot consume {self.input_doc.original_file}: File not found.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def pre_check_duplicate(self):
|
def pre_check_duplicate(self):
|
||||||
"""
|
"""
|
||||||
Using the MD5 of the file, check this exact file doesn't already exist
|
Using the MD5 of the file, check this exact file doesn't already exist
|
||||||
"""
|
"""
|
||||||
with open(self.original_path, "rb") as f:
|
with open(self.input_doc.original_file, "rb") as f:
|
||||||
checksum = hashlib.md5(f.read()).hexdigest()
|
checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
existing_doc = Document.objects.filter(
|
existing_doc = Document.objects.filter(
|
||||||
Q(checksum=checksum) | Q(archive_checksum=checksum),
|
Q(checksum=checksum) | Q(archive_checksum=checksum),
|
||||||
)
|
)
|
||||||
if existing_doc.exists():
|
if existing_doc.exists():
|
||||||
if settings.CONSUMER_DELETE_DUPLICATES:
|
if settings.CONSUMER_DELETE_DUPLICATES:
|
||||||
os.unlink(self.original_path)
|
os.unlink(self.input_doc.original_file)
|
||||||
self._fail(
|
self._fail(
|
||||||
ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS,
|
ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS,
|
||||||
f"Not consuming {self.filename}: It is a duplicate of"
|
f"Not consuming {self.filename}: It is a duplicate of"
|
||||||
@ -348,26 +342,26 @@ class Consumer(LoggingMixin):
|
|||||||
"""
|
"""
|
||||||
Check that if override_asn is given, it is unique and within a valid range
|
Check that if override_asn is given, it is unique and within a valid range
|
||||||
"""
|
"""
|
||||||
if not self.override_asn:
|
if not self.metadata.asn:
|
||||||
# check not necessary in case no ASN gets set
|
# check not necessary in case no ASN gets set
|
||||||
return
|
return
|
||||||
# Validate the range is above zero and less than uint32_t max
|
# Validate the range is above zero and less than uint32_t max
|
||||||
# otherwise, Whoosh can't handle it in the index
|
# otherwise, Whoosh can't handle it in the index
|
||||||
if (
|
if (
|
||||||
self.override_asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
self.metadata.asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
||||||
or self.override_asn > Document.ARCHIVE_SERIAL_NUMBER_MAX
|
or self.metadata.asn > Document.ARCHIVE_SERIAL_NUMBER_MAX
|
||||||
):
|
):
|
||||||
self._fail(
|
self._fail(
|
||||||
ConsumerStatusShortMessage.ASN_RANGE,
|
ConsumerStatusShortMessage.ASN_RANGE,
|
||||||
f"Not consuming {self.filename}: "
|
f"Not consuming {self.filename}: "
|
||||||
f"Given ASN {self.override_asn} is out of range "
|
f"Given ASN {self.metadata.asn} is out of range "
|
||||||
f"[{Document.ARCHIVE_SERIAL_NUMBER_MIN:,}, "
|
f"[{Document.ARCHIVE_SERIAL_NUMBER_MIN:,}, "
|
||||||
f"{Document.ARCHIVE_SERIAL_NUMBER_MAX:,}]",
|
f"{Document.ARCHIVE_SERIAL_NUMBER_MAX:,}]",
|
||||||
)
|
)
|
||||||
if Document.objects.filter(archive_serial_number=self.override_asn).exists():
|
if Document.objects.filter(archive_serial_number=self.metadata.asn).exists():
|
||||||
self._fail(
|
self._fail(
|
||||||
ConsumerStatusShortMessage.ASN_ALREADY_EXISTS,
|
ConsumerStatusShortMessage.ASN_ALREADY_EXISTS,
|
||||||
f"Not consuming {self.filename}: Given ASN {self.override_asn} already exists!",
|
f"Not consuming {self.filename}: Given ASN {self.metadata.asn} already exists!",
|
||||||
)
|
)
|
||||||
|
|
||||||
def run_pre_consume_script(self):
|
def run_pre_consume_script(self):
|
||||||
@ -388,7 +382,7 @@ class Consumer(LoggingMixin):
|
|||||||
self.log.info(f"Executing pre-consume script {settings.PRE_CONSUME_SCRIPT}")
|
self.log.info(f"Executing pre-consume script {settings.PRE_CONSUME_SCRIPT}")
|
||||||
|
|
||||||
working_file_path = str(self.working_copy)
|
working_file_path = str(self.working_copy)
|
||||||
original_file_path = str(self.original_path)
|
original_file_path = str(self.input_doc.original_file)
|
||||||
|
|
||||||
script_env = os.environ.copy()
|
script_env = os.environ.copy()
|
||||||
script_env["DOCUMENT_SOURCE_PATH"] = original_file_path
|
script_env["DOCUMENT_SOURCE_PATH"] = original_file_path
|
||||||
@ -486,50 +480,15 @@ class Consumer(LoggingMixin):
|
|||||||
exception=e,
|
exception=e,
|
||||||
)
|
)
|
||||||
|
|
||||||
def try_consume_file(
|
def run(self) -> str:
|
||||||
self,
|
|
||||||
path: Path,
|
|
||||||
override_filename=None,
|
|
||||||
override_title=None,
|
|
||||||
override_correspondent_id=None,
|
|
||||||
override_document_type_id=None,
|
|
||||||
override_tag_ids=None,
|
|
||||||
override_storage_path_id=None,
|
|
||||||
task_id=None,
|
|
||||||
override_created=None,
|
|
||||||
override_asn=None,
|
|
||||||
override_owner_id=None,
|
|
||||||
override_view_users=None,
|
|
||||||
override_view_groups=None,
|
|
||||||
override_change_users=None,
|
|
||||||
override_change_groups=None,
|
|
||||||
override_custom_field_ids=None,
|
|
||||||
) -> Document:
|
|
||||||
"""
|
"""
|
||||||
Return the document object if it was successfully created.
|
Return the document object if it was successfully created.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.original_path = Path(path).resolve()
|
|
||||||
self.filename = override_filename or self.original_path.name
|
|
||||||
self.override_title = override_title
|
|
||||||
self.override_correspondent_id = override_correspondent_id
|
|
||||||
self.override_document_type_id = override_document_type_id
|
|
||||||
self.override_tag_ids = override_tag_ids
|
|
||||||
self.override_storage_path_id = override_storage_path_id
|
|
||||||
self.task_id = task_id or str(uuid.uuid4())
|
|
||||||
self.override_created = override_created
|
|
||||||
self.override_asn = override_asn
|
|
||||||
self.override_owner_id = override_owner_id
|
|
||||||
self.override_view_users = override_view_users
|
|
||||||
self.override_view_groups = override_view_groups
|
|
||||||
self.override_change_users = override_change_users
|
|
||||||
self.override_change_groups = override_change_groups
|
|
||||||
self.override_custom_field_ids = override_custom_field_ids
|
|
||||||
|
|
||||||
self._send_progress(
|
self._send_progress(
|
||||||
0,
|
0,
|
||||||
100,
|
100,
|
||||||
ConsumerFilePhase.STARTED,
|
ProgressStatusOptions.STARTED,
|
||||||
ConsumerStatusShortMessage.NEW_FILE,
|
ConsumerStatusShortMessage.NEW_FILE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -548,7 +507,7 @@ class Consumer(LoggingMixin):
|
|||||||
dir=settings.SCRATCH_DIR,
|
dir=settings.SCRATCH_DIR,
|
||||||
)
|
)
|
||||||
self.working_copy = Path(tempdir.name) / Path(self.filename)
|
self.working_copy = Path(tempdir.name) / Path(self.filename)
|
||||||
copy_file_with_basic_stats(self.original_path, self.working_copy)
|
copy_file_with_basic_stats(self.input_doc.original_file, self.working_copy)
|
||||||
|
|
||||||
# Determine the parser class.
|
# Determine the parser class.
|
||||||
|
|
||||||
@ -580,7 +539,7 @@ class Consumer(LoggingMixin):
|
|||||||
def progress_callback(current_progress, max_progress): # pragma: no cover
|
def progress_callback(current_progress, max_progress): # pragma: no cover
|
||||||
# recalculate progress to be within 20 and 80
|
# recalculate progress to be within 20 and 80
|
||||||
p = int((current_progress / max_progress) * 50 + 20)
|
p = int((current_progress / max_progress) * 50 + 20)
|
||||||
self._send_progress(p, 100, ConsumerFilePhase.WORKING)
|
self._send_progress(p, 100, ProgressStatusOptions.WORKING)
|
||||||
|
|
||||||
# This doesn't parse the document yet, but gives us a parser.
|
# This doesn't parse the document yet, but gives us a parser.
|
||||||
|
|
||||||
@ -591,9 +550,6 @@ class Consumer(LoggingMixin):
|
|||||||
|
|
||||||
self.log.debug(f"Parser: {type(document_parser).__name__}")
|
self.log.debug(f"Parser: {type(document_parser).__name__}")
|
||||||
|
|
||||||
# However, this already created working directories which we have to
|
|
||||||
# clean up.
|
|
||||||
|
|
||||||
# Parse the document. This may take some time.
|
# Parse the document. This may take some time.
|
||||||
|
|
||||||
text = None
|
text = None
|
||||||
@ -605,7 +561,7 @@ class Consumer(LoggingMixin):
|
|||||||
self._send_progress(
|
self._send_progress(
|
||||||
20,
|
20,
|
||||||
100,
|
100,
|
||||||
ConsumerFilePhase.WORKING,
|
ProgressStatusOptions.WORKING,
|
||||||
ConsumerStatusShortMessage.PARSING_DOCUMENT,
|
ConsumerStatusShortMessage.PARSING_DOCUMENT,
|
||||||
)
|
)
|
||||||
self.log.debug(f"Parsing {self.filename}...")
|
self.log.debug(f"Parsing {self.filename}...")
|
||||||
@ -615,7 +571,7 @@ class Consumer(LoggingMixin):
|
|||||||
self._send_progress(
|
self._send_progress(
|
||||||
70,
|
70,
|
||||||
100,
|
100,
|
||||||
ConsumerFilePhase.WORKING,
|
ProgressStatusOptions.WORKING,
|
||||||
ConsumerStatusShortMessage.GENERATING_THUMBNAIL,
|
ConsumerStatusShortMessage.GENERATING_THUMBNAIL,
|
||||||
)
|
)
|
||||||
thumbnail = document_parser.get_thumbnail(
|
thumbnail = document_parser.get_thumbnail(
|
||||||
@ -630,7 +586,7 @@ class Consumer(LoggingMixin):
|
|||||||
self._send_progress(
|
self._send_progress(
|
||||||
90,
|
90,
|
||||||
100,
|
100,
|
||||||
ConsumerFilePhase.WORKING,
|
ProgressStatusOptions.WORKING,
|
||||||
ConsumerStatusShortMessage.PARSE_DATE,
|
ConsumerStatusShortMessage.PARSE_DATE,
|
||||||
)
|
)
|
||||||
date = parse_date(self.filename, text)
|
date = parse_date(self.filename, text)
|
||||||
@ -664,7 +620,7 @@ class Consumer(LoggingMixin):
|
|||||||
self._send_progress(
|
self._send_progress(
|
||||||
95,
|
95,
|
||||||
100,
|
100,
|
||||||
ConsumerFilePhase.WORKING,
|
ProgressStatusOptions.WORKING,
|
||||||
ConsumerStatusShortMessage.SAVE_DOCUMENT,
|
ConsumerStatusShortMessage.SAVE_DOCUMENT,
|
||||||
)
|
)
|
||||||
# now that everything is done, we can start to store the document
|
# now that everything is done, we can start to store the document
|
||||||
@ -726,13 +682,13 @@ class Consumer(LoggingMixin):
|
|||||||
|
|
||||||
# Delete the file only if it was successfully consumed
|
# Delete the file only if it was successfully consumed
|
||||||
self.log.debug(f"Deleting file {self.working_copy}")
|
self.log.debug(f"Deleting file {self.working_copy}")
|
||||||
self.original_path.unlink()
|
self.input_doc.original_file.unlink()
|
||||||
self.working_copy.unlink()
|
self.working_copy.unlink()
|
||||||
|
|
||||||
# https://github.com/jonaswinkler/paperless-ng/discussions/1037
|
# https://github.com/jonaswinkler/paperless-ng/discussions/1037
|
||||||
shadow_file = os.path.join(
|
shadow_file = os.path.join(
|
||||||
os.path.dirname(self.original_path),
|
os.path.dirname(self.input_doc.original_file),
|
||||||
"._" + os.path.basename(self.original_path),
|
"._" + os.path.basename(self.input_doc.original_file),
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.path.isfile(shadow_file):
|
if os.path.isfile(shadow_file):
|
||||||
@ -758,7 +714,7 @@ class Consumer(LoggingMixin):
|
|||||||
self._send_progress(
|
self._send_progress(
|
||||||
100,
|
100,
|
||||||
100,
|
100,
|
||||||
ConsumerFilePhase.SUCCESS,
|
ProgressStatusOptions.SUCCESS,
|
||||||
ConsumerStatusShortMessage.FINISHED,
|
ConsumerStatusShortMessage.FINISHED,
|
||||||
document.id,
|
document.id,
|
||||||
)
|
)
|
||||||
@ -766,24 +722,24 @@ class Consumer(LoggingMixin):
|
|||||||
# Return the most up to date fields
|
# Return the most up to date fields
|
||||||
document.refresh_from_db()
|
document.refresh_from_db()
|
||||||
|
|
||||||
return document
|
return f"Success. New document id {document.pk} created"
|
||||||
|
|
||||||
def _parse_title_placeholders(self, title: str) -> str:
|
def _parse_title_placeholders(self, title: str) -> str:
|
||||||
local_added = timezone.localtime(timezone.now())
|
local_added = timezone.localtime(timezone.now())
|
||||||
|
|
||||||
correspondent_name = (
|
correspondent_name = (
|
||||||
Correspondent.objects.get(pk=self.override_correspondent_id).name
|
Correspondent.objects.get(pk=self.metadata.correspondent_id).name
|
||||||
if self.override_correspondent_id is not None
|
if self.metadata.correspondent_id is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
doc_type_name = (
|
doc_type_name = (
|
||||||
DocumentType.objects.get(pk=self.override_document_type_id).name
|
DocumentType.objects.get(pk=self.metadata.document_type_id).name
|
||||||
if self.override_document_type_id is not None
|
if self.metadata.document_type_id is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
owner_username = (
|
owner_username = (
|
||||||
User.objects.get(pk=self.override_owner_id).username
|
User.objects.get(pk=self.metadata.owner_id).username
|
||||||
if self.override_owner_id is not None
|
if self.metadata.owner_id is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -808,8 +764,8 @@ class Consumer(LoggingMixin):
|
|||||||
|
|
||||||
self.log.debug("Saving record to database")
|
self.log.debug("Saving record to database")
|
||||||
|
|
||||||
if self.override_created is not None:
|
if self.metadata.created is not None:
|
||||||
create_date = self.override_created
|
create_date = self.metadata.created
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
f"Creation date from post_documents parameter: {create_date}",
|
f"Creation date from post_documents parameter: {create_date}",
|
||||||
)
|
)
|
||||||
@ -820,7 +776,7 @@ class Consumer(LoggingMixin):
|
|||||||
create_date = date
|
create_date = date
|
||||||
self.log.debug(f"Creation date from parse_date: {create_date}")
|
self.log.debug(f"Creation date from parse_date: {create_date}")
|
||||||
else:
|
else:
|
||||||
stats = os.stat(self.original_path)
|
stats = os.stat(self.input_doc.original_file)
|
||||||
create_date = timezone.make_aware(
|
create_date = timezone.make_aware(
|
||||||
datetime.datetime.fromtimestamp(stats.st_mtime),
|
datetime.datetime.fromtimestamp(stats.st_mtime),
|
||||||
)
|
)
|
||||||
@ -829,12 +785,12 @@ class Consumer(LoggingMixin):
|
|||||||
storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
|
|
||||||
title = file_info.title
|
title = file_info.title
|
||||||
if self.override_title is not None:
|
if self.metadata.title is not None:
|
||||||
try:
|
try:
|
||||||
title = self._parse_title_placeholders(self.override_title)
|
title = self._parse_title_placeholders(self.metadata.title)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(
|
self.log.error(
|
||||||
f"Error occurred parsing title override '{self.override_title}', falling back to original. Exception: {e}",
|
f"Error occurred parsing title override '{self.metadata.title}', falling back to original. Exception: {e}",
|
||||||
)
|
)
|
||||||
|
|
||||||
document = Document.objects.create(
|
document = Document.objects.create(
|
||||||
@ -855,53 +811,53 @@ class Consumer(LoggingMixin):
|
|||||||
return document
|
return document
|
||||||
|
|
||||||
def apply_overrides(self, document):
|
def apply_overrides(self, document):
|
||||||
if self.override_correspondent_id:
|
if self.metadata.correspondent_id:
|
||||||
document.correspondent = Correspondent.objects.get(
|
document.correspondent = Correspondent.objects.get(
|
||||||
pk=self.override_correspondent_id,
|
pk=self.metadata.correspondent_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.override_document_type_id:
|
if self.metadata.document_type_id:
|
||||||
document.document_type = DocumentType.objects.get(
|
document.document_type = DocumentType.objects.get(
|
||||||
pk=self.override_document_type_id,
|
pk=self.metadata.document_type_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.override_tag_ids:
|
if self.metadata.tag_ids:
|
||||||
for tag_id in self.override_tag_ids:
|
for tag_id in self.metadata.tag_ids:
|
||||||
document.tags.add(Tag.objects.get(pk=tag_id))
|
document.tags.add(Tag.objects.get(pk=tag_id))
|
||||||
|
|
||||||
if self.override_storage_path_id:
|
if self.metadata.storage_path_id:
|
||||||
document.storage_path = StoragePath.objects.get(
|
document.storage_path = StoragePath.objects.get(
|
||||||
pk=self.override_storage_path_id,
|
pk=self.metadata.storage_path_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.override_asn:
|
if self.metadata.asn:
|
||||||
document.archive_serial_number = self.override_asn
|
document.archive_serial_number = self.metadata.asn
|
||||||
|
|
||||||
if self.override_owner_id:
|
if self.metadata.owner_id:
|
||||||
document.owner = User.objects.get(
|
document.owner = User.objects.get(
|
||||||
pk=self.override_owner_id,
|
pk=self.metadata.owner_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.override_view_users is not None
|
self.metadata.view_users is not None
|
||||||
or self.override_view_groups is not None
|
or self.metadata.view_groups is not None
|
||||||
or self.override_change_users is not None
|
or self.metadata.change_users is not None
|
||||||
or self.override_change_users is not None
|
or self.metadata.change_users is not None
|
||||||
):
|
):
|
||||||
permissions = {
|
permissions = {
|
||||||
"view": {
|
"view": {
|
||||||
"users": self.override_view_users or [],
|
"users": self.metadata.view_users or [],
|
||||||
"groups": self.override_view_groups or [],
|
"groups": self.metadata.view_groups or [],
|
||||||
},
|
},
|
||||||
"change": {
|
"change": {
|
||||||
"users": self.override_change_users or [],
|
"users": self.metadata.change_users or [],
|
||||||
"groups": self.override_change_groups or [],
|
"groups": self.metadata.change_groups or [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
set_permissions_for_object(permissions=permissions, object=document)
|
set_permissions_for_object(permissions=permissions, object=document)
|
||||||
|
|
||||||
if self.override_custom_field_ids:
|
if self.metadata.custom_field_ids:
|
||||||
for field_id in self.override_custom_field_ids:
|
for field_id in self.metadata.custom_field_ids:
|
||||||
field = CustomField.objects.get(pk=field_id)
|
field = CustomField.objects.get(pk=field_id)
|
||||||
CustomFieldInstance.objects.create(
|
CustomFieldInstance.objects.create(
|
||||||
field=field,
|
field=field,
|
||||||
|
@ -3,9 +3,6 @@ import uuid
|
|||||||
|
|
||||||
|
|
||||||
class LoggingMixin:
|
class LoggingMixin:
|
||||||
def __init__(self) -> None:
|
|
||||||
self.renew_logging_group()
|
|
||||||
|
|
||||||
def renew_logging_group(self):
|
def renew_logging_group(self):
|
||||||
"""
|
"""
|
||||||
Creates a new UUID to group subsequent log calls together with
|
Creates a new UUID to group subsequent log calls together with
|
||||||
|
@ -328,6 +328,7 @@ class DocumentParser(LoggingMixin):
|
|||||||
|
|
||||||
def __init__(self, logging_group, progress_callback=None):
|
def __init__(self, logging_group, progress_callback=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.renew_logging_group()
|
||||||
self.logging_group = logging_group
|
self.logging_group = logging_group
|
||||||
self.settings = self.get_settings()
|
self.settings = self.get_settings()
|
||||||
settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True)
|
settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
@ -67,7 +67,8 @@ class ConsumeTaskPlugin(abc.ABC):
|
|||||||
self.status_mgr = status_mgr
|
self.status_mgr = status_mgr
|
||||||
self.task_id: Final = task_id
|
self.task_id: Final = task_id
|
||||||
|
|
||||||
@abc.abstractproperty
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
def able_to_run(self) -> bool:
|
def able_to_run(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if the conditions are met for the plugin to run, False otherwise
|
Return True if the conditions are met for the plugin to run, False otherwise
|
||||||
|
@ -57,7 +57,7 @@ class ProgressManager:
|
|||||||
message: str,
|
message: str,
|
||||||
current_progress: int,
|
current_progress: int,
|
||||||
max_progress: int,
|
max_progress: int,
|
||||||
extra_args: Optional[dict[str, Union[str, int]]] = None,
|
extra_args: Optional[dict[str, Union[str, int, None]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Ensure the layer is open
|
# Ensure the layer is open
|
||||||
self.open()
|
self.open()
|
||||||
|
@ -21,8 +21,7 @@ from documents.barcodes import BarcodePlugin
|
|||||||
from documents.caching import clear_document_caches
|
from documents.caching import clear_document_caches
|
||||||
from documents.classifier import DocumentClassifier
|
from documents.classifier import DocumentClassifier
|
||||||
from documents.classifier import load_classifier
|
from documents.classifier import load_classifier
|
||||||
from documents.consumer import Consumer
|
from documents.consumer import ConsumerPlugin
|
||||||
from documents.consumer import ConsumerError
|
|
||||||
from documents.consumer import WorkflowTriggerPlugin
|
from documents.consumer import WorkflowTriggerPlugin
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
@ -115,6 +114,7 @@ def consume_file(
|
|||||||
CollatePlugin,
|
CollatePlugin,
|
||||||
BarcodePlugin,
|
BarcodePlugin,
|
||||||
WorkflowTriggerPlugin,
|
WorkflowTriggerPlugin,
|
||||||
|
ConsumerPlugin,
|
||||||
]
|
]
|
||||||
|
|
||||||
with ProgressManager(
|
with ProgressManager(
|
||||||
@ -162,33 +162,7 @@ def consume_file(
|
|||||||
finally:
|
finally:
|
||||||
plugin.cleanup()
|
plugin.cleanup()
|
||||||
|
|
||||||
# continue with consumption if no barcode was found
|
return msg
|
||||||
document = Consumer().try_consume_file(
|
|
||||||
input_doc.original_file,
|
|
||||||
override_filename=overrides.filename,
|
|
||||||
override_title=overrides.title,
|
|
||||||
override_correspondent_id=overrides.correspondent_id,
|
|
||||||
override_document_type_id=overrides.document_type_id,
|
|
||||||
override_tag_ids=overrides.tag_ids,
|
|
||||||
override_storage_path_id=overrides.storage_path_id,
|
|
||||||
override_created=overrides.created,
|
|
||||||
override_asn=overrides.asn,
|
|
||||||
override_owner_id=overrides.owner_id,
|
|
||||||
override_view_users=overrides.view_users,
|
|
||||||
override_view_groups=overrides.view_groups,
|
|
||||||
override_change_users=overrides.change_users,
|
|
||||||
override_change_groups=overrides.change_groups,
|
|
||||||
override_custom_field_ids=overrides.custom_field_ids,
|
|
||||||
task_id=self.request.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
if document:
|
|
||||||
return f"Success. New document id {document.pk} created"
|
|
||||||
else:
|
|
||||||
raise ConsumerError(
|
|
||||||
"Unknown error: Returned document was null, but "
|
|
||||||
"no error message was given.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
|
@ -14,6 +14,7 @@ from documents.barcodes import BarcodePlugin
|
|||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
|
from documents.models import Document
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.plugins.base import StopConsumeTaskError
|
from documents.plugins.base import StopConsumeTaskError
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
@ -674,9 +675,7 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, GetReaderPluginMixin, Tes
|
|||||||
dst = settings.SCRATCH_DIR / "barcode-39-asn-123.pdf"
|
dst = settings.SCRATCH_DIR / "barcode-39-asn-123.pdf"
|
||||||
shutil.copy(test_file, dst)
|
shutil.copy(test_file, dst)
|
||||||
|
|
||||||
with mock.patch(
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
"documents.consumer.Consumer.try_consume_file",
|
|
||||||
) as mocked_consumer:
|
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
@ -684,10 +683,10 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, GetReaderPluginMixin, Tes
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
mocked_consumer.assert_called_once()
|
|
||||||
args, kwargs = mocked_consumer.call_args
|
|
||||||
|
|
||||||
self.assertEqual(kwargs["override_asn"], 123)
|
document = Document.objects.first()
|
||||||
|
|
||||||
|
self.assertEqual(document.archive_serial_number, 123)
|
||||||
|
|
||||||
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
|
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
|
||||||
def test_scan_file_for_qrcode_without_upscale(self):
|
def test_scan_file_for_qrcode_without_upscale(self):
|
||||||
|
@ -4,8 +4,9 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import TestCase as UnittestTestCase
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@ -18,9 +19,8 @@ from django.test import override_settings
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from guardian.core import ObjectPermissionChecker
|
from guardian.core import ObjectPermissionChecker
|
||||||
|
|
||||||
from documents.consumer import Consumer
|
|
||||||
from documents.consumer import ConsumerError
|
from documents.consumer import ConsumerError
|
||||||
from documents.consumer import ConsumerFilePhase
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
@ -30,12 +30,14 @@ from documents.models import StoragePath
|
|||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.parsers import DocumentParser
|
from documents.parsers import DocumentParser
|
||||||
from documents.parsers import ParseError
|
from documents.parsers import ParseError
|
||||||
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
from documents.tasks import sanity_check
|
from documents.tasks import sanity_check
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
|
from documents.tests.utils import GetConsumerMixin
|
||||||
|
|
||||||
|
|
||||||
class TestAttributes(TestCase):
|
class TestAttributes(UnittestTestCase):
|
||||||
TAGS = ("tag1", "tag2", "tag3")
|
TAGS = ("tag1", "tag2", "tag3")
|
||||||
|
|
||||||
def _test_guess_attributes_from_name(self, filename, sender, title, tags):
|
def _test_guess_attributes_from_name(self, filename, sender, title, tags):
|
||||||
@ -246,29 +248,33 @@ def fake_magic_from_file(file, mime=False):
|
|||||||
|
|
||||||
|
|
||||||
@mock.patch("documents.consumer.magic.from_file", fake_magic_from_file)
|
@mock.patch("documents.consumer.magic.from_file", fake_magic_from_file)
|
||||||
class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
class TestConsumer(
|
||||||
|
DirectoriesMixin,
|
||||||
|
FileSystemAssertsMixin,
|
||||||
|
GetConsumerMixin,
|
||||||
|
TestCase,
|
||||||
|
):
|
||||||
def _assert_first_last_send_progress(
|
def _assert_first_last_send_progress(
|
||||||
self,
|
self,
|
||||||
first_status=ConsumerFilePhase.STARTED,
|
first_status=ProgressStatusOptions.STARTED,
|
||||||
last_status=ConsumerFilePhase.SUCCESS,
|
last_status=ProgressStatusOptions.SUCCESS,
|
||||||
first_progress=0,
|
first_progress=0,
|
||||||
first_progress_max=100,
|
first_progress_max=100,
|
||||||
last_progress=100,
|
last_progress=100,
|
||||||
last_progress_max=100,
|
last_progress_max=100,
|
||||||
):
|
):
|
||||||
self._send_progress.assert_called()
|
self.assertGreaterEqual(len(self.status.payloads), 2)
|
||||||
|
|
||||||
args, kwargs = self._send_progress.call_args_list[0]
|
payload = self.status.payloads[0]
|
||||||
self.assertEqual(args[0], first_progress)
|
self.assertEqual(payload["data"]["current_progress"], first_progress)
|
||||||
self.assertEqual(args[1], first_progress_max)
|
self.assertEqual(payload["data"]["max_progress"], first_progress_max)
|
||||||
self.assertEqual(args[2], first_status)
|
self.assertEqual(payload["data"]["status"], first_status)
|
||||||
|
|
||||||
args, kwargs = self._send_progress.call_args_list[
|
payload = self.status.payloads[-1]
|
||||||
len(self._send_progress.call_args_list) - 1
|
|
||||||
]
|
self.assertEqual(payload["data"]["current_progress"], last_progress)
|
||||||
self.assertEqual(args[0], last_progress)
|
self.assertEqual(payload["data"]["max_progress"], last_progress_max)
|
||||||
self.assertEqual(args[1], last_progress_max)
|
self.assertEqual(payload["data"]["status"], last_status)
|
||||||
self.assertEqual(args[2], last_status)
|
|
||||||
|
|
||||||
def make_dummy_parser(self, logging_group, progress_callback=None):
|
def make_dummy_parser(self, logging_group, progress_callback=None):
|
||||||
return DummyParser(
|
return DummyParser(
|
||||||
@ -304,34 +310,23 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
]
|
]
|
||||||
self.addCleanup(patcher.stop)
|
self.addCleanup(patcher.stop)
|
||||||
|
|
||||||
# this prevents websocket message reports during testing.
|
|
||||||
patcher = mock.patch("documents.consumer.Consumer._send_progress")
|
|
||||||
self._send_progress = patcher.start()
|
|
||||||
self.addCleanup(patcher.stop)
|
|
||||||
|
|
||||||
self.consumer = Consumer()
|
|
||||||
|
|
||||||
def get_test_file(self):
|
def get_test_file(self):
|
||||||
src = os.path.join(
|
src = (
|
||||||
os.path.dirname(__file__),
|
Path(__file__).parent
|
||||||
"samples",
|
/ "samples"
|
||||||
"documents",
|
/ "documents"
|
||||||
"originals",
|
/ "originals"
|
||||||
"0000001.pdf",
|
/ "0000001.pdf"
|
||||||
)
|
)
|
||||||
dst = os.path.join(self.dirs.scratch_dir, "sample.pdf")
|
dst = self.dirs.scratch_dir / "sample.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
return dst
|
return dst
|
||||||
|
|
||||||
def get_test_archive_file(self):
|
def get_test_archive_file(self):
|
||||||
src = os.path.join(
|
src = (
|
||||||
os.path.dirname(__file__),
|
Path(__file__).parent / "samples" / "documents" / "archive" / "0000001.pdf"
|
||||||
"samples",
|
|
||||||
"documents",
|
|
||||||
"archive",
|
|
||||||
"0000001.pdf",
|
|
||||||
)
|
)
|
||||||
dst = os.path.join(self.dirs.scratch_dir, "sample_archive.pdf")
|
dst = self.dirs.scratch_dir / "sample_archive.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
return dst
|
return dst
|
||||||
|
|
||||||
@ -343,8 +338,12 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
# Roughly equal to file modification time
|
# Roughly equal to file modification time
|
||||||
rough_create_date_local = timezone.localtime(timezone.now())
|
rough_create_date_local = timezone.localtime(timezone.now())
|
||||||
|
|
||||||
# Consume the file
|
with self.get_consumer(filename) as consumer:
|
||||||
document = self.consumer.try_consume_file(filename)
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(document)
|
||||||
|
|
||||||
self.assertEqual(document.content, "The Text")
|
self.assertEqual(document.content, "The Text")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -395,7 +394,12 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
self.assertIsFile(shadow_file)
|
self.assertIsFile(shadow_file)
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(filename)
|
with self.get_consumer(filename) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(document)
|
||||||
|
|
||||||
self.assertIsFile(document.source_path)
|
self.assertIsFile(document.source_path)
|
||||||
|
|
||||||
@ -406,29 +410,48 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
filename = self.get_test_file()
|
filename = self.get_test_file()
|
||||||
override_filename = "Statement for November.pdf"
|
override_filename = "Statement for November.pdf"
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(
|
with self.get_consumer(
|
||||||
filename,
|
filename,
|
||||||
override_filename=override_filename,
|
DocumentMetadataOverrides(filename=override_filename),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(document)
|
||||||
|
|
||||||
self.assertEqual(document.title, "Statement for November")
|
self.assertEqual(document.title, "Statement for November")
|
||||||
|
|
||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
def testOverrideTitle(self):
|
def testOverrideTitle(self):
|
||||||
document = self.consumer.try_consume_file(
|
|
||||||
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_title="Override Title",
|
DocumentMetadataOverrides(title="Override Title"),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(document)
|
||||||
|
|
||||||
self.assertEqual(document.title, "Override Title")
|
self.assertEqual(document.title, "Override Title")
|
||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
def testOverrideTitleInvalidPlaceholders(self):
|
def testOverrideTitleInvalidPlaceholders(self):
|
||||||
with self.assertLogs("paperless.consumer", level="ERROR") as cm:
|
with self.assertLogs("paperless.consumer", level="ERROR") as cm:
|
||||||
document = self.consumer.try_consume_file(
|
|
||||||
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_title="Override {correspondent]",
|
DocumentMetadataOverrides(title="Override {correspondent]"),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(document)
|
||||||
|
|
||||||
self.assertEqual(document.title, "sample")
|
self.assertEqual(document.title, "sample")
|
||||||
expected_str = "Error occurred parsing title override 'Override {correspondent]', falling back to original"
|
expected_str = "Error occurred parsing title override 'Override {correspondent]', falling back to original"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
@ -436,30 +459,44 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
def testOverrideCorrespondent(self):
|
def testOverrideCorrespondent(self):
|
||||||
c = Correspondent.objects.create(name="test")
|
c = Correspondent.objects.create(name="test")
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_correspondent_id=c.pk,
|
DocumentMetadataOverrides(correspondent_id=c.pk),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(document)
|
||||||
|
|
||||||
self.assertEqual(document.correspondent.id, c.id)
|
self.assertEqual(document.correspondent.id, c.id)
|
||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
def testOverrideDocumentType(self):
|
def testOverrideDocumentType(self):
|
||||||
dt = DocumentType.objects.create(name="test")
|
dt = DocumentType.objects.create(name="test")
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_document_type_id=dt.pk,
|
DocumentMetadataOverrides(document_type_id=dt.pk),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(document.document_type.id, dt.id)
|
self.assertEqual(document.document_type.id, dt.id)
|
||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
def testOverrideStoragePath(self):
|
def testOverrideStoragePath(self):
|
||||||
sp = StoragePath.objects.create(name="test")
|
sp = StoragePath.objects.create(name="test")
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_storage_path_id=sp.pk,
|
DocumentMetadataOverrides(storage_path_id=sp.pk),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(document.storage_path.id, sp.id)
|
self.assertEqual(document.storage_path.id, sp.id)
|
||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
@ -467,10 +504,14 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
t1 = Tag.objects.create(name="t1")
|
t1 = Tag.objects.create(name="t1")
|
||||||
t2 = Tag.objects.create(name="t2")
|
t2 = Tag.objects.create(name="t2")
|
||||||
t3 = Tag.objects.create(name="t3")
|
t3 = Tag.objects.create(name="t3")
|
||||||
document = self.consumer.try_consume_file(
|
|
||||||
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_tag_ids=[t1.id, t3.id],
|
DocumentMetadataOverrides(tag_ids=[t1.id, t3.id]),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertIn(t1, document.tags.all())
|
self.assertIn(t1, document.tags.all())
|
||||||
self.assertNotIn(t2, document.tags.all())
|
self.assertNotIn(t2, document.tags.all())
|
||||||
@ -487,10 +528,14 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
name="Custom Field 3",
|
name="Custom Field 3",
|
||||||
data_type="url",
|
data_type="url",
|
||||||
)
|
)
|
||||||
document = self.consumer.try_consume_file(
|
|
||||||
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_custom_field_ids=[cf1.id, cf3.id],
|
DocumentMetadataOverrides(custom_field_ids=[cf1.id, cf3.id]),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
fields_used = [
|
fields_used = [
|
||||||
field_instance.field for field_instance in document.custom_fields.all()
|
field_instance.field for field_instance in document.custom_fields.all()
|
||||||
@ -501,10 +546,15 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
def testOverrideAsn(self):
|
def testOverrideAsn(self):
|
||||||
document = self.consumer.try_consume_file(
|
|
||||||
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_asn=123,
|
DocumentMetadataOverrides(asn=123),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(document.archive_serial_number, 123)
|
self.assertEqual(document.archive_serial_number, 123)
|
||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
@ -512,33 +562,51 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
c = Correspondent.objects.create(name="Correspondent Name")
|
c = Correspondent.objects.create(name="Correspondent Name")
|
||||||
dt = DocumentType.objects.create(name="DocType Name")
|
dt = DocumentType.objects.create(name="DocType Name")
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_correspondent_id=c.pk,
|
DocumentMetadataOverrides(
|
||||||
override_document_type_id=dt.pk,
|
correspondent_id=c.pk,
|
||||||
override_title="{correspondent}{document_type} {added_month}-{added_year_short}",
|
document_type_id=dt.pk,
|
||||||
)
|
title="{correspondent}{document_type} {added_month}-{added_year_short}",
|
||||||
|
),
|
||||||
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
self.assertEqual(document.title, f"{c.name}{dt.name} {now.strftime('%m-%y')}")
|
self.assertEqual(document.title, f"{c.name}{dt.name} {now.strftime('%m-%y')}")
|
||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
def testOverrideOwner(self):
|
def testOverrideOwner(self):
|
||||||
testuser = User.objects.create(username="testuser")
|
testuser = User.objects.create(username="testuser")
|
||||||
document = self.consumer.try_consume_file(
|
|
||||||
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_owner_id=testuser.pk,
|
DocumentMetadataOverrides(owner_id=testuser.pk),
|
||||||
)
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(document.owner, testuser)
|
self.assertEqual(document.owner, testuser)
|
||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
def testOverridePermissions(self):
|
def testOverridePermissions(self):
|
||||||
testuser = User.objects.create(username="testuser")
|
testuser = User.objects.create(username="testuser")
|
||||||
testgroup = Group.objects.create(name="testgroup")
|
testgroup = Group.objects.create(name="testgroup")
|
||||||
document = self.consumer.try_consume_file(
|
|
||||||
|
with self.get_consumer(
|
||||||
self.get_test_file(),
|
self.get_test_file(),
|
||||||
override_view_users=[testuser.pk],
|
DocumentMetadataOverrides(
|
||||||
override_view_groups=[testgroup.pk],
|
view_users=[testuser.pk],
|
||||||
)
|
view_groups=[testgroup.pk],
|
||||||
|
),
|
||||||
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
user_checker = ObjectPermissionChecker(testuser)
|
user_checker = ObjectPermissionChecker(testuser)
|
||||||
self.assertTrue(user_checker.has_perm("view_document", document))
|
self.assertTrue(user_checker.has_perm("view_document", document))
|
||||||
group_checker = ObjectPermissionChecker(testgroup)
|
group_checker = ObjectPermissionChecker(testgroup)
|
||||||
@ -546,53 +614,48 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
def testNotAFile(self):
|
def testNotAFile(self):
|
||||||
self.assertRaisesMessage(
|
|
||||||
ConsumerError,
|
|
||||||
"File not found",
|
|
||||||
self.consumer.try_consume_file,
|
|
||||||
"non-existing-file",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
with self.get_consumer(Path("non-existing-file")) as consumer:
|
||||||
|
with self.assertRaisesMessage(ConsumerError, "File not found"):
|
||||||
|
consumer.run()
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
def testDuplicates1(self):
|
def testDuplicates1(self):
|
||||||
self.consumer.try_consume_file(self.get_test_file())
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
self.assertRaisesMessage(
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
ConsumerError,
|
with self.assertRaisesMessage(ConsumerError, "It is a duplicate"):
|
||||||
"It is a duplicate",
|
consumer.run()
|
||||||
self.consumer.try_consume_file,
|
|
||||||
self.get_test_file(),
|
|
||||||
)
|
|
||||||
|
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
def testDuplicates2(self):
|
def testDuplicates2(self):
|
||||||
self.consumer.try_consume_file(self.get_test_file())
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
self.assertRaisesMessage(
|
with self.get_consumer(self.get_test_archive_file()) as consumer:
|
||||||
ConsumerError,
|
with self.assertRaisesMessage(ConsumerError, "It is a duplicate"):
|
||||||
"It is a duplicate",
|
consumer.run()
|
||||||
self.consumer.try_consume_file,
|
|
||||||
self.get_test_archive_file(),
|
|
||||||
)
|
|
||||||
|
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
def testDuplicates3(self):
|
def testDuplicates3(self):
|
||||||
self.consumer.try_consume_file(self.get_test_archive_file())
|
with self.get_consumer(self.get_test_archive_file()) as consumer:
|
||||||
self.consumer.try_consume_file(self.get_test_file())
|
consumer.run()
|
||||||
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
@mock.patch("documents.parsers.document_consumer_declaration.send")
|
@mock.patch("documents.parsers.document_consumer_declaration.send")
|
||||||
def testNoParsers(self, m):
|
def testNoParsers(self, m):
|
||||||
m.return_value = []
|
m.return_value = []
|
||||||
|
|
||||||
self.assertRaisesMessage(
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
|
with self.assertRaisesMessage(
|
||||||
ConsumerError,
|
ConsumerError,
|
||||||
"sample.pdf: Unsupported mime type application/pdf",
|
"sample.pdf: Unsupported mime type application/pdf",
|
||||||
self.consumer.try_consume_file,
|
):
|
||||||
self.get_test_file(),
|
consumer.run()
|
||||||
)
|
|
||||||
|
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
@ -609,12 +672,12 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertRaisesMessage(
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
|
with self.assertRaisesMessage(
|
||||||
ConsumerError,
|
ConsumerError,
|
||||||
"sample.pdf: Error occurred while consuming document sample.pdf: Does not compute.",
|
"sample.pdf: Error occurred while consuming document sample.pdf: Does not compute.",
|
||||||
self.consumer.try_consume_file,
|
):
|
||||||
self.get_test_file(),
|
consumer.run()
|
||||||
)
|
|
||||||
|
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
@ -631,26 +694,26 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertRaisesMessage(
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
|
with self.assertRaisesMessage(
|
||||||
ConsumerError,
|
ConsumerError,
|
||||||
"sample.pdf: Unexpected error while consuming document sample.pdf: Generic exception.",
|
"sample.pdf: Unexpected error while consuming document sample.pdf: Generic exception.",
|
||||||
self.consumer.try_consume_file,
|
):
|
||||||
self.get_test_file(),
|
consumer.run()
|
||||||
)
|
|
||||||
|
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer._write")
|
@mock.patch("documents.consumer.ConsumerPlugin._write")
|
||||||
def testPostSaveError(self, m):
|
def testPostSaveError(self, m):
|
||||||
filename = self.get_test_file()
|
filename = self.get_test_file()
|
||||||
m.side_effect = OSError("NO.")
|
m.side_effect = OSError("NO.")
|
||||||
|
|
||||||
self.assertRaisesMessage(
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
|
with self.assertRaisesMessage(
|
||||||
ConsumerError,
|
ConsumerError,
|
||||||
"sample.pdf: The following error occurred while storing document sample.pdf after parsing: NO.",
|
"sample.pdf: The following error occurred while storing document sample.pdf after parsing: NO.",
|
||||||
self.consumer.try_consume_file,
|
):
|
||||||
filename,
|
consumer.run()
|
||||||
)
|
|
||||||
|
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
@ -658,13 +721,18 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
self.assertIsFile(filename)
|
self.assertIsFile(filename)
|
||||||
|
|
||||||
# Database empty
|
# Database empty
|
||||||
self.assertEqual(len(Document.objects.all()), 0)
|
self.assertEqual(Document.objects.all().count(), 0)
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
||||||
def testFilenameHandling(self):
|
def testFilenameHandling(self):
|
||||||
filename = self.get_test_file()
|
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(filename, override_title="new docs")
|
with self.get_consumer(
|
||||||
|
self.get_test_file(),
|
||||||
|
DocumentMetadataOverrides(title="new docs"),
|
||||||
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(document.title, "new docs")
|
self.assertEqual(document.title, "new docs")
|
||||||
self.assertEqual(document.filename, "none/new docs.pdf")
|
self.assertEqual(document.filename, "none/new docs.pdf")
|
||||||
@ -684,11 +752,15 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
m.side_effect = lambda f, archive_filename=False: get_filename()
|
m.side_effect = lambda f, archive_filename=False: get_filename()
|
||||||
|
|
||||||
filename = self.get_test_file()
|
|
||||||
|
|
||||||
Tag.objects.create(name="test", is_inbox_tag=True)
|
Tag.objects.create(name="test", is_inbox_tag=True)
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(filename, override_title="new docs")
|
with self.get_consumer(
|
||||||
|
self.get_test_file(),
|
||||||
|
DocumentMetadataOverrides(title="new docs"),
|
||||||
|
) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(document.title, "new docs")
|
self.assertEqual(document.title, "new docs")
|
||||||
self.assertIsNotNone(document.title)
|
self.assertIsNotNone(document.title)
|
||||||
@ -715,7 +787,10 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
m.return_value.predict_document_type.return_value = dtype.pk
|
m.return_value.predict_document_type.return_value = dtype.pk
|
||||||
m.return_value.predict_tags.return_value = [t1.pk]
|
m.return_value.predict_tags.return_value = [t1.pk]
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(self.get_test_file())
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(document.correspondent, correspondent)
|
self.assertEqual(document.correspondent, correspondent)
|
||||||
self.assertEqual(document.document_type, dtype)
|
self.assertEqual(document.document_type, dtype)
|
||||||
@ -728,18 +803,24 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
def test_delete_duplicate(self):
|
def test_delete_duplicate(self):
|
||||||
dst = self.get_test_file()
|
dst = self.get_test_file()
|
||||||
self.assertIsFile(dst)
|
self.assertIsFile(dst)
|
||||||
doc = self.consumer.try_consume_file(dst)
|
|
||||||
|
with self.get_consumer(dst) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
self.assertIsNotFile(dst)
|
self.assertIsNotFile(dst)
|
||||||
self.assertIsNotNone(doc)
|
self.assertIsNotNone(document)
|
||||||
|
|
||||||
self._send_progress.reset_mock()
|
|
||||||
|
|
||||||
dst = self.get_test_file()
|
dst = self.get_test_file()
|
||||||
self.assertIsFile(dst)
|
self.assertIsFile(dst)
|
||||||
self.assertRaises(ConsumerError, self.consumer.try_consume_file, dst)
|
|
||||||
|
with self.get_consumer(dst) as consumer:
|
||||||
|
with self.assertRaises(ConsumerError):
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
self.assertIsNotFile(dst)
|
self.assertIsNotFile(dst)
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
@ -747,32 +828,44 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
def test_no_delete_duplicate(self):
|
def test_no_delete_duplicate(self):
|
||||||
dst = self.get_test_file()
|
dst = self.get_test_file()
|
||||||
self.assertIsFile(dst)
|
self.assertIsFile(dst)
|
||||||
doc = self.consumer.try_consume_file(dst)
|
|
||||||
|
with self.get_consumer(dst) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
self.assertIsNotFile(dst)
|
self.assertIsNotFile(dst)
|
||||||
self.assertIsNotNone(doc)
|
self.assertIsNotNone(document)
|
||||||
|
|
||||||
dst = self.get_test_file()
|
dst = self.get_test_file()
|
||||||
self.assertIsFile(dst)
|
self.assertIsFile(dst)
|
||||||
self.assertRaises(ConsumerError, self.consumer.try_consume_file, dst)
|
|
||||||
self.assertIsFile(dst)
|
|
||||||
|
|
||||||
|
with self.get_consumer(dst) as consumer:
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ConsumerError,
|
||||||
|
r"sample\.pdf: Not consuming sample\.pdf: It is a duplicate of sample \(#\d+\)",
|
||||||
|
):
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
self.assertIsFile(dst)
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{title}")
|
@override_settings(FILENAME_FORMAT="{title}")
|
||||||
@mock.patch("documents.parsers.document_consumer_declaration.send")
|
@mock.patch("documents.parsers.document_consumer_declaration.send")
|
||||||
def test_similar_filenames(self, m):
|
def test_similar_filenames(self, m):
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
Path(__file__).parent / "samples" / "simple.pdf",
|
||||||
os.path.join(settings.CONSUMPTION_DIR, "simple.pdf"),
|
settings.CONSUMPTION_DIR / "simple.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
os.path.join(os.path.dirname(__file__), "samples", "simple.png"),
|
Path(__file__).parent / "samples" / "simple.png",
|
||||||
os.path.join(settings.CONSUMPTION_DIR, "simple.png"),
|
settings.CONSUMPTION_DIR / "simple.png",
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
os.path.join(os.path.dirname(__file__), "samples", "simple-noalpha.png"),
|
Path(__file__).parent / "samples" / "simple-noalpha.png",
|
||||||
os.path.join(settings.CONSUMPTION_DIR, "simple.png.pdf"),
|
settings.CONSUMPTION_DIR / "simple.png.pdf",
|
||||||
)
|
)
|
||||||
m.return_value = [
|
m.return_value = [
|
||||||
(
|
(
|
||||||
@ -784,20 +877,28 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
doc1 = self.consumer.try_consume_file(
|
|
||||||
os.path.join(settings.CONSUMPTION_DIR, "simple.png"),
|
with self.get_consumer(settings.CONSUMPTION_DIR / "simple.png") as consumer:
|
||||||
)
|
consumer.run()
|
||||||
doc2 = self.consumer.try_consume_file(
|
|
||||||
os.path.join(settings.CONSUMPTION_DIR, "simple.pdf"),
|
doc1 = Document.objects.filter(pk=1).first()
|
||||||
)
|
|
||||||
doc3 = self.consumer.try_consume_file(
|
with self.get_consumer(settings.CONSUMPTION_DIR / "simple.pdf") as consumer:
|
||||||
os.path.join(settings.CONSUMPTION_DIR, "simple.png.pdf"),
|
consumer.run()
|
||||||
)
|
|
||||||
|
doc2 = Document.objects.filter(pk=2).first()
|
||||||
|
|
||||||
|
with self.get_consumer(settings.CONSUMPTION_DIR / "simple.png.pdf") as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
doc3 = Document.objects.filter(pk=3).first()
|
||||||
|
|
||||||
self.assertEqual(doc1.filename, "simple.png")
|
self.assertEqual(doc1.filename, "simple.png")
|
||||||
self.assertEqual(doc1.archive_filename, "simple.pdf")
|
self.assertEqual(doc1.archive_filename, "simple.pdf")
|
||||||
|
|
||||||
self.assertEqual(doc2.filename, "simple.pdf")
|
self.assertEqual(doc2.filename, "simple.pdf")
|
||||||
self.assertEqual(doc2.archive_filename, "simple_01.pdf")
|
self.assertEqual(doc2.archive_filename, "simple_01.pdf")
|
||||||
|
|
||||||
self.assertEqual(doc3.filename, "simple.png.pdf")
|
self.assertEqual(doc3.filename, "simple.png.pdf")
|
||||||
self.assertEqual(doc3.archive_filename, "simple.png.pdf")
|
self.assertEqual(doc3.archive_filename, "simple.png.pdf")
|
||||||
|
|
||||||
@ -805,17 +906,10 @@ class TestConsumer(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
|
|
||||||
@mock.patch("documents.consumer.magic.from_file", fake_magic_from_file)
|
@mock.patch("documents.consumer.magic.from_file", fake_magic_from_file)
|
||||||
class TestConsumerCreatedDate(DirectoriesMixin, TestCase):
|
class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# this prevents websocket message reports during testing.
|
|
||||||
patcher = mock.patch("documents.consumer.Consumer._send_progress")
|
|
||||||
self._send_progress = patcher.start()
|
|
||||||
self.addCleanup(patcher.stop)
|
|
||||||
|
|
||||||
self.consumer = Consumer()
|
|
||||||
|
|
||||||
def test_consume_date_from_content(self):
|
def test_consume_date_from_content(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@ -824,17 +918,20 @@ class TestConsumerCreatedDate(DirectoriesMixin, TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Should parse the date from the file content
|
- Should parse the date from the file content
|
||||||
"""
|
"""
|
||||||
src = os.path.join(
|
src = (
|
||||||
os.path.dirname(__file__),
|
Path(__file__).parent
|
||||||
"samples",
|
/ "samples"
|
||||||
"documents",
|
/ "documents"
|
||||||
"originals",
|
/ "originals"
|
||||||
"0000005.pdf",
|
/ "0000005.pdf"
|
||||||
)
|
)
|
||||||
dst = os.path.join(self.dirs.scratch_dir, "sample.pdf")
|
dst = self.dirs.scratch_dir / "sample.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(dst)
|
with self.get_consumer(dst) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
document.created,
|
document.created,
|
||||||
@ -851,17 +948,20 @@ class TestConsumerCreatedDate(DirectoriesMixin, TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Should parse the date from the filename
|
- Should parse the date from the filename
|
||||||
"""
|
"""
|
||||||
src = os.path.join(
|
src = (
|
||||||
os.path.dirname(__file__),
|
Path(__file__).parent
|
||||||
"samples",
|
/ "samples"
|
||||||
"documents",
|
/ "documents"
|
||||||
"originals",
|
/ "originals"
|
||||||
"0000005.pdf",
|
/ "0000005.pdf"
|
||||||
)
|
)
|
||||||
dst = os.path.join(self.dirs.scratch_dir, "Scan - 2022-02-01.pdf")
|
dst = self.dirs.scratch_dir / "Scan - 2022-02-01.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(dst)
|
with self.get_consumer(dst) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
document.created,
|
document.created,
|
||||||
@ -878,17 +978,20 @@ class TestConsumerCreatedDate(DirectoriesMixin, TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Should parse the date from the content
|
- Should parse the date from the content
|
||||||
"""
|
"""
|
||||||
src = os.path.join(
|
src = (
|
||||||
os.path.dirname(__file__),
|
Path(__file__).parent
|
||||||
"samples",
|
/ "samples"
|
||||||
"documents",
|
/ "documents"
|
||||||
"originals",
|
/ "originals"
|
||||||
"0000005.pdf",
|
/ "0000005.pdf"
|
||||||
)
|
)
|
||||||
dst = os.path.join(self.dirs.scratch_dir, "Scan - 2022-02-01.pdf")
|
dst = self.dirs.scratch_dir / "Scan - 2022-02-01.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(dst)
|
with self.get_consumer(dst) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
document.created,
|
document.created,
|
||||||
@ -907,17 +1010,20 @@ class TestConsumerCreatedDate(DirectoriesMixin, TestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- Should parse the date from the filename
|
- Should parse the date from the filename
|
||||||
"""
|
"""
|
||||||
src = os.path.join(
|
src = (
|
||||||
os.path.dirname(__file__),
|
Path(__file__).parent
|
||||||
"samples",
|
/ "samples"
|
||||||
"documents",
|
/ "documents"
|
||||||
"originals",
|
/ "originals"
|
||||||
"0000006.pdf",
|
/ "0000006.pdf"
|
||||||
)
|
)
|
||||||
dst = os.path.join(self.dirs.scratch_dir, "0000006.pdf")
|
dst = self.dirs.scratch_dir / "0000006.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
document = self.consumer.try_consume_file(dst)
|
with self.get_consumer(dst) as consumer:
|
||||||
|
consumer.run()
|
||||||
|
|
||||||
|
document = Document.objects.first()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
document.created,
|
document.created,
|
||||||
@ -925,41 +1031,40 @@ class TestConsumerCreatedDate(DirectoriesMixin, TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PreConsumeTestCase(TestCase):
|
class PreConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
# this prevents websocket message reports during testing.
|
super().setUp()
|
||||||
patcher = mock.patch("documents.consumer.Consumer._send_progress")
|
src = (
|
||||||
self._send_progress = patcher.start()
|
Path(__file__).parent
|
||||||
self.addCleanup(patcher.stop)
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
return super().setUp()
|
/ "originals"
|
||||||
|
/ "0000005.pdf"
|
||||||
|
)
|
||||||
|
self.test_file = self.dirs.scratch_dir / "sample.pdf"
|
||||||
|
shutil.copy(src, self.test_file)
|
||||||
|
|
||||||
@mock.patch("documents.consumer.run_subprocess")
|
@mock.patch("documents.consumer.run_subprocess")
|
||||||
@override_settings(PRE_CONSUME_SCRIPT=None)
|
@override_settings(PRE_CONSUME_SCRIPT=None)
|
||||||
def test_no_pre_consume_script(self, m):
|
def test_no_pre_consume_script(self, m):
|
||||||
c = Consumer()
|
with self.get_consumer(self.test_file) as c:
|
||||||
c.working_copy = "path-to-file"
|
c.run()
|
||||||
c.run_pre_consume_script()
|
|
||||||
m.assert_not_called()
|
m.assert_not_called()
|
||||||
|
|
||||||
@mock.patch("documents.consumer.run_subprocess")
|
@mock.patch("documents.consumer.run_subprocess")
|
||||||
@mock.patch("documents.consumer.Consumer._send_progress")
|
|
||||||
@override_settings(PRE_CONSUME_SCRIPT="does-not-exist")
|
@override_settings(PRE_CONSUME_SCRIPT="does-not-exist")
|
||||||
def test_pre_consume_script_not_found(self, m, m2):
|
def test_pre_consume_script_not_found(self, m):
|
||||||
c = Consumer()
|
with self.get_consumer(self.test_file) as c:
|
||||||
c.filename = "somefile.pdf"
|
|
||||||
c.working_copy = "path-to-file"
|
self.assertRaises(ConsumerError, c.run)
|
||||||
self.assertRaises(ConsumerError, c.run_pre_consume_script)
|
m.assert_not_called()
|
||||||
|
|
||||||
@mock.patch("documents.consumer.run_subprocess")
|
@mock.patch("documents.consumer.run_subprocess")
|
||||||
def test_pre_consume_script(self, m):
|
def test_pre_consume_script(self, m):
|
||||||
with tempfile.NamedTemporaryFile() as script:
|
with tempfile.NamedTemporaryFile() as script:
|
||||||
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
||||||
c = Consumer()
|
with self.get_consumer(self.test_file) as c:
|
||||||
c.original_path = "path-to-file"
|
c.run()
|
||||||
c.working_copy = "/tmp/somewhere/path-to-file"
|
|
||||||
c.task_id = str(uuid.uuid4())
|
|
||||||
c.run_pre_consume_script()
|
|
||||||
|
|
||||||
m.assert_called_once()
|
m.assert_called_once()
|
||||||
|
|
||||||
@ -969,11 +1074,11 @@ class PreConsumeTestCase(TestCase):
|
|||||||
environment = args[1]
|
environment = args[1]
|
||||||
|
|
||||||
self.assertEqual(command[0], script.name)
|
self.assertEqual(command[0], script.name)
|
||||||
self.assertEqual(command[1], "path-to-file")
|
self.assertEqual(command[1], str(self.test_file))
|
||||||
|
|
||||||
subset = {
|
subset = {
|
||||||
"DOCUMENT_SOURCE_PATH": c.original_path,
|
"DOCUMENT_SOURCE_PATH": str(c.input_doc.original_file),
|
||||||
"DOCUMENT_WORKING_PATH": c.working_copy,
|
"DOCUMENT_WORKING_PATH": str(c.working_copy),
|
||||||
"TASK_ID": c.task_id,
|
"TASK_ID": c.task_id,
|
||||||
}
|
}
|
||||||
self.assertDictEqual(environment, {**environment, **subset})
|
self.assertDictEqual(environment, {**environment, **subset})
|
||||||
@ -1000,10 +1105,8 @@ class PreConsumeTestCase(TestCase):
|
|||||||
|
|
||||||
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
||||||
with self.assertLogs("paperless.consumer", level="INFO") as cm:
|
with self.assertLogs("paperless.consumer", level="INFO") as cm:
|
||||||
c = Consumer()
|
with self.get_consumer(self.test_file) as c:
|
||||||
c.working_copy = "path-to-file"
|
c.run()
|
||||||
|
|
||||||
c.run_pre_consume_script()
|
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"INFO:paperless.consumer:This message goes to stdout",
|
"INFO:paperless.consumer:This message goes to stdout",
|
||||||
cm.output,
|
cm.output,
|
||||||
@ -1033,22 +1136,25 @@ class PreConsumeTestCase(TestCase):
|
|||||||
os.chmod(script.name, st.st_mode | stat.S_IEXEC)
|
os.chmod(script.name, st.st_mode | stat.S_IEXEC)
|
||||||
|
|
||||||
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
||||||
c = Consumer()
|
with self.get_consumer(self.test_file) as c:
|
||||||
c.working_copy = "path-to-file"
|
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
ConsumerError,
|
ConsumerError,
|
||||||
c.run_pre_consume_script,
|
c.run,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PostConsumeTestCase(TestCase):
|
class PostConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
# this prevents websocket message reports during testing.
|
super().setUp()
|
||||||
patcher = mock.patch("documents.consumer.Consumer._send_progress")
|
src = (
|
||||||
self._send_progress = patcher.start()
|
Path(__file__).parent
|
||||||
self.addCleanup(patcher.stop)
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
return super().setUp()
|
/ "originals"
|
||||||
|
/ "0000005.pdf"
|
||||||
|
)
|
||||||
|
self.test_file = self.dirs.scratch_dir / "sample.pdf"
|
||||||
|
shutil.copy(src, self.test_file)
|
||||||
|
|
||||||
@mock.patch("documents.consumer.run_subprocess")
|
@mock.patch("documents.consumer.run_subprocess")
|
||||||
@override_settings(POST_CONSUME_SCRIPT=None)
|
@override_settings(POST_CONSUME_SCRIPT=None)
|
||||||
@ -1059,21 +1165,20 @@ class PostConsumeTestCase(TestCase):
|
|||||||
doc.tags.add(tag1)
|
doc.tags.add(tag1)
|
||||||
doc.tags.add(tag2)
|
doc.tags.add(tag2)
|
||||||
|
|
||||||
Consumer().run_post_consume_script(doc)
|
with self.get_consumer(self.test_file) as consumer:
|
||||||
|
consumer.run_post_consume_script(doc)
|
||||||
m.assert_not_called()
|
m.assert_not_called()
|
||||||
|
|
||||||
@override_settings(POST_CONSUME_SCRIPT="does-not-exist")
|
@override_settings(POST_CONSUME_SCRIPT="does-not-exist")
|
||||||
@mock.patch("documents.consumer.Consumer._send_progress")
|
def test_post_consume_script_not_found(self):
|
||||||
def test_post_consume_script_not_found(self, m):
|
|
||||||
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
||||||
c = Consumer()
|
|
||||||
c.filename = "somefile.pdf"
|
with self.get_consumer(self.test_file) as consumer:
|
||||||
self.assertRaises(
|
with self.assertRaisesMessage(
|
||||||
ConsumerError,
|
ConsumerError,
|
||||||
c.run_post_consume_script,
|
"sample.pdf: Configured post-consume script does-not-exist does not exist",
|
||||||
doc,
|
):
|
||||||
)
|
consumer.run_post_consume_script(doc)
|
||||||
|
|
||||||
@mock.patch("documents.consumer.run_subprocess")
|
@mock.patch("documents.consumer.run_subprocess")
|
||||||
def test_post_consume_script_simple(self, m):
|
def test_post_consume_script_simple(self, m):
|
||||||
@ -1081,7 +1186,8 @@ class PostConsumeTestCase(TestCase):
|
|||||||
with override_settings(POST_CONSUME_SCRIPT=script.name):
|
with override_settings(POST_CONSUME_SCRIPT=script.name):
|
||||||
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
||||||
|
|
||||||
Consumer().run_post_consume_script(doc)
|
with self.get_consumer(self.test_file) as consumer:
|
||||||
|
consumer.run_post_consume_script(doc)
|
||||||
|
|
||||||
m.assert_called_once()
|
m.assert_called_once()
|
||||||
|
|
||||||
@ -1100,8 +1206,7 @@ class PostConsumeTestCase(TestCase):
|
|||||||
doc.tags.add(tag1)
|
doc.tags.add(tag1)
|
||||||
doc.tags.add(tag2)
|
doc.tags.add(tag2)
|
||||||
|
|
||||||
consumer = Consumer()
|
with self.get_consumer(self.test_file) as consumer:
|
||||||
consumer.task_id = str(uuid.uuid4())
|
|
||||||
consumer.run_post_consume_script(doc)
|
consumer.run_post_consume_script(doc)
|
||||||
|
|
||||||
m.assert_called_once()
|
m.assert_called_once()
|
||||||
@ -1149,8 +1254,11 @@ class PostConsumeTestCase(TestCase):
|
|||||||
os.chmod(script.name, st.st_mode | stat.S_IEXEC)
|
os.chmod(script.name, st.st_mode | stat.S_IEXEC)
|
||||||
|
|
||||||
with override_settings(POST_CONSUME_SCRIPT=script.name):
|
with override_settings(POST_CONSUME_SCRIPT=script.name):
|
||||||
c = Consumer()
|
|
||||||
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
||||||
c.path = "path-to-file"
|
with self.get_consumer(self.test_file) as consumer:
|
||||||
with self.assertRaises(ConsumerError):
|
with self.assertRaisesRegex(
|
||||||
c.run_post_consume_script(doc)
|
ConsumerError,
|
||||||
|
r"sample\.pdf: Error while executing post-consume script: Command '\[.*\]' returned non-zero exit status \d+\.",
|
||||||
|
):
|
||||||
|
consumer.run_post_consume_script(doc)
|
||||||
|
@ -46,7 +46,7 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
with mock.patch(
|
with mock.patch(
|
||||||
"documents.tasks.ProgressManager",
|
"documents.tasks.ProgressManager",
|
||||||
DummyProgressManager,
|
DummyProgressManager,
|
||||||
), mock.patch("documents.consumer.async_to_sync"):
|
):
|
||||||
msg = tasks.consume_file(
|
msg = tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import shutil
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@ -88,8 +89,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
|
|
||||||
return super().setUp()
|
return super().setUp()
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_workflow_match(self):
|
||||||
def test_workflow_match(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing workflow
|
- Existing workflow
|
||||||
@ -102,7 +102,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_filename="*simple*",
|
filter_filename="*simple*",
|
||||||
filter_path="*/samples/*",
|
filter_path=f"*/{self.dirs.scratch_dir.parts[-1]}/*",
|
||||||
)
|
)
|
||||||
action = WorkflowAction.objects.create(
|
action = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
@ -133,7 +133,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
self.assertEqual(trigger.__str__(), "WorkflowTrigger 1")
|
self.assertEqual(trigger.__str__(), "WorkflowTrigger 1")
|
||||||
self.assertEqual(action.__str__(), "WorkflowAction 1")
|
self.assertEqual(action.__str__(), "WorkflowAction 1")
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
@ -144,26 +147,53 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
|
||||||
_, overrides = m.call_args
|
document = Document.objects.first()
|
||||||
self.assertEqual(overrides["override_correspondent_id"], self.c.pk)
|
self.assertEqual(document.correspondent, self.c)
|
||||||
self.assertEqual(overrides["override_document_type_id"], self.dt.pk)
|
self.assertEqual(document.document_type, self.dt)
|
||||||
|
self.assertEqual(list(document.tags.all()), [self.t1, self.t2, self.t3])
|
||||||
|
self.assertEqual(document.storage_path, self.sp)
|
||||||
|
self.assertEqual(document.owner, self.user2)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_tag_ids"],
|
list(
|
||||||
[self.t1.pk, self.t2.pk, self.t3.pk],
|
get_users_with_perms(
|
||||||
)
|
document,
|
||||||
self.assertEqual(overrides["override_storage_path_id"], self.sp.pk)
|
only_with_perms_in=["view_document"],
|
||||||
self.assertEqual(overrides["override_owner_id"], self.user2.pk)
|
),
|
||||||
self.assertEqual(overrides["override_view_users"], [self.user3.pk])
|
),
|
||||||
self.assertEqual(overrides["override_view_groups"], [self.group1.pk])
|
[self.user3],
|
||||||
self.assertEqual(overrides["override_change_users"], [self.user3.pk])
|
|
||||||
self.assertEqual(overrides["override_change_groups"], [self.group1.pk])
|
|
||||||
self.assertEqual(
|
|
||||||
overrides["override_title"],
|
|
||||||
"Doc from {correspondent}",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_custom_field_ids"],
|
list(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.group1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(
|
||||||
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["change_document"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.user3],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.group1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
document.title,
|
||||||
|
f"Doc from {self.c.name}",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(document.custom_fields.all().values_list("field", flat=True)),
|
||||||
[self.cf1.pk, self.cf2.pk],
|
[self.cf1.pk, self.cf2.pk],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -171,8 +201,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
expected_str = f"Document matched {trigger} from {w}"
|
expected_str = f"Document matched {trigger} from {w}"
|
||||||
self.assertIn(expected_str, info)
|
self.assertIn(expected_str, info)
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_workflow_match_mailrule(self):
|
||||||
def test_workflow_match_mailrule(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing workflow
|
- Existing workflow
|
||||||
@ -211,7 +240,11 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.actions.add(action)
|
w.actions.add(action)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
@ -222,31 +255,55 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
document = Document.objects.first()
|
||||||
_, overrides = m.call_args
|
self.assertEqual(document.correspondent, self.c)
|
||||||
self.assertEqual(overrides["override_correspondent_id"], self.c.pk)
|
self.assertEqual(document.document_type, self.dt)
|
||||||
self.assertEqual(overrides["override_document_type_id"], self.dt.pk)
|
self.assertEqual(list(document.tags.all()), [self.t1, self.t2, self.t3])
|
||||||
|
self.assertEqual(document.storage_path, self.sp)
|
||||||
|
self.assertEqual(document.owner, self.user2)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_tag_ids"],
|
list(
|
||||||
[self.t1.pk, self.t2.pk, self.t3.pk],
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["view_document"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.user3],
|
||||||
)
|
)
|
||||||
self.assertEqual(overrides["override_storage_path_id"], self.sp.pk)
|
|
||||||
self.assertEqual(overrides["override_owner_id"], self.user2.pk)
|
|
||||||
self.assertEqual(overrides["override_view_users"], [self.user3.pk])
|
|
||||||
self.assertEqual(overrides["override_view_groups"], [self.group1.pk])
|
|
||||||
self.assertEqual(overrides["override_change_users"], [self.user3.pk])
|
|
||||||
self.assertEqual(overrides["override_change_groups"], [self.group1.pk])
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_title"],
|
list(
|
||||||
"Doc from {correspondent}",
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.group1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(
|
||||||
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["change_document"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.user3],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.group1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
document.title,
|
||||||
|
f"Doc from {self.c.name}",
|
||||||
)
|
)
|
||||||
|
|
||||||
info = cm.output[0]
|
info = cm.output[0]
|
||||||
expected_str = f"Document matched {trigger} from {w}"
|
expected_str = f"Document matched {trigger} from {w}"
|
||||||
self.assertIn(expected_str, info)
|
self.assertIn(expected_str, info)
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_workflow_match_multiple(self):
|
||||||
def test_workflow_match_multiple(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Multiple existing workflow
|
- Multiple existing workflow
|
||||||
@ -259,7 +316,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
trigger1 = WorkflowTrigger.objects.create(
|
trigger1 = WorkflowTrigger.objects.create(
|
||||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_path="*/samples/*",
|
filter_path=f"*/{self.dirs.scratch_dir.parts[-1]}/*",
|
||||||
)
|
)
|
||||||
action1 = WorkflowAction.objects.create(
|
action1 = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
@ -301,7 +358,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w2.actions.add(action2)
|
w2.actions.add(action2)
|
||||||
w2.save()
|
w2.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
@ -312,21 +372,25 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
document = Document.objects.first()
|
||||||
_, overrides = m.call_args
|
|
||||||
# template 1
|
# template 1
|
||||||
self.assertEqual(overrides["override_document_type_id"], self.dt.pk)
|
self.assertEqual(document.document_type, self.dt)
|
||||||
# template 2
|
# template 2
|
||||||
self.assertEqual(overrides["override_correspondent_id"], self.c2.pk)
|
self.assertEqual(document.correspondent, self.c2)
|
||||||
self.assertEqual(overrides["override_storage_path_id"], self.sp.pk)
|
self.assertEqual(document.storage_path, self.sp)
|
||||||
# template 1 & 2
|
# template 1 & 2
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_tag_ids"],
|
list(document.tags.all()),
|
||||||
[self.t1.pk, self.t2.pk, self.t3.pk],
|
[self.t1, self.t2, self.t3],
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_view_users"],
|
list(
|
||||||
[self.user2.pk, self.user3.pk],
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["view_document"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.user2, self.user3],
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_str = f"Document matched {trigger1} from {w1}"
|
expected_str = f"Document matched {trigger1} from {w1}"
|
||||||
@ -334,8 +398,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
expected_str = f"Document matched {trigger2} from {w2}"
|
expected_str = f"Document matched {trigger2} from {w2}"
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_workflow_fnmatch_path(self):
|
||||||
def test_workflow_fnmatch_path(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing workflow
|
- Existing workflow
|
||||||
@ -348,7 +411,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
trigger = WorkflowTrigger.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_path="*sample*",
|
filter_path=f"*{self.dirs.scratch_dir.parts[-1]}*",
|
||||||
)
|
)
|
||||||
action = WorkflowAction.objects.create(
|
action = WorkflowAction.objects.create(
|
||||||
assign_title="Doc fnmatch title",
|
assign_title="Doc fnmatch title",
|
||||||
@ -363,7 +426,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.actions.add(action)
|
w.actions.add(action)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
@ -374,15 +440,13 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
document = Document.objects.first()
|
||||||
_, overrides = m.call_args
|
self.assertEqual(document.title, "Doc fnmatch title")
|
||||||
self.assertEqual(overrides["override_title"], "Doc fnmatch title")
|
|
||||||
|
|
||||||
expected_str = f"Document matched {trigger} from {w}"
|
expected_str = f"Document matched {trigger} from {w}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_workflow_no_match_filename(self):
|
||||||
def test_workflow_no_match_filename(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing workflow
|
- Existing workflow
|
||||||
@ -414,7 +478,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.actions.add(action)
|
w.actions.add(action)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
@ -425,26 +492,36 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
document = Document.objects.first()
|
||||||
_, overrides = m.call_args
|
self.assertIsNone(document.correspondent)
|
||||||
self.assertIsNone(overrides["override_correspondent_id"])
|
self.assertIsNone(document.document_type)
|
||||||
self.assertIsNone(overrides["override_document_type_id"])
|
self.assertEqual(document.tags.all().count(), 0)
|
||||||
self.assertIsNone(overrides["override_tag_ids"])
|
self.assertIsNone(document.storage_path)
|
||||||
self.assertIsNone(overrides["override_storage_path_id"])
|
self.assertIsNone(document.owner)
|
||||||
self.assertIsNone(overrides["override_owner_id"])
|
self.assertEqual(
|
||||||
self.assertIsNone(overrides["override_view_users"])
|
get_users_with_perms(
|
||||||
self.assertIsNone(overrides["override_view_groups"])
|
document,
|
||||||
self.assertIsNone(overrides["override_change_users"])
|
only_with_perms_in=["view_document"],
|
||||||
self.assertIsNone(overrides["override_change_groups"])
|
).count(),
|
||||||
self.assertIsNone(overrides["override_title"])
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(get_groups_with_perms(document).count(), 0)
|
||||||
|
self.assertEqual(
|
||||||
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["change_document"],
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(get_groups_with_perms(document).count(), 0)
|
||||||
|
self.assertEqual(document.title, "simple")
|
||||||
|
|
||||||
expected_str = f"Document did not match {w}"
|
expected_str = f"Document did not match {w}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
expected_str = f"Document filename {test_file.name} does not match"
|
expected_str = f"Document filename {test_file.name} does not match"
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_workflow_no_match_path(self):
|
||||||
def test_workflow_no_match_path(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing workflow
|
- Existing workflow
|
||||||
@ -475,7 +552,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.actions.add(action)
|
w.actions.add(action)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
@ -486,26 +566,46 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
document = Document.objects.first()
|
||||||
_, overrides = m.call_args
|
self.assertIsNone(document.correspondent)
|
||||||
self.assertIsNone(overrides["override_correspondent_id"])
|
self.assertIsNone(document.document_type)
|
||||||
self.assertIsNone(overrides["override_document_type_id"])
|
self.assertEqual(document.tags.all().count(), 0)
|
||||||
self.assertIsNone(overrides["override_tag_ids"])
|
self.assertIsNone(document.storage_path)
|
||||||
self.assertIsNone(overrides["override_storage_path_id"])
|
self.assertIsNone(document.owner)
|
||||||
self.assertIsNone(overrides["override_owner_id"])
|
self.assertEqual(
|
||||||
self.assertIsNone(overrides["override_view_users"])
|
get_users_with_perms(
|
||||||
self.assertIsNone(overrides["override_view_groups"])
|
document,
|
||||||
self.assertIsNone(overrides["override_change_users"])
|
only_with_perms_in=["view_document"],
|
||||||
self.assertIsNone(overrides["override_change_groups"])
|
).count(),
|
||||||
self.assertIsNone(overrides["override_title"])
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["change_document"],
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(document.title, "simple")
|
||||||
|
|
||||||
expected_str = f"Document did not match {w}"
|
expected_str = f"Document did not match {w}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
expected_str = f"Document path {test_file} does not match"
|
expected_str = f"Document path {test_file} does not match"
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_workflow_no_match_mail_rule(self):
|
||||||
def test_workflow_no_match_mail_rule(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing workflow
|
- Existing workflow
|
||||||
@ -536,7 +636,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.actions.add(action)
|
w.actions.add(action)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
@ -548,26 +651,46 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
document = Document.objects.first()
|
||||||
_, overrides = m.call_args
|
self.assertIsNone(document.correspondent)
|
||||||
self.assertIsNone(overrides["override_correspondent_id"])
|
self.assertIsNone(document.document_type)
|
||||||
self.assertIsNone(overrides["override_document_type_id"])
|
self.assertEqual(document.tags.all().count(), 0)
|
||||||
self.assertIsNone(overrides["override_tag_ids"])
|
self.assertIsNone(document.storage_path)
|
||||||
self.assertIsNone(overrides["override_storage_path_id"])
|
self.assertIsNone(document.owner)
|
||||||
self.assertIsNone(overrides["override_owner_id"])
|
self.assertEqual(
|
||||||
self.assertIsNone(overrides["override_view_users"])
|
get_users_with_perms(
|
||||||
self.assertIsNone(overrides["override_view_groups"])
|
document,
|
||||||
self.assertIsNone(overrides["override_change_users"])
|
only_with_perms_in=["view_document"],
|
||||||
self.assertIsNone(overrides["override_change_groups"])
|
).count(),
|
||||||
self.assertIsNone(overrides["override_title"])
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["change_document"],
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(document.title, "simple")
|
||||||
|
|
||||||
expected_str = f"Document did not match {w}"
|
expected_str = f"Document did not match {w}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
expected_str = "Document mail rule 99 !="
|
expected_str = "Document mail rule 99 !="
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_workflow_no_match_source(self):
|
||||||
def test_workflow_no_match_source(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing workflow
|
- Existing workflow
|
||||||
@ -598,7 +721,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.actions.add(action)
|
w.actions.add(action)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
@ -609,18 +735,39 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
document = Document.objects.first()
|
||||||
_, overrides = m.call_args
|
self.assertIsNone(document.correspondent)
|
||||||
self.assertIsNone(overrides["override_correspondent_id"])
|
self.assertIsNone(document.document_type)
|
||||||
self.assertIsNone(overrides["override_document_type_id"])
|
self.assertEqual(document.tags.all().count(), 0)
|
||||||
self.assertIsNone(overrides["override_tag_ids"])
|
self.assertIsNone(document.storage_path)
|
||||||
self.assertIsNone(overrides["override_storage_path_id"])
|
self.assertIsNone(document.owner)
|
||||||
self.assertIsNone(overrides["override_owner_id"])
|
self.assertEqual(
|
||||||
self.assertIsNone(overrides["override_view_users"])
|
get_users_with_perms(
|
||||||
self.assertIsNone(overrides["override_view_groups"])
|
document,
|
||||||
self.assertIsNone(overrides["override_change_users"])
|
only_with_perms_in=["view_document"],
|
||||||
self.assertIsNone(overrides["override_change_groups"])
|
).count(),
|
||||||
self.assertIsNone(overrides["override_title"])
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["change_document"],
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(document.title, "simple")
|
||||||
|
|
||||||
expected_str = f"Document did not match {w}"
|
expected_str = f"Document did not match {w}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
@ -662,8 +809,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
expected_str = f"No matching triggers with type {WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED} found"
|
expected_str = f"No matching triggers with type {WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED} found"
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_workflow_repeat_custom_fields(self):
|
||||||
def test_workflow_repeat_custom_fields(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing workflows which assign the same custom field
|
- Existing workflows which assign the same custom field
|
||||||
@ -693,7 +839,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.actions.add(action1, action2)
|
w.actions.add(action1, action2)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
@ -704,10 +853,9 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
document = Document.objects.first()
|
||||||
_, overrides = m.call_args
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_custom_field_ids"],
|
list(document.custom_fields.all().values_list("field", flat=True)),
|
||||||
[self.cf1.pk],
|
[self.cf1.pk],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1369,8 +1517,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
group_perms: QuerySet = get_groups_with_perms(doc)
|
group_perms: QuerySet = get_groups_with_perms(doc)
|
||||||
self.assertNotIn(self.group1, group_perms)
|
self.assertNotIn(self.group1, group_perms)
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_removal_action_document_consumed(self):
|
||||||
def test_removal_action_document_consumed(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Workflow with assignment and removal actions
|
- Workflow with assignment and removal actions
|
||||||
@ -1429,7 +1576,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.actions.add(action2)
|
w.actions.add(action2)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
@ -1440,26 +1590,57 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
|
||||||
_, overrides = m.call_args
|
document = Document.objects.first()
|
||||||
self.assertIsNone(overrides["override_correspondent_id"])
|
|
||||||
self.assertIsNone(overrides["override_document_type_id"])
|
self.assertIsNone(document.correspondent)
|
||||||
|
self.assertIsNone(document.document_type)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_tag_ids"],
|
list(document.tags.all()),
|
||||||
[self.t2.pk, self.t3.pk],
|
[self.t2, self.t3],
|
||||||
)
|
)
|
||||||
self.assertIsNone(overrides["override_storage_path_id"])
|
self.assertIsNone(document.storage_path)
|
||||||
self.assertIsNone(overrides["override_owner_id"])
|
self.assertIsNone(document.owner)
|
||||||
self.assertEqual(overrides["override_view_users"], [self.user2.pk])
|
|
||||||
self.assertEqual(overrides["override_view_groups"], [self.group2.pk])
|
|
||||||
self.assertEqual(overrides["override_change_users"], [self.user2.pk])
|
|
||||||
self.assertEqual(overrides["override_change_groups"], [self.group2.pk])
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_title"],
|
list(
|
||||||
"Doc from {correspondent}",
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["view_document"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.user2],
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_custom_field_ids"],
|
list(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.group2],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(
|
||||||
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["change_document"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.user2],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[self.group2],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
document.title,
|
||||||
|
"Doc from None",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
list(document.custom_fields.all().values_list("field", flat=True)),
|
||||||
[self.cf2.pk],
|
[self.cf2.pk],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1467,8 +1648,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
expected_str = f"Document matched {trigger} from {w}"
|
expected_str = f"Document matched {trigger} from {w}"
|
||||||
self.assertIn(expected_str, info)
|
self.assertIn(expected_str, info)
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_removal_action_document_consumed_remove_all(self):
|
||||||
def test_removal_action_document_consumed_removeall(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Workflow with assignment and removal actions with remove all fields set
|
- Workflow with assignment and removal actions with remove all fields set
|
||||||
@ -1519,7 +1699,10 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.actions.add(action2)
|
w.actions.add(action2)
|
||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
@ -1530,23 +1713,46 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
m.assert_called_once()
|
document = Document.objects.first()
|
||||||
_, overrides = m.call_args
|
self.assertIsNone(document.correspondent)
|
||||||
self.assertIsNone(overrides["override_correspondent_id"])
|
self.assertIsNone(document.document_type)
|
||||||
self.assertIsNone(overrides["override_document_type_id"])
|
self.assertEqual(document.tags.all().count(), 0)
|
||||||
|
|
||||||
|
self.assertIsNone(document.storage_path)
|
||||||
|
self.assertIsNone(document.owner)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_tag_ids"],
|
get_users_with_perms(
|
||||||
[],
|
document,
|
||||||
|
only_with_perms_in=["view_document"],
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
self.assertIsNone(overrides["override_storage_path_id"])
|
|
||||||
self.assertIsNone(overrides["override_owner_id"])
|
|
||||||
self.assertEqual(overrides["override_view_users"], [])
|
|
||||||
self.assertEqual(overrides["override_view_groups"], [])
|
|
||||||
self.assertEqual(overrides["override_change_users"], [])
|
|
||||||
self.assertEqual(overrides["override_change_groups"], [])
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
overrides["override_custom_field_ids"],
|
get_groups_with_perms(
|
||||||
[],
|
document,
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_users_with_perms(
|
||||||
|
document,
|
||||||
|
only_with_perms_in=["change_document"],
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_groups_with_perms(
|
||||||
|
document,
|
||||||
|
).count(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
document.custom_fields.all()
|
||||||
|
.values_list(
|
||||||
|
"field",
|
||||||
|
)
|
||||||
|
.count(),
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
info = cm.output[0]
|
info = cm.output[0]
|
||||||
|
@ -3,6 +3,7 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from collections.abc import Generator
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
@ -21,8 +22,10 @@ from django.db.migrations.executor import MigrationExecutor
|
|||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from documents.consumer import ConsumerPlugin
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
|
from documents.data_models import DocumentSource
|
||||||
from documents.parsers import ParseError
|
from documents.parsers import ParseError
|
||||||
from documents.plugins.helpers import ProgressStatusOptions
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
|
|
||||||
@ -326,6 +329,30 @@ class SampleDirMixin:
|
|||||||
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
|
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
|
||||||
|
|
||||||
|
|
||||||
|
class GetConsumerMixin:
|
||||||
|
@contextmanager
|
||||||
|
def get_consumer(
|
||||||
|
self,
|
||||||
|
filepath: Path,
|
||||||
|
overrides: Union[DocumentMetadataOverrides, None] = None,
|
||||||
|
source: DocumentSource = DocumentSource.ConsumeFolder,
|
||||||
|
) -> Generator[ConsumerPlugin, None, None]:
|
||||||
|
# Store this for verification
|
||||||
|
self.status = DummyProgressManager(filepath.name, None)
|
||||||
|
reader = ConsumerPlugin(
|
||||||
|
ConsumableDocument(source, original_file=filepath),
|
||||||
|
overrides or DocumentMetadataOverrides(),
|
||||||
|
self.status, # type: ignore
|
||||||
|
self.dirs.scratch_dir,
|
||||||
|
"task-id",
|
||||||
|
)
|
||||||
|
reader.setup()
|
||||||
|
try:
|
||||||
|
yield reader
|
||||||
|
finally:
|
||||||
|
reader.cleanup()
|
||||||
|
|
||||||
|
|
||||||
class DummyProgressManager:
|
class DummyProgressManager:
|
||||||
"""
|
"""
|
||||||
A dummy handler for progress management that doesn't actually try to
|
A dummy handler for progress management that doesn't actually try to
|
||||||
|
@ -7,7 +7,6 @@ import re
|
|||||||
import tempfile
|
import tempfile
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from platform import machine
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Union
|
from typing import Union
|
||||||
@ -112,7 +111,7 @@ def __get_list(
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _parse_redis_url(env_redis: Optional[str]) -> tuple[str]:
|
def _parse_redis_url(env_redis: Optional[str]) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Gets the Redis information from the environment or a default and handles
|
Gets the Redis information from the environment or a default and handles
|
||||||
converting from incompatible django_channels and celery formats.
|
converting from incompatible django_channels and celery formats.
|
||||||
@ -371,10 +370,7 @@ ASGI_APPLICATION = "paperless.asgi.application"
|
|||||||
STATIC_URL = os.getenv("PAPERLESS_STATIC_URL", BASE_URL + "static/")
|
STATIC_URL = os.getenv("PAPERLESS_STATIC_URL", BASE_URL + "static/")
|
||||||
WHITENOISE_STATIC_PREFIX = "/static/"
|
WHITENOISE_STATIC_PREFIX = "/static/"
|
||||||
|
|
||||||
if machine().lower() == "aarch64": # pragma: no cover
|
_static_backend = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
_static_backend = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
|
||||||
else:
|
|
||||||
_static_backend = "whitenoise.storage.CompressedStaticFilesStorage"
|
|
||||||
|
|
||||||
STORAGES = {
|
STORAGES = {
|
||||||
"staticfiles": {
|
"staticfiles": {
|
||||||
|
@ -425,6 +425,10 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
|
|
||||||
logging_name = "paperless_mail"
|
logging_name = "paperless_mail"
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.renew_logging_group()
|
||||||
|
|
||||||
def _correspondent_from_name(self, name: str) -> Optional[Correspondent]:
|
def _correspondent_from_name(self, name: str) -> Optional[Correspondent]:
|
||||||
try:
|
try:
|
||||||
return Correspondent.objects.get_or_create(name=name)[0]
|
return Correspondent.objects.get_or_create(name=name)[0]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user