diff --git a/.ruff.toml b/.ruff.toml index c69de0ee1..497d3f1eb 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,7 +1,29 @@ -# https://beta.ruff.rs/docs/settings/ -# https://beta.ruff.rs/docs/rules/ -extend-select = ["I", "W", "UP", "COM", "DJ", "EXE", "ISC", "ICN", "G201", "INP", "PIE", "RSE", "SIM", "TID", "PLC", "PLE", "RUF"] -# TODO PTH +# https://docs.astral.sh/ruff/settings/ +# https://docs.astral.sh/ruff/rules/ +extend-select = [ + "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "I", # https://docs.astral.sh/ruff/rules/#isort-i + "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj + "EXE", # https://docs.astral.sh/ruff/rules/#flake8-executable-exe + "ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc + "ICN", # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn + "G201", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp + "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie + "Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q + "RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid + "TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch + "PLC", # https://docs.astral.sh/ruff/rules/#pylint-pl + "PLE", # https://docs.astral.sh/ruff/rules/#pylint-pl + "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + "FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly +] +# TODO PTH https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth ignore = ["DJ001", "SIM105", "RUF012"] fix = true line-length = 88 @@ -13,9 +35,9 @@ show-fixes = true [per-file-ignores] ".github/scripts/*.py" = ["E501", "INP001", "SIM117"] -"docker/wait-for-redis.py" = ["INP001"] +"docker/wait-for-redis.py" = ["INP001", "T201"] "*/tests/*.py" = ["E501", "SIM117"] -"*/migrations/*.py" = ["E501", "SIM"] +"*/migrations/*.py" = ["E501", "SIM", "T201"] "src/paperless_tesseract/tests/test_parser.py" = ["RUF001"] "src/documents/models.py" = ["SIM115"] diff --git a/Dockerfile b/Dockerfile index 73bd0cf57..ea5bd6026 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,8 +39,6 @@ RUN set -eux \ # - Don't leave anything extra in here FROM docker.io/python:3.11-slim-bookworm as main-app -ENV PYTHONWARNINGS="ignore:::django.http.response:517" - LABEL org.opencontainers.image.authors="paperless-ngx team " LABEL org.opencontainers.image.documentation="https://docs.paperless-ngx.com/" LABEL org.opencontainers.image.source="https://github.com/paperless-ngx/paperless-ngx" @@ -57,6 +55,12 @@ ARG JBIG2ENC_VERSION=0.29 ARG QPDF_VERSION=11.6.4 ARG GS_VERSION=10.02.1 +# Set Python environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + # Ignore warning from Whitenoise + PYTHONWARNINGS="ignore:::django.http.response:517" + # # Begin installation and configuration # Order the steps below from least often changed to most diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index 04626fe41..46d9c2b4b 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -640,3 +640,42 @@ single-sided split marker page, the split document(s) will have an empty page at whatever else was on the backside of the split marker page.) You can work around that by having a split marker page that has the split barcode on _both_ sides. This way, the extra page will get automatically removed. + +## SSO and third party authentication with Paperless-ngx + +Paperless-ngx has a built-in authentication system from Django but you can easily integrate an +external authentication solution using one of the following methods: + +### Remote User authentication + +This is a simple option that uses remote user authentication made available by certain SSO +applications. See the relevant configuration options for more information: +[PAPERLESS_ENABLE_HTTP_REMOTE_USER](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER) and +[PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME](configuration.md#PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME) + +### OpenID Connect and social authentication + +Version 2.5.0 of Paperless-ngx added support for integrating other authentication systems via +the [django-allauth](https://github.com/pennersr/django-allauth) package. Once set up, users +can either log in or (optionally) sign up using any third party systems you integrate. See the +relevant [configuration settings](configuration.md#PAPERLESS_SOCIALACCOUNT_PROVIDERS) and +[django-allauth docs](https://docs.allauth.org/en/latest/socialaccount/configuration.html) +for more information. + +As an example, to set up login via Github, the following environment variables would need to be +set: + +```conf +PAPERLESS_APPS="allauth.socialaccount.providers.github" +PAPERLESS_SOCIALACCOUNT_PROVIDERS='{"github": {"APPS": [{"provider_id": "github","name": "Github","client_id": "","secret": ""}]}}' +``` + +Or, to use OpenID Connect ("OIDC"), via Keycloak in this example: + +```conf +PAPERLESS_APPS="allauth.socialaccount.providers.openid_connect" +PAPERLESS_SOCIALACCOUNT_PROVIDERS=' +{"openid_connect": {"APPS": [{"provider_id": "keycloak","name": "Keycloak","client_id": "paperless","secret": "","settings": { "server_url": "https:///realms//.well-known/openid-configuration"}}]}}' +``` + +More details about configuration option for various providers can be found in the allauth documentation: https://docs.allauth.org/en/latest/socialaccount/providers/index.html#provider-specifics diff --git a/docs/configuration.md b/docs/configuration.md index f473921cb..3d1b1d1d1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -535,6 +535,42 @@ This is for use with self-signed certificates against local IMAP servers. Settings this value has security implications for the security of your email. Understand what it does and be sure you need to before setting. +#### [`PAPERLESS_SOCIALACCOUNT_PROVIDERS=`](#PAPERLESS_SOCIALACCOUNT_PROVIDERS) {#PAPERLESS_SOCIALACCOUNT_PROVIDERS} + +: This variable is used to setup login and signup via social account providers which are compatible with django-allauth. +See the corresponding [django-allauth documentation](https://docs.allauth.org/en/0.60.0/socialaccount/providers/index.html) +for a list of provider configurations. You will also likely need to include the relevant Django 'application' inside the +[PAPERLESS_APPS](#PAPERLESS_APPS) setting. + + Defaults to None, which does not enable any third party authentication systems. + +#### [`PAPERLESS_SOCIAL_AUTO_SIGNUP=`](#PAPERLESS_SOCIAL_AUTO_SIGNUP) {#PAPERLESS_SOCIAL_AUTO_SIGNUP} + +: Attempt to signup the user using retrieved email, username etc from the third party authentication +system. See the corresponding +[django-allauth documentation](https://docs.allauth.org/en/0.60.0/socialaccount/configuration.html) + + Defaults to False + +#### [`PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS=`](#PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS) {#PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS} + +: Allow users to signup for a new Paperless-ngx account using any setup third party authentication systems. + + Defaults to True + +#### [`PAPERLESS_ACCOUNT_ALLOW_SIGNUPS=`](#PAPERLESS_ACCOUNT_ALLOW_SIGNUPS) {#PAPERLESS_ACCOUNT_ALLOW_SIGNUPS} + +: Allow users to signup for a new Paperless-ngx account. + + Defaults to False + +#### [`PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL=`](#PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL) {#PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL} + +: The protocol used when generating URLs, e.g. login callback URLs. See the corresponding +[django-allauth documentation](https://docs.allauth.org/en/latest/account/configuration.html) + + Defaults to 'https' + ## OCR settings {#ocr} Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/) @@ -905,6 +941,14 @@ documents. Default is none, which disables the temporary directory. +#### [`PAPERLESS_APPS=`](#PAPERLESS_APPS) {#PAPERLESS_APPS} + +: A comma-separated list of Django apps to be included in Django's +[`INSTALLED_APPS`](https://docs.djangoproject.com/en/5.0/ref/applications/). This setting should +be used with caution! + + Defaults to None, which does not add any additional apps. + ## Document Consumption {#consume_config} #### [`PAPERLESS_CONSUMER_DELETE_DUPLICATES=`](#PAPERLESS_CONSUMER_DELETE_DUPLICATES) {#PAPERLESS_CONSUMER_DELETE_DUPLICATES} @@ -1173,6 +1217,55 @@ combination with PAPERLESS_CONSUMER_BARCODE_UPSCALE bigger than 1.0. Defaults to "300" +#### [`PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE=`](#PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE) {#PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE} + +: Enables the detection of barcodes in the scanned document and +assigns or creates tags if a properly formatted barcode is detected. + + The barcode must match one of the (configurable) regular expressions. + If the barcode text contains ',' (comma), it is split into multiple + barcodes which are individually processed for tagging. + + Matching is case insensitive. + + Defaults to false. + +#### [`PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING=`](#PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING) {#PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING} + +: Defines a dictionary of filter regex and substitute expressions. + + Syntax: {"": "" [,...]]} + + A barcode is considered for tagging if the barcode text matches + at least one of the provided pattern. + + If a match is found, the rule is applied. This allows very + versatile reformatting and mapping of barcode pattern to tag values. + + If a tag is not found it will be created. + + Defaults to: + + {"TAG:(.*)": "\\g<1>"} which defines + - a regex TAG:(.*) which includes barcodes beginning with TAG: + followed by any text that gets stored into match group #1 and + - a substitute \\g<1> that replaces the original barcode text + by the content in match group #1. + Consequently, the tag is the barcode text without its TAG: prefix. + + More examples: + + {"ASN12.*": "JOHN", "ASN13.*": "SMITH"} for example maps + - ASN12nnnn barcodes to the tag JOHN and + - ASN13nnnn barcodes to the tag SMITH. + + {"T-J": "JOHN", "T-S": "SMITH", "T-D": "DOE"} directly maps + - T-J barcodes to the tag JOHN, + - T-S barcodes to the tag SMITH and + - T-D barcodes to the tag DOE. + + Please refer to the Python regex documentation for more information. + ## Audit Trail #### [`PAPERLESS_AUDIT_LOG_ENABLED=`](#PAPERLESS_AUDIT_LOG_ENABLED) {#PAPERLESS_AUDIT_LOG_ENABLED} diff --git a/paperless.conf.example b/paperless.conf.example index 1610dcda9..db557a7b6 100644 --- a/paperless.conf.example +++ b/paperless.conf.example @@ -68,6 +68,8 @@ #PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT #PAPERLESS_CONSUMER_BARCODE_UPSCALE=0.0 #PAPERLESS_CONSUMER_BARCODE_DPI=300 +#PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE=false +#PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING={"TAG:(.*)": "\\g<1>"} #PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=false #PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME=double-sided #PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_TIFF_SUPPORT=false diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index a2cea86c1..f2b356d9a 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -458,7 +458,7 @@ src/app/components/admin/settings/settings.component.html - 354 + 346 src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html @@ -498,11 +498,11 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 167 + 159 src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 99 + 92 src/app/components/document-detail/document-detail.component.html @@ -600,7 +600,7 @@ src/app/components/admin/settings/settings.component.html - 342 + 334 src/app/components/admin/tasks/tasks.component.html @@ -1048,11 +1048,11 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 112 + 104 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 131 + 123 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -1075,11 +1075,11 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 120 + 112 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 139 + 131 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -1105,7 +1105,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 145 + 137 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -1346,7 +1346,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 64 + 60 src/app/components/document-detail/document-detail.component.html @@ -1393,7 +1393,7 @@ Delete src/app/components/admin/settings/settings.component.html - 324 + 322 src/app/components/admin/users-groups/users-groups.component.html @@ -1413,7 +1413,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 84 + 80 src/app/components/common/permissions-select/permissions-select.component.html @@ -1475,6 +1475,10 @@ src/app/components/manage/management-list/management-list.component.html 93 + + src/app/components/manage/management-list/management-list.component.ts + 205 + src/app/components/manage/workflows/workflows.component.html 38 @@ -1484,7 +1488,7 @@ No saved views defined. src/app/components/admin/settings/settings.component.html - 336 + 328 @@ -2433,13 +2437,6 @@ 55 - - Are you sure? - - src/app/components/common/confirm-button/confirm-button.component.ts - 20 - - Confirmation @@ -2518,7 +2515,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 166 + 158 src/app/components/common/permissions-dialog/permissions-dialog.component.html @@ -2526,7 +2523,7 @@ src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 98 + 91 src/app/components/common/select-dialog/select-dialog.component.html @@ -2702,7 +2699,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 190 + 182 @@ -3068,7 +3065,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 100 + 92 @@ -3086,7 +3083,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 101 + 93 @@ -3104,7 +3101,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 164 + 156 src/app/components/common/toasts/toasts.component.html @@ -3438,175 +3435,175 @@ Apply Actions: src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 70 + 66 Add Action src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 72 + 68 Action type src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 94 + 86 Assign title src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 98 + 90 Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>. src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 98 + 90 Assign tags src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 99 + 91 Assign storage path src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 102 + 94 Assign custom fields src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 103 + 95 Assign owner src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 106 + 98 Assign view permissions src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 108 + 100 Assign edit permissions src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 127 + 119 Trigger type src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 174 + 166 Trigger for documents that match all filters specified below. src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 175 + 167 Filter filename src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 178 + 170 Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive. src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 178 + 170 Filter sources src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 180 + 172 Filter path src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 181 + 173 Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a> src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 181 + 173 Filter mail rule src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 182 + 174 Apply to documents consumed via this mail rule. src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 182 + 174 Content matching algorithm src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 185 + 177 Content matching pattern src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 187 + 179 Has tags src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 196 + 188 Has correspondent src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 197 + 189 Has document type src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html - 198 + 190 @@ -4085,14 +4082,14 @@ Regenerate auth token src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 45 + 44 Copied! src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 53 + 48 src/app/components/common/share-links-dropdown/share-links-dropdown.component.html @@ -4103,21 +4100,28 @@ Warning: changing the token cannot be undone src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 55 + 50 Connected social accounts src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 59 + 54 Set a password before disconnecting social account. src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 63 + 58 + + + + Disconnect social account + + src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html + 68 @@ -4127,25 +4131,18 @@ 69 - - Disconnect social account - - src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 71 - - Warning: disconnecting social accounts cannot be undone src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 81 + 74 Connect new social account src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html - 86 + 79 @@ -4995,6 +4992,10 @@ src/app/components/document-detail/document-detail.component.ts 717 + + src/app/components/manage/management-list/management-list.component.ts + 201 + Do you really want to delete document ""? @@ -6187,7 +6188,7 @@ src/app/components/manage/management-list/management-list.component.ts - 285 + 301 @@ -6270,26 +6271,26 @@ {VAR_PLURAL, plural, =1 {One } other { total }} src/app/components/manage/management-list/management-list.component.html - 113 + 107 src/app/components/manage/management-list/management-list.component.html - 113 + 107 src/app/components/manage/management-list/management-list.component.html - 113 + 107 src/app/components/manage/management-list/management-list.component.html - 113 + 107 Automatic src/app/components/manage/management-list/management-list.component.ts - 110 + 113 src/app/data/matching-model.ts @@ -6300,7 +6301,7 @@ None src/app/components/manage/management-list/management-list.component.ts - 112 + 115 src/app/data/matching-model.ts @@ -6311,42 +6312,49 @@ Successfully created . src/app/components/manage/management-list/management-list.component.ts - 155 + 158 Error occurred while creating . src/app/components/manage/management-list/management-list.component.ts - 160 + 163 Successfully updated . src/app/components/manage/management-list/management-list.component.ts - 175 + 178 Error occurred while saving . src/app/components/manage/management-list/management-list.component.ts - 180 + 183 + + + + Associated documents will not be deleted. + + src/app/components/manage/management-list/management-list.component.ts + 203 Error while deleting element src/app/components/manage/management-list/management-list.component.ts - 204 + 219 Permissions updated successfully src/app/components/manage/management-list/management-list.component.ts - 278 + 294 diff --git a/src-ui/src/app/components/admin/users-groups/users-groups.component.html b/src-ui/src/app/components/admin/users-groups/users-groups.component.html index 3f91842d4..a485a97ef 100644 --- a/src-ui/src/app/components/admin/users-groups/users-groups.component.html +++ b/src-ui/src/app/components/admin/users-groups/users-groups.component.html @@ -33,64 +33,64 @@
+ +
+ + + + } + +} + +@if (groups) { +

+ Groups + +

+ @if (groups.length > 0) { +
    +
  • +
    +
    Name
    +
    +
    +
    Actions
    +
    +
  • + @for (group of groups; track group) { +
  • +
    +
    +
    +
    +
    +
    + - -
    +
    -
  • - } -
- } - - @if (groups) { -

- Groups - -

- @if (groups.length > 0) { -
    -
  • -
    -
    Name
    -
    -
    -
    Actions
    -
    -
  • - @for (group of groups; track group) { -
  • -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
  • - } - @if (groups.length === 0) { -
  • No groups defined
  • - } -
- } - } - - @if (!users || !groups) { -
-
-
Loading...
- } + + } + @if (groups.length === 0) { +
  • No groups defined
  • + } + + } +} + +@if (!users || !groups) { +
    +
    +
    Loading...
    +
    +} diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html index 599faa988..cac217716 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html @@ -45,10 +45,18 @@ } @if (editing) { - + @if ((selectionModel.itemsSorted | filter: filterText).length === 0 && createRef !== undefined) { + + } + @if ((selectionModel.itemsSorted | filter: filterText).length > 0) { + + } } @if (!editing && manyToOne) {
    diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts index f88667f34..58aa029ee 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts @@ -500,4 +500,46 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => selectionModel.apply() expect(selectionModel.itemsSorted).toEqual([nullItem, items[1], items[0]]) }) + + it('should set support create, keep open model and call createRef method', fakeAsync(() => { + component.items = items + component.icon = 'tag-fill' + component.selectionModel = selectionModel + fixture.nativeElement + .querySelector('button') + .dispatchEvent(new MouseEvent('click')) // open + fixture.detectChanges() + tick(100) + + component.filterText = 'Test Filter Text' + component.createRef = jest.fn() + component.createClicked() + expect(component.creating).toBeTruthy() + expect(component.createRef).toHaveBeenCalledWith('Test Filter Text') + const openSpy = jest.spyOn(component.dropdown, 'open') + component.dropdownOpenChange(false) + expect(openSpy).toHaveBeenCalled() // should keep open + })) + + it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => { + component.items = items + component.icon = 'tag-fill' + component.editing = true + component.createRef = jest.fn() + const createSpy = jest.spyOn(component, 'createClicked') + expect(component.selectionModel.getSelectedItems()).toEqual([]) + fixture.nativeElement + .querySelector('button') + .dispatchEvent(new MouseEvent('click')) // open + fixture.detectChanges() + tick(100) + component.filterText = 'FooBar' + fixture.detectChanges() + component.listFilterTextInput.nativeElement.dispatchEvent( + new KeyboardEvent('keyup', { key: 'Enter' }) + ) + expect(component.selectionModel.getSelectedItems()).toEqual([]) + tick(300) + expect(createSpy).toHaveBeenCalled() + })) }) diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts index 26b036db9..bb1a9da27 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts @@ -398,6 +398,11 @@ export class FilterableDropdownComponent { @Input() disabled = false + @Input() + createRef: (name) => void + + creating: boolean = false + @Output() apply = new EventEmitter() @@ -437,6 +442,11 @@ export class FilterableDropdownComponent { } } + createClicked() { + this.creating = true + this.createRef(this.filterText) + } + dropdownOpenChange(open: boolean): void { if (open) { setTimeout(() => { @@ -448,9 +458,14 @@ export class FilterableDropdownComponent { } this.opened.next(this) } else { - this.filterText = '' - if (this.applyOnClose && this.selectionModel.isDirty()) { - this.apply.emit(this.selectionModel.diff()) + if (this.creating) { + this.dropdown.open() + this.creating = false + } else { + this.filterText = '' + if (this.applyOnClose && this.selectionModel.isDirty()) { + this.apply.emit(this.selectionModel.diff()) + } } } } @@ -466,6 +481,8 @@ export class FilterableDropdownComponent { this.dropdown.close() } }, 200) + } else if (filtered.length == 0 && this.createRef) { + this.createClicked() } } diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html index 0c261df67..686c07bb3 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -25,6 +25,7 @@ [editing]="true" [manyToOne]="true" [applyOnClose]="applyOnClose" + [createRef]="createTag.bind(this)" (opened)="openTagsDropdown()" [(selectionModel)]="tagSelectionModel" [documentCounts]="tagDocumentCounts" @@ -38,6 +39,7 @@ [disabled]="!userCanEditAll" [editing]="true" [applyOnClose]="applyOnClose" + [createRef]="createCorrespondent.bind(this)" (opened)="openCorrespondentDropdown()" [(selectionModel)]="correspondentSelectionModel" [documentCounts]="correspondentDocumentCounts" @@ -51,6 +53,7 @@ [disabled]="!userCanEditAll" [editing]="true" [applyOnClose]="applyOnClose" + [createRef]="createDocumentType.bind(this)" (opened)="openDocumentTypeDropdown()" [(selectionModel)]="documentTypeSelectionModel" [documentCounts]="documentTypeDocumentCounts" @@ -64,6 +67,7 @@ [disabled]="!userCanEditAll" [editing]="true" [applyOnClose]="applyOnClose" + [createRef]="createStoragePath.bind(this)" (opened)="openStoragePathDropdown()" [(selectionModel)]="storagePathsSelectionModel" [documentCounts]="storagePathDocumentCounts" diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts index 42f8b6d1d..4da9f36df 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts @@ -42,6 +42,16 @@ import { NgSelectModule } from '@ng-select/ng-select' import { GroupService } from 'src/app/services/rest/group.service' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { SwitchComponent } from '../../common/input/switch/switch.component' +import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' +import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' +import { Results } from 'src/app/data/results' +import { Tag } from 'src/app/data/tag' +import { Correspondent } from 'src/app/data/correspondent' +import { DocumentType } from 'src/app/data/document-type' +import { StoragePath } from 'src/app/data/storage-path' +import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' +import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component' +import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' const selectionData: SelectionData = { selected_tags: [ @@ -65,6 +75,10 @@ describe('BulkEditorComponent', () => { let documentService: DocumentService let toastService: ToastService let modalService: NgbModal + let tagService: TagService + let correspondentsService: CorrespondentService + let documentTypeService: DocumentTypeService + let storagePathService: StoragePathService let httpTestingController: HttpTestingController beforeEach(async () => { @@ -165,6 +179,10 @@ describe('BulkEditorComponent', () => { documentService = TestBed.inject(DocumentService) toastService = TestBed.inject(ToastService) modalService = TestBed.inject(NgbModal) + tagService = TestBed.inject(TagService) + correspondentsService = TestBed.inject(CorrespondentService) + documentTypeService = TestBed.inject(DocumentTypeService) + storagePathService = TestBed.inject(StoragePathService) httpTestingController = TestBed.inject(HttpTestingController) fixture = TestBed.createComponent(BulkEditorComponent) @@ -902,4 +920,180 @@ describe('BulkEditorComponent', () => { `${environment.apiBaseUrl}documents/storage_paths/` ) }) + + it('should support create new tag', () => { + const name = 'New Tag' + const newTag = { id: 101, name: 'New Tag' } + const tags: Results = { + results: [ + { id: 1, name: 'Tag 1' }, + { id: 2, name: 'Tag 2' }, + ], + count: 2, + all: [1, 2], + } + + const modalInstance = { + componentInstance: { + dialogMode: EditDialogMode.CREATE, + object: { name }, + succeeded: of(newTag), + }, + } + const tagListAllSpy = jest.spyOn(tagService, 'listAll') + tagListAllSpy.mockReturnValue(of(tags)) + + const tagSelectionModelToggleSpy = jest.spyOn( + component.tagSelectionModel, + 'toggle' + ) + + const modalServiceOpenSpy = jest.spyOn(modalService, 'open') + modalServiceOpenSpy.mockReturnValue(modalInstance as any) + + component.createTag(name) + + expect(modalServiceOpenSpy).toHaveBeenCalledWith(TagEditDialogComponent, { + backdrop: 'static', + }) + expect(tagListAllSpy).toHaveBeenCalled() + + expect(tagSelectionModelToggleSpy).toHaveBeenCalledWith(newTag.id) + expect(component.tags).toEqual(tags.results) + }) + + it('should support create new correspondent', () => { + const name = 'New Correspondent' + const newCorrespondent = { id: 101, name: 'New Correspondent' } + const correspondents: Results = { + results: [ + { id: 1, name: 'Correspondent 1' }, + { id: 2, name: 'Correspondent 2' }, + ], + count: 2, + all: [1, 2], + } + + const modalInstance = { + componentInstance: { + dialogMode: EditDialogMode.CREATE, + object: { name }, + succeeded: of(newCorrespondent), + }, + } + const correspondentsListAllSpy = jest.spyOn( + correspondentsService, + 'listAll' + ) + correspondentsListAllSpy.mockReturnValue(of(correspondents)) + + const correspondentSelectionModelToggleSpy = jest.spyOn( + component.correspondentSelectionModel, + 'toggle' + ) + + const modalServiceOpenSpy = jest.spyOn(modalService, 'open') + modalServiceOpenSpy.mockReturnValue(modalInstance as any) + + component.createCorrespondent(name) + + expect(modalServiceOpenSpy).toHaveBeenCalledWith( + CorrespondentEditDialogComponent, + { backdrop: 'static' } + ) + expect(correspondentsListAllSpy).toHaveBeenCalled() + + expect(correspondentSelectionModelToggleSpy).toHaveBeenCalledWith( + newCorrespondent.id + ) + expect(component.correspondents).toEqual(correspondents.results) + }) + + it('should support create new document type', () => { + const name = 'New Document Type' + const newDocumentType = { id: 101, name: 'New Document Type' } + const documentTypes: Results = { + results: [ + { id: 1, name: 'Document Type 1' }, + { id: 2, name: 'Document Type 2' }, + ], + count: 2, + all: [1, 2], + } + + const modalInstance = { + componentInstance: { + dialogMode: EditDialogMode.CREATE, + object: { name }, + succeeded: of(newDocumentType), + }, + } + const documentTypesListAllSpy = jest.spyOn(documentTypeService, 'listAll') + documentTypesListAllSpy.mockReturnValue(of(documentTypes)) + + const documentTypeSelectionModelToggleSpy = jest.spyOn( + component.documentTypeSelectionModel, + 'toggle' + ) + + const modalServiceOpenSpy = jest.spyOn(modalService, 'open') + modalServiceOpenSpy.mockReturnValue(modalInstance as any) + + component.createDocumentType(name) + + expect(modalServiceOpenSpy).toHaveBeenCalledWith( + DocumentTypeEditDialogComponent, + { backdrop: 'static' } + ) + expect(documentTypesListAllSpy).toHaveBeenCalled() + + expect(documentTypeSelectionModelToggleSpy).toHaveBeenCalledWith( + newDocumentType.id + ) + expect(component.documentTypes).toEqual(documentTypes.results) + }) + + it('should support create new storage path', () => { + const name = 'New Storage Path' + const newStoragePath = { id: 101, name: 'New Storage Path' } + const storagePaths: Results = { + results: [ + { id: 1, name: 'Storage Path 1' }, + { id: 2, name: 'Storage Path 2' }, + ], + count: 2, + all: [1, 2], + } + + const modalInstance = { + componentInstance: { + dialogMode: EditDialogMode.CREATE, + object: { name }, + succeeded: of(newStoragePath), + }, + } + const storagePathsListAllSpy = jest.spyOn(storagePathService, 'listAll') + storagePathsListAllSpy.mockReturnValue(of(storagePaths)) + + const storagePathsSelectionModelToggleSpy = jest.spyOn( + component.storagePathsSelectionModel, + 'toggle' + ) + + const modalServiceOpenSpy = jest.spyOn(modalService, 'open') + modalServiceOpenSpy.mockReturnValue(modalInstance as any) + + component.createStoragePath(name) + + expect(modalServiceOpenSpy).toHaveBeenCalledWith( + StoragePathEditDialogComponent, + { backdrop: 'static' } + ) + expect(storagePathsListAllSpy).toHaveBeenCalled() + + expect(storagePathsSelectionModelToggleSpy).toHaveBeenCalledWith( + newStoragePath.id + ) + expect(component.storagePaths).toEqual(storagePaths.results) + }) }) diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 49d4c070f..0bfb287cb 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -33,7 +33,12 @@ import { PermissionType, } from 'src/app/services/permissions.service' import { FormControl, FormGroup } from '@angular/forms' -import { first, Subject, takeUntil } from 'rxjs' +import { first, map, Subject, switchMap, takeUntil } from 'rxjs' +import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' +import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' +import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' +import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component' +import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' @Component({ selector: 'pngx-bulk-editor', @@ -479,6 +484,92 @@ export class BulkEditorComponent } } + createTag(name: string) { + let modal = this.modalService.open(TagEditDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.dialogMode = EditDialogMode.CREATE + modal.componentInstance.object = { name } + modal.componentInstance.succeeded + .pipe( + switchMap((newTag) => { + return this.tagService + .listAll() + .pipe(map((tags) => ({ newTag, tags }))) + }) + ) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(({ newTag, tags }) => { + this.tags = tags.results + this.tagSelectionModel.toggle(newTag.id) + }) + } + + createCorrespondent(name: string) { + let modal = this.modalService.open(CorrespondentEditDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.dialogMode = EditDialogMode.CREATE + modal.componentInstance.object = { name } + modal.componentInstance.succeeded + .pipe( + switchMap((newCorrespondent) => { + return this.correspondentService + .listAll() + .pipe( + map((correspondents) => ({ newCorrespondent, correspondents })) + ) + }) + ) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(({ newCorrespondent, correspondents }) => { + this.correspondents = correspondents.results + this.correspondentSelectionModel.toggle(newCorrespondent.id) + }) + } + + createDocumentType(name: string) { + let modal = this.modalService.open(DocumentTypeEditDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.dialogMode = EditDialogMode.CREATE + modal.componentInstance.object = { name } + modal.componentInstance.succeeded + .pipe( + switchMap((newDocumentType) => { + return this.documentTypeService + .listAll() + .pipe(map((documentTypes) => ({ newDocumentType, documentTypes }))) + }) + ) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(({ newDocumentType, documentTypes }) => { + this.documentTypes = documentTypes.results + this.documentTypeSelectionModel.toggle(newDocumentType.id) + }) + } + + createStoragePath(name: string) { + let modal = this.modalService.open(StoragePathEditDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.dialogMode = EditDialogMode.CREATE + modal.componentInstance.object = { name } + modal.componentInstance.succeeded + .pipe( + switchMap((newStoragePath) => { + return this.storagePathService + .listAll() + .pipe(map((storagePaths) => ({ newStoragePath, storagePaths }))) + }) + ) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(({ newStoragePath, storagePaths }) => { + this.storagePaths = storagePaths.results + this.storagePathsSelectionModel.toggle(newStoragePath.id) + }) + } + applyDelete() { let modal = this.modalService.open(ConfirmDialogComponent, { backdrop: 'static', diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index cdbd88825..ea9ba9914 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -25,7 +25,7 @@ @if (notesEnabled && document.notes.length) { - + {{document.notes.length}} } @@ -43,14 +43,14 @@ @if (document.document_type) { } @if (document.storage_path) { } @@ -63,25 +63,25 @@
    - + {{document.created_date | customDate:'mediumDate'}}
    @if (document.archive_serial_number | isNumber) {
    - + #{{document.archive_serial_number}}
    } @if (document.owner && document.owner !== settingsService.currentUser.id) {
    - + {{document.owner | username}}
    } @if (document.is_shared_by_requester) {
    - + Shared
    } diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 5de7ff6e7..5409306c6 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -232,7 +232,7 @@ @if (d.notes.length) { - + {{d.notes.length}} } diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html index a803aae9c..2d4c77f93 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html @@ -29,16 +29,16 @@
    - -
    - + + - - } - @if (fields.length === 0) { -
  • No fields defined.
  • - } - + + + + } + @if (fields.length === 0) { +
  • No fields defined.
  • + } + diff --git a/src-ui/src/app/components/manage/mail/mail.component.html b/src-ui/src/app/components/manage/mail/mail.component.html index 68187c6bd..ef294fe43 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.html +++ b/src-ui/src/app/components/manage/mail/mail.component.html @@ -32,72 +32,72 @@
    - - -
    - - - - } - @if (mailAccounts.length === 0) { -
  • No mail accounts defined.
  • - } - - - - - -

    - Mail rules - -

    -
      -
    • -
      -
      Name
      -
      Account
      -
      Actions
      + + +
      -
    • + + + + } + @if (mailAccounts.length === 0) { +
    • No mail accounts defined.
    • + } +
    - @for (rule of mailRules; track rule) { -
  • -
    -
    -
    {{(mailAccountService.getCached(rule.account) | async)?.name}}
    -
    -
    - - - -
    -
    -
    -
  • - } - @if (mailRules.length === 0) { -
  • No mail rules defined.
  • - } - +
    - + +

    + Mail rules + +

    +
      +
    • +
      +
      Name
      +
      Account
      +
      Actions
      +
      +
    • - @if (!mailAccounts || !mailRules) { -
      -
      -
      Loading...
      -
      - } + @for (rule of mailRules; track rule) { +
    • +
      +
      +
      {{(mailAccountService.getCached(rule.account) | async)?.name}}
      +
      +
      + + + +
      +
      +
      +
    • + } + @if (mailRules.length === 0) { +
    • No mail rules defined.
    • + } +
    + +
    + +@if (!mailAccounts || !mailRules) { +
    +
    +
    Loading...
    +
    +} diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.html b/src-ui/src/app/components/manage/workflows/workflows.component.html index 9dc214af4..9bc68fb49 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.html +++ b/src-ui/src/app/components/manage/workflows/workflows.component.html @@ -33,16 +33,16 @@
    - -
    - + + - - } - @if (workflows.length === 0) { -
  • No workflows defined.
  • - } - + + + + } + @if (workflows.length === 0) { +
  • No workflows defined.
  • + } + diff --git a/src/documents/barcodes.py b/src/documents/barcodes.py index 606451f84..4bfb9b791 100644 --- a/src/documents/barcodes.py +++ b/src/documents/barcodes.py @@ -14,6 +14,7 @@ from PIL import Image from documents.converters import convert_from_tiff_to_pdf from documents.data_models import ConsumableDocument +from documents.models import Tag from documents.plugins.base import ConsumeTaskPlugin from documents.plugins.base import StopConsumeTaskError from documents.plugins.helpers import ProgressStatusOptions @@ -65,7 +66,9 @@ class BarcodePlugin(ConsumeTaskPlugin): supported_mimes = {"application/pdf"} return ( - settings.CONSUMER_ENABLE_ASN_BARCODE or settings.CONSUMER_ENABLE_BARCODES + settings.CONSUMER_ENABLE_ASN_BARCODE + or settings.CONSUMER_ENABLE_BARCODES + or settings.CONSUMER_ENABLE_TAG_BARCODE ) and self.input_doc.mime_type in supported_mimes def setup(self): @@ -90,6 +93,16 @@ class BarcodePlugin(ConsumeTaskPlugin): logger.info(f"Found ASN in barcode: {located_asn}") self.metadata.asn = located_asn + # try reading tags from barcodes + if settings.CONSUMER_ENABLE_TAG_BARCODE: + tags = self.tags + if tags is not None and len(tags) > 0: + if self.metadata.tag_ids: + self.metadata.tag_ids += tags + else: + self.metadata.tag_ids = tags + logger.info(f"Found tags in barcode: {tags}") + separator_pages = self.get_separation_pages() if not separator_pages: return "No pages to split on!" @@ -279,6 +292,53 @@ class BarcodePlugin(ConsumeTaskPlugin): return asn + @property + def tags(self) -> Optional[list[int]]: + """ + Search the parsed barcodes for any tags. + Returns the detected tag ids (or empty list) + """ + tags = [] + + # Ensure the barcodes have been read + self.detect() + + for x in self.barcodes: + tag_texts = x.value + + for raw in tag_texts.split(","): + try: + tag = None + for regex in settings.CONSUMER_TAG_BARCODE_MAPPING: + if re.match(regex, raw, flags=re.IGNORECASE): + sub = settings.CONSUMER_TAG_BARCODE_MAPPING[regex] + tag = ( + re.sub(regex, sub, raw, flags=re.IGNORECASE) + if sub + else raw + ) + break + + if tag: + tag = Tag.objects.get_or_create( + name__iexact=tag, + defaults={"name": tag}, + )[0] + + logger.debug( + f"Found Tag Barcode '{raw}', substituted " + f"to '{tag}' and mapped to " + f"tag #{tag.pk}.", + ) + tags.append(tag.pk) + + except Exception as e: + logger.error( + f"Failed to find or create TAG '{raw}' because: {e}", + ) + + return tags + def get_separation_pages(self) -> dict[int, bool]: """ Search the parsed barcodes for separators and returns a dict of page diff --git a/src/documents/caching.py b/src/documents/caching.py index 9b8607dd8..d80f319f7 100644 --- a/src/documents/caching.py +++ b/src/documents/caching.py @@ -90,7 +90,6 @@ def set_suggestions_cache( """ if classifier is not None: doc_key = get_suggestion_cache_key(document_id) - print(classifier.last_auto_type_hash) cache.set( doc_key, SuggestionCacheData( diff --git a/src/documents/classifier.py b/src/documents/classifier.py index 6180a8671..aa0eb70b6 100644 --- a/src/documents/classifier.py +++ b/src/documents/classifier.py @@ -4,11 +4,14 @@ import pickle import re import warnings from collections.abc import Iterator -from datetime import datetime from hashlib import sha256 -from pathlib import Path +from typing import TYPE_CHECKING from typing import Optional +if TYPE_CHECKING: + from datetime import datetime + from pathlib import Path + from django.conf import settings from django.core.cache import cache from sklearn.exceptions import InconsistentVersionWarning diff --git a/src/documents/management/commands/document_retagger.py b/src/documents/management/commands/document_retagger.py index dda3ecebc..10bb54b71 100644 --- a/src/documents/management/commands/document_retagger.py +++ b/src/documents/management/commands/document_retagger.py @@ -69,8 +69,6 @@ class Command(ProgressBarMixin, BaseCommand): def handle(self, *args, **options): self.handle_progress_bar_mixin(**options) - # Detect if we support color - color = self.style.ERROR("test") != "test" if options["inbox_only"]: queryset = Document.objects.filter(tags__is_inbox_tag=True) @@ -96,7 +94,8 @@ class Command(ProgressBarMixin, BaseCommand): use_first=options["use_first"], suggest=options["suggest"], base_url=options["base_url"], - color=color, + stdout=self.stdout, + style_func=self.style, ) if options["document_type"]: @@ -108,7 +107,8 @@ class Command(ProgressBarMixin, BaseCommand): use_first=options["use_first"], suggest=options["suggest"], base_url=options["base_url"], - color=color, + stdout=self.stdout, + style_func=self.style, ) if options["tags"]: @@ -119,7 +119,8 @@ class Command(ProgressBarMixin, BaseCommand): replace=options["overwrite"], suggest=options["suggest"], base_url=options["base_url"], - color=color, + stdout=self.stdout, + style_func=self.style, ) if options["storage_path"]: set_storage_path( @@ -130,5 +131,6 @@ class Command(ProgressBarMixin, BaseCommand): use_first=options["use_first"], suggest=options["suggest"], base_url=options["base_url"], - color=color, + stdout=self.stdout, + style_func=self.style, ) diff --git a/src/documents/management/commands/document_thumbnails.py b/src/documents/management/commands/document_thumbnails.py index ecd265102..d4653f0b3 100644 --- a/src/documents/management/commands/document_thumbnails.py +++ b/src/documents/management/commands/document_thumbnails.py @@ -19,7 +19,7 @@ def _process_document(doc_id): if parser_class: parser = parser_class(logging_group=None) else: - print(f"{document} No parser for mime type {document.mime_type}") + print(f"{document} No parser for mime type {document.mime_type}") # noqa: T201 return try: diff --git a/src/documents/plugins/helpers.py b/src/documents/plugins/helpers.py index 92fe1255b..27d03f30f 100644 --- a/src/documents/plugins/helpers.py +++ b/src/documents/plugins/helpers.py @@ -5,7 +5,9 @@ from typing import Union from asgiref.sync import async_to_sync from channels.layers import get_channel_layer -from channels_redis.pubsub import RedisPubSubChannelLayer + +if TYPE_CHECKING: + from channels_redis.pubsub import RedisPubSubChannelLayer class ProgressStatusOptions(str, enum.Enum): diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 2b717e042..c8657ce1d 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -18,7 +18,6 @@ from django.db import close_old_connections from django.db import models from django.db.models import Q from django.dispatch import receiver -from django.utils import termcolors from django.utils import timezone from filelock import FileLock @@ -54,6 +53,26 @@ def add_inbox_tags(sender, document: Document, logging_group=None, **kwargs): document.tags.add(*inbox_tags) +def _suggestion_printer( + stdout, + style_func, + suggestion_type: str, + document: Document, + selected: MatchingModel, + base_url: Optional[str] = None, +): + """ + Smaller helper to reduce duplication when just outputting suggestions to the console + """ + doc_str = str(document) + if base_url is not None: + stdout.write(style_func.SUCCESS(doc_str)) + stdout.write(style_func.SUCCESS(f"{base_url}/documents/{document.pk}")) + else: + stdout.write(style_func.SUCCESS(f"{doc_str} [{document.pk}]")) + stdout.write(f"Suggest {suggestion_type}: {selected}") + + def set_correspondent( sender, document: Document, @@ -63,7 +82,8 @@ def set_correspondent( use_first=True, suggest=False, base_url=None, - color=False, + stdout=None, + style_func=None, **kwargs, ): if document.correspondent and not replace: @@ -90,23 +110,14 @@ def set_correspondent( if selected or replace: if suggest: - if base_url: - print( - termcolors.colorize(str(document), fg="green") - if color - else str(document), - ) - print(f"{base_url}/documents/{document.pk}") - else: - print( - ( - termcolors.colorize(str(document), fg="green") - if color - else str(document) - ) - + f" [{document.pk}]", - ) - print(f"Suggest correspondent {selected}") + _suggestion_printer( + stdout, + style_func, + "correspondent", + document, + selected, + base_url, + ) else: logger.info( f"Assigning correspondent {selected} to {document}", @@ -126,7 +137,8 @@ def set_document_type( use_first=True, suggest=False, base_url=None, - color=False, + stdout=None, + style_func=None, **kwargs, ): if document.document_type and not replace: @@ -154,23 +166,14 @@ def set_document_type( if selected or replace: if suggest: - if base_url: - print( - termcolors.colorize(str(document), fg="green") - if color - else str(document), - ) - print(f"{base_url}/documents/{document.pk}") - else: - print( - ( - termcolors.colorize(str(document), fg="green") - if color - else str(document) - ) - + f" [{document.pk}]", - ) - print(f"Suggest document type {selected}") + _suggestion_printer( + stdout, + style_func, + "document type", + document, + selected, + base_url, + ) else: logger.info( f"Assigning document type {selected} to {document}", @@ -189,7 +192,8 @@ def set_tags( replace=False, suggest=False, base_url=None, - color=False, + stdout=None, + style_func=None, **kwargs, ): if replace: @@ -212,26 +216,16 @@ def set_tags( ] if not relevant_tags and not extra_tags: return + doc_str = style_func.SUCCESS(str(document)) if base_url: - print( - termcolors.colorize(str(document), fg="green") - if color - else str(document), - ) - print(f"{base_url}/documents/{document.pk}") + stdout.write(doc_str) + stdout.write(f"{base_url}/documents/{document.pk}") else: - print( - ( - termcolors.colorize(str(document), fg="green") - if color - else str(document) - ) - + f" [{document.pk}]", - ) + stdout.write(doc_str + style_func.SUCCESS(f" [{document.pk}]")) if relevant_tags: - print("Suggest tags: " + ", ".join([t.name for t in relevant_tags])) + stdout.write("Suggest tags: " + ", ".join([t.name for t in relevant_tags])) if extra_tags: - print("Extra tags: " + ", ".join([t.name for t in extra_tags])) + stdout.write("Extra tags: " + ", ".join([t.name for t in extra_tags])) else: if not relevant_tags: return @@ -254,7 +248,8 @@ def set_storage_path( use_first=True, suggest=False, base_url=None, - color=False, + stdout=None, + style_func=None, **kwargs, ): if document.storage_path and not replace: @@ -285,23 +280,14 @@ def set_storage_path( if selected or replace: if suggest: - if base_url: - print( - termcolors.colorize(str(document), fg="green") - if color - else str(document), - ) - print(f"{base_url}/documents/{document.pk}") - else: - print( - ( - termcolors.colorize(str(document), fg="green") - if color - else str(document) - ) - + f" [{document.pk}]", - ) - print(f"Suggest storage directory {selected}") + _suggestion_printer( + stdout, + style_func, + "storage directory", + document, + selected, + base_url, + ) else: logger.info( f"Assigning storage path {selected} to {document}", diff --git a/src/documents/tests/test_api_bulk_download.py b/src/documents/tests/test_api_bulk_download.py index 57912c65c..43299b77d 100644 --- a/src/documents/tests/test_api_bulk_download.py +++ b/src/documents/tests/test_api_bulk_download.py @@ -246,8 +246,6 @@ class TestBulkDownload(DirectoriesMixin, APITestCase): self.doc3.title = "Title 2 - Doc 3" self.doc3.save() - print(self.doc3.archive_path) - print(self.doc3.archive_filename) response = self.client.post( self.ENDPOINT, diff --git a/src/documents/tests/test_barcodes.py b/src/documents/tests/test_barcodes.py index 4552a2b77..3dd6d62ff 100644 --- a/src/documents/tests/test_barcodes.py +++ b/src/documents/tests/test_barcodes.py @@ -14,6 +14,7 @@ from documents.barcodes import BarcodePlugin from documents.data_models import ConsumableDocument from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentSource +from documents.models import Tag from documents.tests.utils import DirectoriesMixin from documents.tests.utils import DocumentConsumeDelayMixin from documents.tests.utils import DummyProgressManager @@ -741,3 +742,125 @@ class TestBarcodeZxing(TestBarcode): @override_settings(CONSUMER_BARCODE_SCANNER="ZXING") class TestAsnBarcodesZxing(TestAsnBarcode): pass + + +class TestTagBarcode(DirectoriesMixin, SampleDirMixin, GetReaderPluginMixin, TestCase): + @contextmanager + def get_reader(self, filepath: Path) -> BarcodePlugin: + reader = BarcodePlugin( + ConsumableDocument(DocumentSource.ConsumeFolder, original_file=filepath), + DocumentMetadataOverrides(), + DummyProgressManager(filepath.name, None), + self.dirs.scratch_dir, + "task-id", + ) + reader.setup() + yield reader + reader.cleanup() + + @override_settings(CONSUMER_ENABLE_TAG_BARCODE=True) + def test_scan_file_without_matching_barcodes(self): + """ + GIVEN: + - PDF containing tag barcodes but none with matching prefix (default "TAG:") + WHEN: + - File is scanned for barcodes + THEN: + - No TAG has been created + """ + test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-custom-prefix.pdf" + with self.get_reader(test_file) as reader: + reader.run() + tags = reader.metadata.tag_ids + self.assertEqual(tags, None) + + @override_settings( + CONSUMER_ENABLE_TAG_BARCODE=False, + CONSUMER_TAG_BARCODE_MAPPING={"CUSTOM-PREFIX-(.*)": "\\g<1>"}, + ) + def test_scan_file_with_matching_barcode_but_function_disabled(self): + """ + GIVEN: + - PDF containing a tag barcode with matching custom prefix + - The tag barcode functionality is disabled + WHEN: + - File is scanned for barcodes + THEN: + - No TAG has been created + """ + test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-custom-prefix.pdf" + with self.get_reader(test_file) as reader: + reader.run() + tags = reader.metadata.tag_ids + self.assertEqual(tags, None) + + @override_settings( + CONSUMER_ENABLE_TAG_BARCODE=True, + CONSUMER_TAG_BARCODE_MAPPING={"CUSTOM-PREFIX-(.*)": "\\g<1>"}, + ) + def test_scan_file_for_tag_custom_prefix(self): + """ + GIVEN: + - PDF containing a tag barcode with custom prefix + - The barcode mapping accepts this prefix and removes it from the mapped tag value + - The created tag is the non-prefixed values + WHEN: + - File is scanned for barcodes + THEN: + - The TAG is located + - One TAG has been created + """ + test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-custom-prefix.pdf" + with self.get_reader(test_file) as reader: + reader.metadata.tag_ids = [99] + reader.run() + self.assertEqual(reader.pdf_file, test_file) + tags = reader.metadata.tag_ids + self.assertEqual(len(tags), 2) + self.assertEqual(tags[0], 99) + self.assertEqual(Tag.objects.get(name__iexact="00123").pk, tags[1]) + + @override_settings( + CONSUMER_ENABLE_TAG_BARCODE=True, + CONSUMER_TAG_BARCODE_MAPPING={"ASN(.*)": "\\g<1>"}, + ) + def test_scan_file_for_many_custom_tags(self): + """ + GIVEN: + - PDF containing multiple tag barcode with custom prefix + - The barcode mapping accepts this prefix and removes it from the mapped tag value + - The created tags are the non-prefixed values + WHEN: + - File is scanned for barcodes + THEN: + - The TAG is located + - File Tags have been created + """ + test_file = self.BARCODE_SAMPLE_DIR / "split-by-asn-1.pdf" + with self.get_reader(test_file) as reader: + reader.run() + tags = reader.metadata.tag_ids + self.assertEqual(len(tags), 5) + self.assertEqual(Tag.objects.get(name__iexact="00123").pk, tags[0]) + self.assertEqual(Tag.objects.get(name__iexact="00124").pk, tags[1]) + self.assertEqual(Tag.objects.get(name__iexact="00125").pk, tags[2]) + self.assertEqual(Tag.objects.get(name__iexact="00126").pk, tags[3]) + self.assertEqual(Tag.objects.get(name__iexact="00127").pk, tags[4]) + + @override_settings( + CONSUMER_ENABLE_TAG_BARCODE=True, + CONSUMER_TAG_BARCODE_MAPPING={"CUSTOM-PREFIX-(.*)": "\\g<3>"}, + ) + def test_scan_file_for_tag_raises_value_error(self): + """ + GIVEN: + - Any error occurs during tag barcode processing + THEN: + - The processing should be skipped and not break the import + """ + test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-custom-prefix.pdf" + with self.get_reader(test_file) as reader: + reader.run() + # expect error to be caught and logged only + tags = reader.metadata.tag_ids + self.assertEqual(tags, None) diff --git a/src/documents/tests/test_management_consumer.py b/src/documents/tests/test_management_consumer.py index 99d5d410e..7e2707403 100644 --- a/src/documents/tests/test_management_consumer.py +++ b/src/documents/tests/test_management_consumer.py @@ -88,10 +88,10 @@ class ConsumerThreadMixin(DocumentConsumeDelayMixin): ): eq = filecmp.cmp(input_doc.original_file, self.sample_file, shallow=False) if not eq: - print("Consumed an INVALID file.") + print("Consumed an INVALID file.") # noqa: T201 raise ConsumerError("Incomplete File READ FAILED") else: - print("Consumed a perfectly valid file.") + print("Consumed a perfectly valid file.") # noqa: T201 def slow_write_file(self, target, incomplete=False): with open(self.sample_file, "rb") as f: @@ -102,11 +102,11 @@ class ConsumerThreadMixin(DocumentConsumeDelayMixin): with open(target, "wb") as f: # this will take 2 seconds, since the file is about 20k. - print("Start writing file.") + print("Start writing file.") # noqa: T201 for b in chunked(1000, pdf_bytes): f.write(b) sleep(0.1) - print("file completed.") + print("file completed.") # noqa: T201 @override_settings( diff --git a/src/documents/tests/test_management_fuzzy.py b/src/documents/tests/test_management_fuzzy.py index c215c43ca..7cc1f265e 100644 --- a/src/documents/tests/test_management_fuzzy.py +++ b/src/documents/tests/test_management_fuzzy.py @@ -196,7 +196,7 @@ class TestFuzzyMatchCommand(TestCase): self.assertEqual(Document.objects.count(), 3) stdout, _ = self.call_command("--delete") - print(stdout) + lines = [x.strip() for x in stdout.split("\n") if len(x.strip())] self.assertEqual(len(lines), 3) self.assertEqual( diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index ba5c53a78..95f903239 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -1,16 +1,19 @@ from datetime import timedelta from pathlib import Path +from typing import TYPE_CHECKING from unittest import mock from django.contrib.auth.models import Group from django.contrib.auth.models import User -from django.db.models import QuerySet from django.utils import timezone from guardian.shortcuts import assign_perm from guardian.shortcuts import get_groups_with_perms from guardian.shortcuts import get_users_with_perms from rest_framework.test import APITestCase +if TYPE_CHECKING: + from django.db.models import QuerySet + from documents import tasks from documents.data_models import ConsumableDocument from documents.data_models import DocumentSource diff --git a/src/documents/tests/utils.py b/src/documents/tests/utils.py index 4c3305d13..ba435d5c3 100644 --- a/src/documents/tests/utils.py +++ b/src/documents/tests/utils.py @@ -340,7 +340,6 @@ class DummyProgressManager: def __init__(self, filename: str, task_id: Optional[str] = None) -> None: self.filename = filename self.task_id = task_id - print("hello world") self.payloads = [] def __enter__(self): diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 0c7242462..7ee0c0d8b 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-02 20:17-0800\n" +"POT-Creation-Date: 2024-02-07 06:20+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -777,15 +777,136 @@ msgstr "" msgid "Invalid color." msgstr "" -#: documents/serialisers.py:1049 +#: documents/serialisers.py:1060 #, python-format msgid "File type %(type)s not supported" msgstr "" -#: documents/serialisers.py:1152 +#: documents/serialisers.py:1163 msgid "Invalid variable detected." msgstr "" +#: documents/templates/account/login.html:14 +msgid "Paperless-ngx sign in" +msgstr "" + +#: documents/templates/account/login.html:47 +msgid "Please sign in." +msgstr "" + +#: documents/templates/account/login.html:50 +msgid "Your username and password didn't match. Please try again." +msgstr "" + +#: documents/templates/account/login.html:54 +msgid "Share link was not found." +msgstr "" + +#: documents/templates/account/login.html:58 +msgid "Share link has expired." +msgstr "" + +#: documents/templates/account/login.html:61 +#: documents/templates/socialaccount/signup.html:56 +msgid "Username" +msgstr "" + +#: documents/templates/account/login.html:62 +msgid "Password" +msgstr "" + +#: documents/templates/account/login.html:72 +msgid "Sign in" +msgstr "" + +#: documents/templates/account/login.html:76 +msgid "Forgot your password?" +msgstr "" + +#: documents/templates/account/login.html:83 +msgid "or sign in via" +msgstr "" + +#: documents/templates/account/password_reset.html:15 +msgid "Paperless-ngx reset password request" +msgstr "" + +#: documents/templates/account/password_reset.html:43 +msgid "" +"Enter your email address below, and we'll email instructions for setting a " +"new one." +msgstr "" + +#: documents/templates/account/password_reset.html:46 +msgid "An error occurred. Please try again." +msgstr "" + +#: documents/templates/account/password_reset.html:49 +#: documents/templates/socialaccount/signup.html:57 +msgid "Email" +msgstr "" + +#: documents/templates/account/password_reset.html:56 +msgid "Send me instructions!" +msgstr "" + +#: documents/templates/account/password_reset_done.html:14 +msgid "Paperless-ngx reset password sent" +msgstr "" + +#: documents/templates/account/password_reset_done.html:40 +msgid "Check your inbox." +msgstr "" + +#: documents/templates/account/password_reset_done.html:41 +msgid "" +"We've emailed you instructions for setting your password. You should receive " +"the email shortly!" +msgstr "" + +#: documents/templates/account/password_reset_from_key.html:15 +msgid "Paperless-ngx reset password confirmation" +msgstr "" + +#: documents/templates/account/password_reset_from_key.html:44 +msgid "request a new password reset" +msgstr "" + +#: documents/templates/account/password_reset_from_key.html:46 +msgid "Set a new password." +msgstr "" + +#: documents/templates/account/password_reset_from_key.html:50 +msgid "Passwords did not match or too weak. Try again." +msgstr "" + +#: documents/templates/account/password_reset_from_key.html:53 +msgid "New Password" +msgstr "" + +#: documents/templates/account/password_reset_from_key.html:54 +msgid "Confirm Password" +msgstr "" + +#: documents/templates/account/password_reset_from_key.html:65 +msgid "Change my password" +msgstr "" + +#: documents/templates/account/password_reset_from_key_done.html:14 +msgid "Paperless-ngx reset password complete" +msgstr "" + +#: documents/templates/account/password_reset_from_key_done.html:40 +msgid "Password reset complete." +msgstr "" + +#: documents/templates/account/password_reset_from_key_done.html:42 +#, python-format +msgid "" +"Your new password has been set. You can now log " +"in" +msgstr "" + #: documents/templates/index.html:79 msgid "Paperless-ngx is loading..." msgstr "" @@ -798,131 +919,40 @@ msgstr "" msgid "Here's a link to the docs." msgstr "" -#: documents/templates/registration/logged_out.html:14 -msgid "Paperless-ngx signed out" +#: documents/templates/socialaccount/authentication_error.html:15 +#: documents/templates/socialaccount/login.html:15 +msgid "Paperless-ngx social account sign in" msgstr "" -#: documents/templates/registration/logged_out.html:40 -msgid "You have been successfully logged out. Bye!" -msgstr "" - -#: documents/templates/registration/logged_out.html:41 -msgid "Sign in again" -msgstr "" - -#: documents/templates/registration/login.html:14 -msgid "Paperless-ngx sign in" -msgstr "" - -#: documents/templates/registration/login.html:41 -msgid "Please sign in." -msgstr "" - -#: documents/templates/registration/login.html:44 -msgid "Your username and password didn't match. Please try again." -msgstr "" - -#: documents/templates/registration/login.html:48 -msgid "Share link was not found." -msgstr "" - -#: documents/templates/registration/login.html:52 -msgid "Share link has expired." -msgstr "" - -#: documents/templates/registration/login.html:55 -msgid "Username" -msgstr "" - -#: documents/templates/registration/login.html:56 -msgid "Password" -msgstr "" - -#: documents/templates/registration/login.html:66 -msgid "Sign in" -msgstr "" - -#: documents/templates/registration/login.html:70 -msgid "Forgot your password?" -msgstr "" - -#: documents/templates/registration/password_reset_complete.html:14 -msgid "Paperless-ngx reset password complete" -msgstr "" - -#: documents/templates/registration/password_reset_complete.html:40 -msgid "Password reset complete." -msgstr "" - -#: documents/templates/registration/password_reset_complete.html:42 +#: documents/templates/socialaccount/authentication_error.html:43 #, python-format msgid "" -"Your new password has been set. You can now log " -"in" +"An error occurred while attempting to login via your social network account. " +"Back to the login page" msgstr "" -#: documents/templates/registration/password_reset_confirm.html:14 -msgid "Paperless-ngx reset password confirmation" +#: documents/templates/socialaccount/login.html:44 +#, python-format +msgid "You are about to connect a new third-party account from %(provider)s." msgstr "" -#: documents/templates/registration/password_reset_confirm.html:42 -msgid "Set a new password." +#: documents/templates/socialaccount/login.html:47 +msgid "Continue" msgstr "" -#: documents/templates/registration/password_reset_confirm.html:46 -msgid "Passwords did not match or too weak. Try again." +#: documents/templates/socialaccount/signup.html:14 +msgid "Paperless-ngx social account sign up" msgstr "" -#: documents/templates/registration/password_reset_confirm.html:49 -msgid "New Password" -msgstr "" - -#: documents/templates/registration/password_reset_confirm.html:50 -msgid "Confirm Password" -msgstr "" - -#: documents/templates/registration/password_reset_confirm.html:61 -msgid "Change my password" -msgstr "" - -#: documents/templates/registration/password_reset_confirm.html:65 -msgid "request a new password reset" -msgstr "" - -#: documents/templates/registration/password_reset_done.html:14 -msgid "Paperless-ngx reset password sent" -msgstr "" - -#: documents/templates/registration/password_reset_done.html:40 -msgid "Check your inbox." -msgstr "" - -#: documents/templates/registration/password_reset_done.html:41 +#: documents/templates/socialaccount/signup.html:53 +#, python-format msgid "" -"We've emailed you instructions for setting your password. You should receive " -"the email shortly!" +"You are about to use your %(provider_name)s account to login to\n" +"%(site_name)s. As a final step, please complete the following form:" msgstr "" -#: documents/templates/registration/password_reset_form.html:14 -msgid "Paperless-ngx reset password request" -msgstr "" - -#: documents/templates/registration/password_reset_form.html:41 -msgid "" -"Enter your email address below, and we'll email instructions for setting a " -"new one." -msgstr "" - -#: documents/templates/registration/password_reset_form.html:44 -msgid "An error occurred. Please try again." -msgstr "" - -#: documents/templates/registration/password_reset_form.html:47 -msgid "Email" -msgstr "" - -#: documents/templates/registration/password_reset_form.html:54 -msgid "Send me instructions!" +#: documents/templates/socialaccount/signup.html:72 +msgid "Sign up" msgstr "" #: documents/validators.py:17 @@ -1088,135 +1118,135 @@ msgstr "" msgid "paperless application settings" msgstr "" -#: paperless/settings.py:617 +#: paperless/settings.py:658 msgid "English (US)" msgstr "" -#: paperless/settings.py:618 +#: paperless/settings.py:659 msgid "Arabic" msgstr "" -#: paperless/settings.py:619 +#: paperless/settings.py:660 msgid "Afrikaans" msgstr "" -#: paperless/settings.py:620 +#: paperless/settings.py:661 msgid "Belarusian" msgstr "" -#: paperless/settings.py:621 +#: paperless/settings.py:662 msgid "Bulgarian" msgstr "" -#: paperless/settings.py:622 +#: paperless/settings.py:663 msgid "Catalan" msgstr "" -#: paperless/settings.py:623 +#: paperless/settings.py:664 msgid "Czech" msgstr "" -#: paperless/settings.py:624 +#: paperless/settings.py:665 msgid "Danish" msgstr "" -#: paperless/settings.py:625 +#: paperless/settings.py:666 msgid "German" msgstr "" -#: paperless/settings.py:626 +#: paperless/settings.py:667 msgid "Greek" msgstr "" -#: paperless/settings.py:627 +#: paperless/settings.py:668 msgid "English (GB)" msgstr "" -#: paperless/settings.py:628 +#: paperless/settings.py:669 msgid "Spanish" msgstr "" -#: paperless/settings.py:629 +#: paperless/settings.py:670 msgid "Finnish" msgstr "" -#: paperless/settings.py:630 +#: paperless/settings.py:671 msgid "French" msgstr "" -#: paperless/settings.py:631 +#: paperless/settings.py:672 msgid "Hungarian" msgstr "" -#: paperless/settings.py:632 +#: paperless/settings.py:673 msgid "Italian" msgstr "" -#: paperless/settings.py:633 +#: paperless/settings.py:674 msgid "Japanese" msgstr "" -#: paperless/settings.py:634 +#: paperless/settings.py:675 msgid "Luxembourgish" msgstr "" -#: paperless/settings.py:635 +#: paperless/settings.py:676 msgid "Norwegian" msgstr "" -#: paperless/settings.py:636 +#: paperless/settings.py:677 msgid "Dutch" msgstr "" -#: paperless/settings.py:637 +#: paperless/settings.py:678 msgid "Polish" msgstr "" -#: paperless/settings.py:638 +#: paperless/settings.py:679 msgid "Portuguese (Brazil)" msgstr "" -#: paperless/settings.py:639 +#: paperless/settings.py:680 msgid "Portuguese" msgstr "" -#: paperless/settings.py:640 +#: paperless/settings.py:681 msgid "Romanian" msgstr "" -#: paperless/settings.py:641 +#: paperless/settings.py:682 msgid "Russian" msgstr "" -#: paperless/settings.py:642 +#: paperless/settings.py:683 msgid "Slovak" msgstr "" -#: paperless/settings.py:643 +#: paperless/settings.py:684 msgid "Slovenian" msgstr "" -#: paperless/settings.py:644 +#: paperless/settings.py:685 msgid "Serbian" msgstr "" -#: paperless/settings.py:645 +#: paperless/settings.py:686 msgid "Swedish" msgstr "" -#: paperless/settings.py:646 +#: paperless/settings.py:687 msgid "Turkish" msgstr "" -#: paperless/settings.py:647 +#: paperless/settings.py:688 msgid "Ukrainian" msgstr "" -#: paperless/settings.py:648 +#: paperless/settings.py:689 msgid "Chinese Simplified" msgstr "" -#: paperless/urls.py:214 +#: paperless/urls.py:224 msgid "Paperless-ngx administration" msgstr "" diff --git a/src/paperless/adapter.py b/src/paperless/adapter.py index 7ca25104c..98b0f11ba 100644 --- a/src/paperless/adapter.py +++ b/src/paperless/adapter.py @@ -27,4 +27,4 @@ class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): def populate_user(self, request, sociallogin, data): # TODO: If default global permissions are implemented, should also be here - return super().populate_user(request, sociallogin, data) + return super().populate_user(request, sociallogin, data) # pragma: no cover diff --git a/src/paperless/auth.py b/src/paperless/auth.py index 98e2a8b30..ba9320b5d 100644 --- a/src/paperless/auth.py +++ b/src/paperless/auth.py @@ -1,3 +1,5 @@ +import logging + from django.conf import settings from django.contrib import auth from django.contrib.auth.middleware import PersistentRemoteUserMiddleware @@ -6,6 +8,8 @@ from django.http import HttpRequest from django.utils.deprecation import MiddlewareMixin from rest_framework import authentication +logger = logging.getLogger("paperless.auth") + class AutoLoginMiddleware(MiddlewareMixin): def process_request(self, request: HttpRequest): @@ -35,7 +39,7 @@ class AngularApiAuthenticationOverride(authentication.BaseAuthentication): and request.headers["Referer"].startswith("http://localhost:4200/") ): user = User.objects.filter(is_staff=True).first() - print(f"Auto-Login with user {user}") + logger.debug(f"Auto-Login with user {user}") return (user, None) else: return None diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 3cba3f06e..d51ba9020 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -796,6 +796,11 @@ CACHES = { }, } +if DEBUG and os.getenv("PAPERLESS_CACHE_BACKEND") is None: + CACHES["default"][ + "BACKEND" + ] = "django.core.cache.backends.locmem.LocMemCache" # pragma: no cover + def default_threads_per_worker(task_workers) -> int: # always leave one core open @@ -878,6 +883,19 @@ CONSUMER_BARCODE_UPSCALE: Final[float] = __get_float( CONSUMER_BARCODE_DPI: Final[int] = __get_int("PAPERLESS_CONSUMER_BARCODE_DPI", 300) +CONSUMER_ENABLE_TAG_BARCODE: Final[bool] = __get_boolean( + "PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE", +) + +CONSUMER_TAG_BARCODE_MAPPING = dict( + json.loads( + os.getenv( + "PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING", + '{"TAG:(.*)": "\\\\g<1>"}', + ), + ), +) + CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED: Final[bool] = __get_boolean( "PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED", )