mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-14 07:01:17 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9016a1e6df | ||
|
|
627254d5a7 | ||
|
|
ff31558252 | ||
|
|
9454978264 | ||
|
|
e2d25a7a09 | ||
|
|
85f824f032 | ||
|
|
1a48910e6b | ||
|
|
bffd5829d0 | ||
|
|
7e12bd1bef | ||
|
|
af0817ab74 | ||
|
|
fbf1a051a2 | ||
|
|
7ecf7f704a | ||
|
|
7b7a74d821 | ||
|
|
e4acc33519 | ||
|
|
2fd141d914 |
55
Pipfile.lock
generated
55
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "b395058a24154f74cb1f2d685d51de3f1028ecb48389fac9971209e258a15543"
|
||||
"sha256": "d7ef8db734997cda7c11971f2ddb66bf1918f4232b0956a9bf604c41763ce461"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -566,12 +566,12 @@
|
||||
},
|
||||
"gotenberg-client": {
|
||||
"hashes": [
|
||||
"sha256:3026726d1a47f41e9d43f18c95e530ff64f506e2ec436f116a088da27c7430da",
|
||||
"sha256:c2555f7401faa48213a7cbe29c5e4a68316a003a6953753bc58d1e2b19873771"
|
||||
"sha256:69e9dd5264b75ed0ba1f9eebebdc750b13d190710fd82ca0670d161c249155c9",
|
||||
"sha256:dd0f49d3d4e01399949f39ac5024a5512566c8ded6ee457a336a5f77ce4c1a25"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.4.0"
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@@ -2707,7 +2707,6 @@
|
||||
"sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
|
||||
"sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.13.1"
|
||||
},
|
||||
@@ -2754,11 +2753,11 @@
|
||||
},
|
||||
"identify": {
|
||||
"hashes": [
|
||||
"sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75",
|
||||
"sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"
|
||||
"sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d",
|
||||
"sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.5.31"
|
||||
"version": "==2.5.33"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
@@ -2899,20 +2898,20 @@
|
||||
},
|
||||
"mkdocs-glightbox": {
|
||||
"hashes": [
|
||||
"sha256:8f894435b4f75231164e5d9fb023c01e922e6769e74a121e822c4914f310a41d",
|
||||
"sha256:96aaf98216f83c0d0fad2e42a8d805cfa6329d6ab25b54265012ccb2154010d8"
|
||||
"sha256:096c2753cf4f46f548b02070a2ff5dd8b823a431ce17873a62dcef304cf3364c",
|
||||
"sha256:f572256cca17c912da50a045129026566a79b8c6477e1170258ccc0ac5b162da"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3.4"
|
||||
"version": "==0.3.5"
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:a511d3ff48fa8718b033e7e37d17abd9cc1de0fdf0244a625ca2ae2387e2416d",
|
||||
"sha256:dbc78a4fea97b74319a6aa9a2f0be575a6028be6958f813ba367188f7b8428f6"
|
||||
"sha256:6ed0fbf4682491766f0ec1acc955db6901c2fd424c7ab343964ef51b819741f5",
|
||||
"sha256:ca8b9cd2b3be53e858e5a1a45ac9668bd78d95d77a30288bb5ebc1a31db6184c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==9.4.14"
|
||||
"version": "==9.5.2"
|
||||
},
|
||||
"mkdocs-material-extensions": {
|
||||
"hashes": [
|
||||
@@ -2996,11 +2995,11 @@
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20",
|
||||
"sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"
|
||||
"sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
|
||||
"sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.11.2"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.12.1"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
@@ -3080,12 +3079,12 @@
|
||||
},
|
||||
"pre-commit": {
|
||||
"hashes": [
|
||||
"sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32",
|
||||
"sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"
|
||||
"sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376",
|
||||
"sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.5.0"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==3.6.0"
|
||||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
@@ -3473,11 +3472,11 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
|
||||
"sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
|
||||
"sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2",
|
||||
"sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==68.2.2"
|
||||
"version": "==69.0.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -3548,11 +3547,11 @@
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af",
|
||||
"sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"
|
||||
"sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3",
|
||||
"sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==20.24.6"
|
||||
"version": "==20.25.0"
|
||||
},
|
||||
"watchdog": {
|
||||
"hashes": [
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.1.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: disable toggle for share link creation without archive version, fix auto-copy in Safari [@shamoon](https://github.com/shamoon) ([#4885](https://github.com/paperless-ngx/paperless-ngx/pull/4885))
|
||||
- Fix: storage paths link incorrect in dashboard widget [@shamoon](https://github.com/shamoon) ([#4878](https://github.com/paperless-ngx/paperless-ngx/pull/4878))
|
||||
- Fix: respect baseURI for pdfjs worker URL [@shamoon](https://github.com/shamoon) ([#4865](https://github.com/paperless-ngx/paperless-ngx/pull/4865))
|
||||
- Fix: Allow users to configure the From email for password reset [@stumpylog](https://github.com/stumpylog) ([#4867](https://github.com/paperless-ngx/paperless-ngx/pull/4867))
|
||||
- Fix: dont show move icon for file tasks badge [@shamoon](https://github.com/shamoon) ([#4860](https://github.com/paperless-ngx/paperless-ngx/pull/4860))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Simplifies how the documentation site is deployed [@stumpylog](https://github.com/stumpylog) ([#4858](https://github.com/paperless-ngx/paperless-ngx/pull/4858))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Fix: disable toggle for share link creation without archive version, fix auto-copy in Safari [@shamoon](https://github.com/shamoon) ([#4885](https://github.com/paperless-ngx/paperless-ngx/pull/4885))
|
||||
- Fix: storage paths link incorrect in dashboard widget [@shamoon](https://github.com/shamoon) ([#4878](https://github.com/paperless-ngx/paperless-ngx/pull/4878))
|
||||
- Fix: respect baseURI for pdfjs worker URL [@shamoon](https://github.com/shamoon) ([#4865](https://github.com/paperless-ngx/paperless-ngx/pull/4865))
|
||||
- Fix: Allow users to configure the From email for password reset [@stumpylog](https://github.com/stumpylog) ([#4867](https://github.com/paperless-ngx/paperless-ngx/pull/4867))
|
||||
- Fix: dont show move icon for file tasks badge [@shamoon](https://github.com/shamoon) ([#4860](https://github.com/paperless-ngx/paperless-ngx/pull/4860))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.1.0
|
||||
|
||||
### Features
|
||||
|
||||
@@ -380,7 +380,7 @@ fi
|
||||
docker compose pull
|
||||
|
||||
if [ "$DATABASE_BACKEND" == "postgres" ] || [ "$DATABASE_BACKEND" == "mariadb" ] ; then
|
||||
echo "Starting DB first for initilzation"
|
||||
echo "Starting DB first for initialization"
|
||||
docker compose up --detach db
|
||||
# hopefully enough time for even the slower systems
|
||||
sleep 15
|
||||
|
||||
@@ -19,7 +19,7 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { Observable, of, tap, throwError } from 'rxjs'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
@@ -298,6 +298,21 @@ describe('AppFrameComponent', () => {
|
||||
expect(autocompleteSpy).toHaveBeenCalled()
|
||||
}))
|
||||
|
||||
it('should handle autocomplete backend failure gracefully', fakeAsync(() => {
|
||||
const serviceAutocompleteSpy = jest.spyOn(searchService, 'autocomplete')
|
||||
serviceAutocompleteSpy.mockReturnValue(
|
||||
throwError(() => new Error('autcomplete failed'))
|
||||
)
|
||||
// serviceAutocompleteSpy.mockReturnValue(of([' world']))
|
||||
let result
|
||||
component.searchAutoComplete(of('hello')).subscribe((res) => {
|
||||
result = res
|
||||
})
|
||||
tick(250)
|
||||
expect(serviceAutocompleteSpy).toHaveBeenCalled()
|
||||
expect(result).toEqual([])
|
||||
}))
|
||||
|
||||
it('should support reset search field', () => {
|
||||
const resetSpy = jest.spyOn(component, 'resetSearchField')
|
||||
const input = (fixture.nativeElement as HTMLDivElement).querySelector(
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
map,
|
||||
switchMap,
|
||||
first,
|
||||
catchError,
|
||||
} from 'rxjs/operators'
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
@@ -166,7 +167,13 @@ export class AppFrameComponent
|
||||
}
|
||||
}),
|
||||
switchMap((term) =>
|
||||
term.length < 2 ? from([[]]) : this.searchService.autocomplete(term)
|
||||
term.length < 2
|
||||
? from([[]])
|
||||
: this.searchService.autocomplete(term).pipe(
|
||||
catchError(() => {
|
||||
return from([[]])
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -258,7 +258,6 @@
|
||||
[(page)]="previewCurrentPage"
|
||||
[zoom-scale]="previewZoomScale"
|
||||
[zoom]="previewZoomSetting"
|
||||
[render-text-mode]="2"
|
||||
(error)="onError($event)"
|
||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
||||
</pngx-pdf-viewer>
|
||||
|
||||
@@ -5,7 +5,7 @@ export const environment = {
|
||||
apiBaseUrl: document.baseURI + 'api/',
|
||||
apiVersion: '3',
|
||||
appTitle: 'Paperless-ngx',
|
||||
version: '2.1.1',
|
||||
version: '2.1.2',
|
||||
webSocketHost: window.location.host,
|
||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||
|
||||
@@ -2811,7 +2811,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">38</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Assign custom fields</target>
|
||||
<target state="translated">Assigna camnps personalitzats</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5057200219587080996" datatype="html">
|
||||
<source>Assign owner</source>
|
||||
@@ -3896,7 +3896,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.ts</context>
|
||||
<context context-type="linenumber">44</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">No documents found</target>
|
||||
<target state="translated">Documents no trobats</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5554528553553249088" datatype="html">
|
||||
<source>Show password</source>
|
||||
@@ -4700,7 +4700,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||
<context context-type="linenumber">9</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">-</target>
|
||||
<target state="translated">-</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8479257185772414452" datatype="html">
|
||||
<source>+</source>
|
||||
@@ -4708,7 +4708,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">+</target>
|
||||
<target state="translated">+</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8659635229098859487" datatype="html">
|
||||
<source>Download original</source>
|
||||
@@ -5132,7 +5132,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
<context context-type="linenumber">781</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Page Fit</target>
|
||||
<target state="translated">Encaix Pàgina</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6857598786757174736" datatype="html">
|
||||
<source>Select:</source>
|
||||
@@ -6870,7 +6870,7 @@
|
||||
<context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Document Link</target>
|
||||
<target state="translated">Enllaç Document</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5948496158474272829" datatype="html">
|
||||
<source>Warning: You have unsaved changes to your document(s).</source>
|
||||
|
||||
@@ -3896,7 +3896,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.ts</context>
|
||||
<context context-type="linenumber">44</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">No documents found</target>
|
||||
<target state="translated">Aucun document trouvé</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5554528553553249088" datatype="html">
|
||||
<source>Show password</source>
|
||||
@@ -4700,7 +4700,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||
<context context-type="linenumber">9</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">-</target>
|
||||
<target state="translated">-</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8479257185772414452" datatype="html">
|
||||
<source>+</source>
|
||||
@@ -4708,7 +4708,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">+</target>
|
||||
<target state="translated">+</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8659635229098859487" datatype="html" approved="yes">
|
||||
<source>Download original</source>
|
||||
@@ -6870,7 +6870,7 @@
|
||||
<context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Document Link</target>
|
||||
<target state="translated">Lien du Document</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5948496158474272829" datatype="html" approved="yes">
|
||||
<source>Warning: You have unsaved changes to your document(s).</source>
|
||||
|
||||
@@ -476,7 +476,7 @@
|
||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Auto refresh</target>
|
||||
<target state="translated">ריענון אוטומטי</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3894950702316166331" datatype="html">
|
||||
<source>Loading...</source>
|
||||
@@ -908,7 +908,7 @@
|
||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||
<context context-type="linenumber">174</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Default Permissions</target>
|
||||
<target state="translated">הרשאות ברירת מחדל</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8222269449891326545" datatype="html">
|
||||
<source> Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI </source>
|
||||
|
||||
@@ -1958,7 +1958,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">144</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Password has been changed, you will be logged out momentarily.</target>
|
||||
<target state="translated">A senha foi alterada, você será deslogado momentaneamente.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="2753185112875184719" datatype="html">
|
||||
<source>Saved user "<x id="PH" equiv-text="newUser.username"/>".</source>
|
||||
@@ -2163,7 +2163,7 @@
|
||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">My Profile</target>
|
||||
<target state="translated">Meu Perfil</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3797778920049399855" datatype="html" approved="yes">
|
||||
<source>Logout</source>
|
||||
@@ -4086,7 +4086,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">3</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Edit Profile</target>
|
||||
<target state="translated">Editar Perfil</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8214169742072920158" datatype="html">
|
||||
<source>Confirm Email</source>
|
||||
@@ -4094,7 +4094,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">13</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Confirm Email</target>
|
||||
<target state="translated">Confirmar Email</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3241357959735682038" datatype="html">
|
||||
<source>Confirm Password</source>
|
||||
@@ -4102,7 +4102,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Confirm Password</target>
|
||||
<target state="translated">Confirmar Senha</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="7554924397178347823" datatype="html">
|
||||
<source>API Auth Token</source>
|
||||
@@ -4110,7 +4110,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">31</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">API Auth Token</target>
|
||||
<target state="translated">Token de autenticação da API</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4323470180912194028" datatype="html">
|
||||
<source>Copy</source>
|
||||
@@ -4134,7 +4134,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">41</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Regenerate auth token</target>
|
||||
<target state="translated">Regerar token de autenticação</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5392341774767336507" datatype="html">
|
||||
<source>Copied!</source>
|
||||
@@ -4154,7 +4154,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">49</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Warning: changing the token cannot be undone</target>
|
||||
<target state="translated">Aviso: alterar o token não pode ser desfeito</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6141884091799403188" datatype="html">
|
||||
<source>Emails must match</source>
|
||||
@@ -4162,7 +4162,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">94</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Emails must match</target>
|
||||
<target state="translated">Os e-mails devem ser iguais</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5281933990298241826" datatype="html">
|
||||
<source>Passwords must match</source>
|
||||
@@ -4170,7 +4170,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">122</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Passwords must match</target>
|
||||
<target state="translated">As senhas devem ser iguais</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4219429959475101385" datatype="html">
|
||||
<source>Profile updated successfully</source>
|
||||
@@ -4178,7 +4178,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">141</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Profile updated successfully</target>
|
||||
<target state="translated">Perfil atualizado com sucesso</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3417726855410304962" datatype="html">
|
||||
<source>Error saving profile</source>
|
||||
@@ -4186,7 +4186,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">153</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Error saving profile</target>
|
||||
<target state="translated">Erro ao salvar o perfil</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="154249228726292516" datatype="html">
|
||||
<source>Error generating auth token</source>
|
||||
@@ -4194,7 +4194,7 @@ Curingas como *.pdf ou *invoice* são permitidos. Sem diferenciação de maiúsc
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">170</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Error generating auth token</target>
|
||||
<target state="translated">Erro ao gerar token de autenticação</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3797570084942068182" datatype="html" approved="yes">
|
||||
<source>Select</source>
|
||||
|
||||
@@ -476,7 +476,7 @@
|
||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Auto refresh</target>
|
||||
<target state="translated">Automatsko osvеžavanjе</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3894950702316166331" datatype="html">
|
||||
<source>Loading...</source>
|
||||
@@ -2163,7 +2163,7 @@
|
||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">My Profile</target>
|
||||
<target state="translated">Moj profil</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3797778920049399855" datatype="html">
|
||||
<source>Logout</source>
|
||||
@@ -2811,7 +2811,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">38</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Assign custom fields</target>
|
||||
<target state="translated">Dodeli dodatno polje</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5057200219587080996" datatype="html">
|
||||
<source>Assign owner</source>
|
||||
@@ -3355,7 +3355,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">24</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Filter attachment filename includes</target>
|
||||
<target state="translated">Filter naziva fajla priloga uključuje</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4245210767172267486" datatype="html">
|
||||
<source>Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</source>
|
||||
@@ -3371,7 +3371,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">25</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Filter attachment filename excluding</target>
|
||||
<target state="translated">Filter naziva fajla priloga isključuje</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6774472763442688477" datatype="html">
|
||||
<source>Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</source>
|
||||
@@ -3379,7 +3379,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">25</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</target>
|
||||
<target state="translated">Neće obrađivati dokumente koji se u potpunosti podudaraju sa ovim imenom datoteke ako je navedeno. Dopušteni su zamenski znakovi kao što su *.pdf ili *faktura*. Neosetljivo je na mala i mala slova.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="9216117865911519658" datatype="html">
|
||||
<source>Action</source>
|
||||
@@ -3896,7 +3896,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.ts</context>
|
||||
<context context-type="linenumber">44</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">No documents found</target>
|
||||
<target state="translated">Nije pronađen nijedan dokument</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5554528553553249088" datatype="html">
|
||||
<source>Show password</source>
|
||||
@@ -3904,7 +3904,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/input/password/password.component.html</context>
|
||||
<context context-type="linenumber">5</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Show password</target>
|
||||
<target state="translated">Prikaži lozinku</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="594042705136125260" datatype="html">
|
||||
<source>Edit Permissions</source>
|
||||
@@ -4085,7 +4085,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">3</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Edit Profile</target>
|
||||
<target state="translated">Izmeni profil</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8214169742072920158" datatype="html">
|
||||
<source>Confirm Email</source>
|
||||
@@ -4093,7 +4093,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">13</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Confirm Email</target>
|
||||
<target state="translated">Potvrdi mejl</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3241357959735682038" datatype="html">
|
||||
<source>Confirm Password</source>
|
||||
@@ -4101,7 +4101,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Confirm Password</target>
|
||||
<target state="translated">Potvrdi lozinku</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="7554924397178347823" datatype="html">
|
||||
<source>API Auth Token</source>
|
||||
@@ -4109,7 +4109,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">31</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">API Auth Token</target>
|
||||
<target state="translated">API Auth Token</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4323470180912194028" datatype="html">
|
||||
<source>Copy</source>
|
||||
@@ -4133,7 +4133,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">41</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Regenerate auth token</target>
|
||||
<target state="translated">Ponovo generiši token autentifikacije</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5392341774767336507" datatype="html">
|
||||
<source>Copied!</source>
|
||||
@@ -4153,7 +4153,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">49</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Warning: changing the token cannot be undone</target>
|
||||
<target state="translated">Upozorenje: promena tokena se ne može opozvati</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6141884091799403188" datatype="html">
|
||||
<source>Emails must match</source>
|
||||
@@ -4161,7 +4161,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">94</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Emails must match</target>
|
||||
<target state="translated">Mejlovi se moraju poklapati</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5281933990298241826" datatype="html">
|
||||
<source>Passwords must match</source>
|
||||
@@ -4169,7 +4169,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">122</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Passwords must match</target>
|
||||
<target state="translated">Lozinke se moraju poklapati</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4219429959475101385" datatype="html">
|
||||
<source>Profile updated successfully</source>
|
||||
@@ -4177,7 +4177,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">141</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Profile updated successfully</target>
|
||||
<target state="translated">Profil je uspešno ažuriran</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3417726855410304962" datatype="html">
|
||||
<source>Error saving profile</source>
|
||||
@@ -4185,7 +4185,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">153</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Error saving profile</target>
|
||||
<target state="translated">Greška prilikom čuvanja profila</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="154249228726292516" datatype="html">
|
||||
<source>Error generating auth token</source>
|
||||
@@ -4193,7 +4193,7 @@
|
||||
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">170</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Error generating auth token</target>
|
||||
<target state="translated">Greška prilikom generisanja auth tokena</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3797570084942068182" datatype="html">
|
||||
<source>Select</source>
|
||||
@@ -4700,7 +4700,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||
<context context-type="linenumber">9</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">-</target>
|
||||
<target state="translated">-</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8479257185772414452" datatype="html">
|
||||
<source>+</source>
|
||||
@@ -4708,7 +4708,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">+</target>
|
||||
<target state="translated">+</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8659635229098859487" datatype="html">
|
||||
<source>Download original</source>
|
||||
@@ -5132,7 +5132,7 @@
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
<context context-type="linenumber">781</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Page Fit</target>
|
||||
<target state="translated">Uklopi stranu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6857598786757174736" datatype="html">
|
||||
<source>Select:</source>
|
||||
@@ -6870,7 +6870,7 @@
|
||||
<context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
<target state="needs-translation">Document Link</target>
|
||||
<target state="translated">Veza ka dokumentu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="5948496158474272829" datatype="html">
|
||||
<source>Warning: You have unsaved changes to your document(s).</source>
|
||||
|
||||
@@ -3,6 +3,7 @@ import math
|
||||
import os
|
||||
from collections import Counter
|
||||
from contextlib import contextmanager
|
||||
from typing import Optional
|
||||
|
||||
from dateutil.parser import isoparse
|
||||
from django.conf import settings
|
||||
@@ -25,9 +26,12 @@ from whoosh.index import open_dir
|
||||
from whoosh.qparser import MultifieldParser
|
||||
from whoosh.qparser import QueryParser
|
||||
from whoosh.qparser.dateparse import DateParserPlugin
|
||||
from whoosh.qparser.dateparse import English
|
||||
from whoosh.qparser.plugins import FieldsPlugin
|
||||
from whoosh.scoring import TF_IDF
|
||||
from whoosh.searching import ResultsPage
|
||||
from whoosh.searching import Searcher
|
||||
from whoosh.util.times import timespan
|
||||
from whoosh.writing import AsyncWriter
|
||||
|
||||
# from documents.models import CustomMetadata
|
||||
@@ -356,6 +360,22 @@ class DelayedQuery:
|
||||
return page
|
||||
|
||||
|
||||
class LocalDateParser(English):
|
||||
def reverse_timezone_offset(self, d):
|
||||
return (d.replace(tzinfo=timezone.get_current_timezone())).astimezone(
|
||||
timezone.utc,
|
||||
)
|
||||
|
||||
def date_from(self, *args, **kwargs):
|
||||
d = super().date_from(*args, **kwargs)
|
||||
if isinstance(d, timespan):
|
||||
d.start = self.reverse_timezone_offset(d.start)
|
||||
d.end = self.reverse_timezone_offset(d.end)
|
||||
else:
|
||||
d = self.reverse_timezone_offset(d)
|
||||
return d
|
||||
|
||||
|
||||
class DelayedFullTextQuery(DelayedQuery):
|
||||
def _get_query(self):
|
||||
q_str = self.query_params["query"]
|
||||
@@ -371,7 +391,12 @@ class DelayedFullTextQuery(DelayedQuery):
|
||||
],
|
||||
self.searcher.ixreader.schema,
|
||||
)
|
||||
qp.add_plugin(DateParserPlugin(basedate=timezone.now()))
|
||||
qp.add_plugin(
|
||||
DateParserPlugin(
|
||||
basedate=timezone.now(),
|
||||
dateparser=LocalDateParser(),
|
||||
),
|
||||
)
|
||||
q = qp.parse(q_str)
|
||||
|
||||
corrected = self.searcher.correct_query(q, q_str)
|
||||
@@ -402,7 +427,12 @@ class DelayedMoreLikeThisQuery(DelayedQuery):
|
||||
return q, mask
|
||||
|
||||
|
||||
def autocomplete(ix: FileIndex, term: str, limit: int = 10, user: User = None):
|
||||
def autocomplete(
|
||||
ix: FileIndex,
|
||||
term: str,
|
||||
limit: int = 10,
|
||||
user: Optional[User] = None,
|
||||
):
|
||||
"""
|
||||
Mimics whoosh.reading.IndexReader.most_distinctive_terms with permissions
|
||||
and without scoring
|
||||
@@ -411,6 +441,9 @@ def autocomplete(ix: FileIndex, term: str, limit: int = 10, user: User = None):
|
||||
|
||||
with ix.searcher(weighting=TF_IDF()) as s:
|
||||
qp = QueryParser("content", schema=ix.schema)
|
||||
# Don't let searches with a query that happen to match a field override the
|
||||
# content field query instead and return bogus, not text data
|
||||
qp.remove_plugin_class(FieldsPlugin)
|
||||
q = qp.parse(f"{term.lower()}*")
|
||||
user_criterias = get_permissions_criterias(user)
|
||||
|
||||
@@ -430,7 +463,7 @@ def autocomplete(ix: FileIndex, term: str, limit: int = 10, user: User = None):
|
||||
return terms
|
||||
|
||||
|
||||
def get_permissions_criterias(user: User = None):
|
||||
def get_permissions_criterias(user: Optional[User] = None):
|
||||
user_criterias = [query.Term("has_owner", False)]
|
||||
if user is not None:
|
||||
if user.is_superuser: # superusers see all docs
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
337
src/documents/tests/test_api_bulk_download.py
Normal file
337
src/documents/tests/test_api_bulk_download.py
Normal file
@@ -0,0 +1,337 @@
|
||||
import datetime
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import override_settings
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.models import Correspondent
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
|
||||
|
||||
class TestBulkDownload(DirectoriesMixin, APITestCase):
|
||||
ENDPOINT = "/api/documents/bulk_download/"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
self.doc1 = Document.objects.create(title="unrelated", checksum="A")
|
||||
self.doc2 = Document.objects.create(
|
||||
title="document A",
|
||||
filename="docA.pdf",
|
||||
mime_type="application/pdf",
|
||||
checksum="B",
|
||||
created=timezone.make_aware(datetime.datetime(2021, 1, 1)),
|
||||
)
|
||||
self.doc2b = Document.objects.create(
|
||||
title="document A",
|
||||
filename="docA2.pdf",
|
||||
mime_type="application/pdf",
|
||||
checksum="D",
|
||||
created=timezone.make_aware(datetime.datetime(2021, 1, 1)),
|
||||
)
|
||||
self.doc3 = Document.objects.create(
|
||||
title="document B",
|
||||
filename="docB.jpg",
|
||||
mime_type="image/jpeg",
|
||||
checksum="C",
|
||||
created=timezone.make_aware(datetime.datetime(2020, 3, 21)),
|
||||
archive_filename="docB.pdf",
|
||||
archive_checksum="D",
|
||||
)
|
||||
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
self.doc2.source_path,
|
||||
)
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.png"),
|
||||
self.doc2b.source_path,
|
||||
)
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"),
|
||||
self.doc3.source_path,
|
||||
)
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "test_with_bom.pdf"),
|
||||
self.doc3.archive_path,
|
||||
)
|
||||
|
||||
def test_download_originals(self):
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{"documents": [self.doc2.id, self.doc3.id], "content": "originals"},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response["Content-Type"], "application/zip")
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
|
||||
self.assertEqual(len(zipf.filelist), 2)
|
||||
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
|
||||
self.assertIn("2020-03-21 document B.jpg", zipf.namelist())
|
||||
|
||||
with self.doc2.source_file as f:
|
||||
self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
|
||||
|
||||
with self.doc3.source_file as f:
|
||||
self.assertEqual(f.read(), zipf.read("2020-03-21 document B.jpg"))
|
||||
|
||||
def test_download_default(self):
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps({"documents": [self.doc2.id, self.doc3.id]}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response["Content-Type"], "application/zip")
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
|
||||
self.assertEqual(len(zipf.filelist), 2)
|
||||
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
|
||||
self.assertIn("2020-03-21 document B.pdf", zipf.namelist())
|
||||
|
||||
with self.doc2.source_file as f:
|
||||
self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
|
||||
|
||||
with self.doc3.archive_file as f:
|
||||
self.assertEqual(f.read(), zipf.read("2020-03-21 document B.pdf"))
|
||||
|
||||
def test_download_both(self):
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps({"documents": [self.doc2.id, self.doc3.id], "content": "both"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response["Content-Type"], "application/zip")
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
|
||||
self.assertEqual(len(zipf.filelist), 3)
|
||||
self.assertIn("originals/2021-01-01 document A.pdf", zipf.namelist())
|
||||
self.assertIn("archive/2020-03-21 document B.pdf", zipf.namelist())
|
||||
self.assertIn("originals/2020-03-21 document B.jpg", zipf.namelist())
|
||||
|
||||
with self.doc2.source_file as f:
|
||||
self.assertEqual(
|
||||
f.read(),
|
||||
zipf.read("originals/2021-01-01 document A.pdf"),
|
||||
)
|
||||
|
||||
with self.doc3.archive_file as f:
|
||||
self.assertEqual(
|
||||
f.read(),
|
||||
zipf.read("archive/2020-03-21 document B.pdf"),
|
||||
)
|
||||
|
||||
with self.doc3.source_file as f:
|
||||
self.assertEqual(
|
||||
f.read(),
|
||||
zipf.read("originals/2020-03-21 document B.jpg"),
|
||||
)
|
||||
|
||||
def test_filename_clashes(self):
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps({"documents": [self.doc2.id, self.doc2b.id]}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response["Content-Type"], "application/zip")
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
|
||||
self.assertEqual(len(zipf.filelist), 2)
|
||||
|
||||
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
|
||||
self.assertIn("2021-01-01 document A_01.pdf", zipf.namelist())
|
||||
|
||||
with self.doc2.source_file as f:
|
||||
self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
|
||||
|
||||
with self.doc2b.source_file as f:
|
||||
self.assertEqual(f.read(), zipf.read("2021-01-01 document A_01.pdf"))
|
||||
|
||||
def test_compression(self):
|
||||
self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{"documents": [self.doc2.id, self.doc2b.id], "compression": "lzma"},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
||||
def test_formatted_download_originals(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Defined file naming format
|
||||
WHEN:
|
||||
- Bulk download request for original documents
|
||||
- Bulk download request requests to follow format
|
||||
THEN:
|
||||
- Files in resulting zipfile are formatted
|
||||
"""
|
||||
|
||||
c = Correspondent.objects.create(name="test")
|
||||
c2 = Correspondent.objects.create(name="a space name")
|
||||
|
||||
self.doc2.correspondent = c
|
||||
self.doc2.title = "This is Doc 2"
|
||||
self.doc2.save()
|
||||
|
||||
self.doc3.correspondent = c2
|
||||
self.doc3.title = "Title 2 - Doc 3"
|
||||
self.doc3.save()
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id, self.doc3.id],
|
||||
"content": "originals",
|
||||
"follow_formatting": True,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response["Content-Type"], "application/zip")
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
|
||||
self.assertEqual(len(zipf.filelist), 2)
|
||||
self.assertIn("a space name/Title 2 - Doc 3.jpg", zipf.namelist())
|
||||
self.assertIn("test/This is Doc 2.pdf", zipf.namelist())
|
||||
|
||||
with self.doc2.source_file as f:
|
||||
self.assertEqual(f.read(), zipf.read("test/This is Doc 2.pdf"))
|
||||
|
||||
with self.doc3.source_file as f:
|
||||
self.assertEqual(
|
||||
f.read(),
|
||||
zipf.read("a space name/Title 2 - Doc 3.jpg"),
|
||||
)
|
||||
|
||||
@override_settings(FILENAME_FORMAT="somewhere/{title}")
|
||||
def test_formatted_download_archive(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Defined file naming format
|
||||
WHEN:
|
||||
- Bulk download request for archive documents
|
||||
- Bulk download request requests to follow format
|
||||
THEN:
|
||||
- Files in resulting zipfile are formatted
|
||||
"""
|
||||
|
||||
self.doc2.title = "This is Doc 2"
|
||||
self.doc2.save()
|
||||
|
||||
self.doc3.title = "Title 2 - Doc 3"
|
||||
self.doc3.save()
|
||||
print(self.doc3.archive_path)
|
||||
print(self.doc3.archive_filename)
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id, self.doc3.id],
|
||||
"follow_formatting": True,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response["Content-Type"], "application/zip")
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
|
||||
self.assertEqual(len(zipf.filelist), 2)
|
||||
self.assertIn("somewhere/This is Doc 2.pdf", zipf.namelist())
|
||||
self.assertIn("somewhere/Title 2 - Doc 3.pdf", zipf.namelist())
|
||||
|
||||
with self.doc2.source_file as f:
|
||||
self.assertEqual(f.read(), zipf.read("somewhere/This is Doc 2.pdf"))
|
||||
|
||||
with self.doc3.archive_file as f:
|
||||
self.assertEqual(f.read(), zipf.read("somewhere/Title 2 - Doc 3.pdf"))
|
||||
|
||||
@override_settings(FILENAME_FORMAT="{document_type}/{title}")
|
||||
def test_formatted_download_both(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Defined file naming format
|
||||
WHEN:
|
||||
- Bulk download request for original documents and archive documents
|
||||
- Bulk download request requests to follow format
|
||||
THEN:
|
||||
- Files defined in resulting zipfile are formatted
|
||||
"""
|
||||
|
||||
dc1 = DocumentType.objects.create(name="bill")
|
||||
dc2 = DocumentType.objects.create(name="statement")
|
||||
|
||||
self.doc2.document_type = dc1
|
||||
self.doc2.title = "This is Doc 2"
|
||||
self.doc2.save()
|
||||
|
||||
self.doc3.document_type = dc2
|
||||
self.doc3.title = "Title 2 - Doc 3"
|
||||
self.doc3.save()
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id, self.doc3.id],
|
||||
"content": "both",
|
||||
"follow_formatting": True,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response["Content-Type"], "application/zip")
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
|
||||
self.assertEqual(len(zipf.filelist), 3)
|
||||
self.assertIn("originals/bill/This is Doc 2.pdf", zipf.namelist())
|
||||
self.assertIn("archive/statement/Title 2 - Doc 3.pdf", zipf.namelist())
|
||||
self.assertIn("originals/statement/Title 2 - Doc 3.jpg", zipf.namelist())
|
||||
|
||||
with self.doc2.source_file as f:
|
||||
self.assertEqual(
|
||||
f.read(),
|
||||
zipf.read("originals/bill/This is Doc 2.pdf"),
|
||||
)
|
||||
|
||||
with self.doc3.archive_file as f:
|
||||
self.assertEqual(
|
||||
f.read(),
|
||||
zipf.read("archive/statement/Title 2 - Doc 3.pdf"),
|
||||
)
|
||||
|
||||
with self.doc3.source_file as f:
|
||||
self.assertEqual(
|
||||
f.read(),
|
||||
zipf.read("originals/statement/Title 2 - Doc 3.jpg"),
|
||||
)
|
||||
870
src/documents/tests/test_api_bulk_edit.py
Normal file
870
src/documents/tests/test_api_bulk_edit.py
Normal file
@@ -0,0 +1,870 @@
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from guardian.shortcuts import assign_perm
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents import bulk_edit
|
||||
from documents.models import Correspondent
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
|
||||
|
||||
class TestBulkEdit(DirectoriesMixin, APITestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
patcher = mock.patch("documents.bulk_edit.bulk_update_documents.delay")
|
||||
self.async_task = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.c1 = Correspondent.objects.create(name="c1")
|
||||
self.c2 = Correspondent.objects.create(name="c2")
|
||||
self.dt1 = DocumentType.objects.create(name="dt1")
|
||||
self.dt2 = DocumentType.objects.create(name="dt2")
|
||||
self.t1 = Tag.objects.create(name="t1")
|
||||
self.t2 = Tag.objects.create(name="t2")
|
||||
self.doc1 = Document.objects.create(checksum="A", title="A")
|
||||
self.doc2 = Document.objects.create(
|
||||
checksum="B",
|
||||
title="B",
|
||||
correspondent=self.c1,
|
||||
document_type=self.dt1,
|
||||
)
|
||||
self.doc3 = Document.objects.create(
|
||||
checksum="C",
|
||||
title="C",
|
||||
correspondent=self.c2,
|
||||
document_type=self.dt2,
|
||||
)
|
||||
self.doc4 = Document.objects.create(checksum="D", title="D")
|
||||
self.doc5 = Document.objects.create(checksum="E", title="E")
|
||||
self.doc2.tags.add(self.t1)
|
||||
self.doc3.tags.add(self.t2)
|
||||
self.doc4.tags.add(self.t1, self.t2)
|
||||
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
|
||||
|
||||
def test_set_correspondent(self):
|
||||
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
|
||||
bulk_edit.set_correspondent(
|
||||
[self.doc1.id, self.doc2.id, self.doc3.id],
|
||||
self.c2.id,
|
||||
)
|
||||
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 3)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id])
|
||||
|
||||
def test_unset_correspondent(self):
|
||||
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
|
||||
bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], None)
|
||||
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 0)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
|
||||
|
||||
def test_set_document_type(self):
|
||||
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
|
||||
bulk_edit.set_document_type(
|
||||
[self.doc1.id, self.doc2.id, self.doc3.id],
|
||||
self.dt2.id,
|
||||
)
|
||||
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 3)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id])
|
||||
|
||||
def test_unset_document_type(self):
|
||||
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
|
||||
bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], None)
|
||||
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 0)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
|
||||
|
||||
def test_set_document_storage_path(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- 5 documents without defined storage path
|
||||
WHEN:
|
||||
- Bulk edit called to add storage path to 1 document
|
||||
THEN:
|
||||
- Single document storage path update
|
||||
"""
|
||||
self.assertEqual(Document.objects.filter(storage_path=None).count(), 5)
|
||||
|
||||
bulk_edit.set_storage_path(
|
||||
[self.doc1.id],
|
||||
self.sp1.id,
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.filter(storage_path=None).count(), 4)
|
||||
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
|
||||
self.assertCountEqual(kwargs["document_ids"], [self.doc1.id])
|
||||
|
||||
def test_unset_document_storage_path(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- 4 documents without defined storage path
|
||||
- 1 document with a defined storage
|
||||
WHEN:
|
||||
- Bulk edit called to remove storage path from 1 document
|
||||
THEN:
|
||||
- Single document storage path removed
|
||||
"""
|
||||
self.assertEqual(Document.objects.filter(storage_path=None).count(), 5)
|
||||
|
||||
bulk_edit.set_storage_path(
|
||||
[self.doc1.id],
|
||||
self.sp1.id,
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.filter(storage_path=None).count(), 4)
|
||||
|
||||
bulk_edit.set_storage_path(
|
||||
[self.doc1.id],
|
||||
None,
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.filter(storage_path=None).count(), 5)
|
||||
|
||||
self.async_task.assert_called()
|
||||
args, kwargs = self.async_task.call_args
|
||||
|
||||
self.assertCountEqual(kwargs["document_ids"], [self.doc1.id])
|
||||
|
||||
def test_add_tag(self):
|
||||
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
|
||||
bulk_edit.add_tag(
|
||||
[self.doc1.id, self.doc2.id, self.doc3.id, self.doc4.id],
|
||||
self.t1.id,
|
||||
)
|
||||
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 4)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc3.id])
|
||||
|
||||
def test_remove_tag(self):
|
||||
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
|
||||
bulk_edit.remove_tag([self.doc1.id, self.doc3.id, self.doc4.id], self.t1.id)
|
||||
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 1)
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
self.assertCountEqual(kwargs["document_ids"], [self.doc4.id])
|
||||
|
||||
def test_modify_tags(self):
|
||||
tag_unrelated = Tag.objects.create(name="unrelated")
|
||||
self.doc2.tags.add(tag_unrelated)
|
||||
self.doc3.tags.add(tag_unrelated)
|
||||
bulk_edit.modify_tags(
|
||||
[self.doc2.id, self.doc3.id],
|
||||
add_tags=[self.t2.id],
|
||||
remove_tags=[self.t1.id],
|
||||
)
|
||||
|
||||
self.assertCountEqual(list(self.doc2.tags.all()), [self.t2, tag_unrelated])
|
||||
self.assertCountEqual(list(self.doc3.tags.all()), [self.t2, tag_unrelated])
|
||||
|
||||
self.async_task.assert_called_once()
|
||||
args, kwargs = self.async_task.call_args
|
||||
# TODO: doc3 should not be affected, but the query for that is rather complicated
|
||||
self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
|
||||
|
||||
def test_delete(self):
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
bulk_edit.delete([self.doc1.id, self.doc2.id])
|
||||
self.assertEqual(Document.objects.count(), 3)
|
||||
self.assertCountEqual(
|
||||
[doc.id for doc in Document.objects.all()],
|
||||
[self.doc3.id, self.doc4.id, self.doc5.id],
|
||||
)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_correspondent")
|
||||
def test_api_set_correspondent(self, m):
|
||||
m.return_value = "OK"
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "set_correspondent",
|
||||
"parameters": {"correspondent": self.c1.id},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertEqual(args[0], [self.doc1.id])
|
||||
self.assertEqual(kwargs["correspondent"], self.c1.id)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_correspondent")
|
||||
def test_api_unset_correspondent(self, m):
|
||||
m.return_value = "OK"
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "set_correspondent",
|
||||
"parameters": {"correspondent": None},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertEqual(args[0], [self.doc1.id])
|
||||
self.assertIsNone(kwargs["correspondent"])
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_document_type")
|
||||
def test_api_set_type(self, m):
|
||||
m.return_value = "OK"
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "set_document_type",
|
||||
"parameters": {"document_type": self.dt1.id},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertEqual(args[0], [self.doc1.id])
|
||||
self.assertEqual(kwargs["document_type"], self.dt1.id)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_document_type")
|
||||
def test_api_unset_type(self, m):
|
||||
m.return_value = "OK"
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "set_document_type",
|
||||
"parameters": {"document_type": None},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertEqual(args[0], [self.doc1.id])
|
||||
self.assertIsNone(kwargs["document_type"])
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.add_tag")
|
||||
def test_api_add_tag(self, m):
|
||||
m.return_value = "OK"
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "add_tag",
|
||||
"parameters": {"tag": self.t1.id},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertEqual(args[0], [self.doc1.id])
|
||||
self.assertEqual(kwargs["tag"], self.t1.id)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.remove_tag")
|
||||
def test_api_remove_tag(self, m):
|
||||
m.return_value = "OK"
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "remove_tag",
|
||||
"parameters": {"tag": self.t1.id},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertEqual(args[0], [self.doc1.id])
|
||||
self.assertEqual(kwargs["tag"], self.t1.id)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.modify_tags")
|
||||
def test_api_modify_tags(self, m):
|
||||
m.return_value = "OK"
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id, self.doc3.id],
|
||||
"method": "modify_tags",
|
||||
"parameters": {
|
||||
"add_tags": [self.t1.id],
|
||||
"remove_tags": [self.t2.id],
|
||||
},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertListEqual(args[0], [self.doc1.id, self.doc3.id])
|
||||
self.assertEqual(kwargs["add_tags"], [self.t1.id])
|
||||
self.assertEqual(kwargs["remove_tags"], [self.t2.id])
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.modify_tags")
|
||||
def test_api_modify_tags_not_provided(self, m):
|
||||
"""
|
||||
GIVEN:
|
||||
- API data to modify tags is missing modify_tags field
|
||||
WHEN:
|
||||
- API to edit tags is called
|
||||
THEN:
|
||||
- API returns HTTP 400
|
||||
- modify_tags is not called
|
||||
"""
|
||||
m.return_value = "OK"
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id, self.doc3.id],
|
||||
"method": "modify_tags",
|
||||
"parameters": {
|
||||
"add_tags": [self.t1.id],
|
||||
},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
m.assert_not_called()
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.delete")
|
||||
def test_api_delete(self, m):
|
||||
m.return_value = "OK"
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{"documents": [self.doc1.id], "method": "delete", "parameters": {}},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertEqual(args[0], [self.doc1.id])
|
||||
self.assertEqual(len(kwargs), 0)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_storage_path")
|
||||
def test_api_set_storage_path(self, m):
|
||||
"""
|
||||
GIVEN:
|
||||
- API data to set the storage path of a document
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- set_storage_path is called with correct document IDs and storage_path ID
|
||||
"""
|
||||
m.return_value = "OK"
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "set_storage_path",
|
||||
"parameters": {"storage_path": self.sp1.id},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
|
||||
self.assertListEqual(args[0], [self.doc1.id])
|
||||
self.assertEqual(kwargs["storage_path"], self.sp1.id)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_storage_path")
|
||||
def test_api_unset_storage_path(self, m):
|
||||
"""
|
||||
GIVEN:
|
||||
- API data to clear/unset the storage path of a document
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- set_storage_path is called with correct document IDs and None storage_path
|
||||
"""
|
||||
m.return_value = "OK"
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "set_storage_path",
|
||||
"parameters": {"storage_path": None},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
|
||||
self.assertListEqual(args[0], [self.doc1.id])
|
||||
self.assertEqual(kwargs["storage_path"], None)
|
||||
|
||||
def test_api_invalid_storage_path(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API data to set the storage path of a document
|
||||
- Given storage_path ID isn't valid
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- set_storage_path is called with correct document IDs and storage_path ID
|
||||
"""
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "set_storage_path",
|
||||
"parameters": {"storage_path": self.sp1.id + 10},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.async_task.assert_not_called()
|
||||
|
||||
def test_api_set_storage_path_not_provided(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API data to set the storage path of a document
|
||||
- API data is missing storage path ID
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- set_storage_path is called with correct document IDs and storage_path ID
|
||||
"""
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id],
|
||||
"method": "set_storage_path",
|
||||
"parameters": {},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.async_task.assert_not_called()
|
||||
|
||||
def test_api_invalid_doc(self):
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps({"documents": [-235], "method": "delete", "parameters": {}}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
|
||||
def test_api_invalid_method(self):
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "exterminate",
|
||||
"parameters": {},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(Document.objects.count(), 5)
|
||||
|
||||
def test_api_invalid_correspondent(self):
|
||||
self.assertEqual(self.doc2.correspondent, self.c1)
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "set_correspondent",
|
||||
"parameters": {"correspondent": 345657},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
doc2 = Document.objects.get(id=self.doc2.id)
|
||||
self.assertEqual(doc2.correspondent, self.c1)
|
||||
|
||||
def test_api_no_correspondent(self):
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "set_correspondent",
|
||||
"parameters": {},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_api_invalid_document_type(self):
|
||||
self.assertEqual(self.doc2.document_type, self.dt1)
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "set_document_type",
|
||||
"parameters": {"document_type": 345657},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
doc2 = Document.objects.get(id=self.doc2.id)
|
||||
self.assertEqual(doc2.document_type, self.dt1)
|
||||
|
||||
def test_api_no_document_type(self):
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "set_document_type",
|
||||
"parameters": {},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_api_add_invalid_tag(self):
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "add_tag",
|
||||
"parameters": {"tag": 345657},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
|
||||
def test_api_add_tag_no_tag(self):
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{"documents": [self.doc2.id], "method": "add_tag", "parameters": {}},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_api_delete_invalid_tag(self):
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "remove_tag",
|
||||
"parameters": {"tag": 345657},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
|
||||
def test_api_delete_tag_no_tag(self):
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{"documents": [self.doc2.id], "method": "remove_tag", "parameters": {}},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_api_modify_invalid_tags(self):
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "modify_tags",
|
||||
"parameters": {
|
||||
"add_tags": [self.t2.id, 1657],
|
||||
"remove_tags": [1123123],
|
||||
},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_api_modify_tags_no_tags(self):
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "modify_tags",
|
||||
"parameters": {"remove_tags": [1123123]},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "modify_tags",
|
||||
"parameters": {"add_tags": [self.t2.id, 1657]},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_api_selection_data_empty(self):
|
||||
response = self.client.post(
|
||||
"/api/documents/selection_data/",
|
||||
json.dumps({"documents": []}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for field, Entity in [
|
||||
("selected_correspondents", Correspondent),
|
||||
("selected_tags", Tag),
|
||||
("selected_document_types", DocumentType),
|
||||
]:
|
||||
self.assertEqual(len(response.data[field]), Entity.objects.count())
|
||||
for correspondent in response.data[field]:
|
||||
self.assertEqual(correspondent["document_count"], 0)
|
||||
self.assertCountEqual(
|
||||
map(lambda c: c["id"], response.data[field]),
|
||||
map(lambda c: c["id"], Entity.objects.values("id")),
|
||||
)
|
||||
|
||||
def test_api_selection_data(self):
|
||||
response = self.client.post(
|
||||
"/api/documents/selection_data/",
|
||||
json.dumps(
|
||||
{"documents": [self.doc1.id, self.doc2.id, self.doc4.id, self.doc5.id]},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
self.assertCountEqual(
|
||||
response.data["selected_correspondents"],
|
||||
[
|
||||
{"id": self.c1.id, "document_count": 1},
|
||||
{"id": self.c2.id, "document_count": 0},
|
||||
],
|
||||
)
|
||||
self.assertCountEqual(
|
||||
response.data["selected_tags"],
|
||||
[
|
||||
{"id": self.t1.id, "document_count": 2},
|
||||
{"id": self.t2.id, "document_count": 1},
|
||||
],
|
||||
)
|
||||
self.assertCountEqual(
|
||||
response.data["selected_document_types"],
|
||||
[
|
||||
{"id": self.c1.id, "document_count": 1},
|
||||
{"id": self.c2.id, "document_count": 0},
|
||||
],
|
||||
)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||
def test_set_permissions(self, m):
|
||||
m.return_value = "OK"
|
||||
user1 = User.objects.create(username="user1")
|
||||
user2 = User.objects.create(username="user2")
|
||||
permissions = {
|
||||
"view": {
|
||||
"users": [user1.id, user2.id],
|
||||
"groups": None,
|
||||
},
|
||||
"change": {
|
||||
"users": [user1.id],
|
||||
"groups": None,
|
||||
},
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id, self.doc3.id],
|
||||
"method": "set_permissions",
|
||||
"parameters": {"set_permissions": permissions},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id])
|
||||
self.assertEqual(len(kwargs["set_permissions"]["view"]["users"]), 2)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||
def test_insufficient_permissions_ownership(self, m):
|
||||
"""
|
||||
GIVEN:
|
||||
- Documents owned by user other than logged in user
|
||||
WHEN:
|
||||
- set_permissions bulk edit API endpoint is called
|
||||
THEN:
|
||||
- User is not able to change permissions
|
||||
"""
|
||||
m.return_value = "OK"
|
||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||
self.doc1.save()
|
||||
user1 = User.objects.create(username="user1")
|
||||
self.client.force_authenticate(user=user1)
|
||||
|
||||
permissions = {
|
||||
"owner": user1.id,
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id, self.doc2.id, self.doc3.id],
|
||||
"method": "set_permissions",
|
||||
"parameters": {"set_permissions": permissions},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
m.assert_not_called()
|
||||
self.assertEqual(response.content, b"Insufficient permissions")
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id, self.doc3.id],
|
||||
"method": "set_permissions",
|
||||
"parameters": {"set_permissions": permissions},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_storage_path")
|
||||
def test_insufficient_permissions_edit(self, m):
|
||||
"""
|
||||
GIVEN:
|
||||
- Documents for which current user only has view permissions
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- set_storage_path only called if user can edit all docs
|
||||
"""
|
||||
m.return_value = "OK"
|
||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||
self.doc1.save()
|
||||
user1 = User.objects.create(username="user1")
|
||||
assign_perm("view_document", user1, self.doc1)
|
||||
self.client.force_authenticate(user=user1)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id, self.doc2.id, self.doc3.id],
|
||||
"method": "set_storage_path",
|
||||
"parameters": {"storage_path": self.sp1.id},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
m.assert_not_called()
|
||||
self.assertEqual(response.content, b"Insufficient permissions")
|
||||
|
||||
assign_perm("change_document", user1, self.doc1)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc1.id, self.doc2.id, self.doc3.id],
|
||||
"method": "set_storage_path",
|
||||
"parameters": {"storage_path": self.sp1.id},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
m.assert_called_once()
|
||||
236
src/documents/tests/test_api_consumption_templates.py
Normal file
236
src/documents/tests/test_api_consumption_templates.py
Normal file
@@ -0,0 +1,236 @@
|
||||
import json
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.data_models import DocumentSource
|
||||
from documents.models import ConsumptionTemplate
|
||||
from documents.models import Correspondent
|
||||
from documents.models import CustomField
|
||||
from documents.models import DocumentType
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
from paperless_mail.models import MailAccount
|
||||
from paperless_mail.models import MailRule
|
||||
|
||||
|
||||
class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
|
||||
ENDPOINT = "/api/consumption_templates/"
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_authenticate(user=user)
|
||||
self.user2 = User.objects.create(username="user2")
|
||||
self.user3 = User.objects.create(username="user3")
|
||||
self.group1 = Group.objects.create(name="group1")
|
||||
|
||||
self.c = Correspondent.objects.create(name="Correspondent Name")
|
||||
self.c2 = Correspondent.objects.create(name="Correspondent Name 2")
|
||||
self.dt = DocumentType.objects.create(name="DocType Name")
|
||||
self.t1 = Tag.objects.create(name="t1")
|
||||
self.t2 = Tag.objects.create(name="t2")
|
||||
self.t3 = Tag.objects.create(name="t3")
|
||||
self.sp = StoragePath.objects.create(path="/test/")
|
||||
self.cf1 = CustomField.objects.create(name="Custom Field 1", data_type="string")
|
||||
self.cf2 = CustomField.objects.create(
|
||||
name="Custom Field 2",
|
||||
data_type="integer",
|
||||
)
|
||||
|
||||
self.ct = ConsumptionTemplate.objects.create(
|
||||
name="Template 1",
|
||||
order=0,
|
||||
sources=f"{int(DocumentSource.ApiUpload)},{int(DocumentSource.ConsumeFolder)},{int(DocumentSource.MailFetch)}",
|
||||
filter_filename="*simple*",
|
||||
filter_path="*/samples/*",
|
||||
assign_title="Doc from {correspondent}",
|
||||
assign_correspondent=self.c,
|
||||
assign_document_type=self.dt,
|
||||
assign_storage_path=self.sp,
|
||||
assign_owner=self.user2,
|
||||
)
|
||||
self.ct.assign_tags.add(self.t1)
|
||||
self.ct.assign_tags.add(self.t2)
|
||||
self.ct.assign_tags.add(self.t3)
|
||||
self.ct.assign_view_users.add(self.user3.pk)
|
||||
self.ct.assign_view_groups.add(self.group1.pk)
|
||||
self.ct.assign_change_users.add(self.user3.pk)
|
||||
self.ct.assign_change_groups.add(self.group1.pk)
|
||||
self.ct.assign_custom_fields.add(self.cf1.pk)
|
||||
self.ct.assign_custom_fields.add(self.cf2.pk)
|
||||
self.ct.save()
|
||||
|
||||
def test_api_get_consumption_template(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to get all consumption template
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Existing consumption templates are returned
|
||||
"""
|
||||
response = self.client.get(self.ENDPOINT, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["count"], 1)
|
||||
|
||||
resp_consumption_template = response.data["results"][0]
|
||||
self.assertEqual(resp_consumption_template["id"], self.ct.id)
|
||||
self.assertEqual(
|
||||
resp_consumption_template["assign_correspondent"],
|
||||
self.ct.assign_correspondent.pk,
|
||||
)
|
||||
|
||||
def test_api_create_consumption_template(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a consumption template
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP response
|
||||
- New template is created
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Template 2",
|
||||
"order": 1,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*test*",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(ConsumptionTemplate.objects.count(), 2)
|
||||
|
||||
def test_api_create_invalid_consumption_template(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a consumption template
|
||||
- Neither file name nor path filter are specified
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP 400 response
|
||||
- No template is created
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Template 2",
|
||||
"order": 1,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(ConsumptionTemplate.objects.count(), 1)
|
||||
|
||||
def test_api_create_consumption_template_empty_fields(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a consumption template
|
||||
- Path or filename filter or assign title are empty string
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Template is created but filter or title assignment is not set if ""
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Template 2",
|
||||
"order": 1,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*test*",
|
||||
"filter_path": "",
|
||||
"assign_title": "",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
ct = ConsumptionTemplate.objects.get(name="Template 2")
|
||||
self.assertEqual(ct.filter_filename, "*test*")
|
||||
self.assertIsNone(ct.filter_path)
|
||||
self.assertIsNone(ct.assign_title)
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Template 3",
|
||||
"order": 1,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "",
|
||||
"filter_path": "*/test/*",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
ct2 = ConsumptionTemplate.objects.get(name="Template 3")
|
||||
self.assertEqual(ct2.filter_path, "*/test/*")
|
||||
self.assertIsNone(ct2.filter_filename)
|
||||
|
||||
def test_api_create_consumption_template_with_mailrule(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a consumption template with a mail rule but no MailFetch source
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- New template is created with MailFetch as source
|
||||
"""
|
||||
account1 = MailAccount.objects.create(
|
||||
name="Email1",
|
||||
username="username1",
|
||||
password="password1",
|
||||
imap_server="server.example.com",
|
||||
imap_port=443,
|
||||
imap_security=MailAccount.ImapSecurity.SSL,
|
||||
character_set="UTF-8",
|
||||
)
|
||||
rule1 = MailRule.objects.create(
|
||||
name="Rule1",
|
||||
account=account1,
|
||||
folder="INBOX",
|
||||
filter_from="from@example.com",
|
||||
filter_to="someone@somewhere.com",
|
||||
filter_subject="subject",
|
||||
filter_body="body",
|
||||
filter_attachment_filename_include="file.pdf",
|
||||
maximum_age=30,
|
||||
action=MailRule.MailAction.MARK_READ,
|
||||
assign_title_from=MailRule.TitleSource.FROM_SUBJECT,
|
||||
assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING,
|
||||
order=0,
|
||||
attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
|
||||
)
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Template 2",
|
||||
"order": 1,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_mailrule": rule1.pk,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(ConsumptionTemplate.objects.count(), 2)
|
||||
ct = ConsumptionTemplate.objects.get(name="Template 2")
|
||||
self.assertEqual(ct.sources, [int(DocumentSource.MailFetch).__str__()])
|
||||
1992
src/documents/tests/test_api_documents.py
Normal file
1992
src/documents/tests/test_api_documents.py
Normal file
File diff suppressed because it is too large
Load Diff
224
src/documents/tests/test_api_objects.py
Normal file
224
src/documents/tests/test_api_objects.py
Normal file
@@ -0,0 +1,224 @@
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.models import Correspondent
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
|
||||
|
||||
class TestApiObjects(DirectoriesMixin, APITestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
self.tag1 = Tag.objects.create(name="t1", is_inbox_tag=True)
|
||||
self.tag2 = Tag.objects.create(name="t2")
|
||||
self.tag3 = Tag.objects.create(name="t3")
|
||||
self.c1 = Correspondent.objects.create(name="c1")
|
||||
self.c2 = Correspondent.objects.create(name="c2")
|
||||
self.c3 = Correspondent.objects.create(name="c3")
|
||||
self.dt1 = DocumentType.objects.create(name="dt1")
|
||||
self.dt2 = DocumentType.objects.create(name="dt2")
|
||||
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{title}")
|
||||
self.sp2 = StoragePath.objects.create(name="sp2", path="Something2/{title}")
|
||||
|
||||
def test_object_filters(self):
|
||||
response = self.client.get(
|
||||
f"/api/tags/?id={self.tag2.id}",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
results = response.data["results"]
|
||||
self.assertEqual(len(results), 1)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/tags/?id__in={self.tag1.id},{self.tag3.id}",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
results = response.data["results"]
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/correspondents/?id={self.c2.id}",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
results = response.data["results"]
|
||||
self.assertEqual(len(results), 1)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/correspondents/?id__in={self.c1.id},{self.c3.id}",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
results = response.data["results"]
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/document_types/?id={self.dt1.id}",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
results = response.data["results"]
|
||||
self.assertEqual(len(results), 1)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/document_types/?id__in={self.dt1.id},{self.dt2.id}",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
results = response.data["results"]
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/storage_paths/?id={self.sp1.id}",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
results = response.data["results"]
|
||||
self.assertEqual(len(results), 1)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/storage_paths/?id__in={self.sp1.id},{self.sp2.id}",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
results = response.data["results"]
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
|
||||
class TestApiStoragePaths(DirectoriesMixin, APITestCase):
|
||||
ENDPOINT = "/api/storage_paths/"
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
|
||||
|
||||
def test_api_get_storage_path(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to get all storage paths
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Existing storage paths are returned
|
||||
"""
|
||||
response = self.client.get(self.ENDPOINT, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["count"], 1)
|
||||
|
||||
resp_storage_path = response.data["results"][0]
|
||||
self.assertEqual(resp_storage_path["id"], self.sp1.id)
|
||||
self.assertEqual(resp_storage_path["path"], self.sp1.path)
|
||||
|
||||
def test_api_create_storage_path(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a storage paths
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP response
|
||||
- New storage path is created
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "A storage path",
|
||||
"path": "Somewhere/{asn}",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(StoragePath.objects.count(), 2)
|
||||
|
||||
def test_api_create_invalid_storage_path(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a storage paths
|
||||
- Storage path format is incorrect
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP 400 response
|
||||
- No storage path is created
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Another storage path",
|
||||
"path": "Somewhere/{correspdent}",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(StoragePath.objects.count(), 1)
|
||||
|
||||
def test_api_storage_path_placeholders(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a storage path with placeholders
|
||||
- Storage path is valid
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP response
|
||||
- New storage path is created
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Storage path with placeholders",
|
||||
"path": "{title}/{correspondent}/{document_type}/{created}/{created_year}"
|
||||
"/{created_year_short}/{created_month}/{created_month_name}"
|
||||
"/{created_month_name_short}/{created_day}/{added}/{added_year}"
|
||||
"/{added_year_short}/{added_month}/{added_month_name}"
|
||||
"/{added_month_name_short}/{added_day}/{asn}/{tags}"
|
||||
"/{tag_list}/{owner_username}/{original_name}/{doc_pk}/",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(StoragePath.objects.count(), 2)
|
||||
|
||||
@mock.patch("documents.bulk_edit.bulk_update_documents.delay")
|
||||
def test_api_update_storage_path(self, bulk_update_mock):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to get all storage paths
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Existing storage paths are returned
|
||||
"""
|
||||
document = Document.objects.create(
|
||||
mime_type="application/pdf",
|
||||
storage_path=self.sp1,
|
||||
)
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}{self.sp1.pk}/",
|
||||
data={
|
||||
"path": "somewhere/{created} - {title}",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
bulk_update_mock.assert_called_once()
|
||||
|
||||
args, _ = bulk_update_mock.call_args
|
||||
|
||||
self.assertCountEqual([document.pk], args[0])
|
||||
910
src/documents/tests/test_api_permissions.py
Normal file
910
src/documents/tests/test_api_permissions.py
Normal file
@@ -0,0 +1,910 @@
|
||||
import json
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.models import User
|
||||
from guardian.shortcuts import assign_perm
|
||||
from guardian.shortcuts import get_perms
|
||||
from guardian.shortcuts import get_users_with_perms
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.models import Correspondent
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import MatchingModel
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
|
||||
|
||||
class TestApiAuth(DirectoriesMixin, APITestCase):
|
||||
def test_auth_required(self):
|
||||
d = Document.objects.create(title="Test")
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get("/api/documents/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get(f"/api/documents/{d.id}/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get(f"/api/documents/{d.id}/download/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get(f"/api/documents/{d.id}/preview/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get(f"/api/documents/{d.id}/thumb/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get("/api/tags/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/correspondents/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/document_types/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get("/api/logs/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/saved_views/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get("/api/search/autocomplete/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/documents/bulk_edit/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/documents/bulk_download/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/documents/selection_data/").status_code,
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
def test_api_version_no_auth(self):
|
||||
response = self.client.get("/api/")
|
||||
self.assertNotIn("X-Api-Version", response)
|
||||
self.assertNotIn("X-Version", response)
|
||||
|
||||
def test_api_version_with_auth(self):
|
||||
user = User.objects.create_superuser(username="test")
|
||||
self.client.force_authenticate(user)
|
||||
response = self.client.get("/api/")
|
||||
self.assertIn("X-Api-Version", response)
|
||||
self.assertIn("X-Version", response)
|
||||
|
||||
def test_api_insufficient_permissions(self):
|
||||
user = User.objects.create_user(username="test")
|
||||
self.client.force_authenticate(user)
|
||||
|
||||
Document.objects.create(title="Test")
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get("/api/documents/").status_code,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get("/api/tags/").status_code,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/correspondents/").status_code,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/document_types/").status_code,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get("/api/logs/").status_code,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/saved_views/").status_code,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
def test_api_sufficient_permissions(self):
|
||||
user = User.objects.create_user(username="test")
|
||||
user.user_permissions.add(*Permission.objects.all())
|
||||
self.client.force_authenticate(user)
|
||||
|
||||
Document.objects.create(title="Test")
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get("/api/documents/").status_code,
|
||||
status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
self.assertEqual(self.client.get("/api/tags/").status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/correspondents/").status_code,
|
||||
status.HTTP_200_OK,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/document_types/").status_code,
|
||||
status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
self.assertEqual(self.client.get("/api/logs/").status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
self.client.get("/api/saved_views/").status_code,
|
||||
status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def test_api_get_object_permissions(self):
|
||||
user1 = User.objects.create_user(username="test1")
|
||||
user2 = User.objects.create_user(username="test2")
|
||||
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
||||
self.client.force_authenticate(user1)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get("/api/documents/").status_code,
|
||||
status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
d = Document.objects.create(title="Test", content="the content 1", checksum="1")
|
||||
|
||||
# no owner
|
||||
self.assertEqual(
|
||||
self.client.get(f"/api/documents/{d.id}/").status_code,
|
||||
status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
d2 = Document.objects.create(
|
||||
title="Test 2",
|
||||
content="the content 2",
|
||||
checksum="2",
|
||||
owner=user2,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get(f"/api/documents/{d2.id}/").status_code,
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
def test_api_default_owner(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create an object (Tag)
|
||||
WHEN:
|
||||
- owner is not set at all
|
||||
THEN:
|
||||
- Object created with current user as owner
|
||||
"""
|
||||
user1 = User.objects.create_superuser(username="user1")
|
||||
|
||||
self.client.force_authenticate(user1)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/tags/",
|
||||
json.dumps(
|
||||
{
|
||||
"name": "test1",
|
||||
"matching_algorithm": MatchingModel.MATCH_AUTO,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
tag1 = Tag.objects.filter(name="test1").first()
|
||||
self.assertEqual(tag1.owner, user1)
|
||||
|
||||
def test_api_set_no_owner(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create an object (Tag)
|
||||
WHEN:
|
||||
- owner is passed as None
|
||||
THEN:
|
||||
- Object created with no owner
|
||||
"""
|
||||
user1 = User.objects.create_superuser(username="user1")
|
||||
|
||||
self.client.force_authenticate(user1)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/tags/",
|
||||
json.dumps(
|
||||
{
|
||||
"name": "test1",
|
||||
"matching_algorithm": MatchingModel.MATCH_AUTO,
|
||||
"owner": None,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
tag1 = Tag.objects.filter(name="test1").first()
|
||||
self.assertEqual(tag1.owner, None)
|
||||
|
||||
def test_api_set_owner_w_permissions(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create an object (Tag) that supplies set_permissions object
|
||||
WHEN:
|
||||
- owner is passed as user id
|
||||
- view > users is set & view > groups is set
|
||||
THEN:
|
||||
- Object permissions are set appropriately
|
||||
"""
|
||||
user1 = User.objects.create_superuser(username="user1")
|
||||
user2 = User.objects.create(username="user2")
|
||||
group1 = Group.objects.create(name="group1")
|
||||
|
||||
self.client.force_authenticate(user1)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/tags/",
|
||||
json.dumps(
|
||||
{
|
||||
"name": "test1",
|
||||
"matching_algorithm": MatchingModel.MATCH_AUTO,
|
||||
"owner": user1.id,
|
||||
"set_permissions": {
|
||||
"view": {
|
||||
"users": [user2.id],
|
||||
"groups": [group1.id],
|
||||
},
|
||||
"change": {
|
||||
"users": None,
|
||||
"groups": None,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
tag1 = Tag.objects.filter(name="test1").first()
|
||||
|
||||
from guardian.core import ObjectPermissionChecker
|
||||
|
||||
checker = ObjectPermissionChecker(user2)
|
||||
self.assertEqual(checker.has_perm("view_tag", tag1), True)
|
||||
self.assertIn("view_tag", get_perms(group1, tag1))
|
||||
|
||||
def test_api_set_other_owner_w_permissions(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create an object (Tag)
|
||||
WHEN:
|
||||
- a different owner than is logged in is set
|
||||
- view > groups is set
|
||||
THEN:
|
||||
- Object permissions are set appropriately
|
||||
"""
|
||||
user1 = User.objects.create_superuser(username="user1")
|
||||
user2 = User.objects.create(username="user2")
|
||||
group1 = Group.objects.create(name="group1")
|
||||
|
||||
self.client.force_authenticate(user1)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/tags/",
|
||||
json.dumps(
|
||||
{
|
||||
"name": "test1",
|
||||
"matching_algorithm": MatchingModel.MATCH_AUTO,
|
||||
"owner": user2.id,
|
||||
"set_permissions": {
|
||||
"view": {
|
||||
"users": None,
|
||||
"groups": [group1.id],
|
||||
},
|
||||
"change": {
|
||||
"users": None,
|
||||
"groups": None,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
tag1 = Tag.objects.filter(name="test1").first()
|
||||
|
||||
self.assertEqual(tag1.owner, user2)
|
||||
self.assertIn("view_tag", get_perms(group1, tag1))
|
||||
|
||||
def test_api_set_doc_permissions(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to update doc permissions and owner
|
||||
WHEN:
|
||||
- owner is set
|
||||
- view > users is set & view > groups is set
|
||||
THEN:
|
||||
- Object permissions are set appropriately
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="test",
|
||||
mime_type="application/pdf",
|
||||
content="this is a document",
|
||||
)
|
||||
user1 = User.objects.create_superuser(username="user1")
|
||||
user2 = User.objects.create(username="user2")
|
||||
group1 = Group.objects.create(name="group1")
|
||||
|
||||
self.client.force_authenticate(user1)
|
||||
|
||||
response = self.client.patch(
|
||||
f"/api/documents/{doc.id}/",
|
||||
json.dumps(
|
||||
{
|
||||
"owner": user1.id,
|
||||
"set_permissions": {
|
||||
"view": {
|
||||
"users": [user2.id],
|
||||
"groups": [group1.id],
|
||||
},
|
||||
"change": {
|
||||
"users": None,
|
||||
"groups": None,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
doc = Document.objects.get(pk=doc.id)
|
||||
|
||||
self.assertEqual(doc.owner, user1)
|
||||
from guardian.core import ObjectPermissionChecker
|
||||
|
||||
checker = ObjectPermissionChecker(user2)
|
||||
self.assertTrue(checker.has_perm("view_document", doc))
|
||||
self.assertIn("view_document", get_perms(group1, doc))
|
||||
|
||||
def test_dynamic_permissions_fields(self):
|
||||
user1 = User.objects.create_user(username="user1")
|
||||
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
|
||||
user2 = User.objects.create_user(username="user2")
|
||||
|
||||
Document.objects.create(title="Test", content="content 1", checksum="1")
|
||||
doc2 = Document.objects.create(
|
||||
title="Test2",
|
||||
content="content 2",
|
||||
checksum="2",
|
||||
owner=user2,
|
||||
)
|
||||
doc3 = Document.objects.create(
|
||||
title="Test3",
|
||||
content="content 3",
|
||||
checksum="3",
|
||||
owner=user2,
|
||||
)
|
||||
|
||||
assign_perm("view_document", user1, doc2)
|
||||
assign_perm("view_document", user1, doc3)
|
||||
assign_perm("change_document", user1, doc3)
|
||||
|
||||
self.client.force_authenticate(user1)
|
||||
|
||||
response = self.client.get(
|
||||
"/api/documents/",
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
self.assertNotIn("permissions", resp_data["results"][0])
|
||||
self.assertIn("user_can_change", resp_data["results"][0])
|
||||
self.assertEqual(resp_data["results"][0]["user_can_change"], True) # doc1
|
||||
self.assertEqual(resp_data["results"][1]["user_can_change"], False) # doc2
|
||||
self.assertEqual(resp_data["results"][2]["user_can_change"], True) # doc3
|
||||
|
||||
response = self.client.get(
|
||||
"/api/documents/?full_perms=true",
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
self.assertIn("permissions", resp_data["results"][0])
|
||||
self.assertNotIn("user_can_change", resp_data["results"][0])
|
||||
|
||||
|
||||
class TestApiUser(DirectoriesMixin, APITestCase):
|
||||
ENDPOINT = "/api/users/"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_get_users(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Configured users
|
||||
WHEN:
|
||||
- API call is made to get users
|
||||
THEN:
|
||||
- Configured users are provided
|
||||
"""
|
||||
|
||||
user1 = User.objects.create(
|
||||
username="testuser",
|
||||
password="test",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["count"], 2)
|
||||
returned_user2 = response.data["results"][1]
|
||||
|
||||
self.assertEqual(returned_user2["username"], user1.username)
|
||||
self.assertEqual(returned_user2["password"], "**********")
|
||||
self.assertEqual(returned_user2["first_name"], user1.first_name)
|
||||
self.assertEqual(returned_user2["last_name"], user1.last_name)
|
||||
|
||||
def test_create_user(self):
|
||||
"""
|
||||
WHEN:
|
||||
- API request is made to add a user account
|
||||
THEN:
|
||||
- A new user account is created
|
||||
"""
|
||||
|
||||
user1 = {
|
||||
"username": "testuser",
|
||||
"password": "test",
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
data=user1,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
returned_user1 = User.objects.get(username="testuser")
|
||||
|
||||
self.assertEqual(returned_user1.username, user1["username"])
|
||||
self.assertEqual(returned_user1.first_name, user1["first_name"])
|
||||
self.assertEqual(returned_user1.last_name, user1["last_name"])
|
||||
|
||||
def test_delete_user(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing user account
|
||||
WHEN:
|
||||
- API request is made to delete a user account
|
||||
THEN:
|
||||
- Account is deleted
|
||||
"""
|
||||
|
||||
user1 = User.objects.create(
|
||||
username="testuser",
|
||||
password="test",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
)
|
||||
|
||||
nUsers = User.objects.count()
|
||||
|
||||
response = self.client.delete(
|
||||
f"{self.ENDPOINT}{user1.pk}/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
self.assertEqual(User.objects.count(), nUsers - 1)
|
||||
|
||||
def test_update_user(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing user accounts
|
||||
WHEN:
|
||||
- API request is made to update user account
|
||||
THEN:
|
||||
- The user account is updated, password only updated if not '****'
|
||||
"""
|
||||
|
||||
user1 = User.objects.create(
|
||||
username="testuser",
|
||||
password="test",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
)
|
||||
|
||||
initial_password = user1.password
|
||||
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}{user1.pk}/",
|
||||
data={
|
||||
"first_name": "Updated Name 1",
|
||||
"password": "******",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
returned_user1 = User.objects.get(pk=user1.pk)
|
||||
self.assertEqual(returned_user1.first_name, "Updated Name 1")
|
||||
self.assertEqual(returned_user1.password, initial_password)
|
||||
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}{user1.pk}/",
|
||||
data={
|
||||
"first_name": "Updated Name 2",
|
||||
"password": "123xyz",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
returned_user2 = User.objects.get(pk=user1.pk)
|
||||
self.assertEqual(returned_user2.first_name, "Updated Name 2")
|
||||
self.assertNotEqual(returned_user2.password, initial_password)
|
||||
|
||||
|
||||
class TestApiGroup(DirectoriesMixin, APITestCase):
|
||||
ENDPOINT = "/api/groups/"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_get_groups(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Configured groups
|
||||
WHEN:
|
||||
- API call is made to get groups
|
||||
THEN:
|
||||
- Configured groups are provided
|
||||
"""
|
||||
|
||||
group1 = Group.objects.create(
|
||||
name="Test Group",
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["count"], 1)
|
||||
returned_group1 = response.data["results"][0]
|
||||
|
||||
self.assertEqual(returned_group1["name"], group1.name)
|
||||
|
||||
def test_create_group(self):
|
||||
"""
|
||||
WHEN:
|
||||
- API request is made to add a group
|
||||
THEN:
|
||||
- A new group is created
|
||||
"""
|
||||
|
||||
group1 = {
|
||||
"name": "Test Group",
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
data=group1,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
returned_group1 = Group.objects.get(name="Test Group")
|
||||
|
||||
self.assertEqual(returned_group1.name, group1["name"])
|
||||
|
||||
def test_delete_group(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing group
|
||||
WHEN:
|
||||
- API request is made to delete a group
|
||||
THEN:
|
||||
- Group is deleted
|
||||
"""
|
||||
|
||||
group1 = Group.objects.create(
|
||||
name="Test Group",
|
||||
)
|
||||
|
||||
response = self.client.delete(
|
||||
f"{self.ENDPOINT}{group1.pk}/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
self.assertEqual(len(Group.objects.all()), 0)
|
||||
|
||||
def test_update_group(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing groups
|
||||
WHEN:
|
||||
- API request is made to update group
|
||||
THEN:
|
||||
- The group is updated
|
||||
"""
|
||||
|
||||
group1 = Group.objects.create(
|
||||
name="Test Group",
|
||||
)
|
||||
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}{group1.pk}/",
|
||||
data={
|
||||
"name": "Updated Name 1",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
returned_group1 = Group.objects.get(pk=group1.pk)
|
||||
self.assertEqual(returned_group1.name, "Updated Name 1")
|
||||
|
||||
|
||||
class TestBulkEditObjectPermissions(APITestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
self.t1 = Tag.objects.create(name="t1")
|
||||
self.t2 = Tag.objects.create(name="t2")
|
||||
self.c1 = Correspondent.objects.create(name="c1")
|
||||
self.dt1 = DocumentType.objects.create(name="dt1")
|
||||
self.sp1 = StoragePath.objects.create(name="sp1")
|
||||
self.user1 = User.objects.create(username="user1")
|
||||
self.user2 = User.objects.create(username="user2")
|
||||
self.user3 = User.objects.create(username="user3")
|
||||
|
||||
def test_bulk_object_set_permissions(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing objects
|
||||
WHEN:
|
||||
- bulk_edit_object_perms API endpoint is called
|
||||
THEN:
|
||||
- Permissions and / or owner are changed
|
||||
"""
|
||||
permissions = {
|
||||
"view": {
|
||||
"users": [self.user1.id, self.user2.id],
|
||||
"groups": [],
|
||||
},
|
||||
"change": {
|
||||
"users": [self.user1.id],
|
||||
"groups": [],
|
||||
},
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id],
|
||||
"object_type": "tags",
|
||||
"permissions": permissions,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn(self.user1, get_users_with_perms(self.t1))
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.c1.id],
|
||||
"object_type": "correspondents",
|
||||
"permissions": permissions,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn(self.user1, get_users_with_perms(self.c1))
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.dt1.id],
|
||||
"object_type": "document_types",
|
||||
"permissions": permissions,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn(self.user1, get_users_with_perms(self.dt1))
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.sp1.id],
|
||||
"object_type": "storage_paths",
|
||||
"permissions": permissions,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn(self.user1, get_users_with_perms(self.sp1))
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id],
|
||||
"object_type": "tags",
|
||||
"owner": self.user3.id,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(Tag.objects.get(pk=self.t2.id).owner, self.user3)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.sp1.id],
|
||||
"object_type": "storage_paths",
|
||||
"owner": self.user3.id,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(StoragePath.objects.get(pk=self.sp1.id).owner, self.user3)
|
||||
|
||||
def test_bulk_edit_object_permissions_insufficient_perms(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Objects owned by user other than logged in user
|
||||
WHEN:
|
||||
- bulk_edit_object_perms API endpoint is called
|
||||
THEN:
|
||||
- User is not able to change permissions
|
||||
"""
|
||||
self.t1.owner = User.objects.get(username="temp_admin")
|
||||
self.t1.save()
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id],
|
||||
"object_type": "tags",
|
||||
"owner": self.user1.id,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.content, b"Insufficient permissions")
|
||||
|
||||
def test_bulk_edit_object_permissions_validation(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing objects
|
||||
WHEN:
|
||||
- bulk_edit_object_perms API endpoint is called with invalid params
|
||||
THEN:
|
||||
- Validation fails
|
||||
"""
|
||||
# not a list
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": self.t1.id,
|
||||
"object_type": "tags",
|
||||
"owner": self.user1.id,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# not a list of ints
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": ["one"],
|
||||
"object_type": "tags",
|
||||
"owner": self.user1.id,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# duplicates
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [self.t1.id, self.t2.id, self.t1.id],
|
||||
"object_type": "tags",
|
||||
"owner": self.user1.id,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# not a valid object type
|
||||
response = self.client.post(
|
||||
"/api/bulk_edit_object_perms/",
|
||||
json.dumps(
|
||||
{
|
||||
"objects": [1],
|
||||
"object_type": "madeup",
|
||||
"owner": self.user1.id,
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
123
src/documents/tests/test_api_remote_version.py
Normal file
123
src/documents/tests/test_api_remote_version.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import json
|
||||
import urllib.request
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
from paperless import version
|
||||
|
||||
|
||||
class TestApiRemoteVersion(DirectoriesMixin, APITestCase):
|
||||
ENDPOINT = "/api/remote_version/"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
@mock.patch("urllib.request.urlopen")
|
||||
def test_remote_version_enabled_no_update_prefix(self, urlopen_mock):
|
||||
cm = MagicMock()
|
||||
cm.getcode.return_value = status.HTTP_200_OK
|
||||
cm.read.return_value = json.dumps({"tag_name": "ngx-1.6.0"}).encode()
|
||||
cm.__enter__.return_value = cm
|
||||
urlopen_mock.return_value = cm
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(
|
||||
response.data,
|
||||
{
|
||||
"version": "1.6.0",
|
||||
"update_available": False,
|
||||
},
|
||||
)
|
||||
|
||||
@mock.patch("urllib.request.urlopen")
|
||||
def test_remote_version_enabled_no_update_no_prefix(self, urlopen_mock):
|
||||
cm = MagicMock()
|
||||
cm.getcode.return_value = status.HTTP_200_OK
|
||||
cm.read.return_value = json.dumps(
|
||||
{"tag_name": version.__full_version_str__},
|
||||
).encode()
|
||||
cm.__enter__.return_value = cm
|
||||
urlopen_mock.return_value = cm
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(
|
||||
response.data,
|
||||
{
|
||||
"version": version.__full_version_str__,
|
||||
"update_available": False,
|
||||
},
|
||||
)
|
||||
|
||||
@mock.patch("urllib.request.urlopen")
|
||||
def test_remote_version_enabled_update(self, urlopen_mock):
|
||||
new_version = (
|
||||
version.__version__[0],
|
||||
version.__version__[1],
|
||||
version.__version__[2] + 1,
|
||||
)
|
||||
new_version_str = ".".join(map(str, new_version))
|
||||
|
||||
cm = MagicMock()
|
||||
cm.getcode.return_value = status.HTTP_200_OK
|
||||
cm.read.return_value = json.dumps(
|
||||
{"tag_name": new_version_str},
|
||||
).encode()
|
||||
cm.__enter__.return_value = cm
|
||||
urlopen_mock.return_value = cm
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(
|
||||
response.data,
|
||||
{
|
||||
"version": new_version_str,
|
||||
"update_available": True,
|
||||
},
|
||||
)
|
||||
|
||||
@mock.patch("urllib.request.urlopen")
|
||||
def test_remote_version_bad_json(self, urlopen_mock):
|
||||
cm = MagicMock()
|
||||
cm.getcode.return_value = status.HTTP_200_OK
|
||||
cm.read.return_value = b'{ "blah":'
|
||||
cm.__enter__.return_value = cm
|
||||
urlopen_mock.return_value = cm
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(
|
||||
response.data,
|
||||
{
|
||||
"version": "0.0.0",
|
||||
"update_available": False,
|
||||
},
|
||||
)
|
||||
|
||||
@mock.patch("urllib.request.urlopen")
|
||||
def test_remote_version_exception(self, urlopen_mock):
|
||||
cm = MagicMock()
|
||||
cm.getcode.return_value = status.HTTP_200_OK
|
||||
cm.read.side_effect = urllib.error.URLError("an error")
|
||||
cm.__enter__.return_value = cm
|
||||
urlopen_mock.return_value = cm
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(
|
||||
response.data,
|
||||
{
|
||||
"version": "0.0.0",
|
||||
"update_available": False,
|
||||
},
|
||||
)
|
||||
1077
src/documents/tests/test_api_search.py
Normal file
1077
src/documents/tests/test_api_search.py
Normal file
File diff suppressed because it is too large
Load Diff
240
src/documents/tests/test_api_tasks.py
Normal file
240
src/documents/tests/test_api_tasks.py
Normal file
@@ -0,0 +1,240 @@
|
||||
import uuid
|
||||
|
||||
import celery
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.models import PaperlessTask
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
|
||||
|
||||
class TestTasks(DirectoriesMixin, APITestCase):
|
||||
ENDPOINT = "/api/tasks/"
|
||||
ENDPOINT_ACKNOWLEDGE = "/api/acknowledge_tasks/"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.user = User.objects.create_superuser(username="temp_admin")
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_get_tasks(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Attempted celery tasks
|
||||
WHEN:
|
||||
- API call is made to get tasks
|
||||
THEN:
|
||||
- Attempting and pending tasks are serialized and provided
|
||||
"""
|
||||
|
||||
task1 = PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_one.pdf",
|
||||
)
|
||||
|
||||
task2 = PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_two.pdf",
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
returned_task1 = response.data[1]
|
||||
returned_task2 = response.data[0]
|
||||
|
||||
self.assertEqual(returned_task1["task_id"], task1.task_id)
|
||||
self.assertEqual(returned_task1["status"], celery.states.PENDING)
|
||||
self.assertEqual(returned_task1["task_file_name"], task1.task_file_name)
|
||||
|
||||
self.assertEqual(returned_task2["task_id"], task2.task_id)
|
||||
self.assertEqual(returned_task2["status"], celery.states.PENDING)
|
||||
self.assertEqual(returned_task2["task_file_name"], task2.task_file_name)
|
||||
|
||||
def test_get_single_task_status(self):
|
||||
"""
|
||||
GIVEN
|
||||
- Query parameter for a valid task ID
|
||||
WHEN:
|
||||
- API call is made to get task status
|
||||
THEN:
|
||||
- Single task data is returned
|
||||
"""
|
||||
|
||||
id1 = str(uuid.uuid4())
|
||||
task1 = PaperlessTask.objects.create(
|
||||
task_id=id1,
|
||||
task_file_name="task_one.pdf",
|
||||
)
|
||||
|
||||
_ = PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_two.pdf",
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT + f"?task_id={id1}")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
returned_task1 = response.data[0]
|
||||
|
||||
self.assertEqual(returned_task1["task_id"], task1.task_id)
|
||||
|
||||
def test_get_single_task_status_not_valid(self):
|
||||
"""
|
||||
GIVEN
|
||||
- Query parameter for a non-existent task ID
|
||||
WHEN:
|
||||
- API call is made to get task status
|
||||
THEN:
|
||||
- No task data is returned
|
||||
"""
|
||||
PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_one.pdf",
|
||||
)
|
||||
|
||||
_ = PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_two.pdf",
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT + "?task_id=bad-task-id")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
def test_acknowledge_tasks(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Attempted celery tasks
|
||||
WHEN:
|
||||
- API call is made to get mark task as acknowledged
|
||||
THEN:
|
||||
- Task is marked as acknowledged
|
||||
"""
|
||||
task = PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_one.pdf",
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT_ACKNOWLEDGE,
|
||||
{"tasks": [task.id]},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
def test_task_result_no_error(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- A celery task completed without error
|
||||
WHEN:
|
||||
- API call is made to get tasks
|
||||
THEN:
|
||||
- The returned data includes the task result
|
||||
"""
|
||||
PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_one.pdf",
|
||||
status=celery.states.SUCCESS,
|
||||
result="Success. New document id 1 created",
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
returned_data = response.data[0]
|
||||
|
||||
self.assertEqual(returned_data["result"], "Success. New document id 1 created")
|
||||
self.assertEqual(returned_data["related_document"], "1")
|
||||
|
||||
def test_task_result_with_error(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- A celery task completed with an exception
|
||||
WHEN:
|
||||
- API call is made to get tasks
|
||||
THEN:
|
||||
- The returned result is the exception info
|
||||
"""
|
||||
PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="task_one.pdf",
|
||||
status=celery.states.FAILURE,
|
||||
result="test.pdf: Not consuming test.pdf: It is a duplicate.",
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
returned_data = response.data[0]
|
||||
|
||||
self.assertEqual(
|
||||
returned_data["result"],
|
||||
"test.pdf: Not consuming test.pdf: It is a duplicate.",
|
||||
)
|
||||
|
||||
def test_task_name_webui(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Attempted celery task
|
||||
- Task was created through the webui
|
||||
WHEN:
|
||||
- API call is made to get tasks
|
||||
THEN:
|
||||
- Returned data include the filename
|
||||
"""
|
||||
PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="test.pdf",
|
||||
task_name="documents.tasks.some_task",
|
||||
status=celery.states.SUCCESS,
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
returned_data = response.data[0]
|
||||
|
||||
self.assertEqual(returned_data["task_file_name"], "test.pdf")
|
||||
|
||||
def test_task_name_consume_folder(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Attempted celery task
|
||||
- Task was created through the consume folder
|
||||
WHEN:
|
||||
- API call is made to get tasks
|
||||
THEN:
|
||||
- Returned data include the filename
|
||||
"""
|
||||
PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="anothertest.pdf",
|
||||
task_name="documents.tasks.some_task",
|
||||
status=celery.states.SUCCESS,
|
||||
)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
returned_data = response.data[0]
|
||||
|
||||
self.assertEqual(returned_data["task_file_name"], "anothertest.pdf")
|
||||
65
src/documents/tests/test_api_uisettings.py
Normal file
65
src/documents/tests/test_api_uisettings.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import json
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
|
||||
|
||||
class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
||||
ENDPOINT = "/api/ui_settings/"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.test_user = User.objects.create_superuser(username="test")
|
||||
self.test_user.first_name = "Test"
|
||||
self.test_user.last_name = "User"
|
||||
self.test_user.save()
|
||||
self.client.force_authenticate(user=self.test_user)
|
||||
|
||||
def test_api_get_ui_settings(self):
|
||||
response = self.client.get(self.ENDPOINT, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertDictEqual(
|
||||
response.data["user"],
|
||||
{
|
||||
"id": self.test_user.id,
|
||||
"username": self.test_user.username,
|
||||
"is_superuser": True,
|
||||
"groups": [],
|
||||
"first_name": self.test_user.first_name,
|
||||
"last_name": self.test_user.last_name,
|
||||
},
|
||||
)
|
||||
self.assertDictEqual(
|
||||
response.data["settings"],
|
||||
{
|
||||
"update_checking": {
|
||||
"backend_setting": "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_api_set_ui_settings(self):
|
||||
settings = {
|
||||
"settings": {
|
||||
"dark_mode": {
|
||||
"enabled": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(settings),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
ui_settings = self.test_user.ui_settings
|
||||
self.assertDictEqual(
|
||||
ui_settings.settings,
|
||||
settings["settings"],
|
||||
)
|
||||
@@ -1347,7 +1347,7 @@ class ConsumptionTemplateViewSet(ModelViewSet):
|
||||
|
||||
model = ConsumptionTemplate
|
||||
|
||||
queryset = ConsumptionTemplate.objects.all().order_by("name")
|
||||
queryset = ConsumptionTemplate.objects.all().order_by("order")
|
||||
|
||||
|
||||
class CustomFieldViewSet(ModelViewSet):
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
|
||||
"PO-Revision-Date: 2023-12-05 16:27\n"
|
||||
"PO-Revision-Date: 2023-12-08 12:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Catalan\n"
|
||||
"Language: ca_ES\n"
|
||||
@@ -581,7 +581,7 @@ msgstr "Monetari"
|
||||
|
||||
#: documents/models.py:759
|
||||
msgid "Document Link"
|
||||
msgstr ""
|
||||
msgstr "Enllaç Document"
|
||||
|
||||
#: documents/models.py:771
|
||||
msgid "data type"
|
||||
@@ -685,7 +685,7 @@ msgstr "dóna permissos d'edició a aquests grups"
|
||||
|
||||
#: documents/models.py:1019
|
||||
msgid "assign these custom fields"
|
||||
msgstr ""
|
||||
msgstr "assigna aquests camps personalitzats"
|
||||
|
||||
#: documents/models.py:1023
|
||||
msgid "consumption template"
|
||||
@@ -1172,15 +1172,15 @@ msgstr "filtra cos"
|
||||
|
||||
#: paperless_mail/models.py:143
|
||||
msgid "filter attachment filename inclusive"
|
||||
msgstr ""
|
||||
msgstr "filtra nom del fitxer adjunt incloent"
|
||||
|
||||
#: paperless_mail/models.py:155
|
||||
msgid "filter attachment filename exclusive"
|
||||
msgstr ""
|
||||
msgstr "filtra nom del fitxer adjunt excloent"
|
||||
|
||||
#: paperless_mail/models.py:160
|
||||
msgid "Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
|
||||
msgstr ""
|
||||
msgstr "No consumeixis documents que coincideixin aquest nom d'arxiu especificat. Comodins com *.pdf o *factura* permessos. Cas insensitiu."
|
||||
|
||||
#: paperless_mail/models.py:167
|
||||
msgid "maximum age"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
|
||||
"PO-Revision-Date: 2023-12-05 16:27\n"
|
||||
"PO-Revision-Date: 2023-12-12 00:24\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -581,7 +581,7 @@ msgstr "Monétaire"
|
||||
|
||||
#: documents/models.py:759
|
||||
msgid "Document Link"
|
||||
msgstr ""
|
||||
msgstr "Lien du Document"
|
||||
|
||||
#: documents/models.py:771
|
||||
msgid "data type"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
|
||||
"PO-Revision-Date: 2023-12-05 16:27\n"
|
||||
"PO-Revision-Date: 2023-12-09 00:23\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hebrew\n"
|
||||
"Language: he_IL\n"
|
||||
@@ -533,7 +533,7 @@ msgstr ""
|
||||
|
||||
#: documents/models.py:689
|
||||
msgid "Original"
|
||||
msgstr ""
|
||||
msgstr "מקור"
|
||||
|
||||
#: documents/models.py:700
|
||||
msgid "expiration"
|
||||
@@ -545,11 +545,11 @@ msgstr ""
|
||||
|
||||
#: documents/models.py:739
|
||||
msgid "share link"
|
||||
msgstr ""
|
||||
msgstr "שתף קישור"
|
||||
|
||||
#: documents/models.py:740
|
||||
msgid "share links"
|
||||
msgstr ""
|
||||
msgstr "שתף קישורים"
|
||||
|
||||
#: documents/models.py:752
|
||||
msgid "String"
|
||||
@@ -557,11 +557,11 @@ msgstr ""
|
||||
|
||||
#: documents/models.py:753
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
msgstr "כתובת URL"
|
||||
|
||||
#: documents/models.py:754
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
msgstr "תאריך"
|
||||
|
||||
#: documents/models.py:755
|
||||
msgid "Boolean"
|
||||
@@ -589,11 +589,11 @@ msgstr ""
|
||||
|
||||
#: documents/models.py:779
|
||||
msgid "custom field"
|
||||
msgstr ""
|
||||
msgstr "שדה מותאם אישית"
|
||||
|
||||
#: documents/models.py:780
|
||||
msgid "custom fields"
|
||||
msgstr ""
|
||||
msgstr "שדות מותאמים אישית"
|
||||
|
||||
#: documents/models.py:842
|
||||
msgid "custom field instance"
|
||||
@@ -771,7 +771,7 @@ msgstr "התחבר"
|
||||
|
||||
#: documents/templates/registration/login.html:70
|
||||
msgid "Forgot your password?"
|
||||
msgstr ""
|
||||
msgstr "שכחת את הסיסמה?"
|
||||
|
||||
#: documents/templates/registration/password_reset_complete.html:14
|
||||
msgid "Paperless-ngx reset password complete"
|
||||
@@ -779,7 +779,7 @@ msgstr ""
|
||||
|
||||
#: documents/templates/registration/password_reset_complete.html:40
|
||||
msgid "Password reset complete."
|
||||
msgstr ""
|
||||
msgstr "איפוס סיסמה הושלם."
|
||||
|
||||
#: documents/templates/registration/password_reset_complete.html:42
|
||||
#, python-format
|
||||
@@ -820,11 +820,11 @@ msgstr ""
|
||||
|
||||
#: documents/templates/registration/password_reset_done.html:40
|
||||
msgid "Check your inbox."
|
||||
msgstr ""
|
||||
msgstr "בדוק את תיבת הדואר שלך."
|
||||
|
||||
#: documents/templates/registration/password_reset_done.html:41
|
||||
msgid "We've emailed you instructions for setting your password. You should receive the email shortly!"
|
||||
msgstr ""
|
||||
msgstr "נשלח מייל עם הוראות להגדרת הסיסמה שלך. אתה צריך לקבל מייל בקרוב!"
|
||||
|
||||
#: documents/templates/registration/password_reset_form.html:14
|
||||
msgid "Paperless-ngx reset password request"
|
||||
@@ -836,7 +836,7 @@ msgstr ""
|
||||
|
||||
#: documents/templates/registration/password_reset_form.html:44
|
||||
msgid "An error occurred. Please try again."
|
||||
msgstr ""
|
||||
msgstr "אירעה שגיאה. נא לנסות שוב."
|
||||
|
||||
#: documents/templates/registration/password_reset_form.html:47
|
||||
msgid "Email"
|
||||
@@ -844,7 +844,7 @@ msgstr "דוא\"ל"
|
||||
|
||||
#: documents/templates/registration/password_reset_form.html:54
|
||||
msgid "Send me instructions!"
|
||||
msgstr ""
|
||||
msgstr "שלח לי את ההוראות!"
|
||||
|
||||
#: paperless/apps.py:10
|
||||
msgid "Paperless"
|
||||
@@ -868,7 +868,7 @@ msgstr "בלרוסית"
|
||||
|
||||
#: paperless/settings.py:590
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
msgstr "בולגרית"
|
||||
|
||||
#: paperless/settings.py:591
|
||||
msgid "Catalan"
|
||||
@@ -908,7 +908,7 @@ msgstr "צרפתית"
|
||||
|
||||
#: paperless/settings.py:600
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
msgstr "הונגרית"
|
||||
|
||||
#: paperless/settings.py:601
|
||||
msgid "Italian"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
|
||||
"PO-Revision-Date: 2023-12-05 16:27\n"
|
||||
"PO-Revision-Date: 2023-12-11 00:25\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Polish\n"
|
||||
"Language: pl_PL\n"
|
||||
@@ -665,7 +665,7 @@ msgstr ""
|
||||
|
||||
#: documents/models.py:984
|
||||
msgid "assign this owner"
|
||||
msgstr ""
|
||||
msgstr "przypisz tego właściciela"
|
||||
|
||||
#: documents/models.py:991
|
||||
msgid "grant view permissions to these users"
|
||||
@@ -751,11 +751,11 @@ msgstr "Twoja nazwa użytkownika i hasło nie są zgodne. Spróbuj ponownie."
|
||||
|
||||
#: documents/templates/registration/login.html:48
|
||||
msgid "Share link was not found."
|
||||
msgstr ""
|
||||
msgstr "Link nie został odnaleziony."
|
||||
|
||||
#: documents/templates/registration/login.html:52
|
||||
msgid "Share link has expired."
|
||||
msgstr ""
|
||||
msgstr "Link utracił ważność."
|
||||
|
||||
#: documents/templates/registration/login.html:55
|
||||
msgid "Username"
|
||||
@@ -779,7 +779,7 @@ msgstr ""
|
||||
|
||||
#: documents/templates/registration/password_reset_complete.html:40
|
||||
msgid "Password reset complete."
|
||||
msgstr ""
|
||||
msgstr "Resetowanie hasła zakończone."
|
||||
|
||||
#: documents/templates/registration/password_reset_complete.html:42
|
||||
#, python-format
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
|
||||
"PO-Revision-Date: 2023-12-06 12:09\n"
|
||||
"PO-Revision-Date: 2023-12-10 00:26\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese, Brazilian\n"
|
||||
"Language: pt_BR\n"
|
||||
@@ -385,27 +385,27 @@ msgstr "caminho de armazenamento é"
|
||||
|
||||
#: documents/models.py:448
|
||||
msgid "has correspondent in"
|
||||
msgstr ""
|
||||
msgstr "tem correspondente em"
|
||||
|
||||
#: documents/models.py:449
|
||||
msgid "does not have correspondent in"
|
||||
msgstr ""
|
||||
msgstr "não tem correspondente em"
|
||||
|
||||
#: documents/models.py:450
|
||||
msgid "has document type in"
|
||||
msgstr ""
|
||||
msgstr "possui categoria de documento em"
|
||||
|
||||
#: documents/models.py:451
|
||||
msgid "does not have document type in"
|
||||
msgstr ""
|
||||
msgstr "não possui categoria de documento em"
|
||||
|
||||
#: documents/models.py:452
|
||||
msgid "has storage path in"
|
||||
msgstr ""
|
||||
msgstr "possui caminho de armazenamento em"
|
||||
|
||||
#: documents/models.py:453
|
||||
msgid "does not have storage path in"
|
||||
msgstr ""
|
||||
msgstr "não possui caminho de armazenamento em"
|
||||
|
||||
#: documents/models.py:454
|
||||
msgid "owner is"
|
||||
@@ -513,7 +513,7 @@ msgstr "Os dados retornados pela tarefa"
|
||||
|
||||
#: documents/models.py:650
|
||||
msgid "Note for the document"
|
||||
msgstr ""
|
||||
msgstr "Nota para o documento"
|
||||
|
||||
#: documents/models.py:674
|
||||
msgid "user"
|
||||
@@ -521,7 +521,7 @@ msgstr "usuário"
|
||||
|
||||
#: documents/models.py:679
|
||||
msgid "note"
|
||||
msgstr ""
|
||||
msgstr "nota"
|
||||
|
||||
#: documents/models.py:680
|
||||
msgid "notes"
|
||||
@@ -529,15 +529,15 @@ msgstr "notas"
|
||||
|
||||
#: documents/models.py:688
|
||||
msgid "Archive"
|
||||
msgstr ""
|
||||
msgstr "Arquivo"
|
||||
|
||||
#: documents/models.py:689
|
||||
msgid "Original"
|
||||
msgstr ""
|
||||
msgstr "Original"
|
||||
|
||||
#: documents/models.py:700
|
||||
msgid "expiration"
|
||||
msgstr ""
|
||||
msgstr "validade"
|
||||
|
||||
#: documents/models.py:707
|
||||
msgid "slug"
|
||||
@@ -553,15 +553,15 @@ msgstr "compartilhar link"
|
||||
|
||||
#: documents/models.py:752
|
||||
msgid "String"
|
||||
msgstr ""
|
||||
msgstr "Texto"
|
||||
|
||||
#: documents/models.py:753
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
msgstr "URL"
|
||||
|
||||
#: documents/models.py:754
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
msgstr "Data"
|
||||
|
||||
#: documents/models.py:755
|
||||
msgid "Boolean"
|
||||
@@ -573,7 +573,7 @@ msgstr "Número inteiro"
|
||||
|
||||
#: documents/models.py:757
|
||||
msgid "Float"
|
||||
msgstr ""
|
||||
msgstr "Ponto flutuante"
|
||||
|
||||
#: documents/models.py:758
|
||||
msgid "Monetary"
|
||||
@@ -581,15 +581,15 @@ msgstr "Unidade monetária"
|
||||
|
||||
#: documents/models.py:759
|
||||
msgid "Document Link"
|
||||
msgstr ""
|
||||
msgstr "Link do documento"
|
||||
|
||||
#: documents/models.py:771
|
||||
msgid "data type"
|
||||
msgstr ""
|
||||
msgstr "tipo de dados"
|
||||
|
||||
#: documents/models.py:779
|
||||
msgid "custom field"
|
||||
msgstr ""
|
||||
msgstr "campo personalizado"
|
||||
|
||||
#: documents/models.py:780
|
||||
msgid "custom fields"
|
||||
@@ -597,11 +597,11 @@ msgstr "campos personalizados"
|
||||
|
||||
#: documents/models.py:842
|
||||
msgid "custom field instance"
|
||||
msgstr ""
|
||||
msgstr "instância de campo personalizado"
|
||||
|
||||
#: documents/models.py:843
|
||||
msgid "custom field instances"
|
||||
msgstr ""
|
||||
msgstr "instâncias de campo personalizadas"
|
||||
|
||||
#: documents/models.py:891
|
||||
msgid "Consume Folder"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
|
||||
"PO-Revision-Date: 2023-12-05 16:27\n"
|
||||
"PO-Revision-Date: 2023-12-10 00:26\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Language: pt_PT\n"
|
||||
@@ -385,23 +385,23 @@ msgstr "local de armazenamento é"
|
||||
|
||||
#: documents/models.py:448
|
||||
msgid "has correspondent in"
|
||||
msgstr ""
|
||||
msgstr "tem correspondente em"
|
||||
|
||||
#: documents/models.py:449
|
||||
msgid "does not have correspondent in"
|
||||
msgstr ""
|
||||
msgstr "não tem correspondente em"
|
||||
|
||||
#: documents/models.py:450
|
||||
msgid "has document type in"
|
||||
msgstr ""
|
||||
msgstr "tem categoria de documento em"
|
||||
|
||||
#: documents/models.py:451
|
||||
msgid "does not have document type in"
|
||||
msgstr ""
|
||||
msgstr "não tem categoria de documento em"
|
||||
|
||||
#: documents/models.py:452
|
||||
msgid "has storage path in"
|
||||
msgstr ""
|
||||
msgstr "tem caminho de armazenamento em"
|
||||
|
||||
#: documents/models.py:453
|
||||
msgid "does not have storage path in"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-12-05 08:26-0800\n"
|
||||
"PO-Revision-Date: 2023-12-05 16:27\n"
|
||||
"PO-Revision-Date: 2023-12-10 00:26\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Serbian (Latin)\n"
|
||||
"Language: sr_CS\n"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Final
|
||||
|
||||
__version__: Final[tuple[int, int, int]] = (2, 1, 1)
|
||||
__version__: Final[tuple[int, int, int]] = (2, 1, 2)
|
||||
# Version string like X.Y.Z
|
||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
||||
# Version string like X.Y
|
||||
|
||||
@@ -8,6 +8,7 @@ import traceback
|
||||
from datetime import date
|
||||
from datetime import timedelta
|
||||
from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
@@ -703,12 +704,15 @@ class MailAccountHandler(LoggingMixin):
|
||||
|
||||
if is_mime_type_supported(mime_type):
|
||||
os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
|
||||
_, temp_filename = tempfile.mkstemp(
|
||||
prefix="paperless-mail-",
|
||||
dir=settings.SCRATCH_DIR,
|
||||
|
||||
temp_dir = Path(
|
||||
tempfile.mkdtemp(
|
||||
prefix="paperless-mail-",
|
||||
dir=settings.SCRATCH_DIR,
|
||||
),
|
||||
)
|
||||
with open(temp_filename, "wb") as f:
|
||||
f.write(att.payload)
|
||||
temp_filename = temp_dir / pathvalidate.sanitize_filename(att.filename)
|
||||
temp_filename.write_bytes(att.payload)
|
||||
|
||||
self.log.info(
|
||||
f"Rule {rule}: "
|
||||
|
||||
@@ -1271,7 +1271,10 @@ class TestMail(
|
||||
self.assertEqual(len(self.bogus_mailbox.fetch("UNSEEN", False)), 0)
|
||||
self.assertEqual(len(self.bogus_mailbox.messages), 3)
|
||||
|
||||
def assert_queue_consumption_tasks_call_args(self, expected_call_args: list):
|
||||
def assert_queue_consumption_tasks_call_args(
|
||||
self,
|
||||
expected_call_args: list[list[dict[str, str]]],
|
||||
):
|
||||
"""
|
||||
Verifies that queue_consumption_tasks has been called with the expected arguments.
|
||||
|
||||
@@ -1283,7 +1286,7 @@ class TestMail(
|
||||
|
||||
"""
|
||||
|
||||
# assert number of calls to queue_consumption_tasks mathc
|
||||
# assert number of calls to queue_consumption_tasks match
|
||||
self.assertEqual(
|
||||
len(self._queue_consumption_tasks_mock.call_args_list),
|
||||
len(expected_call_args),
|
||||
|
||||
Reference in New Issue
Block a user