mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-29 13:48:09 -06:00
Compare commits
9 Commits
v2.19.2
...
35bc673648
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35bc673648 | ||
|
|
d0bd111eab | ||
|
|
cd81f750b4 | ||
|
|
48d21da13b | ||
|
|
701aafce06 | ||
|
|
1c4fa7237c | ||
|
|
63dab0ab09 | ||
|
|
276dc31abe | ||
|
|
a11a2ec13f |
2
.github/DISCUSSION_TEMPLATE/support.yml
vendored
2
.github/DISCUSSION_TEMPLATE/support.yml
vendored
@@ -51,5 +51,5 @@ body:
|
|||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Relevant logs or output
|
label: Relevant logs or output
|
||||||
description: If you have logs, errors that might help, paste it here.
|
description: If you have logs, errors that might help, paste it here. For example other containers or services (database, redis, etc).
|
||||||
render: bash
|
render: bash
|
||||||
|
|||||||
10
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -6,8 +6,8 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
### ⚠️ Please remember: issues are for *bugs*
|
### ⚠️ Please remember: issues are for *bugs* only! ⚠️
|
||||||
That is, something you believe affects every single user of Paperless-ngx, not just you. If you're not sure, start with one of the other options below.
|
That is, something you believe affects every single user of Paperless-ngx (and the demo, for example), not just you. If you are not sure, start with one of the other options below.
|
||||||
|
|
||||||
Also, note that **Paperless-ngx does not perform OCR or archive file creation itself**, those are handled by other tools. Problems with OCR or archive versions of specific files should likely be raised 'upstream', see https://github.com/ocrmypdf/OCRmyPDF/issues or https://github.com/tesseract-ocr/tesseract/issues
|
Also, note that **Paperless-ngx does not perform OCR or archive file creation itself**, those are handled by other tools. Problems with OCR or archive versions of specific files should likely be raised 'upstream', see https://github.com/ocrmypdf/OCRmyPDF/issues or https://github.com/tesseract-ocr/tesseract/issues
|
||||||
- type: markdown
|
- type: markdown
|
||||||
@@ -59,6 +59,12 @@ body:
|
|||||||
label: Browser logs
|
label: Browser logs
|
||||||
description: Logs from the web browser related to your issue, if needed
|
description: Logs from the web browser related to your issue, if needed
|
||||||
render: bash
|
render: bash
|
||||||
|
- type: textarea
|
||||||
|
id: logs_services
|
||||||
|
attributes:
|
||||||
|
label: Services logs
|
||||||
|
description: Logs from other services (or containers) related to your issue, if needed. For example, the database or redis logs.
|
||||||
|
render: bash
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -181,10 +181,11 @@ jobs:
|
|||||||
pytest
|
pytest
|
||||||
- name: Upload backend test results to Codecov
|
- name: Upload backend test results to Codecov
|
||||||
if: always()
|
if: always()
|
||||||
uses: codecov/test-results-action@v1
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: backend-python-${{ matrix.python-version }}
|
flags: backend-python-${{ matrix.python-version }}
|
||||||
files: junit.xml
|
files: junit.xml
|
||||||
|
report_type: test_results
|
||||||
- name: Upload backend coverage to Codecov
|
- name: Upload backend coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
@@ -260,11 +261,12 @@ jobs:
|
|||||||
- name: Run Jest unit tests
|
- name: Run Jest unit tests
|
||||||
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||||
- name: Upload frontend test results to Codecov
|
- name: Upload frontend test results to Codecov
|
||||||
uses: codecov/test-results-action@v1
|
|
||||||
if: always()
|
if: always()
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: frontend-node-${{ matrix.node-version }}
|
flags: frontend-node-${{ matrix.node-version }}
|
||||||
directory: src-ui/
|
directory: src-ui/
|
||||||
|
report_type: test_results
|
||||||
- name: Upload frontend coverage to Codecov
|
- name: Upload frontend coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -4539,32 +4539,32 @@
|
|||||||
<source>Create new user account</source>
|
<source>Create new user account</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">70</context>
|
<context context-type="linenumber">72</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2887331217965896363" datatype="html">
|
<trans-unit id="2887331217965896363" datatype="html">
|
||||||
<source>Edit user account</source>
|
<source>Edit user account</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">74</context>
|
<context context-type="linenumber">76</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5872286584705575476" datatype="html">
|
<trans-unit id="5872286584705575476" datatype="html">
|
||||||
<source>Totp deactivated</source>
|
<source>Totp deactivated</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">130</context>
|
<context context-type="linenumber">132</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6439190193788239059" datatype="html">
|
<trans-unit id="6439190193788239059" datatype="html">
|
||||||
<source>Totp deactivation failed</source>
|
<source>Totp deactivation failed</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">133</context>
|
<context context-type="linenumber">135</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">138</context>
|
<context context-type="linenumber">140</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8419515490539218007" datatype="html">
|
<trans-unit id="8419515490539218007" datatype="html">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { GroupService } from 'src/app/services/rest/group.service'
|
|||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component'
|
||||||
import { PasswordComponent } from '../../input/password/password.component'
|
import { PasswordComponent } from '../../input/password/password.component'
|
||||||
import { SelectComponent } from '../../input/select/select.component'
|
import { SelectComponent } from '../../input/select/select.component'
|
||||||
import { TextComponent } from '../../input/text/text.component'
|
import { TextComponent } from '../../input/text/text.component'
|
||||||
@@ -28,6 +29,7 @@ import { PermissionsSelectComponent } from '../../permissions-select/permissions
|
|||||||
SelectComponent,
|
SelectComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
PasswordComponent,
|
PasswordComponent,
|
||||||
|
ConfirmButtonComponent,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from django.conf import settings
|
|||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
|
|
||||||
|
from documents.data_models import ConsumableDocument
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ def send_email(
|
|||||||
subject: str,
|
subject: str,
|
||||||
body: str,
|
body: str,
|
||||||
to: list[str],
|
to: list[str],
|
||||||
attachments: list[Document],
|
attachments: list[Document | ConsumableDocument],
|
||||||
*,
|
*,
|
||||||
use_archive: bool,
|
use_archive: bool,
|
||||||
) -> int:
|
) -> int:
|
||||||
@@ -45,17 +47,20 @@ def send_email(
|
|||||||
# Something could be renaming the file concurrently so it can't be attached
|
# Something could be renaming the file concurrently so it can't be attached
|
||||||
with FileLock(settings.MEDIA_LOCK):
|
with FileLock(settings.MEDIA_LOCK):
|
||||||
for document in attachments:
|
for document in attachments:
|
||||||
attachment_path = (
|
if isinstance(document, ConsumableDocument):
|
||||||
document.archive_path
|
attachment_path = document.original_file
|
||||||
if use_archive and document.has_archive_version
|
friendly_filename = document.original_file.name
|
||||||
else document.source_path
|
else:
|
||||||
)
|
attachment_path = (
|
||||||
|
document.archive_path
|
||||||
friendly_filename = _get_unique_filename(
|
if use_archive and document.has_archive_version
|
||||||
document,
|
else document.source_path
|
||||||
used_filenames,
|
)
|
||||||
archive=use_archive and document.has_archive_version,
|
friendly_filename = _get_unique_filename(
|
||||||
)
|
document,
|
||||||
|
used_filenames,
|
||||||
|
archive=use_archive and document.has_archive_version,
|
||||||
|
)
|
||||||
used_filenames.add(friendly_filename)
|
used_filenames.add(friendly_filename)
|
||||||
|
|
||||||
with attachment_path.open("rb") as f:
|
with attachment_path.open("rb") as f:
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import logging
|
|||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db import transaction
|
|
||||||
|
|
||||||
from documents.templating.utils import convert_format_str_to_template_format
|
from documents.templating.utils import convert_format_str_to_template_format
|
||||||
|
|
||||||
@@ -11,21 +10,34 @@ logger = logging.getLogger("paperless.migrations")
|
|||||||
|
|
||||||
|
|
||||||
def convert_from_format_to_template(apps, schema_editor):
|
def convert_from_format_to_template(apps, schema_editor):
|
||||||
WorkflowActions = apps.get_model("documents", "WorkflowAction")
|
WorkflowAction = apps.get_model("documents", "WorkflowAction")
|
||||||
|
|
||||||
with transaction.atomic():
|
batch_size = 500
|
||||||
for WorkflowAction in WorkflowActions.objects.all():
|
actions_to_update = []
|
||||||
if not WorkflowAction.assign_title:
|
|
||||||
continue
|
queryset = (
|
||||||
WorkflowAction.assign_title = convert_format_str_to_template_format(
|
WorkflowAction.objects.filter(assign_title__isnull=False)
|
||||||
WorkflowAction.assign_title,
|
.exclude(assign_title="")
|
||||||
)
|
.only("id", "assign_title")
|
||||||
logger.debug(
|
)
|
||||||
"Converted WorkflowAction id %d title to template format: %s",
|
|
||||||
WorkflowAction.id,
|
for action in queryset:
|
||||||
WorkflowAction.assign_title,
|
action.assign_title = convert_format_str_to_template_format(
|
||||||
)
|
action.assign_title,
|
||||||
WorkflowAction.save()
|
)
|
||||||
|
logger.debug(
|
||||||
|
"Converted WorkflowAction id %d title to template format: %s",
|
||||||
|
action.id,
|
||||||
|
action.assign_title,
|
||||||
|
)
|
||||||
|
actions_to_update.append(action)
|
||||||
|
|
||||||
|
if actions_to_update:
|
||||||
|
WorkflowAction.objects.bulk_update(
|
||||||
|
actions_to_update,
|
||||||
|
["assign_title"],
|
||||||
|
batch_size=batch_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -35,15 +47,13 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="WorkflowAction",
|
model_name="workflowaction",
|
||||||
name="assign_title",
|
name="assign_title",
|
||||||
field=models.TextField(
|
field=models.TextField(
|
||||||
null=True,
|
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=(
|
help_text="Assign a document title, must be a Jinja2 template, see documentation.",
|
||||||
"Assign a document title, can be a JINJA2 template, "
|
null=True,
|
||||||
"see documentation.",
|
verbose_name="assign title",
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
migrations.RunPython(
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-27 15:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "1073_migrate_workflow_title_jinja"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowrun",
|
||||||
|
name="deleted_at",
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowrun",
|
||||||
|
name="restored_at",
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowrun",
|
||||||
|
name="transaction_id",
|
||||||
|
field=models.UUIDField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1547,7 +1547,7 @@ class Workflow(models.Model):
|
|||||||
return f"Workflow: {self.name}"
|
return f"Workflow: {self.name}"
|
||||||
|
|
||||||
|
|
||||||
class WorkflowRun(models.Model):
|
class WorkflowRun(SoftDeleteModel):
|
||||||
workflow = models.ForeignKey(
|
workflow = models.ForeignKey(
|
||||||
Workflow,
|
Workflow,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def parse_w_workflow_placeholders(
|
|||||||
if doc_url is not None:
|
if doc_url is not None:
|
||||||
formatting.update({"doc_url": doc_url})
|
formatting.update({"doc_url": doc_url})
|
||||||
|
|
||||||
logger.debug(f"Jinja Template is : {text}")
|
logger.debug(f"Parsing Workflow Jinja template: {text}")
|
||||||
try:
|
try:
|
||||||
template = _template_environment.from_string(
|
template = _template_environment.from_string(
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import types
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
from documents import index
|
from documents import index
|
||||||
from documents.admin import DocumentAdmin
|
from documents.admin import DocumentAdmin
|
||||||
@@ -125,3 +127,36 @@ class TestPaperlessAdmin(DirectoriesMixin, TestCase):
|
|||||||
form.request = types.SimpleNamespace(user=superuser)
|
form.request = types.SimpleNamespace(user=superuser)
|
||||||
self.assertTrue(form.is_valid())
|
self.assertTrue(form.is_valid())
|
||||||
self.assertEqual({}, form.errors)
|
self.assertEqual({}, form.errors)
|
||||||
|
|
||||||
|
def test_superuser_can_only_be_modified_by_superuser(self):
|
||||||
|
superuser = User.objects.create_superuser(username="superuser", password="test")
|
||||||
|
user = User.objects.create(
|
||||||
|
username="test",
|
||||||
|
is_superuser=False,
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
change_user_perm = Permission.objects.get(codename="change_user")
|
||||||
|
user.user_permissions.add(change_user_perm)
|
||||||
|
|
||||||
|
self.client.force_login(user)
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/api/users/{superuser.pk}/",
|
||||||
|
{"first_name": "Updated"},
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertEqual(
|
||||||
|
response.content.decode(),
|
||||||
|
"Superusers can only be modified by other superusers",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.logout()
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/api/users/{superuser.pk}/",
|
||||||
|
{"first_name": "Updated"},
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
superuser.refresh_from_db()
|
||||||
|
self.assertEqual(superuser.first_name, "Updated")
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from pytest_django.fixtures import SettingsWrapper
|
|||||||
|
|
||||||
from documents import tasks
|
from documents import tasks
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.matching import document_matches_workflow
|
from documents.matching import document_matches_workflow
|
||||||
from documents.matching import existing_document_matches_workflow
|
from documents.matching import existing_document_matches_workflow
|
||||||
@@ -2788,6 +2789,80 @@ class TestWorkflows(
|
|||||||
self.assertEqual(doc.tags.all().count(), 1)
|
self.assertEqual(doc.tags.all().count(), 1)
|
||||||
self.assertIn(self.t2, doc.tags.all())
|
self.assertIn(self.t2, doc.tags.all())
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
PAPERLESS_EMAIL_HOST="localhost",
|
||||||
|
EMAIL_ENABLED=True,
|
||||||
|
PAPERLESS_URL="http://localhost:8000",
|
||||||
|
)
|
||||||
|
@mock.patch("django.core.mail.message.EmailMessage.send")
|
||||||
|
def test_workflow_assignment_then_email_includes_attachment(self, mock_email_send):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Workflow with assignment and email actions
|
||||||
|
- Email action configured to include the document
|
||||||
|
WHEN:
|
||||||
|
- Workflow is run on a newly created document
|
||||||
|
THEN:
|
||||||
|
- Email action sends the document as an attachment
|
||||||
|
"""
|
||||||
|
|
||||||
|
storage_path = StoragePath.objects.create(
|
||||||
|
name="sp2",
|
||||||
|
path="workflow/{{ document.pk }}",
|
||||||
|
)
|
||||||
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
|
)
|
||||||
|
assignment_action = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.ASSIGNMENT,
|
||||||
|
assign_storage_path=storage_path,
|
||||||
|
assign_owner=self.user2,
|
||||||
|
)
|
||||||
|
assignment_action.assign_tags.add(self.t1)
|
||||||
|
|
||||||
|
email_action_config = WorkflowActionEmail.objects.create(
|
||||||
|
subject="Doc ready {doc_title}",
|
||||||
|
body="Document URL: {doc_url}",
|
||||||
|
to="owner@example.com",
|
||||||
|
include_document=True,
|
||||||
|
)
|
||||||
|
email_action = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.EMAIL,
|
||||||
|
email=email_action_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow = Workflow.objects.create(name="Assignment then email", order=0)
|
||||||
|
workflow.triggers.add(trigger)
|
||||||
|
workflow.actions.set([assignment_action, email_action])
|
||||||
|
|
||||||
|
temp_working_copy = shutil.copy(
|
||||||
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
|
self.dirs.scratch_dir / "working-copy.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
Document.objects.create(
|
||||||
|
title="workflow doc",
|
||||||
|
correspondent=self.c,
|
||||||
|
checksum="wf-assignment-email",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
consumable_document = ConsumableDocument(
|
||||||
|
source=DocumentSource.ConsumeFolder,
|
||||||
|
original_file=temp_working_copy,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_email_send.return_value = 1
|
||||||
|
|
||||||
|
with self.assertNoLogs("paperless.handlers", level="ERROR"):
|
||||||
|
run_workflows(
|
||||||
|
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
|
consumable_document,
|
||||||
|
overrides=DocumentMetadataOverrides(),
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_email_send.assert_called_once()
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
PAPERLESS_EMAIL_HOST="localhost",
|
PAPERLESS_EMAIL_HOST="localhost",
|
||||||
EMAIL_ENABLED=True,
|
EMAIL_ENABLED=True,
|
||||||
|
|||||||
@@ -125,6 +125,10 @@ class UserViewSet(ModelViewSet):
|
|||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
user_to_update: User = self.get_object()
|
user_to_update: User = self.get_object()
|
||||||
|
if not request.user.is_superuser and user_to_update.is_superuser:
|
||||||
|
return HttpResponseForbidden(
|
||||||
|
"Superusers can only be modified by other superusers",
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
not request.user.is_superuser
|
not request.user.is_superuser
|
||||||
and request.data.get("is_superuser") is not None
|
and request.data.get("is_superuser") is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user