Merge branch 'dev' into feature-improve-paperless-task

This commit is contained in:
shamoon 2025-02-21 08:33:40 -08:00
commit 53e9e910d8
17 changed files with 198 additions and 25 deletions

View File

@ -14,7 +14,7 @@ django-celery-results = "*"
django-compression-middleware = "*" django-compression-middleware = "*"
django-cors-headers = "*" django-cors-headers = "*"
django-extensions = "*" django-extensions = "*"
django-filter = "~=24.3" django-filter = "~=25.1"
django-guardian = "*" django-guardian = "*"
django-multiselectfield = "*" django-multiselectfield = "*"
django-soft-delete = "*" django-soft-delete = "*"

16
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "8c1714db280429940577d7b88e5a59a63f92147a15bc6ae14fc4ff2820f2790e" "sha256": "4d54b43e6f093a817b2dc9b923f93b889bf7a42cd937ea971cd8773484fc4636"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@ -577,12 +577,12 @@
}, },
"django-filter": { "django-filter": {
"hashes": [ "hashes": [
"sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64", "sha256:1ec9eef48fa8da1c0ac9b411744b16c3f4c31176c867886e4c48da369c407153",
"sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3" "sha256:4fa48677cf5857b9b1347fed23e355ea792464e0fe07244d1fdfb8a806215b80"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.9'",
"version": "==24.3" "version": "==25.1"
}, },
"django-guardian": { "django-guardian": {
"hashes": [ "hashes": [
@ -3582,12 +3582,12 @@
}, },
"mkdocs-material": { "mkdocs-material": {
"hashes": [ "hashes": [
"sha256:1125622067e26940806701219303b27c0933e04533560725d97ec26fd16a39cf", "sha256:414e8376551def6d644b8e6f77226022868532a792eb2c9accf52199009f568f",
"sha256:c87f7d1c39ce6326da5e10e232aed51bae46252e646755900f4b0fc9192fa832" "sha256:4d1d35e1c1d3e15294cb7fa5d02e0abaee70d408f75027dc7be6e30fb32e6867"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==9.6.3" "version": "==9.6.4"
}, },
"mkdocs-material-extensions": { "mkdocs-material-extensions": {
"hashes": [ "hashes": [

View File

@ -2230,7 +2230,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
<context context-type="linenumber">103</context> <context context-type="linenumber">106</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
@ -2565,7 +2565,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
<context context-type="linenumber">105</context> <context context-type="linenumber">108</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
@ -3322,7 +3322,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
<context context-type="linenumber">85</context> <context context-type="linenumber">87</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1841172489943868696" datatype="html"> <trans-unit id="1841172489943868696" datatype="html">
@ -3333,7 +3333,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
<context context-type="linenumber">93</context> <context context-type="linenumber">96</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6048892649018070225" datatype="html"> <trans-unit id="6048892649018070225" datatype="html">
@ -8193,28 +8193,28 @@
<source>Confirm delete field</source> <source>Confirm delete field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
<context context-type="linenumber">101</context> <context context-type="linenumber">104</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2939457975223185057" datatype="html"> <trans-unit id="2939457975223185057" datatype="html">
<source>This operation will permanently delete this field.</source> <source>This operation will permanently delete this field.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
<context context-type="linenumber">102</context> <context context-type="linenumber">105</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4679555638382452936" datatype="html"> <trans-unit id="4679555638382452936" datatype="html">
<source>Deleted field &quot;<x id="PH" equiv-text="field.name"/>&quot;</source> <source>Deleted field &quot;<x id="PH" equiv-text="field.name"/>&quot;</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
<context context-type="linenumber">111</context> <context context-type="linenumber">114</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4704551499967874824" datatype="html"> <trans-unit id="4704551499967874824" datatype="html">
<source>Error deleting field &quot;<x id="PH" equiv-text="field.name"/>&quot;.</source> <source>Error deleting field &quot;<x id="PH" equiv-text="field.name"/>&quot;.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
<context context-type="linenumber">118</context> <context context-type="linenumber">122</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8084492669582894778" datatype="html"> <trans-unit id="8084492669582894778" datatype="html">

View File

@ -71,6 +71,10 @@ export const DOCUMENT_SOURCE_OPTIONS = [
id: DocumentSource.MailFetch, id: DocumentSource.MailFetch,
name: $localize`Mail Fetch`, name: $localize`Mail Fetch`,
}, },
{
id: DocumentSource.WebUI,
name: $localize`Web UI`,
},
] ]
export const SCHEDULE_DATE_FIELD_OPTIONS = [ export const SCHEDULE_DATE_FIELD_OPTIONS = [

View File

@ -145,7 +145,10 @@ export class SavedViewWidgetComponent
}) })
} }
if (this.savedView.display_fields) { if (
this.savedView.display_fields &&
this.savedView.display_fields.length > 0
) {
this.displayFields = this.savedView.display_fields this.displayFields = this.savedView.display_fields
} }

View File

@ -4,6 +4,7 @@ export enum DocumentSource {
ConsumeFolder = 1, ConsumeFolder = 1,
ApiUpload = 2, ApiUpload = 2,
MailFetch = 3, MailFetch = 3,
WebUI = 4,
} }
export enum WorkflowTriggerType { export enum WorkflowTriggerType {

View File

@ -114,6 +114,48 @@ describe(`Additional service tests for SavedViewService`, () => {
]) ])
}) })
it('should treat empty display_fields as null', () => {
subscription = service
.patch({
id: 1,
name: 'Saved View',
show_on_dashboard: true,
show_in_sidebar: true,
sort_field: 'name',
sort_reverse: true,
filter_rules: [],
display_fields: [],
})
.subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/1/`
)
expect(req.request.body.display_fields).toBeNull()
})
it('should support patch without reload', () => {
subscription = service
.patch(
{
id: 1,
name: 'Saved View',
show_on_dashboard: true,
show_in_sidebar: true,
sort_field: 'name',
sort_reverse: true,
filter_rules: [],
},
false
)
.subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/1/`
)
expect(req.request.method).toEqual('PATCH')
req.flush({})
httpTestingController.verify() // no reload
})
beforeEach(() => { beforeEach(() => {
// Dont need to setup again // Dont need to setup again

View File

@ -87,12 +87,21 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
return super.create(o).pipe(tap(() => this.reload())) return super.create(o).pipe(tap(() => this.reload()))
} }
update(o: SavedView) { patch(o: SavedView, reload: boolean = false): Observable<SavedView> {
return super.update(o).pipe(tap(() => this.reload())) if (o.display_fields?.length === 0) {
o.display_fields = null
}
return super.patch(o).pipe(
tap(() => {
if (reload) {
this.reload()
}
})
)
} }
patchMany(objects: SavedView[]): Observable<SavedView[]> { patchMany(objects: SavedView[]): Observable<SavedView[]> {
return combineLatest(objects.map((o) => super.patch(o))).pipe( return combineLatest(objects.map((o) => this.patch(o, false))).pipe(
tap(() => this.reload()) tap(() => this.reload())
) )
} }

View File

@ -37,6 +37,7 @@ export class UploadDocumentsService {
private uploadFile(file: File) { private uploadFile(file: File) {
let formData = new FormData() let formData = new FormData()
formData.append('document', file, file.name) formData.append('document', file, file.name)
formData.append('from_webui', 'true')
let status = this.websocketStatusService.newFileUpload(file.name) let status = this.websocketStatusService.newFileUpload(file.name)
status.message = $localize`Connecting...` status.message = $localize`Connecting...`

View File

@ -144,6 +144,7 @@ class DocumentSource(IntEnum):
ConsumeFolder = 1 ConsumeFolder = 1
ApiUpload = 2 ApiUpload = 2
MailFetch = 3 MailFetch = 3
WebUI = 4
@dataclasses.dataclass @dataclasses.dataclass

View File

@ -0,0 +1,52 @@
# Generated by Django 5.1.6 on 2025-02-20 04:55
import multiselectfield.db.fields
from django.db import migrations
from django.db import models
# WebUI source was added, so all existing APIUpload sources should be updated to include WebUI
def update_workflow_sources(apps, schema_editor):
WorkflowTrigger = apps.get_model("documents", "WorkflowTrigger")
for trigger in WorkflowTrigger.objects.all():
sources = list(trigger.sources)
if 2 in sources:
sources.append(4)
trigger.sources = sources
trigger.save()
class Migration(migrations.Migration):
dependencies = [
("documents", "1062_alter_savedviewfilterrule_rule_type"),
]
operations = [
migrations.AlterField(
model_name="workflowactionwebhook",
name="url",
field=models.CharField(
help_text="The destination URL for the notification.",
max_length=256,
verbose_name="webhook url",
),
),
migrations.AlterField(
model_name="workflowtrigger",
name="sources",
field=multiselectfield.db.fields.MultiSelectField(
choices=[
(1, "Consume Folder"),
(2, "Api Upload"),
(3, "Mail Fetch"),
(4, "Web UI"),
],
default="1,2,3,4",
max_length=7,
),
),
migrations.RunPython(
code=update_workflow_sources,
reverse_code=migrations.RunPython.noop,
),
]

View File

@ -1054,6 +1054,7 @@ class WorkflowTrigger(models.Model):
CONSUME_FOLDER = DocumentSource.ConsumeFolder.value, _("Consume Folder") CONSUME_FOLDER = DocumentSource.ConsumeFolder.value, _("Consume Folder")
API_UPLOAD = DocumentSource.ApiUpload.value, _("Api Upload") API_UPLOAD = DocumentSource.ApiUpload.value, _("Api Upload")
MAIL_FETCH = DocumentSource.MailFetch.value, _("Mail Fetch") MAIL_FETCH = DocumentSource.MailFetch.value, _("Mail Fetch")
WEB_UI = DocumentSource.WebUI.value, _("Web UI")
class ScheduleDateField(models.TextChoices): class ScheduleDateField(models.TextChoices):
ADDED = "added", _("Added") ADDED = "added", _("Added")
@ -1068,9 +1069,9 @@ class WorkflowTrigger(models.Model):
) )
sources = MultiSelectField( sources = MultiSelectField(
max_length=5, max_length=7,
choices=DocumentSourceChoices.choices, choices=DocumentSourceChoices.choices,
default=f"{DocumentSource.ConsumeFolder},{DocumentSource.ApiUpload},{DocumentSource.MailFetch}", default=f"{DocumentSource.ConsumeFolder},{DocumentSource.ApiUpload},{DocumentSource.MailFetch},{DocumentSource.WebUI}",
) )
filter_path = models.CharField( filter_path = models.CharField(

View File

@ -1147,6 +1147,15 @@ class SavedViewSerializer(OwnedObjectSerializer):
if "user" in validated_data: if "user" in validated_data:
# backwards compatibility # backwards compatibility
validated_data["owner"] = validated_data.pop("user") validated_data["owner"] = validated_data.pop("user")
if (
"display_fields" in validated_data
and isinstance(
validated_data["display_fields"],
list,
)
and len(validated_data["display_fields"]) == 0
):
validated_data["display_fields"] = None
super().update(instance, validated_data) super().update(instance, validated_data)
if rules_data is not None: if rules_data is not None:
SavedViewFilterRule.objects.filter(saved_view=instance).delete() SavedViewFilterRule.objects.filter(saved_view=instance).delete()
@ -1537,6 +1546,12 @@ class PostDocumentSerializer(serializers.Serializer):
required=False, required=False,
) )
from_webui = serializers.BooleanField(
label="Documents are from Paperless-ngx WebUI",
write_only=True,
required=False,
)
def validate_document(self, document): def validate_document(self, document):
document_data = document.file.read() document_data = document.file.read()
mime_type = magic.from_buffer(document_data, mime=True) mime_type = magic.from_buffer(document_data, mime=True)

View File

@ -360,7 +360,7 @@ def empty_trash(doc_ids=None):
) )
try: try:
deleted_document_ids = documents.values_list("id", flat=True) deleted_document_ids = list(documents.values_list("id", flat=True))
# Temporarily connect the cleanup handler # Temporarily connect the cleanup handler
models.signals.post_delete.connect(cleanup_document_deletion, sender=Document) models.signals.post_delete.connect(cleanup_document_deletion, sender=Document)
documents.delete() # this is effectively a hard delete documents.delete() # this is effectively a hard delete

View File

@ -38,6 +38,7 @@ from documents.models import SavedView
from documents.models import ShareLink from documents.models import ShareLink
from documents.models import StoragePath from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.models import WorkflowTrigger
from documents.tests.utils import DirectoriesMixin from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import DocumentConsumeDelayMixin from documents.tests.utils import DocumentConsumeDelayMixin
@ -1362,6 +1363,30 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
self.assertEqual(overrides.filename, "simple.pdf") self.assertEqual(overrides.filename, "simple.pdf")
self.assertEqual(overrides.custom_field_ids, [custom_field.id]) self.assertEqual(overrides.custom_field_ids, [custom_field.id])
def test_upload_with_webui_source(self):
"""
GIVEN: A document with a source file
WHEN: Upload the document with 'from_webui' flag
THEN: Consume is called with the source set as WebUI
"""
self.consume_file_mock.return_value = celery.result.AsyncResult(
id=str(uuid.uuid4()),
)
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "from_webui": True},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.consume_file_mock.assert_called_once()
input_doc, overrides = self.get_last_consume_delay_call_args()
self.assertEqual(input_doc.source, WorkflowTrigger.DocumentSourceChoices.WEB_UI)
def test_upload_invalid_pdf(self): def test_upload_invalid_pdf(self):
""" """
GIVEN: Invalid PDF named "*.pdf" that mime_type is in settings.CONSUMER_PDF_RECOVERABLE_MIME_TYPES GIVEN: Invalid PDF named "*.pdf" that mime_type is in settings.CONSUMER_PDF_RECOVERABLE_MIME_TYPES
@ -1815,6 +1840,19 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
) )
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# empty display fields treated as none
response = self.client.patch(
f"/api/saved_views/{v1.id}/",
{
"display_fields": [],
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
v1.refresh_from_db()
self.assertEqual(v1.display_fields, None)
def test_saved_view_display_customfields(self): def test_saved_view_display_customfields(self):
""" """
GIVEN: GIVEN:

View File

@ -1387,6 +1387,7 @@ class PostDocumentView(GenericAPIView):
created = serializer.validated_data.get("created") created = serializer.validated_data.get("created")
archive_serial_number = serializer.validated_data.get("archive_serial_number") archive_serial_number = serializer.validated_data.get("archive_serial_number")
custom_field_ids = serializer.validated_data.get("custom_fields") custom_field_ids = serializer.validated_data.get("custom_fields")
from_webui = serializer.validated_data.get("from_webui")
t = int(mktime(datetime.now().timetuple())) t = int(mktime(datetime.now().timetuple()))
@ -1401,7 +1402,7 @@ class PostDocumentView(GenericAPIView):
os.utime(temp_file_path, times=(t, t)) os.utime(temp_file_path, times=(t, t))
input_doc = ConsumableDocument( input_doc = ConsumableDocument(
source=DocumentSource.ApiUpload, source=DocumentSource.WebUI if from_webui else DocumentSource.ApiUpload,
original_file=temp_file_path, original_file=temp_file_path,
) )
input_doc_overrides = DocumentMetadataOverrides( input_doc_overrides = DocumentMetadataOverrides(

View File

@ -305,6 +305,11 @@ urlpatterns = [
], ],
), ),
), ),
re_path(
r"^confirm-email/(?P<key>[-:\w]+)/$",
allauth_account_views.ConfirmEmailView.as_view(),
name="account_confirm_email",
),
re_path( re_path(
r"^password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", r"^password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$",
allauth_account_views.password_reset_from_key, allauth_account_views.password_reset_from_key,