mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-11 10:00:48 -05:00
Merge remote-tracking branch 'origin/dev'
This commit is contained in:
commit
8165071edf
@ -589,6 +589,12 @@ case, Paperless will remove the staging copy as well as the scan, and give you a
|
|||||||
message asking you to restart the process from scratch, by scanning the odd pages again,
|
message asking you to restart the process from scratch, by scanning the odd pages again,
|
||||||
followed by the even pages.
|
followed by the even pages.
|
||||||
|
|
||||||
|
It's important that the scan files get consumed in the correct order, and one at a time.
|
||||||
|
You therefore need to make sure that Paperless is running while you upload the files into
|
||||||
|
the directory; and if you're using [polling](/configuration#polling), make sure that
|
||||||
|
`CONSUMER_POLLING` is set to a value lower than it takes for the second scan to appear,
|
||||||
|
like 5-10 or even lower.
|
||||||
|
|
||||||
Another thing that might happen is that you start a double sided scan, but then forget
|
Another thing that might happen is that you start a double sided scan, but then forget
|
||||||
to upload the second file. To avoid collating the wrong documents if you then come back
|
to upload the second file. To avoid collating the wrong documents if you then come back
|
||||||
a day later to scan a new double-sided document, Paperless will only keep an "odd numbered
|
a day later to scan a new double-sided document, Paperless will only keep an "odd numbered
|
||||||
@ -597,11 +603,11 @@ scan a completely new "odd numbered pages" one. The old staging file will get di
|
|||||||
|
|
||||||
### Interaction with "subdirs as tags"
|
### Interaction with "subdirs as tags"
|
||||||
|
|
||||||
The collation feature can be used together with the "subdirs as tags" feature (but this is not
|
The collation feature can be used together with the [subdirs as tags](/configuration#consume_config)
|
||||||
a requirement). Just create a correctly named double-sided subdir in the hierachy and upload
|
feature (but this is not a requirement). Just create a correctly named double-sided subdir
|
||||||
your scans there. For example, both `double-sided/foo/bar` as well as `foo/bar/double-sided` will
|
in the hierachy and upload your scans there. For example, both `double-sided/foo/bar` as
|
||||||
cause the collated document to be treated as if it were uploaded into `foo/bar` and receive both
|
well as `foo/bar/double-sided` will cause the collated document to be treated as if it
|
||||||
`foo` and `bar` tags, but not `double-sided`.
|
were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.
|
||||||
|
|
||||||
### Interaction with document splitting
|
### Interaction with document splitting
|
||||||
|
|
||||||
|
@ -35,6 +35,12 @@ matcher.
|
|||||||
|
|
||||||
Defaults to `redis://localhost:6379`.
|
Defaults to `redis://localhost:6379`.
|
||||||
|
|
||||||
|
`PAPERLESS_REDIS_PREFIX=<prefix>`
|
||||||
|
|
||||||
|
: Prefix to be used in Redis for keys and channels. Useful for sharing one Redis server among multiple Paperless instances.
|
||||||
|
|
||||||
|
Defaults to no prefix.
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
`PAPERLESS_DBENGINE=<engine_name>`
|
`PAPERLESS_DBENGINE=<engine_name>`
|
||||||
@ -495,6 +501,19 @@ HTTP header/value expected by Django, eg `'["HTTP_X_FORWARDED_PROTO", "https"]'`
|
|||||||
Settings this value has security implications. Read the Django documentation
|
Settings this value has security implications. Read the Django documentation
|
||||||
and be sure you understand its usage before setting it.
|
and be sure you understand its usage before setting it.
|
||||||
|
|
||||||
|
`PAPERLESS_EMAIL_CERTIFICATE_FILE=<path>`
|
||||||
|
|
||||||
|
: Configures an additional SSL certificate file containing a [certificate](https://docs.python.org/3/library/ssl.html#certificates)
|
||||||
|
or certificate chain which should be trusted for validating SSL connections against mail providers.
|
||||||
|
This is for use with self-signed certificates against local IMAP servers.
|
||||||
|
|
||||||
|
Defaults to None.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Settings this value has security implications for the security of your email.
|
||||||
|
Understand what it does and be sure you need to before setting.
|
||||||
|
|
||||||
## OCR settings {#ocr}
|
## OCR settings {#ocr}
|
||||||
|
|
||||||
Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
|
Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
|
||||||
|
@ -723,7 +723,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">600</context>
|
<context context-type="linenumber">648</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2526035785704676448" datatype="html">
|
<trans-unit id="2526035785704676448" datatype="html">
|
||||||
@ -2913,19 +2913,19 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">711</context>
|
<context context-type="linenumber">759</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">771</context>
|
<context context-type="linenumber">819</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">838</context>
|
<context context-type="linenumber">886</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">901</context>
|
<context context-type="linenumber">949</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1181910457994920507" datatype="html">
|
<trans-unit id="1181910457994920507" datatype="html">
|
||||||
@ -2940,19 +2940,19 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">713</context>
|
<context context-type="linenumber">761</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">773</context>
|
<context context-type="linenumber">821</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">840</context>
|
<context context-type="linenumber">888</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">903</context>
|
<context context-type="linenumber">951</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5729001209753056399" datatype="html">
|
<trans-unit id="5729001209753056399" datatype="html">
|
||||||
@ -4489,235 +4489,263 @@
|
|||||||
<context context-type="linenumber">372</context>
|
<context context-type="linenumber">372</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="3066660568529853846" datatype="html">
|
||||||
|
<source>Error retrieving groups</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">278</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="1235706724900303689" datatype="html">
|
||||||
|
<source>Error retrieving users</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">287</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5241231471117657636" datatype="html">
|
||||||
|
<source>Error retrieving mail rules</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">314</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3178554336792037159" datatype="html">
|
||||||
|
<source>Error retrieving mail accounts</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
|
<context context-type="linenumber">323</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="5610279464668232148" datatype="html">
|
<trans-unit id="5610279464668232148" datatype="html">
|
||||||
<source>Saved view "<x id="PH" equiv-text="savedView.name"/>" deleted.</source>
|
<source>Saved view "<x id="PH" equiv-text="savedView.name"/>" deleted.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">482</context>
|
<context context-type="linenumber">530</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3891152409365583719" datatype="html">
|
<trans-unit id="3891152409365583719" datatype="html">
|
||||||
<source>Settings saved</source>
|
<source>Settings saved</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">584</context>
|
<context context-type="linenumber">632</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7217000812750597833" datatype="html">
|
<trans-unit id="7217000812750597833" datatype="html">
|
||||||
<source>Settings were saved successfully.</source>
|
<source>Settings were saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">585</context>
|
<context context-type="linenumber">633</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="525012668859298131" datatype="html">
|
<trans-unit id="525012668859298131" datatype="html">
|
||||||
<source>Settings were saved successfully. Reload is required to apply some changes.</source>
|
<source>Settings were saved successfully. Reload is required to apply some changes.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">589</context>
|
<context context-type="linenumber">637</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8491974984518503778" datatype="html">
|
<trans-unit id="8491974984518503778" datatype="html">
|
||||||
<source>Reload now</source>
|
<source>Reload now</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">590</context>
|
<context context-type="linenumber">638</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6839066544204061364" datatype="html">
|
<trans-unit id="6839066544204061364" datatype="html">
|
||||||
<source>Use system language</source>
|
<source>Use system language</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">609</context>
|
<context context-type="linenumber">657</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7729897675462249787" datatype="html">
|
<trans-unit id="7729897675462249787" datatype="html">
|
||||||
<source>Use date format of display language</source>
|
<source>Use date format of display language</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">616</context>
|
<context context-type="linenumber">664</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5260584511980773458" datatype="html">
|
<trans-unit id="5260584511980773458" datatype="html">
|
||||||
<source>Error while storing settings on server.</source>
|
<source>Error while storing settings on server.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">636</context>
|
<context context-type="linenumber">684</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4510369340305901516" datatype="html">
|
<trans-unit id="4510369340305901516" datatype="html">
|
||||||
<source>Password has been changed, you will be logged out momentarily.</source>
|
<source>Password has been changed, you will be logged out momentarily.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">679</context>
|
<context context-type="linenumber">727</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2753185112875184719" datatype="html">
|
<trans-unit id="2753185112875184719" datatype="html">
|
||||||
<source>Saved user "<x id="PH" equiv-text="newUser.username"/>".</source>
|
<source>Saved user "<x id="PH" equiv-text="newUser.username"/>".</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">686</context>
|
<context context-type="linenumber">734</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3471101514724661554" datatype="html">
|
<trans-unit id="3471101514724661554" datatype="html">
|
||||||
<source>Error saving user.</source>
|
<source>Error saving user.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">698</context>
|
<context context-type="linenumber">746</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5565868288871970148" datatype="html">
|
<trans-unit id="5565868288871970148" datatype="html">
|
||||||
<source>Confirm delete user account</source>
|
<source>Confirm delete user account</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">709</context>
|
<context context-type="linenumber">757</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8133663925694885325" datatype="html">
|
<trans-unit id="8133663925694885325" datatype="html">
|
||||||
<source>This operation will permanently delete this user account.</source>
|
<source>This operation will permanently delete this user account.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">710</context>
|
<context context-type="linenumber">758</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="857903183180440990" datatype="html">
|
<trans-unit id="857903183180440990" datatype="html">
|
||||||
<source>Deleted user</source>
|
<source>Deleted user</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">719</context>
|
<context context-type="linenumber">767</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1942566571910298572" datatype="html">
|
<trans-unit id="1942566571910298572" datatype="html">
|
||||||
<source>Error deleting user.</source>
|
<source>Error deleting user.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">727</context>
|
<context context-type="linenumber">775</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5766640174051730159" datatype="html">
|
<trans-unit id="5766640174051730159" datatype="html">
|
||||||
<source>Saved group "<x id="PH" equiv-text="newGroup.name"/>".</source>
|
<source>Saved group "<x id="PH" equiv-text="newGroup.name"/>".</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">748</context>
|
<context context-type="linenumber">796</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8382042988405122578" datatype="html">
|
<trans-unit id="8382042988405122578" datatype="html">
|
||||||
<source>Error saving group.</source>
|
<source>Error saving group.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">758</context>
|
<context context-type="linenumber">806</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6538873300613683004" datatype="html">
|
<trans-unit id="6538873300613683004" datatype="html">
|
||||||
<source>Confirm delete user group</source>
|
<source>Confirm delete user group</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">769</context>
|
<context context-type="linenumber">817</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7710984639498518244" datatype="html">
|
<trans-unit id="7710984639498518244" datatype="html">
|
||||||
<source>This operation will permanently delete this user group.</source>
|
<source>This operation will permanently delete this user group.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">770</context>
|
<context context-type="linenumber">818</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6834066329827670963" datatype="html">
|
<trans-unit id="6834066329827670963" datatype="html">
|
||||||
<source>Deleted group</source>
|
<source>Deleted group</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">779</context>
|
<context context-type="linenumber">827</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8850738980935204840" datatype="html">
|
<trans-unit id="8850738980935204840" datatype="html">
|
||||||
<source>Error deleting group.</source>
|
<source>Error deleting group.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">787</context>
|
<context context-type="linenumber">835</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6327501535846658797" datatype="html">
|
<trans-unit id="6327501535846658797" datatype="html">
|
||||||
<source>Saved account "<x id="PH" equiv-text="newMailAccount.name"/>".</source>
|
<source>Saved account "<x id="PH" equiv-text="newMailAccount.name"/>".</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">813</context>
|
<context context-type="linenumber">861</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8067594003836508139" datatype="html">
|
<trans-unit id="8067594003836508139" datatype="html">
|
||||||
<source>Error saving account.</source>
|
<source>Error saving account.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">825</context>
|
<context context-type="linenumber">873</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5641934153807844674" datatype="html">
|
<trans-unit id="5641934153807844674" datatype="html">
|
||||||
<source>Confirm delete mail account</source>
|
<source>Confirm delete mail account</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">836</context>
|
<context context-type="linenumber">884</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7176985344323395435" datatype="html">
|
<trans-unit id="7176985344323395435" datatype="html">
|
||||||
<source>This operation will permanently delete this mail account.</source>
|
<source>This operation will permanently delete this mail account.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">837</context>
|
<context context-type="linenumber">885</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4233826387148482123" datatype="html">
|
<trans-unit id="4233826387148482123" datatype="html">
|
||||||
<source>Deleted mail account</source>
|
<source>Deleted mail account</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">846</context>
|
<context context-type="linenumber">894</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6202503362522392111" datatype="html">
|
<trans-unit id="6202503362522392111" datatype="html">
|
||||||
<source>Error deleting mail account.</source>
|
<source>Error deleting mail account.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">855</context>
|
<context context-type="linenumber">903</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="123368655395433699" datatype="html">
|
<trans-unit id="123368655395433699" datatype="html">
|
||||||
<source>Saved rule "<x id="PH" equiv-text="newMailRule.name"/>".</source>
|
<source>Saved rule "<x id="PH" equiv-text="newMailRule.name"/>".</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">876</context>
|
<context context-type="linenumber">924</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8951124554918814321" datatype="html">
|
<trans-unit id="8951124554918814321" datatype="html">
|
||||||
<source>Error saving rule.</source>
|
<source>Error saving rule.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">888</context>
|
<context context-type="linenumber">936</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3896080636020672118" datatype="html">
|
<trans-unit id="3896080636020672118" datatype="html">
|
||||||
<source>Confirm delete mail rule</source>
|
<source>Confirm delete mail rule</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">899</context>
|
<context context-type="linenumber">947</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2250372580580310337" datatype="html">
|
<trans-unit id="2250372580580310337" datatype="html">
|
||||||
<source>This operation will permanently delete this mail rule.</source>
|
<source>This operation will permanently delete this mail rule.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">900</context>
|
<context context-type="linenumber">948</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9077981247971516916" datatype="html">
|
<trans-unit id="9077981247971516916" datatype="html">
|
||||||
<source>Deleted mail rule</source>
|
<source>Deleted mail rule</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">909</context>
|
<context context-type="linenumber">957</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2033194641751367552" datatype="html">
|
<trans-unit id="2033194641751367552" datatype="html">
|
||||||
<source>Error deleting mail rule.</source>
|
<source>Error deleting mail rule.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">918</context>
|
<context context-type="linenumber">966</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5101757640976222639" datatype="html">
|
<trans-unit id="5101757640976222639" datatype="html">
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<label class="form-label" for="tags" i18n>Tags</label>
|
<label class="form-label" for="tags" i18n>Tags</label>
|
||||||
|
|
||||||
<div class="input-group flex-nowrap">
|
<div class="input-group flex-nowrap">
|
||||||
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
|
<ng-select #tagSelect name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
[closeOnSelect]="false"
|
[closeOnSelect]="false"
|
||||||
@ -11,11 +11,7 @@
|
|||||||
[addTag]="allowCreate ? createTagRef : false"
|
[addTag]="allowCreate ? createTagRef : false"
|
||||||
addTagText="Add tag"
|
addTagText="Add tag"
|
||||||
i18n-addTagText
|
i18n-addTagText
|
||||||
(change)="onChange(value)"
|
(change)="onChange(value)">
|
||||||
(search)="onSearch($event)"
|
|
||||||
(focus)="clearLastSearchTerm()"
|
|
||||||
(clear)="clearLastSearchTerm()"
|
|
||||||
(blur)="onBlur()">
|
|
||||||
|
|
||||||
<ng-template ng-label-tmp let-item="item">
|
<ng-template ng-label-tmp let-item="item">
|
||||||
<span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)">
|
<span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)">
|
||||||
|
@ -15,16 +15,28 @@ import {
|
|||||||
DEFAULT_MATCHING_ALGORITHM,
|
DEFAULT_MATCHING_ALGORITHM,
|
||||||
MATCH_ALL,
|
MATCH_ALL,
|
||||||
} from 'src/app/data/matching-model'
|
} from 'src/app/data/matching-model'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
import { of } from 'rxjs'
|
import { of } from 'rxjs'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import {
|
import {
|
||||||
|
NgbAccordionModule,
|
||||||
NgbModal,
|
NgbModal,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
|
import { CheckComponent } from '../check/check.component'
|
||||||
|
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||||
|
import { TextComponent } from '../text/text.component'
|
||||||
|
import { ColorComponent } from '../color/color.component'
|
||||||
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
|
import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
|
||||||
|
import { SelectComponent } from '../select/select.component'
|
||||||
|
import { ColorSliderModule } from 'ngx-color/slider'
|
||||||
|
import { By } from '@angular/platform-browser'
|
||||||
|
|
||||||
const tags: PaperlessTag[] = [
|
const tags: PaperlessTag[] = [
|
||||||
{
|
{
|
||||||
@ -56,12 +68,32 @@ describe('TagsComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [TagsComponent],
|
declarations: [
|
||||||
|
TagsComponent,
|
||||||
|
TagEditDialogComponent,
|
||||||
|
TextComponent,
|
||||||
|
ColorComponent,
|
||||||
|
IfOwnerDirective,
|
||||||
|
SelectComponent,
|
||||||
|
TextComponent,
|
||||||
|
PermissionsFormComponent,
|
||||||
|
ColorComponent,
|
||||||
|
CheckComponent,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: TagService,
|
provide: TagService,
|
||||||
useValue: {
|
useValue: {
|
||||||
listAll: () => of(tags),
|
listAll: () =>
|
||||||
|
of({
|
||||||
|
results: tags,
|
||||||
|
}),
|
||||||
|
create: () =>
|
||||||
|
of({
|
||||||
|
name: 'bar',
|
||||||
|
id: 99,
|
||||||
|
color: '#fff000',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -72,6 +104,8 @@ describe('TagsComponent', () => {
|
|||||||
RouterTestingModule,
|
RouterTestingModule,
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
|
NgbAccordionModule,
|
||||||
|
NgbPopoverModule,
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@ -85,7 +119,7 @@ describe('TagsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support suggestions', () => {
|
it('should support suggestions', () => {
|
||||||
expect(component.value).toBeUndefined()
|
expect(component.value).toHaveLength(0)
|
||||||
component.value = []
|
component.value = []
|
||||||
component.tags = tags
|
component.tags = tags
|
||||||
component.suggestions = [1, 2]
|
component.suggestions = [1, 2]
|
||||||
@ -107,18 +141,18 @@ describe('TagsComponent', () => {
|
|||||||
it('should support create new using last search term and open a modal', () => {
|
it('should support create new using last search term and open a modal', () => {
|
||||||
let activeInstances: NgbModalRef[]
|
let activeInstances: NgbModalRef[]
|
||||||
modalService.activeInstances.subscribe((v) => (activeInstances = v))
|
modalService.activeInstances.subscribe((v) => (activeInstances = v))
|
||||||
component.onSearch({ term: 'bar' })
|
component.select.searchTerm = 'foobar'
|
||||||
component.createTag()
|
component.createTag()
|
||||||
expect(modalService.hasOpenModals()).toBeTruthy()
|
expect(modalService.hasOpenModals()).toBeTruthy()
|
||||||
expect(activeInstances[0].componentInstance.object.name).toEqual('bar')
|
expect(activeInstances[0].componentInstance.object.name).toEqual('foobar')
|
||||||
|
const editDialog = activeInstances[0]
|
||||||
|
.componentInstance as TagEditDialogComponent
|
||||||
|
editDialog.save() // create is mocked
|
||||||
|
fixture.detectChanges()
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).toContain('foobar')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should clear search term on blur after delay', fakeAsync(() => {
|
|
||||||
const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
|
|
||||||
component.onBlur()
|
|
||||||
tick(3000)
|
|
||||||
expect(clearSpy).toHaveBeenCalled()
|
|
||||||
}))
|
|
||||||
|
|
||||||
it('support remove tags', () => {
|
it('support remove tags', () => {
|
||||||
component.tags = tags
|
component.tags = tags
|
||||||
@ -132,6 +166,7 @@ describe('TagsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should get tags', () => {
|
it('should get tags', () => {
|
||||||
|
component.tags = null
|
||||||
expect(component.getTag(2)).toBeNull()
|
expect(component.getTag(2)).toBeNull()
|
||||||
component.tags = tags
|
component.tags = tags
|
||||||
expect(component.getTag(2)).toEqual(tags[1])
|
expect(component.getTag(2)).toEqual(tags[1])
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
@ -12,6 +13,8 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
|
|||||||
import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { EditDialogMode } from '../../edit-dialog/edit-dialog.component'
|
import { EditDialogMode } from '../../edit-dialog/edit-dialog.component'
|
||||||
|
import { first, firstValueFrom, tap } from 'rxjs'
|
||||||
|
import { NgSelectComponent } from '@ng-select/ng-select'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
providers: [
|
providers: [
|
||||||
@ -74,14 +77,14 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
|||||||
@Output()
|
@Output()
|
||||||
filterDocuments = new EventEmitter<PaperlessTag[]>()
|
filterDocuments = new EventEmitter<PaperlessTag[]>()
|
||||||
|
|
||||||
value: number[]
|
@ViewChild('tagSelect') select: NgSelectComponent
|
||||||
|
|
||||||
tags: PaperlessTag[]
|
value: number[] = []
|
||||||
|
|
||||||
|
tags: PaperlessTag[] = []
|
||||||
|
|
||||||
public createTagRef: (name) => void
|
public createTagRef: (name) => void
|
||||||
|
|
||||||
private _lastSearchTerm: string
|
|
||||||
|
|
||||||
getTag(id: number) {
|
getTag(id: number) {
|
||||||
if (this.tags) {
|
if (this.tags) {
|
||||||
return this.tags.find((tag) => tag.id == id)
|
return this.tags.find((tag) => tag.id == id)
|
||||||
@ -111,15 +114,20 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
|||||||
})
|
})
|
||||||
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||||
if (name) modal.componentInstance.object = { name: name }
|
if (name) modal.componentInstance.object = { name: name }
|
||||||
else if (this._lastSearchTerm)
|
else if (this.select.searchTerm)
|
||||||
modal.componentInstance.object = { name: this._lastSearchTerm }
|
modal.componentInstance.object = { name: this.select.searchTerm }
|
||||||
modal.componentInstance.succeeded.subscribe((newTag) => {
|
this.select.searchTerm = null
|
||||||
|
this.select.detectChanges()
|
||||||
|
return firstValueFrom(
|
||||||
|
(modal.componentInstance as TagEditDialogComponent).succeeded.pipe(
|
||||||
|
first(),
|
||||||
|
tap(() => {
|
||||||
this.tagService.listAll().subscribe((tags) => {
|
this.tagService.listAll().subscribe((tags) => {
|
||||||
this.tags = tags.results
|
this.tags = tags.results
|
||||||
this.value = [...this.value, newTag.id]
|
|
||||||
this.onChange(this.value)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuggestions() {
|
getSuggestions() {
|
||||||
@ -137,20 +145,6 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
|||||||
this.onChange(this.value)
|
this.onChange(this.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
clearLastSearchTerm() {
|
|
||||||
this._lastSearchTerm = null
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearch($event) {
|
|
||||||
this._lastSearchTerm = $event.term
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur() {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.clearLastSearchTerm()
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasPrivate(): boolean {
|
get hasPrivate(): boolean {
|
||||||
return this.value.some(
|
return this.value.some(
|
||||||
(t) => this.tags?.find((t2) => t2.id === t) === undefined
|
(t) => this.tags?.find((t2) => t2.id === t) === undefined
|
||||||
|
@ -243,7 +243,7 @@
|
|||||||
<ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }">
|
<ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }">
|
||||||
<h4>
|
<h4>
|
||||||
<ng-container i18n>Mail accounts</ng-container>
|
<ng-container i18n>Mail accounts</ng-container>
|
||||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()">
|
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailAccount }">
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -262,7 +262,7 @@
|
|||||||
|
|
||||||
<li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
|
<li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)">{{account.name}}</button></div>
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div>
|
||||||
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
@ -280,7 +280,7 @@
|
|||||||
<ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }">
|
<ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }">
|
||||||
<h4 class="mt-4">
|
<h4 class="mt-4">
|
||||||
<ng-container i18n>Mail rules</ng-container>
|
<ng-container i18n>Mail rules</ng-container>
|
||||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()">
|
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }">
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -299,7 +299,7 @@
|
|||||||
|
|
||||||
<li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
|
<li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)">{{rule.name}}</button></div>
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
|
||||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
@ -323,7 +323,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="SettingsNavIDs.UsersGroups" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" (mouseover)="maybeInitializeTab(SettingsNavIDs.UsersGroups)" (focusin)="maybeInitializeTab(SettingsNavIDs.UsersGroups)">
|
<li [ngbNavItem]="SettingsNavIDs.UsersGroups" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }" (mouseover)="maybeInitializeTab(SettingsNavIDs.UsersGroups)" (focusin)="maybeInitializeTab(SettingsNavIDs.UsersGroups)">
|
||||||
<a ngbNavLink i18n>Users & Groups</a>
|
<a ngbNavLink i18n>Users & Groups</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
@ -334,7 +334,7 @@
|
|||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
</svg>
|
</svg>
|
||||||
<ng-container i18n>Add User</ng-container>
|
<ng-container *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" i18n>Add User</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<ul class="list-group" formGroupName="usersGroup">
|
<ul class="list-group" formGroupName="usersGroup">
|
||||||
@ -350,13 +350,13 @@
|
|||||||
|
|
||||||
<li *ngFor="let user of users" class="list-group-item" [formGroupName]="user.id">
|
<li *ngFor="let user of users" class="list-group-item" [formGroupName]="user.id">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)">{{user.username}}</button></div>
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
|
||||||
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
|
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
|
||||||
<div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
|
<div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-primary" type="button" (click)="editUser(user)" i18n>Edit</button>
|
<button class="btn btn-sm btn-primary" type="button" (click)="editUser(user)" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }" i18n>Edit</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" i18n>Delete</button>
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }" i18n>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -369,7 +369,7 @@
|
|||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
</svg>
|
</svg>
|
||||||
<ng-container i18n>Add Group</ng-container>
|
<ng-container *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }" i18n>Add Group</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<ul *ngIf="groups.length > 0" class="list-group" formGroupName="groupsGroup">
|
<ul *ngIf="groups.length > 0" class="list-group" formGroupName="groupsGroup">
|
||||||
@ -385,13 +385,13 @@
|
|||||||
|
|
||||||
<li *ngFor="let group of groups" class="list-group-item" [formGroupName]="group.id">
|
<li *ngFor="let group of groups" class="list-group-item" [formGroupName]="group.id">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)">{{group.name}}</button></div>
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
|
||||||
<div class="col"></div>
|
<div class="col"></div>
|
||||||
<div class="col"></div>
|
<div class="col"></div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-primary" type="button" (click)="editGroup(group)" i18n>Edit</button>
|
<button class="btn btn-sm btn-primary" type="button" (click)="editGroup(group)" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }" i18n>Edit</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" i18n>Delete</button>
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }" i18n>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
NgbModule,
|
NgbModule,
|
||||||
NgbNavLink,
|
NgbNavLink,
|
||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
|
NgbAlertModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
@ -42,6 +43,13 @@ import { CheckComponent } from '../../common/input/check/check.component'
|
|||||||
import { ColorComponent } from '../../common/input/color/color.component'
|
import { ColorComponent } from '../../common/input/color/color.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { SettingsComponent } from './settings.component'
|
import { SettingsComponent } from './settings.component'
|
||||||
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
|
import { TextComponent } from '../../common/input/text/text.component'
|
||||||
|
import { PasswordComponent } from '../../common/input/password/password.component'
|
||||||
|
import { NumberComponent } from '../../common/input/number/number.component'
|
||||||
|
import { TagsComponent } from '../../common/input/tags/tags.component'
|
||||||
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
|
|
||||||
const savedViews = [
|
const savedViews = [
|
||||||
{ id: 1, name: 'view1' },
|
{ id: 1, name: 'view1' },
|
||||||
@ -90,6 +98,14 @@ describe('SettingsComponent', () => {
|
|||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
CheckComponent,
|
CheckComponent,
|
||||||
ColorComponent,
|
ColorComponent,
|
||||||
|
SafeHtmlPipe,
|
||||||
|
SelectComponent,
|
||||||
|
TextComponent,
|
||||||
|
PasswordComponent,
|
||||||
|
NumberComponent,
|
||||||
|
TagsComponent,
|
||||||
|
MailAccountEditDialogComponent,
|
||||||
|
MailRuleEditDialogComponent,
|
||||||
],
|
],
|
||||||
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
||||||
imports: [
|
imports: [
|
||||||
@ -98,6 +114,8 @@ describe('SettingsComponent', () => {
|
|||||||
RouterTestingModule.withRoutes(routes),
|
RouterTestingModule.withRoutes(routes),
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
NgbAlertModule,
|
||||||
|
NgSelectModule,
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@ -116,6 +134,14 @@ describe('SettingsComponent', () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(permissionsService, 'currentUserOwnsObject')
|
.spyOn(permissionsService, 'currentUserOwnsObject')
|
||||||
.mockReturnValue(true)
|
.mockReturnValue(true)
|
||||||
|
groupService = TestBed.inject(GroupService)
|
||||||
|
savedViewService = TestBed.inject(SavedViewService)
|
||||||
|
mailAccountService = TestBed.inject(MailAccountService)
|
||||||
|
mailRuleService = TestBed.inject(MailRuleService)
|
||||||
|
})
|
||||||
|
|
||||||
|
function completeSetup(excludeService = null) {
|
||||||
|
if (excludeService !== userService) {
|
||||||
jest.spyOn(userService, 'listAll').mockReturnValue(
|
jest.spyOn(userService, 'listAll').mockReturnValue(
|
||||||
of({
|
of({
|
||||||
all: users.map((u) => u.id),
|
all: users.map((u) => u.id),
|
||||||
@ -123,7 +149,8 @@ describe('SettingsComponent', () => {
|
|||||||
results: users.concat([]),
|
results: users.concat([]),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
groupService = TestBed.inject(GroupService)
|
}
|
||||||
|
if (excludeService !== groupService) {
|
||||||
jest.spyOn(groupService, 'listAll').mockReturnValue(
|
jest.spyOn(groupService, 'listAll').mockReturnValue(
|
||||||
of({
|
of({
|
||||||
all: groups.map((g) => g.id),
|
all: groups.map((g) => g.id),
|
||||||
@ -131,7 +158,8 @@ describe('SettingsComponent', () => {
|
|||||||
results: groups.concat([]),
|
results: groups.concat([]),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
savedViewService = TestBed.inject(SavedViewService)
|
}
|
||||||
|
if (excludeService !== savedViewService) {
|
||||||
jest.spyOn(savedViewService, 'listAll').mockReturnValue(
|
jest.spyOn(savedViewService, 'listAll').mockReturnValue(
|
||||||
of({
|
of({
|
||||||
all: savedViews.map((v) => v.id),
|
all: savedViews.map((v) => v.id),
|
||||||
@ -139,7 +167,8 @@ describe('SettingsComponent', () => {
|
|||||||
results: (savedViews as PaperlessSavedView[]).concat([]),
|
results: (savedViews as PaperlessSavedView[]).concat([]),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
mailAccountService = TestBed.inject(MailAccountService)
|
}
|
||||||
|
if (excludeService !== mailAccountService) {
|
||||||
jest.spyOn(mailAccountService, 'listAll').mockReturnValue(
|
jest.spyOn(mailAccountService, 'listAll').mockReturnValue(
|
||||||
of({
|
of({
|
||||||
all: mailAccounts.map((a) => a.id),
|
all: mailAccounts.map((a) => a.id),
|
||||||
@ -147,7 +176,8 @@ describe('SettingsComponent', () => {
|
|||||||
results: (mailAccounts as PaperlessMailAccount[]).concat([]),
|
results: (mailAccounts as PaperlessMailAccount[]).concat([]),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
mailRuleService = TestBed.inject(MailRuleService)
|
}
|
||||||
|
if (excludeService !== mailRuleService) {
|
||||||
jest.spyOn(mailRuleService, 'listAll').mockReturnValue(
|
jest.spyOn(mailRuleService, 'listAll').mockReturnValue(
|
||||||
of({
|
of({
|
||||||
all: mailRules.map((r) => r.id),
|
all: mailRules.map((r) => r.id),
|
||||||
@ -155,13 +185,15 @@ describe('SettingsComponent', () => {
|
|||||||
results: (mailRules as PaperlessMailRule[]).concat([]),
|
results: (mailRules as PaperlessMailRule[]).concat([]),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fixture = TestBed.createComponent(SettingsComponent)
|
fixture = TestBed.createComponent(SettingsComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
})
|
}
|
||||||
|
|
||||||
it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', () => {
|
it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', () => {
|
||||||
|
completeSetup()
|
||||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||||
tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click'))
|
tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||||
@ -187,6 +219,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support direct link to tab by URL, scroll if needed', () => {
|
it('should support direct link to tab by URL, scroll if needed', () => {
|
||||||
|
completeSetup()
|
||||||
jest
|
jest
|
||||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||||
.mockReturnValue(of(convertToParamMap({ section: 'mail' })))
|
.mockReturnValue(of(convertToParamMap({ section: 'mail' })))
|
||||||
@ -199,6 +232,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should lazy load tab data', () => {
|
it('should lazy load tab data', () => {
|
||||||
|
completeSetup()
|
||||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||||
|
|
||||||
expect(component.savedViews).toBeUndefined()
|
expect(component.savedViews).toBeUndefined()
|
||||||
@ -221,6 +255,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support save saved views, show error', () => {
|
it('should support save saved views, show error', () => {
|
||||||
|
completeSetup()
|
||||||
component.maybeInitializeTab(3) // SavedViews
|
component.maybeInitializeTab(3) // SavedViews
|
||||||
|
|
||||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
@ -248,6 +283,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support save local settings updating appearance settings and calling API, show error', () => {
|
it('should support save local settings updating appearance settings and calling API, show error', () => {
|
||||||
|
completeSetup()
|
||||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
const toastSpy = jest.spyOn(toastService, 'show')
|
const toastSpy = jest.spyOn(toastService, 'show')
|
||||||
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
|
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
|
||||||
@ -275,6 +311,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should offer reload if settings changes require', () => {
|
it('should offer reload if settings changes require', () => {
|
||||||
|
completeSetup()
|
||||||
let toast: Toast
|
let toast: Toast
|
||||||
toastService.getToasts().subscribe((t) => (toast = t[0]))
|
toastService.getToasts().subscribe((t) => (toast = t[0]))
|
||||||
component.initialize(true) // reset
|
component.initialize(true) // reset
|
||||||
@ -288,6 +325,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should allow setting theme color, visually apply change immediately but not save', () => {
|
it('should allow setting theme color, visually apply change immediately but not save', () => {
|
||||||
|
completeSetup()
|
||||||
const appearanceSpy = jest.spyOn(
|
const appearanceSpy = jest.spyOn(
|
||||||
settingsService,
|
settingsService,
|
||||||
'updateAppearanceSettings'
|
'updateAppearanceSettings'
|
||||||
@ -304,6 +342,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support delete saved view', () => {
|
it('should support delete saved view', () => {
|
||||||
|
completeSetup()
|
||||||
component.maybeInitializeTab(3) // SavedViews
|
component.maybeInitializeTab(3) // SavedViews
|
||||||
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const deleteSpy = jest.spyOn(savedViewService, 'delete')
|
const deleteSpy = jest.spyOn(savedViewService, 'delete')
|
||||||
@ -316,6 +355,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support edit / create user, show error if needed', () => {
|
it('should support edit / create user, show error if needed', () => {
|
||||||
|
completeSetup()
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.editUser(users[0])
|
component.editUser(users[0])
|
||||||
@ -332,6 +372,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support delete user, show error if needed', () => {
|
it('should support delete user, show error if needed', () => {
|
||||||
|
completeSetup()
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.deleteUser(users[0])
|
component.deleteUser(users[0])
|
||||||
@ -352,6 +393,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should logout current user if password changed, after delay', fakeAsync(() => {
|
it('should logout current user if password changed, after delay', fakeAsync(() => {
|
||||||
|
completeSetup()
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.editUser(users[0])
|
component.editUser(users[0])
|
||||||
@ -371,6 +413,7 @@ describe('SettingsComponent', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
it('should support edit / create group, show error if needed', () => {
|
it('should support edit / create group, show error if needed', () => {
|
||||||
|
completeSetup()
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.editGroup(groups[0])
|
component.editGroup(groups[0])
|
||||||
@ -386,6 +429,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support delete group, show error if needed', () => {
|
it('should support delete group, show error if needed', () => {
|
||||||
|
completeSetup()
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.deleteGroup(users[0])
|
component.deleteGroup(users[0])
|
||||||
@ -406,12 +450,71 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should get group name', () => {
|
it('should get group name', () => {
|
||||||
|
completeSetup()
|
||||||
component.maybeInitializeTab(5) // UsersGroups
|
component.maybeInitializeTab(5) // UsersGroups
|
||||||
expect(component.getGroupName(1)).toEqual(groups[0].name)
|
expect(component.getGroupName(1)).toEqual(groups[0].name)
|
||||||
expect(component.getGroupName(11)).toEqual('')
|
expect(component.getGroupName(11)).toEqual('')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should show errors on load if load mailAccounts failure', () => {
|
||||||
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
jest
|
||||||
|
.spyOn(mailAccountService, 'listAll')
|
||||||
|
.mockImplementation(() =>
|
||||||
|
throwError(() => new Error('failed to load mail accounts'))
|
||||||
|
)
|
||||||
|
completeSetup(mailAccountService)
|
||||||
|
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||||
|
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click')) // mail tab
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(toastErrorSpy).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show errors on load if load mailRules failure', () => {
|
||||||
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
jest
|
||||||
|
.spyOn(mailRuleService, 'listAll')
|
||||||
|
.mockImplementation(() =>
|
||||||
|
throwError(() => new Error('failed to load mail rules'))
|
||||||
|
)
|
||||||
|
completeSetup(mailRuleService)
|
||||||
|
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||||
|
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click')) // mail tab
|
||||||
|
fixture.detectChanges()
|
||||||
|
// tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||||
|
expect(toastErrorSpy).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show errors on load if load users failure', () => {
|
||||||
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
jest
|
||||||
|
.spyOn(userService, 'listAll')
|
||||||
|
.mockImplementation(() =>
|
||||||
|
throwError(() => new Error('failed to load users'))
|
||||||
|
)
|
||||||
|
completeSetup(userService)
|
||||||
|
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||||
|
tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click')) // users tab
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(toastErrorSpy).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show errors on load if load groups failure', () => {
|
||||||
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
jest
|
||||||
|
.spyOn(groupService, 'listAll')
|
||||||
|
.mockImplementation(() =>
|
||||||
|
throwError(() => new Error('failed to load groups'))
|
||||||
|
)
|
||||||
|
completeSetup(groupService)
|
||||||
|
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||||
|
tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click')) // users tab
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(toastErrorSpy).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('should support edit / create mail account, show error if needed', () => {
|
it('should support edit / create mail account, show error if needed', () => {
|
||||||
|
completeSetup()
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.editMailAccount(mailAccounts[0] as PaperlessMailAccount)
|
component.editMailAccount(mailAccounts[0] as PaperlessMailAccount)
|
||||||
@ -427,6 +530,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support delete mail account, show error if needed', () => {
|
it('should support delete mail account, show error if needed', () => {
|
||||||
|
completeSetup()
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.deleteMailAccount(mailAccounts[0] as PaperlessMailAccount)
|
component.deleteMailAccount(mailAccounts[0] as PaperlessMailAccount)
|
||||||
@ -447,6 +551,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support edit / create mail rule, show error if needed', () => {
|
it('should support edit / create mail rule, show error if needed', () => {
|
||||||
|
completeSetup()
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.editMailRule(mailRules[0] as PaperlessMailRule)
|
component.editMailRule(mailRules[0] as PaperlessMailRule)
|
||||||
@ -462,6 +567,7 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support delete mail rule, show error if needed', () => {
|
it('should support delete mail rule, show error if needed', () => {
|
||||||
|
completeSetup()
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.deleteMailRule(mailRules[0] as PaperlessMailRule)
|
component.deleteMailRule(mailRules[0] as PaperlessMailRule)
|
||||||
|
@ -146,7 +146,7 @@ export class SettingsComponent
|
|||||||
private groupsService: GroupService,
|
private groupsService: GroupService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private permissionsService: PermissionsService
|
public permissionsService: PermissionsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.settings.settingsSaved.subscribe(() => {
|
this.settings.settingsSaved.subscribe(() => {
|
||||||
@ -259,24 +259,72 @@ export class SettingsComponent
|
|||||||
navID == SettingsNavIDs.UsersGroups &&
|
navID == SettingsNavIDs.UsersGroups &&
|
||||||
(!this.users || !this.groups)
|
(!this.users || !this.groups)
|
||||||
) {
|
) {
|
||||||
this.usersService.listAll().subscribe((r) => {
|
this.usersService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
next: (r) => {
|
||||||
this.users = r.results
|
this.users = r.results
|
||||||
this.groupsService.listAll().subscribe((r) => {
|
this.groupsService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
next: (r) => {
|
||||||
this.groups = r.results
|
this.groups = r.results
|
||||||
this.initialize(false)
|
this.initialize(false)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error retrieving groups`,
|
||||||
|
10000,
|
||||||
|
JSON.stringify(e)
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error retrieving users`,
|
||||||
|
10000,
|
||||||
|
JSON.stringify(e)
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
navID == SettingsNavIDs.Mail &&
|
navID == SettingsNavIDs.Mail &&
|
||||||
(!this.mailAccounts || !this.mailRules)
|
(!this.mailAccounts || !this.mailRules)
|
||||||
) {
|
) {
|
||||||
this.mailAccountService.listAll().subscribe((r) => {
|
this.mailAccountService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
next: (r) => {
|
||||||
this.mailAccounts = r.results
|
this.mailAccounts = r.results
|
||||||
|
|
||||||
this.mailRuleService.listAll().subscribe((r) => {
|
this.mailRuleService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
next: (r) => {
|
||||||
this.mailRules = r.results
|
this.mailRules = r.results
|
||||||
this.initialize(false)
|
this.initialize(false)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error retrieving mail rules`,
|
||||||
|
10000,
|
||||||
|
JSON.stringify(e)
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error retrieving mail accounts`,
|
||||||
|
10000,
|
||||||
|
JSON.stringify(e)
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { Subject, zip } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
|
|
||||||
export interface Toast {
|
export interface Toast {
|
||||||
title: string
|
title: string
|
||||||
|
@ -5,7 +5,7 @@ export const environment = {
|
|||||||
apiBaseUrl: document.baseURI + 'api/',
|
apiBaseUrl: document.baseURI + 'api/',
|
||||||
apiVersion: '3',
|
apiVersion: '3',
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
version: '1.17.1',
|
version: '1.17.1-dev',
|
||||||
webSocketHost: window.location.host,
|
webSocketHost: window.location.host,
|
||||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||||
|
@ -80,6 +80,9 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
|||||||
.btn {
|
.btn {
|
||||||
--bs-btn-disabled-opacity: 0.35;
|
--bs-btn-disabled-opacity: 0.35;
|
||||||
}
|
}
|
||||||
|
.btn.btn-link {
|
||||||
|
--bs-btn-disabled-opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
&:hover, &:focus, &.active, &:active {
|
&:hover, &:focus, &.active, &:active {
|
||||||
|
@ -3453,6 +3453,110 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
|
|||||||
self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id])
|
self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id])
|
||||||
self.assertEqual(len(kwargs["set_permissions"]["view"]["users"]), 2)
|
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 is 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()
|
||||||
|
|
||||||
|
|
||||||
class TestBulkDownload(DirectoriesMixin, APITestCase):
|
class TestBulkDownload(DirectoriesMixin, APITestCase):
|
||||||
ENDPOINT = "/api/documents/bulk_download/"
|
ENDPOINT = "/api/documents/bulk_download/"
|
||||||
|
@ -54,6 +54,7 @@ from rest_framework.viewsets import ModelViewSet
|
|||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from documents import bulk_edit
|
||||||
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
|
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
|
||||||
from documents.permissions import PaperlessAdminPermissions
|
from documents.permissions import PaperlessAdminPermissions
|
||||||
from documents.permissions import PaperlessObjectPermissions
|
from documents.permissions import PaperlessObjectPermissions
|
||||||
@ -694,7 +695,7 @@ class SavedViewViewSet(ModelViewSet, PassUserMixin):
|
|||||||
serializer.save(owner=self.request.user)
|
serializer.save(owner=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class BulkEditView(GenericAPIView):
|
class BulkEditView(GenericAPIView, PassUserMixin):
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
serializer_class = BulkEditSerializer
|
serializer_class = BulkEditSerializer
|
||||||
parser_classes = (parsers.JSONParser,)
|
parser_classes = (parsers.JSONParser,)
|
||||||
@ -703,10 +704,25 @@ class BulkEditView(GenericAPIView):
|
|||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
user = self.request.user
|
||||||
method = serializer.validated_data.get("method")
|
method = serializer.validated_data.get("method")
|
||||||
parameters = serializer.validated_data.get("parameters")
|
parameters = serializer.validated_data.get("parameters")
|
||||||
documents = serializer.validated_data.get("documents")
|
documents = serializer.validated_data.get("documents")
|
||||||
|
|
||||||
|
if not user.is_superuser:
|
||||||
|
document_objs = Document.objects.filter(pk__in=documents)
|
||||||
|
has_perms = (
|
||||||
|
all((doc.owner == user or doc.owner is None) for doc in document_objs)
|
||||||
|
if method == bulk_edit.set_permissions
|
||||||
|
else all(
|
||||||
|
has_perms_owner_aware(user, "change_document", doc)
|
||||||
|
for doc in document_objs
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_perms:
|
||||||
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# TODO: parameter validation
|
# TODO: parameter validation
|
||||||
result = method(documents, **parameters)
|
result = method(documents, **parameters)
|
||||||
|
@ -177,6 +177,23 @@ def settings_values_check(app_configs, **kwargs):
|
|||||||
)
|
)
|
||||||
return msgs
|
return msgs
|
||||||
|
|
||||||
return (
|
def _email_certificate_validate():
|
||||||
_ocrmypdf_settings_check() + _timezone_validate() + _barcode_scanner_validate()
|
msgs = []
|
||||||
|
# Existence checks
|
||||||
|
if (
|
||||||
|
settings.EMAIL_CERTIFICATE_FILE is not None
|
||||||
|
and not settings.EMAIL_CERTIFICATE_FILE.is_file()
|
||||||
|
):
|
||||||
|
msgs.append(
|
||||||
|
Error(
|
||||||
|
f"Email cert {settings.EMAIL_CERTIFICATE_FILE} is not a file",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return msgs
|
||||||
|
|
||||||
|
return (
|
||||||
|
_ocrmypdf_settings_check()
|
||||||
|
+ _timezone_validate()
|
||||||
|
+ _barcode_scanner_validate()
|
||||||
|
+ _email_certificate_validate()
|
||||||
)
|
)
|
||||||
|
@ -67,11 +67,20 @@ def __get_float(key: str, default: float) -> float:
|
|||||||
return float(os.getenv(key, default))
|
return float(os.getenv(key, default))
|
||||||
|
|
||||||
|
|
||||||
def __get_path(key: str, default: Union[PathLike, str]) -> Path:
|
def __get_path(
|
||||||
|
key: str,
|
||||||
|
default: Optional[Union[PathLike, str]] = None,
|
||||||
|
) -> Optional[Path]:
|
||||||
"""
|
"""
|
||||||
Return a normalized, absolute path based on the environment variable or a default
|
Return a normalized, absolute path based on the environment variable or a default,
|
||||||
|
if provided. If not set and no default, returns None
|
||||||
"""
|
"""
|
||||||
return Path(os.environ.get(key, default)).resolve()
|
if key in os.environ:
|
||||||
|
return Path(os.environ[key]).resolve()
|
||||||
|
elif default is not None:
|
||||||
|
return Path(default).resolve()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def __get_list(
|
def __get_list(
|
||||||
@ -364,6 +373,7 @@ CHANNEL_LAYERS = {
|
|||||||
"hosts": [_CHANNELS_REDIS_URL],
|
"hosts": [_CHANNELS_REDIS_URL],
|
||||||
"capacity": 2000, # default 100
|
"capacity": 2000, # default 100
|
||||||
"expiry": 15, # default 60
|
"expiry": 15, # default 60
|
||||||
|
"prefix": os.getenv("PAPERLESS_REDIS_PREFIX", ""),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -476,6 +486,8 @@ CSRF_COOKIE_NAME = f"{COOKIE_PREFIX}csrftoken"
|
|||||||
SESSION_COOKIE_NAME = f"{COOKIE_PREFIX}sessionid"
|
SESSION_COOKIE_NAME = f"{COOKIE_PREFIX}sessionid"
|
||||||
LANGUAGE_COOKIE_NAME = f"{COOKIE_PREFIX}django_language"
|
LANGUAGE_COOKIE_NAME = f"{COOKIE_PREFIX}django_language"
|
||||||
|
|
||||||
|
EMAIL_CERTIFICATE_FILE = __get_path("PAPERLESS_EMAIL_CERTIFICATE_FILE")
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Database #
|
# Database #
|
||||||
@ -679,6 +691,9 @@ CELERY_TASK_SEND_SENT_EVENT = True
|
|||||||
CELERY_SEND_TASK_SENT_EVENT = True
|
CELERY_SEND_TASK_SENT_EVENT = True
|
||||||
CELERY_BROKER_CONNECTION_RETRY = True
|
CELERY_BROKER_CONNECTION_RETRY = True
|
||||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
||||||
|
CELERY_BROKER_TRANSPORT_OPTIONS = {
|
||||||
|
"global_keyprefix": os.getenv("PAPERLESS_REDIS_PREFIX", ""),
|
||||||
|
}
|
||||||
|
|
||||||
CELERY_TASK_TRACK_STARTED = True
|
CELERY_TASK_TRACK_STARTED = True
|
||||||
CELERY_TASK_TIME_LIMIT: Final[int] = __get_int("PAPERLESS_WORKER_TIMEOUT", 1800)
|
CELERY_TASK_TIME_LIMIT: Final[int] = __get_int("PAPERLESS_WORKER_TIMEOUT", 1800)
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from paperless.checks import binaries_check
|
from paperless.checks import binaries_check
|
||||||
from paperless.checks import debug_mode_check
|
from paperless.checks import debug_mode_check
|
||||||
from paperless.checks import paths_check
|
from paperless.checks import paths_check
|
||||||
@ -57,7 +59,7 @@ class TestChecks(DirectoriesMixin, TestCase):
|
|||||||
self.assertEqual(len(debug_mode_check(None)), 1)
|
self.assertEqual(len(debug_mode_check(None)), 1)
|
||||||
|
|
||||||
|
|
||||||
class TestSettingsChecks(DirectoriesMixin, TestCase):
|
class TestSettingsChecksAgainstDefaults(DirectoriesMixin, TestCase):
|
||||||
def test_all_valid(self):
|
def test_all_valid(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@ -70,6 +72,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
|
|||||||
msgs = settings_values_check(None)
|
msgs = settings_values_check(None)
|
||||||
self.assertEqual(len(msgs), 0)
|
self.assertEqual(len(msgs), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOcrSettingsChecks(DirectoriesMixin, TestCase):
|
||||||
@override_settings(OCR_OUTPUT_TYPE="notapdf")
|
@override_settings(OCR_OUTPUT_TYPE="notapdf")
|
||||||
def test_invalid_output_type(self):
|
def test_invalid_output_type(self):
|
||||||
"""
|
"""
|
||||||
@ -160,6 +164,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
self.assertIn('OCR clean mode "cleanme"', msg.msg)
|
self.assertIn('OCR clean mode "cleanme"', msg.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimezoneSettingsChecks(DirectoriesMixin, TestCase):
|
||||||
@override_settings(TIME_ZONE="TheMoon\\MyCrater")
|
@override_settings(TIME_ZONE="TheMoon\\MyCrater")
|
||||||
def test_invalid_timezone(self):
|
def test_invalid_timezone(self):
|
||||||
"""
|
"""
|
||||||
@ -178,6 +184,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
self.assertIn('Timezone "TheMoon\\MyCrater"', msg.msg)
|
self.assertIn('Timezone "TheMoon\\MyCrater"', msg.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBarcodeSettingsChecks(DirectoriesMixin, TestCase):
|
||||||
@override_settings(CONSUMER_BARCODE_SCANNER="Invalid")
|
@override_settings(CONSUMER_BARCODE_SCANNER="Invalid")
|
||||||
def test_barcode_scanner_invalid(self):
|
def test_barcode_scanner_invalid(self):
|
||||||
msgs = settings_values_check(None)
|
msgs = settings_values_check(None)
|
||||||
@ -200,3 +208,26 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
|
|||||||
def test_barcode_scanner_valid(self):
|
def test_barcode_scanner_valid(self):
|
||||||
msgs = settings_values_check(None)
|
msgs = settings_values_check(None)
|
||||||
self.assertEqual(len(msgs), 0)
|
self.assertEqual(len(msgs), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailCertSettingsChecks(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||||
|
@override_settings(EMAIL_CERTIFICATE_FILE=Path("/tmp/not_actually_here.pem"))
|
||||||
|
def test_not_valid_file(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Default settings
|
||||||
|
- Email certificate is set
|
||||||
|
WHEN:
|
||||||
|
- Email certificate file doesn't exist
|
||||||
|
THEN:
|
||||||
|
- system check error reported for email certificate
|
||||||
|
"""
|
||||||
|
self.assertIsNotFile("/tmp/not_actually_here.pem")
|
||||||
|
|
||||||
|
msgs = settings_values_check(None)
|
||||||
|
|
||||||
|
self.assertEqual(len(msgs), 1)
|
||||||
|
|
||||||
|
msg = msgs[0]
|
||||||
|
|
||||||
|
self.assertIn("Email cert /tmp/not_actually_here.pem is not a file", msg.msg)
|
||||||
|
@ -395,12 +395,16 @@ def get_mailbox(server, port, security) -> MailBox:
|
|||||||
"""
|
"""
|
||||||
Returns the correct MailBox instance for the given configuration.
|
Returns the correct MailBox instance for the given configuration.
|
||||||
"""
|
"""
|
||||||
|
ssl_context = ssl.create_default_context()
|
||||||
|
if settings.EMAIL_CERTIFICATE_FILE is not None: # pragma: nocover
|
||||||
|
ssl_context.load_verify_locations(cafile=settings.EMAIL_CERTIFICATE_FILE)
|
||||||
|
|
||||||
if security == MailAccount.ImapSecurity.NONE:
|
if security == MailAccount.ImapSecurity.NONE:
|
||||||
mailbox = MailBoxUnencrypted(server, port)
|
mailbox = MailBoxUnencrypted(server, port)
|
||||||
elif security == MailAccount.ImapSecurity.STARTTLS:
|
elif security == MailAccount.ImapSecurity.STARTTLS:
|
||||||
mailbox = MailBoxTls(server, port, ssl_context=ssl.create_default_context())
|
mailbox = MailBoxTls(server, port, ssl_context=ssl_context)
|
||||||
elif security == MailAccount.ImapSecurity.SSL:
|
elif security == MailAccount.ImapSecurity.SSL:
|
||||||
mailbox = MailBox(server, port, ssl_context=ssl.create_default_context())
|
mailbox = MailBox(server, port, ssl_context=ssl_context)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Unknown IMAP security") # pragma: nocover
|
raise NotImplementedError("Unknown IMAP security") # pragma: nocover
|
||||||
return mailbox
|
return mailbox
|
||||||
|
@ -215,7 +215,11 @@ class MailDocumentParser(DocumentParser):
|
|||||||
file_multi_part[2],
|
file_multi_part[2],
|
||||||
)
|
)
|
||||||
|
|
||||||
response = httpx.post(url_merge, files=pdf_collection, timeout=30.0)
|
response = httpx.post(
|
||||||
|
url_merge,
|
||||||
|
files=pdf_collection,
|
||||||
|
timeout=settings.CELERY_TASK_TIME_LIMIT,
|
||||||
|
)
|
||||||
response.raise_for_status() # ensure we notice bad responses
|
response.raise_for_status() # ensure we notice bad responses
|
||||||
|
|
||||||
archive_path.write_bytes(response.content)
|
archive_path.write_bytes(response.content)
|
||||||
@ -330,7 +334,7 @@ class MailDocumentParser(DocumentParser):
|
|||||||
files=files,
|
files=files,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=data,
|
data=data,
|
||||||
timeout=30.0,
|
timeout=settings.CELERY_TASK_TIME_LIMIT,
|
||||||
)
|
)
|
||||||
response.raise_for_status() # ensure we notice bad responses
|
response.raise_for_status() # ensure we notice bad responses
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@ -409,7 +413,12 @@ class MailDocumentParser(DocumentParser):
|
|||||||
file_multi_part[2],
|
file_multi_part[2],
|
||||||
)
|
)
|
||||||
|
|
||||||
response = httpx.post(url, files=files, data=data, timeout=30.0)
|
response = httpx.post(
|
||||||
|
url,
|
||||||
|
files=files,
|
||||||
|
data=data,
|
||||||
|
timeout=settings.CELERY_TASK_TIME_LIMIT,
|
||||||
|
)
|
||||||
response.raise_for_status() # ensure we notice bad responses
|
response.raise_for_status() # ensure we notice bad responses
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise ParseError(f"Error while converting document to PDF: {err}") from err
|
raise ParseError(f"Error while converting document to PDF: {err}") from err
|
||||||
|
@ -100,7 +100,7 @@ class TikaDocumentParser(DocumentParser):
|
|||||||
files=files,
|
files=files,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=data,
|
data=data,
|
||||||
timeout=30.0,
|
timeout=settings.CELERY_TASK_TIME_LIMIT,
|
||||||
)
|
)
|
||||||
response.raise_for_status() # ensure we notice bad responses
|
response.raise_for_status() # ensure we notice bad responses
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user