mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-10-28 03:46:06 -05:00
Compare commits
7 Commits
dependabot
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2df250952 | ||
|
|
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
|
||||
attributes:
|
||||
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
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -6,8 +6,8 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### ⚠️ Please remember: issues are for *bugs*
|
||||
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.
|
||||
### ⚠️ Please remember: issues are for *bugs* only! ⚠️
|
||||
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
|
||||
- type: markdown
|
||||
@@ -59,6 +59,12 @@ body:
|
||||
label: Browser logs
|
||||
description: Logs from the web browser related to your issue, if needed
|
||||
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
|
||||
id: version
|
||||
attributes:
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -181,10 +181,11 @@ jobs:
|
||||
pytest
|
||||
- name: Upload backend test results to Codecov
|
||||
if: always()
|
||||
uses: codecov/test-results-action@v1
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
flags: backend-python-${{ matrix.python-version }}
|
||||
files: junit.xml
|
||||
report_type: test_results
|
||||
- name: Upload backend coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
@@ -260,11 +261,12 @@ jobs:
|
||||
- name: Run Jest unit tests
|
||||
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||
- name: Upload frontend test results to Codecov
|
||||
uses: codecov/test-results-action@v1
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
flags: frontend-node-${{ matrix.node-version }}
|
||||
directory: src-ui/
|
||||
report_type: test_results
|
||||
- name: Upload frontend coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
||||
@@ -32,7 +32,7 @@ RUN set -eux \
|
||||
# Purpose: Installs s6-overlay and rootfs
|
||||
# Comments:
|
||||
# - Don't leave anything extra in here either
|
||||
FROM ghcr.io/astral-sh/uv:0.9.4-python3.12-bookworm-slim AS s6-overlay-base
|
||||
FROM ghcr.io/astral-sh/uv:0.9.5-python3.12-bookworm-slim AS s6-overlay-base
|
||||
|
||||
WORKDIR /usr/src/s6
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ dependencies = [
|
||||
"djangorestframework~=3.16",
|
||||
"djangorestframework-guardian~=0.4.0",
|
||||
"drf-spectacular~=0.28",
|
||||
"drf-spectacular-sidecar~=2025.10.1",
|
||||
"drf-spectacular-sidecar~=2025.9.1",
|
||||
"drf-writable-nested~=0.7.1",
|
||||
"filelock~=3.20.0",
|
||||
"flower~=2.0.1",
|
||||
|
||||
@@ -4539,32 +4539,32 @@
|
||||
<source>Create new user account</source>
|
||||
<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="linenumber">70</context>
|
||||
<context context-type="linenumber">72</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2887331217965896363" datatype="html">
|
||||
<source>Edit user account</source>
|
||||
<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="linenumber">74</context>
|
||||
<context context-type="linenumber">76</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5872286584705575476" datatype="html">
|
||||
<source>Totp deactivated</source>
|
||||
<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="linenumber">130</context>
|
||||
<context context-type="linenumber">132</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6439190193788239059" datatype="html">
|
||||
<source>Totp deactivation failed</source>
|
||||
<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="linenumber">133</context>
|
||||
<context context-type="linenumber">135</context>
|
||||
</context-group>
|
||||
<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="linenumber">138</context>
|
||||
<context context-type="linenumber">140</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<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 { SettingsService } from 'src/app/services/settings.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 { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
@@ -28,6 +29,7 @@ import { PermissionsSelectComponent } from '../../permissions-select/permissions
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PasswordComponent,
|
||||
ConfirmButtonComponent,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
|
||||
@@ -7,6 +7,8 @@ from django.conf import settings
|
||||
from django.core.mail import EmailMessage
|
||||
from filelock import FileLock
|
||||
|
||||
from documents.data_models import ConsumableDocument
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from documents.models import Document
|
||||
|
||||
@@ -15,7 +17,7 @@ def send_email(
|
||||
subject: str,
|
||||
body: str,
|
||||
to: list[str],
|
||||
attachments: list[Document],
|
||||
attachments: list[Document | ConsumableDocument],
|
||||
*,
|
||||
use_archive: bool,
|
||||
) -> int:
|
||||
@@ -45,17 +47,20 @@ def send_email(
|
||||
# Something could be renaming the file concurrently so it can't be attached
|
||||
with FileLock(settings.MEDIA_LOCK):
|
||||
for document in attachments:
|
||||
attachment_path = (
|
||||
document.archive_path
|
||||
if use_archive and document.has_archive_version
|
||||
else document.source_path
|
||||
)
|
||||
|
||||
friendly_filename = _get_unique_filename(
|
||||
document,
|
||||
used_filenames,
|
||||
archive=use_archive and document.has_archive_version,
|
||||
)
|
||||
if isinstance(document, ConsumableDocument):
|
||||
attachment_path = document.original_file
|
||||
friendly_filename = document.original_file.name
|
||||
else:
|
||||
attachment_path = (
|
||||
document.archive_path
|
||||
if use_archive and document.has_archive_version
|
||||
else document.source_path
|
||||
)
|
||||
friendly_filename = _get_unique_filename(
|
||||
document,
|
||||
used_filenames,
|
||||
archive=use_archive and document.has_archive_version,
|
||||
)
|
||||
used_filenames.add(friendly_filename)
|
||||
|
||||
with attachment_path.open("rb") as f:
|
||||
|
||||
@@ -35,15 +35,13 @@ class Migration(migrations.Migration):
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="WorkflowAction",
|
||||
model_name="workflowaction",
|
||||
name="assign_title",
|
||||
field=models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=(
|
||||
"Assign a document title, can be a JINJA2 template, "
|
||||
"see documentation.",
|
||||
),
|
||||
help_text="Assign a document title, must be a Jinja2 template, see documentation.",
|
||||
null=True,
|
||||
verbose_name="assign title",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
|
||||
@@ -2,9 +2,11 @@ import types
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
|
||||
from documents import index
|
||||
from documents.admin import DocumentAdmin
|
||||
@@ -125,3 +127,36 @@ class TestPaperlessAdmin(DirectoriesMixin, TestCase):
|
||||
form.request = types.SimpleNamespace(user=superuser)
|
||||
self.assertTrue(form.is_valid())
|
||||
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.data_models import ConsumableDocument
|
||||
from documents.data_models import DocumentMetadataOverrides
|
||||
from documents.data_models import DocumentSource
|
||||
from documents.matching import 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.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(
|
||||
PAPERLESS_EMAIL_HOST="localhost",
|
||||
EMAIL_ENABLED=True,
|
||||
|
||||
@@ -125,6 +125,10 @@ class UserViewSet(ModelViewSet):
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
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 (
|
||||
not request.user.is_superuser
|
||||
and request.data.get("is_superuser") is not None
|
||||
|
||||
10
uv.lock
generated
10
uv.lock
generated
@@ -959,14 +959,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "drf-spectacular-sidecar"
|
||||
version = "2025.10.1"
|
||||
version = "2025.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/e4/99cd1b1c8c69788bd6cb6a2459674f8c75728e79df23ac7beddd094bf805/drf_spectacular_sidecar-2025.10.1.tar.gz", hash = "sha256:506a5a21ce1ad7211c28acb4e2112e213f6dc095a2052ee6ed6db1ffe8eb5a7b", size = 2420998, upload-time = "2025-10-01T11:23:27.092Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/e2/85a0b8dbed8631165a6b49b2aee57636da8e4e710c444566636ffd972a7b/drf_spectacular_sidecar-2025.9.1.tar.gz", hash = "sha256:da2aa45da48fff76de7a1e357b84d1eb0b9df40ca89ec19d5fe94ad1037bb3c8", size = 2420902, upload-time = "2025-09-01T11:23:24.156Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/87/70c67391e4ce68715d4dfae8dd33caeda2552af22f436ba55b8867a040fe/drf_spectacular_sidecar-2025.10.1-py3-none-any.whl", hash = "sha256:f1de343184d1a938179ce363d318258fe1e5f02f2f774625272364835f1c42bd", size = 2440241, upload-time = "2025-10-01T11:23:25.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/24/db59146ba89491fe1d44ca8aef239c94bf3c7fd41523976090f099430312/drf_spectacular_sidecar-2025.9.1-py3-none-any.whl", hash = "sha256:8e80625209b8a23ff27616db305b9ab71c2e2d1069dacd99720a9c11e429af50", size = 2440255, upload-time = "2025-09-01T11:23:22.822Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2262,7 +2262,7 @@ requires-dist = [
|
||||
{ name = "concurrent-log-handler", specifier = "~=0.9.25" },
|
||||
{ name = "dateparser", specifier = "~=1.2" },
|
||||
{ name = "django", specifier = "~=5.2.5" },
|
||||
{ name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.4.0" },
|
||||
{ name = "django-allauth", extras = ["socialaccount", "mfa"], specifier = "~=65.4.0" },
|
||||
{ name = "django-auditlog", specifier = "~=3.2.1" },
|
||||
{ name = "django-cachalot", specifier = "~=2.8.0" },
|
||||
{ name = "django-celery-results", specifier = "~=2.6.0" },
|
||||
@@ -2277,7 +2277,7 @@ requires-dist = [
|
||||
{ name = "djangorestframework", specifier = "~=3.16" },
|
||||
{ name = "djangorestframework-guardian", specifier = "~=0.4.0" },
|
||||
{ name = "drf-spectacular", specifier = "~=0.28" },
|
||||
{ name = "drf-spectacular-sidecar", specifier = "~=2025.10.1" },
|
||||
{ name = "drf-spectacular-sidecar", specifier = "~=2025.9.1" },
|
||||
{ name = "drf-writable-nested", specifier = "~=0.7.1" },
|
||||
{ name = "filelock", specifier = "~=3.20.0" },
|
||||
{ name = "flower", specifier = "~=2.0.1" },
|
||||
|
||||
Reference in New Issue
Block a user