From cf59853f34d775b4b6fcf621a9eaeb078fdc69f5 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:27:13 -0800 Subject: [PATCH 1/8] Tweakhancement: use anchor element for management list quick filter buttons (#11692) --- .../correspondent-list.component.ts | 2 ++ .../custom-fields.component.html | 18 ++++++++++---- .../custom-fields.component.spec.ts | 24 +++++++++++++------ .../custom-fields/custom-fields.component.ts | 6 +++-- .../document-type-list.component.ts | 2 ++ .../management-list.component.html | 21 ++++++++++++---- .../management-list.component.spec.ts | 14 +++++++---- .../management-list.component.ts | 4 ++-- .../storage-path-list.component.ts | 2 ++ .../manage/tag-list/tag-list.component.ts | 2 ++ .../document-list-view.service.spec.ts | 21 ++++++++++++++++ .../services/document-list-view.service.ts | 14 ++++++++++- 12 files changed, 105 insertions(+), 25 deletions(-) diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts index 0131ac992..957371e08 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts @@ -1,6 +1,7 @@ import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common' import { Component, inject } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { RouterModule } from '@angular/router' import { NgbDropdownModule, NgbPaginationModule, @@ -29,6 +30,7 @@ import { ManagementListComponent } from '../management-list/management-list.comp TitleCasePipe, FormsModule, ReactiveFormsModule, + RouterModule, NgClass, NgTemplateOutlet, NgbDropdownModule, diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html index 185e9da35..0a6d80658 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html @@ -42,7 +42,13 @@ @if (field.document_count > 0) { - + Filter Documents ({{ field.document_count }}) } @@ -57,9 +63,13 @@ @if (field.document_count > 0) {
} diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts index e94470d64..b86d476f3 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts @@ -4,6 +4,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { provideHttpClientTesting } from '@angular/common/http/testing' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { By } from '@angular/platform-browser' +import { RouterTestingModule } from '@angular/router/testing' import { NgbModal, NgbModalModule, @@ -61,6 +62,7 @@ describe('CustomFieldsComponent', () => { NgbModalModule, NgbPopoverModule, NgxBootstrapIconsModule.pick(allIcons), + RouterTestingModule, CustomFieldsComponent, IfPermissionsDirective, PageHeaderComponent, @@ -108,7 +110,9 @@ describe('CustomFieldsComponent', () => { const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reload') - const createButton = fixture.debugElement.queryAll(By.css('button'))[1] + const createButton = fixture.debugElement + .queryAll(By.css('button')) + .find((btn) => btn.nativeElement.textContent.trim().includes('Add Field')) createButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() @@ -133,7 +137,11 @@ describe('CustomFieldsComponent', () => { const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const reloadSpy = jest.spyOn(component, 'reload') - const editButton = fixture.debugElement.queryAll(By.css('button'))[2] + const editButton = fixture.debugElement + .queryAll(By.css('button')) + .find((btn) => + btn.nativeElement.textContent.trim().includes(fields[0].name) + ) editButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() @@ -158,7 +166,9 @@ describe('CustomFieldsComponent', () => { const deleteSpy = jest.spyOn(customFieldsService, 'delete') const reloadSpy = jest.spyOn(component, 'reload') - const deleteButton = fixture.debugElement.queryAll(By.css('button'))[5] + const deleteButton = fixture.debugElement + .queryAll(By.css('button')) + .find((btn) => btn.nativeElement.textContent.trim().includes('Delete')) deleteButton.triggerEventHandler('click') expect(modal).not.toBeUndefined() @@ -176,10 +186,10 @@ describe('CustomFieldsComponent', () => { expect(reloadSpy).toHaveBeenCalled() }) - it('should support filter documents', () => { - const filterSpy = jest.spyOn(listViewService, 'quickFilter') - component.filterDocuments(fields[0]) - expect(filterSpy).toHaveBeenCalledWith([ + it('should provide document filter url', () => { + const urlSpy = jest.spyOn(listViewService, 'getQuickFilterUrl') + component.getDocumentFilterUrl(fields[0]) + expect(urlSpy).toHaveBeenCalledWith([ { rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: JSON.stringify([ diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts index 9e7ecf78a..8ecd713ef 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit, inject } from '@angular/core' +import { RouterModule } from '@angular/router' import { NgbDropdownModule, NgbModal, @@ -36,6 +37,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading NgbDropdownModule, NgbPaginationModule, NgxBootstrapIconsModule, + RouterModule, ], }) export class CustomFieldsComponent @@ -130,8 +132,8 @@ export class CustomFieldsComponent return DATA_TYPE_LABELS.find((l) => l.id === field.data_type).name } - filterDocuments(field: CustomField) { - this.documentListViewService.quickFilter([ + getDocumentFilterUrl(field: CustomField) { + return this.documentListViewService.getQuickFilterUrl([ { rule_type: FILTER_CUSTOM_FIELDS_QUERY, value: JSON.stringify([ diff --git a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts index 21a4779e9..b561af2d1 100644 --- a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts +++ b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts @@ -1,6 +1,7 @@ import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common' import { Component, inject } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { RouterModule } from '@angular/router' import { NgbDropdownModule, NgbPaginationModule, @@ -27,6 +28,7 @@ import { ManagementListComponent } from '../management-list/management-list.comp IfPermissionsDirective, FormsModule, ReactiveFormsModule, + RouterModule, NgClass, NgTemplateOutlet, NgbDropdownModule, diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.html b/src-ui/src/app/components/manage/management-list/management-list.component.html index 8fac6f44f..1cfb3aa0d 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.html +++ b/src-ui/src/app/components/manage/management-list/management-list.component.html @@ -120,7 +120,14 @@ @if (getDocumentCount(object) > 0) { - + Filter Documents ({{ getDocumentCount(object) }}) } @@ -135,9 +142,15 @@ @if (getDocumentCount(object) > 0) { } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts index 813c81148..a9f7a0626 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.spec.ts @@ -13,6 +13,7 @@ import { } from '@angular/core/testing' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { By } from '@angular/platform-browser' +import { RouterLinkWithHref } from '@angular/router' import { RouterTestingModule } from '@angular/router/testing' import { NgbModal, @@ -230,12 +231,15 @@ describe('ManagementListComponent', () => { }) it('should support quick filter for objects', () => { - const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') - const filterButton = fixture.debugElement.queryAll(By.css('button'))[9] - filterButton.triggerEventHandler('click') - expect(qfSpy).toHaveBeenCalledWith([ + const expectedUrl = documentListViewService.getQuickFilterUrl([ { rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() }, - ]) // subclasses set the filter rule type + ]) + const filterLink = fixture.debugElement.query( + By.css('a.btn-outline-secondary') + ) + expect(filterLink).toBeTruthy() + const routerLink = filterLink.injector.get(RouterLinkWithHref) + expect(routerLink.urlTree).toEqual(expectedUrl) }) it('should reload on sort', () => { diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index b1af1f1d1..e8e7a3bb3 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -230,8 +230,8 @@ export abstract class ManagementListComponentA#VCWXl50t0r7G({3ri} z5Ksf+(6f|uM zlz>VMozfNXTj;-DKb-#%P#M7Sw+t|d>$hzG_bK4_nVK3}hPQ`1dMZ4EZl2IT&jvSH z&xW9nAppi4O8Kb#pdfnW*CEj%h>Y(L8R<;`@cHx~yxTp0p4?+xJld@y>_5cWjyBE; zRojxW!dxwP`*nj&`C+=KzMK7wJ@?8nH1Gxu2f@4xLL~@}ZE& Mtn4iG_LC1j~#vV;p^V6MRt~AjvP#y%j1EWc96Oe>Y zI!&G5bYebw%Co3(=P z^oDtP3WF+hcj>pu2cyA_aW?fh1GoPye-mchbEiF1 p~05##4< zqOdrk&rv&`-F5m?kbRNl( 3I{-81)mq>n+|<9tpA aLj9Is%_mh}BEiL@ G8URg2^Vu*f1zhM!SL3?Q-x zj{sZ;u<9c000INg_~H1(SQD8x(Y?hZDI5pze?`I;0O;Y6Ln-rR>4~DlzZbyKBk{wQ zh|3hX(PIxqxD;%t3cJBFhDGO3se&|+QzdX!aW$ULh@GoGcY9_NqL&{tPV }U zkB`J&H|NC_Mz-wwb`0XhU=32~DqA=Ef?6F^xvuwx%pr()-QtSU52+2+vrBv3=nFYn zkYi`}F`^*yYGnU9(iKP$O(fKEJ?+@m3o`%#*v)h-bH#Co`+)A)wRnu)f^tM<0*4$d zwT4LzM;h|1Gt5NF3GfB81@T!JTS!Ers4Rs!CNd% 6POV_jY z*G^(zs9IhMBL+&oq{P7tel6WYfrTma()u;3BpxMxQY5`74#g-y9y9edQID?V^Fqvt z5Gx5c06(TSrvK~x*IBPAdJxTUPGC+DPY6J9UJ>aME#l0SD- alfONSQ~zB+klu0h0z zof yV)o!&H`22};_Ong&FQ*=`VktkZhVg6wOSs_`Gg=+mf?RQesST--t zk H! zuxCm4 xZ#)YMJeS*r{ci_prnNj3E$2+81xuUz<>?QVaYL zGY|Pm f!4c7H0C;u;5UlzZBmZ+AnmzW*MF3}r8ZbNS4Z^IXR zyS AP0VP-k}#VtK5>qMoN=XHI#jGftUe_5u$?gP zE`cthbtco~<48nIkR(=8@PPCt4Kg;(YZpn}LcEDYE9H+g{Fp+o1A1PX;edkErAKJD zu~jgKqdxVV_Go>_H3K>a@rt*oWK#=MwNbXwRAbFWW%Y Av_rKPr=q T18O-+-iCikt_Z_-+guRMNQ)`rSGsX7T>uBbS&*m@FWlJdv% z9~?grtRqpAK<4ZjQ6pm8bW;P9<}`J*--7I #w+=kpWOHG@M&wJ*mSLq#OR^or zAF$tKbrJDS6qFB;%%y0jZl|ev)BdJmpc#icT(m%Kp1uVKGa5%KsZea9Ed7-o!zd0= z9)>0xOGd81{M9dpGKSO?A9;?F&`Alx{8-gK1{HeO6saMEA^aiQEg)$Kx{3@mK)z9e zU65R=UN}|Ekzb!*U*=kJT7Xg-Q>aj=P$nR)EvYT7Ei)(U8Fk3GjMjwNgy5#`=IR#X z#^ko|rtj8#M(;x@L?wjK3e!r^O47>M%G!$5iq?wViti8bhx3Q^C-P^#m%CBB(Yukp zQ8?57fv_vH+yA5Nhw*rOiE+_t{-e|j4dzsFNWm{vsds7`G!=L=uWh7+B+v72)Vs@1 zra5iUKPBqPzc1ldTPzov&YEtXmYFV_)}E%>quaySBZr539t_<=#e&4L#d5`RI)gex zI)giNIuj^jDdH(oDIyCJ3sMT=3!(}_3)0r{y6L;&x?Lk)L|~xqqw1sH#u7*)Q=w1+ zi|C6`i;#<0ix`SnikONpz=&XGFfy1MOaW#9vw*R|2w?IJzy`qvxKFo_C$=(nl^~Eq zOwo&$5gQ>HCK)anHkme=A{jRsI~ia7r8 cEn($Yh-d{XoP>Hbi`w%dZcZ{bmU}2CL;lqXI5?QVB!F78xrjB zOY;u7<~yZ4Wjy8CCdGq8NeWL2kC953%9IM1O6!g7&F#(U&7%pSNumj*$ty`Od0P^- z#XijS*7Pj`^AvN9&RgBK4|esiS|m)VsiB&Qn$emWnvpXpGx0MyGkIJgTuEHX=Ww=2 zwk)xWj0+m6qjpS$Bt=xRCYPwQf8hwE+Yh-()=G}qnMMb^{R zS=Qy503jm7D_twS!X3gx!h^#75ApXLx3ssy2=Sr8p)nF+5)l${-9g>a-5K4HRIyaK zR5?_6MPWtpMd3v;;2?0y2B;q;o+chhvY7pnE;u !7g^>0oEEd?yeckD+av&VSx`Q~^7T@Ia18{ZjZ8&De58SEKE7;qa@8w40684MbH zGmtV!HP~t7|5EL|yzSF-p1sd9M^~-n&=I#|@qK$8Z$oQs_B;31=bej9^zZRo${VB5 z`(^cp!`9(;Td#kDZ>DdqZ}_dir7;;QMrvfTOqfi(OlW_4e|&!qT_RlsUFr_63P^=i z1x1Btj3xJ0a7wVoTgyA(_~(h4H=$Rfw{;70%R-Aq3wn!D3q{LG3t`KPmaP`FmV=f# zFBva7uN|*6uQ~6)Q;KVPzn^~Me(ZjGe$sxQ{SN(t{Xl*)*T1giulKGOu6(W|uE|cm z{%YO0?YfL1A5>Yl+`<2zwm!GUv)!<9xrwozu%WU8Tm$Ynt{tr%?X-74ra#g=B0M OlK(lsF~2B3!6eM2&ZK&cuv4#7tCOsgqBAn2K13m8KSV&>ilX9W!%HRESF+jz zY6DUO#`N;^n)E8~G lx|@PceNBPus_T;L zB ESfij;noZzwA$r71lqb15k(ktn}WmPy)3>Ph-Y zMo5uGwY>BO{!%j6#sLm7GXfY{Dp+kO_W(XRoCch_XX#e8ySft!g>#Ze(aKS6c!t=^ zg`J$paiO&G>iIt=nXRiois~`5glfrF7IIBBSxbDB`N+56yTG%MOCT1jsi>-`rKm8X zI3hQq!mP)v^fOE+T&GAUZJwppxmLFpQtN2u``ybK&soe_*O|rH)!EWH@Py#R?L_>9 z@r3ro;e_Hu<~y)Q 5cL2S305IiG?ow6Ec9!w6AKCJ8&(<3sJy|O-86#K^tU=G zSCq9>QAy@0o5?9}rxQ6--BQkyW8Vrg>H>6WzA<}i{`$OGyva1kHORtHjMIwKkP? n%tyZa~KZ$7VW9?v#WX)v_{vk0bJo#!;rUa$9 zvRJ>^w^*jQtk_r~@XZ5D=v&j2^|Yk76DgoH=Om<*sN}&nd@Qb7rH0#0BSIOL>UF6AnQQD+vTM0APVbV0F 7^Jsm(q(953TGsJHKbjkYNNHM z*;L6{bf#r3VQrjIHB!P{P*rVL@w1BLU3PwUr9p+CSfl>+=U*wm3V${Isz!blE+M5T zrP(XlD@P+rV?-lUBDvKfpEhKl%8)9R>Yi$-S;!b!@uecOqP(KsCf}xT4>ZF*BRxYl zBTz9_;i%bO{){V{C?PW?voy0Lvsv$*Ui1ReC!0?(HupA{GZr&i)1Rj&_7wNT_mKDC z_b&F@_CR~OdzRD0(*iS*GiB5GGwl^e)`?tSxTv`}xtO_@xVkvixR5z*I9aWi#tG)U z%1>TxG|(z1D Nt#q2ZU9}y({TWX>MoK|aL3dbZSdK}R z>DB%ht#GZFS+4iKdi8otdW#DL_IUP~_7HopJ>7xm0ri34f&78iLBPT70`TM9$H4tH zw|Tc7w`sQ#Hv@NZcQrR;Hyif_cZWU2-Im$w!um3W*{#{s*_qi-vqDwGC0XUZ(w8j` z4Tq%5gv*fSJRf=?S|L**8X>R{T`QoKy4A1Mrj@((S!<-vsgLoQ$NAum;LYR>|IO!{ zl^f8_x0{lixSQ4+@J;;<(@i_76Z}v3@9=x@c<^NKnD9T~%g~5Wo6w|DEl}U0aidK{ zFh>+dR76-rI7FC6SVu@joJZtDG@zBCrJ)8A6~yYqO2%o#jl?d+cEwJ{Dih{#?6DJp zGC_LW3+#Oa^+Z##GjS}jLa|h_9I;~rc|3<8GOhrJ7Mr_8IL9t)O6x* %CwAD$DQClw- gLxi`NzrB@&(@2!;rw}P{Rl7g**Bh-`z!^p!iZ+TegSXOlc z8pQQ#btbB&-!s2&sWPi_sTz9Es)wedq{E`;-yp5Wp+8kM^ZtDmhJL@!j-IwogYN3b z+>Zga_ucUJ-8NJXMb=>O1{mAXb*XfrwA*69V!`5@#Uh^Nur-r?rR}~6u~n9xzLmb? zBE)yjW7gyAMGa|h{Hk*2==|sm>k?~s^?dbU_3B~4a>z1|Z?RCl&{w1Gm4%gl;T7Sr zw!VkDd;8n63#JPJbZP`61aAZ?1pG+0NViC{NZQDN$VhY)^i_-k^jb13kz)6Oi^Ru* zVu=@#qhx#hoIX~E<7E=Q@ftD|GPp9XGQfU 2Me@FElDN);ildbzgm(eQSv^5IPvTUGT$n zfBne0r)XfKMJBIbB0(yFJXc3WS%pyLxeET6(-`xZ%NX|<#Telj?pV8#n~}It1@smv zY!qem!f2<(r-jU`_2MRgz(0NC! Nu9ynYUfD<# SMDt zleMXiPaXGwK0qztAYVd^LX1#sODrwK0YM1kh6EL%9!>#%A3*>?25tvV`-L$Q8Cn2t z3uXxVI%*OM2 x7-4Fw#AR}D` ze+AQ?(X{yVn3XtZ;79q7?rx%PHg0lm`e#mOc4t1Q>=9JbtbNpdOtci+=8E8vxO6&D zCFr(xq!z38rnb6vqSm|iyw<-KyLPx%!1>I%(0Rc*-g(`b&zZ}4*%{;eN{@Yi@<#Xu z*9Pf#iH-X2mEQ$6j(UMTjx>o_aM;t>Ke5EI*s&S0tErb1bY$lhscDgEE9e$zBB(8B zwP-zPR}>6nm*gpFkZ7!_`KZHbAvBtF?leNOKFMcH8E=^1RHgn*CrQfAGDurWzN7bM zxE{ldpfZy=i~m9vCHy9^cwP1}shp~k0T50j1ub~!Wul=dp)cWsux)t{`7ko3GHE`2 z(TQ-ed}97Y<<99Yd~QhuQeso0`bxpV%tH5``#qgK<$?dg;j7Z3H!^4mJ|uA>KEJG{ zCOV785~|CJrW&VGr`}IJpPHItniQBiDr=W}%n{2I8y^4a^!4>1Z;~?w7_ed8x1(rI zt6geVs%ar=;bS3XVYz)j(vZ`rptcTW0Pg zr@hgB>&bUyw35}5;}_u< +-XdAfcn3pd=;UWGQ)*lsyU8Zax2y!ud z=&^WKBKlFZrsF-J4f=%r$)tEqv(tj2QT3y0&G>t^wqp%b+jZG>zx8doA-M&)*5pGv zY~@^G?~~1( 14qDQ0u%KV<;fGLOZUL%5W zgGC)Epn<39QCL-O_vI(#n+0EqLDkZSrOG>5Z(VQ8Yp?5DWYx}B1v;kcn0n4tg#~8$ z-@SVq fnAZ8F}h|JdgP{~j$<7dWj#{G)Kilquy&F7k9m2H~Xl~NVw6+)HC zl?s(L8VTC%HP5)RxrVqrxYoJ6x#GAaxPZ3Ywm)oXY~pPFZ4+j-W-exxX96m^r%&dr zXB($`XL72WHF&fxs~s+x7j?tyxh*Be$V7Y}ey)8>v*g+Tu}`z7wCBG+wQsw3yH~b9 zH?}^Clrtr?6Ttez;78-AE5_J+P5_Y?(KHbWu^CY`(E?E)(F*s^TuXr(uayhGtLwW{ zr5(q-K>vr!7o{k5_{=~! VXD^`9hSFKS?)U)PG zn~Nzc8dKA&XCJMLGUgR+x$Q0-gcl5!m*xbg?mmT&Q!bc|yL_Q}Us6$018Lf_Wh&78 zIFmoq%=P|L)2ABu((`?(?@|j=-7&)W2}V_o^MNNndL8XIxof#MxZ6L+es20qY+r9L z!a8C2rp4TY;aKgW#5K*q(8t@lIj7dJbh%Eb_} 2YzS+R ^Cq63XSTGn U6kaFtL*a}ULK7dI_{G11uj4C@GrWj?#It< zRO1^TwFrCP52JOVEud`>KjU$6$ZiMuflvAlVfup>f=)%dM89>+1k*hy<{9Lnam-vr zZOrhjzuw;cTB3i 2- jl&X3R68`lpO6&r9J7FXjN{kJ`K4*DDBWMrwZsJ5vn zsIIBrQ(a06FNrN s9s!7X`b<{S }vR-n)7RlwRk z_%iD#VI}Yp=3MkRxbt~C!Z!?7!7rz4%e;c4BGm$iLb-e^Y{G&M+Z0{{x2ih@bKPf= z=NOS>tinryJ*Tp_NjpVF38TV9ejksm97BA?ZM$5*@AiC)>Wm7HYL&~CJCLgxJR7_j zEEwz^yzg&Ga3>=V$UeDR+E`3jRTfkCQ} n$`iGQX8r$Vxr*_MFRhsig1@Et8 zPcpu9^jk)aG3K6&_@3D9@ZGwe?;MUz<>vVdpZPiCjr{t$_w{CUdxZOI{MYIc(;X{U z3)ceIa92{-WLIP3@zz9t(@V+=sUI;b_+!R*K7$u(2PKQc(`lB*=&gRI$UeP~Lu;vz zmEamZV I_-sY=Yv|Hjho?OStRnC^i)9rixGyRDM3&-x2TtDBfgX9L0 z6;P{sz|OD3f#wCz(4!URPK6K!X$3I_y0;5yv}xRL%b^eY@xyY%Gs8S-_$v}eFII@# zF#Pf!A0}_3E{VzCk>d#S2FO0Bp03YE_TqX;IrUo6c$JuxfFU+pw~QMVzbb}phW5VA ztk0BiO=s?Ae$%^JDF1Z$smykf{F|_i-`QRIb?wQ *7SyeOc6KW=1-pUIq0gGrJd2LbtBmbve)?DF{`hwezl6?wCoZdx z#@ZflI!`2SBgYb!$ngUOZ)#7H*UKhV=JKpXhyzyc8=K_jQ&v7MD$Z9ew0yd@Kxw3@ zgS6o`YPQTYoSWHtW;rgJ2v{{cHM{y+_}=381K%49x5wYx+;Uy-FFKa4R $QM`X1lk`0gJq8cws? z&$;keH27Wi9d?nmK*OAE_>J04bL~9s7|>g%(q;E$%;oKc@xjsK?NN^y#e=s&`il;V z7cenMFclTgUZTSUeSZjqVZ#?${htbHo@y(eYC?E9dH(@I2R&7T{Iya<#of)x-A(Ry z-3GMCLq 1_J(Z0$R~wZv9k?C1(yT?69`5hPcqXLTf$%a#9l9AU )^?j}S@`6pv1a|_caiT}de)Y{tqH}|J=KnGW#wfS$6pbY<@=7(0>xCwIz0{?ml zo$v4G!Hy0VjxP454q%9|_}|I?+WRCO@JXD%i1+_O I4;s?Qh~h>G&m} zuaAJ%7HT*;+6imBSVLbhAqf1h{l69Y_fCi;^!*dU>}>38EFcgIh*tx|#m~;h&&kWg z2I6OfuH(N@{x2)}L)lP&g8Cou?+*B+@&7Zn|19MHZ+85TMgC)vf0G9O<68eF#s14| z|6`GVS#|%Wf&a0{ze%zGGTZ-H n-u#m zv;9BEBL9e=LGg@2l%7w~vHvMz_!pq23KTQv4Gmx+16A!z!4P|B@DC_%=Loj_E9&fT z5yrov&i(+8N!dZ{p|DCQl+4t@+| S$s1FAXNL6l5?1K<8!22d3WB>i*pzc-$=0aVccy}W;(^f#kFCslU0chyH`1Ly<( zCY|A50;B&mBKw5hb4C7dfIVd3-w}Jz$m-uAd(c^d3fAVX`hRHl=}^Ma{Rueumnqo) zE2vTe8h5pGw1lo4I@ccvL1dtc6U0Fb{1oif|7|4zP)An};NWg&_s2grDDQt(-QO*s zXzE}|4{>18RA+$7Y3gDt2MtU@-QuYUG5-@D2%X_C!Dd(e-?3;lL+HBx2m}K_zj3#J zmHRJn$v;DSg3?mb(rj#8JZ#V^CpI>2KIonkx`)CtdDx-G_*)?IPdZMCKWMn2w7==0 zbez!f+}wYDpyRnYpdV;Il!T1~%9mXVx@Uv(`aLC79Z#V_dP!(Fkev+#0`Y>l*x7!^ z1?kw>=otPYu9pkM0%~S<4rC~&KQ{n37Z(Q?zyk1R9}gD?)ZkAWz~TQY+=_7
iIzwS=we$U&?!S+P!s`D6h(o=UGn;zf@Ux^bR~{QiN51tfzOz8%?u_+(c;KQ zhSD}T@ LJ)eXGF>TK0gtN`{OwrawtjqNr zu=Ty=!&Y~Iu{Jf(7qWchg&`<|%RL{t1SLLk>*XB}8ke?Pd;Gj%M2wgYKkM)?7506b z)Cc-o(|s#&`Rh0lg~C4S#JPxi2#hpVu65C6obhw`Ur2K@=FM_GeSBSizlWDuDCw-V kPGfa=d?`gW`dlS1xI->&7F}1o7(8T1_&&7T@9RN#cXxA{fdBvi literal 0 HcmV?d00001 diff --git a/src/paperless_remote/tests/test_checks.py b/src/paperless_remote/tests/test_checks.py new file mode 100644 index 000000000..8a257952e --- /dev/null +++ b/src/paperless_remote/tests/test_checks.py @@ -0,0 +1,24 @@ +from unittest import TestCase + +from django.test import override_settings + +from paperless_remote import check_remote_parser_configured + + +class TestChecks(TestCase): + @override_settings(REMOTE_OCR_ENGINE=None) + def test_no_engine(self): + msgs = check_remote_parser_configured(None) + self.assertEqual(len(msgs), 0) + + @override_settings(REMOTE_OCR_ENGINE="azureai") + @override_settings(REMOTE_OCR_API_KEY="somekey") + @override_settings(REMOTE_OCR_ENDPOINT=None) + def test_azure_no_endpoint(self): + msgs = check_remote_parser_configured(None) + self.assertEqual(len(msgs), 1) + self.assertTrue( + msgs[0].msg.startswith( + "Azure AI remote parser requires endpoint and API key to be configured.", + ), + ) diff --git a/src/paperless_remote/tests/test_parser.py b/src/paperless_remote/tests/test_parser.py new file mode 100644 index 000000000..793778ec3 --- /dev/null +++ b/src/paperless_remote/tests/test_parser.py @@ -0,0 +1,128 @@ +import uuid +from pathlib import Path +from unittest import mock + +from django.test import TestCase +from django.test import override_settings + +from documents.tests.utils import DirectoriesMixin +from documents.tests.utils import FileSystemAssertsMixin +from paperless_remote.parsers import RemoteDocumentParser +from paperless_remote.signals import get_parser + + +class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase): + SAMPLE_FILES = Path(__file__).resolve().parent / "samples" + + def assertContainsStrings(self, content: str, strings: list[str]): + # Asserts that all strings appear in content, in the given order. + indices = [] + for s in strings: + if s in content: + indices.append(content.index(s)) + else: + self.fail(f"'{s}' is not in '{content}'") + self.assertListEqual(indices, sorted(indices)) + + @mock.patch("paperless_tesseract.parsers.run_subprocess") + @mock.patch("azure.ai.documentintelligence.DocumentIntelligenceClient") + def test_get_text_with_azure(self, mock_client_cls, mock_subprocess): + # Arrange mock Azure client + mock_client = mock.Mock() + mock_client_cls.return_value = mock_client + + # Simulate poller result and its `.details` + mock_poller = mock.Mock() + mock_poller.wait.return_value = None + mock_poller.details = {"operation_id": "fake-op-id"} + mock_client.begin_analyze_document.return_value = mock_poller + mock_poller.result.return_value.content = "This is a test document." + + # Return dummy PDF bytes + mock_client.get_analyze_result_pdf.return_value = [ + b"%PDF-", + b"1.7 ", + b"FAKEPDF", + ] + + # Simulate pdftotext by writing dummy text to sidecar file + def fake_run(cmd, *args, **kwargs): + with Path(cmd[-1]).open("w", encoding="utf-8") as f: + f.write("This is a test document.") + + mock_subprocess.side_effect = fake_run + + with override_settings( + REMOTE_OCR_ENGINE="azureai", + REMOTE_OCR_API_KEY="somekey", + REMOTE_OCR_ENDPOINT="https://endpoint.cognitiveservices.azure.com", + ): + parser = get_parser(uuid.uuid4()) + parser.parse( + self.SAMPLE_FILES / "simple-digital.pdf", + "application/pdf", + ) + + self.assertContainsStrings( + parser.text.strip(), + ["This is a test document."], + ) + + @mock.patch("azure.ai.documentintelligence.DocumentIntelligenceClient") + def test_get_text_with_azure_error_logged_and_returns_none(self, mock_client_cls): + mock_client = mock.Mock() + mock_client.begin_analyze_document.side_effect = RuntimeError("fail") + mock_client_cls.return_value = mock_client + + with override_settings( + REMOTE_OCR_ENGINE="azureai", + REMOTE_OCR_API_KEY="somekey", + REMOTE_OCR_ENDPOINT="https://endpoint.cognitiveservices.azure.com", + ): + parser = get_parser(uuid.uuid4()) + with mock.patch.object(parser.log, "error") as mock_log_error: + parser.parse( + self.SAMPLE_FILES / "simple-digital.pdf", + "application/pdf", + ) + + self.assertIsNone(parser.text) + mock_client.begin_analyze_document.assert_called_once() + mock_client.close.assert_called_once() + mock_log_error.assert_called_once() + self.assertIn( + "Azure AI Vision parsing failed", + mock_log_error.call_args[0][0], + ) + + @override_settings( + REMOTE_OCR_ENGINE="azureai", + REMOTE_OCR_API_KEY="key", + REMOTE_OCR_ENDPOINT="https://endpoint.cognitiveservices.azure.com", + ) + def test_supported_mime_types_valid_config(self): + parser = RemoteDocumentParser(uuid.uuid4()) + expected_types = { + "application/pdf": ".pdf", + "image/png": ".png", + "image/jpeg": ".jpg", + "image/tiff": ".tiff", + "image/bmp": ".bmp", + "image/gif": ".gif", + "image/webp": ".webp", + } + self.assertEqual(parser.supported_mime_types(), expected_types) + + def test_supported_mime_types_invalid_config(self): + parser = get_parser(uuid.uuid4()) + self.assertEqual(parser.supported_mime_types(), {}) + + @override_settings( + REMOTE_OCR_ENGINE=None, + REMOTE_OCR_API_KEY=None, + REMOTE_OCR_ENDPOINT=None, + ) + def test_parse_with_invalid_config(self): + parser = get_parser(uuid.uuid4()) + parser.parse(self.SAMPLE_FILES / "simple-digital.pdf", "application/pdf") + self.assertEqual(parser.text, "") diff --git a/uv.lock b/uv.lock index d1cf11ee2..c621b203d 100644 --- a/uv.lock +++ b/uv.lock @@ -95,6 +95,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, ] +[[package]] +name = "azure-ai-documentintelligence" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005 }, +] + +[[package]] +name = "azure-core" +version = "1.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/aa/7c9db8edd626f1a7d99d09ef7926f6f4fb34d5f9fa00dc394afdfe8e2a80/azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9", size = 295633 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/b7/76b7e144aa53bd206bf1ce34fa75350472c3f69bf30e5c8c18bc9881035d/azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f", size = 207071 }, +] + [[package]] name = "babel" version = "2.17.0" @@ -1451,6 +1479,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/fc/4e5a141c3f7c7bed550ac1f69e599e92b6be449dd4677ec09f325cad0955/inotifyrecursive-0.3.5-py3-none-any.whl", hash = "sha256:7e5f4a2e1dc2bef0efa3b5f6b339c41fb4599055a2b54909d020e9e932cc8d2f", size = 8009, upload-time = "2020-11-20T12:38:46.981Z" }, ] +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -2118,6 +2155,7 @@ name = "paperless-ngx" version = "2.20.3" source = { virtual = "." } dependencies = [ + { name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "celery", extra = ["redis"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2255,6 +2293,7 @@ typing = [ [package.metadata] requires-dist = [ + { name = "azure-ai-documentintelligence", specifier = ">=1.0.2" }, { name = "babel", specifier = ">=2.17" }, { name = "bleach", specifier = "~=6.3.0" }, { name = "celery", extras = ["redis"], specifier = "~=5.5.1" }, From ba4d88c801e75b2cd754aa5213ad3180e92c087b Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:51:48 +0000 Subject: [PATCH 8/8] Auto translate strings --- src/locale/en_US/LC_MESSAGES/django.po | 76 +++++++++++++------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 6f64a0b88..850c20ed5 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-08 21:37+0000\n" +"POT-Creation-Date: 2026-01-08 21:50+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -1702,151 +1702,151 @@ msgstr "" msgid "paperless application settings" msgstr "" -#: paperless/settings.py:767 +#: paperless/settings.py:768 msgid "English (US)" msgstr "" -#: paperless/settings.py:768 +#: paperless/settings.py:769 msgid "Arabic" msgstr "" -#: paperless/settings.py:769 +#: paperless/settings.py:770 msgid "Afrikaans" msgstr "" -#: paperless/settings.py:770 +#: paperless/settings.py:771 msgid "Belarusian" msgstr "" -#: paperless/settings.py:771 +#: paperless/settings.py:772 msgid "Bulgarian" msgstr "" -#: paperless/settings.py:772 +#: paperless/settings.py:773 msgid "Catalan" msgstr "" -#: paperless/settings.py:773 +#: paperless/settings.py:774 msgid "Czech" msgstr "" -#: paperless/settings.py:774 +#: paperless/settings.py:775 msgid "Danish" msgstr "" -#: paperless/settings.py:775 +#: paperless/settings.py:776 msgid "German" msgstr "" -#: paperless/settings.py:776 +#: paperless/settings.py:777 msgid "Greek" msgstr "" -#: paperless/settings.py:777 +#: paperless/settings.py:778 msgid "English (GB)" msgstr "" -#: paperless/settings.py:778 +#: paperless/settings.py:779 msgid "Spanish" msgstr "" -#: paperless/settings.py:779 +#: paperless/settings.py:780 msgid "Persian" msgstr "" -#: paperless/settings.py:780 +#: paperless/settings.py:781 msgid "Finnish" msgstr "" -#: paperless/settings.py:781 +#: paperless/settings.py:782 msgid "French" msgstr "" -#: paperless/settings.py:782 +#: paperless/settings.py:783 msgid "Hungarian" msgstr "" -#: paperless/settings.py:783 +#: paperless/settings.py:784 msgid "Indonesian" msgstr "" -#: paperless/settings.py:784 +#: paperless/settings.py:785 msgid "Italian" msgstr "" -#: paperless/settings.py:785 +#: paperless/settings.py:786 msgid "Japanese" msgstr "" -#: paperless/settings.py:786 +#: paperless/settings.py:787 msgid "Korean" msgstr "" -#: paperless/settings.py:787 +#: paperless/settings.py:788 msgid "Luxembourgish" msgstr "" -#: paperless/settings.py:788 +#: paperless/settings.py:789 msgid "Norwegian" msgstr "" -#: paperless/settings.py:789 +#: paperless/settings.py:790 msgid "Dutch" msgstr "" -#: paperless/settings.py:790 +#: paperless/settings.py:791 msgid "Polish" msgstr "" -#: paperless/settings.py:791 +#: paperless/settings.py:792 msgid "Portuguese (Brazil)" msgstr "" -#: paperless/settings.py:792 +#: paperless/settings.py:793 msgid "Portuguese" msgstr "" -#: paperless/settings.py:793 +#: paperless/settings.py:794 msgid "Romanian" msgstr "" -#: paperless/settings.py:794 +#: paperless/settings.py:795 msgid "Russian" msgstr "" -#: paperless/settings.py:795 +#: paperless/settings.py:796 msgid "Slovak" msgstr "" -#: paperless/settings.py:796 +#: paperless/settings.py:797 msgid "Slovenian" msgstr "" -#: paperless/settings.py:797 +#: paperless/settings.py:798 msgid "Serbian" msgstr "" -#: paperless/settings.py:798 +#: paperless/settings.py:799 msgid "Swedish" msgstr "" -#: paperless/settings.py:799 +#: paperless/settings.py:800 msgid "Turkish" msgstr "" -#: paperless/settings.py:800 +#: paperless/settings.py:801 msgid "Ukrainian" msgstr "" -#: paperless/settings.py:801 +#: paperless/settings.py:802 msgid "Vietnamese" msgstr "" -#: paperless/settings.py:802 +#: paperless/settings.py:803 msgid "Chinese Simplified" msgstr "" -#: paperless/settings.py:803 +#: paperless/settings.py:804 msgid "Chinese Traditional" msgstr ""