Compare commits

..

143 Commits

Author SHA1 Message Date
stumpylog 749079963e Dynamically update commitish so it should pick things for the changelog from beta 2026-05-05 09:03:22 -07:00
stumpylog 6b86f6f723 Corrects the Docker image build check name 2026-05-05 09:00:02 -07:00
github-actions[bot] 5966b12362 New Crowdin translations by GitHub Action (#12674)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-05 08:37:58 -07:00
GitHub Actions bb5100b3d8 Auto translate strings 2026-05-05 01:02:33 +00:00
Trenton H c3b353873b use a few named tuples and data classes instead of so much unpacking (#12709) 2026-05-04 18:00:48 -07:00
Trenton H 3adeda28b4 Upgrades uv to the 0.11.x branch (#12710) 2026-05-04 23:44:36 +00:00
Trenton H e822e72964 Feature: Further reduce document importer memory usage (#12707)
* Replaces loaddata with streaming bulk_create

Replaces call_command('loaddata') with a streaming implementation that
reads manifest records one at a time via ijson, accumulates per-model
batches up to --batch-size, and flushes via bulk_create.  This reduces
peak memory and no longer scales directly with the size of the import.

* fix(importer): avoid guardian lru_cache poisoning; include M2M through tables in check_constraints

clear_cache() inside the import transaction emptied Django's ContentType
manager cache while fixture PKs were live, causing downstream ContentType
lookups to repopulate guardian's separate @lru_cache(None) with
fixture-PK objects. After the TestCase transaction rolled back to
original PKs, guardian's lru_cache held stale fixture ContentType
objects, causing MixedContentTypeError in unrelated subsequent tests.

Remove clear_cache() since it was defending against a theoretical
stale-cache scenario that doesn't occur in a proper same-install restore.

Fix check_constraints() to explicitly include auto-created M2M through
tables (populated by .set() after bulk_create) alongside the model tables,
addressing the gap where join-table FK violations would have gone
undetected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Excludes the consumer and AnonymousUser from any models which might have a FK relation to it.  This prevents orphan things like UI setting, which have a relation to no existing user

* Splits into more sub functions for Sonar

* Improvements to the typing of the new functions

* Coverage for some error cases, and removes handling for pk only models.  No need to support these

* Final coverage gaps

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 16:36:05 -07:00
GitHub Actions 209c5d2deb Auto translate strings 2026-05-04 21:36:15 +00:00
shamoon a76b6b826c Fix (dev): resolve tantivy search-filtered documents in bulk edit (#12705) 2026-05-04 14:34:08 -07:00
shamoon 1b08417062 Tweak: add icons to ai suggestion dropdown (#12708) 2026-05-04 14:22:37 -07:00
shamoon 8695e92b8b Update SECURITY.md 2026-05-04 14:20:25 -07:00
dependabot[bot] ab550f9198 Chore(deps-dev): Bump @playwright/test from 1.59.0 to 1.59.1 in /src-ui (#12692)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-05-04 21:16:10 +00:00
dependabot[bot] 45fad1b298 Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates (#12689)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 21:06:02 +00:00
dependabot[bot] fa5c790c9e Chore(deps-dev): Bump @codecov/webpack-plugin from 1.9.1 to 2.0.1 in /src-ui (#12691)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 20:54:32 +00:00
dependabot[bot] bd4d33102c Chore(deps-dev): Bump the frontend-jest-dependencies group in /src-ui with 2 updates (#12685)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 20:42:11 +00:00
dependabot[bot] 974ee41a02 Chore(deps-dev): Bump @types/node from 25.5.0 to 25.6.0 in /src-ui (#12690)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 20:23:34 +00:00
dependabot[bot] 5218a71804 Chore(deps-dev): Bump webpack from 5.105.3 to 5.106.2 in /src-ui (#12693)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 20:07:12 +00:00
GitHub Actions 3f8fd1d60d Auto translate strings 2026-05-04 19:46:26 +00:00
dependabot[bot] ba2ddebf7e Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 15 updates (#12684)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 19:44:33 +00:00
GitHub Actions 774a67543d Auto translate strings 2026-05-04 19:31:10 +00:00
shamoon b03f254aea Tweakhancement: use fixed position instead of display none (#12706) 2026-05-04 12:29:25 -07:00
GitHub Actions 4db77776c1 Auto translate strings 2026-05-04 16:09:04 +00:00
shamoon e75860dcd1 Fix (dev): respect base path for pdf worker js (#12704) 2026-05-04 09:07:05 -07:00
shamoon ca4444c9a3 Fix (dev): return empty list for non-positive search limit (#12703) 2026-05-04 08:50:15 -07:00
GitHub Actions 31afd0483f Auto translate strings 2026-05-04 13:13:43 +00:00
Trenton H 4a915f8e3a Fix: Always require a set, non-empty, not whitespace secret key, even in DEBUG (#12680) 2026-05-04 13:11:56 +00:00
Trenton H 5010f37174 Fix: avoid unnecessary close_old_connections in Celery task dispatch (#12701) 2026-05-04 06:02:28 -07:00
dependabot[bot] 7e0dc2bca4 Chore(deps): Bump the utilities-patch group across 1 directory with 7 updates (#12702)
Bumps the utilities-patch group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [drf-spectacular-sidecar](https://github.com/tfranzel/drf-spectacular-sidecar) | `2026.4.1` | `2026.4.14` |
| [llama-index-core](https://github.com/run-llama/llama_index) | `0.14.19` | `0.14.21` |
| [rapidfuzz](https://github.com/rapidfuzz/RapidFuzz) | `3.14.3` | `3.14.5` |
| [prek](https://github.com/j178/prek) | `0.3.8` | `0.3.10` |
| [pytest-httpx](https://github.com/Colin-b/pytest_httpx) | `0.36.0` | `0.36.2` |
| [mypy](https://github.com/python/mypy) | `1.20.0` | `1.20.2` |
| [mypy-baseline](https://github.com/orsinium-labs/mypy-baseline) | `0.7.3` | `0.7.4` |



Updates `drf-spectacular-sidecar` from 2026.4.1 to 2026.4.14
- [Commits](https://github.com/tfranzel/drf-spectacular-sidecar/compare/2026.4.1...2026.4.14)

Updates `llama-index-core` from 0.14.19 to 0.14.21
- [Release notes](https://github.com/run-llama/llama_index/releases)
- [Changelog](https://github.com/run-llama/llama_index/blob/main/CHANGELOG.md)
- [Commits](https://github.com/run-llama/llama_index/compare/v0.14.19...v0.14.21)

Updates `rapidfuzz` from 3.14.3 to 3.14.5
- [Release notes](https://github.com/rapidfuzz/RapidFuzz/releases)
- [Changelog](https://github.com/rapidfuzz/RapidFuzz/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/rapidfuzz/RapidFuzz/compare/v3.14.3...v3.14.5)

Updates `prek` from 0.3.8 to 0.3.10
- [Release notes](https://github.com/j178/prek/releases)
- [Changelog](https://github.com/j178/prek/blob/master/CHANGELOG.md)
- [Commits](https://github.com/j178/prek/compare/v0.3.8...v0.3.10)

Updates `pytest-httpx` from 0.36.0 to 0.36.2
- [Release notes](https://github.com/Colin-b/pytest_httpx/releases)
- [Changelog](https://github.com/Colin-b/pytest_httpx/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/Colin-b/pytest_httpx/compare/v0.36.0...0.36.2)

Updates `mypy` from 1.20.0 to 1.20.2
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.20.0...v1.20.2)

Updates `mypy-baseline` from 0.7.3 to 0.7.4
- [Release notes](https://github.com/orsinium-labs/mypy-baseline/releases)
- [Changelog](https://github.com/orsinium-labs/mypy-baseline/blob/master/docs/history.md)
- [Commits](https://github.com/orsinium-labs/mypy-baseline/compare/0.7.3...0.7.4)

---
updated-dependencies:
- dependency-name: drf-spectacular-sidecar
  dependency-version: 2026.4.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: llama-index-core
  dependency-version: 0.14.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: rapidfuzz
  dependency-version: 3.14.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: prek
  dependency-version: 0.3.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: pytest-httpx
  dependency-version: 0.36.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: mypy
  dependency-version: 1.20.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: mypy-baseline
  dependency-version: 0.7.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-03 14:56:05 -07:00
dependabot[bot] c72c4809c2 Chore(deps): Bump the utilities-minor group across 1 directory with 9 updates (#12696)
* Chore(deps): Bump the utilities-minor group across 1 directory with 9 updates

Bumps the utilities-minor group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [django-treenode](https://github.com/fabiocaccamo/django-treenode) | `0.23.3` | `0.24.0` |
| [filelock](https://github.com/tox-dev/py-filelock) | `3.25.2` | `3.29.0` |
| [imap-tools](https://github.com/ikvk/imap_tools) | `1.11.1` | `1.12.1` |
| [openai](https://github.com/openai/openai-python) | `2.30.0` | `2.32.0` |
| [regex](https://github.com/mrabarnett/mrab-regex) | `2026.3.32` | `2026.4.4` |
| [sentence-transformers](https://github.com/huggingface/sentence-transformers) | `5.3.0` | `5.4.1` |
| [faker](https://github.com/joke2k/faker) | `40.12.0` | `40.15.0` |
| [pyrefly](https://github.com/facebook/pyrefly) | `0.59.0` | `0.62.0` |
| [types-pygments](https://github.com/python/typeshed) | `2.19.0.20251121` | `2.20.0.20260408` |



Updates `django-treenode` from 0.23.3 to 0.24.0
- [Release notes](https://github.com/fabiocaccamo/django-treenode/releases)
- [Changelog](https://github.com/fabiocaccamo/django-treenode/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fabiocaccamo/django-treenode/compare/0.23.3...0.24.0)

Updates `filelock` from 3.25.2 to 3.29.0
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.25.2...3.29.0)

Updates `imap-tools` from 1.11.1 to 1.12.1
- [Release notes](https://github.com/ikvk/imap_tools/releases)
- [Changelog](https://github.com/ikvk/imap_tools/blob/master/docs/release_notes.rst)
- [Commits](https://github.com/ikvk/imap_tools/compare/v1.11.1...v1.12.1)

Updates `openai` from 2.30.0 to 2.32.0
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v2.30.0...v2.32.0)

Updates `regex` from 2026.3.32 to 2026.4.4
- [Changelog](https://github.com/mrabarnett/mrab-regex/blob/hg/changelog.txt)
- [Commits](https://github.com/mrabarnett/mrab-regex/compare/2026.3.32...2026.4.4)

Updates `sentence-transformers` from 5.3.0 to 5.4.1
- [Release notes](https://github.com/huggingface/sentence-transformers/releases)
- [Commits](https://github.com/huggingface/sentence-transformers/compare/v5.3.0...v5.4.1)

Updates `faker` from 40.12.0 to 40.15.0
- [Release notes](https://github.com/joke2k/faker/releases)
- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md)
- [Commits](https://github.com/joke2k/faker/compare/v40.12.0...v40.15.0)

Updates `pyrefly` from 0.59.0 to 0.62.0
- [Release notes](https://github.com/facebook/pyrefly/releases)
- [Commits](https://github.com/facebook/pyrefly/compare/0.59.0...0.62.0)

Updates `types-pygments` from 2.19.0.20251121 to 2.20.0.20260408
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: django-treenode
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: faker
  dependency-version: 40.15.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: filelock
  dependency-version: 3.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: imap-tools
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: openai
  dependency-version: 2.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: pyrefly
  dependency-version: 0.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: regex
  dependency-version: 2026.4.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: sentence-transformers
  dependency-version: 5.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: types-pygments
  dependency-version: 2.20.0.20260408
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Linting

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Trenton Holmes <797416+stumpylog@users.noreply.github.com>
2026-05-03 21:09:27 +00:00
dependabot[bot] 44c8f24c62 Chore(deps): Bump the actions group with 17 updates (#12686)
Bumps the actions group with 17 updates:

| Package | From | To |
| --- | --- | --- |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `8.0.0` | `8.1.0` |
| [actions/cache](https://github.com/actions/cache) | `5.0.4` | `5.0.5` |
| [docker/login-action](https://github.com/docker/login-action) | `4.0.0` | `4.1.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `7.0.0` | `7.1.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` |
| [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) | `4.0.0` | `5.0.0` |
| [pnpm/action-setup](https://github.com/pnpm/action-setup) | `5.0.0` | `6.0.3` |
| [actions/setup-node](https://github.com/actions/setup-node) | `6.3.0` | `6.4.0` |
| [j178/prek-action](https://github.com/j178/prek-action) | `2.0.1` | `2.0.2` |
| [lewagon/wait-on-check-action](https://github.com/lewagon/wait-on-check-action) | `1.5.0` | `1.7.0` |
| [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) | `7.1.1` | `7.2.0` |
| [shogo82148/actions-upload-release-asset](https://github.com/shogo82148/actions-upload-release-asset) | `1.10.0` | `1.10.1` |
| [actions/github-script](https://github.com/actions/github-script) | `8.0.0` | `9.0.0` |
| [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) | `0.5.2` | `0.5.3` |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.35.1` | `4.35.2` |
| [crowdin/github-action](https://github.com/crowdin/github-action) | `2.16.0` | `2.16.2` |
| [peakoss/anti-slop](https://github.com/peakoss/anti-slop) | `0.2.1` | `0.3.0` |


Updates `astral-sh/setup-uv` from 8.0.0 to 8.1.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/cec208311dfd045dd5311c1add060b2062131d57...08807647e7069bb48b6ef5acd8ec9567f424441b)

Updates `actions/cache` from 5.0.4 to 5.0.5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/668228422ae6a00e4ad889ee87cd7109ec5666a7...27d5ce7f107fe9357f9df03efb73ab90386fccae)

Updates `docker/login-action` from 4.0.0 to 4.1.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/b45d80f862d83dbcd57f89517bcf500b2ab88fb2...4907a6ddec9925e35a0a9e82d7399ccc52663121)

Updates `docker/build-push-action` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/d08e5c354a6adb9ed34480a06d141179aa583294...bcafcacb16a39f128d818304e6c9c0c18556b85f)

Updates `actions/upload-artifact` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `actions/upload-pages-artifact` from 4.0.0 to 5.0.0
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/7b1f4a764d45c48632c6b24a0339c27f5614fb0b...fc324d3547104276b827a68afc52ff2a11cc49c9)

Updates `pnpm/action-setup` from 5.0.0 to 6.0.3
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/fc06bc1257f339d1d5d8b3a19a8cae5388b55320...903f9c1a6ebcba6cf41d87230be49611ac97822e)

Updates `actions/setup-node` from 6.3.0 to 6.4.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/53b83947a5a98c8d113130e565377fae1a50d02f...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e)

Updates `j178/prek-action` from 2.0.1 to 2.0.2
- [Release notes](https://github.com/j178/prek-action/releases)
- [Commits](https://github.com/j178/prek-action/compare/53276d8b0d10f8b6672aa85b4588c6921d0370cc...cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3)

Updates `lewagon/wait-on-check-action` from 1.5.0 to 1.7.0
- [Release notes](https://github.com/lewagon/wait-on-check-action/releases)
- [Changelog](https://github.com/lewagon/wait-on-check-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/lewagon/wait-on-check-action/compare/74049309dfeff245fe8009a0137eacf28136cb3c...9312864dfbc9fd208e9c0417843430751c042800)

Updates `release-drafter/release-drafter` from 7.1.1 to 7.2.0
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](https://github.com/release-drafter/release-drafter/compare/139054aeaa9adc52ab36ddf67437541f039b88e2...5de93583980a40bd78603b6dfdcda5b4df377b32)

Updates `shogo82148/actions-upload-release-asset` from 1.10.0 to 1.10.1
- [Release notes](https://github.com/shogo82148/actions-upload-release-asset/releases)
- [Commits](https://github.com/shogo82148/actions-upload-release-asset/compare/96bc1f0cb850b65efd58a6b5eaa0a69f88d38077...ee2ae851dc5d938b90075b3ef12c540abfd1ee72)

Updates `actions/github-script` from 8.0.0 to 9.0.0
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/ed597411d8f924073f98dfc5c65a23a2325f34cd...3a2844b7e9c422d3c10d287c895573f7108da1b3)

Updates `zizmorcore/zizmor-action` from 0.5.2 to 0.5.3
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/71321a20a9ded102f6e9ce5718a2fcec2c4f70d8...b1d7e1fb5de872772f31590499237e7cce841e8e)

Updates `github/codeql-action` from 4.35.1 to 4.35.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...95e58e9a2cdfd71adc6e0353d5c52f41a045d225)

Updates `crowdin/github-action` from 2.16.0 to 2.16.2
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](https://github.com/crowdin/github-action/compare/7ca9c452bfe9197d3bb7fa83a4d7e2b0c9ae835d...8868a33591d21088edfc398968173a3b98d51706)

Updates `peakoss/anti-slop` from 0.2.1 to 0.3.0
- [Release notes](https://github.com/peakoss/anti-slop/releases)
- [Changelog](https://github.com/peakoss/anti-slop/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peakoss/anti-slop/compare/85daca1880e9e1af197fc06ea03349daf08f4202...57858eead489d08b255fab2af45a506c2ca6eab2)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/cache
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/build-push-action
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/upload-pages-artifact
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: j178/prek-action
  dependency-version: 2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: lewagon/wait-on-check-action
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: release-drafter/release-drafter
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: shogo82148/actions-upload-release-asset
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/github-script
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-version: 4.35.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: crowdin/github-action
  dependency-version: 2.16.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: peakoss/anti-slop
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 22:26:14 +00:00
dependabot[bot] 3bbb5166a1 Chore(deps): Bump ocrmypdf (#12687)
Bumps the document-processing group with 1 update in the / directory: [ocrmypdf](https://github.com/ocrmypdf/OCRmyPDF).


Updates `ocrmypdf` from 17.4.0 to 17.4.2
- [Release notes](https://github.com/ocrmypdf/OCRmyPDF/releases)
- [Commits](https://github.com/ocrmypdf/OCRmyPDF/compare/v17.4.0...v17.4.2)

---
updated-dependencies:
- dependency-name: ocrmypdf
  dependency-version: 17.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: document-processing
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 22:02:49 +00:00
dependabot[bot] aa5485019d Chore(deps-dev): Bump the development group with 2 updates (#12683)
Bumps the development group with 2 updates: [zensical](https://github.com/zensical/zensical) and [ruff](https://github.com/astral-sh/ruff).


Updates `zensical` from 0.0.31 to 0.0.36
- [Release notes](https://github.com/zensical/zensical/releases)
- [Commits](https://github.com/zensical/zensical/compare/v0.0.31...v0.0.36)

Updates `ruff` from 0.15.8 to 0.15.12
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.8...0.15.12)

---
updated-dependencies:
- dependency-name: zensical
  dependency-version: 0.0.36
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: ruff
  dependency-version: 0.15.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 21:48:15 +00:00
dependabot[bot] 1a32fdb858 Chore(deps): Bump the pre-commit-dependencies group with 4 updates (#12694)
---
updated-dependencies:
- dependency-name: https://github.com/rbubley/mirrors-prettier
  dependency-version: 3.8.3
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
- dependency-name: prettier
  dependency-version: 3.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pre-commit-dependencies
- dependency-name: https://github.com/astral-sh/ruff-pre-commit
  dependency-version: 0.15.12
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
- dependency-name: https://github.com/tox-dev/pyproject-fmt
  dependency-version: 2.21.1
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 14:39:32 -07:00
Trenton H b38d112691 Chore: Convert more testing to pytest style (#12678) 2026-04-30 11:40:11 -07:00
GitHub Actions 59c08e84e2 Auto translate strings 2026-04-30 12:54:34 +00:00
shamoon d4a1de18f7 Change (dev): separate llm suggestions endpoint (#12675) 2026-04-30 05:52:45 -07:00
github-actions[bot] 40f33e397e New Crowdin translations by GitHub Action (#11627)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-29 16:49:16 -07:00
GitHub Actions 9529d91a3c Auto translate strings 2026-04-29 23:40:19 +00:00
shamoon fbadc2e1b8 Enhancement: Paperless-ngx v3 Logo (#12673) 2026-04-29 16:38:25 -07:00
GitHub Actions aa7d7827de Auto translate strings 2026-04-29 23:32:42 +00:00
shamoon bf4fa1dd29 Tweakhancement: localize some more task result messages (#12672) 2026-04-29 16:31:02 -07:00
Trenton H 493d282059 Chore: Upgrades tantivy-py to the latest release (#12605) 2026-04-29 10:09:50 -07:00
GitHub Actions 88824f94e5 Auto translate strings 2026-04-28 20:02:11 +00:00
shamoon 354df34e47 Enhancement: chat message document links (#12670) 2026-04-28 13:00:20 -07:00
GitHub Actions 8e6fd010a0 Auto translate strings 2026-04-28 17:08:38 +00:00
shamoon 69cb4d06c6 Enhancement (dev): Use OpenAI-like backend (#12668) 2026-04-28 10:06:59 -07:00
shamoon 2f8f126223 Fix: fix a minor pdf viewer uncaught error (#12669) 2026-04-28 09:47:20 -07:00
Trenton H 14fe520319 Chore: Update typing and baselines again (#12641)
a
2026-04-28 09:28:05 -07:00
shamoon ff95512b9a Fix: apply tag changes directly to document in db (#12664) 2026-04-28 08:18:40 -07:00
shamoon 4c0ed41368 Tweakhancement: make upload notification open an anchor link (#12659) 2026-04-26 20:28:47 -07:00
GitHub Actions 29b4b419fb Auto translate strings 2026-04-27 03:24:32 +00:00
shamoon e00fea5222 Enhancement: tweak tasks UI, make open doc an anchor (#12658) 2026-04-26 20:22:57 -07:00
shamoon 71b630d101 Format changelog with new prek 2026-04-26 20:20:46 -07:00
GitHub Actions 441d1b8c9f Auto translate strings 2026-04-27 03:19:34 +00:00
shamoon b246aa22d2 Merge branch 'main' into dev 2026-04-26 20:17:38 -07:00
github-actions[bot] fb02621777 Documentation: Add v2.20.15 changelog (#12657)
---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-04-26 20:17:00 -07:00
shamoon 05e48b2316 Bump version to 2.20.15 2026-04-26 19:22:18 -07:00
shamoon fa0c5bde1e Fix constructor 2026-04-26 19:20:40 -07:00
shamoon 21ff254ffc Merge branch 'main' into dev 2026-04-26 19:12:40 -07:00
shamoon cdc385e34e Bump version to 2.20.15 2026-04-26 19:05:38 -07:00
Gaëtan GOUZI caac5088e4 Fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type (#12597)
* fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type

* Add regression test

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-04-26 19:05:37 -07:00
Gaëtan GOUZI be8658d61a fix: Return HTTP 400 instead of HTTP 500 on DELETE /api/documents/{id}/notes/ with missing or invalid note id (#12582) 2026-04-26 19:05:36 -07:00
shamoon 10e61c5a7a Merge branch 'release/v2.20.x' 2026-04-26 19:00:38 -07:00
shamoon 552e5cf422 Merge commit from fork 2026-04-26 17:46:11 -07:00
shamoon d574867abb Fix: use only allauth login/logout endpoints (#12639) 2026-04-26 15:57:01 -07:00
GitHub Actions 0aa8c149bc Auto translate strings 2026-04-26 05:44:42 +00:00
shamoon 778d6b9fe3 Fix(dev): catch llm configuration error on get suggestions (#12647) 2026-04-25 22:43:04 -07:00
GitHub Actions 8cab1d0c13 Auto translate strings 2026-04-25 13:41:46 +00:00
Trenton H a2dbe17a78 Fix: Use FileResponse for file API responses (#12638)
* Updates code to use a FileResponse for streaming and unlink the file, but keep a handle to it

* Transitions the rest of the code to use FileResponse instead of a basic response, fixes up tests which assumed .content exists

* While here, let's add schema for it
2026-04-25 06:40:09 -07:00
GitHub Actions ff6ba7526c Auto translate strings 2026-04-24 20:36:44 +00:00
Trenton H 610702d757 Fix: v9 API task response, removing pagination (#12637) 2026-04-24 13:34:53 -07:00
GitHub Actions bbda3808a9 Auto translate strings 2026-04-24 17:33:15 +00:00
Trenton H d6e45093e8 Chore: Paginate the task listing (#12633)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-04-24 10:31:37 -07:00
shamoon e5561ba06f Fix: correctly scope mail account enumeration (#12636) 2026-04-24 10:15:59 -07:00
GitHub Actions dbce393604 Auto translate strings 2026-04-24 16:13:23 +00:00
shamoon 5bb9241e9a Enhancement: show small task summary in system status (#12634) 2026-04-24 09:11:42 -07:00
shamoon 22d3b208c9 Documentation: update zensical and add lightbox support (#12631) 2026-04-23 11:22:04 -07:00
GitHub Actions 2ca9e6764a Auto translate strings 2026-04-23 16:13:36 +00:00
Trenton H aab03501c2 Security: Rejects a default secret key where the user did not, in fact, change-me (#12630) 2026-04-23 09:11:41 -07:00
dependabot[bot] 1a3b56496a Chore(deps): Bump uuid (#12627)
Bumps the npm_and_yarn group with 1 update in the /src-ui directory: [uuid](https://github.com/uuidjs/uuid).


Updates `uuid` from 13.0.0 to 14.0.0
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 23:18:16 -07:00
Trenton H bdbecac7e8 Replaces two sentinel files with .index_settings.json which can properly store multiple values and handle None (#12625) 2026-04-23 02:38:26 +00:00
GitHub Actions 08131b48fa Auto translate strings 2026-04-23 00:42:28 +00:00
shamoon 55393b258c Enhancement: new Tasks UI (#12614) 2026-04-23 00:41:01 +00:00
Trenton H ceb67fef4d Fix: Changes bare metal webserver to use uvloop (#12626) 2026-04-22 17:34:25 -07:00
GitHub Actions 4b6bb23a9a Auto translate strings 2026-04-22 20:50:26 +00:00
Trenton H 0c25c2dac5 Feature: Allow monitoring access to tasks summary (#12624)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-04-22 13:48:54 -07:00
dependabot[bot] 2a20cc29a6 Chore(deps): Bump lxml in the uv group across 1 directory (#12619)
Bumps the uv group with 1 update in the / directory: [lxml](https://github.com/lxml/lxml).


Updates `lxml` from 6.0.2 to 6.1.0
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-6.0.2...lxml-6.1.0)

---
updated-dependencies:
- dependency-name: lxml
  dependency-version: 6.1.0
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 09:58:15 -07:00
Trenton H 89a9e7f190 Performance: Increases workflow related M2M prefetching (#12618) 2026-04-21 22:01:51 +00:00
GitHub Actions c669c3416e Auto translate strings 2026-04-21 21:50:36 +00:00
shamoon 88430c8ab7 Tweak: remove 'stale' indicator for index in system status (#12616) 2026-04-21 21:49:04 +00:00
GitHub Actions edfebcbe44 Auto translate strings 2026-04-21 18:02:57 +00:00
Trenton H a89cd2d5d9 Fix: Exact custom field monetary exact searching (#12592) 2026-04-21 18:01:27 +00:00
GitHub Actions 02e913b475 Auto translate strings 2026-04-21 17:26:25 +00:00
Trenton H 6017b11c42 Fix: Prefetches the custom field instance and the custom field all at once (#12617) 2026-04-21 10:24:51 -07:00
shamoon ffaa2bb77a Fix: prevent sidebar animation at startup (#12615) 2026-04-20 23:17:16 -07:00
Trenton H 50ec987a81 Chore: Refactors all of the mail tests to use model factories instead of bare create (#12613) 2026-04-20 15:43:43 -07:00
shamoon f784a74eba Enhancement: add highlighting to title + content searches (#12593) 2026-04-20 21:28:02 +00:00
GitHub Actions 814fdf5892 Auto translate strings 2026-04-20 20:21:27 +00:00
Trenton H 58789e5061 Chore: Structured consume task return values (#12612) 2026-04-20 13:19:54 -07:00
GitHub Actions 7492cda794 Auto translate strings 2026-04-20 18:41:41 +00:00
Trenton H fbf4e32646 Chore: Converts all call sites and test asserts to use apply_async and headers (#12591) 2026-04-20 11:40:04 -07:00
GitHub Actions 733d873e34 Auto translate strings 2026-04-20 18:06:35 +00:00
Trenton H 5e609101d1 Chore: Update API schema fields (#12611)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 11:05:00 -07:00
GitHub Actions dfdf418adc Auto translate strings 2026-04-20 16:30:23 +00:00
Trenton H 8e67828bd7 Feature: Redesign the task system (#12584)
* feat(tasks): replace PaperlessTask model with structured redesign

Drop the old string-based PaperlessTask table and recreate it with
Status/TaskType/TriggerSource enums, JSONField result storage, and
duration tracking fields. Update all call sites to use the new API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tasks): rewrite signal handlers to track all task types

Replace the old consume_file-only handler with a full rewrite that tracks
6 task types (consume_file, train_classifier, sanity_check, index_optimize,
llm_index, mail_fetch) with proper trigger source detection, input data
extraction, legacy result string parsing, duration/wait time recording,
and structured error capture on failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tasks): add traceback and revoked state coverage to signal tests

* refactor(tasks): remove manual PaperlessTask creation and scheduled/auto params

All task records are now created exclusively via Celery signals (Task 2).
Removed PaperlessTask creation/update from train_classifier, sanity_check,
llmindex_index, and check_sanity. Removed scheduled= and auto= parameters
from all 7 call sites. Updated apply_async callers to use trigger_source
headers instead. Exceptions now propagate naturally from task functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tasks): auto-inject trigger_source=scheduled header for all beat tasks

Inject `headers: {"trigger_source": "scheduled"}` into every Celery beat
schedule entry so signal handlers can identify scheduler-originated tasks
without per-task instrumentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tasks): update serializer, filter, and viewset with v9 backwards compat

- Replace TasksViewSerializer/RunTaskViewSerializer with TaskSerializerV10
  (new field names), TaskSerializerV9 (v9 compat), TaskSummarySerializer,
  and RunTaskSerializer
- Add AcknowledgeTasksViewSerializer unchanged (kept existing validation)
- Expand PaperlessTaskFilterSet with MultipleChoiceFilter for task_type,
  trigger_source, status; add is_complete, date_created_after/before filters
- Replace TasksViewSet.get_serializer_class() to branch on request.version
- Add get_queryset() v9 compat for task_name/type query params
- Add acknowledge_all, summary, active actions to TasksViewSet
- Rewrite run action to use apply_async with trigger_source header
- Add timedelta import to views.py; add MultipleChoiceFilter/DateTimeFilter
  to filters.py imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tasks): add read_only_fields to TaskSerializerV9, enforce admin via permission_classes on run action

* test(tasks): rewrite API task tests for redesigned model and v9 compat

Replaces the old Django TestCase-based tests with pytest-style classes using
PaperlessTaskFactory. Covers v10 field names, v9 backwards-compat field
mapping, filtering, ordering, acknowledge, acknowledge_all, summary, active,
and run endpoints. Also adds PaperlessTaskFactory to factories.py and fixes
a redundant source= kwarg in TaskSerializerV10.related_document_ids.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tasks): fix two spec gaps in task API test suite

Move test_list_is_owner_aware to TestGetTasksV10 (it tests GET /api/tasks/,
not acknowledge). Add test_related_document_ids_includes_duplicate_of to
cover the duplicate_of path in the related_document_ids property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tasks): address code quality review findings

Remove trivial field-existence tests per project conventions. Fix
potentially flaky ordering test to use explicit date_created values.
Add is_complete=false filter test, v9 type filter input direction test,
and tighten TestActive second test to target REVOKED specifically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tasks): update TaskAdmin for redesigned model

Add date_created, duration_seconds to list_display; add trigger_source
to list_filter; add input_data, duration_seconds, wait_time_seconds to
readonly_fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tasks): update Angular types and service for task redesign

Replace PaperlessTaskName/PaperlessTaskType/PaperlessTaskStatus enums
with new PaperlessTaskType, PaperlessTaskTriggerSource, PaperlessTaskStatus
enums. Update PaperlessTask interface to new field names (task_type,
trigger_source, input_data, result_message, related_document_ids).
Update TasksService to filter by task_type instead of task_name.
Update tasks component and system-status-dialog to use new field names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(tasks): remove django-celery-results

PaperlessTask now tracks all task results via Celery signals. The
django-celery-results DB backend was write-only -- nothing reads
from it. Drop the package and add a migration to clean up the
orphaned tables.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: fix remaining tests broken by task system redesign

Update all tests that created PaperlessTask objects with old field names
to use PaperlessTaskFactory and new field names (task_type, trigger_source,
status, result_message). Use apply_async instead of delay where mocked.
Drop TestCheckSanityTaskRecording — tests PaperlessTask creation that was
intentionally removed from check_sanity().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tasks): improve test_api_tasks.py structure and add api marker

- Move admin_client, v9_client, user_client fixtures to conftest.py so
  they can be reused by other API tests; all three now build on the
  rest_api_client fixture instead of creating APIClient() directly
- Move regular_user fixture to conftest.py (was already done, now also
  used by the new client fixtures)
- Add docstrings to every test method describing the behaviour under test
- Move timedelta/timezone imports to module level
- Register 'api' pytest marker in pyproject.toml and apply pytestmark to
  the entire file so all 40 tests are selectable via -m api

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(tasks): simplify task tracking code after redesign

- Extract COMPLETE_STATUSES as a class constant on PaperlessTask,
  eliminating the repeated status tuple across models.py, views.py (3×),
  and filters.py
- Extract _CELERY_STATE_TO_STATUS as a module-level constant instead of
  rebuilding the dict on every task_postrun
- Extract _V9_TYPE_TO_TRIGGER_SOURCE and _RUNNABLE_TASKS as class
  constants on TasksViewSet instead of rebuilding on every request
- Extract _TRIGGER_SOURCE_TO_V9_TYPE as a class constant on
  TaskSerializerV9 instead of rebuilding per serialized object
- Extract _get_consume_args helper to deduplicate identical arg
  extraction logic in _extract_input_data, _determine_trigger_source,
  and _extract_owner_id
- Move inline imports (re, traceback) and Avg to module level
- Fix _DOCUMENT_SOURCE_TO_TRIGGER type annotation key type to
  DocumentSource instead of Any
- Remove redundant truthiness checks in SystemStatusView branches
  already guarded by an is-None check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(tasks): add docstrings and rename _parse_legacy_result

- Add docstrings to _extract_input_data, _determine_trigger_source,
  _extract_owner_id explaining what each helper does and why
- Rename _parse_legacy_result -> _parse_consume_result: the function
  parses current consume_file string outputs (consumer.py returns
  "New document id N created" and "It is a duplicate of X (#N)"),
  not legacy data; the old name was misleading

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tasks): extend and harden the task system redesign

- TaskType: add EMPTY_TRASH, CHECK_WORKFLOWS, CLEANUP_SHARE_LINKS;
  remove INDEX_REBUILD (no backing task — beat schedule uses index_optimize)
- TRACKED_TASKS: wire up all nine task types including the three new ones
  and llmindex_index / process_mail_accounts
- Add task_revoked_handler so cancelled/expired tasks are marked REVOKED
- Fix double-write: task_postrun_handler no longer overwrites result_data
  when status is already FAILURE (task_failure_handler owns that write)
- v9 serialiser: map EMAIL_CONSUME and FOLDER_CONSUME to AUTO_TASK
- views: scope task list to owner for regular users, admins see all;
  validate ?days= query param and return 400 on bad input
- tests: add test_list_admin_sees_all_tasks; rename/fix
  test_parses_duplicate_string (duplicates produce SUCCESS, not FAILURE);
  use PaperlessTaskFactory in modified tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tasks): fix MAIL_FETCH null input_data and postrun double-query

- _extract_input_data: return {} instead of {"account_ids": None} when
  process_mail_accounts is called without an explicit account list (the
  normal beat-scheduled path); add test to cover this path
- task_postrun_handler: replace filter().first() + filter().update() with
  get() + save(update_fields=[...]) — single fetch, single write,
  consistent with task_prerun_handler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tasks): add queryset stub to satisfy drf-spectacular schema generation

TasksViewSet.get_queryset() accesses request.user, which drf-spectacular
cannot provide during static schema generation.  Adding a class-level
queryset = PaperlessTask.objects.none() gives spectacular a model to
introspect without invoking get_queryset(), eliminating both warnings
and the test_valid_schema failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tasks): fill coverage gaps in task system

- test_task_signals: add TestTaskRevokedHandler (marks REVOKED, ignores
  None request, ignores unknown id); switch existing direct
  PaperlessTask.objects.create calls to PaperlessTaskFactory; import
  pytest_mock and use MockerFixture typing on mocker params
- test_api_tasks: add test_rejects_invalid_days_param to TestSummary
- tasks.service.spec: add dismissAllTasks test (POST acknowledge_all +
  reload)
- models: add pragma: no cover to __str__, is_complete, and
  related_document_ids (trivial delegates, covered indirectly)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Well, that was a bad push.

* Fixes v9 API compatability with testing coverage

* fix(tasks): restore INDEX_OPTIMIZE enum and remove no-op run button

INDEX_OPTIMIZE was dropped from the TaskType enum but still referenced
in _RUNNABLE_TASKS (views.py) and the frontend system-status-dialog,
causing an AttributeError at import time. Restore the enum value in the
model and migration so the serializer accepts it, but remove it from
_RUNNABLE_TASKS since index_optimize is a Tantivy no-op. Remove the
frontend "Run Task" button for index optimization accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tasks): v9 type filter now matches all equivalent trigger sources

The v9 ?type= query param mapped each value to a single TriggerSource,
but the serializer maps multiple sources to the same v9 type value.
A task serialized as "auto_task" would not appear when filtering by
?type=auto_task if its trigger_source was email_consume or
folder_consume. Same issue for "manual_task" missing web_ui and
api_upload sources. Changed to trigger_source__in with the full set
of sources for each v9 type value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tasks): give task_failure_handler full ownership of FAILURE path

task_postrun_handler now early-returns for FAILURE states instead of
redundantly writing status and date_done. task_failure_handler now
computes duration_seconds and wait_time_seconds so failed tasks get
complete timing data. This eliminates a wasted .get() + .save() round
trip on every failed task and gives each handler a clean, non-overlapping
responsibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tasks): resolve trigger_source header via TriggerSource enum lookup

Replace two hardcoded string comparisons ("scheduled", "system") with a
single TriggerSource(header_source) lookup so the enum values are the
single source of truth. Any valid TriggerSource DB value passed in the
header is accepted; invalid values fall through to the document-source /
MANUAL logic. Update tests to pass enum values in headers rather than raw
strings, and add a test for the invalid-header fallback path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tasks): use TriggerSource enum values at all apply_async call sites

Replace raw strings ("system", "manual") with PaperlessTask.TriggerSource
enum values in the three callers that can import models. The settings
file remains a raw string (models cannot be imported at settings load
time) with a comment pointing to the enum value it must match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(tasks): parametrize repetitive test cases in task test files

test_api_tasks.py:
- Collapse six trigger_source->v9-type tests into one parametrized test,
  adding the previously untested API_UPLOAD case
- Collapse three task_name mapping tests (two remaps + pass-through)
  into one parametrized test
- Collapse two acknowledge_all status tests into one parametrized test
- Collapse two run-endpoint 400 tests into one parametrized test
- Update run/ assertions to use TriggerSource enum values

test_task_signals.py:
- Collapse three trigger_source header tests into one parametrized test
- Collapse two DocumentSource->TriggerSource mapping tests into one
  parametrized test
- Collapse two prerun ignore-invalid-id tests into one parametrized test

All parametrize cases use pytest.param with descriptive ids.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Handle JSON serialization for datetime and Path.  Further restrist the v9 permissions as Copilot suggests

* That should fix the generated schema/browser

* Use XSerializer for the schema

* A few more basic cases I see no value in covering

* Drops the migration related stuff too.  Just in case we want it again or it confuses people

* fix: annotate tasks_summary_retrieve as array of TaskSummarySerializer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: annotate tasks_active_retrieve as array of TaskSerializerV10

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Restore task running to superuser only

* Removes the acknowledge/dismiss all stuff

* Aligns v10 and v9 task permissions with each other

* Short blurb just to warn users about the tasks being cleared

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 09:28:41 -07:00
shamoon 20aa0937e8 Fix (dev): retain backwards compatibility with natural-date keywords in tantivy (#12602) 2026-04-20 08:26:33 -07:00
shamoon 750a2723a2 Add anti-slop job PR template requirement 2026-04-20 01:36:00 -07:00
shamoon 8edbc70dbc Oops, it should be dark 2026-04-18 16:02:09 -07:00
shamoon 862e8e2292 Fix: added date check visibility (#12600) 2026-04-18 05:14:31 -07:00
Gaëtan GOUZI ae2b780509 Fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type (#12597)
* fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type

* Add regression test

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-04-17 23:23:48 +00:00
dependabot[bot] bf6915114b Chore(deps): Bump hono (#12585)
Bumps the npm_and_yarn group with 1 update in the /src-ui directory: [hono](https://github.com/honojs/hono).


Updates `hono` from 4.12.12 to 4.12.14
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.12...v4.12.14)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.14
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 20:12:23 -07:00
GitHub Actions dc06b679d3 Auto translate strings 2026-04-15 23:22:00 +00:00
Trenton H 3ffbb8862c Feature: paginate search highlights and remove 10k document search limit (#12518)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-04-15 23:20:31 +00:00
Trenton H 21db608d57 Feature: Tune SQLite default settings for increased speed (#12580) 2026-04-15 15:26:00 -07:00
Trenton H 2fd1a1cf3a Feature: Document fuzzy match improvements (#12579)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 20:59:43 +00:00
GitHub Actions 2b4c1fe20d Auto translate strings 2026-04-15 18:58:56 +00:00
Gaëtan GOUZI 02008e40d8 fix: Return HTTP 400 instead of HTTP 500 on DELETE /api/documents/{id}/notes/ with missing or invalid note id (#12582) 2026-04-15 11:56:15 -07:00
GitHub Actions d9061d5e55 Auto translate strings 2026-04-14 22:15:38 +00:00
shamoon 8f036c9521 Merge branch 'main' into dev
# Conflicts:
#	docs/usage.md
#	src/documents/signals/handlers.py
#	src/documents/tests/test_api_documents.py
#	src/documents/views.py
2026-04-14 15:11:23 -07:00
Trenton H 17c13c1a03 Breaking: Remove the positional arguments from the pre/post consume scripts. Environment has been preferred for quite some time now (#12573) 2026-04-14 21:18:20 +00:00
github-actions[bot] 1692c916f8 Documentation: Add v2.20.14 changelog (#12577)
* Changelog v2.20.14 - GHA

* Reorganize

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-04-14 14:03:26 -07:00
dependabot[bot] 1876e38540 Chore(deps): Bump follow-redirects (#12575)
Bumps the npm_and_yarn group with 1 update in the /src-ui directory: [follow-redirects](https://github.com/follow-redirects/follow-redirects).


Updates `follow-redirects` from 1.15.11 to 1.16.0
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 20:24:03 +00:00
shamoon ffd886eae0 Bump version to 2.20.14 2026-04-14 13:11:48 -07:00
shamoon 12c0dc635e Merge branch 'release/v2.20.x' 2026-04-14 13:10:52 -07:00
shamoon 1e01ce42c0 Update usage.md 2026-04-14 13:10:40 -07:00
dependabot[bot] b51c4572b1 Chore(deps): Bump pillow in the uv group across 1 directory (#12574)
Bumps the uv group with 1 update in the / directory: [pillow](https://github.com/python-pillow/Pillow).


Updates `pillow` from 12.1.1 to 12.2.0
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/12.1.1...12.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.2.0
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 10:38:23 -07:00
dependabot[bot] a5bb3b9f8e Chore(deps-dev): Bump pytest in the uv group across 1 directory (#12568)
Bumps the uv group with 1 update in the / directory: [pytest](https://github.com/pytest-dev/pytest).


Updates `pytest` from 9.0.2 to 9.0.3
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.2...9.0.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:development
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 08:42:39 -07:00
GitHub Actions 3ed7297939 Auto translate strings 2026-04-13 21:14:40 +00:00
Trenton H 3b6edcdd8e Chore: Add generic type params and update our baselines (#12566)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:12:59 -07:00
GitHub Actions b27d10646e Auto translate strings 2026-04-13 20:12:04 +00:00
Trenton H 8c1225e120 Fixes an N+1 query in matching with the version content fetching by prefetching versions (#12562) 2026-04-13 13:10:28 -07:00
shamoon e46f4a5aaa Fix: do not submit permissions for non-owners (#12571) 2026-04-13 12:43:06 -07:00
Trenton H 54d5269145 Fix: Use an iterator in the sanity checking (#12563) 2026-04-13 12:32:22 -07:00
Trenton H f5729811fe Chore: Upgrades Django manually, since dependabot is failing. Resolves security alerts (#12567) 2026-04-13 10:20:35 -07:00
shamoon 1ba6c31385 Clarify User permission scope in docs 2026-04-07 13:38:24 -07:00
shamoon f3ee820fa4 Fix: prevent duplicate parent tag IDs (#12522) 2026-04-06 21:59:11 -07:00
shamoon 2f5bcdf66e Fix: dont defer tag change application in workflows (#12478) 2026-04-02 11:54:37 -07:00
shamoon 501cdd92d2 Fix: limit share link viewset actions (#12461) 2026-03-30 09:34:13 -07:00
shamoon 66c5c46913 Fix: add fallback ordering for documents by id after created (#12440) 2026-03-26 06:16:14 -07:00
shamoon 782634d912 Documentation: clarify barcode file type support 2026-03-23 15:45:27 -07:00
shamoon 3cfe9fa2a8 Fixhancement: default mail -created correspondent matching to exact (#12414) 2026-03-21 16:15:18 -07:00
github-actions[bot] 784fed447f Documentation: Add v2.20.13 changelog (#12408)
* Changelog v2.20.13 - GHA

* Update changelog for version 2.20.13

Added bug fixes for version 2.20.13 in changelog.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-03-21 08:50:27 -07:00
shamoon 3292a0e7cc Fix: validate date CF value in serializer (#12410) 2026-03-21 08:49:52 -07:00
329 changed files with 237042 additions and 154982 deletions
+5 -4
View File
@@ -11,7 +11,7 @@ concurrency:
group: backend-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
DEFAULT_UV_VERSION: "0.10.x"
DEFAULT_UV_VERSION: "0.11.x"
NLTK_DATA: "/usr/share/nltk_data"
permissions: {}
jobs:
@@ -100,7 +100,7 @@ jobs:
with:
python-version: "${{ matrix.python-version }}"
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -165,6 +165,7 @@ jobs:
contents: read
env:
DEFAULT_PYTHON: "3.12"
PAPERLESS_SECRET_KEY: "ci-typing-not-a-real-secret"
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -176,7 +177,7 @@ jobs:
with:
python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -200,7 +201,7 @@ jobs:
check \
src/
- name: Cache Mypy
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .mypy_cache
# Keyed by OS, Python version, and dependency hashes
+6 -6
View File
@@ -108,7 +108,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -132,7 +132,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
- name: Build and push by digest
id: build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./Dockerfile
@@ -154,7 +154,7 @@ jobs:
echo "${digest}" > "/tmp/digests/digest-${{ matrix.arch }}.txt"
- name: Upload digest
if: steps.check-push.outputs.should-push == 'true'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digests-${{ matrix.arch }}
path: /tmp/digests/digest-${{ matrix.arch }}.txt
@@ -184,20 +184,20 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
if: needs.build-arch.outputs.push-external == 'true'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Quay.io
if: needs.build-arch.outputs.push-external == 'true'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
+3 -3
View File
@@ -11,7 +11,7 @@ concurrency:
permissions:
contents: read
env:
DEFAULT_UV_VERSION: "0.10.x"
DEFAULT_UV_VERSION: "0.11.x"
DEFAULT_PYTHON_VERSION: "3.12"
jobs:
changes:
@@ -78,7 +78,7 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -94,7 +94,7 @@ jobs:
--frozen \
zensical build --clean
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site
name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
+16 -16
View File
@@ -81,18 +81,18 @@ jobs:
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.pnpm-store
@@ -113,17 +113,17 @@ jobs:
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.pnpm-store
@@ -152,17 +152,17 @@ jobs:
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.pnpm-store
@@ -191,7 +191,7 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
container: mcr.microsoft.com/playwright:v1.59.0-noble
container: mcr.microsoft.com/playwright:v1.59.1-noble
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
@@ -207,17 +207,17 @@ jobs:
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.pnpm-store
@@ -244,17 +244,17 @@ jobs:
fetch-depth: 2
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.pnpm-store
+1 -1
View File
@@ -25,4 +25,4 @@ jobs:
with:
python-version: "3.14"
- name: Run prek
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2
+14 -12
View File
@@ -8,7 +8,7 @@ concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
DEFAULT_UV_VERSION: "0.10.x"
DEFAULT_UV_VERSION: "0.11.x"
DEFAULT_PYTHON_VERSION: "3.12"
permissions: {}
jobs:
@@ -20,10 +20,10 @@ jobs:
statuses: read
steps:
- name: Wait for Docker build
uses: lewagon/wait-on-check-action@74049309dfeff245fe8009a0137eacf28136cb3c # v1.5.0
uses: lewagon/wait-on-check-action@9312864dfbc9fd208e9c0417843430751c042800 # v1.7.0
with:
ref: ${{ github.sha }}
check-name: 'Build Docker Image'
check-name: 'Merge and Push Manifest'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 60
build-release:
@@ -39,11 +39,11 @@ jobs:
persist-credentials: false
# ---- Frontend Build ----
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.x
package-manager-cache: false
@@ -58,7 +58,7 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false
@@ -88,6 +88,7 @@ jobs:
uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt
- name: Compile messages
env:
PAPERLESS_SECRET_KEY: "ci-release-not-a-real-secret"
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
run: |
cd src/
@@ -96,6 +97,7 @@ jobs:
manage.py compilemessages
- name: Collect static files
env:
PAPERLESS_SECRET_KEY: "ci-release-not-a-real-secret"
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
run: |
cd src/
@@ -133,7 +135,7 @@ jobs:
sudo chown -R 1000:1000 paperless-ngx/
tar -cJf paperless-ngx.tar.xz paperless-ngx/
- name: Upload release artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release
path: dist/paperless-ngx.tar.xz
@@ -168,18 +170,18 @@ jobs:
fi
- name: Create release and changelog
id: create-release
uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1
uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
with:
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.version }}
version: ${{ steps.get-version.outputs.version }}
prerelease: ${{ steps.get-version.outputs.prerelease }}
publish: true
commitish: main
commitish: ${{ steps.get-version.outputs.prerelease == 'true' && 'dev' || 'main' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release archive
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1.10.0
uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1.10.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
upload_url: ${{ steps.create-release.outputs.upload_url }}
@@ -209,7 +211,7 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false
@@ -248,7 +250,7 @@ jobs:
git commit -am "Changelog ${VERSION} - GHA"
git push origin "${branch_name}"
- name: Create pull request
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
VERSION: ${{ needs.publish-release.outputs.version }}
with:
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
semgrep:
name: Semgrep CE
runs-on: ubuntu-24.04
@@ -44,7 +44,7 @@ jobs:
- name: Run Semgrep
run: semgrep scan --config auto --sarif-output results.sarif
- name: Upload results to GitHub code scanning
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
if: always()
with:
sarif_file: results.sarif
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -47,4 +47,4 @@ jobs:
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
token: ${{ secrets.PNGX_BOT_PAT }}
persist-credentials: false
- name: crowdin action
uses: crowdin/github-action@7ca9c452bfe9197d3bb7fa83a4d7e2b0c9ae835d # v2.16.0
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
with:
upload_translations: false
download_translations: true
+6 -5
View File
@@ -3,17 +3,18 @@ on:
pull_request_target:
types: [opened]
jobs:
anti-slop:
Anti-slop:
runs-on: ubuntu-latest
permissions:
contents: read
issues: read
pull-requests: write
steps:
- uses: peakoss/anti-slop@85daca1880e9e1af197fc06ea03349daf08f4202 # v0.2.1
- uses: peakoss/anti-slop@57858eead489d08b255fab2af45a506c2ca6eab2 # v0.3.0
with:
max-failures: 4
failure-add-pr-labels: 'ai'
require-pr-template: true
pr-bot:
name: Automated PR Bot
runs-on: ubuntu-latest
@@ -37,7 +38,7 @@ jobs:
fail_if_xl: 'false'
excluded_files: /\.lock$/ /\.txt$/ ^src-ui/pnpm-lock\.yaml$ ^src-ui/messages\.xlf$ ^src/locale/en_US/LC_MESSAGES/django\.po$
- name: Label by PR title
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pr = context.payload.pull_request;
@@ -63,7 +64,7 @@ jobs:
}
- name: Label bot-generated PRs
if: ${{ contains(github.actor, 'dependabot') || contains(github.actor, 'crowdin-bot') }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pr = context.payload.pull_request;
@@ -88,7 +89,7 @@ jobs:
}
- name: Welcome comment
if: ${{ !contains(github.actor, 'bot') }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pr = context.payload.pull_request;
+1 -1
View File
@@ -19,6 +19,6 @@ jobs:
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
steps:
- name: Label PR with release-drafter
uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1
uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -3
View File
@@ -62,7 +62,7 @@ jobs:
permissions:
discussions: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
function sleep(ms) {
@@ -121,7 +121,7 @@ jobs:
permissions:
discussions: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
function sleep(ms) {
@@ -215,7 +215,7 @@ jobs:
permissions:
discussions: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
function sleep(ms) {
+9 -4
View File
@@ -3,6 +3,8 @@ on:
push:
branches:
- dev
env:
DEFAULT_UV_VERSION: "0.11.x"
jobs:
generate-translate-strings:
name: Generate Translation Strings
@@ -27,8 +29,9 @@ jobs:
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
- name: Install backend python dependencies
run: |
@@ -36,20 +39,22 @@ jobs:
--group dev \
--frozen
- name: Generate backend translation strings
env:
PAPERLESS_SECRET_KEY: "ci-translate-not-a-real-secret"
run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*"
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.pnpm-store
+3
View File
@@ -79,6 +79,7 @@ virtualenv
/docker-compose.env
/docker-compose.yml
.ruff_cache/
.mypy_cache/
# Used for development
scripts/import-for-development
@@ -111,4 +112,6 @@ celerybeat-schedule*
# ignore pnpm package store folder created when setting up the devcontainer
.pnpm-store/
# Git worktree local folder
.worktrees
+1076 -1211
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -38,7 +38,7 @@ repos:
- json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.8.1'
rev: 'v3.8.3'
hooks:
- id: prettier
types_or:
@@ -46,16 +46,16 @@ repos:
- ts
- markdown
additional_dependencies:
- prettier@3.8.1
- prettier@3.8.3
- 'prettier-plugin-organize-imports@4.3.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.8
rev: v0.15.12
hooks:
- id: ruff-check
- id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.21.0"
rev: "v2.21.1"
hooks:
- id: pyproject-fmt
# Dockerfile hooks
+6165 -8973
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -30,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.10.9-python3.12-trixie-slim AS s6-overlay-base
FROM ghcr.io/astral-sh/uv:0.11.6-python3.12-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6
+3 -3
View File
@@ -7,9 +7,9 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/resources/logo/web/png/White%20logo%20-%20no%20background.png" width="50%">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/docs/assets/logo_full_white.png" width="50%">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/docs/assets/logo_full_black.png" width="50%">
<img src="https://github.com/paperless-ngx/paperless-ngx/blob/main/docs/assets/logo_full_black.png" width="50%">
</picture>
</p>
+1
View File
@@ -57,6 +57,7 @@ We may close reports that are:
The following are not generally considered vulnerabilities unless accompanied by a concrete, reproducible impact in Paperless-ngx:
- large uploads or resource usage that do not bypass documented limits or privileges
- IDOR / access control claims regarding the ability to attach an un-viewable object to a document. This is expected behavior.
- claims based solely on the presence of a library, framework feature or code pattern without a working exploit
- reports that rely on admin-level access, workflow-editing privileges, shell access, or other high-trust roles unless they demonstrate an unintended privilege boundary bypass
- optional webhook, mail, AI, OCR, or integration behavior described without a product-level vulnerability
+7 -6
View File
@@ -357,12 +357,13 @@ and the script does the rest of the work:
document_importer source
```
| Option | Required | Default | Description |
| ------------------- | -------- | ------- | ------------------------------------------------------------------------- |
| source | Yes | N/A | The directory containing an export |
| `--no-progress-bar` | No | False | If provided, the progress bar will be hidden |
| `--data-only` | No | False | If provided, only import data, do not import document files or thumbnails |
| `--passphrase` | No | N/A | If your export was encrypted with a passphrase, must be provided |
| Option | Required | Default | Description |
| ------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------ |
| source | Yes | N/A | The directory containing an export |
| `--no-progress-bar` | No | False | If provided, the progress bar will be hidden |
| `--data-only` | No | False | If provided, only import data, do not import document files or thumbnails |
| `--passphrase` | No | N/A | If your export was encrypted with a passphrase, must be provided |
| `--batch-size` | No | 500 | Number of database records inserted per batch. Lower values reduce peak memory usage on very large installs. |
When you use the provided docker compose script, put the export inside
the `export` folder in your paperless source directory. Specify
+1 -1
View File
@@ -842,7 +842,7 @@ MariaDB: `mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql -p`
## Barcodes {#barcodes}
Paperless is able to utilize barcodes for automatically performing some tasks.
Paperless is able to utilize barcodes for automatically performing some tasks. Barcodes are only supported for PDF documents or TIFF, [if enabled](configuration.md#PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT).
At this time, the library utilized for detection of barcodes supports the following types:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 748 B

-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M299,891.7c-4.2-19.8-12.5-59.6-13.6-59.6c-176.7-105.7-155.8-288.7-97.3-393.4
c12.5,131.8,245.8,222.8,109.8,383.9c-1.1,2,6.2,27.2,12.5,50.2c27.2-46,68-101.4,65.8-106.7C208.9,358.2,731.9,326.9,840.6,73.7
c49.1,244.8-25.1,623.5-445.5,719.7c-2,1.1-76.3,131.8-79.5,132.9c0-2-31.4-1.1-27.2-11.5C290.7,908.4,294.8,900.1,299,891.7
L299,891.7z M293.8,793.4c53.3-61.8-9.4-167.4-47.1-201.9C310.5,701.3,306.3,765.1,293.8,793.4L293.8,793.4z"/>
</svg>

Before

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 80 KiB

+18 -67
View File
@@ -1,68 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2962.2 860.2" style="enable-background:new 0 0 2962.2 860.2;" xml:space="preserve">
<style type="text/css">
.st0{fill:#17541F;stroke:#000000;stroke-miterlimit:10;}
</style>
<path d="M1055.6,639.7v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3v249.6L1055.6,639.7L1055.6,639.7z M1059.1,514.9c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
c16.8,0,30.3-5.9,40.6-17.7C1054,546.9,1059.1,532.3,1059.1,514.9z"/>
<path d="M1417.8,398.2c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V390.2h85.3v20.6
c18-20,43.1-30.1,75.4-30.1C1379.2,380.7,1399.5,386.6,1417.8,398.2z M1389.5,514.9c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7S1389.5,532.3,1389.5,514.9z"/>
<path d="M1713.6,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2s37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8c3.6,11.4,10.5,20.7,20.9,28.1
c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1695.8,570.1,1704.9,563.7,1713.6,555.3z M1596.9,486.2h92.9
c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11S1599,473.9,1596.9,486.2z"/>
<path d="M1908.8,418.4c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V390.2h85.3V418.4L1908.8,418.4z"/>
<path d="M2113,258.2v381.5h-85.3V258.2H2113z"/>
<path d="M2360.8,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2343.1,570.1,2352.1,563.7,2360.8,555.3z
M2244.1,486.2h92.9c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11C2251.7,464.1,2246.2,473.9,2244.1,486.2z"/>
<path d="M2565.9,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2590.5,448.7,2577.6,446.3,2565.9,446.3z"/>
<path d="M2817.3,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2841.8,448.7,2828.9,446.3,2817.3,446.3z"/>
<g>
<path d="M2508,724h60.2v17.3H2508V724z"/>
<path d="M2629.2,694.4c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6v-52.6
c0-9.3-1.7-16.2-5.1-20.7c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2620.2,699.4,2624.4,696.4,2629.2,694.4z"/>
<path d="M2790.3,833.2c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2s-6.6-11.9-7.1-19.6h19.6
c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7c-3.6,3.4-8,6.2-13.3,8.2
c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6s2-17.3,6-24.7
s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2803.2,815.9,2798.9,826.4,2790.3,833.2z M2782.2,755.7c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2776.1,764.1,2779.6,760.4,2782.2,755.7z"/>
<path d="M2843.5,788.4h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2843.5,788.4z
"/>
</g>
<path d="M835.8,319.2c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2H647v-135h72.7c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7c11.5-18.9,17.3-39.5,17.3-61.9
C853.1,358.9,847.4,338.1,835.8,319.2z M747,416.6c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C761.1,396.6,756.4,407.7,747,416.6z"/>
<path class="st0" d="M164.7,698.7c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4C82.8,431.4,277,507.1,163.8,641.2
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C89.7,254.7,525,228.6,615.5,17.9c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C157.8,712.6,161.2,705.7,164.7,698.7L164.7,698.7z M160.4,616.9
c44.4-51.4-7.8-139.3-39.2-168C174.3,540.2,170.8,593.3,160.4,616.9L160.4,616.9z"/>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2670 860">
<path id="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/>
<g id="text" style="fill: #000;">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2670 860">
<path id="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/>
<g id="text" style="fill: #eee;">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 97 KiB

+18 -68
View File
@@ -1,69 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2962.2 860.2" style="enable-background:new 0 0 2962.2 860.2;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
.st1{fill:#17541F;stroke:#000000;stroke-miterlimit:10;}
</style>
<path class="st0" d="M1055.6,639.7v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3v249.6L1055.6,639.7L1055.6,639.7z M1059.1,514.9c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
c16.8,0,30.3-5.9,40.6-17.7C1054,546.9,1059.1,532.3,1059.1,514.9z"/>
<path class="st0" d="M1417.8,398.2c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V390.2h85.3v20.6
c18-20,43.1-30.1,75.4-30.1C1379.2,380.7,1399.5,386.6,1417.8,398.2z M1389.5,514.9c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7S1389.5,532.3,1389.5,514.9z"/>
<path class="st0" d="M1713.6,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2s37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8c3.6,11.4,10.5,20.7,20.9,28.1
c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1695.8,570.1,1704.9,563.7,1713.6,555.3z M1596.9,486.2h92.9
c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11S1599,473.9,1596.9,486.2z"/>
<path class="st0" d="M1908.8,418.4c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7
c-8.4-2.1-15.7-3.1-22-3.1c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V390.2h85.3V418.4L1908.8,418.4z"/>
<path class="st0" d="M2113,258.2v381.5h-85.3V258.2H2113z"/>
<path class="st0" d="M2360.8,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2343.1,570.1,2352.1,563.7,2360.8,555.3z
M2244.1,486.2h92.9c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11C2251.7,464.1,2246.2,473.9,2244.1,486.2z"/>
<path class="st0" d="M2565.9,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2590.5,448.7,2577.6,446.3,2565.9,446.3z"/>
<path class="st0" d="M2817.3,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2841.8,448.7,2828.9,446.3,2817.3,446.3z"/>
<g>
<path class="st0" d="M2508,724h60.2v17.3H2508V724z"/>
<path class="st0" d="M2629.2,694.4c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6v-52.6
c0-9.3-1.7-16.2-5.1-20.7c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2620.2,699.4,2624.4,696.4,2629.2,694.4z"/>
<path class="st0" d="M2790.3,833.2c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2s-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2803.2,815.9,2798.9,826.4,2790.3,833.2z M2782.2,755.7c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2776.1,764.1,2779.6,760.4,2782.2,755.7z"/>
<path class="st0" d="M2843.5,788.4h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35
L2843.5,788.4z"/>
</g>
<path class="st0" d="M835.8,319.2c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2H647v-135h72.7c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7c11.5-18.9,17.3-39.5,17.3-61.9
C853.1,358.9,847.4,338.1,835.8,319.2z M747,416.6c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C761.1,396.6,756.4,407.7,747,416.6z"/>
<path class="st1" d="M164.7,698.7c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4C82.8,431.4,277,507.1,163.8,641.2
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C89.7,254.7,525,228.6,615.5,17.9c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C157.8,712.6,161.2,705.7,164.7,698.7L164.7,698.7z M160.4,616.9
c44.4-51.4-7.8-139.3-39.2-168C174.3,540.2,170.8,593.3,160.4,616.9L160.4,616.9z"/>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2670 860">
<path id="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/>
<g id="text" style="fill: #fff;">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000">
<defs>
<style>
.st0 {
fill: #005616;
}
</style>
</defs>
<path class="st0" d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 644 B

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000">
<defs>
<style>
.st0 {
fill: #fff;
}
</style>
</defs>
<path class="st0" d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 641 B

+59
View File
@@ -1,5 +1,64 @@
# Changelog
## paperless-ngx 2.20.15
### Security
- Resolve [GHSA-96jx-fj7m-qh6x](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-8c6x-pfjq-9gr7)
### Bug Fixes
- Fix: use only allauth login/logout endpoints [@shamoon](https://github.com/shamoon) ([#12639](https://github.com/paperless-ngx/paperless-ngx/pull/12639))
- Fix: correctly scope mail account enumeration [@shamoon](https://github.com/shamoon) ([#12636](https://github.com/paperless-ngx/paperless-ngx/pull/12636))
- Fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type [@ggouzi](https://github.com/ggouzi) ([#12597](https://github.com/paperless-ngx/paperless-ngx/pull/12597))
- Fix: reject invalid requests to API notes endpoint [@ggouzi](https://github.com/ggouzi) ([#12582](https://github.com/paperless-ngx/paperless-ngx/pull/12582))
### All App Changes
<details>
<summary>4 changes</summary>
- Fix: use only allauth login/logout endpoints [@shamoon](https://github.com/shamoon) ([#12639](https://github.com/paperless-ngx/paperless-ngx/pull/12639))
- Fix: correctly scope mail account enumeration [@shamoon](https://github.com/shamoon) ([#12636](https://github.com/paperless-ngx/paperless-ngx/pull/12636))
- Fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type [@ggouzi](https://github.com/ggouzi) ([#12597](https://github.com/paperless-ngx/paperless-ngx/pull/12597))
- Fix: reject invalid requests to API notes endpoint [@ggouzi](https://github.com/ggouzi) ([#12582](https://github.com/paperless-ngx/paperless-ngx/pull/12582))
</details>
## paperless-ngx 2.20.14
### Bug Fixes
- Fix: do not submit permissions for non-owners [@shamoon](https://github.com/shamoon) ([#12571](https://github.com/paperless-ngx/paperless-ngx/pull/12571))
- Fix: prevent duplicate parent tag IDs [@shamoon](https://github.com/shamoon) ([#12522](https://github.com/paperless-ngx/paperless-ngx/pull/12522))
- Fix: dont defer tag change application in workflows [@shamoon](https://github.com/shamoon) ([#12478](https://github.com/paperless-ngx/paperless-ngx/pull/12478))
- Fix: limit share link viewset actions [@shamoon](https://github.com/shamoon) ([#12461](https://github.com/paperless-ngx/paperless-ngx/pull/12461))
- Fix: add fallback ordering for documents by id after created [@shamoon](https://github.com/shamoon) ([#12440](https://github.com/paperless-ngx/paperless-ngx/pull/12440))
- Fixhancement: default mail-created correspondent matching to exact [@shamoon](https://github.com/shamoon) ([#12414](https://github.com/paperless-ngx/paperless-ngx/pull/12414))
- Fix: validate date CF value in serializer [@shamoon](https://github.com/shamoon) ([#12410](https://github.com/paperless-ngx/paperless-ngx/pull/12410))
### All App Changes
<details>
<summary>7 changes</summary>
- Fix: do not submit permissions for non-owners [@shamoon](https://github.com/shamoon) ([#12571](https://github.com/paperless-ngx/paperless-ngx/pull/12571))
- Fix: prevent duplicate parent tag IDs [@shamoon](https://github.com/shamoon) ([#12522](https://github.com/paperless-ngx/paperless-ngx/pull/12522))
- Fix: dont defer tag change application in workflows [@shamoon](https://github.com/shamoon) ([#12478](https://github.com/paperless-ngx/paperless-ngx/pull/12478))
- Fix: limit share link viewset actions [@shamoon](https://github.com/shamoon) ([#12461](https://github.com/paperless-ngx/paperless-ngx/pull/12461))
- Fix: add fallback ordering for documents by id after created [@shamoon](https://github.com/shamoon) ([#12440](https://github.com/paperless-ngx/paperless-ngx/pull/12440))
- Fixhancement: default mail-created correspondent matching to exact [@shamoon](https://github.com/shamoon) ([#12414](https://github.com/paperless-ngx/paperless-ngx/pull/12414))
- Fix: validate date CF value in serializer [@shamoon](https://github.com/shamoon) ([#12410](https://github.com/paperless-ngx/paperless-ngx/pull/12410))
</details>
## paperless-ngx 2.20.13
### Bug Fixes
- Fix: suggest corrections only if visible results
- Fix: require view permission for more-like search
- Fix: validate document link targets
- Fix: enforce permissions when attaching accounts to mail rules
## paperless-ngx 2.20.12
### Security
+45 -19
View File
@@ -101,7 +101,7 @@ and `mariadb`.
#### [`PAPERLESS_DB_OPTIONS=<options>`](#PAPERLESS_DB_OPTIONS) {#PAPERLESS_DB_OPTIONS}
: Advanced database connection options as a semicolon-delimited key-value string.
: Advanced database connection options as a comma-delimited key-value string.
Keys and values are separated by `=`. Dot-notation produces nested option
dictionaries; for example, `pool.max_size=20` sets
`OPTIONS["pool"]["max_size"] = 20`.
@@ -123,18 +123,36 @@ dictionaries; for example, `pool.max_size=20` sets
to handle all pool connections across all workers:
`(web_workers + celery_workers) * pool.max_size + safety_margin`.
!!! note "SQLite defaults"
SQLite connections are pre-configured with WAL journal mode, optimised
synchronous and cache settings, and a 5-second busy timeout. These defaults
suit most deployments. To override `init_command`, use `;` between PRAGMAs
within the value and `,` between options:
```bash
PAPERLESS_DB_OPTIONS="init_command=PRAGMA journal_mode=DELETE;PRAGMA synchronous=FULL,transaction_mode=DEFERRED"
```
!!! note "MariaDB: READ COMMITTED isolation level"
MariaDB connections default to `READ COMMITTED` isolation level, which
eliminates gap locking and reduces deadlock frequency. If binary logging is
enabled on your MariaDB server, this requires `binlog_format=ROW` (the
default for most managed MariaDB instances). Statement-based replication is
not compatible with `READ COMMITTED`.
**Examples:**
```bash title="PostgreSQL: require SSL, set a custom CA certificate, and limit the pool size"
PAPERLESS_DB_OPTIONS="sslmode=require;sslrootcert=/certs/ca.pem;pool.max_size=5"
PAPERLESS_DB_OPTIONS="sslmode=require,sslrootcert=/certs/ca.pem,pool.max_size=5"
```
```bash title="MariaDB: require SSL with a custom CA certificate"
PAPERLESS_DB_OPTIONS="ssl_mode=REQUIRED;ssl.ca=/certs/ca.pem"
PAPERLESS_DB_OPTIONS="ssl_mode=REQUIRED,ssl.ca=/certs/ca.pem"
```
```bash title="SQLite: set a busy timeout of 30 seconds"
# PostgreSQL: set a connection timeout
```bash title="PostgreSQL or MariaDB: set a connection timeout"
PAPERLESS_DB_OPTIONS="connect_timeout=10"
```
@@ -1996,49 +2014,57 @@ suggestions. This setting is required to be set to true in order to use the AI f
#### [`PAPERLESS_AI_LLM_EMBEDDING_BACKEND=<str>`](#PAPERLESS_AI_LLM_EMBEDDING_BACKEND) {#PAPERLESS_AI_LLM_EMBEDDING_BACKEND}
: The embedding backend to use for RAG. This can be either "openai" or "huggingface".
: The embedding backend to use for RAG. This can be either "openai-like" or "huggingface". The
"openai-like" backend uses an OpenAI-compatible embeddings API.
Defaults to None.
#### [`PAPERLESS_AI_LLM_EMBEDDING_MODEL=<str>`](#PAPERLESS_AI_LLM_EMBEDDING_MODEL) {#PAPERLESS_AI_LLM_EMBEDDING_MODEL}
: The model to use for the embedding backend for RAG. This can be set to any of the embedding models supported by the current embedding backend. If not supplied, defaults to "text-embedding-3-small" for OpenAI and "sentence-transformers/all-MiniLM-L6-v2" for Huggingface.
: The model to use for the embedding backend for RAG. This can be set to any of the embedding
models supported by the current embedding backend. If not supplied, defaults to
"text-embedding-3-small" for the OpenAI-compatible backend and
"sentence-transformers/all-MiniLM-L6-v2" for Huggingface.
Defaults to None.
#### [`PAPERLESS_AI_LLM_BACKEND=<str>`](#PAPERLESS_AI_LLM_BACKEND) {#PAPERLESS_AI_LLM_BACKEND}
: The AI backend to use. This can be either "openai" or "ollama". If set to "ollama", the AI
features will be run locally on your machine. If set to "openai", the AI features will be run
using the OpenAI API. This setting is required to be set to use the AI features.
: The AI backend to use. This can be either "openai-like" or "ollama". If set to "ollama", the AI
features will be run locally on your machine. If set to "openai-like", the AI features will use
an OpenAI-compatible API endpoint, including OpenAI itself and compatible providers. This
setting is required to be set to use the AI features.
Defaults to None.
!!! note
The OpenAI API is a paid service. You will need to set up an OpenAI account and
will be charged for usage incurred by Paperless-ngx features and your document data
will (of course) be sent to the OpenAI API. Paperless-ngx does not endorse the use of the
OpenAI API in any way.
Remote AI providers may be paid services. If you use a hosted OpenAI-compatible API, you
are responsible for any usage charges incurred by Paperless-ngx features, and your
document data will be sent to the provider you configure.
Refer to the OpenAI terms of service, and use at your own risk.
Paperless-ngx does not endorse any specific provider. Refer to your provider's terms of
service and privacy policy, and use at your own risk.
#### [`PAPERLESS_AI_LLM_MODEL=<str>`](#PAPERLESS_AI_LLM_MODEL) {#PAPERLESS_AI_LLM_MODEL}
: The model to use for the AI backend, i.e. "gpt-3.5-turbo", "gpt-4" or any of the models supported by the
current backend. If not supplied, defaults to "gpt-3.5-turbo" for OpenAI and "llama3.1" for Ollama.
: The model to use for the AI backend, i.e. "gpt-3.5-turbo", "gpt-4" or any of the models supported
by the current backend. If not supplied, defaults to "gpt-3.5-turbo" for the OpenAI-compatible
backend and "llama3.1" for Ollama.
Defaults to None.
#### [`PAPERLESS_AI_LLM_API_KEY=<str>`](#PAPERLESS_AI_LLM_API_KEY) {#PAPERLESS_AI_LLM_API_KEY}
: The API key to use for the AI backend. This is required for the OpenAI backend (optional for others).
: The API key to use for the AI backend. This is typically required for the OpenAI-compatible
backend (optional for others).
Defaults to None.
#### [`PAPERLESS_AI_LLM_ENDPOINT=<str>`](#PAPERLESS_AI_LLM_ENDPOINT) {#PAPERLESS_AI_LLM_ENDPOINT}
: The endpoint / url to use for the AI backend. This is required for the Ollama backend (optional for others).
: The endpoint / url to use for the AI backend. This is required for the Ollama backend and may be
used with the OpenAI-compatible backend to target a custom provider or local gateway.
Defaults to None.
+1 -1
View File
@@ -4,7 +4,7 @@ title: Home
<div class="grid-left" markdown>
![image](assets/logo_full_black.svg#only-light){.index-logo}
![image](assets/logo_full_white.svg#only-dark){.index-logo}
![image](assets/logo_full_eee.svg#only-dark){.index-logo}
**Paperless-ngx** is a _community-supported_ open-source document management system that transforms your
physical documents into a searchable online archive so you can keep, well, _less paper_.
+70 -1
View File
@@ -120,7 +120,7 @@ Users with any of the deprecated variables set should migrate to `PAPERLESS_DB_O
Multiple options are combined in a single value:
```bash
PAPERLESS_DB_OPTIONS="sslmode=require;sslrootcert=/certs/ca.pem;pool.max_size=10"
PAPERLESS_DB_OPTIONS="sslmode=require,sslrootcert=/certs/ca.pem,pool.max_size=10"
```
## OCR and Archive File Generation Settings
@@ -241,3 +241,72 @@ For example:
}
}
```
## Task History Cleared on Upgrade
The task tracking system has been redesigned in this release. All existing task history records are dropped from the database during the upgrade. Previously completed, failed, or acknowledged tasks will no longer appear in the task list after upgrading.
No user action is required.
## Consume Script Positional Arguments Removed
Pre- and post-consumption scripts no longer receive positional arguments. All information is
now passed exclusively via environment variables, which have been available since earlier versions.
### Pre-consumption script
Previously, the original file path was passed as `$1`. It is now only available as
`DOCUMENT_SOURCE_PATH`.
**Before:**
```bash
#!/usr/bin/env bash
# $1 was the original file path
process_document "$1"
```
**After:**
```bash
#!/usr/bin/env bash
process_document "${DOCUMENT_SOURCE_PATH}"
```
### Post-consumption script
Previously, document metadata was passed as positional arguments `$1` through `$8`:
| Argument | Environment Variable Equivalent |
| -------- | ------------------------------- |
| `$1` | `DOCUMENT_ID` |
| `$2` | `DOCUMENT_FILE_NAME` |
| `$3` | `DOCUMENT_SOURCE_PATH` |
| `$4` | `DOCUMENT_THUMBNAIL_PATH` |
| `$5` | `DOCUMENT_DOWNLOAD_URL` |
| `$6` | `DOCUMENT_THUMBNAIL_URL` |
| `$7` | `DOCUMENT_CORRESPONDENT` |
| `$8` | `DOCUMENT_TAGS` |
**Before:**
```bash
#!/usr/bin/env bash
DOCUMENT_ID=$1
CORRESPONDENT=$7
TAGS=$8
```
**After:**
```bash
#!/usr/bin/env bash
# Use environment variables directly
echo "Document ${DOCUMENT_ID} from ${DOCUMENT_CORRESPONDENT} tagged: ${DOCUMENT_TAGS}"
```
### Action Required
Update any pre- or post-consumption scripts that read `$1`, `$2`, etc. to use the
corresponding environment variables instead. Environment variables have been the preferred
option since v1.8.0.
+18 -9
View File
@@ -302,13 +302,19 @@ Paperless-ngx includes several features that use AI to enhance the document mana
!!! warning
Remember that Paperless-ngx will send document content to the AI provider you have configured, so consider the privacy implications of using these features, especially if using a remote model (e.g. OpenAI), instead of the default local model.
Remember that Paperless-ngx will send document content to the AI provider you have configured,
so consider the privacy implications of using these features, especially if using a remote
model or API provider instead of the default local model.
The AI features work by creating an embedding of the text content and metadata of documents, which is then used for various tasks such as similarity search and question answering. This uses the FAISS vector store.
### AI-Enhanced Suggestions
If enabled, Paperless-ngx can use an AI LLM model to suggest document titles, dates, tags, correspondents and document types for documents. This feature will always be "opt-in" and does not disable the existing classifier-based suggestion system. Currently, both remote (via the OpenAI API) and local (via Ollama) models are supported, see [configuration](configuration.md#ai) for details.
If enabled, Paperless-ngx can use an AI LLM model to suggest document titles, dates, tags,
correspondents and document types for documents. This feature will always be "opt-in" and does not
disable the existing classifier-based suggestion system. Currently, both remote
(via OpenAI-compatible APIs) and local (via Ollama) models are supported, see
[configuration](configuration.md#ai) for details.
### Document Chat
@@ -414,10 +420,10 @@ still have "object-level" permissions.
| SavedView | Add, edit, delete or view Saved Views. |
| ShareLink | Add, delete or view Share Links. |
| StoragePath | Add, edit, delete or view Storage Paths. |
| SystemStatus | View the system status dialog and corresponding API endpoint. Admin users also retain system status access. |
| SystemMonitoring | View the system status dialog, tasks summary and their API endpoints. Admin users also retain system status access. |
| Tag | Add, edit, delete or view Tags. |
| UISettings | Add, edit, delete or view the UI settings that are used by the web app.<br/>:warning: **Users that will access the web UI must be granted at least _View_ permissions.** |
| User | Add, edit, delete or view Users. |
| User | Add, edit, delete or view other user accounts via Settings > Users & Groups and `/api/users/`. These permissions are not needed for users to edit their own profile via "My Profile" or `/api/profile/`. |
| Workflow | Add, edit, delete or view Workflows.<br/>Note that Workflows are global; all users who can access workflows see the same set. Workflows have other permission implications — see [Workflow permissions](#workflow-permissions). |
#### Detailed Explanation of Object Permissions {#object-permissions}
@@ -428,6 +434,8 @@ still have "object-level" permissions.
| View | Confers the ability to view (not edit) a document, tag, etc.<br/>Users without 'view' (or higher) permissions will be shown _'Private'_ in place of the object name for example when viewing a document with a tag for which the user doesn't have permissions. |
| Edit | Confers the ability to edit (and view) a document, tag, etc. |
For related metadata such as tags, correspondents, document types, and storage paths, object visibility and document assignment are intentionally distinct. A user may still retain or submit a known object ID when editing a document even if that related object is displayed as _Private_ or omitted from search and selection results. This allows documents to preserve existing assignments that the current user cannot necessarily inspect in detail.
### Password reset
In order to enable the password reset feature you will need to setup an SMTP backend, see
@@ -853,13 +861,14 @@ Matching natural date keywords:
```
added:today
modified:yesterday
created:this_week
added:last_month
modified:this_year
created:"previous week"
added:"previous month"
modified:"this year"
```
Supported date keywords: `today`, `yesterday`, `this_week`, `last_week`,
`this_month`, `last_month`, `this_year`, `last_year`.
Supported date keywords: `today`, `yesterday`, `previous week`,
`this month`, `previous month`, `this year`, `previous year`,
`previous quarter`.
#### Searching custom fields
+16 -2
View File
@@ -30,11 +30,25 @@
"**/.idea": true,
"**/.venv": true,
"**/.coverage": true,
"**/coverage.json": true
"**/coverage.json": true,
"htmlcov/": true,
"coverage.xml": true,
"junit.xml": true
},
"python.defaultInterpreterPath": ".venv/bin/python3",
"python.languageServer": "Pylance",
"python.defaultInterpreterPath": "${workspaceFolder:paperless-ngx}/.venv/bin/python3",
"python.analysis.extraPaths": ["${workspaceFolder:paperless-ngx}/src"],
"python.analysis.inlayHints.pytestParameters": true,
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
}
},
"extensions": {
"recommendations": ["ms-python.python", "charliermarsh.ruff", "editorconfig.editorconfig"],
-150
View File
@@ -1,150 +0,0 @@
"""
Temporary profiling utilities for comparing implementations.
Usage in a management command or shell::
from profiling import profile_block, profile_cpu, measure_memory
with profile_block("new check_sanity"):
messages = check_sanity()
with profile_block("old check_sanity"):
messages = check_sanity_old()
Drop this file when done.
"""
from __future__ import annotations
import tracemalloc
from collections.abc import Callable # noqa: TC003
from collections.abc import Generator # noqa: TC003
from contextlib import contextmanager
from time import perf_counter
from typing import Any
from django.db import connection
from django.db import reset_queries
from django.test.utils import override_settings
@contextmanager
def profile_block(label: str = "block") -> Generator[None, None, None]:
"""Profile memory, wall time, and DB queries for a code block.
Prints a summary to stdout on exit. Requires no external packages.
Enables DEBUG temporarily to capture Django's query log.
"""
tracemalloc.start()
snapshot_before = tracemalloc.take_snapshot()
with override_settings(DEBUG=True):
reset_queries()
start = perf_counter()
yield
elapsed = perf_counter() - start
queries = list(connection.queries)
snapshot_after = tracemalloc.take_snapshot()
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
# Compare snapshots for top allocations
stats = snapshot_after.compare_to(snapshot_before, "lineno")
query_time = sum(float(q["time"]) for q in queries)
mem_diff = sum(s.size_diff for s in stats)
print(f"\n{'=' * 60}") # noqa: T201
print(f" Profile: {label}") # noqa: T201
print(f"{'=' * 60}") # noqa: T201
print(f" Wall time: {elapsed:.4f}s") # noqa: T201
print(f" Queries: {len(queries)} ({query_time:.4f}s)") # noqa: T201
print(f" Memory delta: {mem_diff / 1024:.1f} KiB") # noqa: T201
print(f" Peak memory: {peak / 1024:.1f} KiB") # noqa: T201
print("\n Top 5 allocations:") # noqa: T201
for stat in stats[:5]:
print(f" {stat}") # noqa: T201
print(f"{'=' * 60}\n") # noqa: T201
def profile_cpu(
fn: Callable[[], Any],
*,
label: str,
top: int = 30,
sort: str = "cumtime",
) -> tuple[Any, float]:
"""Run *fn()* under cProfile, print stats, return (result, elapsed_s).
Args:
fn: Zero-argument callable to profile.
label: Human-readable label printed in the header.
top: Number of cProfile rows to print.
sort: cProfile sort key (default: cumulative time).
Returns:
``(result, elapsed_s)`` where *result* is the return value of *fn()*.
"""
import cProfile
import io
import pstats
pr = cProfile.Profile()
t0 = perf_counter()
pr.enable()
result = fn()
pr.disable()
elapsed = perf_counter() - t0
buf = io.StringIO()
ps = pstats.Stats(pr, stream=buf).sort_stats(sort)
ps.print_stats(top)
print(f"\n{'=' * 72}") # noqa: T201
print(f" {label}") # noqa: T201
print(f" wall time: {elapsed * 1000:.1f} ms") # noqa: T201
print(f"{'=' * 72}") # noqa: T201
print(buf.getvalue()) # noqa: T201
return result, elapsed
def measure_memory(fn: Callable[[], Any], *, label: str) -> tuple[Any, float, float]:
"""Run *fn()* under tracemalloc, print allocation report.
Args:
fn: Zero-argument callable to profile.
label: Human-readable label printed in the header.
Returns:
``(result, peak_kib, delta_kib)``.
"""
tracemalloc.start()
snapshot_before = tracemalloc.take_snapshot()
t0 = perf_counter()
result = fn()
elapsed = perf_counter() - t0
snapshot_after = tracemalloc.take_snapshot()
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
stats = snapshot_after.compare_to(snapshot_before, "lineno")
delta_kib = sum(s.size_diff for s in stats) / 1024
print(f"\n{'=' * 72}") # noqa: T201
print(f" [memory] {label}") # noqa: T201
print(f" wall time: {elapsed * 1000:.1f} ms") # noqa: T201
print(f" memory delta: {delta_kib:+.1f} KiB") # noqa: T201
print(f" peak traced: {peak / 1024:.1f} KiB") # noqa: T201
print(f"{'=' * 72}") # noqa: T201
print(" Top allocation sites (by size_diff):") # noqa: T201
for stat in stats[:20]:
if stat.size_diff != 0:
print( # noqa: T201
f" {stat.size_diff / 1024:+8.1f} KiB {stat.traceback.format()[0]}",
)
return result, peak / 1024, delta_kib
+25 -23
View File
@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
version = "2.20.13"
version = "2.20.15"
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
readme = "README.md"
requires-python = ">=3.11"
@@ -24,11 +24,10 @@ dependencies = [
"dateparser~=1.2",
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.10",
"django~=5.2.13",
"django-allauth[mfa,socialaccount]~=65.15.0",
"django-auditlog~=3.4.1",
"django-cachalot~=2.9.0",
"django-celery-results~=2.6.0",
"django-compression-middleware~=0.5.0",
"django-cors-headers~=4.9.0",
"django-extensions~=4.1",
@@ -37,30 +36,30 @@ dependencies = [
"django-multiselectfield~=1.0.1",
"django-rich~=2.2.0",
"django-soft-delete~=1.0.18",
"django-treenode>=0.23.2",
"django-treenode>=0.24",
"djangorestframework~=3.16",
"djangorestframework-guardian~=0.4.0",
"drf-spectacular~=0.28",
"drf-spectacular-sidecar~=2026.4.1",
"drf-spectacular-sidecar~=2026.4.14",
"drf-writable-nested~=0.7.1",
"faiss-cpu>=1.10",
"filelock~=3.25.2",
"filelock~=3.29.0",
"flower~=2.0.1",
"gotenberg-client~=0.14.0",
"httpx-oauth~=0.16",
"ijson>=3.2",
"imap-tools~=1.11.0",
"imap-tools~=1.12.1",
"jinja2~=3.1.5",
"langdetect~=1.0.9",
"llama-index-core>=0.14.12",
"llama-index-core>=0.14.21",
"llama-index-embeddings-huggingface>=0.6.1",
"llama-index-embeddings-openai>=0.5.1",
"llama-index-embeddings-openai-like>=0.2.2",
"llama-index-llms-ollama>=0.9.1",
"llama-index-llms-openai>=0.6.13",
"llama-index-llms-openai-like>=0.7.1",
"llama-index-vector-stores-faiss>=0.5.2",
"nltk~=3.9.1",
"ocrmypdf~=17.4.0",
"openai>=1.76",
"ocrmypdf~=17.4.2",
"openai>=2.32",
"pathvalidate~=3.3.1",
"pdf2image~=1.17.0",
"python-dateutil~=2.9.0",
@@ -68,13 +67,13 @@ dependencies = [
"python-gnupg~=0.5.4",
"python-ipware~=3.0.0",
"python-magic~=0.4.27",
"rapidfuzz~=3.14.0",
"rapidfuzz~=3.14.5",
"redis[hiredis]~=5.2.1",
"regex>=2025.9.18",
"regex>=2026.4.4",
"scikit-learn~=1.8.0",
"sentence-transformers>=4.1",
"sentence-transformers>=5.4.1",
"setproctitle~=1.3.4",
"tantivy>=0.25.1",
"tantivy~=0.26.0",
"tika-client~=0.11.0",
"torch~=2.11.0",
"watchfiles>=1.1.1",
@@ -102,18 +101,18 @@ dev = [
{ include-group = "testing" },
]
docs = [
"zensical>=0.0.21",
"zensical>=0.0.36",
]
lint = [
"prek~=0.3.0",
"ruff~=0.15.0",
"prek~=0.3.10",
"ruff~=0.15.12",
]
testing = [
"daphne",
"factory-boy~=3.3.1",
"faker~=40.12.0",
"faker~=40.15.0",
"imagehash",
"pytest~=9.0.0",
"pytest~=9.0.3",
"pytest-cov~=7.1.0",
"pytest-django~=4.12.0",
"pytest-env~=1.6.0",
@@ -144,7 +143,8 @@ typing = [
"types-python-dateutil",
"types-pytz",
"types-redis",
"types-setuptools",
"types-regex",
"types-setuptools"
]
[tool.uv]
@@ -179,6 +179,8 @@ respect-gitignore = true
fix = true
show-fixes = true
output-format = "grouped"
[tool.ruff.format]
line-ending = "lf"
[tool.ruff.lint]
# https://docs.astral.sh/ruff/rules/
extend-select = [
@@ -312,7 +314,7 @@ markers = [
"date_parsing: Tests which cover date parsing from content or filename",
"management: Tests which cover management commands/functionality",
"search: Tests for the Tantivy search backend",
"profiling: Performance profiling tests — print measurements, no assertions",
"api: Tests for REST API endpoints",
]
[tool.pytest_env]
-16
View File
@@ -1,16 +0,0 @@
9w
{@@N
Q@@@@H
G@@@@@@@\
SilN@@@@@@@
*Q *@@@@@@@@S /= = = = = = = = = = = = = = = = = =\
*@ B@@@@@@@@N || ||
N R$ A@@@@@@@@@@ || PAPERLESS-NGX ||
x@@ $U B@@@@@@@@@R || ||
N@@N^ @ N@@@@@@@@@* \= = = = = = = = = = = = = = = = = =/
|@@@u @ E@@@@@@@@l
Q@@@ \ Px@@@@@@P
1@@S` @@@o'
z$ ;
v
/
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,329 +0,0 @@
%PDF-1.6 %âãÏÓ
3 0 obj <</Metadata 7 0 R/OCProperties<</D<</ON[8 0 R 22 0 R]/Order 23 0 R/RBGroups[]>>/OCGs[8 0 R 22 0 R]>>/Pages 4 0 R/Type/Catalog>> endobj 7 0 obj <</Length 8109/Subtype/XML/Type/Metadata>>stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 7.1-c000 79.a8731b9, 2021/09/09-00:37:38 ">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"
xmlns:pdf="http://ns.adobe.com/pdf/1.3/"
xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/">
<xmp:CreateDate>2018-12-29T21:47:38Z</xmp:CreateDate>
<xmp:CreatorTool>Chromium</xmp:CreatorTool>
<xmp:ModifyDate>2022-02-26T20:11:14-08:00</xmp:ModifyDate>
<xmp:MetadataDate>2022-02-26T20:11:14-08:00</xmp:MetadataDate>
<xmp:Thumbnails>
<rdf:Alt>
<rdf:li rdf:parseType="Resource">
<xmpGImg:width>256</xmpGImg:width>
<xmpGImg:height>76</xmpGImg:height>
<xmpGImg:format>JPEG</xmpGImg:format>
<xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA&#xA;AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK&#xA;DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f&#xA;Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgATAEAAwER&#xA;AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA&#xA;AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB&#xA;UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE&#xA;1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ&#xA;qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy&#xA;obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp&#xA;0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo&#xA;+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYqlnmXzJ&#xA;pHlvR7jV9VmENpAP9k7n7MaD9pm7DBKQAssZSAFl5x+RvnjUfN2r+br+8+BWmtJLaCtVijZZUVB8&#xA;liFT3O+VYp8RLVhnxEvWsub3Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FWG+f/zU8seTLdlvJfrWqMKwaZCw9U1Gxc7iNfdvoByueQRa55BF8t+evzB8&#xA;w+c9T+t6pLxgjJFpYx1EMKn+Ud2PdjufltmJOZlzcOczI7vVP+cVYSbjzJNX4VSzSnuxmP8Axrl2&#xA;n6t2m6voLMlynYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FVk88FvBJPPIsMESl5ZZCFRVUVLMx2AA74q8A/Mv/nIqV2l0ryYeEYqkusuPiPY/V0Ybf67fQB1z&#xA;GyZugcXJn6B4TcXFxczyXFxK808rF5ZZGLuzHclmNSScxnGU8VfTH/OMOlNB5R1LUXFDfXnBPdII&#xA;wAf+CkYZl4Bs5enGz2TL3IdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs&#xA;VdirsVYj5X/MGw13zf5i8vRFRJosiJDv8UqgcJ2/55zfD92QjOyQ1xnZIZdk2x8v/np+a1zr2qTe&#xA;XNJmMeh2MhjuHQ0+tTIaMWI6xow+EdD9rwpiZclmhycPNks0OTyPKGh2Kr4YZZ5khhQySysEjjUV&#xA;ZmY0AA8ScVfbnkXy2nlryjpeiinqWkAE5HQzPV5SPnIxzYQjQp2MI0KT3JMnYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXw9oPm/WdE8zx+Y7OWl+szTSg14SCQ1kj&#xA;cd1etD/XNeJEG3XRkQbfUt9+Ytlqv5U6n5q0ZykqWcw9Ov7y3uuPHi3TdGYH3G/fMwzuNhzDkuNh&#xA;8f5guC7FXYq9q/5x5/LaXUNTTzdqUVNPsWP6NRx/e3A29QA/sxdj/N8jmRhhe7kYMdmy+ksynLdi&#xA;rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir4a836BP5f8AM2pa&#xA;NOpVrOd40r+1HWsbivZkIYZr5CjTrpRo0y3ytqd6dCuLK3J9DzFYXmn3cP8ANe6dCLi3kUdjJG6R&#xA;e5qcnE7e9nE7e953lTU7FXrv5W/kPquuzQ6r5kiew0RSHS1eqXFyOwpsY4z3Y7n9nryF+PCTuW/H&#xA;hJ3PJ9NWlpbWltFa2sSwW0CiOGGMBURFFAqgdAMywHMAVcVdirsVdirsVdirsVdirsVdirsVdirs&#xA;VdirsVdirsVdirsVdirsVdirsVdirsVdiryr86/yjfzbbprOjKq69aJwaI0UXMQ3Ccugdf2SevQ9&#xA;qU5cfFuObRlxcW45vL/ym0y40i+vNc8xwvZaP5Raa5uopk4yPezxCGKDi1DypuPeleuU4xW56NOI&#xA;VueiJ8n/APOP2q+Z7WLXbq6j0XStQLT2tmqNNOsLMSgoxRQCv2TyO29MMcJO6Y4Cd3snk78mvI3l&#xA;Z0uLa0N7qKUIvrykrqw7otAifNVr75fHEA5EcUYs4yxsdirsVdirsVdirsVdirsVdirsVdirsVdi&#xA;rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVeRedfy/wBa8z/mjBbyQyxeUJYra81aQlfSnntP&#xA;URUWhrUpIqUNNqnsMolAmXk0TgTLyeuIiIioihUUBVVRQADYAAZe3t4q7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq//2Q==</xmpGImg:image>
</rdf:li>
</rdf:Alt>
</xmp:Thumbnails>
<pdf:Producer>Skia/PDF m64</pdf:Producer>
<xmpTPg:NPages>1</xmpTPg:NPages>
<xmpTPg:HasVisibleTransparency>False</xmpTPg:HasVisibleTransparency>
<xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint>
<xmpTPg:MaxPageSize rdf:parseType="Resource">
<stDim:w>2409.000000</stDim:w>
<stDim:h>909.000000</stDim:h>
<stDim:unit>Pixels</stDim:unit>
</xmpTPg:MaxPageSize>
<xmpTPg:PlateNames>
<rdf:Seq>
<rdf:li>Cyan</rdf:li>
<rdf:li>Magenta</rdf:li>
<rdf:li>Yellow</rdf:li>
<rdf:li>Black</rdf:li>
</rdf:Seq>
</xmpTPg:PlateNames>
<xmpTPg:SwatchGroups>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<xmpG:groupName>Default Swatch Group</xmpG:groupName>
<xmpG:groupType>0</xmpG:groupType>
</rdf:li>
</rdf:Seq>
</xmpTPg:SwatchGroups>
<xmpMM:InstanceID>uuid:e5f59418-0be8-dd42-a564-bc1f41615750</xmpMM:InstanceID>
<xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass>
<xmpMM:DocumentID>uuid:c2483dfa-3a53-3149-80a7-6822614a9dee</xmpMM:DocumentID>
<dc:format>application/pdf</dc:format>
<illustrator:CreatorSubTool>Adobe Illustrator</illustrator:CreatorSubTool>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?> endstream endobj 4 0 obj <</Count 1/Kids[5 0 R]/Type/Pages>> endobj 5 0 obj <</ArtBox[152.941 154.947 2262.04 755.845]/BleedBox[0.0 0.0 2409.0 909.0]/Contents 24 0 R/CropBox[0.0 0.0 2409.0 909.0]/LastModified(D:20220226201114-08'00')/MediaBox[0 0 2409 909]/Parent 4 0 R/PieceInfo<</Illustrator 25 0 R>>/Resources<</ExtGState<</GS0 26 0 R>>/Properties<</MC0 22 0 R>>>>/Thumb 27 0 R/TrimBox[0.0 0.0 2409.0 909.0]/Type/Page>> endobj 24 0 obj <</Filter/FlateDecode/Length 3540>>stream
H‰ì—ÍŽ¹„ïýõU"™ü½Z|2 Ã?ÀÀ»{Øõûþ"ɪîI¼;ö° i²ø“ÌŒˆÌüð·Û‡¿~ ÛŸþüq»ý| ÛcK™ÿvýúå_·nÿ¾ÅMÿ~ùñöá/ÿÛÿ¹ýÌ0ð/n}ԣƴÙÈG¨£l/Ÿnú¢ÿc:j­ÛnGëeKv¤·=Æ#uÛ,?/·=v¾o9%—mOý¨ŒòÑjÛvf»ÕsÈâ’Ž1Òõ½¦£Ç¡Í¹¥moáH9?œÝ& ¿{ض÷xX—ak´rýÞÏokç~š½ŽÞïg¯»÷Óòbx€¿olßïÆëm¡ÝWÌ—ï§õË3WLÏíËüpYÜæŸ8‘«»butŒûtM´ÂÄØ^ñn9d+²®6;†ž3ÚÑ
Æ—vd³k,—q©õûŠÞÒ5¶£h>†8ßÕË8Jä’ÐqN«G Yf#Ëá5ÏD“sø’ÇÑkòM<»š<š¡=xÅru¯X~œGh)k†¹¾ÆºûsÝÂ%Íß";
þ’Ñ=\ŽXÚ†Ùâý-]&t‡¯eã±Yn[ÎÀµ·Ë[k¨Mwžß9¾Qw6öÍ`‹ÖªÇßÇ|m-›2çÏÆŠÕˆMÁ»‡wNütûáö÷öÅh툱~“ìzæ_á„ Ôô ,©_ ¬ãÅžh¸äèÊü“ê<ÐOÛ.ú­sŸé§‹ïô›vío ¦Ÿ^öH?=ü‘òÌ3ÿ›¯ò/á<Ÿn \Ü’€Ú‚^áL¹i!“P^À­F£`Â]°?Ç|Θ¯­‰wœ‹v|'8²˜']^|_ÌtƒïÌm\8îÔÝãù<»‡'mÈD%–ûŠ*q™uÕ‘vßÕGÑ_3‘4aèöëä}Ó­_˜»¨pnülb‘‰ î.H æiÚÉÄeû©…r¢öÿ]aDò¶Ç ¾¼%XêÚHŒrdE…ÕÁ*g§ÌÏ×[­„hl_ `F,B|Rä.¹\ȶ%Òf†tXÿú,tÑ-ÉÕÇ­ øk'² X;
u‹à]Ê{#ÔpsD™¬ë‰{!“Á"Nr
õ
T;ïˆÁt
ekìiÒdq>Ú8’¼}Ž_n+‡ø²øwPX´‘†&‚üà˜0ÖHÖ‰…%îÊäQ TVÝ¢“¾“qûð@^©¸àå4¡%¸¥Ì¡é]Ië=]3~å^a £ÁÝèçg’ÄJÆB5ÎÖ0;°¯‘\É‚).qf;ÆckÈŽŸév5^©ø±sˆIâàzŽk÷³ÑpQÇ8DZ%ËŠÁWx[ -6Ljz||š‚=¦ŠG^ÇúØ-J…†,%y›Ðò´kÌß<Jüš4™ªÂÃ@ËrÐÖü¿óþŠ›y`’·«ÞžÐXåyðnÜ.OÅîâpå Ž5œXíš(诈q2Ø‚ÀSÚÊ&wÉ¥1{¼ZÂQ㈭yØž~ßϘZ”ÛU¢e…_B~1µ4¤ã( ·á`ÐÎýØŽátóÒ“7FÈcªkA Xû3ޝçÈ#«d³Æà3@Ièr}V
F “Ôa½9xIéÉëØ¡ZŠaGx°¤9”v`R#þýšÀR³Š‹z„d79LKžùTôt¥+\7†Ò•
™¢ì VApﮡàcÄ/Ma}I*9ÂãÏ=\EH¬¼³Éæ ]¶k'©»/z‡ãCÂÀèòm% peJ—”Tpž¢ö›•´<)iùº’Bõ ¤ý%ý JjßIIíû)i{£¤í;)©ýO%Ío•´þ_Jê”äØìB*–KH‘ão éP]$™É¨Sù¨Tˆ)ÔUgRBÜÎ.“I¿_µj¢À—"¿$d'|í>Zò¥Êüœa½÷‚T1ÝËGpe>]­¬j.Ì“Œf¥¼ãÓüÏ2þkb]QÁ¾[¡[쎥1Èdî^®-…jTá.É Žà⤂}  qV®Ñå<ú–¾œÛ9\Ô“©lë¨Ò9dÕ`7Ëׄô¡)) #b5O‰¨B¬i^Ì+šËtW­³ÔЄÜ(E*^5|„Àj¾å}^ÖâÃS¥í¢Ê¿z³dòQi*«Eõ>hDƒ/:~Œj.mf>1µN[x»/T_ œéÏðD”]¡ÏþU.Ò'zÕT« ªÓ•úD,­WtaŸv¶kBš­žo+¥ç5ì$tóôyNdÏÌî—t5¡ë,ìO®DÖ¼óÌ’8äæ­LîzÒÌgÖšš²§Áî4xy ìêw¡fÞKé“VÄÙyrsqè}øÆ®/8ªw*^"H[JùY+Ám’Èœc¸g³=[3@TË!#aâÍC[’¬B˜½œ÷‰KE–R™?ý¾F¸:‚frÂOÎ*ϼté±ÍÖ¶™·¶
©‡ƒd2sôpƒý«7óÚ(†I÷½Þ8«9¶)) ¦5Ï„8_³ÔBC¢0;Œb,˜‘"ir}ØfŽ~–´IÙˆã7%­ý!i_”´ü,iù­¤åß«¤…º$­M
_#Q“ ~­¨¥w5%ûwÓ´þžšÖÞ_ÓâïUÓâ¯Ô´þŽ’Ö…SÒ«'I£‚³Ä e5-I•vOŽß£0ý¦çJA %/HÞ“J˜NÙ£fÞÝÐáuN^;¢4•«¯ŽÙOY<‡ÞD9~ð dU¸·žüPþ[Xç™ÙUNI¹†IÎŸÏ ’éü¨›m•°o'L"RÏ Ž ·¨N‹Ñ§~N’¡;n‚ÛÃÅÁôWXBÓ‘¶Æ™÷Ñ‹ªvo±‹ÊøÀ’ïÿôú„ÉYŒŸ XÖî#ìÕI²&x™š"EZò>ŸÁ£A˜Y[Þ I59¸$ÕiŸº0uo¸RÖðǾ’þ(õI:DÐ{Le<‘dÇõ¼м!x”–ÝA=ÎàDg5.¯I®Hy?×ðA.Ò ¸(fŒ©å³‚ªæÚàrJ£·²dCÅL L¿²¢†qJgbIcWœÝ%d Éd³zªì:ü;ö¶8¶sæÒ¸VÏÃbS¢—ܰèg‡êÈ?Ý‘],g'&Sò˜©>Î ]Fw°$ݵLßWÊÖK³ûÒgÚ¤ÈX`ÓÁ«ID"‹ÃEJ'‘õÓquô[Ê+¶PÛIΙäB³’%ìkoýä¢ÉÀêã¿8Ø<ch¦‹Q}ñ Í›GáARýÀÛÁ „–WªEì†ÊT3‹›ˆÓhó”c2uõL_dd÷½ÍqQ‰A,Zð¤©|žìšyõ¸ôu[Ô-ØÕ”ˆ¹ÍñÃRóŽ”ÏÐèíêlcñØ%2ìÊžå…mó±E÷ºÐôV­H3Êžé,g~R­†#¼ZÙ>OîÅ!:;ÈP„n I æ&øzszV7¡ „»¢3¦”ÈXŽªÅY>F=:ûê˜ecô‚ ¬ÔV^Òilc=Þ$^É$¯K’l•Rjþ’dI€ÇäNÉôòL‰JY˜š¤TYÀÅÝqLU¥Žaæzê¢HŽ$ô­ú(D/T›Û¯± î¿d—=rÛ0F{ŸÂÐ  YgRú*â"irÿ"ï-@HŠ{R$±ûí÷ã„6§©á¯g3[ý­~¢»i<o¯ÀƒÎfû5Ë266¢M‘³)t+
b÷ó¸ÚbJRT8kÝÊÛÚ®éÝb£Ø¦©«vAaÀùuôÞiVË&1Vµé9cDåJÞ {µ5pÁÇk’‡Uâ:/~¬fŠàh½¸k6y]gÓó¯™eM†~«=<Ç0>ñcí̵{4 UoÞüýßqz?5[ø§.í ²užIà&)ýTù¹ êTçR¹8µ/oð{í4†aý?ÁÏå8FŒRÏù>0%Ý'ó2pÿÞJ{€3J/nV†›•˜æšr7P?H™öA«ßÝNœº»Pþ¹ª‡Ïîm´€Ž"Fàr‹§ÇOØdš«e»겆Aš…¾Öf=*V;J<}ç]˘ä8x3ÊXá¼E®œÔÀÊ:G8û‘hÐñ`×¼©&gT<Õ‘ŸÖV7öÙ…}pËÛµH-Z3?þy~ØÖÆ51¨EjWeœ÷œìY63Ó»YSn£Ö³ä·i3ï¼?Bþ›:[@Å9L7¡u ´ÖÝ
̵^CA†I|T“0Êçk'Çœ&ã*Tü›)þß_ϳËsæÒójoNÙ℃Íá
ú.¸(‚ç¤êåy(Å@™7mÚ̘ʶPuÚÚªÚ­œ”P°¶åJçUÒÙÃgéˆ^À’}æÉ–t9jËjHÉ5t]î!:>H|H>@‰êDë8p€s:¿â‰ÜlSÀ¬Ì  ÷¨/^6“p=ëR [B°Ú ëIžfŽÝ-Љµ+‡®ªÆ\q²9EÂÊ$NÅ“ƒ:=á
aǸÇy§Ñ±œÝVö$8 (fóÉ~†/ç{²'<RdN87¸.O9ƒÛºû¶ÅWÜvås˜ÙvÁÖüS ²^ÚxŸÔùóëÇ'ÿþ 0B¸| endstream endobj 27 0 obj <</BitsPerComponent 8/ColorSpace 28 0 R/Filter[/ASCII85Decode/FlateDecode]/Height 39/Length 181/Width 105>>stream
8;Z]!\Ij?G$j8@t/k5dUU3<am*`9`037'CAm[R>[,!)9)1.3+''h?`o>gmB[ET8ck
@lE>-DRC'5>'BJ23HlQilI&Ga&7If\2VcDkpR`P&Ag+(rGsf,$]V4,Oi!oYPO?6G/
Ye+**Y]%)+Lu]7C/1+obTM<jR!k7bhp#"6_FO&2i!:ndgO8~> endstream endobj 28 0 obj [/Indexed/DeviceRGB 255 29 0 R] endobj 29 0 obj <</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream
8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0
b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup`
E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn
6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1
VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH<
PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O(
l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 22 0 obj <</Intent 30 0 R/Name(Layer 1)/Type/OCG/Usage 31 0 R>> endobj 30 0 obj [/View/Design] endobj 31 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 26.0)/Subtype/Artwork>>>> endobj 26 0 obj <</AIS false/BM/Normal/CA 1.0/OP false/OPM 1/SA true/SMask/None/Type/ExtGState/ca 1.0/op false>> endobj 25 0 obj <</LastModified(D:20220226201114-08'00')/Private 32 0 R>> endobj 32 0 obj <</AIMetaData 33 0 R/AIPDFPrivateData1 34 0 R/ContainerVersion 12/CreatorVersion 26/RoundtripStreamType 2/RoundtripVersion 26>> endobj 33 0 obj <</Length 1444>>stream
%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 24.0 %%AI8_CreatorVersion: 26.0.3 %%For: (Michael Shamoon) () %%Title: (White logo - no background.pdf) %%CreationDate: 2/26/22 8:11 PM %%Canvassize: 16383 %%BoundingBox: 152 154 2263 756 %%HiResBoundingBox: 152.941359391029 154.946950299891 2262.04187549133 755.845102922764 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 14.0 %AI12_BuildNumber: 778 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 0 0 2409 909 %AI3_TemplateBox: 1203.5 454.5 1203.5 454.5 %AI3_TileBox: 826.5 166.5 1560.5 742.5 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI24_LargeCanvasScale: 1 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:24 4 %AI10_OpenToVie: -2651 3020 0.25059563884769 0 7787.44597860344 8164.54751330906 2548 1389 18 0 0 6 45 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -2651 3020 0.25059563884769 2548 1389 18 0 0 6 45 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %AI17_Begin_Content_if_version_gt:24 4 %AI17_Alternate_Content %AI17_End_Versioned_Content %%PageOrigin:704 -46 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 34 0 obj <</Length 33953>>stream
%AI24_ZStandard_Data(µ/ýX<ž¿EZ-D¤™æ£Ù¥¶[ã*DÞJï§ënYÉwzOê°@Yþ= )œàr²
}€h>@>@$F"ôãr.)™ Kˆj…Ãq
E`ð è‡YXZª>Tü!ò¹T Õ¸A×À”ŸQìt€h4îRØdBFs2Êš´µdˆ:-¢Hæd”
â€àd”ÙÅŠKÑ™‡c4(Vš¦X‰b'S6 
“y8p+cÁa3á.±€_&ÔAõ!êl&\âeÑñáÀºPÀêpM´ ,0O´ˆbǵ@U»Œˆ:‰•Pm2ŸƒMK¢€Dv´X$+Q@ƒI±H"H¦”( Š £°:­Ïš€@ìHE;ٕΤ‚‚¡€G*ŠˆŒÃf4À±ŒÒÙŒv¤‚À*X—N«³Ù„É,¤€¸¤¤Èô¡ð0!i™],  ±A"˜ˆ"+2‰b'ƒÒpâ`"KDÂŽ
H*2‰@bƒ´A—ÖÀ à>)¹É® D‚Ë¢d¦€x!Ù)“`Ñp°/&‘¥³ ±TuY”•f µHÊñhiùÀRAl²ËDÔÙp@#j@!âUÂ5€…¥eD‰AK†¨³É®—¡(„t6ê°‰ø‰0Àˆ:­„ÎFÃ@s"Rél"² %Z%„¨\2xT2 )‰]«ÑÆAe±z„ c“ñ’‘Àép° m…“]‚BlF*‹„,¢ÓÙhˆX$š] ¢Ø!e'£ÌðÅÎAFgb© ²kJÉ“e>¤•AFg£±±9HP}è0eVˆ ê%ÔÙì«XVo (|6Ù•¡©i†ìZHX Ð0aàTÁHDà"j!mZÑ* ::T¡Ü–Ð+¤·-»F^ž ÁÛha°qzò²d D + =L```Š
¢Vv,NÁã˜B !­ÂȣİÉ.…Œ†ƒØ24 +bÀƒ ”†‡ÀB@J;ÛöÒÀɃ´à@­<˜ ³É.æhN®8¼³ÙPp >Ÿ’ƒø8" Ž:BŸƒç!SL§a£NüQ:%hÄ‡à‚“]Á­j= Xx(¾¤œ»Iˆ“+Íã¨ìCàÀ”ÆFBc<ˆ88 ¬xD'Ü'%³Ë¥Ñ±9Cf¡¡!"G¾ )XŒGv"» 4 O„ˆ„Æ %UÊ@:½™3ht6YI'ÄÒ’]ûÁ2Ze´
|IvÒ°]J6`úHqL x, hJ`8m<¢ËA
ËCÀÈ`G(¥  ”ŠuHm*J"ë)!­Àì:Ù@X8² ‘ñ YXZv˜(|ïµÙgc€!x ‚®d×Kv2Ê•ÕYP•xŒü™´ì8ÑTÙ5‚NcÁb¡€³0*ͳm™B!m[F@vQ|
øä¢W +®@¡o`İi}$GsÓñ`mÙ•€ ¡!«!@ÁlZ *Ö¶5˜²ÃdB \
˜¼lÙ•ÀÑœô, œdÅÈ(Ó9XR…ˆf5Œbò&/›Ë‚)#H0Ù5 p'ZNé‚Ùpd[JAÂ$³‹aÄhY¸P@±HŽ„LR »$X0HHH4JL":
‰Ñ™|¼‰ÕGƒ€‡V)"€r¢±Â¡’:øÈhD\h#Š G*È®d86.¶ .ÙI,T2±<Á#‰¨ÈÕ«´ÑTá+R€Hˆe$b¤¡/FÅq‚Vv…|F;ð¤hàã$A”¤þX¤X2‚Ç⸠I(³ c[6
^%4p1”!!-Ò 4+ž]Ò9`%T¬€tÉg°6 ¬MƒpÀ‘Âydq\v]t(”pTLn² Ù@ù ÙH² %úŽÁƒƒŒNÁɦjS´"¨áhNNlidó`1l[vYtÅÄA…Š@”'ŒlaÈ. ˆŽPŽ„tLa€O€C†rd£%SàN^¶ìZ©
XX<Yì‘€¬-»V´ñP>-’ídca =-»VÞFCJ L‰°°AV†£9iÒl ,†mcU œ„¼¬Z°–ÕÙe€u6/°‚] l+o£á‚¥‚xNž@½m¬Š 'V%m Ù?Œô䃢- ΀ÍSÒ­E,<¤•H7 Tüa†¢a“]ïbƒ´ Yu "P<Z `;(w6+PˆÉ£ÀqU ’…ì
e‹óÀñ=X´\„pJ,`”ÞÈcñbqñ‚qضf2ÊÎ †CŸ&"RrB"ãaBÂ…åAQ´¬€0­çUœŒ‡÷¼ @Œô‚]IE,X$]¬Žòè(Áð`FD)Ÿ-$5a&wAC@c|2NFÙ2áBiãJpx x޽8 °²qe >òŒ<$r‰@AùKh™)€±á ©Y2Eàqp2NNüÐâ!Ò%Œdp"-=¼Ä-Ī€Y]šÆSïAPÒæ„@%Aòá0hÑ@8¨ + P´È Y Ââ<lh"D'qä,Žcygã0q2JSJÅ3á™Ë‚Ê&#Y)œˆ ø`¸ðdב’XŒŠ‡ÅËzH+›–Âce2;!™"5lŒXD1Ñêp`Ðk±á$·V6 '9(?ñÎ&»0ͤ*i ‡[À®V´U CêTaJ‘°VJ;™H›èÓ
22ÃiEúDè§”S+¥™$L)ÕGâÓ"¡¨¶’'UR'Õ©EÚh¥T¡–ÆByáxyj±4ОP¬T)%‘Á²©ÐÚ<-ª-ô°l$MkUJeÑZj[)©R*HhÒ&z¤ M©N
õRO
…:R(-/+Z‹´i)Mtú¨P<š
©E"hÒ&ÚÕP§ŽçKʨX­H[Çê´•XÈ¥ J±¤^0Ö)E…i¬ EJR‹„­”^XXé„Z­HXëE5 …‘N˜V"¡V˜ª…™4-…½¨ØTX KH**©SëdÒ°”
KP+ìE5€Æby`.ÊH0 ÓP§,EÂTR'H,,¥­ca˜¶Ò´„i,m¥#9À°”
3i,„Jh*¤ (£UÂíô¢ h±H!ˆM…Ê–ÓÖ±qªØH¦©Åae¬Jx´µy>ÊbÙH*ÚF‡I”¡*áùd<EU@ÓP%Hc½h(”ª”:m(ͤj1­VÕb¡Z¬Õ
Õbm«•Ém+Õß­Â{Qß'Bƒm°2´”ZJºÒR¹­•:¹–Ò¾˜X+•[ ´R¥R$Ò©•"¹U´Öi«–[ ”zIq¬Í´­ Z±6ŠªÀê„*¹UZZ'É™¶’6ÕÊê„Jq¦­•ÒN,%
Åm¨ÒŠÊ¦r¦­”R9±4“CQ@QµR§MKP)šÉ±´¨¬ËÖ©…¢*pJ ÅÚT)©“¦BZ¥IÓP( ű´¨¤T­‹¥m«U‰¤¢¥kEZµ¨JŠr¬Õ¦™H+,ÇZÀm«
…”Ò@Z¹"¡6MUª´"€ÓP¤jUb9ÓVêèªVІ‚ê4RËÉm«Õú«ZT)Û
er­T
J@C9ÖŠQ^X®•JÙ\JŠÊm«Õ¦ pÚ@Ø(˜IKR4Pf#¥N(º±[À±V,––jP)-åX«MÅ­ ZqÛjUÚK‹Êªµi¤T%·
ÀŠ”2FP­ph)›–ÒpÊÀ°\ OéÂT)) Õ"¡PV'JcmšÚ–A¡H(”ª¤’"mš
iåPTK•:aª_jGr`¨Jx°U Ï µ:¡>Ê"PìÔJyÆ‘q82"0Õo±H3i¦MUÒ68h¬–Æby4
©µ©Vˆà´J'PcPT´ €†Zµ¨JØê„*¡H(˜
[j+uÚV›&€jµÂÒ,°(áÑÖ˪õ‚ÁX+D‚µ¼œLš k;…¾þ¯Tñµ©Åqb>R´ŠÝÛ­Çרs×ý‘R•4Žh¦”
¥}ž2Ä০»å†*©Ó
ë”"}hœZ$”¶‘®€åJ•i*¤©m-ê%$áaÉŒR*œ6ÕÈ7E«à@¡´w¤hºoS´ŠÕ…Ò>ÚÒX))¤–;톘»
K)Ru©¤NÐN°SZ+.¡o´R´ŠÖ)åÑJÑ*Fè%Ò
#]%åÑJ¤PkEå´á­˜ŠzCl**db:m¢¿Ì¢ÚZ,mSÙ´‹´©Z/LÂÃÂ"µ¨V¬ ¥*µ;­LBL&Òê1 36l€RJÛH+VŠ´µ°(áQÁZ/*–’*Ųp{tB‘°S+EzÚ
€ÖÂ´Ô ÕzQa›–‚%JJujP/-'¶i+Ò

ÓZ$”ŠuÂrÃÛT(U‰„Eg÷'€“„§uBPH­h˜&À¦±´-S•ðxHÚæY©¶RX­­¢EÕj©P2õ©gª‹©»Ò2ðÃÝ·®0B—ùZ+*'me€´Òö¢âÐJ'” …¥¥T)“ÆRR¡ô„¢ql$X`m$X`-C‹ªÅz)™°Rª%¤ ¤¡V-$Ö‚ ,­õ‚rÚZ-RJÛL¨ÓJ‰ÕÒ84
é´‘PRd"µ´–Õ­ø‚" ÒLª”‚fR¥P-0d4“ê¢Â„ÃÀ`ÒZ% ¥q°$2J¥‚Ixxp0 J¢h)Ú“L/ª‚F¦Vh…«€!JLQ˜Ùðdd0eÙHP+Ö ”ÁZ) Îó?a|3b©µ.2˜Jê´µ84“ªtJyp0¤BimžŒIv%`šM©N­K•‘ÁL²kCÕÒ6Z©“*EÒ68X@Ú6Ï)¥:µJ§Œc’]¤ “ÞÞëT‘q•2®K (Z«¥¡H¦T–¢8ØÅÀ¹L ˜bzm€`y™°$!€Y`©
`‰M`y ‚ååÁÓÇêõ½5£K,>зߋ »ŸëoGÿ\bþbÇþ׺û]^X^$½0°ÔP«–çJÀ´ÒÓ´P/¤ D s-`)¨Rœ‹¥¡hq´6OÌLÅ2W1Í\šÌeJE8LÃY)½¨:0ŽfÓL¶i*¤ÉSPpL£­H+˜)õ¢Zµ¨>ZëôÂmš)uJYi2@ZZŠ„Êh¥N¤Š”"¡4N,SJCA`+,”SÅJ©EǦjL˜§VI[±6­tZY6•
Eú´6Okó¬´H)ŒTJ¥ê@©TØ
ëeƒÀ6­Õ"•´k U:µ6RF ë„R¡´ À±R¡4R-*Ee0 ¦m$TH³:\Ó¨¤N%ÒÖe0M«¤@ÁÀLšN›ªUÒ>2—*%%±zQm¢ÔIå•a/.•Ô©•:m)Õ…Éꥥq¨¨Pª-#Ë.V+¥µ´¨>¦TD].Óâc²)­Õ"¡BÛ
]ßǸ17ª&tÛ76‘É·Öù ©“ÊiÃÐTHÙ@;}¸I31¥" µH©Mk¡<&¯Þ؃ŠEÈŠÁöÅ^ÆdVÿv×O!ÖÍùàGïvÿ±Äâû†ŸGO_—Xn2¦þV[ÐNªŽ”¶Â0I•ðdWc½ -LRN&­”ÖJ©>&©“ŠaòÊÚ´H-/°õ‚R±´–¦²‘:”„
µ6‰£4“Ê"Jx²K± E*qh*©Â¤W“][`µN)MC½ldvs´6R]` ÐN¹ËçO1wo!ê䮽1wœ«µH¨U‹Ö3b1{Y,©e¥Ú4$)Õ餢mÈ$¶!ÚȲ+±–JêÔjiš
©EÂä%<Ù…-h*$–‡ Kx² þc ›†ÒòraB^ LV%<еH-*¤¡N!—ðdK‰„‚À B¡´ŒÔJi*$ ,Eàƒ-°6Jõa±´’jËh¥´–—’
”eW¿ß›ê ûcÌÛj§Û·‹i„1nz|]»bqS#GgíèXÚÆi”e×óÇøíb튭+¶„JêdJ½¨ ²ìbÈO±M'$@ŠôɲË"S]€Ø.ç-g±­E¥T'MŠiåL©Õ¦ÂR"¡`ª¦€©TT(LkV Õ:udÙ¥É`š”JÕBZáÀPi¥t²È` ›*”e»to±…”êÄÚ´(áɲ+]/¦ÙÈlªik±^P( [ØT-’¶}²ìÂÒ±‚†¢2y820YÙ•±¡4"2* ’…QDJVd ¤<ž“Š&#%dìiPYp4^ÖÃÒá”al8³À)ÉdDØ øD€€~ (Zf—·´à‡ED(¤Ð'äã$¤Ñ$ÞÙ´Š…+‰w6™!M’xg“v$Þ1(øH†‡MLˆÈ8PûD¼ '³«€t²,R²'+""%ïl^ CÄöK±áÞÙ¨èïl4$(Mà ðΆóÑí€Ò;€¶;¸$„“Ù¥ÞÙd†I$âTðÂáQ"$"­ £å B#ÍBdŒ¨"¥”áÉ.‡‚Ø`xpÐl‡w6
­°™Œ’=4JªÞÙ@ ‡’r4¼³Ñ…’f„E”4¼³qåÂHó¡`¤ÎÔ*"Í®„ ”Œ „HU>+…ö% Ù¥PÚTlH­¨¡X9HÐä„|#±+‰¼gôÀ9' \êÄ$eQÒÀɃgÙ(
æ<# (Q'U!ÂPÂ"2XbˆÁÄâ< ˜>/»@6D$ŸˆD ‰€^>ÙëŒ "LšÝ8h8y6T²÷Ð9܆*P6º-¨,8Ùo”ôeÃâäIÈtlx41a€é=-*%Œñ¶Á’›Ãˆ[Š*°<h<¨´ÓÊ  2$<n Võp¡"ºàh\<$ Ëê!»T¤¤K˃{HyOL¨4m ©L)†+ã`¯¼:ð`<+AQUÃJÅé§B”(,U‰8©z‰Ð¯XV[œ`8ôæAä*Dª¹È®K,àT ˜qT”Â;…ME0•Dhf¡ãB@ 6ÓMJ8'ä" Qì¸sRa”ÐàœÜP-'…¥eÆ„¤%)b€§{Ta…E²FÊ"CpÂ10m0x˜3.=ïl|#šÈg™0„æƒñ˜Yœ„,>ž¡£A}´<»º@‹“. [2)©ÅÉ̃Š4KZ%{-,™‹ÅÉš0¦dQØÄ‚¢%,I€„…“
,mb
'U+;[‚ÂI%€ÂIDŠ&Ç…Ô¤”f·CAB8iuh¡ìÁ¨'4ŸT<8©ád‹ÛœŒ’@Ag³éðΆY8,8L ƒƒà‘ÁÉF'7Œ’Še5Di8q ´‘EepÒdÀ
Ov=dpòãáÀº!âäKÉjF@ !ÁÀ”NBTò•4”(†G…¡€9…OgÓ] ú‘pNkÁ#¥+2 —ƒV@K’–—É‚NvTTtÅ¡Ê01 mº"%FÞÁ±HÀT¬\0 &“L#È`bf!%]:­Õ‹ J+…‚²­¸h("((+#¬% x ÙBDR!šKÈJˆ2bÒÙ¬j$¤b1Ê–-#¤£ÊxaÉ XÉè¸|
XZ†HɆ’Qb’ÑÙ”Œ”„d¤>20(ÑJvµ´Šhd$„dÌÈÈ‚`5ó²/B ¶Ž
â ¢ð…ÒË–]"*[âJL". ðFa@Ãा, Ƴ*Ã;ɈBb ˆ˜ØÚõIGN D\&·L‹“. Mr\g$B7£„ÎF¡3¡{‘ÐÙhš“#ƒÑ‹†€ˆ\y +b€!`y°V\ˆ‡ƒ€Í¦ºèŒãZà4lÛGÛlÖ…ÄɄήìZølMv1H 0ô¶aLBÙURÀ£Ã3‚°á°ðÙ4
±‚æôɸH(OÅbò<{@â„f;(Ð( ²+¡À‰‡´¼Æ‹Â¦¢-%“õ°D2ž àppD Øxéð‡–ìÊ®ìú`áäÁ &“°mÌÄ"cb¦Ã Gh0Yò‘€Y¸ Á„œ+m0, ¦â€£€áˆQÀ,sÚ „+m0ÙõàJL¹Ð`^‹„"¤íJëXx>X¬;„P6ª
•lptEB”G’’ñ°d(2sàò±Ê®ìÊ®ì:€xÈ®UÈÆÁÇà Ç%õÀqBŠÈ®ìÊ.6™À” C*`46V. ˜Ì.Jò\ P%!ÕTKå
WtÕ€²Q‰– Gçmxt)è‹ b?
ŸìÚh˜ ñÛ–]Ù•±à°m ‚Î(»v@vµxFíZÙ•]$­è`¡%ÂÀB;<Rë@ E?% Íãá Îv‘’Xˆ‚L³R‰H³‹€ ³4ˆD PÇD•±ÞFR,ŸÏŠcðÒÙ DL†"»NZÄÂÃFÅI¨eBÔ1è08lbvm°L0<(IË$4@€s ]+
Z]ÙuÁ: 
±N- Çãò §ŒQÄHɧÓ]& /Œ¸D,<|HTYv‰öck‘ÐQyaL|²Ë´QC'ˆ¡‚ÖÆyÀÈ ½€
JPPŽB²E0¬:R.NÞÈ3`…ôJU‚gÂ7 Ð…FÉó«ÏEÄ€ˆMñ¡"<n ¼Œˆ³Kãå`E%0‹édjeÁa±TW,#šÅ `É‘Sf9&!NF ˜xt2
fåm4¬¼†ÍÊ€I$ÂH#—_Á¼pxX¶¨1d×6à£Ã³"rÚD°'ÃÑ qĤtBŠ@}x`eÒØ4dhiÂlÙFªƒ‡Í tú<¸„F"T²iÈpÀ(6eá”TCÊg;Ùp “•“ÒfWsŒDè–ÊV8..ä àa óÂá™x$@™ žì:q…H20 R ‘† t@am°4R@ êðdW… Á(Á$´‚ñN"0-“DÅǃöÁJ÷ hÊâ`X:L*@Ló mæÂ•6šŒÈ3a2IÀ„µÁ<dD6˜‹Žf7@ #šÅ¨Ú€'N #÷ŒN¢€ƒÉf׿ …‘/@ AÃ;Þpivé…w6Ri(5€‚(± '5'=#Õ \xgƒ¡¢! Ÿ „(»”Â;
Ϭè˜<,'ŸÑÇw6
õHPŠMC‡DóNn2JSfÕ¡¸øpà‰Þ,$œ\ äÄIpŸ”$Á<ì*¤³ÙŒ6.)š‘w6­€v0j™xgã2-Nf:CË-Ä;ÒŠÓá IÀŒ^6˜‰Ž&#r¡¡@0iÉÁ
qvq¼† „çá=.4»‘
@:©XpX ½ 8¡(çäY-NF–R„Av2àŒ†¨³qïl„ÂÓ :XY
5jyˆe•'³k31¡ÎãAPGp2J–€Š5ÐÅÎAƒ”Ê‚¤ ò:Í(äáÀ(.⠴؈P«8xˆ@,Ê’X>$Íx AU4@RçÁÂITv2JWAᤄ€ 1eV@,GÃÁ*| 44¡ '£t(œÌ®g`JRBÊ wxg³a*N %M ˆËê)O‡gEñ+ŸÏg¡Áã¥g#„“$€(ŒÐ[ù¤hJ4ÈFƒ~t#ÂI+ÐlKu80H'Y&É*
ds#ƒ“®"ƒ“/
œLpn>±E%ÁÉ(×CÄÉìjµÄÞ¸ACÄIN…ˆ“›GÃÁ–(„D%›‘†àd”šQveÅCÉ u*6áAÀBI„",,d2¦Ì.Lw8©Òé±úpà‘(<#ˆ Ç9÷Iyg“0 )#ïlXXZf䜌’5â‚“Q&˜€@lH[áxHÃÁRИ„FZ
H*!1Ê„^²ЦdÀ
«BÕâ­,ºÒ3¢‘RÀtP' ¦Ä€£€É`¥³ÁL 2 /!REZˆ°<œ4¤¼ÇåKëa­@,™]--ÒÒá!Å‚£áðPR‘Цä†1@RÉiÃ(H„X¡ÍH„0¨x…ˆmt°âÏ vƒÇÉA;yÙ@©Ì”-.ŒÅ&n>J"šìbX)ÈlCq1sñRKÇ
G+J;¨àH±dÕGÁÂ+ àl„<˜ ˆÍJ©ÁÓªð„D@-VCO*TCÃ@³ÙµzH±à€hØ0Ë„?`H¤ G°^¸  –”*Ã;“VYGGȃIY¼`€@
 `Ä–ÈJ(‚Ì' d,+"ŽŒ Å 2@2ŒŠÉÇCJ¼aŒL'…ƒÖn[k£9™&/ÛGJ_6ÕkãDlÛ¦‘±8Éì:á“lñ†I¦€””$Æ
Ij<D2ó¡0 _J>ÕÊLYayäÇI¢##[p䊆ÂH•”–Š4¹¨H…¨!ÊŠ“*” ŠP.(ˆHNˆˆT±œ:6Ù²Á `]: O‡Y`<z€1ØŒd˜”ѱáÕ3! Åy8mt:N8*<!ëA°øð£°àdB>­T³k´y,<ŒÄò°±% OJ&ºx'UîÄ"[5t^V^V<D8¼÷IQpŸ”ä|4¸ÌB
ˆd3É̈Bv=ˆd\F\F€  L>š’4'+ý|h$âó9HÉUÈ*D"ê|€ dçƒÀÂÊ. KK­À˜òó10åç€5&$M“]   îd”Ÿ– QKÆ£¸4ópàÌÂÆ‚Á&[<Ñâ 8»&Z&2:aà
˜B&œDÉJЬHHHh2¬…‡–&êlF£‘
«ÓTv¤rR²'«%{²/&i3 \Ah€Ä2ÈèsÐ! -ÕÂ&?Ìi@xÙ
˜ ³IÈh8Ø‚ yDDäÅÜA Ñ+™œ€.:#x(BéH¡8d4+¾ük=j0TŠÄˆ&Ñl|B†R@4Bà³ÐÁŒÖ†Ãh s°¦ÐH(»2DÐÛœ¨:86 Ž 4`ñQPGÃÈ["¨‡ jp`8<,!)!•‘–/!•'Ãíg0XQŽÈ€dX#
2 )!Ÿ³Pôm&›ÉÆeD´¬0 )
.#@ Ø éÇeD4"úøø0 ‰8HɃ”ƒ”¼BDŸÏÅáAÐeaiÊÂÒ’…¥¥æd”š’F±¢È<C±¢Ðh,8l4(²÷p`
ŠçeB'Z&Ø%¼LèDK¦
˜'Z4™…ή‰„— uh5œŒr¤+RR¢€ ÄŽT˜3Ê\@ VA2NÉÀ8´^:-î“"2°d6Lˆ‰PÐH„®B"J6 (Á(„D”@l": ÌD`A@ ™í%Ô’9ñŒ¬8HD‰Kªå¥DÓbùhF0j
$wPá0V>£4$¤K¶e£à"ã6œ¼l!šÎ.-.ÜÐVXÐT2™PŽ‹-`E ˆ„4Á[ •°Qp'/›Á€€¸°)k[y±‚Ýg”€™p¥ æ¡ÀJ¦£ÀJ&$NDÄ L {0% &»TXëÞÉØШ€„8Õ9Èxmt.8$%µð|,<T0H¸<h `Xò €¢aó«²øpÜ'%Y(¢MHH Å}RÒi@6Ï,tfa3 /%  ëš2a@S6š’«Uˆ¨ó2¡ŸŒ …1¡Îå åD³Q}ˆTªìò1ø¼TxôÃ
yôÃÂÒ<:R¢Î†äåád”Ÿ4!iiBÒòÅ$»0 /.±€_&Ôa¢eb]b¿L¨…:L´L P¨ÃË*0kB\³Â"ÙÉj 2-’Í8HˆÈ8H8ÉŽ²‹Õiu6ÙÕrhiGBg£;RauZÛ‹R©†ƒ-ˆ,"Ó„C«³±T* IÄ‚,+2m²+"…^@HQgâUBÈ*!$¢4±J‰PÉ®ˆ’ˆ„Õg$"•2fBZ 2:™ ŒL)3“— Ó€1 `˜^.Ô#A6“‘D(#ÀãŒ`²‹`B…µ-¤h€](lcD*!!½m+„+~ábØxÐ ÁضäM$JLjJBƃ¥BIh`DÓ,(™ONde0XT Mv¹ …æÃÒÙ`2L& ˜ ¦Ãd’€á(ùHÀ Ø%`”0, †£ÀJæ 62˜¬`÷°ÑÒr$Rˆ1 '],"Ua|P”%CbÁшšýÐN<8¼ºX(!ƒˆuŸ”t÷IIvŸÌ
Ä~B@B@ V?ä£á`?/³’Ùµ™l&¢o&Ù•YHɆƒu™…„ÌBJf—€ªÅ)-H$g1@² ù€ºŒˆ:.##¢Î—Š’nD$=Hp9H¹²ˆÎŽ
H .ËPSè0F†q¡c} »uÝLר1õ©óå_]_Ùu2¶®µ{SvT,êæ¿è0.öâoFøÜéÞ£b_tS!\øøã«íúÞ?ÇøÌ YãæBß·ÛÐi¾ØÑa\Ù£ûîS¨Œ=„ðŸï?uî¹Ø¾‡B~èx[~Ï·a:UçÕç÷ë²3.Œ¡¯¾çß§î=cʬ7¹ÓÅÏÅÚŸ/|½íê2_ÂþçÉìüj÷ã„®é<2Ö1n\†žÑýËÅþår+wö»ÍÆú¡ê®¿\ìmG…í`6w³ÿsêêº:ÄôõeŒð÷9í\ìõùcØ•ßa;å\,êËŒ1fûƒî¿ ×Y]Ô\O…î𺨭߻ëÌÅtýAÏ\NètëŒ úªc1&|ýw«¾süåv£^¥5óÙù6ÃÎÔfø^·óUÝocQá*'?dçš
5Ó9º¸ß÷õwÝñó†é¼î¹±îØ0½W£ÛŠýFùݺȽçã{QûÅÝ…qaÔ×üŽðÝTŒ¡sþòÃÔÞÑ%Œª¿Ý™y݃Q¯ì·—›ßwæ{t˜û‘ÛqŒ¿ª¿ðiï»|ÿŸû¿ïúýãÏ—ï!Œ©ºîÿßKítŸ5ß·?ü^.kº:Í}½/nÔørì_|›Î¾…ÑÙéBÅ:¡¶FÇ«Îñe\çpsóéF…î-Ô«åÅ}ïý0ö†Ü1s¡2TÈcn|Møvsß)C}«Ÿ¶ë…ÝüôÓûݽB…Ð>]&üVÝmý£³FáçÆ×øÑ)ü·ðõ‹f»þÉü¬2tï=ëÛ}O¡Óç ;ákê¢ÿ§ò«ª²;pcŒ sw¡Ó_½S5²{õy÷êCø¼ï½‹úÂ…îª2ìe†n¾÷Mý…ÏÓŸö:Uøû.o„ê8>»nj»7týÿ.ûï{uп};¡¯BuÜÿüòÓÍvp_cä¸úÚÕ=ý×
ß}¾Âì…ÎÛêb|ÝŸ²C×ñÁߎþÒy|ûºad}é.Æçìªÿ2=ºÏù wŒÙ=ýŽý™¹éîà~û¾Oï|ÖôÖütè¢?ÿèþŸŸF“ÛÁøŸÙßê«k†ßc„oÝ>Èþn6ï»…Ó¹ÕuCýŠÏú:ò?§ùZ°ø¾ÍoßF—Ð1øZ[»ý½ã÷4åªàïq¯ïg\NÇÞzUôs7„ßeä×ïç×ï•Ý'Ÿ»Ãÿèýê®Æç v|o\æèzÓŒü`÷Ëç|þëè¹êôáóºî–]õ-du¸¹nÛS“£óK_خʜþ\»+Æšüì¹±Uý=˜/FO—Ïû×_õoìX}{û¹?׫û£§vŒüùTÛû=—ïz}°ñY£køñc¾ußó÷²ÿ½èÊ]tèêúŒßÚ0B÷Ïõ*½mÖÍ|.º+ö>|¨/a;c˜ïùƒüN݃1c»÷v»SØî­Ý ·{¿çTæ¸nac‘7ßëî}çìŠé&»ç?çt°ÿWûÕÝ/·7÷‹:ýÆ4yáﯷËõÖèOUŸ•]¿Æ^fúf÷ªsõÏ2|»­ñês®b›
¿Õ£·;Ž Wÿ©Gã¾×;ú¿Œ¿Q7Õ½LoÛŸ{ázîrt¬ýé¿ûð±FW×~Ÿ½ñ=‡¬±Ûu7ÆÏ«Ïª.™ªÇÍç´{~ܸÐ=küï}úØ=n æç÷Ç×è¸YÝ÷9o:åÆö1s„é¿.û×üÜ#ÜþÐs]W<lŽ19õ;ëÕ‚¹sôÜøù¢kmŒsaú{WÊõÁâk\‡£Çö~ïüÖýcÅ<&3ôÏÈëÚƒqó_æsùX¯Ö¯“Ù¡;|.1†ÌÙž0Ù±³bû~v»»»oÆâ:ÇŸ¿v¯7>äìæ„îù9oÔVmΛ±_îWøÐAeÅâ÷ëw›1öe˜ÞùÐ¥ÆdU³7~f²nìÎøß ß÷ûü¸ï<®bý´!ìíWuï1öÍpÕóó]F~QŸ?˜W¶Ëª®ïÚ®Ÿ±ÈŸ‘£n»ÜnÿÎ}^Žn!wÜö|×Ïu6ƽÊú.·ÛUfkCÇ î¯{ÚßúÜ.»±}þ²Ó_»ÏVÌ_Ìç„ïuoÜÞ÷ç`dÅ Tñy3?êªGç¨ïÔ§® ¦oÆÇìÛûNŸ{Îueè»ß÷ÝsÖ}îoY]6ëUzáó&L·ËÌñyÿñ;NVl_·>Oîÿ¥{7Æ×‘sEcòÛýtûÞýF09®{_Å¢G}þýVÙ÷c|}Þé¼÷—£;ŒÿBN}o÷½wU÷Ⱥ¯Cw¨Ì‰ANϧêª[íëìÿØ!|
_rôO×ý†ßñyt¼«˜6|uÈ :5c·~dw•cüŒ±õe|ûºŸÚÐ=^Åâæö‹þÜÜcl¸Î{s ùÅ]VÏÕn÷]×;[W]Œ
Ý[c:þUŒú÷w¾ïOùÚï \}©Ì0Â÷ {Éz•:².»_VlÙ2\¸°9rþîKÇ‹îýÑó¹ºO^]FÖ«ó†ú˜{ó²>§Úo=²¾çú1ö¼¯¹ßœŽŸŸUá'¿wîûè›îyjŒßÜ ù]ôÔŽá{ÊOݿԎ7c|è®?US;ÂÍmv SãûÇÔw÷bª¿×}O3¡ƒ©ú`ç¶»ƒªºŸ¿p3~t¾ªûðŸ:älǬ³=BݧêžU?*ïF¸úýðásÑÛu«b­ü±·_„úZ¿¡:çõéì]˜XL÷÷ßÛÙ1öóŸìWö\Na|‡ÿ1»ßëÍøùÿÜG½>Ø‹¿²>O]^¸í §bÚ¯ýeo„¿9Æ÷vú©šüžÐE˜z•Xã§+³²cUÅ–!ÜöVUN}¨ÏÅUÅô¥ÇæÚ¹ìÙÙu3丌q>?~¸˜ö6?³¿s÷‹Oa¿n§¸ñiLæW±ßó|øâ{ÿ´ÛUø*ûsþ|•0ÿLÝnŽ“³Ù»_B…o×e®b¯ ßC†á§{Qõ*aï9*ÌW/jüìŽBU¬™óá&oçû|~v
U±÷`«Ì_:ŒûœN_õjÁÆ…¹ kTç§ïÞU/›ˆ>8<®”«âiLþ}XôLŽºúþêïöÇøB‡±÷ÝY#óÂÎtðÁ«´Ú±u×7ÙÅflõ[õÁ÷Ú÷7|‡ÝëŽ3f|ªÛÇ¿»ù"„ ]>cïå—ï9Û)䫤ê»Ñ÷>3¿äu®sõ¸»ûËLîè z?}Ù ÿ±Bÿý÷¯í¯_|ýý6¾c~í—±=êsÜð*5'û·¿ÿ;wˆñS÷nõÇÐAn,B¨Ýù¢rtÿ Æñ£Âto}®¹1æW?eèþáUÂb§æ{ózt¾}C¸yã²s1N^èÙšqÝkcÑ3®*÷Ðm„dUç\v~÷²î&CõȘïGÕç^„Ó‹åÁâ•rU¨v璘«ãä§¾þîêžÇ«ƒœïUă®a0Ž­¯£Foèâk 6?¸ï廆½·šÑc7§S¯R|oý­ïówãèÝÌÜìîacüسa~¼J̧êïßkºøýpÿ=jjºËo,æ?˜QÝÛ)|L_„q¿]á;èýGØË/z§ÓçLØëºÑ¡ãîîü¨ áãèà6özê;LO÷ü1oöO~ÿ·nµ;òƒð_ŒŽ£¾ÜÜÎ|ûΟ1Ž“õŸ¿uëõû¾Â×qÕEgì½ØÐ!'üèÜÿ*a®OWëçºnÆ4ò¿¦6lwüƒþò³6\—±Wß+|¨êô[föçî•1èþ²'\w
ÿ*ašÐaÿƒ.&cóõ³¾×ñ1o|Íew™ŒíÓNÕÝÞt0aÆ}ÿù¡Û…WIcÇí×øê"Cì;>}3•Ý‹‹ENM¨0f|nÄ`ûkôÜ÷Ø3&Ô}}Ñ%„WÆ«0£r箃ڂùòcV¸¾ë8[nøö}û¥ëKLY¬õzA¸4@°¼<X^&ÙÅàÑÚ<&‰iÔ[À@ œV„Ë+UiË\(XbéXbî‹ÁU,¾nÚ¤
S*ÂDõê €•²ðŽ·´8I’]ÙE‘]îã^±¬.0¡BúŒ.ÞÈ#âJ¶‡ŽÐi£³mÙÅz00e.gBÂn–½³m> =bÎÉ(³k•s2Êäp8NçC!`¼MÈ®Là$`8^¶LFãNF™p’]šš aÀƒË®FÃá°Ä&axá`6$2Û@Bðp]-6a‘¸ˆåX$€ Xa$BYT (QæäÉT"‡ÎÀ‡ÄÌŽ"
>›ˆS‚惵žÏFHå¬Ý6VņH»ðΆ³°éÊ8l²KáÂ;W²¹@hp˜ÈÏ*A…D硳Ê> Jq’ÊÍPxg³a`#ïdœ‘+©s‰üÀRAäÄv
Œt³àšvÇr™ÌVAÆÀ‰U²aQeh:\F@Œ^‹óê[辬ýòþbÌŸ²ÿûæŽýbô^øœÃÅœý¡Gèÿ9z„­pÕ×u?+³ºMŽÏÙ/£ë^lã.üþý~#cœ›7áKv¿úº·½1]íîè2;üÏæ|×ìXÌ| »_?;øŠuú?ü百Ñm+];¾¹]>Æú½®CÝèë’õú¨.k·çcvÏ«ÌýïUctÏ‹½Ž¿‘Âuû‹µ.G˜
YÓqïÕ‚qú¿Â„¿.ßbp[WÝyïEÁ`LïߟëªKl!ôü~¯ÚqÙ%ÆÏ™.±}ú±!Ëùö]Ó!Ìl—G¨ûŸ#t‡.U¯öÖýcszö¦ºýÆþy“×½X|ÝáòBu ëß„u7>#'ìæ3¾‹OßCØú<s;:—]ßCÿa½œù½óëóu¸°µÕ½ñ¥þ¿c‘_Œé.×¹£w¾ø–ŸcWưû½ý×ð¯C†Üíx½¢z· ÿó¢jüØî±ÝöÃÞ‹›0f~ü¸ÏuctwìS…ÿ÷ÛÏø¡»}ïjÁ>öGV¼Ïùk¬¡¿æÈ·]|^)×ökBøÜß»]Ç¢óBèª{Ÿût ng¾ç|øØ×US#„/ß‹ü1êo*ôw¯s~0_ô¡»èiv?ÿö_,¾«~ÜN÷º]ÝrcÑÛW“9r;ÝÏÖw•tcŒîXû?r2|/ßgcÑ¡ë'ëS÷NßUù&t§ÙÏüø¹ýªÀúé;Œ1a¼>˜æß{cÍcÌèñ­SvÌ_û¦óÛ×½áºËvÌzº†1;äTèþï`{Ç„êblÌÓóutîŽÅŽß­Î×1úüÚðß:øƒûôéw¿¿[w ²f¿çÛÝ¿_Ͱû¡Ç—Ï1ôëã?wûa› Õ÷ùÁ÷°õäe… Çv·úÃÖw ßò{Õ—.>ÇØ]_¾øPÛÅø0o}eýlO§Ïãs­qö{p]ºc»þ^|Î÷¯ì5¾çÎåu÷Ø1Ï‘=ÿ_ºæ¿@ÃÇFígv×?ê.dçêUÙ9ooçGèqŸºÂ§¾ÊÜ.òKͨ®¶ïg»K‡¾Uß+tÍëP]£®ç{Ãöºœïå&ŒËî£~ô3c7lÞøñ=|è>çkjä.î&»ÏÝ«"ò3Ô|_÷jñîoÏ\ý8ôä×cî»Ø°õu|þ¼“#³wçCÝl~.5êkÞníù~£Â.Ƹ0ŸûäoåÝåå÷ØÏ}rÃ÷ÏŶã¾óg\÷Ç
ßóç ÷Ótœ »õÅ—ºÌÎæ¹ÓéfïÆå¸Ð­.öžGè^GØ/æƒÊì á¿tuQýáròs]÷v»³WŸ§÷‹úÒ¥:üü<ß!gô¸žýzuÿA¸ü ÇõäUçÍd~Íó!ŒQ{—ã{öÅ:•£¾?U÷¾óƒÿ<ÝúbïÓãëF÷ÁÜ}˜.uá:nŽéê`3¯fì×ÿ^›?ÓmGMøî1f;È‹íƒïúÖŸ;\ýÔvÍû¯cÜŒ ûåc…2|íÙõ=ϸìÜ×ßö;ö•á6»ÝÅ`BOßáwÔð_ú»CS¯”듹ýwÙaGå÷Ý ûu*{¾ý!lد­Ð™ùaîGÏœñyC… ݽ/Ö°£¿†ðá/³Bèö-W½Ùõ×=Sßî¾gÍç «¾ü̸©ÐùÛÿ×}_ÜÈU£~dvå÷îÛì¦'L÷©í ¶ƒíðµò{tÛ‹íöƒÐ_Gì!Ôçž¹ÿîñbÑûßÂÜ9êã}é.y1˜ºÉùUáÛ·®w1w~¯ŸþöØêvdøà:»wŒîVsÛ»×iêb¬ùÿ“³Ý+|0¶ªëØ™ÿÙÐÓ-»ëC˜ì`ö¾úý2»£þ÷g¶cÕŞ·ÿâ‡Ð[9ýy>UUUö[—¯«Ð]¯6'Ç~ÈëÏ%ëb0¾Fuþ\ßB×w½îÉÛ°cºÓ¸ ¿—c'w>÷ Õ7Õ#ìþßè`T¬=!ÜüT}
ߣË\ B]l}ù½^þ|‡Î¡C~ÊÍÌ.¾bÌ !|Fu뺘¦û÷»~Åâ³'?t†N_±§º1ù9º{­A}Ö3îSíLŽ Yßr>×v;TVYõûßcøúñÿ~?o޽P9›ÝÙýSLõ¡ƒ«›ÉoõÝÁ§ØÛçgüÚu¿jïkŽ›°_;B]ǯØç:ä\Vu¯ÚþV7û¹÷úbìOç÷AØþܵªû{c'L§P±—ùvuaܘÙñ3:Å⃑Ý9{O#o¹—ݦf¾TeVèÞÓ×ÎSëð)§gC·ïAõVwlß{ûåï¿‹®ÇÈ}ÓŸ[ÕÌÇù—cëSf÷ªùùBL#|û®ÆN†1>×~
ݯîõéíÜu1û0êò{n[n+o»wÅà{¨0z¯»tMØê›üìö)¶Ûãû†ïžëî~Þû+ûF§o1Ïüè¬Êéï÷»øF÷tÝ:ÔÔøÝ›éÞ£BýïíKsÓ3ÝÓÜemö¨°ßcõfÇûï9á?y1»¿.9s‡¼­©îîº=aä55v:ØÛž19ºæ\ìékv~lm¨ ãkÝsÇÇQõAýU~uÛ¹×>¯ÓŸ{ý¶ý%¿¾]ýõ˜Ùn=k¸Ïß{èúœúbÿVUùuküíÍf{1mÌ0}±ým„ùúaÿ2«fºÓ—‹-ä}¾u1ö˜PÇí_…Êûë˜7{=²?¿Ûǹ £ë¨¿ìùºãsóó1|aº×‹5|]ÿs1VØüfÆuì‹iÆÏæþ5F^}±Çaº|×åçÿÛ×üW…ÎÎ_.„Ùü:~Lº¿~ýp;ŸŸ:õ\L¡®çf2|ÎßbÏa櫾ºï\ìµïÓèú-æñù:îÜ«So÷þ§¯®}Ÿ¿‡o]ÜŠý÷6¾Èzµ`ü"»Æ~}Ði/öÜnªGÈ ¡k¨0}]G…qÿ=#»Î]L™ÆW…ϰŸ¦kÝ×¹¯®Žwµ™yÛòjo¦Bm羓ckçî{¹÷ÿ½ºÇû ôNgîv» óÌÝw…P=¦³ãÖ×õ—êîƒ/r»S×}þÞaª>³êGOUW_ÃÔtºÿý0?ÓaÜ…ðãSw™ºžíÿïîSþ«:CøûúÍaüfã²nóÛT˜n[æ²7».+GæØÛýÓ݇ ÕÁ×e¸ÝùP3:‡¡+|›/_„Pjª;꿼é¼ÝGÝÕ‡ÿ.£îUâ·é¡?^Ïe÷žî®Ç×ìýÎèð=vÇ­«=>§¬ 2ä·üÒíê2¿«3äçZuyýS]Æ\,¶³îû+³ft/Æõ~“ß·?&\^S76业êzucç>¨p;]¶îgf¶þãÏFmÖןùþ^Ýæj{Ã×ýqÝî>˜úž:Èë0¶?¸ Óµ¯.~ô¸ì ï‹Ÿ°áëŒ.}ß¿ùÙ÷÷ö¾¨ýÏ5/ä˜þoŸëÝmØ®u»>Öfu››ÛüÞßÝC}ܪŸr;øtáûÝ̳u_¦»ºîïo²jtîºÍªîݽëBø.Ÿ.dqcjŒŽŸîrì]^Ÿ.ÿ¾Cn÷^·ä™ùytšÌ1ûß×m7¶Ñ_³S}ð½?~öös#;Ý\¬ó_j{Ըι_v„°Fü.™ùu²«Ëν*0…ÿð;õe:_v…ÍíöæãWýOèþ5öÊÛíçbo3asÃíuü9\O~Ÿº~¹{ï?aBçȯŽ=Wûu£.t k˜Ýí"çbð!§‹°±¸»Ü¾ªï|s9#ÜݨÎ.5÷*­FfÖýÇŽ£²¾§ÝÿT½ÝÁ»_½ágŒ¾Þ©®Ðãgk:ÞÑa¾tþ"ÔþüØÑñk jÜíUÓéFŘ»›c§w;ØÊÛÎþ¼ŸƒËÿb„Ž•W_¦¿ãlìÅîøV¡º{þ4a¶?…îz£bÛõ¹BWu2ö>ò2Œñ‹ð½M¸Où·ãzlÐEÎø4ÓÅgìÿy|í¯ÝÅŽÚñ_÷ëÂtÑ£bÝÿýÐí3ö²ýíC¸ïò£ú²*Œ [õÁv=ª;øûä3ªóȼ¿
û9ÔäŒnò+ëËn~øûõ±èší1_kì˜Êʬên5ãÇ16ÇÍtùüqC®ûéžÃýajÆíog[sóîÇøòÝgîUÒ×ûœó?uNõ…Üê‹p÷uÙ1ŒŠ9„›ïfd÷š¹ãf»ý£»ûFþ›ÍðÙ]¾¨ØÇøÔù½‡ù:º~~|v„ð½ý9=_osäø :+„Ð=¾;òsÊ‘75ê‹ïÛå2ö õÿÁT÷ƒ™ó¨t¡X”å0ŽcJA†fˆÆ à (Iæòñ¶Æ]D(F<B(,‘‰òX0$9¢0Š£¤œbÌ¡3
C“¸dv)1UµÜÈ䖬܅ùAص4CíòN׎²_÷J}µ®ºf'hy¶Ø=Å̱/‰Ç«÷?¯Ç‘È̘„¥>4”Å<'s¦v…íÚvó|à‚AÁRd¢ÄD¯ë^m ˜íÒ§‹Þ=ÝZ­Þ¨¢{À÷w;ÂvXhž‰ˆÖ^“M‹t ôÂYc±±ž)^NekÓá÷'F¡®¬™<ƒIf#ÞÃWãKô©JùG]xkŠ…ô‘…ñFÖÃèEPW”"d<º›¤èó$® `Ûš¥RÚ&1ãá÷$cAѶ€Uâòº|A
9Ö–OýÀªG‘ðÖʲônbNŠÒQ3ªzJÜŸÉ×F;ðIf=Æ$ÎÄY¸IÆWÔýN×s·a&»…°}D·¬ã}DÔî.m(/Uù¯à6klå6¤žÃ’<,ø¯À–íÄÓª`(ä æø¢Ek—±‹Ö¹²…Â裿U/“›SнÓý§š+hwfbwé3P£l ¹´6žaQRÒõ÷?DÆ¡í-“‘þ& %Áai¯IÏqÃB 7<j
ÎÑ9´>õ
L h悸àåTJTã)F9¾R/"(¨Ã«ë˜ŽŸ‚2g޶E5­JþSq¤°ù|«.ÈÙ7UˆÓ>U|cuœ1mBA_¾Ë’.ùe]hŽ8"ï©þÀÎásÀQãË’6K}SÕš×BÉl
ЦQEîómš¦Sbªri«”0‡ÕnïáóÙ^ bÆ>þ’ðà$¿W--7õá–<bqwvIªƒÒ¾ßfºêÆkEMý‹A“DRÉCåRÈ1504mq>÷róé”í&w­KDþ|ˆ©ú@°¯‰ù`_Ó€$l +™(¶9õˆŠT7ðˬ.ý2ÛÊ!æáþ«öÖrE¦[:§]T¿ïæâ:^ÊÒý
ÓÑ-[Akÿ”¦Ð¸©¥èï2>]”ÝzcóúJàˆÞ‘×F‚êöhfcQìµ£Þ³žQ…Ä™¯¼ÐENVÜDÈVú=ïæ8Qìδ+E³ñ4낪¢ŸUÁûYrzÔ„ËŠ=J!AK5(Rraé¾¥¬ûŸÇõ§cElâ³4M ˜“\cÞJ5±j€Ã±C?™Á*M®ÚW½Xni0 .O:cÿ @xŸk@ô¢YO¡ßur
ˆ(ËKY4¯Ô zaY¿W¸”–&æ¿< |{¡.Ά¾˜W}|AHîCÇÞcNÆ-Ðy
$œ”Â?£wNh TS >QÔë!=6ÏTSjIL£„W-òR£ÑF£ZŒÞ^xŸQ²¶ã”¾ÒÚ®U@ {ÆÀ¨ÂY—5Xìa_øç™ƒ”ïùžãØwï¦êìZÒD…ö~šÚÏÓ?Njs÷;ûs¢fV%n
*ÚÐûéÜ2BrH˜—DѺhŠÓGƒá
çYþ4ä›7Ît¬ÞIB>¹lT¢SÁcå^UŒK„Ÿšú«àŠÒ«©ÅÚ¦^§^eòBÞyÆ¢¢‡ècýV:Ü™C„£žÎêÕLæÙ©›ÚYž%2©|eaŽÝBÙùŠá¯HÈ3%ÝÁ&%ÉüK#ì· CqR ¼ÖL-ýÀâΨIƒ#Z«ÝGÞA€ÅÀ%¡‹ñçÀ*͹†Àq¤u lÔ¡}0“¸äÆüJŽxP4gSÏÃD‰ü_“Í÷…¾"h¯è«bUÞ@æ÷jgy5£t¯¼Újô’ðÑóÔsÍࣘ{uð)©ôtw½•KB ¢âò"CY±*}ÎWì(Ðî¼ßšßWí€â¯ÆWÁé`Òj2£©¯¸%«ÑúWÁ.<MŸ$Æ_ö«fœ ü+©sžìÙÔJPœ³ ïWßòO¥øª^fø½º­]°V
újÅ
…ºDÊ“­˜eKI Ø8Ñž)ѵHÅ389ùÓøÙ#‹‹^j¹jExñÚX÷|h­s~¸ý3{€÷×µAþlmëæ•Ej©‹¡Øú]:ÐÛ>‹ œ¨‹°#'»Ë©e?™²&q¾h~`Ô_‹Ì%îhŠ—€»Æj'¼
ÚB³GjFõ׊£ãI…o¬ÂßQ
jÔ² !Ó\Äñ8¿3Igk¬bà=9Êm fuX ”N2=²Ït0è3¢l衱 à÷'x䆖Vi;©¦Ï=µ§ÒRâ CW¬íåÍæ[UO>Jkw×Óْٕû~'LÇ$8OÝ«ÓS•'ê4dê&€‚ð=÷ž+(³^^Åñ# ?ßfÑX•WT¾¿ÕÄ_eŽü#¡H¦ÿÖ(|MÝý÷Ȳ+ÎÊŸmÞSôlÒôYé‘X| Ê€ž†ÿ+@'àõ´Lû¼>FÃ>U=·:j&Ë1¨Z$2™“6뎧xÇFUÿ=µ§HSÕ╊ªI¢Fä/>UG\r!Ûþ!`‹½¶°¢œCGý,È?^ê*tT©V$ ¸:Q==ÂÀ.å;Ö;uRý¦3c¢V>rcj(³v|©Ã¨RFÙ—›êuL*Ýô¨¼$p´Q—[ òZT®.ÎeØá~¹¨¬Âî×ÛàŽQ{­¹»#f{Ž¢´úWÔ Áã0ë¨bM¾þ¿fm—›E<&ªNhæˆE?¢:TΩßêÝEÔÓ4“D«6ÔXb‡
„)ì7N²ÆÐ¹]œV:«Z #zäK|JijÚÍ-D¾òÜhpàúº½atsúêY±NÛ-Þ4£"û‡jGéß+|ÕngnåJ;äü3È2¥© 'ÖY?°8TØ2µqKõb î§äz/ö1^x—#ç¯40´»ÇSF¿ª6™Å¥"SNç½r|ô-•¨n›ø\¯Ä}¥mßpsó“Æø(æ'ìÑ2þÛÒ@@CÚ›’·úº™šÒÃj;à3À?DX,p`³±§—ˆÙÒƒN²¤V¬8­¹”š­Šø~Ú~'¢‘òšÊâY ÿ55èáâ %òAÑ.`w]²Sm“­»j¸'nM›×n:¼\Ùºeà@vY¿nÑ”W§¢Ú-ªGW¬.ªjÚhVÓý{‹Üº(ú$nJàO³œa„SëV kTüT')XŠ»F®XÀç@¯¨w‰,;áŒ#?Rˆ† # 㙿ég³=hDY2“ó—úAžëÑts¾G¶x*@ºîŒÐíò3CµC§˜Zgü˜%ª\Fp¥M™øšìyr”íp´c/CÞès4Öo翯1%ÿ¾ï°QŽkùh)d‹A—¨†X2²¯{ëpüNƒ´wïÀ’Lêø0øué*‘ó%7éš ™/Å`=Hí¬ëÂÞ~èÃn½ÖYÌK‰úšßE­ÏH‹F8$
É„†„õmB¶) ]U¯J.×dÞœÆ
±3=¢Ð¬–“Žé~2¤—OlPŸ¡×ã±?`€¡Íîrþ÷c\2ˆçÚrzï4ç j¶àCEbz1À‰QÐ3âX+PO—²6VzéÑ€¢÷¨ëSý~öŸ xÒÓÚ‚ƒÕVhtšÄ´oUĤcÿ= ”Ëxö¡Àâ?/Zò EÔËö¸C`)ˆoÌúoiCÍ÷ wyràtoÏ%>2åLòÕy&å÷¿CêéI(Ø…ÓÓ²ô€.3»ÜöLdvw×CݱN¯%»#@.µá¼ ©î•Œy„…ÊV¡Äšú€ôXŸ­Bº^x"šˆ@«S d 4¤§@¦r(A ”eÐòDzM1|?“"Ť$±?#ù´é˜„ËÓûc‚gÖâ*¦èîÚ¬ä¾(<Øg·YàÖtLÁÅŠÉ=.˲,jc½þt2')¦ws|#ùŠ ý˜¬’/³øÔ{ ÀQL.9PWÿ¿_’DÅÎé§l¢$Š|‘—­íÒ¥§ù_*õm)±Ã:„}—–Ȭ||߱ïo+ÏÎ`¿rV‡+_wÃÊ Á¸2œ¨Œ¨”#öLèJ¹+•œ ì$êíÉpKM¤šPœºº¯ˆ#P‰Ýß yVs•Ý…Fþ›ÞC©ŒýäÐEû?nÁ†…PÐ0³¦ƒ4ÔayjôZá‰7†ÿgK:–°À×NvÆUÅ•ŠÔ”0qXF^F´˜(ÝOxe0–£ýË;d–á L ҼݎXì™áÀ–sœðªPc܇ÖìØP¸9†5ûDdRâZìñªhëˆtð æ?å\OBˆ.=1Öê¤"¼ Œ¹íþÆ€—G­‚¿œQµOñd‚¿Å ¿úý ¼ó€Ð.óä{²ÊÔv<ZðO²Ï\/l7KïdÅVÑÈ<2ð¿V»€Ô(ªxp mIlcðãd7ûïÄø}ý\ÀøK !xz6ûéØð+³¥8+ÉÎYf+_(•~ïù—ŒWRœGsœPþ²Ïn´¬“
~Ä3øŠ9^ƒXpçæŠ‹F©Qík™ò#XÂg>[ºÀÛ»ŽÉ‹Ý½s#õ÷ˆ`‰4´{óê%n‰õŒŒåG á#Òþ˜3ï•Wò½#Jì´«NRHô ûª&†p <ÙË*FB—wSZ­Ä0íF995YzŠ亸©"»šö9óE^1i¾PÓ´³9'ª<¢ C¨$ËpyÌRÊòªýJÄ~—o»€Ã°œÂ"0×b秨 öT>Hh…œ*æ:”Kàœ¬É6“£^˜¸t,×¼CgÚWJ‘ie¦„GÚ>=Çãý£j­~Âûã8Øê|Qù_ÇÕb¼¿YF.x9~NÛø¼¾]0™®®f‘˜*†ÔaŽÌ¨Õ¥ÒÜv1žô%h Q°a':ZÐíº¤FÐAbm¦-ëg*"ÌóÀGÃOÉzmZ È#h¸ýN[?Ðî{ý”Ç@l×ÎÇá`u72Ößp7šmÔ<3œц[´ÀÓ£øº4ƒö·ê ‹¢U_³8t'šŽ=`TÙ,¦6¤'rJ´7BÄ6ý»ðZQˆ¦@ubI‚¿fžÔ+†Êqõ1a©ž­&á2sô²=ˆ±È'6ï§;#ŽÍ&ég>þß/²æú˜XÓYbK÷Í„wµñª_*„Òœ ú\。ΤŒWF{
“äé  ¶­ÍRðÁ-3où}¿ÞK·ÙÞܺ‚ ŸÀTÛ#â˲›l@Ï ½U…"dWYãþꞂƄ$gù¼8 žôÑo¶CcÛF:2£ôêm±ì\ÌîXFP›¿W$ØÜºà´9¿Z דl‹VJ{¤ö<íÅaA+þK˜I±ku8Bޯʽƒ¬ÛBndna|Øæ©>?üO2;ƒKVE1çªH‡Ež`ž0 ûªjz†6dT;ÿÉò oôVæ7¢B¶ÚeÎ=½9(ƒL%þÕÓšåÎÕ”L7 amJ®+êþä °p$s ÿè´w/ûZmÃS+Ïœ¯¯ÀÃ}‡P è½ m[¨•ô÷UñnmZ88ho؈‰^Ç`ã<!£mé1,ƒ±l0"ØU~¦ªßB­åžÖ1xÊÊËn¥´½ì¼MuÑÇ”¡b“&5ñ$Õ«Ô÷*ß…Ð- ˜—q ®!g"{m‘ÛL`‰XBOÛl6°¤ràµ?ˆõH†å/²Æ’Ø ³;å”8x JèåLþR†y(šþtžATL™f8o†^Z@®éÂJî8—\Ecìu¯‰ŒªÆ< —ÅìTÆbÐõ*9‘€K¼I Œ<Tj|q”f“hä}&¤š-NðÙQ£L‹ÿ«ZIÏXZvñ‘ÜIÒ)a¹0:Ê®éÙ®Ò—ÈX©*ïXN©aw±p[ЏQ„º9
Ë: Ÿ.Ëï=íy áX(Pód˜Náâ *ߘ+3ã/ýu*ã×4h”g‚•3:*-@hãÄÉD"´ )壘û·“åK2ÒsA©¶ÅØæ!ꔑ¿à÷ˆÊÅÓïC,ÓT•’ a¤÷ ˆ ã8Ñ ™øÖ³e3Á.í3Ãg–íj*3Œ&_º"`sQTD?)¡¾áƒ÷
NðZA±ÉŽ«Q] ¦Z€9*#ñ}Ä—6‘ü­ÝÑ’jN&5¬Ò)ÄÁÛ¾2O'Öý%¿&Ä…•¶õZü)5´)‹ŠÑ ½…AU‘ÓüéÊà©ð‹ì†ˆHnqÆÊ¥ÀfȾ
aýJ”r-Ì¢‡ÄH‹Â.”¹Ò6:HŠ…½Z±!ÃK±êTr‚YòFs«¾$¸xÉ‹.Fƒ‡Ö©ÇùÎ÷PëÏzãäy\ÿxpO[Z,›òàn'WotEK£düŸúc±r¼”qXi-ýõ.Â[šãö!õ'9=rüæ÷puÆúþO)÷Èjä~Òmr>ú-Ï©ôdHåJZþ;FIõÊVUQ¡á­‰—½™¹c˜aüÂæé¬p6BÉÖec’XÌ$Žð!sY·ôëÕ]€ëÔÔ»é€NoqÊ=¾Ó³ÜlrNŠlÃ|Kõw#¡÷h֋ǽÂ?—o<Ë©ê9?ê*AwwoyS,yC€ß†ŸÇþþÓ]äOìÆ—î•3 .4ï(1 Sîhéà ‹ùr| )ßý¨,‚³¦¿Iñ=3j¶†KbmFµBgîa|±pÁÓjú¥µ ŽÐöeøú"6*g
™?¨ôŽÌý±[‘Q¬Ì?“¼ªja®–A`OF±º`á^9¾TÙÅ–ÅsÑ^°Æ! bëÈ:8Š»Ùù)’n&’Šæç â˜è’ÇþL|'\g‹«hw+›äê+3:ߢ:ݧþ#• ¦7ü"ŸvÕ÷_W[„aÉ‘«à/±JkW!\íýêŽÆ•’£è¤¼H*jú¹ÇcÃÎ’iÒXY2{‚0{.%ÚDƒ)ˆ¯¯‡îà3Ä6áåŠð}b¾ø>~p†C£MBÂ_,÷×oçR±ü ¤­{±Q‹.š%ç¿™‡Lï»2*
è!ê­‰I¨pïækn±X%êÍ…º;U©]Î"('ºÎ+”Ý£‰Ò"‚Ž$š·€º ~k…uÉ+³a9C‡fHèÿ,`õ¤íƒ, úÆïØÜ -ïÞí¿Í¦¡È77sLl#
9~è™»§óá#[H1Ñ·dË¿ì{¸pæô»ZÞß<Nm-ä7_Á”ñD›ƒÜN9W‚¯ýÊH‚Üœlð®ñDºEüœØ|Öùr¢tzŸ’]û.ËÓäZª÷¢1à‚¿{~-ú;q.W·Ù×
*€¯+Ð?u/lÑSLXÈ#*—=Y:ì¬KȯHÖjFúÊØøòüUæßa¶&_
+˜Iœs`î¶ÇRÌÆ!1id@º“fˆê4Ü‘\ÃZ‰K³Ü ‘Àoñ¬jšf†õy@¼¤_v€ð~·œýM¬À~á9é)P G‚Áܾïô…ä"”Ǫ¤cy"{߃÷ïi5õcÑÍãj×Ì,ÂÐg*£G=ß°£¤¶™©íÑD¦x6KUÄ4xX±JPŒ¾§?½
ìZe¼ a–fL¦ãŒLÝ×+•Ÿp`ÛÛRú2ÛYq+ó>Çž€•.u{ü À{G*_†üø~ÜÒ’J)]U¢µþM¬¡!äe…0ó‰]Ò¶h¾YBÌ&( ³_<1Æ (#fÉ3¹ÐªšB½;
ÓOȾI7wàr„æýÝçÀmHÛ`È™ÿGe×2Ïwå|äuF&®ܨÈLßÛÌÎf Ü9Ž/Àk6r•Š‹rXèÔÁcl8Å–SÉ韻9‹&…wuT4Òiá§àsŽy¾±ãm ®ƒ›oF&ëyºÈ¿Fß× ˆ–#/b¹e_¤„¡á•ƒo/€oKµ“ÓTݱË}P’ë
÷5’[ô@¯Œ±€–¾ÐVîl„.v{†/ñtÜC|%¸V.ý›({7_ß—b= ´btfqÅž§p7†u¬¹„t.`zŒë÷‚ñ~ àX§BPVvuW™™Ò^[ ) ‰Û^¤,­ô–r¨ä!¢¸’ÿ B±êkôÌôïÙcжÎåmú›¥+ÞL!Ì“#óÄ÷x”EåÍ{Üõ¹
©ð®`ràp µýZj¼¦ì÷ âba²²ö0ð3L×Å5 …¹_l5üP<‰Ó­éD<±þ|¢Äƒ¦=0r ó0€ŽŸP<Ñ›Ðñ’A¼2kôPÅ“›>¨0íÁáM½kî_ö4À& j`ý@EÑ9«'è\)žL¯@š93ZàZ«±ÑÒÆïA…4Æ~‚(¢ˆ9íÜíß¾6ݺe4a{ëPªyNáSº'õŒØÇÙ‡ˆ5Zò¸›G-§¥H›§µ}k Ÿì¢Q¦¡8
î:*þ¡ðöV¬'O‡Rœáz-–ÛJg§»%YSè`Ö€Gð7PüZÔʇ#‘%£ P/Ç^êÈÌ΋Z²˺-*­‡¹{V­z+PÍϰd0-?ŠŽù|5 Üžï›KN1qH×Ûz=ósZ\$îËÌC+øBfáñ D‡’R*“ÚÕÂ%B6Mú×(†‡HÈ¡^QÆIç‡÷®G`UáI3ź“ $Ò¹%I=ncÐpL"µŸn©–üçs™¤øèÊc:‹Ü"?ˆÍ¯x¥1²ÎÉÃob˜ôI[Ü_î¨xJ2sˆ…¯¸ò©¾cææÍÝ.`Ûý̜ۙOHjžÑóJ<QÅ!â ųøÝð}íó
h¬'«ñÄo]vCCK¤© «W¢ à8›ê hÖä§òÚÅ26Ôx"x<™fvEÕx b|·;ÈÇ“VM@ÿ²Æ“ÝwíÝJð…aœÆS0 ©\}6'YW±])ý¤­Fh´}b®fŸâèå‰Ì¦ï§Þ2™µJ„%§NeËrèì¦×ÏÌÏSÈB\EdÏMq
ô~D{y“ 38™I߯oýq‰ÂGkqz{+HJï@³­4=)ð0¦Õ 9C¥ «¤ú´´ãÝç=ÈRu&Õ'cB¯aQ 3¼L»ß ŵԉÝìîÅfT‹“ îŽ-ÄFµ´²Ž÷E8Núó1³S®¨µ" Q©w/7þeW{zðSµ$óX^ïwÉNfˆM7ØÿívSl~ìm*—¡ @ºòÁ-ŒÔÅMžþ3¯n-ÌSZÊKþ²k%Áiˆ’èÛîÅÁ[4" çK«ßÜ
ûDð§Áo;
9ÔÎq³ñ­\[Á±‰VQ‹.øñ.Gð¤$%Þ?×h#¨ÌtòæSŸÊ”Œž{ýng‡›èeÎ
Òóñ-NBö\¬ÞÍ»[Ëþ¦…ŠÖÏühؾ&rU)å8#yÒ/”tû±žNzÊÖÊæ÷+wþªÑÏ-A¦¸–=Èå'0m]¡ 6f*² ¬(î›
ÙbMJáðl™Ž*)(©Ü­ìÉÌ“IºÚ*ªÅH1¡þ&mëä઀Øl*ÍE÷× bÕè÷'jÄklà2`ïÙ ÃÖYªÎ݈Oð 8<ŒØ*”h4R š@¦ÐC ò/vœü¡• ÷†J+¯’ôW„YkdPÎi/è'ëRy$V^K>I³I/Ã->bG1Ä×ûd.„ËŒs£$¬ îÔ66]`À€JM »Ï—ä~ýðø'§ÂñgRMÏ=9¹%hc̽ L9¡Øò®Aú9_TÇ ëžêš4Ì\yw?ÍS—¯7ÂðˆL2mìQ38r¯îÕaÙŸýOÐ(C‚ž²@³5?ø@;cen2«´ið»~zÐuQE üë €‰Š«Y•ø·s …öôï7M¬ìlPs“÷ž"Õá_§@è#§Éªÿïâ_n6¨R¼qÑ¿ÀP ýe
þmn)ˆf–­×'ÍßÜ®ìFÅÛFöî/ý;2™fðožŸ³äè_Œ`:c…þM‰0\
­ŒRÇ¿ZÖÿ­ã£ê 3-„ô¯Ý›¯ ÒWÿ»DFHx3'Ø‹­A
¹ÿº7pô/ ŒAig|äÜðo@ž¹ð&¿+Fÿ63e—ÿF!¢uÅQm\Æ«•³þVÖwÚ^ •,MÊ´ÿ^5æÈ"$?™›Ä ¦ &‘•³m¹½[È+u§¶À­PMî4W) ¥%:¶Eßi²€yøˆÿIY´[(´Ý®ž]©9Œ‰U3o¨ýWR?Óº@ˆk«É-ì–vÇ9IigW¬øßCˆy‚
®§ÃÂIphôKö5'5” i• àTÔÝ/$Å{¤/©T ¹›¿ýØ©Õê4]‰½4'±—+îNs/¤Çîn˜xu©ø!X„³JQÜDÂOÅÌ—w!ÙX”Ö^åwx®jùy?!¢¯ËnÕ²àiB/ pþšým÷ëÏãï
žY,wÙ¦¥Û'__qGæ‚ð&Y·Y`P˽—ߤää;–ŸÚ…e'-€Ù\ñެÊã¯õÃY0ðeØ5Wkj* š=SQmÜ<iQˆm¹2ä'4Øn¸ŠÍƒz|(ˆ#ÿ|®ðB“ë.ÌXà ¾šËתPk|+lš…EÀ:›â€ ™eœ…W¡×ëDéæI¾Ò  Cô£ôÀHsŸÌ·«×®ß†Û¡¾¹a¸ÍÏ´e­ÈUøËý+„ ×@ϧ&åñƒ¯á¢Ð—ž ’Cq%õ¥´88İB h|Ã4êÓ²–õÚOº™Ž…zË&¦¦èI©‘-­‹ê¬ ]y_¬SŽd,Ò†zIkÎûk7™ õöˆ¼ÇN5¼"+õ¶HPÙ»¦h¥€Í-ššÔµF3#8Ø„x¤8ŠâCJÞ^Q^CÖ1WøØ§Ú縶 ÕÙÕ…ŽäLq7S,$›¹l…q9(-Êd¢.!×̶XS%¨oeÐnÓ‰í¼h.'ëþí”ÌL—yX騡?HДÄZtª'«£¡©}fX½!otª‡¾‹ÖvËÐor§‘zOÀºr%r*ÎÏz @Å·ˆÌh2OýÔä·¦+Aí)NˆsZ‘cÆ­:ïCñIü+$°u¼ÆÔr1¦¸1”Éy¬äÒó­±LŵÊïõ<›µ,r³r°ÇÜm)¯%ãQ3BíQû‰N©×O‚^mÁ‚o£íÎááé2õæ'Kþ5Ê·Ó›+¼Ç.ý0ÿÐÙ00jO¸â©Ý›i½Üv>ß_вz1¢7¡«NpE ¿„bY¾÷0Ñià} ‘”žtó²<PxTƒsæ¿z¥žiUïÔAñ¸PødI¿z'ã£S§B¿Òšp_½¶øæU¯7ª'µÒª¯›îÔTL©z ¥ànÝû5®^!BèVBƒgvE‹G•š/YÚŠß«4çfzõ²KïûÌä«Þ‘shv#v"tõnÏŸ+¬CJdâßrÀ^m‚ž®zMÚØ×¯q¼]·Q¨’Üšƒr© ¹ša4."¹¸’üÐD8`U*ªÛôȈY©”ž|zšˆ—4
ôH,UÄ,´dLÔZ‰Ê¶<™Dkª‘”o¹/„ì$¡k™ùºz=†x;²±rv‹xÒ LV¯šhÀsm±‡åiÀ„[ÿÈMü]† êÒŒ²D§ˆ2ž§×©·³»—X‰¾Ý¤De=¶äEdHp¦Í+`j%¬_H™1_ÊYc5;½¡òkxéeì̘$@¼šg/Lèºgd¸~VAs2Ï-»àŠFïÉEr¼r,Éôå›ý¨vÝ ¨žÓ+iÎF3ãAúà ԛ˨²¬hɆJhÞìÿvÆfè-ß©ëשáJ.=ŒÝå»÷;á OŽe^_V4NÝ7o–“QC¥ÆxxBýé¬ÑŠAÑ—ðžóyu·$“¹`IMA¶;™2zwµÆŠOÇ+™Ap™©šo|wG„€/ÁIÈoÁõ.2ª´ÕX7ÖP3SY­ôcAijs ¢ò¯¬h•?h®OA~n¼›Làæ×¼»ûŒ¡dø£°w·/Ü*Z¼ëÔ=K…mOd‹»x7¬i¬µ‘\|wǸ‡ÆöUÞÿÚïnÌ¿+Ê%øQ õîÂÞ²ð%h}ytc³µjFÝÄ»’4dÌÊ»»é"5„&s¬[ÚÞ9Û$Vµê-³ Æþ%ÅÞ,h³8BdHêųÞÜû{ŽÉ-’n=ƒpë·•²pø;ñjÑãнÃ.ð·6ùqP0öÏXo©‹¡ýé²}#_9I*³¹³-WNµKV:³7HSœïg«™¸’µ&ïÞ’M¾¸ÑϺñ/!¸Ì¿\±ñËòs‰g­ÔåÅÌÕó0ÌáW’I2(‹ÎP‘!Cv“Z£È’Âô)/{赢ݰ¼8⤑;±€
®Hlõå„cqÚÛôÇx\1Yn[‚Á‰Ïòɯõ°*H•Öñ^·Ï&ö]—©p9UÓ$tcÞ‘Ÿ`+¦ÔKØáSr¹Úµm¡?Ob¶6{bx͑ܬQºI*^ËË1ÖC:AÁ7V´Þ“aà°ÿ7fûR7áTP@_ ÞláÎ.³Âìkµ‹(tÇ¢´(RIÓ;1Öñ‚æDTÿqÂ5¬ªþ}DÞhÈåÛï:xCÍè  ¢4 ˜€zoæø´¤±ža°SÚþ°k F-LY‡ã£w>ÏñR:Å춆.å–[IÆ=ÙÇ» “”pÇn
'52ŒOíœ.qr­` Æ0Cûå`À.”ú• 4¦í´Sð@Öäzhvº r%ÄÛƒ [u"#ì…%¤îþ¨6Çënï$àÀØî×Ñ_ù¬ 4ÜŠ)^–åÍ`z‡Þ±p©|y^ô¿Éý(éÕ“ð†Š1‰i£÷Sª7~¡¿†D1û¢’ ÁF|™ÝEŬ%´àldªE·]Õ½ÝSEj:º4cÂ&»,«8º5§³Ýß=ºV;“=<ºà}ŸÞ Yl}AÒfwŠ®xtß0e¬ð ›£Ë…—…®¡wx¾Š2æ,±µŽ„ùÎá Pk·H×#æµ @^3v€’!Ë")FNl$5‹½§£^ “ô±¼PŒ\±üK›÷Á”娤8CEŽâˆ{ ŠãÛ_GlàVþŠ´J…÷ùY(Â,¾6Û=ßZƒ½ó­gø=ù{œ²aã“QÜΫÙäÛ¼H¼Haœ×ÚPQÍD²Hº(H‰1Ì‹áüZÎ˨-î3ÇȬë—M&¤.ôùòæ÷}Š-
ÁŒÍµzF\Î$…$a±–ËUß­*tf„Ê— íKyúl® ÂÁ£nYäfüi ©èÕ'Ò-%–©G£Ó¼æIÉ®—øÚ./jzÍéÔeÂÜßÈ¢!#·bó³<ŸÙÿ8Û{`}ô9lw7Ùd¾·ç×i7¹ ܈ŠäL¸vä’ï§çö$‰Š¸Z¼‘FÉ&¬ýÃ[$c¤²UGñ–h#þÅ=¥Ÿ%Øý¶j·D™¨é®Ïóˆ³ü½»“ä|$\D»¶ý žßCiX¢q™÷^_'©3P¨t2ÓÍüTlšpsLW~´¥]šá9 äÁt3ágc— )]}
QÿAÜ’r˜®ÿYä_é*”0Q2]-çk±ø’®*Ó…ˆËÇÙe·Z1Èt«ëK:ÎtÏVM›ø„O¾ä\-ÌÌë{¸ÝMs=S”Â@9о2 Þ
â'=RÄ\:dÅêÊÈr°g¦2ªÄ÷pîÝ&JÝð×"¡î
6SZ%uºÂ#® vï¼Ö_856XÄœa¬}Ïà&“Á—uå(Qpæ“…r{à}C×ÙDé.r*Ã7}Ïd£9o»Ú+fÀÔR­.`͉GJG:>•¼"ði,¹¿&Ëa%#[ß›Ž):xÚ&A*î® r—¤,POp@l]ÚwJ}І†ò’¶"¤pNWcÈoz‰†«5Næ½)MÙ¿PBŸiŽã€ðšÄvAÂÌÿI]2×1æÕ“Gd„ CzÛTÌåÊ.·Œ‰»‚í] Èý­~Ö^¨¹ÆÂ1}Ô‰b ì1ñ*_ ç”a .,¹ …œ_Ìj7p§ÈL±…«3Ã<‡Ã?zës+B%JW3Ø
¿ nÑ™–Àp¨øIóÚ§jƒ¨°ÇQÐÏê4ˆ^¤Ô ÅÈ%„ýÇ7AÆ­¿ÝLdqQ©ÂÆøö×ýaNÿ{rX(ý`¿Å»Lt¿“úËÔ8/Ùáϲ]Ê÷.Ïñê;é’úv ± ›ø‹Š¶ñ5ä:qš&²‡POc±ÆÂ0^£Ìû>Ðt1(Md”).9n>Ü:^ï]ùþ°žÉIÏeˆ0—{ß‘´ûg×=½Äƒp—ØýO‹³»`R)¤Œ¦j{òôUmRç‰$Ïu”ar”!“ô9d×)H•w,3¸EÿÙ¹LcðЦ›) BÓš»LGÿH›-†q=ØT½ÿ˜
¯·¤™u‹¤¸í´=®êÐr—?‘÷©ï;ÃøÆB7³fü´Õ7míé RþXfžäi1—.o4¤7cd
‰!^`ÏV?ÚAºû=GS#ïâzÆs«+ÿPík蟪Kq©¡®LüCÒ2m½¥$cÇm\Þ}ÂV¯œ¨m<†Ø˜Î}Ø’Ö…ù­÷e߯ €/ï—gi6.®
¾M\<uGU]ow‰å‹3H£¾Ò÷lMñ)o˜ÿiYdÂí`ôò·––ù´«ý¶_@IÇAc Û•=€G
Ø}÷×͈Xd£Ý€]Î~¨ìÃ/îÛXŽþÌ„êj‹ç»®›ìª¹"×_ìˬÛ^2~ì;ŸüÕúCH±áu®ïXXŸ})Ë'¼†øvöPáíÿ$ö§Û—ÿWdUJ<¾\Fk\W´›Ìµ¢Ú #¬²+]À‹Ö©P7‹Bii‹¡Å_!í…Íòÿ¥„g‡j p5¨_"Q8% dKáÜ »ÏPÿ़GGŸNFNºãyN‘²‘ÆG8XS8UÙõ™ð5ú_#›å޽¦ ÐT®òE;¸áC¤ž€8ßš.ÑM\+_€ë°á?o줎Îò5ëõÝÔ$cšâ«¡*l4L¶¶A` /0y+FOx9¶«eÄõÛñ{'Û¯H™"€!*…”{6pj‡ê![14¶XTÌ´ÏÈ>GÄÃ>I±pʨôÃÒ;¥Ã_ó9T/ý"Y=hš>¦§°‰dw í•_”kcë1cxID Ö[Z"^¬/‰áãsÊŠñs–ëÉçÀrkÛÆ'ߊ/'~¡–iP.·ÑQÑ-Ï& g
&[ºÿËT&|¬ÂÇ{ÅÀ2ži:€MæŠÂmRPѨSù_'@2¾¬ ß”]ªh}P+jMŠó/­ew†tàÒ?÷Ù])˰Ü_ë\¯êÇ5Í»5Ù ›TÔÇ ŒÃ
^äPq¡Bæ¸1D“,|,ª´èNI|³ ŸÕÊìR¼üz½F>®ï6Õe¡L«ñê~è›s¯[¹pņŠuaÉb©†±ƒó&OJ5­¿ ³]Tcð³®ïi~ÿà'&Ãä4z`ŠÇ”¡ã›ÓB#+c-Ò7°«—÷‹nÀBèŸÁ†déKDˉ×öK5rjF/›Çí9Þ½1Í\@#WÂO”â²|­VwH>À'š«]fâ$ŠhÞp®´¯öo¨7¸<)%õ³z¾û`†hµ}ªT)ä… ÞðéÓ“ßÝji¸ˆÖ.ìæT‰p<cëètŽ÷þš]=ª)Ž)Ú÷~ÞÈÞZ;ú• 08›×õs\q÷I˜#ͶYÉ%0yåÿ&×%ƒ™uŸd…ãö¤ú­F)"ÞÒwòRѼa&©U7½ÅN{Ãf¸"˜_ b÷'N#ŒèN
).¼—Å1wä_Ž™×ÃWÖý «–êõêi‰Z1âõ×ý˜uöûFœ0ƒsr8”eÔˆ´'¯n¦‘=ƒNóôw ñ“1ÌbI²pXfaVaDHW#YMAë½õÚ†oåÞsI pk¹È4ÿzGh?#Otý§ óùêÒ†®V%#o`ó ^
Xßy®ßN¨Ec~tHÊþ ô¹áFšQåºa«ø)g[@!ñPnÍ»&[Ù´ûïZI@ÖËÒvÃDAnÜzXÔ¶ˆo’—Z¢/V,ù¥õ°"âqƒÊ"F:Æ<là”èÑ¢3”PÀÛµÉʇŒ&œ¸ÐÉâ —‘ 3?]ª½þáÊ€q¡Ùûw­ù%LšøOàHôË%챌Ñ"ðó¼Bƒ õÔ¼‚Ðÿ²„¤de2\?á€B*³\OÏ4)HYî[næL©–JsóÆÒ¼€F¶±–™Êܹ¥µW/‚V«FÄþæ.& Ô=B–ݵxmóPP©U«™¬Múå°­škFÛ%I\ø,£?‡eGTv!æø<Ù ÞÓñy¾…ÀœÐá\Y§¼b„÷ë»2QGºÊi5[Õx©‡å×Ö±‹NѦåþ6Û¨ªQ 7 `f<`(ýû>åp
¯´7¦«öYíë÷R£ 7
ÞæŒOÊœT5}/lÍ’aå£ÕuŒñ SmGwͱNðÇ`رp33x¿·¸´´ýí/Ýk-³+먡$½æ°÷JSíîÛ/º®—&©±½â
·“'936Ê(müu ÓHäÐŒ.Íñø Óßmé¯%¾VK'ª§ZBgà‰ƒ„oö»½w!}+ÔFÖÒ%O^>ü<(X|&œã9sBæ»üeBÄl#7Ë€Ÿt2Q
q‘OeåFNú’Ý—Ú_ahýÉ&pÝ
Ñ8ô¹hÝe÷ *ÜÃCûf‡sx™ÖÊÞQÔåðÆ•‡bŒÍœèl‚€“ÿR˜6g-öJ©Ø ÿ
æúèñ–ò¶Ï*ªÑr™žèpæ ÉlMðM>&¶Ì9¥õOš åøé›cðbdX"Y¯!Z¨K‡É0t ¾‚V»wXH´ÚNDÎØ¾í#.¦Cyº\S¸vÆvL^ÁŠé*£ó~{³>ÜM1õ2@fî øÌòÐ*ÑøôEžm¹‰`õ¹ÍúI?ßws·ÚŠ0®‘ 6mî¢9QëBØp"à}‡¼‡AÕ—73œãIäÛW†•Ç[_;ó½³×ƒŒé7žº("s.ÖÖx·Gõ6`ˆð™YeÇh¥@gÕ¥.ÿ’"ZÛxÄ¢2µ|…”=õ‘Ü`a¬ÃQ@S‘–eCw¡ô~B2½—þ_ôb¯ÔPêa¡-|²ÜÔ]¬‰‘ˆ/1ïi#õ{Ö;ÀÝ *åü2ì†t"9szè}¼|ÈÝnçðÒ¿~JÇ
:>9†œ¤Iöå? ZcDŽÛ7zgÁ5ÊÝDVâ³M*#Å2bÈoHJâr Ê /^´ú?Z•ô™Åax¯ae™L¦†&ù°¦JèŒ5‘›È,°hÓƒ·ÉYí 9>Å|Òäd åÛ7æÊ“çV^ ³®¦–ŽU¾ä>ত/Þ:žà³¯ÿ›U޵e-îdÙ¾2ÖTãÅ) ý3êv§·¶Þ©ãÃÅ´v⯓luzc¼L&x0Üaø9™Ûd¨Àö‰z¿¯óoÙ3üœÕÍ}oƒà ÓA›•a
Q<ž")ÐÉÝ1ÈœZ·oíò¼dpHÄ “10¸¼—?:ßdzn²9$\­ƒ¶´mu¼Z%£Y)rpý(ÞØ0EÍÍë„ Û§¸Ÿ‰:{ è/9dÉRõ2•~ʦ0¿]²X˜2;T³Õ«É7¶7œ£Ã„,BX…;þ<vº,ǯ`¥èt€·Ðx£km}cÀ£À
eÍ«€ýaÓ~ ¾Ã‡€ï!çÖ7šUº|üåÖ×âlQ Í 8¦<8(8×`|{)#NA¨øb$ ÖüÐò÷¸Kߥ`7ø‡&ZÓ›QÃäiDšÇl?Æ5´â¯øbĤÓ7dªŽ ëÙÈËNNª1òœvœðG¾ßT]wõ‡x39e«ECxa%×Ä
¢-[= åÿ”’ÕöFDåºO?5a™mŸ­|.ùvc¶xàN(²U$ã|±'ÌÔåÂHðŸMVÆè¿Ò¯žF«ä€ÙèUozº¿n¾º!4¢ü-Þ¹¡œyo¡‹~!’7½HIc&ŸÂÊZlúÀ¹
µ#”í¢®9Èæöy ®;£ot3ƒÊpOÆ¢éE¸0JüU)‡ÓQ;œ±¤š%X]8UǬ&zÿøÞ?w õo¦P—Ã
H°ö-ú_:±+MOð 3Ū/~wob8AôÇÝ^÷ÁøïHL•þ?YKÚë[¦éîHÊ@&J„øMeDóéTJ àÎÒd’åy‚ê0“öóÉ0:¸´Š£).)ÿ§§núLÑdÁ*d#$—Ë~ëA{°ÄUp½3µ0Œid÷6*d?@d¸Keho-Í`J5ÒÑe»p)ʱí§ƒ"­UN¹óùNV[ÓKÙ%i^ÃnMßnêÛŽZBÅ©K0a^™¸ùJ¨sõ–~Ê¢dʦ¥vÏÃk° §P-í[ukYs¼P2~îxIÃbA8¾°F£ýŸò}üµŸ²Ê9™² ?KM*$Ó¦,
Ù]Ú_=´U@§DHÖõaø}Zü@â8™óÓéhÙ!\פ`Ƶê++PÔà(Mt<˜Næ,[„ aY&-Iü%î§p«ç ¯¾@zÛ}Pì¤Å–Ä9“ÏÒ÷‰ãÞPQQ¾Å|ê*A<ÀäS™òíhcWÍ
¬8Õªw˜¢%}ž£aÿü7ï8l1¤}qIÙÝq¦hΩõ«ŒÊ›ñÛ
Ä|]L' :3’Œ¥7àLCíCdÚd ¾¨ßo5žÞ3;=¢L!Zí×|©Ñç–†ùÅaO©lÍÒ¬Ì2€Á¤ÁÛ¹±œy%Ï$¶1O68•ú¡ë,æõÊ·y€C½ˆo—ñ'ÙA£õÁþ¶ç¹„4fsB&°µИ!xÓü˜ˆ=û‹÷‚{Þ<ƒ‰4Ê‘/Ý´h·m®+˜ ¨QN6œ¼`v¢E¼aëŸ4£J6qQKqÈM Â]ãÈxÜäLO… o‚Õ.¾m=Ú‘Žê$zÙt·o逩º'‡°’êGÔrþ0,_åfyž›œ¸èEñà„"ÀÔnHâx†&¥K6ï˵W ï_êœ6M3ýi/S`n+ã%…ƒ¼LÝF5§“¤×~}Œâa±Å’˜:U¤r“Á,bþ1Wn„,-m×|÷ù—Kr­å‰œ
n×1ø÷X ŽDÕŒU6é©ùaô´3ÙVƒ‚sã…‚K¡2#ç±ÅaÖ¨zúHû.½vø„ ›Ìª8÷G9Ò†Bl •+5:½ð9`ŒY¨WŠÁ:}!u K4ØEÇ—¸k >sRl,NÖ\POyhó"YÐÀÙRi"ØÏ¬ÑXðz94O”’Å‹-eËŽ€•^û_ãJoõ1 “ó
x6zOÈLë+k€é"賈Ͳ(n܉€ !õ 3rhœôÁ/-lcªU´š*Ɀßp˵·þ0Å¿”¦^—®©TwYdz{³£¾ŠºÏï“ÍJ“¼jZ Båÿ%÷æ¡ò¼â©¢ûëK¹—ëUûÌâhŽit«æÝÞ–t-‹7):”º˜)æÕ{ºìµï)ÞkxbÀ~¤æó«}‘·Íõ(ä†Q* mÖ< ÒꌶùP™‹—‚oq"<¤4/D ¬BþþºU)¹=Ú[RÓ_:1z»@òqó@š”–­äFKr$æ9ɺ^6@-˜¦x¶J,q =µ8¦¼{ÍiÍ[™xŠ£ë‚›7 Ëd®*TáæÔf=‹[Þ‚¿Pƒn³\Q1ŒæŽ³À$öM†€²"¡2؉ò—Á(xu>ôìÚÛ¹_—‚çÓ?Xœ NÕ?ñõºÙCÒþ½\GûPe Fä6FÙ‚FʈšôÌëÏ…9åþ^Ðh•XQ]hÜÇÓ"NË1;D°êÜú qÓa*[eß@]…ü{bL@KûJ"è¸Y«©yB±ïJ!;øP?šTedÅ@+'ÐáH÷sòSN§jjYÄò³HsÊ ‰cߌxÄô„£
¾Á
í~öãBý]luú4Àð~Ü0u™h#)Î}R·VL0d¶@¥Ï„°ÈR[ *Gø%Òw âQ´Y*››¸dĪ'·}‚¦²«úóø{›È}ëd£$NÍrˆ%!R|K3jÍšÐ-`ÛÏGÎ iܾ?umLð†ú î¹wIµ„à•ϸ˜` Þq8{"p£¸ƒo‰"õ¸±­ ÉuÞ ÕÌA·ÙŒ3"'~¤Ä&ûÁü˜~`Ú^ˆ`¡X‡r20Éä/A!úˆóÐÇñüÍÿN@ì ç~}>>É9ðþ·êl‹ú¥ i®†á„;WÀëâ½hô¯µ+øÕÀ£h"S·¬­Æ2J‹êð¾ÚÆôe…¡+ØŒ›nK«Õœ‰üõÇ -—æ¿?àÿMÎ-ö„FX?$rBÊUn „÷h{u£-Ĩ.'Œ&µk %§@Œö(SÈD ó8™§æ{íóNpdn,JôVµòbÎeX†õáK²€Ê¡e‘ã–üóÓðæäŒ²æ%YXƒ³‚W{wZéJ£»$t¢¸y_R!”â+š}no EÀE8…MVÚ£¨jÁ×ÿ•ߢû¶KöÈ2ÃEÇŒQ¨¶b*‚¹Oôæ¡ùù—@uçhÛ/úÖò©÷ÀŸM?¹°U/9t¦z2#«w8$|ðQÏ\qc q{aÀú¥8r½y+*•Z£eà¶¿W`sƒ#{x4É|¼{gè¨8ÍS÷Åôúð½s/Ä9äb´p§EðTþÄuÎv¨kr²áŒé\×ñ£Y+½þÇô9ܳ¦!c«¨yK§vª<•ÊêðœDT£w?ÒÿER<T9i°EZ`ÓhÜOБžÚðRÃKš¢#àdá¥t¼)MذvÖ9› =ŒÉEvîæ‡†QaR¤¤Âuzå¯yÖ©‘wƒ(êH{)Ù é\«•¹Ž·£ÌeÆ. ìdljÑ_Qåa¤HüT&ø‡ÞmÍß»wÁ;( ÿv’Lµ%Èá¶Ž$6ÅÌ"ºÌN6Ò„@èÛ$"ƒeäEAÊ$,…¼Ù^£˜}%ZkÙj6³ïå€þâCÑV.sAËÈÿÒêU-XïÍffžð¬Õ7cTOPO÷ó¿cýâÛo·ù47_2äÌö?ò·cèÔìg¸ï_t599¾{³{ÌlÕw‡ï=v›Ñ#¿ßVÇÌØ¾Ç÷Ïq\¬Ÿ²+„Ï¡»[¨™ÙÊ»ºÝñ}±×ü†¯ñEÙ÷7Ó÷õßÓ‘a¿öww›|}°¨êB•±£z¾¦Ç×]…«>cïï9¶ƒîX„_Œ0áÃÞÓ¿ç»>Øë¸/þKõwý{µ`«Ð¿ò‹6låvÛ{¥\þ8÷­~FNŒy¿¡>‹š 9ræS×}ÿø dwÙ;~n²{ñáBwñáîË¿]Ý]6·3üW#"˲ìÒTH+¨VX˜¶©Z,mSÁ4ÒJk0MKåÚ4Ô‰´u((Tt¦{f웡fºô}Ñ¿c{lw뙹 [WÕm7;t¼Ïûaîò‹ìïçKvøxÝ;c‘—< !
£È Gƒ˜®"øOâ$'-@é¾0ÛHñt® ^W´Ã$9’Si¼HÕºvftsɾÈtìÜÃ5ûUè‹oµ â0Qž`&4.^í€TÒÓ
m¡¥ ’ŸŸ¬UsXnîp¥ÓŽ¢Ù@½çÆ¡rÃ`í†_|  cD£^Ü÷Ví´[ l?Þá±h…´Ö?C^;Ä3¸µøŸZ
I|Æ~1éi¤H¤%·ÀËms2 endstream endobj 8 0 obj <</Intent 16 0 R/Name(Layer 1)/Type/OCG/Usage 17 0 R>> endobj 16 0 obj [/View/Design] endobj 17 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 26.0)/Subtype/Artwork>>>> endobj 23 0 obj [22 0 R] endobj 35 0 obj <</CreationDate(D:20181229214738Z)/Creator(Chromium)/ModDate(D:20220226201114-08'00')/Producer(Skia/PDF m64)>> endobj xref
0 36
0000000001 65535 f
0000000002 00000 f
0000000006 00000 f
0000000016 00000 n
0000008344 00000 n
0000008395 00000 n
0000000000 00000 f
0000000159 00000 n
0000049269 00000 n
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000049339 00000 n
0000049370 00000 n
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000013251 00000 n
0000049455 00000 n
0000008757 00000 n
0000013551 00000 n
0000013438 00000 n
0000012367 00000 n
0000012690 00000 n
0000012738 00000 n
0000013322 00000 n
0000013353 00000 n
0000013625 00000 n
0000013769 00000 n
0000015264 00000 n
0000049480 00000 n
trailer <</Size 36/Root 3 0 R/Info 35 0 R/ID[<0637B01B244A4B52B4C4B4C4E2A49950><5DCE82ED017C4F1688E44790ACB41DBA>]>> startxref 49607 %%EOF
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#17541F;}
</style>
<path d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944c0-9.3-1.7-16.2-5.1-20.7
c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2c-4.2-5.4-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2968.8,996.6z
"/>
</g>
<path d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st0" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#17541F;}
</style>
<path d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944c0-9.3-1.7-16.2-5.1-20.7
c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2c-4.2-5.4-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2968.8,996.6z
"/>
</g>
<path d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st0" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#17541F;}
</style>
<rect class="st0" width="3212.8" height="1212.8"/>
<path d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944c0-9.3-1.7-16.2-5.1-20.7
c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2c-4.2-5.4-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2968.8,996.6z
"/>
</g>
<path d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st1" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#17541F;}
</style>
<path class="st0" d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path class="st0" d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path class="st0" d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path class="st0" d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7
c-8.4-2.1-15.7-3.1-22-3.1c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path class="st0" d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path class="st0" d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path class="st0" d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path class="st0" d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path class="st0" d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path class="st0" d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944
c0-9.3-1.7-16.2-5.1-20.7c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path class="st0" d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2
c-4.2-5.4-6.6-11.9-7.1-19.6h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9
v-24.7c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3
c-4-7.3-6-15.5-6-24.6s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8
h19.4v107.8C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16
c-2.6-4.8-6.1-8.5-10.5-11.1c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4
c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1
C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path class="st0" d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35
L2968.8,996.6z"/>
</g>
<path class="st0" d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st1" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

-82
View File
@@ -1,82 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="900"
height="900"
id="svg3923"
sodipodi:docname="square.svg"
inkscape:export-filename="/tmp/test.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata3929">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3927" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3840"
inkscape:window-height="2096"
id="namedview3925"
showgrid="false"
inkscape:zoom="1.1360927"
inkscape:cx="635.07139"
inkscape:cy="606.383"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="g3921" />
<g
transform="matrix(10.638298,0,0,10.638298,106.38298,-206.38301)"
id="g3921">
<defs
id="SvgjsDefs1018" />
<g
id="SvgjsG1019"
featureKey="root"
style="fill:#ffffff" />
<g
id="SvgjsG1020"
featureKey="symbol1"
transform="matrix(0.10341565,0,0,0.10341565,-11.43874,18.048418)"
inkscape:export-filename="/tmp/test.png"
inkscape:export-xdpi="116.02285"
inkscape:export-ydpi="116.02285"
style="fill:#17541f">
<defs
id="defs3911" />
<g
id="g3915">
<path
d="M 231,798 C 227,779 219,741 218,741 49,640 69,465 125,365 c 12,126 235,213 105,367 -1,2 6,26 12,48 26,-44 65,-97 63,-102 C 145,288 645,258 749,16 c 47,234 -24,596 -426,688 -2,1 -73,126 -76,127 0,-2 -30,-1 -26,-11 2,-6 6,-14 10,-22 z M 330,625 C 267,476 452,312 544,271 356,439 324,564 330,625 Z m -104,79 c 51,-59 -9,-160 -45,-193 61,105 57,166 45,193 z"
style="fill:#17541f"
id="path3913"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

+1 -1
View File
@@ -23,7 +23,7 @@ ExecStart=/bin/sh -c '\
[ -n "$PAPERLESS_WEBSERVER_WORKERS" ] && export GRANIAN_WORKERS=$PAPERLESS_WEBSERVER_WORKERS; \
# URL path prefix: only set if PAPERLESS_FORCE_SCRIPT_NAME exists \
[ -n "$PAPERLESS_FORCE_SCRIPT_NAME" ] && export GRANIAN_URL_PATH_PREFIX=$PAPERLESS_FORCE_SCRIPT_NAME; \
exec granian --interface asginl --ws "paperless.asgi:application"'
exec granian --interface asginl --ws --loop uvloop "paperless.asgi:application"'
[Install]
WantedBy=multi-user.target
+565 -374
View File
File diff suppressed because it is too large Load Diff
+27 -27
View File
@@ -1,6 +1,6 @@
{
"name": "paperless-ngx-ui",
"version": "2.20.13",
"version": "2.20.15",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
@@ -11,17 +11,17 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^21.2.4",
"@angular/common": "~21.2.6",
"@angular/compiler": "~21.2.6",
"@angular/core": "~21.2.6",
"@angular/forms": "~21.2.6",
"@angular/localize": "~21.2.6",
"@angular/platform-browser": "~21.2.6",
"@angular/platform-browser-dynamic": "~21.2.6",
"@angular/router": "~21.2.6",
"@angular/cdk": "^21.2.8",
"@angular/common": "~21.2.10",
"@angular/compiler": "~21.2.10",
"@angular/core": "~21.2.10",
"@angular/forms": "~21.2.10",
"@angular/localize": "~21.2.10",
"@angular/platform-browser": "~21.2.10",
"@angular/platform-browser-dynamic": "~21.2.10",
"@angular/router": "~21.2.10",
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
"@ng-select/ng-select": "^21.7.0",
"@ng-select/ng-select": "^21.8.0",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8",
@@ -36,39 +36,39 @@
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"utif": "^3.1.0",
"uuid": "^13.0.0",
"uuid": "^14.0.0",
"zone.js": "^0.16.1"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^21.0.3",
"@angular-builders/jest": "^21.0.3",
"@angular-devkit/core": "^21.2.6",
"@angular-devkit/schematics": "^21.2.6",
"@angular-devkit/core": "^21.2.8",
"@angular-devkit/schematics": "^21.2.8",
"@angular-eslint/builder": "21.3.1",
"@angular-eslint/eslint-plugin": "21.3.1",
"@angular-eslint/eslint-plugin-template": "21.3.1",
"@angular-eslint/schematics": "21.3.1",
"@angular-eslint/template-parser": "21.3.1",
"@angular/build": "^21.2.6",
"@angular/cli": "~21.2.6",
"@angular/compiler-cli": "~21.2.6",
"@codecov/webpack-plugin": "^1.9.1",
"@playwright/test": "^1.59.0",
"@angular/build": "^21.2.8",
"@angular/cli": "~21.2.8",
"@angular/compiler-cli": "~21.2.10",
"@codecov/webpack-plugin": "^2.0.1",
"@playwright/test": "^1.59.1",
"@types/jest": "^30.0.0",
"@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@typescript-eslint/parser": "^8.58.0",
"@typescript-eslint/utils": "^8.58.0",
"eslint": "^10.1.0",
"@types/node": "^25.6.0",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/parser": "^8.59.1",
"@typescript-eslint/utils": "^8.59.1",
"eslint": "^10.2.1",
"jest": "30.3.0",
"jest-environment-jsdom": "^30.3.0",
"jest-junit": "^16.0.0",
"jest-preset-angular": "^16.1.2",
"jest-junit": "^17.0.0",
"jest-preset-angular": "^16.1.4",
"jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.1",
"typescript": "^5.9.3",
"webpack": "^5.105.3"
"webpack": "^5.106.2"
},
"packageManager": "pnpm@10.17.1",
"pnpm": {
+1079 -1053
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -41,7 +41,10 @@ export class AppComponent implements OnInit, OnDestroy {
constructor() {
let anyWindow = window as any
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.mjs'
anyWindow.pdfWorkerSrc = new URL(
'assets/js/pdf.worker.min.mjs',
document.baseURI
).toString()
this.settings.updateAppearanceSettings()
}
@@ -219,7 +222,7 @@ export class AppComponent implements OnInit, OnDestroy {
},
{
anchorId: 'tour.file-tasks',
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
content: $localize`Tasks helps you track background work, what needs attention, and what recently completed.`,
route: '/tasks',
backdropConfig: {
offset: 0,
@@ -337,7 +337,7 @@ describe('SettingsComponent', () => {
.mockImplementation(
(action, type) =>
action === PermissionAction.View &&
type === PermissionType.SystemStatus
type === PermissionType.SystemMonitoring
)
completeSetup()
expect(component['systemStatus']).toEqual(status) // private
@@ -359,7 +359,7 @@ describe('SettingsComponent', () => {
.mockImplementation(
(action, type) =>
action === PermissionAction.View &&
type === PermissionType.SystemStatus
type === PermissionType.SystemMonitoring
)
completeSetup()
component.showSystemStatus()
@@ -652,7 +652,7 @@ export class SettingsComponent
this.permissionsService.isAdmin() ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.SystemStatus
PermissionType.SystemMonitoring
)
)
}
@@ -1,41 +1,16 @@
<pngx-page-header
title="File Tasks"
title="Tasks"
i18n-title
info="File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process."
info="Tasks shows detailed information about document consumption and system tasks."
i18n-info
>
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="visibleTasks.length === 0">
<i-bs name="check2-all" class="me-1"></i-bs>{{dismissButtonText}}
</button>
<div class="form-inline d-flex align-items-center">
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
<span class="input-group-text text-muted" i18n>Filter by</span>
@if (filterTargets.length > 1) {
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{filterTargetName}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
@for (t of filterTargets; track t.id) {
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="filterTargetID = t.id">{{t.name}}</button>
}
</div>
</div>
} @else {
<span class="input-group-text">{{filterTargetName}}</span>
}
@if (filterText?.length) {
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()">
<i-bs width="1em" height="1em" name="x"></i-bs>
</button>
}
<input #filterInput class="form-control form-control-sm" type="text"
(keyup)="filterInputKeyup($event)"
[(ngModel)]="filterText">
</div>
</div>
<div class="form-check form-switch mb-0 ms-2">
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
@@ -48,139 +23,263 @@
<div class="visually-hidden" i18n>Loading...</div>
}
<ng-template let-tasks="tasks" #tasksTemplate>
<table class="table table-striped align-middle border shadow-sm">
<thead>
<tr>
<th scope="col">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" [(ngModel)]="togggleAll" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-tasks"></label>
</div>
</th>
<th scope="col" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
<div class="task-controls mb-3 btn-toolbar align-items-center" role="toolbar">
<div class="task-view-scope btn-group btn-group-sm me-3" role="group">
<input
type="radio"
class="btn-check"
[checked]="selectedSection === TaskSection.All"
id="section-all"
(click)="setSection(TaskSection.All)"
(keydown)="setSection(TaskSection.All)" />
<label class="btn btn-outline-primary" for="section-all">
<ng-container i18n>All</ng-container>
</label>
@for (section of sections; track section) {
<input
type="radio"
class="btn-check"
[checked]="selectedSection === section"
id="section-{{section}}"
(click)="setSection(section)"
(keydown)="setSection(section)" />
<label class="btn btn-outline-primary" for="section-{{section}}">
{{ sectionLabel(section) }}
@if (sectionCount(section) > 0) {
<span class="badge ms-2" [class.bg-danger]="section === TaskSection.NeedsAttention" [class.bg-secondary]="section !== TaskSection.NeedsAttention">{{sectionCount(section)}}</span>
}
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task.id) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
<label class="form-check-label" for="task{{task.id}}"></label>
</div>
</td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<td class="d-none d-lg-table-cell">
@if (task.result?.length > 50) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span>
</div>
}
@if (task.result?.length <= 50) {
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
}
<ng-template #resultPopover>
<pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
&hellip;
}</pre>
@if (task.result?.length > 300) {
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
}
</ng-template>
@if (task.duplicate_documents?.length > 0) {
<div class="small text-warning-emphasis d-flex align-items-center gap-1">
<i-bs class="lh-1" width="1em" height="1em" name="exclamation-triangle"></i-bs>
<span i18n>Duplicate(s) detected</span>
</div>
}
</td>
</label>
}
</div>
<div class="ms-3 me-2 text-muted"><ng-container i18n>Filter by</ng-container>:</div>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary me-3" ngbDropdownToggle>{{selectedTaskTypeLabel}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<button ngbDropdownItem [class.active]="selectedTaskType === null" (click)="setTaskType(null)" i18n>All types</button>
@for (option of taskTypeOptions; track option.value) {
<button ngbDropdownItem [class.active]="selectedTaskType === option.value" [disabled]="isTaskTypeOptionDisabled(option.value)" (click)="setTaskType(option.value)">{{option.label}}</button>
}
</div>
</div>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary me-3" ngbDropdownToggle>{{selectedTriggerSourceLabel}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<button ngbDropdownItem [class.active]="selectedTriggerSource === null" (click)="setTriggerSource(null)" i18n>All sources</button>
@for (option of triggerSourceOptions; track option.value) {
<button ngbDropdownItem [class.active]="selectedTriggerSource === option.value" [disabled]="isTriggerSourceOptionDisabled(option.value)" (click)="setTriggerSource(option.value)">{{option.label}}</button>
}
</div>
</div>
<div class="form-inline d-flex align-items-center flex-grow-1 task-search">
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{filterTargetName}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
@for (t of filterTargets; track t.id) {
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="filterTargetID = t.id">{{t.name}}</button>
}
<td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
</button>
</td>
<td scope="row">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
<i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container>
</button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document) {
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
<i-bs name="file-text" class="me-1"></i-bs><ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>
</div>
</td>
</tr>
</div>
</div>
@if (filterText?.length) {
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()">
<i-bs width="1em" height="1em" name="x"></i-bs>
</button>
}
<input #filterInput class="form-control form-control-sm" type="text"
(keyup)="filterInputKeyup($event)"
[(ngModel)]="filterText">
</div>
</div>
@if (isFiltered) {
<button class="btn btn-link py-0 ms-md-auto" (click)="resetFilters()">
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
</button>
}
<ngb-pagination
class="ms-md-3 mb-0"
[pageSize]="pageSize"
[collectionSize]="totalTasks"
[page]="page"
[maxSize]="5"
[rotate]="true"
size="sm"
aria-label="Tasks pagination"
(pageChange)="setPage($event)">
</ngb-pagination>
</div>
<ng-template let-tasks="tasks" let-section="section" #tasksTemplate>
<div class="section-header d-flex align-items-center justify-content-between mb-2">
<div>
<h5 class="mb-0">{{ sectionLabel(section) }}</h5>
<div class="small text-muted">
<ng-container i18n>{tasks.length, plural, =1 {1 task} other {{{tasks.length}} tasks}}</ng-container>
</div>
</div>
</div>
<div class="card border table-responsive mb-3">
<table class="table table-striped align-middle shadow-sm mb-0 tasks-table">
<thead>
<tr>
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
<th scope="col" class="select-col">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
[id]="'all-tasks-' + section"
[disabled]="tasks.length === 0"
[checked]="areAllSelected(tasks)"
(click)="toggleSection(section, $event); $event.stopPropagation();"
(keydown)="toggleSection(section, $event); $event.stopPropagation();" />
<label class="form-check-label" for="all-tasks-{{section}}"><span class="visually-hidden">Check all</span></label>
</div>
</th>
<th scope="col" class="name-col-header" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell created-col" i18n>Created</th>
@if (sectionShowsResults(section)) {
<th scope="col" class="d-none d-lg-table-cell results-col" i18n>Results</th>
}
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" class="actions-col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (task of tasks; track task.id) {
<tr (click)="toggleSelected(task); $event.stopPropagation();" (keydown)="toggleSelected(task); $event.stopPropagation();">
<td class="select-col">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="task{{task.id}}"
[checked]="selectedTasks.has(task.id)"
(click)="toggleSelected(task); $event.stopPropagation();"
(keydown)="toggleSelected(task); $event.stopPropagation();" />
<label class="form-check-label" for="task{{task.id}}"></label>
</div>
</td>
<td class="overflow-auto name-col">
<div>{{ taskDisplayName(task) }}</div>
<div class="small text-muted">
@if (taskShowsSeparateTypeLabel(task)) {
<span>{{ task.task_type_display }}</span>
<span class="mx-1">&bull;</span>
}
<span>{{ task.trigger_source_display }}</span>
</div>
</td>
<td class="d-none d-lg-table-cell created-col">{{ task.date_created | customDate:'short' }}</td>
@if (sectionShowsResults(section)) {
<td class="d-none d-lg-table-cell results-col">
@if (taskHasLongResultMessage(task)) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ taskResultPreview(task) }}</span>
</div>
}
@if (taskHasResultMessage(task) && !taskHasLongResultMessage(task)) {
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ taskResultMessage(task) }}</span>
}
@if (duplicateDocumentId(task)) {
<div class="small text-warning-emphasis d-flex align-items-center gap-1 mt-1">
<i-bs class="lh-1" width="1em" height="1em" name="exclamation-triangle"></i-bs>
<span>{{ duplicateTaskLabel(task) }}</span>
</div>
}
<ng-template #resultPopover>
<pre class="small mb-0">{{ taskResultPopoverMessage(task) }}@if (taskResultMessageOverflowsPopover(task)) {
&hellip;
}</pre>
@if (taskResultMessageOverflowsPopover(task)) {
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
}
</ng-template>
</td>
}
<td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
</button>
</td>
<td scope="row" class="actions-col">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
<i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container>
</button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document_ids?.[0]) {
<a class="btn btn-sm btn-outline-primary" [routerLink]="['/documents', task.related_document_ids[0]]" (click)="dismissTask(task)">
<i-bs name="file-text" class="me-1"></i-bs><ng-container i18n>Open Document</ng-container>
</a>
}
</ng-container>
</div>
</td>
</tr>
<tr>
<td class="px-2 py-0" [class.border-0]="expandedTask !== task.id" [attr.colspan]="sectionShowsResults(section) ? 5 : 4">
<div #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="task-detail-panel bg-darker small mb-0">
<div class="p-2 p-lg-3 ms-lg-3">
@if (taskHasResultMessage(task)) {
<div class="detail-section mb-3">
<div class="detail-label fs-7 fw-bold text-uppercase text-muted mb-1" i18n>Result message</div>
<pre class="detail-block border border-dark bg-body p-3 rounded-2 mb-0">{{ taskResultMessage(task) }}</pre>
</div>
}
@if (duplicateDocumentId(task); as duplicateDocumentId) {
<div class="detail-section mb-3">
<div class="detail-label fs-7 fw-bold text-uppercase text-muted mb-1" i18n>Duplicate</div>
<div class="detail-block border border-dark bg-body p-3 rounded-2 mb-0">
<div class="d-flex align-items-center justify-content-between gap-3">
<div class="text-break">{{ duplicateTaskLabel(task) }}</div>
<button
class="btn btn-sm btn-outline-primary"
type="button"
(click)="openDuplicateDocument(duplicateDocumentId)">
<ng-container i18n>Open</ng-container>
</button>
</div>
</div>
</div>
}
<div class="row g-3">
<div class="col-12 col-xl-6">
<div class="detail-section h-100">
<div class="detail-label fs-7 fw-bold text-uppercase text-muted mb-1" i18n>Input data</div>
<pre class="detail-block border border-dark bg-body p-3 rounded-2 mb-0">{{ task.input_data | json }}</pre>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="detail-section h-100">
<div class="detail-label fs-7 fw-bold text-uppercase text-muted mb-1" i18n>Result data</div>
<pre class="detail-block border border-dark bg-body p-3 rounded-2 mb-0">{{ (task.result_data ?? {}) | json }}</pre>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
@if (tasks.length > 0) {
<div class="pb-2 pb-sm-0">
<ng-container i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</ng-container>
@if (selectedTasks.size > 0) {
<ng-container i18n>&nbsp;({{selectedTasks.size}} selected)</ng-container>
}
</div>
}
@if (tasks.length > pageSize) {
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
}
</tbody>
</table>
</div>
</ng-template>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange()" (navChange)="beforeTabChange()">
<li ngbNavItem="failed">
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="completed">
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="started">
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="queued">
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav"></div>
@if (visibleSections.length > 0) {
@for (section of visibleSections; track section) {
<div class="mb-4">
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks: tasksForSection(section), section: section}"></ng-container>
</div>
}
} @else {
<div class="alert alert-secondary fst-italic" i18n>No tasks match the current filters.</div>
}
@@ -16,6 +16,39 @@ pre {
cursor: pointer;
}
.tasks-table {
width: 100%;
}
@media (min-width: 992px) {
.tasks-table {
table-layout: fixed;
}
.tasks-table .select-col {
width: 3rem;
}
.tasks-table .created-col {
width: 13rem;
white-space: nowrap;
}
.tasks-table .results-col {
width: 24%;
}
.tasks-table .actions-col {
width: 18rem;
white-space: nowrap;
}
.tasks-table .name-col,
.tasks-table .results-col {
overflow: hidden;
}
}
.btn .spinner-border-sm {
width: 0.8rem;
height: 0.8rem;
@@ -30,10 +63,12 @@ pre {
.input-group .dropdown .btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.z-10 {
z-index: 10;
}
tbody tr:nth-last-child(2) td {
border-bottom: none !important;
}
@@ -9,21 +9,17 @@ import { FormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import {
NgbModal,
NgbModalRef,
NgbModule,
NgbNavItem,
} from '@ng-bootstrap/ng-bootstrap'
import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
import {
PaperlessTask,
PaperlessTaskName,
PaperlessTaskStatus,
PaperlessTaskTriggerSource,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
import { Results } from 'src/app/data/results'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -33,90 +29,131 @@ import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { TasksComponent, TaskTab } from './tasks.component'
import { TasksComponent, TaskSection } from './tasks.component'
const tasks: PaperlessTask[] = [
{
id: 467,
task_id: '11ca1a5b-9f81-442c-b2c8-7e4ae53657f1',
task_file_name: 'test.pdf',
input_data: { filename: 'test.pdf' },
date_created: new Date('2023-03-01T10:26:03.093116Z'),
date_done: new Date('2023-03-01T10:26:07.223048Z'),
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Failed,
result: 'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
trigger_source_display: 'Folder Consume',
status: PaperlessTaskStatus.Failure,
status_display: 'Failure',
result_data: {
error_message:
'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
},
acknowledged: false,
related_document: null,
related_document_ids: [],
},
{
id: 466,
task_id: '10ca1a5b-3c08-442c-b2c8-7e4ae53657f1',
task_file_name: '191092.pdf',
input_data: { filename: '191092.pdf' },
date_created: new Date('2023-03-01T09:26:03.093116Z'),
date_done: new Date('2023-03-01T09:26:07.223048Z'),
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Failed,
result:
'191092.pd: Not consuming 191092.pdf: It is a duplicate of 191092 (#311)',
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
trigger_source_display: 'Folder Consume',
status: PaperlessTaskStatus.Failure,
status_display: 'Failure',
result_data: { duplicate_of: 311 },
acknowledged: false,
related_document: null,
related_document_ids: [],
},
{
id: 465,
task_id: '3612d477-bb04-44e3-985b-ac580dd496d8',
task_file_name: 'Scan Jun 6, 2023 at 3.19 PM.pdf',
input_data: { filename: 'Scan Jun 6, 2023 at 3.19 PM.pdf' },
date_created: new Date('2023-06-06T15:22:05.722323-07:00'),
date_done: new Date('2023-06-06T15:22:14.564305-07:00'),
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
trigger_source_display: 'Folder Consume',
status: PaperlessTaskStatus.Pending,
result: null,
status_display: 'Pending',
result_data: null,
acknowledged: false,
related_document: null,
related_document_ids: [],
},
{
id: 464,
task_id: '2eac4716-2aa6-4dcd-9953-264e11656d7e',
task_file_name: 'paperless-mail-l4dkg8ir',
input_data: { filename: 'paperless-mail-l4dkg8ir' },
date_created: new Date('2023-06-04T11:24:32.898089-07:00'),
date_done: new Date('2023-06-04T11:24:44.678605-07:00'),
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Complete,
result: 'Success. New document id 422 created',
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.EmailConsume,
trigger_source_display: 'Email Consume',
status: PaperlessTaskStatus.Success,
status_display: 'Success',
result_data: { document_id: 422, duplicate_of: 99 },
acknowledged: false,
related_document: 422,
related_document_ids: [422],
},
{
id: 463,
task_id: '28125528-1575-4d6b-99e6-168906e8fa5c',
task_file_name: 'onlinePaymentSummary.pdf',
input_data: { filename: 'onlinePaymentSummary.pdf' },
date_created: new Date('2023-06-01T13:49:51.631305-07:00'),
date_done: new Date('2023-06-01T13:49:54.190220-07:00'),
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Complete,
result: 'Success. New document id 421 created',
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
trigger_source_display: 'Folder Consume',
status: PaperlessTaskStatus.Success,
status_display: 'Success',
result_data: { document_id: 421 },
acknowledged: false,
related_document: 421,
related_document_ids: [421],
},
{
id: 462,
task_id: 'a5b9ca47-0c8e-490f-a04c-6db5d5fc09e5',
task_file_name: 'paperless-mail-_rrpmqk6',
input_data: { filename: 'paperless-mail-_rrpmqk6' },
date_created: new Date('2023-06-07T02:54:35.694916Z'),
date_done: null,
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.EmailConsume,
trigger_source_display: 'Email Consume',
status: PaperlessTaskStatus.Started,
result: null,
status_display: 'Started',
result_data: null,
acknowledged: false,
related_document: null,
related_document_ids: [],
},
{
id: 461,
task_id: 'bb79efb3-1e78-4f31-b4be-0966620b0ce1',
input_data: { dry_run: false, scope: 'global' },
date_created: new Date('2023-06-07T03:54:35.694916Z'),
date_done: null,
task_type: PaperlessTaskType.SanityCheck,
task_type_display: 'Sanity Check',
trigger_source: PaperlessTaskTriggerSource.System,
trigger_source_display: 'System',
status: PaperlessTaskStatus.Started,
status_display: 'Started',
result_data: { issues_found: 0 },
acknowledged: false,
related_document_ids: [],
},
]
const paginatedTasks: Results<PaperlessTask> = {
count: tasks.length,
results: tasks,
}
describe('TasksComponent', () => {
let component: TasksComponent
let fixture: ComponentFixture<TasksComponent>
@@ -165,60 +202,193 @@ describe('TasksComponent', () => {
component = fixture.componentInstance
jest.useFakeTimers()
fixture.detectChanges()
httpTestingController
.expectOne(
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
.flush(tasks)
.flush(paginatedTasks)
httpTestingController
.expectOne(
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '25' &&
req.params.get('page') === '1'
)
.flush(paginatedTasks)
})
it('should display file tasks in 4 tabs by status', () => {
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavItem))
it('should display task sections with counts', () => {
expect(component.selectedSection).toBe(TaskSection.All)
expect(component.selectedTaskType).toBeNull()
expect(component.selectedTriggerSource).toBeNull()
let currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Failed
).length
component.activeTab = TaskTab.Failed
fixture.detectChanges()
expect(tabButtons[0].nativeElement.textContent).toEqual(
`Failed${currentTasksLength}`
const viewScope = fixture.debugElement.query(By.css('.task-view-scope'))
const text = viewScope.nativeElement.textContent
expect(text).toContain('All')
expect(text).toContain('Needs attention')
expect(text).toContain('2')
expect(text).toContain('In progress')
expect(text).toContain('3')
expect(text).toContain('Recently completed')
})
it('should filter visible sections by selected status', () => {
component.setSection(TaskSection.InProgress)
fixture.detectChanges()
expect(component.visibleSections).toEqual([TaskSection.InProgress])
expect(fixture.nativeElement.textContent).toContain('In progress')
expect(fixture.nativeElement.textContent).not.toContain('Recent completed')
})
it('should filter tasks by task type', () => {
component.setSection(TaskSection.InProgress)
component.setTaskType(PaperlessTaskType.SanityCheck)
expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
expect(component.tasksForSection(TaskSection.InProgress)[0].task_type).toBe(
PaperlessTaskType.SanityCheck
)
})
it('should filter tasks by trigger source', () => {
component.setSection(TaskSection.InProgress)
component.setTriggerSource(PaperlessTaskTriggerSource.EmailConsume)
expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
expect(
component.tasksForSection(TaskSection.InProgress)[0].trigger_source
).toBe(PaperlessTaskTriggerSource.EmailConsume)
})
it('should reset all active filters together', () => {
component.setSection(TaskSection.InProgress)
component.setTaskType(PaperlessTaskType.SanityCheck)
component.setTriggerSource(PaperlessTaskTriggerSource.System)
component.filterText = 'system'
jest.advanceTimersByTime(150)
expect(component.isFiltered).toBe(true)
component.resetFilters()
expect(component.selectedSection).toBe(TaskSection.InProgress)
expect(component.selectedTaskType).toBeNull()
expect(component.selectedTriggerSource).toBeNull()
expect(component.filterText).toBe('')
expect(component.isFiltered).toBe(false)
})
it('should keep header controls focused on actions and auto refresh', () => {
fixture.detectChanges()
const header = fixture.debugElement.query(By.css('pngx-page-header'))
const headerText = header.nativeElement.textContent
expect(headerText).toContain('Dismiss visible')
expect(headerText).toContain('Auto refresh')
expect(headerText).not.toContain('All types')
expect(headerText).not.toContain('All sources')
expect(headerText).not.toContain('Reset filters')
})
it('should render the view scope row above the filter bar', () => {
fixture.detectChanges()
const controls = fixture.debugElement.query(By.css('.task-controls'))
const viewScope = controls.query(By.css('.task-view-scope'))
const search = controls.query(By.css('.task-search'))
expect(viewScope).not.toBeNull()
expect(search).not.toBeNull()
expect(
viewScope.nativeElement.compareDocumentPosition(search.nativeElement) &
Node.DOCUMENT_POSITION_FOLLOWING
).toBeTruthy()
})
it('should render pagination controls next to the task filter', () => {
fixture.detectChanges()
const controls = fixture.debugElement.query(By.css('.task-controls'))
const search = controls.query(By.css('.task-search'))
const pagination = controls.query(By.css('ngb-pagination'))
expect(search).not.toBeNull()
expect(pagination).not.toBeNull()
})
it('should load a different task page when pagination changes', () => {
component.setPage(2)
const pageTwoTasks = {
count: 30,
results: [tasks[0]],
}
httpTestingController
.expectOne(
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '25' &&
req.params.get('page') === '2'
)
.flush(pageTwoTasks)
expect(component.page).toBe(2)
expect(component.totalTasks).toBe(30)
expect(component.pagedTasks).toEqual([tasks[0]])
})
it('should expose stable task type options and disable empty ones', () => {
expect(component.taskTypeOptions.map((option) => option.value)).toContain(
PaperlessTaskType.TrainClassifier
)
expect(
fixture.debugElement.queryAll(By.css('table input[type="checkbox"]'))
).toHaveLength(currentTasksLength + 1)
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Complete
).length
component.activeTab = TaskTab.Completed
fixture.detectChanges()
expect(tabButtons[1].nativeElement.textContent).toEqual(
`Complete${currentTasksLength}`
)
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Started
).length
component.activeTab = TaskTab.Started
fixture.detectChanges()
expect(tabButtons[2].nativeElement.textContent).toEqual(
`Started${currentTasksLength}`
)
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Pending
).length
component.activeTab = TaskTab.Queued
fixture.detectChanges()
expect(tabButtons[3].nativeElement.textContent).toEqual(
`Queued${currentTasksLength}`
)
component.isTaskTypeOptionDisabled(PaperlessTaskType.TrainClassifier)
).toBe(true)
expect(
component.isTaskTypeOptionDisabled(PaperlessTaskType.ConsumeFile)
).toBe(false)
})
it('should to go page 1 between tab switch', () => {
component.page = 10
component.duringTabChange()
expect(component.page).toEqual(1)
it('should fall back to the raw selected task type label when no option matches', () => {
component.selectedTaskType = 'unknown_task_type' as PaperlessTaskType
expect(component.selectedTaskTypeLabel).toBe('unknown_task_type')
})
it('should expose stable trigger source options and disable empty ones', () => {
expect(
component.triggerSourceOptions.map((option) => option.value)
).toContain(PaperlessTaskTriggerSource.ApiUpload)
expect(
component.isTriggerSourceOptionDisabled(
PaperlessTaskTriggerSource.ApiUpload
)
).toBe(true)
expect(
component.isTriggerSourceOptionDisabled(
PaperlessTaskTriggerSource.EmailConsume
)
).toBe(false)
})
it('should fall back to the raw selected trigger source label when no option matches', () => {
component.selectedTriggerSource =
'unknown_trigger_source' as PaperlessTaskTriggerSource
expect(component.selectedTriggerSourceLabel).toBe('unknown_trigger_source')
})
it('should support expanding / collapsing one task at a time', () => {
@@ -230,6 +400,31 @@ describe('TasksComponent', () => {
expect(component.expandedTask).toBeUndefined()
})
it('should show structured task details when expanded', () => {
component.setSection(TaskSection.InProgress)
component.expandTask(tasks[6])
fixture.detectChanges()
const detailText = fixture.nativeElement.textContent
expect(detailText).toContain('Input data')
expect(detailText).toContain('Result data')
expect(detailText).toContain('"scope": "global"')
expect(detailText).toContain('"issues_found": 0')
})
it('should show duplicate warnings and duplicate details when present', () => {
component.setSection(TaskSection.Completed)
component.expandTask(tasks[3])
fixture.detectChanges()
const content = fixture.nativeElement.textContent
expect(content).toContain('Duplicate of document #99')
expect(content).toContain('Duplicate')
expect(content).toContain('Open')
})
it('should support dismiss single task', () => {
const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
component.dismissTask(tasks[0])
@@ -240,7 +435,7 @@ describe('TasksComponent', () => {
component.toggleSelected(tasks[0])
component.toggleSelected(tasks[1])
component.toggleSelected(tasks[3])
component.toggleSelected(tasks[3]) // uncheck, for coverage
component.toggleSelected(tasks[3])
const selected = new Set([tasks[0].id, tasks[1].id])
expect(component.selectedTasks).toEqual(selected)
let modal: NgbModalRef
@@ -289,41 +484,70 @@ describe('TasksComponent', () => {
expect(component.selectedTasks.size).toBe(0)
})
it('should support dismiss all tasks', () => {
it('should support dismiss visible tasks', () => {
component.setSection(TaskSection.NeedsAttention)
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
component.dismissTasks()
expect(modal).not.toBeUndefined()
modal.componentInstance.confirmClicked.emit()
expect(dismissSpy).toHaveBeenCalledWith(new Set(tasks.map((t) => t.id)))
expect(dismissSpy).toHaveBeenCalledWith(new Set([467, 466]))
})
it('should support toggle all tasks', () => {
it('should dismiss the currently visible scoped and filtered tasks', () => {
component.setSection(TaskSection.InProgress)
component.setTaskType(PaperlessTaskType.SanityCheck)
component.setTriggerSource(PaperlessTaskTriggerSource.System)
const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
component.dismissTasks()
expect(dismissSpy).toHaveBeenCalledWith(new Set([461]))
})
it('should support toggling a full section', () => {
component.setSection(TaskSection.NeedsAttention)
fixture.detectChanges()
const toggleCheck = fixture.debugElement.query(
By.css('table input[type=checkbox]')
)
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
expect(component.selectedTasks).toEqual(
new Set(
tasks
.filter((t) => t.status === PaperlessTaskStatus.Failed)
.map((t) => t.id)
)
By.css('#all-tasks-needs_attention')
)
expect(toggleCheck).not.toBeNull()
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
expect(component.selectedTasks).toEqual(new Set([467, 466]))
})
it('should remove a full section from selection when toggled off', () => {
component.setSection(TaskSection.NeedsAttention)
component.selectedTasks = new Set([467, 466])
component.toggleSection(TaskSection.NeedsAttention, {
target: { checked: false },
} as unknown as PointerEvent)
expect(component.selectedTasks).toEqual(new Set())
})
it('should support dismiss and open a document', () => {
const routerSpy = jest.spyOn(router, 'navigate')
component.dismissAndGo(tasks[3])
expect(routerSpy).toHaveBeenCalledWith([
'documents',
tasks[3].related_document,
])
const dismissSpy = jest.spyOn(component, 'dismissTask')
fixture.detectChanges()
const openDocumentLink = fixture.debugElement
.queryAll(By.css('a'))
.find((link) => link.nativeElement.textContent.includes('Open Document'))
expect(openDocumentLink).not.toBeNull()
openDocumentLink.triggerEventHandler(
'click',
new MouseEvent('click', { ctrlKey: true })
)
expect(dismissSpy).toHaveBeenCalledWith(tasks[3])
})
it('should auto refresh, allow toggle', () => {
@@ -336,57 +560,127 @@ describe('TasksComponent', () => {
})
it('should filter tasks by file name', () => {
fixture.detectChanges()
const input = fixture.debugElement.query(
By.css('pngx-page-header input[type=text]')
By.css('.task-search input[type=text]')
)
expect(input).not.toBeNull()
input.nativeElement.value = '191092'
input.nativeElement.dispatchEvent(new Event('input'))
jest.advanceTimersByTime(150) // debounce time
jest.advanceTimersByTime(150)
fixture.detectChanges()
expect(component.filterText).toEqual('191092')
expect(
fixture.debugElement.queryAll(By.css('table tbody tr')).length
).toEqual(2) // 1 task x 2 lines
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
1
)
})
it('should match task type and source in name filtering', () => {
component.setSection(TaskSection.InProgress)
component.filterText = 'system'
jest.advanceTimersByTime(150)
expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
expect(component.tasksForSection(TaskSection.InProgress)[0].task_type).toBe(
PaperlessTaskType.SanityCheck
)
})
it('should fall back to task type when filename is unavailable', () => {
component.setSection(TaskSection.InProgress)
fixture.detectChanges()
const nameColumn = fixture.debugElement.queryAll(
By.css('tbody td.name-col')
)
const sanityTaskRow = nameColumn.find((cell) =>
cell.nativeElement.textContent.includes('Sanity Check')
)
expect(sanityTaskRow.nativeElement.textContent).toContain('Sanity Check')
expect(sanityTaskRow.nativeElement.textContent).toContain('System')
})
it('should filter tasks by result', () => {
component.activeTab = TaskTab.Failed
fixture.detectChanges()
component.setSection(TaskSection.NeedsAttention)
component.filterTargetID = 1
fixture.detectChanges()
const input = fixture.debugElement.query(
By.css('pngx-page-header input[type=text]')
By.css('.task-search input[type=text]')
)
expect(input).not.toBeNull()
input.nativeElement.value = 'duplicate'
input.nativeElement.dispatchEvent(new Event('input'))
jest.advanceTimersByTime(150) // debounce time
jest.advanceTimersByTime(150)
fixture.detectChanges()
expect(component.filterText).toEqual('duplicate')
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
2
)
})
it('should prefer explicit reason in the result message', () => {
expect(
fixture.debugElement.queryAll(By.css('table tbody tr')).length
).toEqual(4) // 2 tasks x 2 lines
component.taskResultMessage({
...tasks[0],
result_data: { reason: 'Manual review required', duplicate_of: 311 },
})
).toBe('Manual review required')
})
it('should return null preview and popover text when there is no result message', () => {
expect(component.taskResultPreview(tasks[2])).toBeNull()
expect(component.taskResultPopoverMessage(tasks[2])).toBe('')
expect(component.taskResultMessageOverflowsPopover(tasks[2])).toBe(false)
})
it('should navigate to a duplicate document details page', () => {
const routerSpy = jest.spyOn(router, 'navigate')
component.openDuplicateDocument(99)
expect(routerSpy).toHaveBeenCalledWith(['documents', 99, 'details'])
})
it('should report when a result message overflows the popover limit', () => {
const longMessage = 'x'.repeat(350)
const task = {
...tasks[0],
result_data: { error_message: longMessage },
}
expect(component.taskResultPopoverMessage(task)).toBe(
longMessage.slice(0, 300)
)
expect(component.taskResultMessageOverflowsPopover(task)).toBe(true)
})
it('should support keyboard events for filtering', () => {
fixture.detectChanges()
const input = fixture.debugElement.query(
By.css('pngx-page-header input[type=text]')
By.css('.task-search input[type=text]')
)
expect(input).not.toBeNull()
input.nativeElement.value = '191092'
input.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { key: 'Enter' })
)
expect(component.filterText).toEqual('191092') // no debounce needed
expect(component.filterText).toEqual('191092')
input.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { key: 'Escape' })
)
expect(component.filterText).toEqual('')
})
it('should reset filter and target on tab switch', () => {
component.filterText = '191092'
component.filterTargetID = 1
component.activeTab = TaskTab.Completed
component.beforeTabChange()
expect(component.filterText).toEqual('')
expect(component.filterTargetID).toEqual(0)
it('should keep clearing selection independent from resetting filters', () => {
component.setTaskType(PaperlessTaskType.ConsumeFile)
component.toggleSelected(tasks[0])
expect(component.selectedTasks.size).toBe(1)
component.clearSelection()
expect(component.selectedTasks.size).toBe(0)
expect(component.selectedTaskType).toBe(PaperlessTaskType.ConsumeFile)
expect(component.isFiltered).toBe(true)
})
})
@@ -1,12 +1,11 @@
import { NgTemplateOutlet, SlicePipe } from '@angular/common'
import { JsonPipe, NgTemplateOutlet } from '@angular/common'
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router } from '@angular/router'
import { Router, RouterLink } from '@angular/router'
import {
NgbCollapseModule,
NgbDropdownModule,
NgbModal,
NgbNavModule,
NgbPaginationModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
@@ -20,7 +19,12 @@ import {
takeUntil,
timer,
} from 'rxjs'
import { PaperlessTask } from 'src/app/data/paperless-task'
import {
PaperlessTask,
PaperlessTaskStatus,
PaperlessTaskTriggerSource,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { TasksService } from 'src/app/services/tasks.service'
@@ -29,11 +33,11 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
export enum TaskTab {
Queued = 'queued',
Started = 'started',
export enum TaskSection {
All = 'all',
NeedsAttention = 'needs_attention',
InProgress = 'in_progress',
Completed = 'completed',
Failed = 'failed',
}
enum TaskFilterTargetID {
@@ -46,6 +50,82 @@ const FILTER_TARGETS = [
{ id: TaskFilterTargetID.Result, name: $localize`Result` },
]
const SECTION_LABELS = {
[TaskSection.All]: $localize`All`,
[TaskSection.NeedsAttention]: $localize`Needs attention`,
[TaskSection.InProgress]: $localize`In progress`,
[TaskSection.Completed]: $localize`Recently completed`,
}
const TASK_TYPE_OPTIONS: Array<{
value: PaperlessTaskType
label: string
}> = [
{
value: PaperlessTaskType.ConsumeFile,
label: $localize`Consume File`,
},
{
value: PaperlessTaskType.TrainClassifier,
label: $localize`Train Classifier`,
},
{
value: PaperlessTaskType.SanityCheck,
label: $localize`Sanity Check`,
},
{ value: PaperlessTaskType.MailFetch, label: $localize`Mail Fetch` },
{ value: PaperlessTaskType.LlmIndex, label: $localize`LLM Index` },
{
value: PaperlessTaskType.EmptyTrash,
label: $localize`Empty Trash`,
},
{
value: PaperlessTaskType.CheckWorkflows,
label: $localize`Check Workflows`,
},
{
value: PaperlessTaskType.BulkUpdate,
label: $localize`Bulk Update`,
},
{
value: PaperlessTaskType.ReprocessDocument,
label: $localize`Reprocess Document`,
},
{
value: PaperlessTaskType.BuildShareLink,
label: $localize`Build Share Link`,
},
{
value: PaperlessTaskType.BulkDelete,
label: $localize`Bulk Delete`,
},
]
const TRIGGER_SOURCE_OPTIONS: Array<{
value: PaperlessTaskTriggerSource
label: string
}> = [
{
value: PaperlessTaskTriggerSource.Scheduled,
label: $localize`Scheduled`,
},
{ value: PaperlessTaskTriggerSource.WebUI, label: $localize`Web UI` },
{
value: PaperlessTaskTriggerSource.ApiUpload,
label: $localize`API Upload`,
},
{
value: PaperlessTaskTriggerSource.FolderConsume,
label: $localize`Folder Consume`,
},
{
value: PaperlessTaskTriggerSource.EmailConsume,
label: $localize`Email Consume`,
},
{ value: PaperlessTaskTriggerSource.System, label: $localize`System` },
{ value: PaperlessTaskTriggerSource.Manual, label: $localize`Manual` },
]
@Component({
selector: 'pngx-tasks',
templateUrl: './tasks.component.html',
@@ -54,13 +134,13 @@ const FILTER_TARGETS = [
PageHeaderComponent,
IfPermissionsDirective,
CustomDatePipe,
SlicePipe,
JsonPipe,
FormsModule,
ReactiveFormsModule,
NgTemplateOutlet,
RouterLink,
NgbCollapseModule,
NgbDropdownModule,
NgbNavModule,
NgbPaginationModule,
NgbPopoverModule,
NgxBootstrapIconsModule,
@@ -75,15 +155,22 @@ export class TasksComponent
private readonly router = inject(Router)
private readonly toastService = inject(ToastService)
public activeTab: TaskTab
readonly TaskSection = TaskSection
readonly sections = [
TaskSection.NeedsAttention,
TaskSection.InProgress,
TaskSection.Completed,
]
public selectedTasks: Set<number> = new Set()
public togggleAll: boolean = false
public expandedTask: number
public pageSize: number = 25
public page: number = 1
public autoRefreshEnabled: boolean = true
public readonly pageSize = 25
public page: number = 1
public totalTasks: number = 0
public pagedTasks: PaperlessTask[] = []
public selectedSection: TaskSection = TaskSection.All
public selectedTaskType: PaperlessTaskType | null = null
public selectedTriggerSource: PaperlessTaskTriggerSource | null = null
private _filterText: string = ''
get filterText() {
@@ -95,24 +182,86 @@ export class TasksComponent
public filterTargetID: TaskFilterTargetID = TaskFilterTargetID.Name
public get filterTargetName(): string {
return this.filterTargets.find((t) => t.id == this.filterTargetID).name
return FILTER_TARGETS.find((t) => t.id == this.filterTargetID).name
}
private filterDebounce: Subject<string> = new Subject<string>()
public get filterTargets(): Array<{ id: number; name: string }> {
return [TaskTab.Failed, TaskTab.Completed].includes(this.activeTab)
? FILTER_TARGETS
: FILTER_TARGETS.slice(0, 1)
return FILTER_TARGETS
}
public get taskTypeOptions(): Array<{
value: PaperlessTaskType
label: string
}> {
return TASK_TYPE_OPTIONS
}
public get triggerSourceOptions(): Array<{
value: PaperlessTaskTriggerSource
label: string
}> {
return TRIGGER_SOURCE_OPTIONS
}
public get selectedTaskTypeLabel(): string {
if (this.selectedTaskType === null) {
return $localize`All types`
}
return (
this.taskTypeOptions.find(
(option) => option.value === this.selectedTaskType
)?.label ?? this.selectedTaskType
)
}
public get selectedTriggerSourceLabel(): string {
if (this.selectedTriggerSource === null) {
return $localize`All sources`
}
return (
this.triggerSourceOptions.find(
(option) => option.value === this.selectedTriggerSource
)?.label ?? this.selectedTriggerSource
)
}
get dismissButtonText(): string {
return this.selectedTasks.size > 0
? $localize`Dismiss selected`
: $localize`Dismiss all`
: $localize`Dismiss visible`
}
get visibleSections(): TaskSection[] {
const sections =
this.selectedSection === TaskSection.All
? this.sections
: [this.selectedSection]
return sections.filter(
(section) => this.tasksForSection(section).length > 0
)
}
get visibleTasks(): PaperlessTask[] {
return this.visibleSections.flatMap((section) =>
this.tasksForSection(section)
)
}
get isFiltered(): boolean {
return (
this.selectedTaskType !== null ||
this.selectedTriggerSource !== null ||
this._filterText.length > 0
)
}
ngOnInit() {
this.tasksService.reload()
this.reloadPage()
timer(5000, 5000)
.pipe(
filter(() => this.autoRefreshEnabled),
@@ -120,6 +269,7 @@ export class TasksComponent
)
.subscribe(() => {
this.tasksService.reload()
this.reloadPage(false)
})
this.filterDebounce
@@ -129,7 +279,10 @@ export class TasksComponent
distinctUntilChanged(),
filter((query) => !query.length || query.length > 2)
)
.subscribe((query) => (this._filterText = query))
.subscribe((query) => {
this._filterText = query
this.clearSelection()
})
}
ngOnDestroy() {
@@ -143,20 +296,25 @@ export class TasksComponent
dismissTasks(task: PaperlessTask = undefined) {
let tasks = task ? new Set([task.id]) : new Set(this.selectedTasks.values())
if (!task && tasks.size == 0)
tasks = new Set(this.tasksService.allFileTasks.map((t) => t.id))
if (!task && tasks.size == 0) {
tasks = new Set(this.visibleTasks.map((t) => t.id))
}
if (tasks.size > 1) {
let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.title = $localize`Confirm Dismiss All`
modal.componentInstance.messageBold = $localize`Dismiss all ${tasks.size} tasks?`
modal.componentInstance.title = $localize`Confirm Dismiss`
modal.componentInstance.messageBold = $localize`Dismiss ${tasks.size} tasks?`
modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Dismiss`
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false
modal.close()
this.tasksService.dismissTasks(tasks).subscribe({
next: () => {
this.reloadPage(false)
},
error: (e) => {
this.toastService.showError($localize`Error dismissing tasks`, e)
modal.componentInstance.buttonsEnabled = true
@@ -164,8 +322,11 @@ export class TasksComponent
})
this.clearSelection()
})
} else {
} else if (tasks.size === 1) {
this.tasksService.dismissTasks(tasks).subscribe({
next: () => {
this.reloadPage(false)
},
error: (e) =>
this.toastService.showError($localize`Error dismissing task`, e),
})
@@ -173,11 +334,6 @@ export class TasksComponent
}
}
dismissAndGo(task: PaperlessTask) {
this.dismissTask(task)
this.router.navigate(['documents', task.related_document])
}
expandTask(task: PaperlessTask) {
this.expandedTask = this.expandedTask == task.id ? undefined : task.id
}
@@ -188,75 +344,177 @@ export class TasksComponent
: this.selectedTasks.add(task.id)
}
get currentTasks(): PaperlessTask[] {
let tasks: PaperlessTask[] = []
switch (this.activeTab) {
case TaskTab.Queued:
tasks = this.tasksService.queuedFileTasks
break
case TaskTab.Started:
tasks = this.tasksService.startedFileTasks
break
case TaskTab.Completed:
tasks = this.tasksService.completedFileTasks
break
case TaskTab.Failed:
tasks = this.tasksService.failedFileTasks
break
toggleSection(section: TaskSection, event: PointerEvent) {
const sectionTasks = this.tasksForSection(section)
if ((event.target as HTMLInputElement).checked) {
sectionTasks.forEach((task) => this.selectedTasks.add(task.id))
} else {
sectionTasks.forEach((task) => this.selectedTasks.delete(task.id))
}
if (this._filterText.length) {
tasks = tasks.filter((t) => {
if (this.filterTargetID == TaskFilterTargetID.Name) {
return t.task_file_name
.toLowerCase()
.includes(this._filterText.toLowerCase())
} else if (this.filterTargetID == TaskFilterTargetID.Result) {
return t.result.toLowerCase().includes(this._filterText.toLowerCase())
}
})
}
return tasks
}
toggleAll(event: PointerEvent) {
if ((event.target as HTMLInputElement).checked) {
this.selectedTasks = new Set(this.currentTasks.map((t) => t.id))
} else {
this.clearSelection()
areAllSelected(tasks: PaperlessTask[]): boolean {
return (
tasks.length > 0 && tasks.every((task) => this.selectedTasks.has(task.id))
)
}
taskDisplayName(task: PaperlessTask): string {
return task.input_data?.filename?.toString() || task.task_type_display
}
taskShowsSeparateTypeLabel(task: PaperlessTask): boolean {
return this.taskDisplayName(task) !== task.task_type_display
}
taskResultMessage(task: PaperlessTask): string | null {
if (!task.result_data) {
return null
}
const documentId = task.result_data?.['document_id']
if (typeof documentId === 'number') {
return $localize`Success. New document id ${documentId} created`
}
const reason = task.result_data?.['reason']
if (typeof reason === 'string') {
return reason
}
const duplicateOf = task.result_data?.['duplicate_of']
if (typeof duplicateOf === 'number') {
return $localize`Duplicate of document #${duplicateOf}`
}
const errorMessage = task.result_data?.['error_message']
if (typeof errorMessage === 'string') {
return errorMessage
}
return null
}
taskResultPreview(task: PaperlessTask): string | null {
const message = this.taskResultMessage(task)
if (!message) {
return null
}
return message.length > 50 ? `${message.slice(0, 50)}...` : message
}
taskHasLongResultMessage(task: PaperlessTask): boolean {
return (this.taskResultMessage(task)?.length ?? 0) > 50
}
taskHasResultMessage(task: PaperlessTask): boolean {
return !!this.taskResultMessage(task)
}
duplicateDocumentId(task: PaperlessTask): number | null {
const duplicateOf = task.result_data?.['duplicate_of']
return typeof duplicateOf === 'number' ? duplicateOf : null
}
duplicateTaskLabel(task: PaperlessTask): string {
return $localize`Duplicate of document #${this.duplicateDocumentId(task)}`
}
openDuplicateDocument(documentId: number) {
this.router.navigate(['documents', documentId, 'details'])
}
taskResultPopoverMessage(task: PaperlessTask): string {
return this.taskResultMessage(task)?.slice(0, 300) ?? ''
}
taskResultMessageOverflowsPopover(task: PaperlessTask): boolean {
return (this.taskResultMessage(task)?.length ?? 0) > 300
}
tasksForSection(section: TaskSection): PaperlessTask[] {
let tasks = this.pagedTasks.filter((task) =>
this.taskBelongsToSection(task, section)
)
return tasks.filter((task) => this.taskMatchesCurrentFilters(task))
}
sectionLabel(section: TaskSection): string {
return SECTION_LABELS[section]
}
sectionCount(section: TaskSection): number {
return this.pagedTasks.filter((task) =>
this.taskBelongsToSection(task, section)
).length
}
sectionShowsResults(section: TaskSection): boolean {
return section !== TaskSection.InProgress
}
setSection(section: TaskSection) {
this.selectedSection = section
this.clearSelection()
}
setTaskType(taskType: PaperlessTaskType | null) {
this.selectedTaskType = taskType
this.clearSelection()
}
setTriggerSource(triggerSource: PaperlessTaskTriggerSource | null) {
this.selectedTriggerSource = triggerSource
this.clearSelection()
}
taskTypeOptionCount(taskType: PaperlessTaskType | null): number {
return this.tasksForOptionCounts({ taskType }).length
}
triggerSourceOptionCount(
triggerSource: PaperlessTaskTriggerSource | null
): number {
return this.tasksForOptionCounts({ triggerSource }).length
}
isTaskTypeOptionDisabled(taskType: PaperlessTaskType | null): boolean {
return this.taskTypeOptionCount(taskType) === 0
}
isTriggerSourceOptionDisabled(
triggerSource: PaperlessTaskTriggerSource | null
): boolean {
return this.triggerSourceOptionCount(triggerSource) === 0
}
clearSelection() {
this.togggleAll = false
this.selectedTasks.clear()
}
duringTabChange() {
this.page = 1
}
beforeTabChange() {
this.resetFilter()
this.filterTargetID = TaskFilterTargetID.Name
}
get activeTabLocalized(): string {
switch (this.activeTab) {
case TaskTab.Queued:
return $localize`queued`
case TaskTab.Started:
return $localize`started`
case TaskTab.Completed:
return $localize`completed`
case TaskTab.Failed:
return $localize`failed`
setPage(page: number) {
if (this.page === page) {
return
}
this.page = page
this.clearSelection()
this.reloadPage()
}
public resetFilter() {
this._filterText = ''
}
public resetFilters() {
this.selectedTaskType = null
this.selectedTriggerSource = null
this.resetFilter()
this.clearSelection()
}
filterInputKeyup(event: KeyboardEvent) {
if (event.key == 'Enter') {
this._filterText = (event.target as HTMLInputElement).value
@@ -264,4 +522,116 @@ export class TasksComponent
this.resetFilter()
}
}
private taskBelongsToSection(
task: PaperlessTask,
section: TaskSection
): boolean {
switch (section) {
case TaskSection.NeedsAttention:
return [
PaperlessTaskStatus.Failure,
PaperlessTaskStatus.Revoked,
].includes(task.status)
case TaskSection.InProgress:
return [
PaperlessTaskStatus.Pending,
PaperlessTaskStatus.Started,
].includes(task.status)
case TaskSection.Completed:
return task.status === PaperlessTaskStatus.Success
}
}
private taskMatchesCurrentFilters(task: PaperlessTask): boolean {
return this.taskMatchesFilters(task, {
taskType: this.selectedTaskType,
triggerSource: this.selectedTriggerSource,
})
}
private taskMatchesFilters(
task: PaperlessTask,
{
taskType,
triggerSource,
}: {
taskType: PaperlessTaskType | null
triggerSource: PaperlessTaskTriggerSource | null
}
): boolean {
if (taskType !== null && task.task_type !== taskType) {
return false
}
if (triggerSource !== null && task.trigger_source !== triggerSource) {
return false
}
if (!this._filterText.length) {
return true
}
const query = this._filterText.toLowerCase()
if (this.filterTargetID == TaskFilterTargetID.Name) {
return [
this.taskDisplayName(task),
task.task_type_display,
task.trigger_source_display,
]
.filter(Boolean)
.some((value) => value.toLowerCase().includes(query))
}
return this.taskResultMessage(task)?.toLowerCase().includes(query) ?? false
}
private tasksForOptionCounts({
taskType = this.selectedTaskType,
triggerSource = this.selectedTriggerSource,
}: {
taskType?: PaperlessTaskType | null
triggerSource?: PaperlessTaskTriggerSource | null
}): PaperlessTask[] {
const sections =
this.selectedSection === TaskSection.All
? this.sections
: [this.selectedSection]
return this.pagedTasks.filter(
(task) =>
sections.some((section) => this.taskBelongsToSection(task, section)) &&
this.taskMatchesFilters(task, { taskType, triggerSource })
)
}
private reloadPage(resetToFirstPage: boolean = false) {
if (resetToFirstPage) {
this.page = 1
}
this.loading = true
this.tasksService
.list(this.page, this.pageSize, { acknowledged: false })
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
this.pagedTasks = result.results
this.totalTasks = result.count
this.loading = false
if (
this.page > 1 &&
this.pagedTasks.length === 0 &&
this.totalTasks > 0
) {
this.page -= 1
this.reloadPage()
}
},
error: () => {
this.loading = false
},
})
}
}
@@ -8,10 +8,8 @@
[ngClass]="{ 'slim': slimSidebarEnabled, 'col-auto col-md-3 col-lg-2 col-xxxl-1' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
routerLink="/dashboard"
tourAnchor="tour.intro">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" height="1.5em" fill="currentColor">
<path
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
transform="translate(0 0)" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1.5em" height="1.5em" fill="currentColor">
<path d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
</svg>
<div class="ms-2 ms-md-3 d-inline-block" [class.d-md-none]="slimSidebarEnabled">
@if (customAppTitle?.length) {
@@ -294,13 +292,13 @@
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }"
tourAnchor="tour.file-tasks">
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
ngbPopover="Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="list-task"></i-bs><span><ng-container i18n>File Tasks</ng-container>@if (tasksService.failedFileTasks.length > 0) {
<span><span class="badge bg-danger ms-2 d-inline">{{tasksService.failedFileTasks.length}}</span></span>
<i-bs class="me-2" name="list-task"></i-bs><span><ng-container i18n>Tasks</ng-container>@if (tasksService.needsAttentionTasks.length > 0) {
<span><span class="badge bg-danger ms-2 d-inline">{{tasksService.needsAttentionTasks.length}}</span></span>
}</span>
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0 d-none d-md-block">{{tasksService.failedFileTasks.length}}</span>
@if (tasksService.needsAttentionTasks.length > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0 d-none d-md-block">{{tasksService.needsAttentionTasks.length}}</span>
}
</a>
</li>
@@ -94,12 +94,18 @@ main {
}
.sidebar.slim:not(.animating) {
transition: none;
li.nav-item span,
.sidebar-heading span {
display: none;
}
}
.sidebar.slim:not(.animating) ~ main.col-slim {
transition: none;
}
.sidebar.animating {
li.nav-item span,
.sidebar-heading span {
@@ -36,6 +36,7 @@ import { RemoteVersionService } from 'src/app/services/rest/remote-version.servi
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SearchService } from 'src/app/services/rest/search.service'
import { SettingsService } from 'src/app/services/settings.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
@@ -97,6 +98,7 @@ describe('AppFrameComponent', () => {
let savedViewSpy
let modalService: NgbModal
let maybeRefreshSpy
let tasksService: TasksService
beforeEach(async () => {
TestBed.configureTestingModule({
@@ -174,6 +176,7 @@ describe('AppFrameComponent', () => {
openDocumentsService = TestBed.inject(OpenDocumentsService)
modalService = TestBed.inject(NgbModal)
router = TestBed.inject(Router)
tasksService = TestBed.inject(TasksService)
jest
.spyOn(settingsService, 'displayName', 'get')
@@ -444,6 +447,16 @@ describe('AppFrameComponent', () => {
expect(maybeRefreshSpy).toHaveBeenCalled()
})
it('should show tasks badge for needs-attention tasks', () => {
jest
.spyOn(tasksService, 'needsAttentionTasks', 'get')
.mockReturnValue([{} as any, {} as any])
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Tasks2')
})
it('should indicate attributes management availability when any permission is granted', () => {
jest
.spyOn(permissionsService, 'currentUserCan')
@@ -8,10 +8,21 @@
<div class="chat-messages font-monospace small">
@for (message of messages; track message) {
<div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'">
<span class="p-2 m-2" [class.bg-dark]="message.role === 'user'">
{{ message.content }}
@if (message.isStreaming) { <span class="blinking-cursor">|</span> }
</span>
<div class="p-2 m-2" [class.bg-dark]="message.role === 'user'">
<span>
{{ message.content }}
@if (message.isStreaming) { <span class="blinking-cursor">|</span> }
</span>
@if (message.role === 'assistant' && message.references?.length) {
<div class="chat-references list-group mt-3">
@for (reference of message.references; track reference.id) {
<a class="list-group-item list-group-item-action text-primary" [routerLink]="['/documents', reference.id]">
<i-bs width="0.9em" height="0.9em" name="file-text" class="me-1"></i-bs><span>{{ reference.title }}</span>
</a>
}
</div>
}
</div>
</div>
}
<div #scrollAnchor></div>
@@ -7,6 +7,10 @@
overflow-y: auto;
}
.chat-references {
font-family: var(--bs-font-sans-serif);
}
.dropdown-toggle::after {
display: none;
}
@@ -3,9 +3,13 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ElementRef } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NavigationEnd, Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs'
import { ChatService } from 'src/app/services/chat.service'
import {
CHAT_METADATA_DELIMITER,
ChatService,
} from 'src/app/services/chat.service'
import { ChatComponent } from './chat.component'
describe('ChatComponent', () => {
@@ -18,7 +22,11 @@ describe('ChatComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [NgxBootstrapIconsModule.pick(allIcons), ChatComponent],
imports: [
NgxBootstrapIconsModule.pick(allIcons),
RouterTestingModule,
ChatComponent,
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
@@ -84,6 +92,57 @@ describe('ChatComponent', () => {
expect(component.messages[1].isStreaming).toBe(false)
})
it('should parse references from the metadata trailer without showing it', () => {
component.input = 'Hello'
component.sendMessage()
mockStream$.next(
`Hi there${CHAT_METADATA_DELIMITER}{"references":[{"id":42,"title":"Bread Recipe"}]}`
)
jest.advanceTimersByTime(1000)
expect(component.messages[1].content).toBe('Hi there')
expect(component.messages[1].references).toEqual([
{ id: 42, title: 'Bread Recipe' },
])
})
it('should render document reference links under assistant messages', () => {
component.input = 'Hello'
component.sendMessage()
mockStream$.next(
`Hi there${CHAT_METADATA_DELIMITER}{"references":[{"id":42,"title":"Bread Recipe"}]}`
)
jest.advanceTimersByTime(1000)
fixture.detectChanges()
const link = fixture.nativeElement.querySelector('.chat-references a')
expect(link.textContent).toContain('Bread Recipe')
expect(link.getAttribute('href')).toContain('/documents/42')
})
it('should remove delimiter fragments that were already streamed', () => {
component.input = 'Hello'
component.sendMessage()
mockStream$.next(`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`)
jest.advanceTimersByTime(1000)
expect(component.messages[1].content).toBe(
`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`
)
mockStream$.next(
`Hi there${CHAT_METADATA_DELIMITER}{"references":[{"id":42,"title":"Bread Recipe"}]}`
)
jest.advanceTimersByTime(1000)
expect(component.messages[1].content).toBe('Hi there')
expect(component.messages[1].references).toEqual([
{ id: 42, title: 'Bread Recipe' },
])
})
it('should handle errors during streaming', () => {
component.input = 'Hello'
component.sendMessage()
@@ -1,16 +1,21 @@
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NavigationEnd, Router } from '@angular/router'
import { NavigationEnd, Router, RouterModule } from '@angular/router'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { filter, map } from 'rxjs'
import { ChatMessage, ChatService } from 'src/app/services/chat.service'
import {
ChatMessage,
ChatService,
parseChatResponse,
} from 'src/app/services/chat.service'
@Component({
selector: 'pngx-chat',
imports: [
FormsModule,
ReactiveFormsModule,
RouterModule,
NgxBootstrapIconsModule,
NgbDropdownModule,
],
@@ -70,13 +75,24 @@ export class ChatComponent implements OnInit {
this.messages.push(assistantMessage)
this.loading = true
let lastPartialLength = 0
let lastVisibleContent = ''
this.chatService.streamChat(this.documentId, this.input).subscribe({
next: (chunk) => {
const delta = chunk.substring(lastPartialLength)
lastPartialLength = chunk.length
this.enqueueTypewriter(delta, assistantMessage)
const nextResponse = parseChatResponse(chunk)
if (nextResponse.content.length < lastVisibleContent.length) {
this.resetTypewriter(assistantMessage, nextResponse.content)
lastVisibleContent = nextResponse.content
} else {
const visibleDelta = nextResponse.content.substring(
lastVisibleContent.length
)
lastVisibleContent = nextResponse.content
this.enqueueTypewriter(visibleDelta, assistantMessage)
}
assistantMessage.references = nextResponse.references
},
error: () => {
assistantMessage.content += '\n\n⚠️ Error receiving response.'
@@ -93,6 +109,13 @@ export class ChatComponent implements OnInit {
this.input = ''
}
private resetTypewriter(message: ChatMessage, content: string): void {
this.typewriterBuffer = []
this.typewriterActive = false
message.content = content
this.scrollToBottom()
}
enqueueTypewriter(chunk: string, message: ChatMessage): void {
if (!chunk) return
@@ -86,7 +86,7 @@
<div class="selected-icon">
@if (addedRelativeDate) {
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearAddedRelativeDate()">
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
<i-bs width="1em" height="1em" name="check" class="variant-unfocused text-dark"></i-bs>
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
</a>
}
@@ -26,6 +26,7 @@ import {
} from 'src/app/data/matching-model'
import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { PermissionsService } from 'src/app/services/permissions.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -87,6 +88,7 @@ describe('EditDialogComponent', () => {
let component: TestComponent
let fixture: ComponentFixture<TestComponent>
let tagService: TagService
let permissionsService: PermissionsService
let settingsService: SettingsService
let activeModal: NgbActiveModal
let httpTestingController: HttpTestingController
@@ -118,8 +120,10 @@ describe('EditDialogComponent', () => {
}).compileComponents()
tagService = TestBed.inject(TagService)
permissionsService = TestBed.inject(PermissionsService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = currentUser
permissionsService.initialize([], currentUser as any)
activeModal = TestBed.inject(NgbActiveModal)
httpTestingController = TestBed.inject(HttpTestingController)
@@ -226,6 +230,25 @@ describe('EditDialogComponent', () => {
expect(updateSpy).toHaveBeenCalled()
})
it('should not submit owner or permissions for non-owner edits', () => {
component.object = tag
component.dialogMode = EditDialogMode.EDIT
component.ngOnInit()
component.objectForm.get('name').setValue('Updated tag')
component.save()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}tags/${tag.id}/`
)
expect(req.request.method).toEqual('PUT')
expect(req.request.body.name).toEqual('Updated tag')
expect(req.request.body.owner).toEqual(tag.owner)
expect(req.request.body.set_permissions).toBeUndefined()
req.flush({})
})
it('should create an object on save in edit mode', () => {
const createSpy = jest.spyOn(tagService, 'create')
component.dialogMode = EditDialogMode.CREATE
@@ -18,6 +18,7 @@ import { ObjectWithId } from 'src/app/data/object-with-id'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user'
import { PermissionsService } from 'src/app/services/permissions.service'
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -42,6 +43,7 @@ export abstract class EditDialogComponent<
protected activeModal = inject(NgbActiveModal)
protected userService = inject(UserService)
protected settingsService = inject(SettingsService)
protected permissionsService = inject(PermissionsService)
users: User[]
@@ -69,10 +71,6 @@ export abstract class EditDialogComponent<
ngOnInit(): void {
if (this.object != null && this.dialogMode !== EditDialogMode.CREATE) {
if ((this.object as ObjectWithPermissions).permissions) {
this.object['set_permissions'] = this.object['permissions']
}
this.object['permissions_form'] = {
owner: (this.object as ObjectWithPermissions).owner,
set_permissions: (this.object as ObjectWithPermissions).permissions,
@@ -151,18 +149,28 @@ export abstract class EditDialogComponent<
return Object.assign({}, this.objectForm.value)
}
protected shouldSubmitPermissions(): boolean {
return (
this.dialogMode === EditDialogMode.CREATE ||
this.permissionsService.currentUserOwnsObject(this.object)
)
}
save() {
this.error = null
const formValues = this.getFormValues()
const permissionsObject: PermissionsFormObject =
this.objectForm.get('permissions_form')?.value
if (permissionsObject) {
if (permissionsObject && this.shouldSubmitPermissions()) {
formValues.owner = permissionsObject.owner
formValues.set_permissions = permissionsObject.set_permissions
delete formValues.permissions_form
}
delete formValues.permissions_form
var newObject = Object.assign(Object.assign({}, this.object), formValues)
if (!this.shouldSubmitPermissions()) {
delete newObject['set_permissions']
}
var serverResponse: Observable<T>
switch (this.dialogMode) {
case EditDialogMode.CREATE:
@@ -9,7 +9,6 @@ import { first } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -39,7 +38,6 @@ export class UserEditDialogComponent
implements OnInit
{
private toastService = inject(ToastService)
private permissionsService = inject(PermissionsService)
private groupsService: GroupService
groups: Group[]
@@ -205,6 +205,20 @@ describe('TagsComponent', () => {
expect(component.value).toEqual([2, 1])
})
it('should not duplicate parents when adding sibling nested tags', () => {
const root: Tag = { id: 1, name: 'root' }
const parent: Tag = { id: 2, name: 'parent', parent: 1 }
const leafA: Tag = { id: 3, name: 'leaf-a', parent: 2 }
const leafB: Tag = { id: 4, name: 'leaf-b', parent: 2 }
component.tags = [root, parent, leafA, leafB]
component.value = []
component.addTag(3)
component.addTag(4)
expect(component.value).toEqual([3, 2, 1, 4])
})
it('should return ancestors from root to parent using getParentChain', () => {
const root: Tag = { id: 1, name: 'root' }
const mid: Tag = { id: 2, name: 'mid', parent: 1 }
@@ -153,11 +153,13 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
}
public onAdd(tag: Tag) {
if (tag.parent) {
if (tag?.parent) {
// add all parents recursively
const parent = this.getTag(tag.parent)
this.value = [...this.value, parent.id]
this.onAdd(parent)
if (parent && !this.value.includes(parent.id)) {
this.value = [...this.value, parent.id]
this.onAdd(parent)
}
}
}
@@ -1,22 +1,22 @@
@if (customLogo) {
<img src="{{customLogo}}" [class]="getClasses()" [attr.style]="'height:'+height" />
} @else {
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'height:'+height">
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
<g class="text" style="fill:#000">
<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>
<path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/>
<path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/>
<path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" transform="translate(0)"/>
<rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/>
<path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/>
<path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/>
<path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/>
<rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/>
<path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/>
<path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/>
<polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5 2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 " transform="translate(0)"/>
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
</g>
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2670 860" [attr.style]="'height:'+height">
<path class="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/>
<g class="text" style="fill: #000;">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/>
</g>
</svg>
}
@@ -9,6 +9,15 @@ describe('PngxPdfViewerComponent', () => {
let fixture: ComponentFixture<PngxPdfViewerComponent>
let component: PngxPdfViewerComponent
const setBaseHref = (href: string) => {
let base = document.querySelector('base')
if (!base) {
base = document.createElement('base')
document.head.appendChild(base)
}
base.setAttribute('href', href)
}
const initComponent = async (src = 'test.pdf') => {
component.src = src
fixture.detectChanges()
@@ -24,6 +33,10 @@ describe('PngxPdfViewerComponent', () => {
component = fixture.componentInstance
})
afterEach(() => {
setBaseHref('/')
})
it('loads a document and emits events', async () => {
const loadSpy = jest.fn()
const renderedSpy = jest.fn()
@@ -33,7 +46,7 @@ describe('PngxPdfViewerComponent', () => {
await initComponent()
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(
'/assets/js/pdf.worker.min.mjs'
new URL('assets/js/pdf.worker.min.mjs', document.baseURI).toString()
)
const isVisible = (component as any).findController.onIsPageVisible as
| (() => boolean)
@@ -46,6 +59,19 @@ describe('PngxPdfViewerComponent', () => {
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
})
it('resolves the worker source relative to the document base URI', async () => {
setBaseHref('/paperless/')
await initComponent()
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(
new URL('assets/js/pdf.worker.min.mjs', document.baseURI).toString()
)
expect(pdfjs.GlobalWorkerOptions.workerSrc).toContain(
'/paperless/assets/js/pdf.worker.min.mjs'
)
})
it('initializes single-page viewer and disables text layer', async () => {
component.renderMode = PdfRenderMode.Single
component.selectable = false
@@ -1,8 +1,10 @@
import {
AfterViewInit,
Component,
DOCUMENT,
ElementRef,
EventEmitter,
inject,
Input,
OnChanges,
OnDestroy,
@@ -39,6 +41,8 @@ import {
export class PngxPdfViewerComponent
implements AfterViewInit, OnChanges, OnDestroy
{
private readonly document = inject<Document>(DOCUMENT)
@Input() src!: PdfSource
@Input() page?: number
@Output() pageChange = new EventEmitter<number>()
@@ -166,7 +170,10 @@ export class PngxPdfViewerComponent
this.lastFindQuery = ''
this.loadingTask?.destroy()
GlobalWorkerOptions.workerSrc = '/assets/js/pdf.worker.min.mjs'
GlobalWorkerOptions.workerSrc = new URL(
'assets/js/pdf.worker.min.mjs',
this.document.baseURI
).toString()
this.loadingTask = getDocument(this.src)
try {
@@ -272,7 +279,7 @@ export class PngxPdfViewerComponent
if (!this.hasRenderedPage) {
return
}
const query = this.searchQuery.trim()
const query = this.searchQuery?.trim()
if (query === this.lastFindQuery) {
return
}
@@ -133,28 +133,28 @@ describe('PermissionsSelectComponent', () => {
expect(viewInput.nativeElement.disabled).toBeFalsy()
})
it('should treat system status as view-only', () => {
it('should treat system monitoring as view-only', () => {
component.ngOnInit()
fixture.detectChanges()
expect(
component.isActionSupported(
PermissionType.SystemStatus,
PermissionType.SystemMonitoring,
PermissionAction.View
)
).toBeTruthy()
expect(
component.isActionSupported(
PermissionType.SystemStatus,
PermissionType.SystemMonitoring,
PermissionAction.Change
)
).toBeFalsy()
const changeInput = fixture.debugElement.query(
By.css('input#SystemStatus_Change')
By.css('input#SystemMonitoring_Change')
)
const viewInput = fixture.debugElement.query(
By.css('input#SystemStatus_View')
By.css('input#SystemMonitoring_View')
)
expect(changeInput.nativeElement.disabled).toBeTruthy()
@@ -261,7 +261,7 @@ export class PermissionsSelectComponent
// Global statistics and system status only support view
if (
type === PermissionType.GlobalStatistics ||
type === PermissionType.SystemStatus
type === PermissionType.SystemMonitoring
) {
return action === PermissionAction.View
}
@@ -25,19 +25,19 @@
</div>
}
@if (suggestions?.suggested_tags.length > 0) {
<small class="list-group-item text-uppercase text-muted small">Tags</small>
<small class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="tags"></i-bs>Tags</small>
@for (tag of suggestions.suggested_tags; track tag) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addTag.emit(tag)" i18n>{{ tag }}</button>
}
}
@if (suggestions?.suggested_document_types.length > 0) {
<div class="list-group-item text-uppercase text-muted small">Document Types</div>
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="hash"></i-bs>Document Types</div>
@for (type of suggestions.suggested_document_types; track type) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addDocumentType.emit(type)" i18n>{{ type }}</button>
}
}
@if (suggestions?.suggested_correspondents.length > 0) {
<div class="list-group-item text-uppercase text-muted small">Correspondents</div>
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="person"></i-bs>Correspondents</div>
@for (correspondent of suggestions.suggested_correspondents; track correspondent) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)" i18n>{{ correspondent }}</button>
}
@@ -142,6 +142,31 @@
}
</ng-template>
</dd>
<dt i18n>Recent Task Activity <span class="small text-muted fw-light">({{status.tasks.summary.days}} days)</span></dt>
<dd class="mb-0">
@if (status.tasks.summary.total_count > 0) {
<ul class="list-group border-light mt-2">
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span class="small"><ng-container i18n>Total</ng-container>:</span>
<span class="badge bg-light rounded-pill">{{status.tasks.summary.total_count}}</span>
</li>
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span class="small"><ng-container i18n>Successful</ng-container>:</span>
<span class="badge bg-primary rounded-pill">{{status.tasks.summary.success_count}}</span>
</li>
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span class="small"><ng-container i18n>Failed</ng-container>:</span>
<span class="badge bg-danger rounded-pill">{{status.tasks.summary.failure_count}}</span>
</li>
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span class="small"><ng-container i18n>Pending</ng-container>:</span>
<span class="badge bg-warning rounded-pill">{{status.tasks.summary.pending_count}}</span>
</li>
</ul>
} @else {
<span class="small text-muted" i18n>No recent tasks</span>
}
</dd>
</dl>
</div>
</div>
@@ -159,25 +184,11 @@
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="indexStatus" triggers="click mouseenter:mouseleave">
{{status.tasks.index_status}}
@if (status.tasks.index_status === 'OK') {
@if (isStale(status.tasks.index_last_modified)) {
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
} @else {
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
}
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
} @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
}
</button>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskName.IndexOptimize)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.IndexOptimize)">
<i-bs name="play-fill" class="me-1"></i-bs>
<ng-container i18n>Run Task</ng-container>
</button>
}
}
</dd>
<ng-template #indexStatus>
@if (status.tasks.index_status === 'OK') {
@@ -203,10 +214,10 @@
}
</button>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskName.TrainClassifier)) {
@if (isRunning(PaperlessTaskType.TrainClassifier)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.TrainClassifier)">
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskType.TrainClassifier)">
<i-bs name="play-fill" class="me-1"></i-bs>
<ng-container i18n>Run Task</ng-container>
</button>
@@ -237,10 +248,10 @@
}
</button>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskName.SanityCheck)) {
@if (isRunning(PaperlessTaskType.SanityCheck)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.SanityCheck)">
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskType.SanityCheck)">
<i-bs name="play-fill" class="me-1"></i-bs>
<ng-container i18n>Run Task</ng-container>
</button>
@@ -285,10 +296,10 @@
}
</button>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskName.LLMIndexUpdate)) {
@if (isRunning(PaperlessTaskType.LlmIndex)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.LLMIndexUpdate)">
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskType.LlmIndex)">
<i-bs name="play-fill" class="me-1"></i-bs>
<ng-container i18n>Run Task</ng-container>
</button>
@@ -25,7 +25,7 @@ import {
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject, of, throwError } from 'rxjs'
import { PaperlessTaskName } from 'src/app/data/paperless-task'
import { PaperlessTaskType } from 'src/app/data/paperless-task'
import {
InstallType,
SystemStatus,
@@ -71,6 +71,13 @@ const status: SystemStatus = {
llmindex_status: SystemStatusItemStatus.OK,
llmindex_last_modified: new Date().toISOString(),
llmindex_error: null,
summary: {
days: 30,
total_count: 12,
pending_count: 1,
success_count: 10,
failure_count: 1,
},
},
}
@@ -138,9 +145,9 @@ describe('SystemStatusDialogComponent', () => {
})
it('should check if task is running', () => {
component.runTask(PaperlessTaskName.IndexOptimize)
expect(component.isRunning(PaperlessTaskName.IndexOptimize)).toBeTruthy()
expect(component.isRunning(PaperlessTaskName.SanityCheck)).toBeFalsy()
component.runTask(PaperlessTaskType.SanityCheck)
expect(component.isRunning(PaperlessTaskType.SanityCheck)).toBeTruthy()
expect(component.isRunning(PaperlessTaskType.TrainClassifier)).toBeFalsy()
})
it('should support running tasks, refresh status and show toasts', () => {
@@ -151,22 +158,22 @@ describe('SystemStatusDialogComponent', () => {
// fail first
runSpy.mockReturnValue(throwError(() => new Error('error')))
component.runTask(PaperlessTaskName.IndexOptimize)
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
component.runTask(PaperlessTaskType.SanityCheck)
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskType.SanityCheck)
expect(toastErrorSpy).toHaveBeenCalledWith(
`Failed to start task ${PaperlessTaskName.IndexOptimize}, see the logs for more details`,
`Failed to start task ${PaperlessTaskType.SanityCheck}, see the logs for more details`,
expect.any(Error)
)
// succeed
runSpy.mockReturnValue(of({}))
getStatusSpy.mockReturnValue(of(status))
component.runTask(PaperlessTaskName.IndexOptimize)
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
component.runTask(PaperlessTaskType.SanityCheck)
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskType.SanityCheck)
expect(getStatusSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
`Task ${PaperlessTaskName.IndexOptimize} started`
`Task ${PaperlessTaskType.SanityCheck} started`
)
})
@@ -8,7 +8,7 @@ import {
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, takeUntil } from 'rxjs'
import { PaperlessTaskName } from 'src/app/data/paperless-task'
import { PaperlessTaskType } from 'src/app/data/paperless-task'
import {
SystemStatus,
SystemStatusItemStatus,
@@ -49,14 +49,14 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
private settingsService = inject(SettingsService)
public SystemStatusItemStatus = SystemStatusItemStatus
public PaperlessTaskName = PaperlessTaskName
public PaperlessTaskType = PaperlessTaskType
public status: SystemStatus
public frontendVersion: string = environment.version
public versionMismatch: boolean = false
public copied: boolean = false
private runningTasks: Set<PaperlessTaskName> = new Set()
private runningTasks: Set<PaperlessTaskType> = new Set()
private unsubscribeNotifier: Subject<any> = new Subject()
get currentUserIsSuperUser(): boolean {
@@ -107,11 +107,11 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
return now.getTime() - date.getTime() > hours * 60 * 60 * 1000
}
public isRunning(taskName: PaperlessTaskName): boolean {
public isRunning(taskName: PaperlessTaskType): boolean {
return this.runningTasks.has(taskName)
}
public runTask(taskName: PaperlessTaskName) {
public runTask(taskName: PaperlessTaskType) {
this.runningTasks.add(taskName)
this.toastService.showInfo(`Task ${taskName} started`)
this.tasksService.run(taskName).subscribe({
@@ -1,5 +1,5 @@
<pngx-page-header title="Dashboard" [subTitle]="subtitle" i18n-title tourAnchor="tour.dashboard">
<pngx-logo extra_classes="d-none d-md-block mt-n2" height="3.5rem"></pngx-logo>
<pngx-logo extra_classes="d-none d-md-block mt-n2" height="3rem"></pngx-logo>
</pngx-page-header>
<div class="row">
@@ -54,10 +54,10 @@
@if (isFinished(status)) {
<div>
@if (status.documentId) {
<button class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
<a class="btn btn-sm btn-outline-primary btn-open" [routerLink]="['/documents', status.documentId]" (click)="dismiss(status)">
<small i18n>Open document</small>
<i-bs name="arrow-right-short"></i-bs>
</button>
</a>
}
</div>
}
@@ -1381,6 +1381,7 @@ describe('DocumentDetailComponent', () => {
it('should get suggestions', () => {
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
const aiSuggestionsSpy = jest.spyOn(documentService, 'getAiSuggestions')
suggestionsSpy.mockReturnValue(
of({
tags: [42, 43],
@@ -1391,6 +1392,35 @@ describe('DocumentDetailComponent', () => {
)
initNormally()
expect(suggestionsSpy).toHaveBeenCalled()
expect(aiSuggestionsSpy).not.toHaveBeenCalled()
expect(component.suggestions).toEqual({
tags: [42, 43],
suggested_tags: [],
suggested_document_types: [],
suggested_correspondents: [],
})
})
it('should get AI suggestions when AI is enabled', () => {
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
const aiSuggestionsSpy = jest.spyOn(documentService, 'getAiSuggestions')
aiSuggestionsSpy.mockReturnValue(
of({
tags: [42, 43],
suggested_tags: [],
suggested_document_types: [],
suggested_correspondents: [],
})
)
initNormally()
expect(suggestionsSpy).not.toHaveBeenCalled()
expect(aiSuggestionsSpy).toHaveBeenCalled()
expect(component.suggestions).toEqual({
tags: [42, 43],
suggested_tags: [],
@@ -2191,10 +2221,20 @@ describe('DocumentDetailComponent', () => {
expect(createElementSpy).toHaveBeenCalledWith('iframe')
expect(appendChildSpy).toHaveBeenCalledWith(mockIframe)
expect(createObjectURLSpy).toHaveBeenCalledWith(blob)
expect(mockIframe.style).toEqual({
position: 'fixed',
right: '0',
bottom: '0',
width: '0',
height: '0',
border: '0',
visibility: 'hidden',
})
if (mockIframe.onload) {
mockIframe.onload({} as any)
}
tick()
expect(mockContentWindow.focus).toHaveBeenCalled()
expect(mockContentWindow.print).toHaveBeenCalled()
@@ -2307,6 +2347,7 @@ describe('DocumentDetailComponent', () => {
mockIframe.onload(new Event('load'))
}
tick()
tick(200)
if (expectToast) {
@@ -981,8 +981,10 @@ export class DocumentDetailComponent
getSuggestions() {
this.suggestionsLoading = true
this.documentsService
.getSuggestions(this.documentId)
const suggestionsObservable = this.aiEnabled
? this.documentsService.getAiSuggestions(this.documentId)
: this.documentsService.getSuggestions(this.documentId)
suggestionsObservable
.pipe(
first(),
takeUntil(this.unsubscribeNotifier),
@@ -1871,31 +1873,39 @@ export class DocumentDetailComponent
next: (blob) => {
const blobUrl = URL.createObjectURL(blob)
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.style.position = 'fixed'
iframe.style.right = '0'
iframe.style.bottom = '0'
iframe.style.width = '0'
iframe.style.height = '0'
iframe.style.border = '0'
iframe.style.visibility = 'hidden'
iframe.src = blobUrl
document.body.appendChild(iframe)
iframe.onload = () => {
try {
iframe.contentWindow.focus()
iframe.contentWindow.print()
iframe.contentWindow.onafterprint = () => {
document.body.removeChild(iframe)
URL.revokeObjectURL(blobUrl)
timer(0).subscribe(() => {
try {
iframe.contentWindow.focus()
iframe.contentWindow.print()
iframe.contentWindow.onafterprint = () => {
document.body.removeChild(iframe)
URL.revokeObjectURL(blobUrl)
}
} catch (err) {
// FF throws cross-origin error on onafterprint
const isCrossOriginAfterPrintError =
err instanceof DOMException &&
err.message.includes('onafterprint')
if (!isCrossOriginAfterPrintError) {
this.toastService.showError($localize`Print failed.`, err)
}
timer(100).subscribe(() => {
// delay to avoid FF print failure
document.body.removeChild(iframe)
URL.revokeObjectURL(blobUrl)
})
}
} catch (err) {
// FF throws cross-origin error on onafterprint
const isCrossOriginAfterPrintError =
err instanceof DOMException &&
err.message.includes('onafterprint')
if (!isCrossOriginAfterPrintError) {
this.toastService.showError($localize`Print failed.`, err)
}
timer(100).subscribe(() => {
// delay to avoid FF print failure
document.body.removeChild(iframe)
URL.revokeObjectURL(blobUrl)
})
}
})
}
},
error: () => {
@@ -43,7 +43,7 @@
</div>
<p class="card-text">
@if (document) {
@if (document.__search_hit__ && document.__search_hit__.highlights) {
@if (hasSearchHighlights) {
<span [innerHtml]="document.__search_hit__.highlights"></span>
}
@for (highlight of searchNoteHighlights; track highlight) {
@@ -52,7 +52,7 @@
<span [innerHtml]="highlight"></span>
</span>
}
@if (!document.__search_hit__?.score) {
@if (shouldShowContentFallback) {
<span class="result-content">{{contentTrimmed}}</span>
}
} @else {
@@ -65,7 +65,9 @@
}
}
span ::ng-deep .match {
.card-text ::ng-deep .match,
.card-text ::ng-deep b {
font-weight: normal;
color: black;
background-color: rgb(255, 211, 66);
}
@@ -127,6 +127,19 @@ describe('DocumentCardLargeComponent', () => {
expect(component.searchNoteHighlights).toContain('<span>bananas</span>')
})
it('should fall back to document content when a search hit has no highlights', () => {
component.document.__search_hit__ = {
score: 0.9,
rank: 1,
highlights: '',
note_highlights: null,
}
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Cupcake ipsum')
expect(component.shouldShowContentFallback).toBe(true)
})
it('should try to close the preview on mouse leave', () => {
component.popupPreview = {
close: jest.fn(),
@@ -164,6 +164,17 @@ export class DocumentCardLargeComponent
)
}
get hasSearchHighlights() {
return Boolean(this.document?.__search_hit__?.highlights?.trim()?.length)
}
get shouldShowContentFallback() {
return (
this.document?.__search_hit__?.score == null ||
(!this.hasSearchHighlights && this.searchNoteHighlights.length === 0)
)
}
get notesEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
}
+2 -2
View File
@@ -55,12 +55,12 @@ export const ConfigCategory = {
}
export const LLMEmbeddingBackendConfig = {
OPENAI: 'openai',
OPENAI_LIKE: 'openai-like',
HUGGINGFACE: 'huggingface',
}
export const LLMBackendConfig = {
OPENAI: 'openai',
OPENAI_LIKE: 'openai-like',
OLLAMA: 'ollama',
}
+50 -33
View File
@@ -1,49 +1,66 @@
import { Document } from './document'
import { ObjectWithId } from './object-with-id'
export enum PaperlessTaskType {
Auto = 'auto_task',
ScheduledTask = 'scheduled_task',
ManualTask = 'manual_task',
}
export enum PaperlessTaskName {
ConsumeFile = 'consume_file',
TrainClassifier = 'train_classifier',
SanityCheck = 'check_sanity',
IndexOptimize = 'index_optimize',
LLMIndexUpdate = 'llmindex_update',
SanityCheck = 'sanity_check',
MailFetch = 'mail_fetch',
LlmIndex = 'llm_index',
EmptyTrash = 'empty_trash',
CheckWorkflows = 'check_workflows',
BulkUpdate = 'bulk_update',
ReprocessDocument = 'reprocess_document',
BuildShareLink = 'build_share_link',
BulkDelete = 'bulk_delete',
}
export enum PaperlessTaskTriggerSource {
Scheduled = 'scheduled',
WebUI = 'web_ui',
ApiUpload = 'api_upload',
FolderConsume = 'folder_consume',
EmailConsume = 'email_consume',
System = 'system',
Manual = 'manual',
}
export enum PaperlessTaskStatus {
Pending = 'PENDING',
Started = 'STARTED',
Complete = 'SUCCESS',
Failed = 'FAILURE',
Pending = 'pending',
Started = 'started',
Success = 'success',
Failure = 'failure',
Revoked = 'revoked',
}
export interface PaperlessTask extends ObjectWithId {
type: PaperlessTaskType
status: PaperlessTaskStatus
acknowledged: boolean
task_id: string
task_file_name: string
task_name: PaperlessTaskName
task_type: PaperlessTaskType
task_type_display: string
trigger_source: PaperlessTaskTriggerSource
trigger_source_display: string
status: PaperlessTaskStatus
status_display: string
date_created: Date
date_started?: Date
date_done?: Date
result?: string
related_document?: number
duplicate_documents?: Document[]
duration_seconds?: number
wait_time_seconds?: number
input_data: Record<string, any>
result_data?: Record<string, any>
related_document_ids: number[]
acknowledged: boolean
owner?: number
}
export interface PaperlessTaskSummary {
task_type: PaperlessTaskType
total_count: number
pending_count: number
success_count: number
failure_count: number
avg_duration_seconds: number | null
avg_wait_time_seconds: number | null
last_run: Date | null
last_success: Date | null
last_failure: Date | null
}

Some files were not shown because too many files have changed in this diff Show More