Merge remote-tracking branch 'paperless/dev' into feature-consume-eml
No files matched your search
@@ -0,0 +1,254 @@
|
||||
import logging
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
from typing import Final
|
||||
from typing import List
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from common import get_log_level
|
||||
|
||||
logger = logging.getLogger("cleanup-tags")
|
||||
|
||||
|
||||
class GithubContainerRegistry:
|
||||
def __init__(
|
||||
self,
|
||||
session: requests.Session,
|
||||
token: str,
|
||||
owner_or_org: str,
|
||||
):
|
||||
self._session: requests.Session = session
|
||||
self._token = token
|
||||
self._owner_or_org = owner_or_org
|
||||
# https://docs.github.com/en/rest/branches/branches
|
||||
self._BRANCHES_ENDPOINT = "https://api.github.com/repos/{OWNER}/{REPO}/branches"
|
||||
if self._owner_or_org == "paperless-ngx":
|
||||
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-an-organization
|
||||
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
|
||||
# https://docs.github.com/en/rest/packages#delete-package-version-for-an-organization
|
||||
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
|
||||
else:
|
||||
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-the-authenticated-user
|
||||
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
|
||||
# https://docs.github.com/en/rest/packages#delete-a-package-version-for-the-authenticated-user
|
||||
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
|
||||
|
||||
def __enter__(self):
|
||||
self._session.headers.update(
|
||||
{
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"Authorization": f"token {self._token}",
|
||||
},
|
||||
)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if "Accept" in self._session.headers:
|
||||
del self._session.headers["Accept"]
|
||||
if "Authorization" in self._session.headers:
|
||||
del self._session.headers["Authorization"]
|
||||
|
||||
def _read_all_pages(self, endpoint):
|
||||
internal_data = []
|
||||
|
||||
while True:
|
||||
resp = self._session.get(endpoint)
|
||||
if resp.status_code == 200:
|
||||
internal_data += resp.json()
|
||||
if "next" in resp.links:
|
||||
endpoint = resp.links["next"]["url"]
|
||||
else:
|
||||
logger.debug("Exiting pagination loop")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Request to {endpoint} return HTTP {resp.status_code}")
|
||||
break
|
||||
|
||||
return internal_data
|
||||
|
||||
def get_branches(self, repo: str):
|
||||
endpoint = self._BRANCHES_ENDPOINT.format(OWNER=self._owner_or_org, REPO=repo)
|
||||
internal_data = self._read_all_pages(endpoint)
|
||||
return internal_data
|
||||
|
||||
def filter_branches_by_name_pattern(self, branch_data, pattern: str):
|
||||
matches = {}
|
||||
|
||||
for branch in branch_data:
|
||||
if branch["name"].startswith(pattern):
|
||||
matches[branch["name"]] = branch
|
||||
|
||||
return matches
|
||||
|
||||
def get_package_versions(
|
||||
self,
|
||||
package_name: str,
|
||||
package_type: str = "container",
|
||||
) -> List:
|
||||
package_name = quote(package_name, safe="")
|
||||
endpoint = self._PACKAGES_VERSIONS_ENDPOINT.format(
|
||||
ORG=self._owner_or_org,
|
||||
PACKAGE_TYPE=package_type,
|
||||
PACKAGE_NAME=package_name,
|
||||
)
|
||||
|
||||
internal_data = self._read_all_pages(endpoint)
|
||||
|
||||
return internal_data
|
||||
|
||||
def filter_packages_by_tag_pattern(self, package_data, pattern: str):
|
||||
matches = {}
|
||||
|
||||
for package in package_data:
|
||||
if "metadata" in package and "container" in package["metadata"]:
|
||||
container_metadata = package["metadata"]["container"]
|
||||
if "tags" in container_metadata:
|
||||
container_tags = container_metadata["tags"]
|
||||
for tag in container_tags:
|
||||
if tag.startswith(pattern):
|
||||
matches[tag] = package
|
||||
break
|
||||
|
||||
return matches
|
||||
|
||||
def filter_packages_untagged(self, package_data):
|
||||
matches = {}
|
||||
|
||||
for package in package_data:
|
||||
if "metadata" in package and "container" in package["metadata"]:
|
||||
container_metadata = package["metadata"]["container"]
|
||||
if "tags" in container_metadata:
|
||||
container_tags = container_metadata["tags"]
|
||||
if not len(container_tags):
|
||||
matches[package["name"]] = package
|
||||
|
||||
return matches
|
||||
|
||||
def delete_package_version(self, package_name, package_data):
|
||||
package_name = quote(package_name, safe="")
|
||||
endpoint = self._PACKAGE_VERSION_DELETE_ENDPOINT.format(
|
||||
ORG=self._owner_or_org,
|
||||
PACKAGE_TYPE=package_data["metadata"]["package_type"],
|
||||
PACKAGE_NAME=package_name,
|
||||
PACKAGE_VERSION_ID=package_data["id"],
|
||||
)
|
||||
resp = self._session.delete(endpoint)
|
||||
if resp.status_code != 204:
|
||||
logger.warning(
|
||||
f"Request to delete {endpoint} returned HTTP {resp.status_code}",
|
||||
)
|
||||
|
||||
|
||||
def _main():
|
||||
parser = ArgumentParser(
|
||||
description="Using the GitHub API locate and optionally delete container"
|
||||
" tags which no longer have an associated feature branch",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--delete",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="If provided, actually delete the container tags",
|
||||
)
|
||||
|
||||
# TODO There's a lot of untagged images, do those need to stay for anything?
|
||||
parser.add_argument(
|
||||
"--untagged",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="If provided, delete untagged containers as well",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--loglevel",
|
||||
default="info",
|
||||
help="Configures the logging level",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
level=get_log_level(args),
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
format="%(asctime)s %(levelname)-8s %(message)s",
|
||||
)
|
||||
|
||||
repo_owner: Final[str] = os.environ["GITHUB_REPOSITORY_OWNER"]
|
||||
repo: Final[str] = os.environ["GITHUB_REPOSITORY"]
|
||||
gh_token: Final[str] = os.environ["GITHUB_TOKEN"]
|
||||
|
||||
with requests.session() as sess:
|
||||
with GithubContainerRegistry(sess, gh_token, repo_owner) as gh_api:
|
||||
all_branches = gh_api.get_branches("paperless-ngx")
|
||||
logger.info(f"Located {len(all_branches)} branches of {repo_owner}/{repo} ")
|
||||
|
||||
feature_branches = gh_api.filter_branches_by_name_pattern(
|
||||
all_branches,
|
||||
"feature-",
|
||||
)
|
||||
logger.info(f"Located {len(feature_branches)} feature branches")
|
||||
|
||||
for package_name in ["paperless-ngx", "paperless-ngx/builder/cache/app"]:
|
||||
|
||||
all_package_versions = gh_api.get_package_versions(package_name)
|
||||
logger.info(
|
||||
f"Located {len(all_package_versions)} versions of package {package_name}",
|
||||
)
|
||||
|
||||
packages_tagged_feature = gh_api.filter_packages_by_tag_pattern(
|
||||
all_package_versions,
|
||||
"feature-",
|
||||
)
|
||||
logger.info(
|
||||
f'Located {len(packages_tagged_feature)} versions of package {package_name} tagged "feature-"',
|
||||
)
|
||||
|
||||
untagged_packages = gh_api.filter_packages_untagged(
|
||||
all_package_versions,
|
||||
)
|
||||
logger.info(
|
||||
f"Located {len(untagged_packages)} untagged versions of package {package_name}",
|
||||
)
|
||||
|
||||
to_delete = list(
|
||||
set(packages_tagged_feature.keys()) - set(feature_branches.keys()),
|
||||
)
|
||||
logger.info(
|
||||
f"Located {len(to_delete)} versions of package {package_name} to delete",
|
||||
)
|
||||
|
||||
for tag_to_delete in to_delete:
|
||||
package_version_info = packages_tagged_feature[tag_to_delete]
|
||||
|
||||
if args.delete:
|
||||
logger.info(
|
||||
f"Deleting {tag_to_delete} (id {package_version_info['id']})",
|
||||
)
|
||||
gh_api.delete_package_version(
|
||||
package_name,
|
||||
package_version_info,
|
||||
)
|
||||
|
||||
else:
|
||||
logger.info(
|
||||
f"Would delete {tag_to_delete} (id {package_version_info['id']})",
|
||||
)
|
||||
|
||||
if args.untagged:
|
||||
logger.info(f"Deleting untagged packages of {package_name}")
|
||||
for to_delete_name in untagged_packages:
|
||||
to_delete_version = untagged_packages[to_delete_name]
|
||||
logger.info(f"Deleting id {to_delete_version['id']}")
|
||||
if args.delete:
|
||||
gh_api.delete_package_version(
|
||||
package_name,
|
||||
to_delete_version,
|
||||
)
|
||||
else:
|
||||
logger.info("Leaving untagged images untouched")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_main()
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import logging
|
||||
from argparse import ArgumentError
|
||||
|
||||
|
||||
def get_image_tag(
|
||||
@@ -25,3 +27,18 @@ def get_cache_image_tag(
|
||||
rebuilds, generally almost instant for the same version
|
||||
"""
|
||||
return f"ghcr.io/{repo_name}/builder/cache/{pkg_name}:{pkg_version}"
|
||||
|
||||
|
||||
def get_log_level(args) -> int:
|
||||
levels = {
|
||||
"critical": logging.CRITICAL,
|
||||
"error": logging.ERROR,
|
||||
"warn": logging.WARNING,
|
||||
"warning": logging.WARNING,
|
||||
"info": logging.INFO,
|
||||
"debug": logging.DEBUG,
|
||||
}
|
||||
level = levels.get(args.loglevel.lower())
|
||||
if level is None:
|
||||
level = logging.INFO
|
||||
return level
|
||||
@@ -50,7 +50,6 @@ def _main():
|
||||
|
||||
# Default output values
|
||||
version = None
|
||||
git_tag = None
|
||||
extra_config = {}
|
||||
|
||||
if args.package in pipfile_data["default"]:
|
||||
@@ -59,12 +58,6 @@ def _main():
|
||||
pkg_version = pkg_data["version"].split("==")[-1]
|
||||
version = pkg_version
|
||||
|
||||
# Based on the package, generate the expected Git tag name
|
||||
if args.package == "pikepdf":
|
||||
git_tag = f"v{pkg_version}"
|
||||
elif args.package == "psycopg2":
|
||||
git_tag = pkg_version.replace(".", "_")
|
||||
|
||||
# Any extra/special values needed
|
||||
if args.package == "pikepdf":
|
||||
extra_config["qpdf_version"] = build_json["qpdf"]["version"]
|
||||
@@ -72,8 +65,6 @@ def _main():
|
||||
elif args.package in build_json:
|
||||
version = build_json[args.package]["version"]
|
||||
|
||||
if "git_tag" in build_json[args.package]:
|
||||
git_tag = build_json[args.package]["git_tag"]
|
||||
else:
|
||||
raise NotImplementedError(args.package)
|
||||
|
||||
@@ -81,7 +72,6 @@ def _main():
|
||||
output = {
|
||||
"name": args.package,
|
||||
"version": version,
|
||||
"git_tag": git_tag,
|
||||
"image_tag": get_image_tag(repo_name, args.package, version),
|
||||
"cache_tag": get_cache_image_tag(
|
||||
repo_name,
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
run: pipx install pipenv
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
cache: "pipenv"
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
-
|
||||
@@ -135,18 +135,24 @@ jobs:
|
||||
-
|
||||
name: Check pushing to Docker Hub
|
||||
id: docker-hub
|
||||
# Only push to Dockerhub from the main repo
|
||||
# Only push to Dockerhub from the main repo AND the ref is either:
|
||||
# main
|
||||
# dev
|
||||
# beta
|
||||
# a tag
|
||||
# Otherwise forks would require a Docker Hub account and secrets setup
|
||||
run: |
|
||||
if [[ ${{ github.repository }} == "paperless-ngx/paperless-ngx" ]] ; then
|
||||
if [[ ${{ github.repository }} == "paperless-ngx/paperless-ngx" && ( ${{ github.ref_name }} == "main" || ${{ github.ref_name }} == "dev" || ${{ github.ref_name }} == "beta" || ${{ startsWith(github.ref, 'refs/tags/v') }} == "true" ) ]] ; then
|
||||
echo "Enabling DockerHub image push"
|
||||
echo ::set-output name=enable::"true"
|
||||
else
|
||||
echo "Not pushing to DockerHub"
|
||||
echo ::set-output name=enable::"false"
|
||||
fi
|
||||
-
|
||||
name: Gather Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
@@ -163,20 +169,20 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
# Don't attempt to login is not pushing to Docker Hub
|
||||
if: steps.docker-hub.outputs.enable == 'true'
|
||||
with:
|
||||
@@ -184,7 +190,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -231,7 +237,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
-
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Cleanup Image Tags
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * SAT'
|
||||
delete:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/cleanup-tags.yml"
|
||||
- ".github/scripts/cleanup-tags.py"
|
||||
- ".github/scripts/common.py"
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: Cleanup Image Tags
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.9"
|
||||
-
|
||||
name: Install requests
|
||||
run: |
|
||||
python -m pip install requests
|
||||
-
|
||||
name: Cleanup feature tags
|
||||
run: |
|
||||
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --delete
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
-
|
||||
@@ -122,7 +122,6 @@ jobs:
|
||||
dockerfile: ./docker-builders/Dockerfile.psycopg2
|
||||
build-json: ${{ needs.prepare-docker-build.outputs.psycopg2-json }}
|
||||
build-args: |
|
||||
PSYCOPG2_GIT_TAG=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).git_tag }}
|
||||
PSYCOPG2_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }}
|
||||
|
||||
build-pikepdf-wheel:
|
||||
@@ -137,5 +136,4 @@ jobs:
|
||||
build-args: |
|
||||
REPO=${{ github.repository }}
|
||||
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
|
||||
PIKEPDF_GIT_TAG=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).git_tag }}
|
||||
PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }}
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
run: pipx install pipenv
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
cache: "pipenv"
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng libzbar0 poppler-utils
|
||||
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript libzbar0 poppler-utils
|
||||
-
|
||||
name: Install Python dependencies
|
||||
run: |
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
-
|
||||
name: Get changed files
|
||||
id: changed-files-specific
|
||||
uses: tj-actions/changed-files@v19
|
||||
uses: tj-actions/changed-files@v23.1
|
||||
with:
|
||||
files: |
|
||||
src/**
|
||||
|
||||
@@ -28,20 +28,20 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Build ${{ fromJSON(inputs.build-json).name }}
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
|
||||
@@ -70,6 +70,7 @@ target/
|
||||
.virtualenv
|
||||
virtualenv
|
||||
/venv
|
||||
.venv/
|
||||
/docker-compose.env
|
||||
/docker-compose.yml
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
repos:
|
||||
# General hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.2.0
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: check-json
|
||||
@@ -27,7 +27,7 @@ repos:
|
||||
- id: check-case-conflict
|
||||
- id: detect-private-key
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: "v2.6.2"
|
||||
rev: "v2.7.1"
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or:
|
||||
@@ -37,7 +37,7 @@ repos:
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v3.1.0
|
||||
rev: v3.8.1
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
exclude: "(migrations)"
|
||||
@@ -59,11 +59,11 @@ repos:
|
||||
args:
|
||||
- "--config=./src/setup.cfg"
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.32.1
|
||||
rev: v2.37.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
exclude: "(migrations)"
|
||||
|
||||
@@ -77,15 +77,12 @@ ARG RUNTIME_PACKAGES="\
|
||||
libraqm0 \
|
||||
libgnutls30 \
|
||||
libjpeg62-turbo \
|
||||
optipng \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
postgresql-client \
|
||||
# For Numpy
|
||||
libatlas3-base \
|
||||
# thumbnail size reduction
|
||||
pngquant \
|
||||
# OCRmyPDF dependencies
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
@@ -151,14 +148,14 @@ RUN --mount=type=bind,from=qpdf-builder,target=/qpdf \
|
||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/libqpdf28_*.deb \
|
||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/qpdf_*.deb \
|
||||
&& echo "Installing pikepdf and dependencies" \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/packaging*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/lxml*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/Pillow*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/pyparsing*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/pikepdf*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/packaging*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/lxml*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/Pillow*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pyparsing*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pikepdf*.whl \
|
||||
&& python -m pip list \
|
||||
&& echo "Installing psycopg2" \
|
||||
&& python3 -m pip install --no-cache-dir /psycopg2/usr/src/psycopg2/wheels/psycopg2*.whl \
|
||||
&& python3 -m pip install --no-cache-dir /psycopg2/usr/src/wheels/psycopg2*.whl \
|
||||
&& python -m pip list
|
||||
|
||||
# Python dependencies
|
||||
@@ -169,6 +166,7 @@ COPY requirements.txt ../
|
||||
# dependencies
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
git \
|
||||
python3-dev"
|
||||
|
||||
RUN set -eux \
|
||||
|
||||
@@ -13,8 +13,8 @@ dateparser = "~=1.1"
|
||||
django = "~=4.0"
|
||||
django-cors-headers = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=21.1"
|
||||
django-q = "~=1.3"
|
||||
django-filter = "~=22.1"
|
||||
django-q = {editable = true, ref = "paperless-main", git = "https://github.com/paperless-ngx/django-q.git"}
|
||||
djangorestframework = "~=3.13"
|
||||
filelock = "*"
|
||||
fuzzywuzzy = {extras = ["speedup"], version = "*"}
|
||||
@@ -22,20 +22,17 @@ gunicorn = "*"
|
||||
imap-tools = "*"
|
||||
langdetect = "*"
|
||||
pathvalidate = "*"
|
||||
pillow = "~=9.1"
|
||||
# Any version update to pikepdf requires a base image update
|
||||
pillow = "~=9.2"
|
||||
pikepdf = "~=5.1"
|
||||
python-gnupg = "*"
|
||||
python-dotenv = "*"
|
||||
python-dateutil = "*"
|
||||
python-magic = "*"
|
||||
# Any version update to psycopg2 requires a base image update
|
||||
psycopg2 = "*"
|
||||
redis = "*"
|
||||
# Pinned because aarch64 wheels and updates cause warnings when loading the classifier model.
|
||||
scikit-learn="==1.0.2"
|
||||
whitenoise = "~=6.0.0"
|
||||
watchdog = "~=2.1.0"
|
||||
scikit-learn="~=1.1"
|
||||
whitenoise = "~=6.2.0"
|
||||
watchdog = "~=2.1.9"
|
||||
whoosh="~=2.7.4"
|
||||
inotifyrecursive = "~=0.3"
|
||||
ocrmypdf = "~=13.4"
|
||||
@@ -65,7 +62,7 @@ pytest-django = "*"
|
||||
pytest-env = "*"
|
||||
pytest-sugar = "*"
|
||||
pytest-xdist = "*"
|
||||
sphinx = "~=4.5.0"
|
||||
sphinx = "~=5.0.2"
|
||||
sphinx_rtd_theme = "*"
|
||||
tox = "*"
|
||||
black = "*"
|
||||
|
||||
@@ -34,6 +34,7 @@ branch_name=$(git rev-parse --abbrev-ref HEAD)
|
||||
export DOCKER_BUILDKIT=1
|
||||
|
||||
docker build --file "$1" \
|
||||
--progress=plain \
|
||||
--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:"${branch_name}" \
|
||||
--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:dev \
|
||||
--build-arg JBIG2ENC_VERSION="${jbig2enc_version}" \
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
# Inputs:
|
||||
# - REPO - Docker repository to pull qpdf from
|
||||
# - QPDF_VERSION - The image qpdf version to copy .deb files from
|
||||
# - PIKEPDF_GIT_TAG - The Git tag to clone and build from
|
||||
# - PIKEPDF_VERSION - Used to force the built pikepdf version to match
|
||||
# - PIKEPDF_VERSION - Version of pikepdf to build wheel for
|
||||
|
||||
# Default to pulling from the main repo registry when manually building
|
||||
ARG REPO="paperless-ngx/paperless-ngx"
|
||||
@@ -23,7 +22,6 @@ ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
git \
|
||||
# qpdf requirement - https://github.com/qpdf/qpdf#crypto-providers
|
||||
libgnutls28-dev \
|
||||
# lxml requrements - https://lxml.de/installation.html
|
||||
@@ -72,21 +70,19 @@ RUN set -eux \
|
||||
# For better caching, seperate the basic installs from
|
||||
# the building
|
||||
|
||||
ARG PIKEPDF_GIT_TAG
|
||||
ARG PIKEPDF_VERSION
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "building pikepdf wheel" \
|
||||
# Note the v in the tag name here
|
||||
&& git clone --quiet --depth 1 --branch "${PIKEPDF_GIT_TAG}" https://github.com/pikepdf/pikepdf.git \
|
||||
&& cd pikepdf \
|
||||
# pikepdf seems to specifciy either a next version when built OR
|
||||
# a post release tag.
|
||||
# In either case, this won't match what we want from requirements.txt
|
||||
# Directly modify the setup.py to set the version we just checked out of Git
|
||||
&& sed -i "s/use_scm_version=True/version=\"${PIKEPDF_VERSION}\"/g" setup.py \
|
||||
# https://github.com/pikepdf/pikepdf/issues/323
|
||||
&& rm pyproject.toml \
|
||||
&& echo "Building pikepdf wheel ${PIKEPDF_VERSION}" \
|
||||
&& mkdir wheels \
|
||||
&& python3 -m pip wheel . --wheel-dir wheels \
|
||||
&& python3 -m pip wheel \
|
||||
# Build the package at the required version
|
||||
pikepdf==${PIKEPDF_VERSION} \
|
||||
# Output the *.whl into this directory
|
||||
--wheel-dir wheels \
|
||||
# Do not use a binary packge for the package being built
|
||||
--no-binary=pikepdf \
|
||||
# Do use binary packages for dependencies
|
||||
--prefer-binary \
|
||||
--no-cache-dir \
|
||||
&& ls -ahl wheels
|
||||
@@ -1,7 +1,6 @@
|
||||
# This Dockerfile builds the psycopg2 wheel
|
||||
# Inputs:
|
||||
# - PSYCOPG2_GIT_TAG - The Git tag to clone and build from
|
||||
# - PSYCOPG2_VERSION - Unused, kept for future possible usage
|
||||
# - PSYCOPG2_VERSION - Version to build
|
||||
|
||||
FROM python:3.9-slim-bullseye as main
|
||||
|
||||
@@ -11,7 +10,6 @@ ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
git \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
# https://www.psycopg.org/docs/install.html#prerequisites
|
||||
@@ -32,14 +30,20 @@ RUN set -eux \
|
||||
# For better caching, seperate the basic installs from
|
||||
# the building
|
||||
|
||||
ARG PSYCOPG2_GIT_TAG
|
||||
ARG PSYCOPG2_VERSION
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Building psycopg2 wheel" \
|
||||
&& echo "Building psycopg2 wheel ${PSYCOPG2_VERSION}" \
|
||||
&& cd /usr/src \
|
||||
&& git clone --quiet --depth 1 --branch ${PSYCOPG2_GIT_TAG} https://github.com/psycopg/psycopg2.git \
|
||||
&& cd psycopg2 \
|
||||
&& mkdir wheels \
|
||||
&& python3 -m pip wheel . --wheel-dir wheels \
|
||||
&& python3 -m pip wheel \
|
||||
# Build the package at the required version
|
||||
psycopg2==${PSYCOPG2_VERSION} \
|
||||
# Output the *.whl into this directory
|
||||
--wheel-dir wheels \
|
||||
# Do not use a binary packge for the package being built
|
||||
--no-binary=psycopg2 \
|
||||
# Do use binary packages for dependencies
|
||||
--prefer-binary \
|
||||
--no-cache-dir \
|
||||
&& ls -ahl wheels/
|
||||
@@ -31,13 +31,13 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: redis:6.0
|
||||
image: docker.io/library/redis:6.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
image: docker.io/library/postgres:13
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: redis:6.0
|
||||
image: docker.io/library/redis:6.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
image: docker.io/library/postgres:13
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
@@ -77,7 +77,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7.4
|
||||
image: docker.io/gotenberg/gotenberg:7.4
|
||||
restart: unless-stopped
|
||||
|
||||
tika:
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: redis:6.0
|
||||
image: docker.io/library/redis:6.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
image: docker.io/library/postgres:13
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: redis:6.0
|
||||
image: docker.io/library/redis:6.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
@@ -65,7 +65,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7.4
|
||||
image: docker.io/gotenberg/gotenberg:7.4
|
||||
restart: unless-stopped
|
||||
|
||||
tika:
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: redis:6.0
|
||||
image: docker.io/library/redis:6.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
@@ -2,6 +2,37 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Adapted from:
|
||||
# https://github.com/docker-library/postgres/blob/master/docker-entrypoint.sh
|
||||
# usage: file_env VAR
|
||||
# ie: file_env 'XYZ_DB_PASSWORD' will allow for "$XYZ_DB_PASSWORD_FILE" to
|
||||
# fill in the value of "$XYZ_DB_PASSWORD" from a file, especially for Docker's
|
||||
# secrets feature
|
||||
file_env() {
|
||||
local var="$1"
|
||||
local fileVar="${var}_FILE"
|
||||
|
||||
# Basic validation
|
||||
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
|
||||
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only export var if the _FILE exists
|
||||
if [ "${!fileVar:-}" ]; then
|
||||
# And the file exists
|
||||
if [[ -f ${!fileVar} ]]; then
|
||||
echo "Setting ${var} from file"
|
||||
val="$(< "${!fileVar}")"
|
||||
export "$var"="$val"
|
||||
else
|
||||
echo "File ${!fileVar} doesn't exist"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Source: https://github.com/sameersbn/docker-gitlab/
|
||||
map_uidgid() {
|
||||
USERMAP_ORIG_UID=$(id -u paperless)
|
||||
@@ -15,23 +46,53 @@ map_uidgid() {
|
||||
fi
|
||||
}
|
||||
|
||||
map_folders() {
|
||||
# Export these so they can be used in docker-prepare.sh
|
||||
export DATA_DIR="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
|
||||
export MEDIA_ROOT_DIR="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}"
|
||||
}
|
||||
|
||||
initialize() {
|
||||
|
||||
# Setup environment from secrets before anything else
|
||||
for env_var in \
|
||||
PAPERLESS_DBUSER \
|
||||
PAPERLESS_DBPASS \
|
||||
PAPERLESS_SECRET_KEY \
|
||||
PAPERLESS_AUTO_LOGIN_USERNAME \
|
||||
PAPERLESS_ADMIN_USER \
|
||||
PAPERLESS_ADMIN_MAIL \
|
||||
PAPERLESS_ADMIN_PASSWORD; do
|
||||
# Check for a version of this var with _FILE appended
|
||||
# and convert the contents to the env var value
|
||||
file_env ${env_var}
|
||||
done
|
||||
|
||||
# Change the user and group IDs if needed
|
||||
map_uidgid
|
||||
|
||||
for dir in export data data/index media media/documents media/documents/originals media/documents/thumbnails; do
|
||||
if [[ ! -d "../$dir" ]]; then
|
||||
echo "Creating directory ../$dir"
|
||||
mkdir ../$dir
|
||||
# Check for overrides of certain folders
|
||||
map_folders
|
||||
|
||||
local export_dir="/usr/src/paperless/export"
|
||||
|
||||
for dir in "${export_dir}" "${DATA_DIR}" "${DATA_DIR}/index" "${MEDIA_ROOT_DIR}" "${MEDIA_ROOT_DIR}/documents" "${MEDIA_ROOT_DIR}/documents/originals" "${MEDIA_ROOT_DIR}/documents/thumbnails"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
echo "Creating directory ${dir}"
|
||||
mkdir "${dir}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Creating directory /tmp/paperless"
|
||||
mkdir -p /tmp/paperless
|
||||
local tmp_dir="/tmp/paperless"
|
||||
echo "Creating directory ${tmp_dir}"
|
||||
mkdir -p "${tmp_dir}"
|
||||
|
||||
set +e
|
||||
echo "Adjusting permissions of paperless files. This may take a while."
|
||||
chown -R paperless:paperless /tmp/paperless
|
||||
find .. -not \( -user paperless -and -group paperless \) -exec chown paperless:paperless {} +
|
||||
chown -R paperless:paperless ${tmp_dir}
|
||||
for dir in "${export_dir}" "${DATA_DIR}" "${MEDIA_ROOT_DIR}"; do
|
||||
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown paperless:paperless {} +
|
||||
done
|
||||
set -e
|
||||
|
||||
gosu paperless /sbin/docker-prepare.sh
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
set -e
|
||||
|
||||
wait_for_postgres() {
|
||||
attempt_num=1
|
||||
max_attempts=5
|
||||
local attempt_num=1
|
||||
local max_attempts=5
|
||||
|
||||
echo "Waiting for PostgreSQL to start..."
|
||||
|
||||
host="${PAPERLESS_DBHOST:=localhost}"
|
||||
port="${PAPERLESS_DBPORT:=5432}"
|
||||
local host="${PAPERLESS_DBHOST:-localhost}"
|
||||
local port="${PAPERLESS_DBPORT:-5432}"
|
||||
|
||||
|
||||
while [ ! "$(pg_isready -h $host -p $port)" ]; do
|
||||
# Disable warning, host and port can't have spaces
|
||||
# shellcheck disable=SC2086
|
||||
while [ ! "$(pg_isready -h ${host} -p ${port})" ]; do
|
||||
|
||||
if [ $attempt_num -eq $max_attempts ]; then
|
||||
echo "Unable to connect to database."
|
||||
@@ -43,17 +44,18 @@ migrations() {
|
||||
flock 200
|
||||
echo "Apply database migrations..."
|
||||
python3 manage.py migrate
|
||||
) 200>/usr/src/paperless/data/migration_lock
|
||||
) 200>"${DATA_DIR}/migration_lock"
|
||||
}
|
||||
|
||||
search_index() {
|
||||
index_version=1
|
||||
index_version_file=/usr/src/paperless/data/.index_version
|
||||
|
||||
if [[ (! -f "$index_version_file") || $(<$index_version_file) != "$index_version" ]]; then
|
||||
local index_version=1
|
||||
local index_version_file=${DATA_DIR}/.index_version
|
||||
|
||||
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
|
||||
echo "Search index out of date. Updating..."
|
||||
python3 manage.py document_index reindex --no-progress-bar
|
||||
echo $index_version | tee $index_version_file >/dev/null
|
||||
echo ${index_version} | tee "${index_version_file}" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
|
||||
set -eu
|
||||
|
||||
for command in document_archiver document_exporter document_importer mail_fetcher document_create_classifier document_index document_renamer document_retagger document_thumbnails document_sanity_checker manage_superuser;
|
||||
for command in decrypt_documents \
|
||||
document_archiver \
|
||||
document_exporter \
|
||||
document_importer \
|
||||
mail_fetcher \
|
||||
document_create_classifier \
|
||||
document_index \
|
||||
document_renamer \
|
||||
document_retagger \
|
||||
document_thumbnails \
|
||||
document_sanity_checker \
|
||||
manage_superuser;
|
||||
do
|
||||
echo "installing $command..."
|
||||
sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command
|
||||
|
||||
@@ -26,9 +26,11 @@ if __name__ == "__main__":
|
||||
try:
|
||||
client.ping()
|
||||
break
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Redis ping #{attempt} failed, waiting {RETRY_SLEEP_SECONDS}s",
|
||||
f"Redis ping #{attempt} failed.\n"
|
||||
f"Error: {str(e)}.\n"
|
||||
f"Waiting {RETRY_SLEEP_SECONDS}s",
|
||||
flush=True,
|
||||
)
|
||||
time.sleep(RETRY_SLEEP_SECONDS)
|
||||
|
||||
@@ -31,7 +31,8 @@ The objects served by the document endpoint contain the following fields:
|
||||
* ``tags``: List of IDs of tags assigned to this document, or empty list.
|
||||
* ``document_type``: Document type of this document, or null.
|
||||
* ``correspondent``: Correspondent of this document or null.
|
||||
* ``created``: The date at which this document was created.
|
||||
* ``created``: The date time at which this document was created.
|
||||
* ``created_date``: The date (YYYY-MM-DD) at which this document was created. Optional. If also passed with created, this is ignored.
|
||||
* ``modified``: The date at which this document was last edited in paperless. Read-only.
|
||||
* ``added``: The date at which this document was added to paperless. Read-only.
|
||||
* ``archive_serial_number``: The identifier of this document in a physical document archive.
|
||||
|
||||
@@ -424,14 +424,23 @@ PAPERLESS_OCR_IMAGE_DPI=<num>
|
||||
the produced PDF documents are A4 sized.
|
||||
|
||||
PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num>
|
||||
Paperless will not OCR images that have more pixels than this limit.
|
||||
This is intended to prevent decompression bombs from overloading paperless.
|
||||
Increasing this limit is desired if you face a DecompressionBombError despite
|
||||
the concerning file not being malicious; this could e.g. be caused by invalidly
|
||||
recognized metadata.
|
||||
If you have enough resources or if you are certain that your uploaded files
|
||||
are not malicious you can increase this value to your needs.
|
||||
The default value is 256000000, an image with more pixels than that would not be parsed.
|
||||
Paperless will raise a warning when OCRing images which are over this limit and
|
||||
will not OCR images which are more than twice this limit. Note this does not
|
||||
prevent the document from being consumed, but could result in missing text content.
|
||||
|
||||
If unset, will default to the value determined by
|
||||
`Pillow <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.MAX_IMAGE_PIXELS>`_.
|
||||
|
||||
.. note::
|
||||
|
||||
Increasing this limit could cause Paperless to consume additional resources
|
||||
when consuming a file. Be sure you have sufficient system resources.
|
||||
|
||||
.. caution::
|
||||
|
||||
The limit is intended to prevent malicious files from consuming system resources
|
||||
and causing crashes and other errors. Only increase this value if you are certain
|
||||
your documents are not malicious and you need the text which was not OCRed
|
||||
|
||||
PAPERLESS_OCR_USER_ARGS=<json>
|
||||
OCRmyPDF offers many more options. Use this parameter to specify any
|
||||
@@ -700,13 +709,6 @@ PAPERLESS_CONVERT_TMPDIR=<path>
|
||||
|
||||
Default is none, which disables the temporary directory.
|
||||
|
||||
PAPERLESS_OPTIMIZE_THUMBNAILS=<bool>
|
||||
Use optipng to optimize thumbnails. This usually reduces the size of
|
||||
thumbnails by about 20%, but uses considerable compute time during
|
||||
consumption.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
PAPERLESS_POST_CONSUME_SCRIPT=<filename>
|
||||
After a document is consumed, Paperless can trigger an arbitrary script if
|
||||
you like. This script will be passed a number of arguments for you to work
|
||||
@@ -777,9 +779,6 @@ PAPERLESS_CONVERT_BINARY=<path>
|
||||
PAPERLESS_GS_BINARY=<path>
|
||||
Defaults to "/usr/bin/gs".
|
||||
|
||||
PAPERLESS_OPTIPNG_BINARY=<path>
|
||||
Defaults to "/usr/bin/optipng".
|
||||
|
||||
|
||||
.. _configuration-docker:
|
||||
|
||||
|
||||
@@ -200,6 +200,19 @@ Install Paperless from Docker Hub
|
||||
You can copy any setting from the file ``paperless.conf.example`` and paste it here.
|
||||
Have a look at :ref:`configuration` to see what's available.
|
||||
|
||||
.. note::
|
||||
|
||||
You can utilize Docker secrets for some configuration settings by
|
||||
appending `_FILE` to some configuration values. This is supported currently
|
||||
only by:
|
||||
* PAPERLESS_DBUSER
|
||||
* PAPERLESS_DBPASS
|
||||
* PAPERLESS_SECRET_KEY
|
||||
* PAPERLESS_AUTO_LOGIN_USERNAME
|
||||
* PAPERLESS_ADMIN_USER
|
||||
* PAPERLESS_ADMIN_MAIL
|
||||
* PAPERLESS_ADMIN_PASSWORD
|
||||
|
||||
.. caution::
|
||||
|
||||
Some file systems such as NFS network shares don't support file system
|
||||
@@ -286,7 +299,6 @@ writing. Windows is not and will never be supported.
|
||||
|
||||
* ``fonts-liberation`` for generating thumbnails for plain text files
|
||||
* ``imagemagick`` >= 6 for PDF conversion
|
||||
* ``optipng`` for optimizing thumbnails
|
||||
* ``gnupg`` for handling encrypted documents
|
||||
* ``libpq-dev`` for PostgreSQL
|
||||
* ``libmagic-dev`` for mime type detection
|
||||
@@ -298,7 +310,7 @@ writing. Windows is not and will never be supported.
|
||||
|
||||
.. code::
|
||||
|
||||
python3 python3-pip python3-dev imagemagick fonts-liberation optipng gnupg libpq-dev libmagic-dev mime-support libzbar0 poppler-utils
|
||||
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev libmagic-dev mime-support libzbar0 poppler-utils
|
||||
|
||||
These dependencies are required for OCRmyPDF, which is used for text recognition.
|
||||
|
||||
@@ -730,8 +742,6 @@ configuring some options in paperless can help improve performance immensely:
|
||||
* If you want to perform OCR on the device, consider using ``PAPERLESS_OCR_CLEAN=none``.
|
||||
This will speed up OCR times and use less memory at the expense of slightly worse
|
||||
OCR results.
|
||||
* Set ``PAPERLESS_OPTIMIZE_THUMBNAILS`` to 'false' if you want faster consumption
|
||||
times. Thumbnails will be about 20% larger.
|
||||
* If using docker, consider setting ``PAPERLESS_WEBSERVER_WORKERS`` to
|
||||
1. This will save some memory.
|
||||
|
||||
|
||||
@@ -161,6 +161,9 @@ These are as follows:
|
||||
will not consume flagged mails.
|
||||
* **Move to folder:** Moves consumed mails out of the way so that paperless wont
|
||||
consume them again.
|
||||
* **Add custom Tag:** Adds a custom tag to mails with consumed documents (the IMAP
|
||||
standard calls these "keywords"). Paperless will not consume mails already tagged.
|
||||
Not all mail servers support this feature!
|
||||
|
||||
.. caution::
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
#PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false
|
||||
#PAPERLESS_CONSUMER_ENABLE_BARCODES=false
|
||||
#PAPERLESS_CONSUMER_ENABLE_BARCODES=PATCHT
|
||||
#PAPERLESS_OPTIMIZE_THUMBNAILS=true
|
||||
#PAPERLESS_PRE_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
||||
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
||||
#PAPERLESS_FILENAME_DATE_ORDER=YMD
|
||||
@@ -84,4 +83,3 @@
|
||||
|
||||
#PAPERLESS_CONVERT_BINARY=/usr/bin/convert
|
||||
#PAPERLESS_GS_BINARY=/usr/bin/gs
|
||||
#PAPERLESS_OPTIPNG_BINARY=/usr/bin/optipng
|
||||
@@ -1,10 +1,3 @@
|
||||
#
|
||||
# These requirements were autogenerated by pipenv
|
||||
# To regenerate from the project's Pipfile, run:
|
||||
#
|
||||
# pipenv lock --requirements
|
||||
#
|
||||
|
||||
-i https://pypi.python.org/simple
|
||||
--extra-index-url https://www.piwheels.org/simple
|
||||
aioredis==1.3.1
|
||||
@@ -13,30 +6,32 @@ arrow==1.2.2; python_version >= '3.6'
|
||||
asgiref==3.5.2; python_version >= '3.7'
|
||||
async-timeout==4.0.2; python_version >= '3.6'
|
||||
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
autobahn==22.4.2; python_version >= '3.7'
|
||||
autobahn==22.6.1; python_version >= '3.7'
|
||||
automat==20.2.0
|
||||
backports.zoneinfo==0.2.1; python_version < '3.9'
|
||||
blessed==1.19.1; python_version >= '2.7'
|
||||
certifi==2021.10.8
|
||||
cffi==1.15.0
|
||||
certifi==2022.6.15; python_version >= '3.6'
|
||||
cffi==1.15.1
|
||||
channels==3.0.5
|
||||
channels-redis==3.4.0
|
||||
channels==3.0.4
|
||||
charset-normalizer==2.0.12; python_version >= '3.5'
|
||||
charset-normalizer==2.1.0; python_version >= '3.6'
|
||||
click==8.1.3; python_version >= '3.7'
|
||||
coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
concurrent-log-handler==0.9.20
|
||||
constantly==15.1.0
|
||||
cryptography==36.0.2; python_version >= '3.6'
|
||||
cryptography==37.0.4; python_version >= '3.6'
|
||||
daphne==3.0.2; python_version >= '3.6'
|
||||
dateparser==1.1.1
|
||||
django-cors-headers==3.12.0
|
||||
django-extensions==3.1.5
|
||||
django-filter==21.1
|
||||
django-picklefield==3.0.1; python_version >= '3'
|
||||
django-q==1.3.9
|
||||
django==4.0.4
|
||||
deprecated==1.2.13; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
deprecation==2.1.0
|
||||
django==4.0.6
|
||||
django-cors-headers==3.13.0
|
||||
django-extensions==3.2.0
|
||||
django-filter==22.1
|
||||
django-picklefield==3.1; python_version >= '3'
|
||||
-e git+https://github.com/paperless-ngx/django-q.git@bf20d57f859a7d872d5979cd8879fac9c9df981c#egg=django-q
|
||||
djangorestframework==3.13.1
|
||||
filelock==3.7.0
|
||||
filelock==3.7.1
|
||||
fuzzywuzzy[speedup]==0.18.0
|
||||
gunicorn==20.1.0
|
||||
h11==0.13.0; python_version >= '3.6'
|
||||
@@ -44,50 +39,50 @@ hiredis==2.0.0; python_version >= '3.6'
|
||||
httptools==0.4.0
|
||||
humanfriendly==10.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
hyperlink==21.0.0
|
||||
idna==3.3; python_version >= '3'
|
||||
imap-tools==0.55.0
|
||||
idna==3.3; python_version >= '3.5'
|
||||
imap-tools==0.56.0
|
||||
img2pdf==0.4.4
|
||||
importlib-resources==5.7.1; python_version < '3.9'
|
||||
importlib-resources==5.8.0; python_version < '3.9'
|
||||
incremental==21.3.0
|
||||
inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
inotifyrecursive==0.3.5
|
||||
joblib==1.1.0; python_version >= '3.6'
|
||||
langdetect==1.0.9
|
||||
lxml==4.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
msgpack==1.0.3
|
||||
numpy==1.22.3; python_version >= '3.8'
|
||||
ocrmypdf==13.4.4
|
||||
lxml==4.9.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
msgpack==1.0.4
|
||||
numpy==1.23.1; python_version >= '3.8'
|
||||
ocrmypdf==13.6.0
|
||||
packaging==21.3; python_version >= '3.6'
|
||||
pathvalidate==2.5.0
|
||||
pdf2image==1.16.0
|
||||
pdfminer.six==20220506
|
||||
pikepdf==5.1.3
|
||||
pillow==9.1.0
|
||||
pdfminer.six==20220524
|
||||
pikepdf==5.3.1
|
||||
pillow==9.2.0
|
||||
pluggy==1.0.0; python_version >= '3.6'
|
||||
portalocker==2.4.0; python_version >= '3'
|
||||
portalocker==2.5.1; python_version >= '3'
|
||||
psycopg2==2.9.3
|
||||
pyasn1-modules==0.2.8
|
||||
pyasn1==0.4.8
|
||||
pyasn1-modules==0.2.8
|
||||
pycparser==2.21
|
||||
pyopenssl==22.0.0
|
||||
pyparsing==3.0.9; python_full_version >= '3.6.8'
|
||||
python-dateutil==2.8.2
|
||||
python-dotenv==0.20.0
|
||||
python-gnupg==0.4.8
|
||||
python-gnupg==0.4.9
|
||||
python-levenshtein==0.12.2
|
||||
python-magic==0.4.25
|
||||
pytz-deprecation-shim==0.1.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
python-magic==0.4.27
|
||||
pytz==2022.1
|
||||
pytz-deprecation-shim==0.1.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
pyyaml==6.0
|
||||
pyzbar==0.1.9
|
||||
redis==3.5.3
|
||||
redis==4.3.4
|
||||
regex==2022.3.2; python_version >= '3.6'
|
||||
reportlab==3.6.9; python_version >= '3.7' and python_version < '4'
|
||||
requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
scikit-learn==1.0.2
|
||||
scipy==1.8.0; python_version < '3.11' and python_version >= '3.8'
|
||||
reportlab==3.6.11; python_version >= '3.7' and python_version < '4'
|
||||
requests==2.28.1; python_version >= '3.7' and python_version < '4'
|
||||
scikit-learn==1.1.1
|
||||
scipy==1.8.1; python_version < '3.11' and python_version >= '3.8'
|
||||
service-identity==21.1.0
|
||||
setuptools==62.2.0; python_version >= '3.7'
|
||||
setuptools==63.1.0; python_version >= '3.7'
|
||||
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
sniffio==1.2.0; python_version >= '3.5'
|
||||
sqlparse==0.4.2; python_version >= '3.5'
|
||||
@@ -96,17 +91,18 @@ tika==1.24
|
||||
tqdm==4.64.0
|
||||
twisted[tls]==22.4.0; python_full_version >= '3.6.7'
|
||||
txaio==22.2.1; python_version >= '3.6'
|
||||
typing-extensions==4.2.0; python_version >= '3.7'
|
||||
typing-extensions==4.3.0; python_version >= '3.7'
|
||||
tzdata==2022.1; python_version >= '3.6'
|
||||
tzlocal==4.2; python_version >= '3.6'
|
||||
urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
||||
uvicorn[standard]==0.17.6
|
||||
urllib3==1.26.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'
|
||||
uvicorn[standard]==0.18.2
|
||||
uvloop==0.16.0
|
||||
watchdog==2.1.8
|
||||
watchgod==0.8.2
|
||||
watchdog==2.1.9
|
||||
watchfiles==0.15.0
|
||||
wcwidth==0.2.5
|
||||
websockets==10.3
|
||||
whitenoise==6.0.0
|
||||
whitenoise==6.2.0
|
||||
whoosh==2.7.4
|
||||
wrapt==1.14.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
zipp==3.8.0; python_version < '3.9'
|
||||
zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
videosFolder: 'cypress/videos',
|
||||
screenshotsFolder: 'cypress/screenshots',
|
||||
fixturesFolder: 'cypress/fixtures',
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./cypress/plugins/index.ts')(on, config)
|
||||
},
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"integrationFolder": "cypress/integration",
|
||||
"supportFile": "cypress/support/index.ts",
|
||||
"videosFolder": "cypress/videos",
|
||||
"screenshotsFolder": "cypress/screenshots",
|
||||
"pluginsFile": "cypress/plugins/index.ts",
|
||||
"fixturesFolder": "cypress/fixtures",
|
||||
"baseUrl": "http://localhost:4200"
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
describe('document-detail', () => {
|
||||
beforeEach(() => {
|
||||
// also uses global fixtures from cypress/support/e2e.ts
|
||||
|
||||
this.modifiedDocuments = []
|
||||
|
||||
cy.intercept('http://localhost:8000/api/ui_settings/', {
|
||||
fixture: 'ui_settings/settings.json',
|
||||
})
|
||||
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||
cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => {
|
||||
let response = { ...documentsJson }
|
||||
@@ -18,30 +17,6 @@ describe('document-detail', () => {
|
||||
req.reply({ result: 'OK' })
|
||||
}).as('saveDoc')
|
||||
|
||||
cy.intercept('http://localhost:8000/api/documents/1/metadata/', {
|
||||
fixture: 'documents/1/metadata.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/documents/1/suggestions/', {
|
||||
fixture: 'documents/1/suggestions.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/saved_views/*', {
|
||||
fixture: 'saved_views/savedviews.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/tags/*', {
|
||||
fixture: 'tags/tags.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/correspondents/*', {
|
||||
fixture: 'correspondents/correspondents.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/document_types/*', {
|
||||
fixture: 'document_types/doctypes.json',
|
||||
})
|
||||
|
||||
cy.viewport(1024, 1024)
|
||||
cy.visit('/documents/1/')
|
||||
})
|
||||
@@ -1,11 +1,9 @@
|
||||
describe('documents-list', () => {
|
||||
beforeEach(() => {
|
||||
// also uses global fixtures from cypress/support/e2e.ts
|
||||
|
||||
this.bulkEdits = {}
|
||||
|
||||
// mock API methods
|
||||
cy.intercept('http://localhost:8000/api/ui_settings/', {
|
||||
fixture: 'ui_settings/settings.json',
|
||||
})
|
||||
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||
// bulk edit
|
||||
cy.intercept(
|
||||
@@ -56,40 +54,25 @@ describe('documents-list', () => {
|
||||
})
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/documents/1/thumb/', {
|
||||
fixture: 'documents/lorem-ipsum.png',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/tags/*', {
|
||||
fixture: 'tags/tags.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/correspondents/*', {
|
||||
fixture: 'correspondents/correspondents.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/document_types/*', {
|
||||
fixture: 'document_types/doctypes.json',
|
||||
})
|
||||
|
||||
cy.viewport(1280, 1024)
|
||||
cy.visit('/documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents rendered as cards with thumbnails', () => {
|
||||
cy.contains('3 documents')
|
||||
cy.contains('lorem-ipsum')
|
||||
cy.contains('lorem ipsum')
|
||||
cy.get('app-document-card-small:first-of-type img')
|
||||
.invoke('attr', 'src')
|
||||
.should('eq', 'http://localhost:8000/api/documents/1/thumb/')
|
||||
})
|
||||
|
||||
it('should change to table "details" view', () => {
|
||||
cy.get('div.btn-group-toggle input[value="details"]').parent().click()
|
||||
cy.get('div.btn-group input[value="details"]').next().click()
|
||||
cy.get('table')
|
||||
})
|
||||
|
||||
it('should change to large cards view', () => {
|
||||
cy.get('div.btn-group-toggle input[value="largeCards"]').parent().click()
|
||||
cy.get('div.btn-group input[value="largeCards"]').next().click()
|
||||
cy.get('app-document-card-large')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
|
||||
describe('documents query params', () => {
|
||||
beforeEach(() => {
|
||||
// also uses global fixtures from cypress/support/e2e.ts
|
||||
|
||||
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||
// mock api filtering
|
||||
cy.intercept('GET', 'http://localhost:8000/api/documents/*', (req) => {
|
||||
let response = { ...documentsJson }
|
||||
|
||||
if (req.query.hasOwnProperty('ordering')) {
|
||||
const sort_field = req.query['ordering'].toString().replace('-', '')
|
||||
const reverse = req.query['ordering'].toString().indexOf('-') !== -1
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).sort((docA, docB) => {
|
||||
let result = 0
|
||||
switch (sort_field) {
|
||||
case 'created':
|
||||
case 'added':
|
||||
result =
|
||||
new Date(docA[sort_field]) < new Date(docB[sort_field])
|
||||
? -1
|
||||
: 1
|
||||
break
|
||||
case 'archive_serial_number':
|
||||
result = docA[sort_field] < docB[sort_field] ? -1 : 1
|
||||
break
|
||||
}
|
||||
if (reverse) result = -result
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
if (req.query.hasOwnProperty('tags__id__in')) {
|
||||
const tag_ids: Array<number> = req.query['tags__id__in']
|
||||
.toString()
|
||||
.split(',')
|
||||
.map((v) => +v)
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter(
|
||||
(d) =>
|
||||
d.tags.length > 0 &&
|
||||
d.tags.filter((t) => tag_ids.includes(t)).length > 0
|
||||
)
|
||||
response.count = response.results.length
|
||||
} else if (req.query.hasOwnProperty('tags__id__none')) {
|
||||
const tag_ids: Array<number> = req.query['tags__id__none']
|
||||
.toString()
|
||||
.split(',')
|
||||
.map((v) => +v)
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => d.tags.filter((t) => tag_ids.includes(t)).length == 0)
|
||||
response.count = response.results.length
|
||||
} else if (
|
||||
req.query.hasOwnProperty('is_tagged') &&
|
||||
req.query['is_tagged'] == '0'
|
||||
) {
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => d.tags.length == 0)
|
||||
response.count = response.results.length
|
||||
}
|
||||
|
||||
if (req.query.hasOwnProperty('document_type__id')) {
|
||||
const doctype_id = +req.query['document_type__id']
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => d.document_type == doctype_id)
|
||||
response.count = response.results.length
|
||||
} else if (
|
||||
req.query.hasOwnProperty('document_type__isnull') &&
|
||||
req.query['document_type__isnull'] == '1'
|
||||
) {
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => d.document_type == undefined)
|
||||
response.count = response.results.length
|
||||
}
|
||||
|
||||
if (req.query.hasOwnProperty('correspondent__id')) {
|
||||
const correspondent_id = +req.query['correspondent__id']
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => d.correspondent == correspondent_id)
|
||||
response.count = response.results.length
|
||||
} else if (
|
||||
req.query.hasOwnProperty('correspondent__isnull') &&
|
||||
req.query['correspondent__isnull'] == '1'
|
||||
) {
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => d.correspondent == undefined)
|
||||
response.count = response.results.length
|
||||
}
|
||||
|
||||
if (req.query.hasOwnProperty('storage_path__id')) {
|
||||
const storage_path_id = +req.query['storage_path__id']
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => d.storage_path == storage_path_id)
|
||||
response.count = response.results.length
|
||||
} else if (
|
||||
req.query.hasOwnProperty('storage_path__isnull') &&
|
||||
req.query['storage_path__isnull'] == '1'
|
||||
) {
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => d.storage_path == undefined)
|
||||
response.count = response.results.length
|
||||
}
|
||||
|
||||
if (req.query.hasOwnProperty('created__date__gt')) {
|
||||
const date = new Date(req.query['created__date__gt'])
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => new Date(d.created) > date)
|
||||
response.count = response.results.length
|
||||
} else if (req.query.hasOwnProperty('created__date__lt')) {
|
||||
const date = new Date(req.query['created__date__lt'])
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => new Date(d.created) < date)
|
||||
response.count = response.results.length
|
||||
}
|
||||
|
||||
if (req.query.hasOwnProperty('added__date__gt')) {
|
||||
const date = new Date(req.query['added__date__gt'])
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => new Date(d.added) > date)
|
||||
response.count = response.results.length
|
||||
} else if (req.query.hasOwnProperty('added__date__lt')) {
|
||||
const date = new Date(req.query['added__date__lt'])
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => new Date(d.added) < date)
|
||||
response.count = response.results.length
|
||||
}
|
||||
|
||||
if (req.query.hasOwnProperty('title_content')) {
|
||||
const title_content_regexp = new RegExp(
|
||||
req.query['title_content'].toString(),
|
||||
'i'
|
||||
)
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter(
|
||||
(d) =>
|
||||
title_content_regexp.test(d.title) ||
|
||||
title_content_regexp.test(d.content)
|
||||
)
|
||||
response.count = response.results.length
|
||||
}
|
||||
|
||||
if (req.query.hasOwnProperty('archive_serial_number')) {
|
||||
const asn = +req.query['archive_serial_number']
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) => d.archive_serial_number == asn)
|
||||
response.count = response.results.length
|
||||
} else if (req.query.hasOwnProperty('archive_serial_number__isnull')) {
|
||||
const isnull = req.query['storage_path__isnull'] == '1'
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter((d) =>
|
||||
isnull
|
||||
? d.archive_serial_number == undefined
|
||||
: d.archive_serial_number != undefined
|
||||
)
|
||||
response.count = response.results.length
|
||||
} else if (req.query.hasOwnProperty('archive_serial_number__gt')) {
|
||||
const asn = +req.query['archive_serial_number__gt']
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter(
|
||||
(d) => d.archive_serial_number > 0 && d.archive_serial_number > asn
|
||||
)
|
||||
response.count = response.results.length
|
||||
} else if (req.query.hasOwnProperty('archive_serial_number__lt')) {
|
||||
const asn = +req.query['archive_serial_number__lt']
|
||||
response.results = (
|
||||
documentsJson.results as Array<PaperlessDocument>
|
||||
).filter(
|
||||
(d) => d.archive_serial_number > 0 && d.archive_serial_number < asn
|
||||
)
|
||||
response.count = response.results.length
|
||||
}
|
||||
|
||||
req.reply(response)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should show a list of documents sorted by created', () => {
|
||||
cy.visit('/documents?sort=created')
|
||||
cy.get('app-document-card-small').first().contains('No latin title')
|
||||
})
|
||||
|
||||
it('should show a list of documents reverse sorted by created', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true')
|
||||
cy.get('app-document-card-small').first().contains('sit amet')
|
||||
})
|
||||
|
||||
it('should show a list of documents sorted by added', () => {
|
||||
cy.visit('/documents?sort=added')
|
||||
cy.get('app-document-card-small').first().contains('No latin title')
|
||||
})
|
||||
|
||||
it('should show a list of documents reverse sorted by added', () => {
|
||||
cy.visit('/documents?sort=added&reverse=true')
|
||||
cy.get('app-document-card-small').first().contains('sit amet')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by any tags', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&tags__id__in=2,4,5')
|
||||
cy.contains('3 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by excluded tags', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&tags__id__none=2,4')
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by no tags', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&is_tagged=0')
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by document type', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&document_type__id=1')
|
||||
cy.contains('3 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by no document type', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&document_type__isnull=1')
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by correspondent', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&correspondent__id=9')
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by no correspondent', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1')
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by storage path', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&storage_path__id=2')
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by no storage path', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&storage_path__isnull=1')
|
||||
cy.contains('3 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by title or content', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&title_content=lorem')
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by asn', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&archive_serial_number=12345')
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by empty asn', () => {
|
||||
cy.visit(
|
||||
'/documents?sort=created&reverse=true&archive_serial_number__isnull=1'
|
||||
)
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by non-empty asn', () => {
|
||||
cy.visit(
|
||||
'/documents?sort=created&reverse=true&archive_serial_number__isnull=0'
|
||||
)
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by asn greater than', () => {
|
||||
cy.visit(
|
||||
'/documents?sort=created&reverse=true&archive_serial_number__gt=12346'
|
||||
)
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by asn less than', () => {
|
||||
cy.visit(
|
||||
'/documents?sort=created&reverse=true&archive_serial_number__lt=12346'
|
||||
)
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by created date greater than', () => {
|
||||
cy.visit(
|
||||
'/documents?sort=created&reverse=true&created__date__gt=2022-03-23'
|
||||
)
|
||||
cy.contains('3 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by created date less than', () => {
|
||||
cy.visit(
|
||||
'/documents?sort=created&reverse=true&created__date__lt=2022-03-23'
|
||||
)
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by added date greater than', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&added__date__gt=2022-03-24')
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by added date less than', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&added__date__lt=2022-03-24')
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by multiple filters', () => {
|
||||
cy.visit(
|
||||
'/documents?sort=created&reverse=true&document_type__id=1&correspondent__id=9&tags__id__in=4,5'
|
||||
)
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,5 @@
|
||||
describe('manage', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('http://localhost:8000/api/ui_settings/', {
|
||||
fixture: 'ui_settings/settings.json',
|
||||
})
|
||||
cy.intercept('http://localhost:8000/api/correspondents/*', {
|
||||
fixture: 'correspondents/correspondents.json',
|
||||
})
|
||||
cy.intercept('http://localhost:8000/api/tags/*', {
|
||||
fixture: 'tags/tags.json',
|
||||
})
|
||||
})
|
||||
// also uses global fixtures from cypress/support/e2e.ts
|
||||
|
||||
it('should show a list of correspondents with bottom pagination as well', () => {
|
||||
cy.visit('/correspondents')
|
||||
@@ -1,5 +1,7 @@
|
||||
describe('settings', () => {
|
||||
beforeEach(() => {
|
||||
// also uses global fixtures from cypress/support/e2e.ts
|
||||
|
||||
this.modifiedViews = []
|
||||
|
||||
// mock API methods
|
||||
@@ -42,14 +44,6 @@ describe('settings', () => {
|
||||
req.reply(response)
|
||||
})
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/documents/1/metadata/', {
|
||||
fixture: 'documents/1/metadata.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/documents/1/suggestions/', {
|
||||
fixture: 'documents/1/suggestions.json',
|
||||
})
|
||||
})
|
||||
|
||||
cy.viewport(1024, 1024)
|
||||
@@ -0,0 +1,60 @@
|
||||
describe('tasks', () => {
|
||||
beforeEach(() => {
|
||||
this.dismissedTasks = new Set<number>()
|
||||
|
||||
cy.fixture('tasks/tasks.json').then((tasksViewsJson) => {
|
||||
// acknowledge tasks POST
|
||||
cy.intercept(
|
||||
'POST',
|
||||
'http://localhost:8000/api/acknowledge_tasks/',
|
||||
(req) => {
|
||||
req.body['tasks'].forEach((t) => this.dismissedTasks.add(t)) // store this for later
|
||||
req.reply({ result: 'OK' })
|
||||
}
|
||||
)
|
||||
|
||||
cy.intercept('GET', 'http://localhost:8000/api/tasks/', (req) => {
|
||||
let response = [...tasksViewsJson]
|
||||
if (this.dismissedTasks.size) {
|
||||
response = response.filter((t) => {
|
||||
return !this.dismissedTasks.has(t.id)
|
||||
})
|
||||
}
|
||||
|
||||
req.reply(response)
|
||||
}).as('tasks')
|
||||
})
|
||||
|
||||
cy.visit('/tasks')
|
||||
cy.wait('@tasks')
|
||||
})
|
||||
|
||||
it('should show a list of dismissable tasks in tabs', () => {
|
||||
cy.get('tbody').find('tr:visible').its('length').should('eq', 10) // double because collapsible result tr
|
||||
cy.wait(500) // stabilizes the test, for some reason...
|
||||
cy.get('tbody')
|
||||
.find('button:visible')
|
||||
.contains('Dismiss')
|
||||
.first()
|
||||
.click()
|
||||
.wait('@tasks')
|
||||
.wait(2000)
|
||||
.then(() => {
|
||||
cy.get('tbody').find('tr:visible').its('length').should('eq', 8) // double because collapsible result tr
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow toggling all tasks in list and warn on dismiss', () => {
|
||||
cy.get('thead').find('input[type="checkbox"]').first().click()
|
||||
cy.get('body').find('button').contains('Dismiss selected').first().click()
|
||||
cy.contains('Confirm')
|
||||
cy.get('.modal')
|
||||
.contains('button', 'Dismiss')
|
||||
.click()
|
||||
.wait('@tasks')
|
||||
.wait(2000)
|
||||
.then(() => {
|
||||
cy.get('tbody').find('tr:visible').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1 +1,73 @@
|
||||
{"count":3,"next":null,"previous":null,"results":[{"id":1,"correspondent":9,"document_type":1,"title":"lorem-ipsum","content":"Test document PDF \n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla est purus, ultrices in porttitor \nin, accumsan non quam. Nam consectetur porttitor rhoncus. Curabitur eu est et leo feugiat \nauctor vel quis lorem. Ut et ligula dolor, sit amet consequat lorem. Aliquam porta eros sed \nvelit imperdiet egestas. Maecenas tempus eros ut diam ullamcorper id dictum libero \ntempor. Donec quis augue quis magna condimentum lobortis. Quisque imperdiet ipsum vel \nmagna viverra rutrum. Cras viverra molestie urna, vitae vestibulum turpis varius id. \nVestibulum mollis, arcu iaculis bibendum varius, velit sapien blandit metus, ac posuere lorem \nnulla ac dolor. Maecenas urna elit, tincidunt in dapibus nec, vehicula eu dui. Duis lacinia \nfringilla massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur \nridiculus mus. Ut consequat ultricies est, non rhoncus mauris congue porta. Vivamus viverra \nsuscipit felis eget condimentum. Cum sociis natoque penatibus et magnis dis parturient \nmontes, nascetur ridiculus mus. Integer bibendum sagittis ligula, non faucibus nulla volutpat \nvitae. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. \nIn aliquet quam et velit bibendum accumsan. Cum sociis natoque penatibus et magnis dis \nparturient montes, nascetur ridiculus mus. Vestibulum vitae ipsum nec arcu semper \nadipiscing at ac lacus. Praesent id pellentesque orci. Morbi congue viverra nisl nec rhoncus. \nInteger mattis, ipsum a tincidunt commodo, lacus arcu elementum elit, at mollis eros ante ac \nrisus. In volutpat, ante at pretium ultricies, velit magna suscipit enim, aliquet blandit massa \norci nec lorem. Nulla facilisi. Duis eu vehicula arcu. Nulla facilisi. Maecenas pellentesque \nvolutpat felis, quis tristique ligula luctus vel. Sed nec mi eros. Integer augue enim, sollicitudin \nullamcorper mattis eget, aliquam in est. Morbi sollicitudin libero nec augue dignissim ut \nconsectetur dui volutpat. Nulla facilisi. Mauris egestas vestibulum neque cursus tincidunt. \nDonec sit amet pulvinar orci. \nQuisque volutpat pharetra tincidunt. Fusce sapien arcu, molestie eget varius egestas, \nfaucibus ac urna. Sed at nisi in velit egestas aliquam ut a felis. Aenean malesuada iaculis nisl, \nut tempor lacus egestas consequat. Nam nibh lectus, gravida sed egestas ut, feugiat quis \ndolor. Donec eu leo enim, non laoreet ante. Morbi dictum tempor vulputate. Phasellus \nultricies risus vel augue sagittis euismod. Vivamus tincidunt placerat nisi in aliquam. Cras \nquis mi ac nunc pretium aliquam. Aenean elementum erat ac metus commodo rhoncus. \nAliquam nulla augue, porta non sagittis quis, accumsan vitae sem. Phasellus id lectus tortor, \neget pulvinar augue. Etiam eget velit ac purus fringilla blandit. Donec odio odio, sagittis sed \niaculis sed, consectetur eget sem. Lorem ipsum dolor sit amet, consectetur adipiscing elit. \nMaecenas accumsan velit vel turpis rutrum in sodales diam placerat. \nQuisque luctus ullamcorper velit sit amet lobortis. Etiam ligula felis, vulputate quis rhoncus \nnec, fermentum eget odio. Vivamus vel ipsum ac augue sodales mollis euismod nec tellus. \nFusce et augue rutrum nunc semper vehicula vel semper nisl. Nam laoreet euismod quam at \nvarius. Sed aliquet auctor nibh. Curabitur malesuada fermentum lacus vel accumsan. Duis \nornare scelerisque nulla, ac pulvinar ligula tempus sit amet. In placerat nulla ac ante \nscelerisque posuere. Phasellus at ante felis. Sed hendrerit risus a metus posuere rutrum. \nPhasellus eu augue dui. Proin in vestibulum ipsum. Aenean accumsan mollis sapien, ut \neleifend sem blandit at. Vivamus luctus mi eget lorem lobortis pharetra. Phasellus at tortor \nquam, a volutpat purus. Etiam sollicitudin arcu vel elit bibendum et imperdiet risus tincidunt. \nEtiam elit velit, posuere ut pulvinar ac, condimentum eget justo. Fusce a erat velit. Vivamus \nimperdiet ultrices orci in hendrerit.","tags":[4],"created":"2022-03-22T07:24:18Z","modified":"2022-03-22T07:24:23.264859Z","added":"2022-03-22T07:24:22.922631Z","archive_serial_number":null,"original_file_name":"2022-03-22 lorem-ipsum.pdf","archived_file_name":"2022-03-22 lorem-ipsum.pdf"},{"id":2,"correspondent":null,"document_type":null,"title":"lorem-ipsum","content":"Test document PDF","tags":[],"created":"2022-03-23T07:24:18Z","modified":"2022-03-23T07:24:23.264859Z","added":"2022-03-23T07:24:22.922631Z","archive_serial_number":null,"original_file_name":"2022-03-23 lorem-ipsum.pdf","archived_file_name":"2022-03-23 lorem-ipsum.pdf"},{"id":3,"correspondent":null,"document_type":1,"title":"lorem-ipsum","content":"Test document PDF","tags":[2],"created":"2022-03-24T07:24:18Z","modified":"2022-03-24T07:24:23.264859Z","added":"2022-03-24T07:24:22.922631Z","archive_serial_number":null,"original_file_name":"2022-03-24 lorem-ipsum.pdf","Line truncated
|
||||
{
|
||||
"count": 3,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"correspondent": 9,
|
||||
"document_type": 1,
|
||||
"storage_path": null,
|
||||
"title": "No latin title",
|
||||
"content": "Test document PDF \n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla est purus, ultrices in porttitor \nin, accumsan non quam. Nam consectetur porttitor rhoncus. Curabitur eu est et leo feugiat \nauctor vel quis lorem. Ut et ligula dolor, sit amet consequat lorem. Aliquam porta eros sed \nvelit imperdiet egestas. Maecenas tempus eros ut diam ullamcorper id dictum libero \ntempor. Donec quis augue quis magna condimentum lobortis. Quisque imperdiet ipsum vel \nmagna viverra rutrum. Cras viverra molestie urna, vitae vestibulum turpis varius id. \nVestibulum mollis, arcu iaculis bibendum varius, velit sapien blandit metus, ac posuere lorem \nnulla ac dolor. Maecenas urna elit, tincidunt in dapibus nec, vehicula eu dui. Duis lacinia \nfringilla massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur \nridiculus mus. Ut consequat ultricies est, non rhoncus mauris congue porta. Vivamus viverra \nsuscipit felis eget condimentum. Cum sociis natoque penatibus et magnis dis parturient \nmontes, nascetur ridiculus mus. Integer bibendum sagittis ligula, non faucibus nulla volutpat \nvitae. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. \nIn aliquet quam et velit bibendum accumsan. Cum sociis natoque penatibus et magnis dis \nparturient montes, nascetur ridiculus mus. Vestibulum vitae ipsum nec arcu semper \nadipiscing at ac lacus. Praesent id pellentesque orci. Morbi congue viverra nisl nec rhoncus. \nInteger mattis, ipsum a tincidunt commodo, lacus arcu elementum elit, at mollis eros ante ac \nrisus. In volutpat, ante at pretium ultricies, velit magna suscipit enim, aliquet blandit massa \norci nec lorem. Nulla facilisi. Duis eu vehicula arcu. Nulla facilisi. Maecenas pellentesque \nvolutpat felis, quis tristique ligula luctus vel. Sed nec mi eros. Integer augue enim, sollicitudin \nullamcorper mattis eget, aliquam in est. Morbi sollicitudin libero nec augue dignissim ut \nconsectetur dui volutpat. Nulla facilisi. Mauris egestas vestibulum neque cursus tincidunt. \nDonec sit amet pulvinar orci. \nQuisque volutpat pharetra tincidunt. Fusce sapien arcu, molestie eget varius egestas, \nfaucibus ac urna. Sed at nisi in velit egestas aliquam ut a felis. Aenean malesuada iaculis nisl, \nut tempor lacus egestas consequat. Nam nibh lectus, gravida sed egestas ut, feugiat quis \ndolor. Donec eu leo enim, non laoreet ante. Morbi dictum tempor vulputate. Phasellus \nultricies risus vel augue sagittis euismod. Vivamus tincidunt placerat nisi in aliquam. Cras \nquis mi ac nunc pretium aliquam. Aenean elementum erat ac metus commodo rhoncus. \nAliquam nulla augue, porta non sagittis quis, accumsan vitae sem. Phasellus id lectus tortor, \neget pulvinar augue. Etiam eget velit ac purus fringilla blandit. Donec odio odio, sagittis sed \niaculis sed, consectetur eget sem. Lorem ipsum dolor sit amet, consectetur adipiscing elit. \nMaecenas accumsan velit vel turpis rutrum in sodales diam placerat. \nQuisque luctus ullamcorper velit sit amet lobortis. Etiam ligula felis, vulputate quis rhoncus \nnec, fermentum eget odio. Vivamus vel ipsum ac augue sodales mollis euismod nec tellus. \nFusce et augue rutrum nunc semper vehicula vel semper nisl. Nam laoreet euismod quam at \nvarius. Sed aliquet auctor nibh. Curabitur malesuada fermentum lacus vel accumsan. Duis \nornare scelerisque nulla, ac pulvinar ligula tempus sit amet. In placerat nulla ac ante \nscelerisque posuere. Phasellus at ante felis. Sed hendrerit risus a metus posuere rutrum. \nPhasellus eu augue dui. Proin in vestibulum ipsum. Aenean accumsan mollis sapien, ut \neleifend sem blandit at. Vivamus luctus mi eget lorem lobortis pharetra. Phasellus at tortor \nquam, a volutpat purus. Etiam sollicitudin arcu vel elit bibendum et imperdiet risus tincidunt. \nEtiam elit velit, posuere ut pulvinar ac, condimentum eget justo. Fusce a erat velit. Vivamus \nimperdiet ultrices orci in hendrerit.",
|
||||
"tags": [
|
||||
4
|
||||
],
|
||||
"created": "2022-03-22T07:24:18Z",
|
||||
"modified": "2022-03-22T07:24:23.264859Z",
|
||||
"added": "2022-03-22T07:24:22.922631Z",
|
||||
"archive_serial_number": null,
|
||||
"original_file_name": "2022-03-22 no latin title.pdf",
|
||||
"archived_file_name": "2022-03-22 no latin title.pdf"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"correspondent": null,
|
||||
"document_type": null,
|
||||
"storage_path": 2,
|
||||
"title": "lorem ipsum dolor sit amet",
|
||||
"content": "Test document PDF",
|
||||
"tags": [],
|
||||
"created": "2022-03-23T07:24:18Z",
|
||||
"modified": "2022-03-23T07:24:23.264859Z",
|
||||
"added": "2022-03-23T07:24:22.922631Z",
|
||||
"archive_serial_number": 12345,
|
||||
"original_file_name": "2022-03-23 lorem ipsum dolor sit amet.pdf",
|
||||
"archived_file_name": "2022-03-23 llorem ipsum dolor sit amet.pdf"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"correspondent": null,
|
||||
"document_type": 1,
|
||||
"storage_path": null,
|
||||
"title": "dolor",
|
||||
"content": "Test document PDF",
|
||||
"tags": [
|
||||
2
|
||||
],
|
||||
"created": "2022-03-24T07:24:18Z",
|
||||
"modified": "2022-03-24T07:24:23.264859Z",
|
||||
"added": "2022-03-24T07:24:22.922631Z",
|
||||
"archive_serial_number": null,
|
||||
"original_file_name": "2022-03-24 dolor.pdf",
|
||||
"archived_file_name": "2022-03-24 dolor.pdf"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"correspondent": 9,
|
||||
"document_type": 1,
|
||||
"storage_path": null,
|
||||
"title": "sit amet",
|
||||
"content": "Test document PDF",
|
||||
"tags": [
|
||||
4, 5
|
||||
],
|
||||
"created": "2022-06-01T07:24:18Z",
|
||||
"modified": "2022-06-01T07:24:23.264859Z",
|
||||
"added": "2022-06-01T07:24:22.922631Z",
|
||||
"archive_serial_number": 12347,
|
||||
"original_file_name": "2022-06-01 sit amet.pdf",
|
||||
"archived_file_name": "2022-06-01 sit amet.pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":"v1.7.1","update_available":false,"feature_is_set":true}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 2,
|
||||
"slug": "year-title",
|
||||
"name": "Year - Title",
|
||||
"path": "{created_year}/{title}",
|
||||
"match": "",
|
||||
"matching_algorithm": 6,
|
||||
"is_insensitive": true,
|
||||
"document_count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +1,103 @@
|
||||
{"count":8,"next":null,"previous":null,"results":[{"id":4,"slug":"another-sample-tag","name":"Another Sample Tag","color":"#a6cee3","text_color":"#000000","match":"","matching_algorithm":6,"is_insensitive":true,"is_inbox_tag":false,"document_count":3},{"id":7,"slug":"newone","name":"NewOne","color":"#9e4ad1","text_color":"#ffffff","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":2},{"id":6,"slug":"partial-tag","name":"Partial Tag","color":"#72dba7","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":1},{"id":2,"slug":"tag-2","name":"Tag 2","color":"#612db7","text_color":"#ffffff","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":3},{"id":3,"slug":"tag-3","name":"Tag 3","color":"#b2df8a","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":4},{"id":5,"slug":"tagwithpartial","name":"TagWithPartial","color":"#3b2db4","text_color":"#ffffff","match":"","matching_algorithm":6,"is_insensitive":true,"is_inbox_tag":false,"document_count":2},{"id":8,"slug":"test-another","name":"Test Another","color":"#3ccea5","text_color":"#000000","match":"","matching_algorithm":4,"is_insensitive":true,"is_inbox_tag":false,"document_count":0},{"id":1,"slug":"test-tag","name":"Test Tag","color":"#fb9a99","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":4}]}
|
||||
{
|
||||
"count": 8,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 4,
|
||||
"slug": "another-sample-tag",
|
||||
"name": "Another Sample Tag",
|
||||
"color": "#a6cee3",
|
||||
"text_color": "#000000",
|
||||
"match": "",
|
||||
"matching_algorithm": 6,
|
||||
"is_insensitive": true,
|
||||
"is_inbox_tag": false,
|
||||
"document_count": 3
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"slug": "newone",
|
||||
"name": "NewOne",
|
||||
"color": "#9e4ad1",
|
||||
"text_color": "#ffffff",
|
||||
"match": "",
|
||||
"matching_algorithm": 1,
|
||||
"is_insensitive": true,
|
||||
"is_inbox_tag": false,
|
||||
"document_count": 2
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"slug": "partial-tag",
|
||||
"name": "Partial Tag",
|
||||
"color": "#72dba7",
|
||||
"text_color": "#000000",
|
||||
"match": "",
|
||||
"matching_algorithm": 1,
|
||||
"is_insensitive": true,
|
||||
"is_inbox_tag": false,
|
||||
"document_count": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"slug": "tag-2",
|
||||
"name": "Tag 2",
|
||||
"color": "#612db7",
|
||||
"text_color": "#ffffff",
|
||||
"match": "",
|
||||
"matching_algorithm": 1,
|
||||
"is_insensitive": true,
|
||||
"is_inbox_tag": false,
|
||||
"document_count": 3
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"slug": "tag-3",
|
||||
"name": "Tag 3",
|
||||
"color": "#b2df8a",
|
||||
"text_color": "#000000",
|
||||
"match": "",
|
||||
"matching_algorithm": 1,
|
||||
"is_insensitive": true,
|
||||
"is_inbox_tag": false,
|
||||
"document_count": 4
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"slug": "tagwithpartial",
|
||||
"name": "TagWithPartial",
|
||||
"color": "#3b2db4",
|
||||
"text_color": "#ffffff",
|
||||
"match": "",
|
||||
"matching_algorithm": 6,
|
||||
"is_insensitive": true,
|
||||
"is_inbox_tag": false,
|
||||
"document_count": 2
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"slug": "test-another",
|
||||
"name": "Test Another",
|
||||
"color": "#3ccea5",
|
||||
"text_color": "#000000",
|
||||
"match": "",
|
||||
"matching_algorithm": 4,
|
||||
"is_insensitive": true,
|
||||
"is_inbox_tag": false,
|
||||
"document_count": 0
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"slug": "test-tag",
|
||||
"name": "Test Tag",
|
||||
"color": "#fb9a99",
|
||||
"text_color": "#000000",
|
||||
"match": "",
|
||||
"matching_algorithm": 1,
|
||||
"is_insensitive": true,
|
||||
"is_inbox_tag": false,
|
||||
"document_count": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[{"id":141,"type":"file","result":"sample 2.pdf: Not consuming sample 2.pdf: It is a duplicate. : Traceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ngx.nosync-udqDZzaE/lib/python3.8/site-packages/django_q/cluster.py\", line 432, in worker\n res = f(*task[\"args\"], **task[\"kwargs\"])\n File \"/Users/admin/Documents/paperless-ngx/src/documents/tasks.py\", line 316, in consume_file\n document = Consumer().try_consume_file(\n File \"/Users/admin/Documents/paperless-ngx/src/documents/consumer.py\", line 218, in try_consume_file\n self.pre_check_duplicate()\n File \"/Users/admin/Documents/paperless-ngx/src/documents/consumer.py\", line 113, in pre_check_duplicate\n self._fail(\n File \"/Users/admin/Documents/paperless-ngx/src/documents/consumer.py\", line 84, in _fail\n raise ConsumerError(f\"{self.filename}: {log_message or message}\")\ndocuments.consumer.ConsumerError: sample 2.pdf: Not consuming sample 2.pdf: It is a duplicate.\n","status":"failed","task_id":"d8ddbe298a42427d82553206ddf0bc94","name":"sample 2.pdf","created":"2022-05-26T23:17:38.333474-07:00","acknowledged":false,"attempted_task":{"id":"d8ddbe298a42427d82553206ddf0bc94","name":"sample 2.pdf","func":"documents.tasks.consume_file","hook":null,"args":"gAWVLgAAAAAAAACMKC90bXAvcGFwZXJsZXNzL3BhcGVybGVzcy11cGxvYWQtanJxNGs1aHOUhZQu","kwargs":"gAWVzQAAAAAAAAB9lCiMEW92ZXJyaWRlX2ZpbGVuYW1llIwMc2FtcGxlIDIucGRmlIwOb3ZlcnJpZGVfdGl0bGWUTowZb3ZlcnJpZGVfY29ycmVzcG9uZGVudF9pZJROjBlvdmVycmlkZV9kb2N1bWVudF90eXBlX2lklE6MEG92ZXJyaWRlX3RhZ19pZHOUTowHdGFza19pZJSMJDcyMGExYjI5LWI2OTYtNDY3My05Y2ZmLTJkY2ZiZWNmNWViMpSMEG92ZXJyaWRlX2NyZWF0ZWSUTnUu","result":"gAWVMQQAAAAAAABYKgQAAHNhbXBsZSAyLnBkZjogTm90IGNvbnN1bWluZyBzYW1wbGUgMi5wZGY6IEl0IGlzIGEgZHVwbGljYXRlLiA6IFRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KToKICBGaWxlICIvVXNlcnMvbW9vbmVyLy5sb2NhbC9zaGFyZS92aXJ0dWFsZW52cy9wYXBlcmxlc3Mtbmd4Lm5vc3luYy11ZHFEWnphRS9saWIvcHl0aG9uMy44L3NpdGUtcGFja2FnZXMvZGphbmdvX3EvY2x1c3Rlci5weSIsIGxpbmUgNDMyLCBpbiB3b3JrZXIKICAgIHJlcyA9IGYoKnRhc2tbImFyZ3MiXSwgKip0YXNrWyJrd2FyZ3MiXSkKICBGaWxlICIvVXNlcnMvbW9vbmVyL0RvY3VtZW50cy9Xb3JrL0Rldi5ub3N5bmMvQ29udHJpYnV0aW9ucy9wYXBlcmxlc3Mtbmd4L3NyYy9kb2N1bWVudHMvdGFza3MucHkiLCBsaW5lIDMxNiwgaW4gY29uc3VtZV9maWxlCiAgICBkb2N1bWVudCA9IENvbnN1bWVyKCkudHJ5X2NvbnN1bWVfZmlsZSgKICBGaWxlICIvVXNlcnMvbW9vbmVyL0RvY3VtZW50cy9Xb3JrL0Rldi5ub3N5bmMvQ29udHJpYnV0aW9ucy9wYXBlcmxlc3Mtbmd4L3NyYy9kb2N1bWVudHMvY29uc3VtZXIucHkiLCBsaW5lIDIxOCwgaW4gdHJ5X2NvbnN1bWVfZmlsZQogICAgc2VsZi5wcmVfY2hlY2tfZHVwbGljYXRlKCkKICBGaWxlICIvVXNlcnMvbW9vbmVyL0RvY3VtZW50cy9Xb3JrL0Rldi5ub3N5bmMvQ29udHJpYnV0aW9ucy9wYXBlcmxlc3Mtbmd4L3NyYy9kb2N1bWVudHMvY29uc3VtZXIucHkiLCBsaW5lIDExMywgaW4gcHJlX2NoZWNrX2R1cGxpY2F0ZQogICAgc2VsZi5fZmFpbCgKICBGaWxlICIvVXNlcnMvbW9vbmVyL0RvY3VtZW50cy9Xb3JrL0Rldi5ub3N5bmMvQ29udHJpYnV0aW9ucy9wYXBlcmxlc3Mtbmd4L3NyYy9kb2N1bWVudHMvY29uc3VtZXIucHkiLCBsaW5lIDg0LCBpbiBfZmFpbAogICAgcmFpc2UgQ29uc3VtZXJFcnJvcihmIntzZWxmLmZpbGVuYW1lfToge2xvZ19tZXNzYWdlIG9yIG1lc3NhZ2V9IikKZG9jdW1lbnRzLmNvbnN1bWVyLkNvbnN1bWVyRXJyb3I6IHNhbXBsZSAyLnBkZjogTm90IGNvbnN1bWluZyBzYW1wbGUgMi5wZGY6IEl0IGlzIGEgZHVwbGljYXRlLgqULg==","group":null,"started":"2022-05-26T23:17:37.702432-07:00","stopped":"2022-05-26T23:17:38.313306-07:00","success":false,"attempt_count":1}},{"id":132,"type":"file","result":" : Traceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/subprocess.py\", line 131, in get_version\n env=env,\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/subprocess.py\", line 68, in run\n proc = subprocess_run(args, env=env, **kwargs)\n File \"/Users/admin/opt/anaconda3/envs/paperless-ng/lib/python3.6/subprocess.py\", line 423, in run\n with Popen(*popenargs, **kwargs) as process:\n File \"/Users/admin/opt/anaconda3/envs/paperless-ng/lib/python3.6/subprocess.py\", line 729, in __init__\n restore_signals, start_new_session)\n File \"/Users/admin/opt/anaconda3/envs/paperless-ng/lib/python3.6/subprocess.py\", line 1364, in _execute_child\n raise child_exception_type(errno_num, err_msg, err_filename)\nFileNotFoundError: [Errno 2] No such file or directory: 'unpaper': 'unpaper'\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/subprocess.py\", line 287, in check_external_program\n found_version = version_checker()\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/_exec/unpaper.py\", line 34, in version\n return get_version('unpaper')\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/subprocess.py\", line 137, in get_version\n ) from e\nocrmypdf.exceptions.MissingDependencyError: Could not find program 'unpaper' on the PATH\n\nDuring handling of the above exceptioLine truncated
|
||||
@@ -0,0 +1,43 @@
|
||||
// mock API methods
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept('http://localhost:8000/api/ui_settings/', {
|
||||
fixture: 'ui_settings/settings.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/remote_version/', {
|
||||
fixture: 'remote_version/remote_version.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/saved_views/*', {
|
||||
fixture: 'saved_views/savedviews.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/tags/*', {
|
||||
fixture: 'tags/tags.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/correspondents/*', {
|
||||
fixture: 'correspondents/correspondents.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/document_types/*', {
|
||||
fixture: 'document_types/doctypes.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/storage_paths/*', {
|
||||
fixture: 'storage_paths/storage_paths.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/documents/1/metadata/', {
|
||||
fixture: 'documents/1/metadata.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/documents/1/suggestions/', {
|
||||
fixture: 'documents/1/suggestions.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/documents/1/thumb/', {
|
||||
fixture: 'documents/lorem-ipsum.png',
|
||||
})
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||
// import './commands';
|
||||
@@ -13,48 +13,48 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "~13.3.5",
|
||||
"@angular/compiler": "~13.3.5",
|
||||
"@angular/core": "~13.3.5",
|
||||
"@angular/forms": "~13.3.5",
|
||||
"@angular/localize": "~13.3.5",
|
||||
"@angular/platform-browser": "~13.3.5",
|
||||
"@angular/platform-browser-dynamic": "~13.3.5",
|
||||
"@angular/router": "~13.3.5",
|
||||
"@ng-bootstrap/ng-bootstrap": "^12.1.1",
|
||||
"@ng-select/ng-select": "^8.1.1",
|
||||
"@angular/common": "~14.0.4",
|
||||
"@angular/compiler": "~14.0.4",
|
||||
"@angular/core": "~14.0.4",
|
||||
"@angular/forms": "~14.0.4",
|
||||
"@angular/localize": "~14.0.4",
|
||||
"@angular/platform-browser": "~14.0.4",
|
||||
"@angular/platform-browser-dynamic": "~14.0.4",
|
||||
"@angular/router": "~14.0.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^13.0.0-beta.1",
|
||||
"@ng-select/ng-select": "^9.0.2",
|
||||
"@ngneat/dirty-check-forms": "^3.0.2",
|
||||
"@popperjs/core": "^2.11.4",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"bootstrap": "^5.1.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"ng2-pdf-viewer": "^9.0.0",
|
||||
"ngx-color": "^7.3.3",
|
||||
"ngx-cookie-service": "^13.1.2",
|
||||
"ngx-cookie-service": "^14.0.1",
|
||||
"ngx-file-drop": "^13.0.0",
|
||||
"rxjs": "~7.5.5",
|
||||
"tslib": "^2.3.1",
|
||||
"uuid": "^8.3.1",
|
||||
"zone.js": "~0.11.4"
|
||||
"zone.js": "~0.11.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "13.0.3",
|
||||
"@angular-devkit/build-angular": "~13.3.4",
|
||||
"@angular/cli": "~13.3.4",
|
||||
"@angular/compiler-cli": "~13.3.5",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "^17.0.30",
|
||||
"@angular-builders/jest": "14.0.0",
|
||||
"@angular-devkit/build-angular": "~14.0.4",
|
||||
"@angular/cli": "~14.0.4",
|
||||
"@angular/compiler-cli": "~14.0.4",
|
||||
"@types/jest": "28.1.4",
|
||||
"@types/node": "^18.0.0",
|
||||
"codelyzer": "^6.0.2",
|
||||
"concurrently": "7.1.0",
|
||||
"jest": "28.0.3",
|
||||
"jest-environment-jsdom": "^28.0.2",
|
||||
"jest-preset-angular": "^12.0.0-next.1",
|
||||
"ts-node": "~10.7.0",
|
||||
"concurrently": "7.2.2",
|
||||
"jest": "28.1.2",
|
||||
"jest-environment-jsdom": "^28.1.2",
|
||||
"jest-preset-angular": "^12.1.0",
|
||||
"ts-node": "~10.8.1",
|
||||
"tslint": "~6.1.3",
|
||||
"typescript": "~4.6.3",
|
||||
"wait-on": "~6.0.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^1.6.0",
|
||||
"cypress": "~9.6.0"
|
||||
"@cypress/schematic": "^2.0.0",
|
||||
"cypress": "~10.3.0"
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
@@ -35,6 +36,7 @@ const routes: Routes = [
|
||||
component: SettingsComponent,
|
||||
canDeactivate: [DirtyFormGuard],
|
||||
},
|
||||
{ path: 'tasks', component: TasksComponent },
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ConsumerStatusService } from './services/consumer-status.service'
|
||||
import { ToastService } from './services/toast.service'
|
||||
import { NgxFileDropEntry } from 'ngx-file-drop'
|
||||
import { UploadDocumentsService } from './services/upload-documents.service'
|
||||
import { TasksService } from './services/tasks.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -27,7 +28,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private toastService: ToastService,
|
||||
private router: Router,
|
||||
private uploadDocumentsService: UploadDocumentsService
|
||||
private uploadDocumentsService: UploadDocumentsService,
|
||||
private tasksService: TasksService
|
||||
) {
|
||||
let anyWindow = window as any
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
|
||||
@@ -65,6 +67,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.successSubscription = this.consumerStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.subscribe((status) => {
|
||||
this.tasksService.reload()
|
||||
if (
|
||||
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)
|
||||
) {
|
||||
@@ -83,6 +86,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.failedSubscription = this.consumerStatusService
|
||||
.onDocumentConsumptionFailed()
|
||||
.subscribe((status) => {
|
||||
this.tasksService.reload()
|
||||
if (
|
||||
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED)
|
||||
) {
|
||||
@@ -95,6 +99,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.newDocumentSubscription = this.consumerStatusService
|
||||
.onDocumentDetected()
|
||||
.subscribe((status) => {
|
||||
this.tasksService.reload()
|
||||
if (
|
||||
this.showNotification(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT
|
||||
|
||||
@@ -61,7 +61,7 @@ import { SafeUrlPipe } from './pipes/safeurl.pipe'
|
||||
import { SafeHtmlPipe } from './pipes/safehtml.pipe'
|
||||
import { CustomDatePipe } from './pipes/custom-date.pipe'
|
||||
import { DateComponent } from './components/common/input/date/date.component'
|
||||
import { ISODateTimeAdapter } from './utils/ngb-iso-date-time-adapter'
|
||||
import { ISODateAdapter } from './utils/ngb-iso-date-adapter'
|
||||
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
|
||||
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
|
||||
import { ColorSliderModule } from 'ngx-color/slider'
|
||||
@@ -90,6 +90,7 @@ import localeZh from '@angular/common/locales/zh'
|
||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { SettingsService } from './services/settings.service'
|
||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||
|
||||
registerLocaleData(localeBe)
|
||||
registerLocaleData(localeCs)
|
||||
@@ -171,6 +172,7 @@ function initializeApp(settings: SettingsService) {
|
||||
DateComponent,
|
||||
ColorComponent,
|
||||
DocumentAsnComponent,
|
||||
TasksComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -205,7 +207,7 @@ function initializeApp(settings: SettingsService) {
|
||||
},
|
||||
FilterPipe,
|
||||
DocumentTitlePipe,
|
||||
{ provide: NgbDateAdapter, useClass: ISODateTimeAdapter },
|
||||
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
||||
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -141,6 +141,13 @@
|
||||
</svg> <ng-container i18n>Storage paths</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#list-task"/>
|
||||
</svg> <ng-container i18n>File Tasks<ng-container *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></ng-container></ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormControl } from '@angular/forms'
|
||||
import { ActivatedRoute, Router, Params } from '@angular/router'
|
||||
import { from, Observable, Subscription, BehaviorSubject } from 'rxjs'
|
||||
import { from, Observable } from 'rxjs'
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
@@ -15,15 +15,14 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SearchService } from 'src/app/services/rest/search.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||
import { Meta } from '@angular/platform-browser'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'
|
||||
import {
|
||||
RemoteVersionService,
|
||||
AppRemoteVersion,
|
||||
} from 'src/app/services/rest/remote-version.service'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-frame',
|
||||
@@ -38,14 +37,16 @@ export class AppFrameComponent {
|
||||
private searchService: SearchService,
|
||||
public savedViewService: SavedViewService,
|
||||
private remoteVersionService: RemoteVersionService,
|
||||
private queryParamsService: QueryParamsService,
|
||||
public settingsService: SettingsService
|
||||
private list: DocumentListViewService,
|
||||
public settingsService: SettingsService,
|
||||
public tasksService: TasksService
|
||||
) {
|
||||
this.remoteVersionService
|
||||
.checkForUpdates()
|
||||
.subscribe((appRemoteVersion: AppRemoteVersion) => {
|
||||
this.appRemoteVersion = appRemoteVersion
|
||||
})
|
||||
tasksService.reload()
|
||||
}
|
||||
|
||||
versionString = `${environment.appTitle} ${environment.version}`
|
||||
@@ -94,7 +95,7 @@ export class AppFrameComponent {
|
||||
|
||||
search() {
|
||||
this.closeMenu()
|
||||
this.queryParamsService.navigateWithFilterRules([
|
||||
this.list.quickFilter([
|
||||
{
|
||||
rule_type: FILTER_FULLTEXT_QUERY,
|
||||
value: (this.searchField.value as string).trim(),
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="list-group list-group-flush">
|
||||
<div *ngIf="!editing && multiple" class="list-group-item d-flex">
|
||||
<div class="btn-group btn-group-xs btn-group-toggle flex-fill" ngbRadioGroup [(ngModel)]="selectionModel.logicalOperator" (change)="selectionModel.toggleOperator()" [disabled]="!operatorToggleEnabled">
|
||||
<label ngbButtonLabel class="btn btn-outline-primary">
|
||||
<input ngbButton type="radio" class="btn-check" name="logicalOperator" value="and" i18n> All
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn btn-outline-primary">
|
||||
<input ngbButton type="radio" class="btn-check" name="logicalOperator" value="or" i18n> Any
|
||||
</label>
|
||||
<div class="btn-group btn-group-xs flex-fill">
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd" value="and">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorAnd" i18n>All</label>
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr" value="or">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorOr" i18n>Any</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
|
||||
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)"
|
||||
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
|
||||
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel">
|
||||
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, forwardRef, OnInit } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@Component({
|
||||
@@ -19,7 +21,10 @@ export class DateComponent
|
||||
extends AbstractInputComponent<string>
|
||||
implements OnInit
|
||||
{
|
||||
constructor(private settings: SettingsService) {
|
||||
constructor(
|
||||
private settings: SettingsService,
|
||||
private ngbDateParserFormatter: NgbDateParserFormatter
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@@ -30,7 +35,20 @@ export class DateComponent
|
||||
|
||||
placeholder: string
|
||||
|
||||
// prevent chars other than numbers and separators
|
||||
onPaste(event: ClipboardEvent) {
|
||||
const clipboardData: DataTransfer =
|
||||
event.clipboardData || window['clipboardData']
|
||||
if (clipboardData) {
|
||||
event.preventDefault()
|
||||
let pastedText = clipboardData.getData('text')
|
||||
pastedText = pastedText.replace(/[\sa-z#!$%\^&\*;:{}=\-_`~()]+/g, '')
|
||||
const parsedDate = this.ngbDateParserFormatter.parse(pastedText)
|
||||
const formattedDate = this.ngbDateParserFormatter.format(parsedDate)
|
||||
this.writeValue(formattedDate)
|
||||
this.onChange(formattedDate)
|
||||
}
|
||||
}
|
||||
|
||||
onKeyPress(event: KeyboardEvent) {
|
||||
if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
a {
|
||||
cursor: pointer;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
text-align: end;
|
||||
}
|
||||
@@ -4,5 +4,5 @@
|
||||
[class]="toast.classname"
|
||||
(hidden)="toastService.closeToast(toast)">
|
||||
<p>{{toast.content}}</p>
|
||||
<p *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
|
||||
<p class="mb-0" *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
|
||||
</ngb-toast>
|
||||
@@ -12,7 +12,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let doc of documents" (click)="openDocumentsService.openDocument(doc)">
|
||||
<td>{{doc.created | customDate}}</td>
|
||||
<td>{{doc.created_date | customDate}}</td>
|
||||
<td>{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t); $event.stopPropagation();"></app-tag></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-saved-view-widget',
|
||||
@@ -21,7 +21,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private router: Router,
|
||||
private queryParamsService: QueryParamsService,
|
||||
private list: DocumentListViewService,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
public openDocumentsService: OpenDocumentsService
|
||||
) {}
|
||||
@@ -47,7 +47,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.loading = true
|
||||
this.loading = this.documents.length == 0
|
||||
this.documentService
|
||||
.listFiltered(
|
||||
1,
|
||||
@@ -73,7 +73,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
clickTag(tag: PaperlessTag) {
|
||||
this.queryParamsService.navigateWithFilterRules([
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() },
|
||||
])
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
<app-input-text #inputTitle i18n-title title="Title" formControlName="title" (keyup)="titleKeyUp($event)" [error]="error?.title"></app-input-text>
|
||||
<app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number>
|
||||
<app-input-date i18n-title title="Date created" formControlName="created" [error]="error?.created"></app-input-date>
|
||||
<app-input-date i18n-title title="Date created" formControlName="created_date" [error]="error?.created_date"></app-input-date>
|
||||
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
|
||||
(createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents"></app-input-select>
|
||||
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
|
||||
|
||||
@@ -14,10 +14,14 @@
|
||||
}
|
||||
|
||||
::ng-deep .ng2-pdf-viewer-container .page {
|
||||
--page-margin: 1px 0 -8px;
|
||||
--page-margin: 1px 0 10px;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
::ng-deep .ng2-pdf-viewer-container .page:last-child {
|
||||
--page-margin: 1px 0 20px;
|
||||
}
|
||||
|
||||
.password-prompt {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
|
||||
@@ -31,8 +31,6 @@ import {
|
||||
} from 'rxjs/operators'
|
||||
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions'
|
||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
||||
import { normalizeDateStr } from 'src/app/utils/date'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
@@ -74,7 +72,7 @@ export class DocumentDetailComponent
|
||||
documentForm: FormGroup = new FormGroup({
|
||||
title: new FormControl(''),
|
||||
content: new FormControl(''),
|
||||
created: new FormControl(),
|
||||
created_date: new FormControl(),
|
||||
correspondent: new FormControl(),
|
||||
document_type: new FormControl(),
|
||||
storage_path: new FormControl(),
|
||||
@@ -120,8 +118,7 @@ export class DocumentDetailComponent
|
||||
private documentTitlePipe: DocumentTitlePipe,
|
||||
private toastService: ToastService,
|
||||
private settings: SettingsService,
|
||||
private storagePathService: StoragePathService,
|
||||
private queryParamsService: QueryParamsService
|
||||
private storagePathService: StoragePathService
|
||||
) {}
|
||||
|
||||
titleKeyUp(event) {
|
||||
@@ -141,27 +138,8 @@ export class DocumentDetailComponent
|
||||
ngOnInit(): void {
|
||||
this.documentForm.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((changes) => {
|
||||
.subscribe(() => {
|
||||
this.error = null
|
||||
if (this.ogDate) {
|
||||
try {
|
||||
let newDate = new Date(normalizeDateStr(changes['created']))
|
||||
newDate.setHours(
|
||||
this.ogDate.getHours(),
|
||||
this.ogDate.getMinutes(),
|
||||
this.ogDate.getSeconds(),
|
||||
this.ogDate.getMilliseconds()
|
||||
)
|
||||
this.documentForm.patchValue(
|
||||
{ created: newDate.toISOString() },
|
||||
{ emitEvent: false }
|
||||
)
|
||||
} catch (e) {
|
||||
// catch this before we try to save and simulate an api error
|
||||
this.error = { created: e.message }
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(this.document, this.documentForm.value)
|
||||
})
|
||||
|
||||
@@ -233,13 +211,11 @@ export class DocumentDetailComponent
|
||||
},
|
||||
})
|
||||
|
||||
this.ogDate = new Date(normalizeDateStr(doc.created.toString()))
|
||||
|
||||
// Initialize dirtyCheck
|
||||
this.store = new BehaviorSubject({
|
||||
title: doc.title,
|
||||
content: doc.content,
|
||||
created: this.ogDate.toISOString(),
|
||||
created_date: doc.created_date,
|
||||
correspondent: doc.correspondent,
|
||||
document_type: doc.document_type,
|
||||
storage_path: doc.storage_path,
|
||||
@@ -247,12 +223,6 @@ export class DocumentDetailComponent
|
||||
tags: [...doc.tags],
|
||||
})
|
||||
|
||||
// start with ISO8601 string
|
||||
this.documentForm.patchValue(
|
||||
{ created: this.ogDate.toISOString() },
|
||||
{ emitEvent: false }
|
||||
)
|
||||
|
||||
this.isDirty$ = dirtyCheck(
|
||||
this.documentForm,
|
||||
this.store.asObservable()
|
||||
@@ -494,7 +464,7 @@ export class DocumentDetailComponent
|
||||
}
|
||||
|
||||
moreLike() {
|
||||
this.queryParamsService.navigateWithFilterRules([
|
||||
this.documentListViewService.quickFilter([
|
||||
{
|
||||
rule_type: FILTER_FULLTEXT_MORELIKE,
|
||||
value: this.documentId.toString(),
|
||||
|
||||
@@ -66,23 +66,30 @@
|
||||
</div>
|
||||
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button type="button" [disabled]="awaitingDownload" class="btn btn-outline-primary btn-sm" (click)="downloadSelected()">
|
||||
<svg *ngIf="!awaitingDownload" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#download" />
|
||||
</svg>
|
||||
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Preparing download...</span>
|
||||
</div>
|
||||
|
||||
<ng-container i18n>Download</ng-container>
|
||||
</button>
|
||||
<div class="btn-group" ngbDropdown role="group" aria-label="Button group with nested dropdown">
|
||||
<button [disabled]="awaitingDownload" class="btn btn-outline-primary btn-sm dropdown-toggle-split" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<button ngbDropdownItem i18n (click)="downloadSelected('originals')">Download originals</button>
|
||||
|
||||
<div ngbDropdown class="me-2 d-flex">
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
|
||||
</svg>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||
<button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected()" i18n>
|
||||
Download
|
||||
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Preparing download...</span>
|
||||
</div>
|
||||
</button>
|
||||
<button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected('originals')" i18n>
|
||||
Download originals
|
||||
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Preparing download...</span>
|
||||
</div>
|
||||
</button>
|
||||
<button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
|
||||
@@ -379,4 +379,19 @@ export class BulkEditorComponent {
|
||||
this.awaitingDownload = false
|
||||
})
|
||||
}
|
||||
|
||||
redoOcrSelected() {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Redo OCR confirm`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently redo OCR for ${this.list.selected.size} selected document(s).`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, 'redo_ocr', {})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@
|
||||
<path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/>
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||
</svg>
|
||||
<small>{{document.created | customDate:'mediumDate'}}</small>
|
||||
<small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
<div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score">
|
||||
<small class="text-muted" i18n>Score:</small>
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="top: 0; right: 0; font-size: large" class="text-end position-absolute me-1">
|
||||
<div *ngFor="let t of getTagsLimited$() | async">
|
||||
<app-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Filter by tag" i18n-linkTitle></app-tag>
|
||||
</div>
|
||||
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
|
||||
<app-tag *ngFor="let t of getTagsLimited$() | async" [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></app-tag>
|
||||
<div *ngIf="moreTags">
|
||||
<span class="badge badge-secondary">+ {{moreTags}}</span>
|
||||
</div>
|
||||
@@ -23,21 +21,21 @@
|
||||
<div class="card-body p-2">
|
||||
<p class="card-text">
|
||||
<ng-container *ngIf="document.correspondent">
|
||||
<a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>:
|
||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>:
|
||||
</ng-container>
|
||||
{{document.title | documentTitle}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer pt-0 pb-2 px-2">
|
||||
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
|
||||
<button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Filter by document type" i18n-title
|
||||
<button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
<svg class="metadata-icon me-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
|
||||
</svg>
|
||||
<small>{{(document.document_type$ | async)?.name}}</small>
|
||||
</button>
|
||||
<button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Filter by storage path" i18n-title
|
||||
<button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
||||
@@ -57,7 +55,7 @@
|
||||
<path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/>
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||
</svg>
|
||||
<small>{{document.created | customDate:'mediumDate'}}</small>
|
||||
<small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
<div *ngIf="document.archive_serial_number" class="ps-0 p-1">
|
||||
<svg class="metadata-icon me-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor">
|
||||
|
||||
@@ -78,3 +78,11 @@
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tags {
|
||||
top: 0;
|
||||
right: 0;
|
||||
max-width: 80%;
|
||||
row-gap: .2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -13,23 +13,21 @@
|
||||
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-toggle flex-fill" ngbRadioGroup [(ngModel)]="displayMode"
|
||||
(ngModelChange)="saveDisplayMode()">
|
||||
<label ngbButtonLabel class="btn-outline-primary btn-sm">
|
||||
<input ngbButton type="radio" class="btn-check btn-sm" value="details">
|
||||
<div class="btn-group flex-fill" role="group">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails">
|
||||
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#list-ul" />
|
||||
</svg>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-outline-primary btn-sm">
|
||||
<input ngbButton type="radio" class="btn-check btn-sm" value="smallCards">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall">
|
||||
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#grid" />
|
||||
</svg>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-outline-primary btn-sm">
|
||||
<input ngbButton type="radio" class="btn-check btn-sm" value="largeCards">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge">
|
||||
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#hdd-stack" />
|
||||
</svg>
|
||||
@@ -39,15 +37,15 @@
|
||||
<div ngbDropdown class="btn-group ms-2 flex-fill">
|
||||
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow dropdown-menu-right">
|
||||
<div class="w-100 d-flex btn-group-toggle pb-2 mb-1 border-bottom" ngbRadioGroup [(ngModel)]="listSort">
|
||||
<label ngbButtonLabel class="btn-outline-primary btn-sm mx-2 flex-fill">
|
||||
<input ngbButton type="radio" class="btn btn-check btn-sm" [value]="false">
|
||||
<div class="w-100 d-flex pb-2 mb-1 border-bottom">
|
||||
<input type="radio" class="btn-check" [value]="false" [(ngModel)]="listSortReverse" id="listSortReverseFalse">
|
||||
<label class="btn btn-outline-primary btn-sm mx-2 flex-fill" for="listSortReverseFalse">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
|
||||
</svg>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-outline-primary btn-sm me-2 flex-fill">
|
||||
<input ngbButton type="radio" class="btn btn-check btn-sm" [value]="true">
|
||||
<input type="radio" class="btn-check" [value]="true" [(ngModel)]="listSortReverse" id="listSortReverseTrue">
|
||||
<label class="btn btn-outline-primary btn-sm me-2 flex-fill" for="listSortReverseTrue">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
|
||||
</svg>
|
||||
@@ -93,7 +91,7 @@
|
||||
<span i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" (pageChange)="setPage($event)" [page]="list.currentPage" [maxSize]="5"
|
||||
[rotate]="true" aria-label="Default pagination"></ngb-pagination>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -188,7 +186,7 @@
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
{{d.created | customDate}}
|
||||
{{d.created_date | customDate}}
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
{{d.added | customDate}}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
@@ -21,7 +20,6 @@ import {
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import {
|
||||
DOCUMENT_SORT_FIELDS,
|
||||
DOCUMENT_SORT_FIELDS_FULLTEXT,
|
||||
@@ -36,7 +34,7 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
|
||||
templateUrl: './document-list.component.html',
|
||||
styleUrls: ['./document-list.component.scss'],
|
||||
})
|
||||
export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
public list: DocumentListViewService,
|
||||
public savedViewService: SavedViewService,
|
||||
@@ -45,7 +43,6 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private queryParamsService: QueryParamsService,
|
||||
public openDocumentsService: OpenDocumentsService
|
||||
) {}
|
||||
|
||||
@@ -74,26 +71,24 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
: DOCUMENT_SORT_FIELDS
|
||||
}
|
||||
|
||||
set listSort(reverse: boolean) {
|
||||
set listSortReverse(reverse: boolean) {
|
||||
this.list.sortReverse = reverse
|
||||
this.queryParamsService.sortField = this.list.sortField
|
||||
this.queryParamsService.sortReverse = reverse
|
||||
}
|
||||
|
||||
get listSort(): boolean {
|
||||
get listSortReverse(): boolean {
|
||||
return this.list.sortReverse
|
||||
}
|
||||
|
||||
setSortField(field: string) {
|
||||
this.list.sortField = field
|
||||
this.queryParamsService.sortField = field
|
||||
this.queryParamsService.sortReverse = this.listSort
|
||||
}
|
||||
|
||||
onSort(event: SortEvent) {
|
||||
this.list.setSort(event.column, event.reverse)
|
||||
this.queryParamsService.sortField = event.column
|
||||
this.queryParamsService.sortReverse = event.reverse
|
||||
}
|
||||
|
||||
setPage(page: number) {
|
||||
this.list.currentPage = page
|
||||
}
|
||||
|
||||
get isBulkEditing(): boolean {
|
||||
@@ -133,7 +128,6 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
this.list.activateSavedView(view)
|
||||
this.list.reload()
|
||||
this.queryParamsService.updateFromView(view)
|
||||
this.unmodifiedFilterRules = view.filter_rules
|
||||
})
|
||||
|
||||
@@ -148,22 +142,12 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.loadViewConfig(parseInt(queryParams.get('view')))
|
||||
} else {
|
||||
this.list.activateSavedView(null)
|
||||
this.queryParamsService.parseQueryParams(queryParams)
|
||||
this.list.loadFromQueryParams(queryParams)
|
||||
this.unmodifiedFilterRules = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.filterEditor.filterRulesChange
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (filterRules) => {
|
||||
this.queryParamsService.updateFilterRules(filterRules)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// unsubscribes all
|
||||
this.unsubscribeNotifier.next(this)
|
||||
@@ -175,9 +159,8 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
.getCached(viewId)
|
||||
.pipe(first())
|
||||
.subscribe((view) => {
|
||||
this.list.loadSavedView(view)
|
||||
this.list.activateSavedView(view)
|
||||
this.list.reload()
|
||||
this.queryParamsService.updateFromView(view)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -246,34 +229,26 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
clickTag(tagID: number) {
|
||||
this.list.selectNone()
|
||||
setTimeout(() => {
|
||||
this.filterEditor.addTag(tagID)
|
||||
})
|
||||
this.filterEditor.toggleTag(tagID)
|
||||
}
|
||||
|
||||
clickCorrespondent(correspondentID: number) {
|
||||
this.list.selectNone()
|
||||
setTimeout(() => {
|
||||
this.filterEditor.addCorrespondent(correspondentID)
|
||||
})
|
||||
this.filterEditor.toggleCorrespondent(correspondentID)
|
||||
}
|
||||
|
||||
clickDocumentType(documentTypeID: number) {
|
||||
this.list.selectNone()
|
||||
setTimeout(() => {
|
||||
this.filterEditor.addDocumentType(documentTypeID)
|
||||
})
|
||||
this.filterEditor.toggleDocumentType(documentTypeID)
|
||||
}
|
||||
|
||||
clickStoragePath(storagePathID: number) {
|
||||
this.list.selectNone()
|
||||
setTimeout(() => {
|
||||
this.filterEditor.addStoragePath(storagePathID)
|
||||
})
|
||||
this.filterEditor.toggleStoragePath(storagePathID)
|
||||
}
|
||||
|
||||
clickMoreLike(documentID: number) {
|
||||
this.queryParamsService.navigateWithFilterRules([
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() },
|
||||
])
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { FilterRule } from 'src/app/data/filter-rule'
|
||||
import { filterRulesDiffer, FilterRule } from 'src/app/data/filter-rule'
|
||||
import {
|
||||
FILTER_ADDED_AFTER,
|
||||
FILTER_ADDED_BEFORE,
|
||||
@@ -204,7 +204,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
set unmodifiedFilterRules(value: FilterRule[]) {
|
||||
this._unmodifiedFilterRules = value
|
||||
this.checkIfRulesHaveChanged()
|
||||
this.rulesModified = filterRulesDiffer(
|
||||
this._unmodifiedFilterRules,
|
||||
this._filterRules
|
||||
)
|
||||
}
|
||||
|
||||
get unmodifiedFilterRules(): FilterRule[] {
|
||||
@@ -313,7 +316,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
break
|
||||
case FILTER_ASN_ISNULL:
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||
this.textFilterModifier = TEXT_FILTER_MODIFIER_NULL
|
||||
this.textFilterModifier =
|
||||
rule.value == 'true' || rule.value == '1'
|
||||
? TEXT_FILTER_MODIFIER_NULL
|
||||
: TEXT_FILTER_MODIFIER_NOTNULL
|
||||
break
|
||||
case FILTER_ASN_GT:
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||
@@ -327,7 +333,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
break
|
||||
}
|
||||
})
|
||||
this.checkIfRulesHaveChanged()
|
||||
this.rulesModified = filterRulesDiffer(
|
||||
this._unmodifiedFilterRules,
|
||||
this._filterRules
|
||||
)
|
||||
}
|
||||
|
||||
get filterRules(): FilterRule[] {
|
||||
@@ -470,31 +479,6 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
|
||||
rulesModified: boolean = false
|
||||
|
||||
private checkIfRulesHaveChanged() {
|
||||
let modified = false
|
||||
if (this._unmodifiedFilterRules.length != this._filterRules.length) {
|
||||
modified = true
|
||||
} else {
|
||||
modified = this._unmodifiedFilterRules.some((rule) => {
|
||||
return (
|
||||
this._filterRules.find(
|
||||
(fri) => fri.rule_type == rule.rule_type && fri.value == rule.value
|
||||
) == undefined
|
||||
)
|
||||
})
|
||||
|
||||
if (!modified) {
|
||||
// only check other direction if we havent already determined is modified
|
||||
modified = this._filterRules.some((rule) => {
|
||||
this._unmodifiedFilterRules.find(
|
||||
(fr) => fr.rule_type == rule.rule_type && fr.value == rule.value
|
||||
) == undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
this.rulesModified = modified
|
||||
}
|
||||
|
||||
updateRules() {
|
||||
this.filterRulesChange.next(this.filterRules)
|
||||
}
|
||||
@@ -547,29 +531,20 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this.updateRules()
|
||||
}
|
||||
|
||||
addTag(tagId: number) {
|
||||
this.tagSelectionModel.set(tagId, ToggleableItemState.Selected)
|
||||
toggleTag(tagId: number) {
|
||||
this.tagSelectionModel.toggle(tagId)
|
||||
}
|
||||
|
||||
addCorrespondent(correspondentId: number) {
|
||||
this.correspondentSelectionModel.set(
|
||||
correspondentId,
|
||||
ToggleableItemState.Selected
|
||||
)
|
||||
toggleCorrespondent(correspondentId: number) {
|
||||
this.correspondentSelectionModel.toggle(correspondentId)
|
||||
}
|
||||
|
||||
addDocumentType(documentTypeId: number) {
|
||||
this.documentTypeSelectionModel.set(
|
||||
documentTypeId,
|
||||
ToggleableItemState.Selected
|
||||
)
|
||||
toggleDocumentType(documentTypeId: number) {
|
||||
this.documentTypeSelectionModel.toggle(documentTypeId)
|
||||
}
|
||||
|
||||
addStoragePath(storagePathID: number) {
|
||||
this.storagePathSelectionModel.set(
|
||||
storagePathID,
|
||||
ToggleableItemState.Selected
|
||||
)
|
||||
toggleStoragePath(storagePathID: number) {
|
||||
this.storagePathSelectionModel.toggle(storagePathID)
|
||||
}
|
||||
|
||||
onTagsDropdownOpen() {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||
@@ -20,7 +20,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles
|
||||
correspondentsService: CorrespondentService,
|
||||
modalService: NgbModal,
|
||||
toastService: ToastService,
|
||||
queryParamsService: QueryParamsService,
|
||||
documentListViewService: DocumentListViewService,
|
||||
private datePipe: CustomDatePipe
|
||||
) {
|
||||
super(
|
||||
@@ -28,7 +28,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles
|
||||
modalService,
|
||||
CorrespondentEditDialogComponent,
|
||||
toastService,
|
||||
queryParamsService,
|
||||
documentListViewService,
|
||||
FILTER_CORRESPONDENT,
|
||||
$localize`correspondent`,
|
||||
$localize`correspondents`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||
@@ -18,14 +18,14 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless
|
||||
documentTypeService: DocumentTypeService,
|
||||
modalService: NgbModal,
|
||||
toastService: ToastService,
|
||||
queryParamsService: QueryParamsService
|
||||
documentListViewService: DocumentListViewService
|
||||
) {
|
||||
super(
|
||||
documentTypeService,
|
||||
modalService,
|
||||
DocumentTypeEditDialogComponent,
|
||||
toastService,
|
||||
queryParamsService,
|
||||
documentListViewService,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
$localize`document type`,
|
||||
$localize`document types`,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
SortableDirective,
|
||||
SortEvent,
|
||||
} from 'src/app/directives/sortable.directive'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
@@ -42,7 +42,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
private modalService: NgbModal,
|
||||
private editDialogComponent: any,
|
||||
private toastService: ToastService,
|
||||
private queryParamsService: QueryParamsService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
protected filterRuleType: number,
|
||||
public typeName: string,
|
||||
public typeNamePlural: string,
|
||||
@@ -141,7 +141,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
}
|
||||
|
||||
filterDocuments(object: ObjectWithId) {
|
||||
this.queryParamsService.navigateWithFilterRules([
|
||||
this.documentListViewService.quickFilter([
|
||||
{ rule_type: this.filterRuleType, value: object.id.toString() },
|
||||
])
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale != 'en-US'"> - {{lang.englishName}}</span></option>
|
||||
</select>
|
||||
|
||||
<small class="form-text text-muted" i18n>You need to reload the page after applying a new language.</small>
|
||||
<small *ngIf="displayLanguageIsDirty" class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
LanguageOption,
|
||||
SettingsService,
|
||||
} from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { Toast, ToastService } from 'src/app/services/toast.service'
|
||||
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
||||
import { Observable, Subscription, BehaviorSubject, first } from 'rxjs'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
@@ -61,6 +61,13 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
)
|
||||
}
|
||||
|
||||
get displayLanguageIsDirty(): boolean {
|
||||
return (
|
||||
this.settingsForm.get('displayLanguage').value !=
|
||||
this.store?.getValue()['displayLanguage']
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
public savedViewService: SavedViewService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
@@ -170,6 +177,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
}
|
||||
|
||||
private saveLocalSettings() {
|
||||
const reloadRequired = this.displayLanguageIsDirty // just this one, for now
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE,
|
||||
this.settingsForm.value.bulkEditApplyOnClose
|
||||
@@ -235,7 +243,20 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
this.store.next(this.settingsForm.value)
|
||||
this.documentListViewService.updatePageSize()
|
||||
this.settings.updateAppearanceSettings()
|
||||
this.toastService.showInfo($localize`Settings saved successfully.`)
|
||||
let savedToast: Toast = {
|
||||
title: $localize`Settings saved`,
|
||||
content: $localize`Settings were saved successfully.`,
|
||||
delay: 500000,
|
||||
}
|
||||
if (reloadRequired) {
|
||||
;(savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.`),
|
||||
(savedToast.actionName = $localize`Reload now`)
|
||||
savedToast.action = () => {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
this.toastService.show(savedToast)
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
@@ -19,14 +18,14 @@ export class StoragePathListComponent extends ManagementListComponent<PaperlessS
|
||||
directoryService: StoragePathService,
|
||||
modalService: NgbModal,
|
||||
toastService: ToastService,
|
||||
queryParamsService: QueryParamsService
|
||||
documentListViewService: DocumentListViewService
|
||||
) {
|
||||
super(
|
||||
directoryService,
|
||||
modalService,
|
||||
StoragePathEditDialogComponent,
|
||||
toastService,
|
||||
queryParamsService,
|
||||
documentListViewService,
|
||||
FILTER_STORAGE_PATH,
|
||||
$localize`storage path`,
|
||||
$localize`storage paths`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||
@@ -18,14 +18,14 @@ export class TagListComponent extends ManagementListComponent<PaperlessTag> {
|
||||
tagService: TagService,
|
||||
modalService: NgbModal,
|
||||
toastService: ToastService,
|
||||
queryParamsService: QueryParamsService
|
||||
documentListViewService: DocumentListViewService
|
||||
) {
|
||||
super(
|
||||
tagService,
|
||||
modalService,
|
||||
TagEditDialogComponent,
|
||||
toastService,
|
||||
queryParamsService,
|
||||
documentListViewService,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
$localize`tag`,
|
||||
$localize`tags`,
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<app-page-header title="File Tasks" i18n-title>
|
||||
<div class="btn-toolbar col col-md-auto">
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size == 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" [disabled]="tasksService.total == 0">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
|
||||
</svg> <ng-container i18n>{{dismissButtonText}}</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tasksService.reload()">
|
||||
<svg *ngIf="!tasksService.loading" class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-clockwise"/>
|
||||
</svg>
|
||||
<ng-container *ngIf="tasksService.loading">
|
||||
<div class="spinner-border spinner-border-sm fw-normal" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</ng-container> <ng-container i18n>Refresh</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</app-page-header>
|
||||
|
||||
<ng-container *ngIf="!tasksService.completedFileTasks && tasksService.loading">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</ng-container>
|
||||
|
||||
<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" (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>
|
||||
<th scope="col" class="d-none d-lg-table-cell" *ngIf="activeTab != 'started' && activeTab != 'queued'" i18n>Results</th>
|
||||
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
|
||||
<th scope="col" i18n>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let task of tasks">
|
||||
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
|
||||
<th>
|
||||
<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>
|
||||
</th>
|
||||
<td class="overflow-auto">{{ task.name }}</td>
|
||||
<td class="d-none d-lg-table-cell">{{ task.created | customDate:'short' }}</td>
|
||||
<td class="d-none d-lg-table-cell" *ngIf="activeTab != 'incomplete'">
|
||||
<div *ngIf="task.result.length > 50" 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 }}…</span>
|
||||
</div>
|
||||
<span *ngIf="task.result.length <= 50" 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 }}<ng-container *ngIf="task.result.length > 300">…</ng-container></pre>
|
||||
<ng-container *ngIf="task.result.length > 300"><br/><em>(<ng-container i18n>click for full output</ng-container>)</em></ng-container>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="d-lg-none">
|
||||
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
|
||||
<svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td scope="row">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg> <ng-container i18n>Dismiss</ng-container>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs">
|
||||
<li ngbNavItem="failed">
|
||||
<a ngbNavLink i18n>Failed <span *ngIf="tasksService.failedFileTasks.length > 0" class="badge bg-danger ms-1">{{tasksService.failedFileTasks.length}}</span></a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="completed">
|
||||
<a ngbNavLink i18n>Complete <span *ngIf="tasksService.completedFileTasks.length > 0" class="badge bg-secondary ms-1">{{tasksService.completedFileTasks.length}}</span></a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="started">
|
||||
<a ngbNavLink i18n>Started <span *ngIf="tasksService.startedFileTasks.length > 0" class="badge bg-secondary ms-1">{{tasksService.startedFileTasks.length}}</span></a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem="queued">
|
||||
<a ngbNavLink i18n>Queued <span *ngIf="tasksService.queuedFileTasks.length > 0" class="badge bg-secondary ms-1">{{tasksService.queuedFileTasks.length}}</span></a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav"></div>
|
||||
@@ -0,0 +1,22 @@
|
||||
::ng-deep .popover {
|
||||
max-width: 350px;
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.result {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn .spinner-border-sm {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { takeUntil, Subject, first } from 'rxjs'
|
||||
import { PaperlessTask } from 'src/app/data/paperless-task'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
templateUrl: './tasks.component.html',
|
||||
styleUrls: ['./tasks.component.scss'],
|
||||
})
|
||||
export class TasksComponent implements OnInit, OnDestroy {
|
||||
public activeTab: string
|
||||
public selectedTasks: Set<number> = new Set()
|
||||
private unsubscribeNotifer = new Subject()
|
||||
public expandedTask: number
|
||||
|
||||
get dismissButtonText(): string {
|
||||
return this.selectedTasks.size > 0
|
||||
? $localize`Dismiss selected`
|
||||
: $localize`Dismiss all`
|
||||
}
|
||||
|
||||
constructor(
|
||||
public tasksService: TasksService,
|
||||
private modalService: NgbModal
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.tasksService.reload()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeNotifer.next(true)
|
||||
}
|
||||
|
||||
dismissTask(task: PaperlessTask) {
|
||||
this.dismissTasks(task)
|
||||
}
|
||||
|
||||
dismissTasks(task: PaperlessTask = undefined) {
|
||||
let tasks = task ? new Set([task.id]) : this.selectedTasks
|
||||
if (!task && this.selectedTasks.size == 0)
|
||||
tasks = new Set(this.tasksService.allFileTasks.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} ` + $localize`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)
|
||||
this.selectedTasks.clear()
|
||||
})
|
||||
} else {
|
||||
this.tasksService.dismissTasks(tasks)
|
||||
this.selectedTasks.clear()
|
||||
}
|
||||
}
|
||||
|
||||
expandTask(task: PaperlessTask) {
|
||||
this.expandedTask = this.expandedTask == task.id ? undefined : task.id
|
||||
}
|
||||
|
||||
toggleSelected(task: PaperlessTask) {
|
||||
this.selectedTasks.has(task.id)
|
||||
? this.selectedTasks.delete(task.id)
|
||||
: this.selectedTasks.add(task.id)
|
||||
}
|
||||
|
||||
get currentTasks(): PaperlessTask[] {
|
||||
let tasks: PaperlessTask[]
|
||||
switch (this.activeTab) {
|
||||
case 'queued':
|
||||
tasks = this.tasksService.queuedFileTasks
|
||||
break
|
||||
case 'started':
|
||||
tasks = this.tasksService.startedFileTasks
|
||||
break
|
||||
case 'completed':
|
||||
tasks = this.tasksService.completedFileTasks
|
||||
break
|
||||
case 'failed':
|
||||
tasks = this.tasksService.failedFileTasks
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
toggleAll(event: PointerEvent) {
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
this.selectedTasks = new Set(this.currentTasks.map((t) => t.id))
|
||||
} else {
|
||||
this.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.selectedTasks.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,38 @@
|
||||
export const FILTER_TITLE = 0
|
||||
export const FILTER_CONTENT = 1
|
||||
|
||||
export const FILTER_ASN = 2
|
||||
export const FILTER_ASN_ISNULL = 18
|
||||
export const FILTER_ASN_GT = 23
|
||||
export const FILTER_ASN_LT = 24
|
||||
|
||||
export const FILTER_CORRESPONDENT = 3
|
||||
|
||||
export const FILTER_DOCUMENT_TYPE = 4
|
||||
|
||||
export const FILTER_IS_IN_INBOX = 5
|
||||
export const FILTER_HAS_TAGS_ALL = 6
|
||||
export const FILTER_HAS_ANY_TAG = 7
|
||||
export const FILTER_DOES_NOT_HAVE_TAG = 17
|
||||
export const FILTER_HAS_TAGS_ANY = 22
|
||||
|
||||
export const FILTER_STORAGE_PATH = 25
|
||||
|
||||
export const FILTER_CREATED_BEFORE = 8
|
||||
export const FILTER_CREATED_AFTER = 9
|
||||
export const FILTER_CREATED_YEAR = 10
|
||||
export const FILTER_CREATED_MONTH = 11
|
||||
export const FILTER_CREATED_DAY = 12
|
||||
|
||||
export const FILTER_ADDED_BEFORE = 13
|
||||
export const FILTER_ADDED_AFTER = 14
|
||||
|
||||
export const FILTER_MODIFIED_BEFORE = 15
|
||||
export const FILTER_MODIFIED_AFTER = 16
|
||||
|
||||
export const FILTER_DOES_NOT_HAVE_TAG = 17
|
||||
|
||||
export const FILTER_ASN_ISNULL = 18
|
||||
export const FILTER_ASN_GT = 19
|
||||
export const FILTER_ASN_LT = 20
|
||||
|
||||
export const FILTER_TITLE_CONTENT = 21
|
||||
|
||||
export const FILTER_FULLTEXT_QUERY = 22
|
||||
export const FILTER_FULLTEXT_MORELIKE = 23
|
||||
|
||||
export const FILTER_STORAGE_PATH = 30
|
||||
export const FILTER_TITLE_CONTENT = 19
|
||||
export const FILTER_FULLTEXT_QUERY = 20
|
||||
export const FILTER_FULLTEXT_MORELIKE = 21
|
||||
|
||||
export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
{
|
||||
|
||||
@@ -25,6 +25,25 @@ export function isFullTextFilterRule(filterRules: FilterRule[]): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
export function filterRulesDiffer(
|
||||
filterRulesA: FilterRule[],
|
||||
filterRulesB: FilterRule[]
|
||||
): boolean {
|
||||
let differ = false
|
||||
if (filterRulesA.length != filterRulesB.length) {
|
||||
differ = true
|
||||
} else {
|
||||
differ = filterRulesA.some((rule) => {
|
||||
return (
|
||||
filterRulesB.find(
|
||||
(fri) => fri.rule_type == rule.rule_type && fri.value == rule.value
|
||||
) == undefined
|
||||
)
|
||||
})
|
||||
}
|
||||
return differ
|
||||
}
|
||||
|
||||
export interface FilterRule {
|
||||
rule_type: number
|
||||
value: string
|
||||
|
||||
@@ -37,8 +37,12 @@ export interface PaperlessDocument extends ObjectWithId {
|
||||
|
||||
checksum?: string
|
||||
|
||||
// UTC
|
||||
created?: Date
|
||||
|
||||
// localized date
|
||||
created_date?: Date
|
||||
|
||||
modified?: Date
|
||||
|
||||
added?: Date
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ObjectWithId } from './object-with-id'
|
||||
|
||||
export enum PaperlessTaskType {
|
||||
// just file tasks, for now
|
||||
File = 'file',
|
||||
}
|
||||
|
||||
export enum PaperlessTaskStatus {
|
||||
Queued = 'queued',
|
||||
Started = 'started',
|
||||
Complete = 'complete',
|
||||
Failed = 'failed',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export interface PaperlessTask extends ObjectWithId {
|
||||
type: PaperlessTaskType
|
||||
|
||||
status: PaperlessTaskStatus
|
||||
|
||||
acknowledged: boolean
|
||||
|
||||
task_id: string
|
||||
|
||||
name: string
|
||||
|
||||
created: Date
|
||||
|
||||
started?: Date
|
||||
|
||||
result: string
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { DatePipe } from '@angular/common'
|
||||
import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core'
|
||||
import { SETTINGS_KEYS } from '../data/paperless-uisettings'
|
||||
import { SettingsService } from '../services/settings.service'
|
||||
import { normalizeDateStr } from '../utils/date'
|
||||
|
||||
const FORMAT_TO_ISO_FORMAT = {
|
||||
longDate: 'y-MM-dd',
|
||||
@@ -35,7 +34,6 @@ export class CustomDatePipe implements PipeTransform {
|
||||
this.settings.get(SETTINGS_KEYS.DATE_LOCALE) ||
|
||||
this.defaultLocale
|
||||
let f = format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT)
|
||||
if (typeof value == 'string') value = normalizeDateStr(value)
|
||||
if (l == 'iso-8601') {
|
||||
return this.datePipe.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone)
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||
import { ParamMap, Router } from '@angular/router'
|
||||
import { Observable } from 'rxjs'
|
||||
import {
|
||||
filterRulesDiffer,
|
||||
cloneFilterRules,
|
||||
FilterRule,
|
||||
isFullTextFilterRule,
|
||||
@@ -10,13 +11,14 @@ import { PaperlessDocument } from '../data/paperless-document'
|
||||
import { PaperlessSavedView } from '../data/paperless-saved-view'
|
||||
import { SETTINGS_KEYS } from '../data/paperless-uisettings'
|
||||
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
|
||||
import { generateParams, parseParams } from '../utils/query-params'
|
||||
import { DocumentService, DOCUMENT_SORT_FIELDS } from './rest/document.service'
|
||||
import { SettingsService } from './settings.service'
|
||||
|
||||
/**
|
||||
* Captures the current state of the list view.
|
||||
*/
|
||||
interface ListViewState {
|
||||
export interface ListViewState {
|
||||
/**
|
||||
* Title of the document list view. Either "Documents" (localized) or the name of a saved view.
|
||||
*/
|
||||
@@ -32,7 +34,7 @@ interface ListViewState {
|
||||
/**
|
||||
* Total amount of documents with the current filter rules. Used to calculate the number of pages.
|
||||
*/
|
||||
collectionSize: number
|
||||
collectionSize?: number
|
||||
|
||||
/**
|
||||
* Currently selected sort field.
|
||||
@@ -66,6 +68,7 @@ interface ListViewState {
|
||||
})
|
||||
export class DocumentListViewService {
|
||||
isReloading: boolean = false
|
||||
initialized: boolean = false
|
||||
error: string = null
|
||||
|
||||
rangeSelectionAnchorIndex: number
|
||||
@@ -85,6 +88,32 @@ export class DocumentListViewService {
|
||||
return this.activeListViewState.title
|
||||
}
|
||||
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private settings: SettingsService,
|
||||
private router: Router
|
||||
) {
|
||||
let documentListViewConfigJson = localStorage.getItem(
|
||||
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG
|
||||
)
|
||||
if (documentListViewConfigJson) {
|
||||
try {
|
||||
let savedState: ListViewState = JSON.parse(documentListViewConfigJson)
|
||||
// Remove null elements from the restored state
|
||||
Object.keys(savedState).forEach((k) => {
|
||||
if (savedState[k] == null) {
|
||||
delete savedState[k]
|
||||
}
|
||||
})
|
||||
//only use restored state attributes instead of defaults if they are not null
|
||||
let newState = Object.assign(this.defaultListViewState(), savedState)
|
||||
this.listViewStates.set(null, newState)
|
||||
} catch (e) {
|
||||
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private defaultListViewState(): ListViewState {
|
||||
return {
|
||||
title: null,
|
||||
@@ -122,20 +151,53 @@ export class DocumentListViewService {
|
||||
if (closeCurrentView) {
|
||||
this._activeSavedViewId = null
|
||||
}
|
||||
|
||||
this.activeListViewState.filterRules = cloneFilterRules(view.filter_rules)
|
||||
this.activeListViewState.sortField = view.sort_field
|
||||
this.activeListViewState.sortReverse = view.sort_reverse
|
||||
if (this._activeSavedViewId) {
|
||||
this.activeListViewState.title = view.name
|
||||
}
|
||||
|
||||
this.reduceSelectionToFilter()
|
||||
|
||||
if (!this.router.routerState.snapshot.url.includes('/view/')) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { view: view.id },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reload(onFinish?) {
|
||||
loadFromQueryParams(queryParams: ParamMap) {
|
||||
const paramsEmpty: boolean = queryParams.keys.length == 0
|
||||
let newState: ListViewState = this.listViewStates.get(null)
|
||||
if (!paramsEmpty) newState = parseParams(queryParams)
|
||||
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
|
||||
|
||||
// only reload if things have changed
|
||||
if (
|
||||
!this.initialized ||
|
||||
paramsEmpty ||
|
||||
this.activeListViewState.sortField !== newState.sortField ||
|
||||
this.activeListViewState.sortReverse !== newState.sortReverse ||
|
||||
this.activeListViewState.currentPage !== newState.currentPage ||
|
||||
filterRulesDiffer(
|
||||
this.activeListViewState.filterRules,
|
||||
newState.filterRules
|
||||
)
|
||||
) {
|
||||
this.activeListViewState.filterRules = newState.filterRules
|
||||
this.activeListViewState.sortField = newState.sortField
|
||||
this.activeListViewState.sortReverse = newState.sortReverse
|
||||
this.activeListViewState.currentPage = newState.currentPage
|
||||
this.reload(null, paramsEmpty) // update the params if there arent any
|
||||
}
|
||||
}
|
||||
|
||||
reload(onFinish?, updateQueryParams: boolean = true) {
|
||||
this.isReloading = true
|
||||
this.error = null
|
||||
let activeListViewState = this.activeListViewState
|
||||
|
||||
this.documentService
|
||||
.listFiltered(
|
||||
activeListViewState.currentPage,
|
||||
@@ -146,9 +208,18 @@ export class DocumentListViewService {
|
||||
)
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.initialized = true
|
||||
this.isReloading = false
|
||||
activeListViewState.collectionSize = result.count
|
||||
activeListViewState.documents = result.results
|
||||
|
||||
if (updateQueryParams && !this._activeSavedViewId) {
|
||||
let base = ['/documents']
|
||||
this.router.navigate(base, {
|
||||
queryParams: generateParams(activeListViewState),
|
||||
})
|
||||
}
|
||||
|
||||
if (onFinish) {
|
||||
onFinish()
|
||||
}
|
||||
@@ -191,6 +262,7 @@ export class DocumentListViewService {
|
||||
) {
|
||||
this.activeListViewState.sortField = 'created'
|
||||
}
|
||||
this._activeSavedViewId = null
|
||||
this.activeListViewState.filterRules = filterRules
|
||||
this.reload()
|
||||
this.reduceSelectionToFilter()
|
||||
@@ -202,6 +274,7 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
set sortField(field: string) {
|
||||
this._activeSavedViewId = null
|
||||
this.activeListViewState.sortField = field
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
@@ -212,6 +285,7 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
set sortReverse(reverse: boolean) {
|
||||
this._activeSavedViewId = null
|
||||
this.activeListViewState.sortReverse = reverse
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
@@ -221,13 +295,6 @@ export class DocumentListViewService {
|
||||
return this.activeListViewState.sortReverse
|
||||
}
|
||||
|
||||
get sortParams(): Params {
|
||||
return {
|
||||
sortField: this.sortField,
|
||||
sortReverse: this.sortReverse,
|
||||
}
|
||||
}
|
||||
|
||||
get collectionSize(): number {
|
||||
return this.activeListViewState.collectionSize
|
||||
}
|
||||
@@ -237,6 +304,8 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
set currentPage(page: number) {
|
||||
if (this.activeListViewState.currentPage == page) return
|
||||
this._activeSavedViewId = null
|
||||
this.activeListViewState.currentPage = page
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
@@ -273,6 +342,10 @@ export class DocumentListViewService {
|
||||
}
|
||||
}
|
||||
|
||||
quickFilter(filterRules: FilterRule[]) {
|
||||
this.filterRules = filterRules
|
||||
}
|
||||
|
||||
getLastPage(): number {
|
||||
return Math.ceil(this.collectionSize / this.currentPageSize)
|
||||
}
|
||||
@@ -431,29 +504,4 @@ export class DocumentListViewService {
|
||||
documentIndexInCurrentView(documentID: number): number {
|
||||
return this.documents.map((d) => d.id).indexOf(documentID)
|
||||
}
|
||||
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private settings: SettingsService
|
||||
) {
|
||||
let documentListViewConfigJson = localStorage.getItem(
|
||||
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG
|
||||
)
|
||||
if (documentListViewConfigJson) {
|
||||
try {
|
||||
let savedState: ListViewState = JSON.parse(documentListViewConfigJson)
|
||||
// Remove null elements from the restored state
|
||||
Object.keys(savedState).forEach((k) => {
|
||||
if (savedState[k] == null) {
|
||||
delete savedState[k]
|
||||
}
|
||||
})
|
||||
//only use restored state attributes instead of defaults if they are not null
|
||||
let newState = Object.assign(this.defaultListViewState(), savedState)
|
||||
this.listViewStates.set(null, newState)
|
||||
} catch (e) {
|
||||
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ParamMap, Params, Router } from '@angular/router'
|
||||
import { FilterRule } from '../data/filter-rule'
|
||||
import { FilterRuleType, FILTER_RULE_TYPES } from '../data/filter-rule-type'
|
||||
import { PaperlessSavedView } from '../data/paperless-saved-view'
|
||||
import { DocumentListViewService } from './document-list-view.service'
|
||||
|
||||
const SORT_FIELD_PARAMETER = 'sort'
|
||||
const SORT_REVERSE_PARAMETER = 'reverse'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class QueryParamsService {
|
||||
constructor(private router: Router, private list: DocumentListViewService) {}
|
||||
|
||||
private filterParams: Params = {}
|
||||
private sortParams: Params = {}
|
||||
|
||||
updateFilterRules(
|
||||
filterRules: FilterRule[],
|
||||
updateQueryParams: boolean = true
|
||||
) {
|
||||
this.filterParams = filterRulesToQueryParams(filterRules)
|
||||
if (updateQueryParams) this.updateQueryParams()
|
||||
}
|
||||
|
||||
set sortField(field: string) {
|
||||
this.sortParams[SORT_FIELD_PARAMETER] = field
|
||||
this.updateQueryParams()
|
||||
}
|
||||
|
||||
set sortReverse(reverse: boolean) {
|
||||
if (!reverse) this.sortParams[SORT_REVERSE_PARAMETER] = undefined
|
||||
else this.sortParams[SORT_REVERSE_PARAMETER] = reverse
|
||||
this.updateQueryParams()
|
||||
}
|
||||
|
||||
get params(): Params {
|
||||
return {
|
||||
...this.sortParams,
|
||||
...this.filterParams,
|
||||
}
|
||||
}
|
||||
|
||||
private updateQueryParams() {
|
||||
// if we were on a saved view we navigate 'away' to /documents
|
||||
let base = []
|
||||
if (this.router.routerState.snapshot.url.includes('/view/'))
|
||||
base = ['/documents']
|
||||
|
||||
this.router.navigate(base, {
|
||||
queryParams: this.params,
|
||||
})
|
||||
}
|
||||
|
||||
public parseQueryParams(queryParams: ParamMap) {
|
||||
let filterRules = filterRulesFromQueryParams(queryParams)
|
||||
if (
|
||||
filterRules.length ||
|
||||
queryParams.has(SORT_FIELD_PARAMETER) ||
|
||||
queryParams.has(SORT_REVERSE_PARAMETER)
|
||||
) {
|
||||
this.list.filterRules = filterRules
|
||||
this.list.sortField = queryParams.get(SORT_FIELD_PARAMETER)
|
||||
this.list.sortReverse =
|
||||
queryParams.has(SORT_REVERSE_PARAMETER) ||
|
||||
(!queryParams.has(SORT_FIELD_PARAMETER) &&
|
||||
!queryParams.has(SORT_REVERSE_PARAMETER))
|
||||
this.list.reload()
|
||||
} else if (
|
||||
filterRules.length == 0 &&
|
||||
!queryParams.has(SORT_FIELD_PARAMETER)
|
||||
) {
|
||||
// this is navigating to /documents so we need to update the params from the list
|
||||
this.updateFilterRules(this.list.filterRules, false)
|
||||
this.sortParams[SORT_FIELD_PARAMETER] = this.list.sortField
|
||||
this.sortParams[SORT_REVERSE_PARAMETER] = this.list.sortReverse
|
||||
this.router.navigate([], {
|
||||
queryParams: this.params,
|
||||
replaceUrl: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateFromView(view: PaperlessSavedView) {
|
||||
if (!this.router.routerState.snapshot.url.includes('/view/')) {
|
||||
// navigation for /documents?view=
|
||||
this.router.navigate([], {
|
||||
queryParams: { view: view.id },
|
||||
})
|
||||
}
|
||||
// make sure params are up-to-date
|
||||
this.updateFilterRules(view.filter_rules, false)
|
||||
this.sortParams[SORT_FIELD_PARAMETER] = this.list.sortField
|
||||
this.sortParams[SORT_REVERSE_PARAMETER] = this.list.sortReverse
|
||||
}
|
||||
|
||||
navigateWithFilterRules(filterRules: FilterRule[]) {
|
||||
this.updateFilterRules(filterRules)
|
||||
this.router.navigate(['/documents'], {
|
||||
queryParams: this.params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function filterRulesToQueryParams(filterRules: FilterRule[]): Object {
|
||||
if (filterRules) {
|
||||
let params = {}
|
||||
for (let rule of filterRules) {
|
||||
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
|
||||
if (ruleType.multi) {
|
||||
params[ruleType.filtervar] = params[ruleType.filtervar]
|
||||
? params[ruleType.filtervar] + ',' + rule.value
|
||||
: rule.value
|
||||
} else if (ruleType.isnull_filtervar && rule.value == null) {
|
||||
params[ruleType.isnull_filtervar] = true
|
||||
} else {
|
||||
params[ruleType.filtervar] = rule.value
|
||||
}
|
||||
}
|
||||
return params
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function filterRulesFromQueryParams(queryParams: ParamMap) {
|
||||
const allFilterRuleQueryParams: string[] = FILTER_RULE_TYPES.map(
|
||||
(rt) => rt.filtervar
|
||||
)
|
||||
.concat(FILTER_RULE_TYPES.map((rt) => rt.isnull_filtervar))
|
||||
.filter((rt) => rt !== undefined)
|
||||
|
||||
// transform query params to filter rules
|
||||
let filterRulesFromQueryParams: FilterRule[] = []
|
||||
allFilterRuleQueryParams
|
||||
.filter((frqp) => queryParams.has(frqp))
|
||||
.forEach((filterQueryParamName) => {
|
||||
const rule_type: FilterRuleType = FILTER_RULE_TYPES.find(
|
||||
(rt) =>
|
||||
rt.filtervar == filterQueryParamName ||
|
||||
rt.isnull_filtervar == filterQueryParamName
|
||||
)
|
||||
const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName
|
||||
const valueURIComponent: string = queryParams.get(filterQueryParamName)
|
||||
const filterQueryParamValues: string[] = rule_type.multi
|
||||
? valueURIComponent.split(',')
|
||||
: [valueURIComponent]
|
||||
|
||||
filterRulesFromQueryParams = filterRulesFromQueryParams.concat(
|
||||
// map all values to filter rules
|
||||
filterQueryParamValues.map((val) => {
|
||||
return {
|
||||
rule_type: rule_type.id,
|
||||
value: isNullRuleType ? null : val,
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return filterRulesFromQueryParams
|
||||
}
|
||||
@@ -6,12 +6,12 @@ import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Observable } from 'rxjs'
|
||||
import { Results } from 'src/app/data/results'
|
||||
import { FilterRule } from 'src/app/data/filter-rule'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { map, tap } from 'rxjs/operators'
|
||||
import { CorrespondentService } from './correspondent.service'
|
||||
import { DocumentTypeService } from './document-type.service'
|
||||
import { TagService } from './tag.service'
|
||||
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions'
|
||||
import { filterRulesToQueryParams } from '../query-params.service'
|
||||
import { queryParamsFromFilterRules } from '../../utils/query-params'
|
||||
import { StoragePathService } from './storage-path.service'
|
||||
|
||||
export const DOCUMENT_SORT_FIELDS = [
|
||||
@@ -70,7 +70,13 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
||||
doc.document_type$ = this.documentTypeService.getCached(doc.document_type)
|
||||
}
|
||||
if (doc.tags) {
|
||||
doc.tags$ = this.tagService.getCachedMany(doc.tags)
|
||||
doc.tags$ = this.tagService
|
||||
.getCachedMany(doc.tags)
|
||||
.pipe(
|
||||
tap((tags) =>
|
||||
tags.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name))
|
||||
)
|
||||
)
|
||||
}
|
||||
if (doc.storage_path) {
|
||||
doc.storage_path$ = this.storagePathService.getCached(doc.storage_path)
|
||||
@@ -91,7 +97,7 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
||||
pageSize,
|
||||
sortField,
|
||||
sortReverse,
|
||||
Object.assign(extraParams, filterRulesToQueryParams(filterRules))
|
||||
Object.assign(extraParams, queryParamsFromFilterRules(filterRules))
|
||||
).pipe(
|
||||
map((results) => {
|
||||
results.results.forEach((doc) => this.addObservablesToDocument(doc))
|
||||
@@ -127,6 +133,12 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
||||
return url
|
||||
}
|
||||
|
||||
update(o: PaperlessDocument): Observable<PaperlessDocument> {
|
||||
// we want to only set created_date
|
||||
o.created = undefined
|
||||
return super.update(o)
|
||||
}
|
||||
|
||||
uploadDocument(formData) {
|
||||
return this.http.post(
|
||||
this.getResourceUrl(null, 'post_document'),
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { first, map } from 'rxjs/operators'
|
||||
import {
|
||||
PaperlessTask,
|
||||
PaperlessTaskStatus,
|
||||
PaperlessTaskType,
|
||||
} from 'src/app/data/paperless-task'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TasksService {
|
||||
private baseUrl: string = environment.apiBaseUrl
|
||||
|
||||
loading: boolean
|
||||
|
||||
private fileTasks: PaperlessTask[] = []
|
||||
|
||||
public get total(): number {
|
||||
return this.fileTasks?.length
|
||||
}
|
||||
|
||||
public get allFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.slice(0)
|
||||
}
|
||||
|
||||
public get queuedFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Queued)
|
||||
}
|
||||
|
||||
public get startedFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Started)
|
||||
}
|
||||
|
||||
public get completedFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter(
|
||||
(t) => t.status == PaperlessTaskStatus.Complete
|
||||
)
|
||||
}
|
||||
|
||||
public get failedFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Failed)
|
||||
}
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
public reload() {
|
||||
this.loading = true
|
||||
|
||||
this.http
|
||||
.get<PaperlessTask[]>(`${this.baseUrl}tasks/`)
|
||||
.pipe(first())
|
||||
.subscribe((r) => {
|
||||
this.fileTasks = r.filter((t) => t.type == PaperlessTaskType.File) // they're all File tasks, for now
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
public dismissTasks(task_ids: Set<number>) {
|
||||
this.http
|
||||
.post(`${this.baseUrl}acknowledge_tasks/`, {
|
||||
tasks: [...task_ids],
|
||||
})
|
||||
.pipe(first())
|
||||
.subscribe((r) => {
|
||||
this.reload()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// see https://github.com/dateutil/dateutil/issues/878 , JS Date does not
|
||||
// seem to accept these strings as valid dates so we must normalize offset
|
||||
export function normalizeDateStr(dateStr: string): string {
|
||||
return dateStr.replace(/[\+-](\d\d):\d\d:\d\d/gm, `-$1:00`)
|
||||
}
|
||||
@@ -5,11 +5,20 @@ import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'
|
||||
export class ISODateAdapter extends NgbDateAdapter<string> {
|
||||
fromModel(value: string | null): NgbDateStruct | null {
|
||||
if (value) {
|
||||
let date = new Date(value)
|
||||
return {
|
||||
day: date.getDate(),
|
||||
month: date.getMonth() + 1,
|
||||
year: date.getFullYear(),
|
||||
if (value.match(/\d\d\d\d\-\d\d\-\d\d/g)) {
|
||||
const segs = value.split('-')
|
||||
return {
|
||||
year: parseInt(segs[0]),
|
||||
month: parseInt(segs[1]),
|
||||
day: parseInt(segs[2]),
|
||||
}
|
||||
} else {
|
||||
let date = new Date(value)
|
||||
return {
|
||||
day: date.getDate(),
|
||||
month: date.getMonth() + 1,
|
||||
year: date.getFullYear(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
@Injectable()
|
||||
export class ISODateTimeAdapter extends NgbDateAdapter<string> {
|
||||
fromModel(value: string | null): NgbDateStruct | null {
|
||||
if (value) {
|
||||
let date = new Date(value)
|
||||
return {
|
||||
day: date.getDate(),
|
||||
month: date.getMonth() + 1,
|
||||
year: date.getFullYear(),
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
toModel(date: NgbDateStruct | null): string | null {
|
||||
return date
|
||||
? new Date(date.year, date.month - 1, date.day).toISOString()
|
||||
: null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { ParamMap, Params } from '@angular/router'
|
||||
import { FilterRule } from '../data/filter-rule'
|
||||
import { FilterRuleType, FILTER_RULE_TYPES } from '../data/filter-rule-type'
|
||||
import { ListViewState } from '../services/document-list-view.service'
|
||||
|
||||
const SORT_FIELD_PARAMETER = 'sort'
|
||||
const SORT_REVERSE_PARAMETER = 'reverse'
|
||||
const PAGE_PARAMETER = 'page'
|
||||
|
||||
export function generateParams(viewState: ListViewState): Params {
|
||||
let params = queryParamsFromFilterRules(viewState.filterRules)
|
||||
params[SORT_FIELD_PARAMETER] = viewState.sortField
|
||||
params[SORT_REVERSE_PARAMETER] = viewState.sortReverse ? 1 : undefined
|
||||
params[PAGE_PARAMETER] = isNaN(viewState.currentPage)
|
||||
? 1
|
||||
: viewState.currentPage
|
||||
return params
|
||||
}
|
||||
|
||||
export function parseParams(queryParams: ParamMap): ListViewState {
|
||||
let filterRules = filterRulesFromQueryParams(queryParams)
|
||||
let sortField = queryParams.get(SORT_FIELD_PARAMETER)
|
||||
let sortReverse =
|
||||
queryParams.has(SORT_REVERSE_PARAMETER) ||
|
||||
(!queryParams.has(SORT_FIELD_PARAMETER) &&
|
||||
!queryParams.has(SORT_REVERSE_PARAMETER))
|
||||
let currentPage = queryParams.has(PAGE_PARAMETER)
|
||||
? parseInt(queryParams.get(PAGE_PARAMETER))
|
||||
: 1
|
||||
return {
|
||||
currentPage: currentPage,
|
||||
filterRules: filterRules,
|
||||
sortField: sortField,
|
||||
sortReverse: sortReverse,
|
||||
}
|
||||
}
|
||||
|
||||
export function filterRulesFromQueryParams(
|
||||
queryParams: ParamMap
|
||||
): FilterRule[] {
|
||||
const allFilterRuleQueryParams: string[] = FILTER_RULE_TYPES.map(
|
||||
(rt) => rt.filtervar
|
||||
)
|
||||
.concat(FILTER_RULE_TYPES.map((rt) => rt.isnull_filtervar))
|
||||
.filter((rt) => rt !== undefined)
|
||||
|
||||
// transform query params to filter rules
|
||||
let filterRulesFromQueryParams: FilterRule[] = []
|
||||
allFilterRuleQueryParams
|
||||
.filter((frqp) => queryParams.has(frqp))
|
||||
.forEach((filterQueryParamName) => {
|
||||
const rule_type: FilterRuleType = FILTER_RULE_TYPES.find(
|
||||
(rt) =>
|
||||
rt.filtervar == filterQueryParamName ||
|
||||
rt.isnull_filtervar == filterQueryParamName
|
||||
)
|
||||
const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName
|
||||
const valueURIComponent: string = queryParams.get(filterQueryParamName)
|
||||
const filterQueryParamValues: string[] = rule_type.multi
|
||||
? valueURIComponent.split(',')
|
||||
: [valueURIComponent]
|
||||
|
||||
filterRulesFromQueryParams = filterRulesFromQueryParams.concat(
|
||||
// map all values to filter rules
|
||||
filterQueryParamValues.map((val) => {
|
||||
if (rule_type.datatype == 'boolean')
|
||||
val = val.replace('1', 'true').replace('0', 'false')
|
||||
return {
|
||||
rule_type: rule_type.id,
|
||||
value: isNullRuleType ? null : val,
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return filterRulesFromQueryParams
|
||||
}
|
||||
|
||||
export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params {
|
||||
if (filterRules) {
|
||||
let params = {}
|
||||
for (let rule of filterRules) {
|
||||
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
|
||||
if (ruleType.multi) {
|
||||
params[ruleType.filtervar] = params[ruleType.filtervar]
|
||||
? params[ruleType.filtervar] + ',' + rule.value
|
||||
: rule.value
|
||||
} else if (ruleType.isnull_filtervar && rule.value == null) {
|
||||
params[ruleType.isnull_filtervar] = 1
|
||||
} else {
|
||||
params[ruleType.filtervar] = rule.value
|
||||
if (ruleType.datatype == 'boolean')
|
||||
params[ruleType.filtervar] =
|
||||
rule.value == 'true' || rule.value == '1' ? 1 : 0
|
||||
}
|
||||
}
|
||||
return params
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1"
|
||||
id="svg4812" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="logo-dark-notext.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 198.4 238.9"
|
||||
style="enable-background:new 0 0 198.4 238.9;" xml:space="preserve">
|
||||
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="SvgjsG1020" inkscape:cx="328.04904" inkscape:cy="330.33332" inkscape:document-rotation="0" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1280" inkscape:window-y="27" inkscape:zoom="0.98994949" pagecolor="#ffffff" showgrid="false">
|
||||
</sodipodi:namedview>
|
||||
<g id="layer1" transform="translate(-9.9999792,-10.000082)" inkscape:groupmode="layer" inkscape:label="Layer 1">
|
||||
<g id="SvgjsG1020" transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)">
|
||||
<path id="path57" d="M1967.5,16C1672.7,702,255.4,787,709,1892.5c5.7,14.2-104.9,164.4-178.6,289.1c-17-62.4-36.9-130.4-34-136.1
|
||||
c368.5-436.5-263.6-683.1-297.6-1040.3C40,1288.7-16.7,1784.8,462.3,2071.1c2.8,0,25.5,107.7,36.9,161.6
|
||||
c-11.3,22.7-22.7,45.4-28.3,62.4c-11.3,28.3,73.7,25.5,73.7,31.2c8.5-2.8,209.8-357.2,215.4-360
|
||||
C1899.5,1705.4,2100.8,679.3,1967.5,16z M1386.4,738.8C853.5,1215,762.8,1569.4,779.8,1742.3
|
||||
C601.2,1319.9,1125.7,855,1386.4,738.8z M357.5,1419.1c102,93.5,272.1,379.8,127.6,547.1C519,1889.7,530.4,1716.8,357.5,1419.1z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.4 238.9" style="enable-background:new 0 0 198.4 238.9" xml:space="preserve">
|
||||
<path d="M194.7 0C164.211 70.943 17.64 79.733 64.55 194.06c.59 1.468-10.848 17-18.47 29.897-1.758-6.453-3.816-13.486-3.516-14.075 38.109-45.141-27.26-70.643-30.776-107.583-16.423 29.318-22.286 80.623 27.25 110.23.29 0 2.637 11.138 3.816 16.712-1.169 2.348-2.348 4.695-2.927 6.454-1.168 2.926 7.622 2.637 7.622 3.226.879-.29 21.697-36.94 22.276-37.23C187.667 174.711 208.485 68.596 194.699 0zm-60.096 74.749c-55.11 49.246-64.49 85.897-62.732 103.777-18.47-43.682 35.772-91.76 62.732-103.777zM28.2 145.102c10.548 9.67 28.14 39.278 13.196 56.58 3.506-7.912 4.684-25.793-13.196-56.58z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 727 B |
@@ -1,71 +1,4 @@
|
||||
<?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="svg9580" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="logo.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2897.4 896.6"
|
||||
style="enable-background:new 0 0 2897.4 896.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#17541F;}
|
||||
</style>
|
||||
<sodipodi:namedview bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview9582" inkscape:current-layer="g9578" inkscape:cx="1393.617" inkscape:cy="393.61704" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1280" inkscape:window-y="27" inkscape:zoom="0.46439736" objecttolerance="10" pagecolor="#ffffff" showgrid="false">
|
||||
</sodipodi:namedview>
|
||||
<g>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"
|
||||
/>
|
||||
<rect x="1985" y="277.4" width="84.5" height="377.8"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<rect x="2460.7" y="738.7" width="59.6" height="17.2"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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 "/>
|
||||
<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"/>
|
||||
</g>
|
||||
<path class="st0" 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"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" style="enable-background:new 0 0 2897.4 896.6" xml:space="preserve">
|
||||
<path d="M1022.3 428.7c-17.8-19.9-42.7-29.8-74.7-29.8-22.3 0-42.4 5.7-60.5 17.3-18.1 11.6-32.3 27.5-42.5 47.8s-15.3 42.9-15.3 67.8 5.1 47.5 15.3 67.8c10.3 20.3 24.4 36.2 42.5 47.8 18.1 11.5 38.3 17.3 60.5 17.3 32 0 56.9-9.9 74.7-29.8V655.5h84.5V408.3h-84.5v20.4zM1010.5 575c-10.2 11.7-23.6 17.6-40.2 17.6s-29.9-5.9-40-17.6-15.1-26.1-15.1-43.3c0-17.1 5-31.6 15.1-43.3s23.4-17.6 40-17.6 30 5.9 40.2 17.6 15.3 26.1 15.3 43.3-5.1 31.6-15.3 43.3zM1381 416.1c-18.1-11.5-38.3-17.3-60.5-17.4-32 0-56.9 9.9-74.7 29.8v-20.4h-84.5v390.7h84.5v-164c17.8 19.9 42.7 29.8 74.7 29.8 22.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.8c-10.3-20.3-24.4-36.2-42.5-47.8zM1337.9 575c-10.1 11.7-23.4 17.6-40 17.6s-29.9-5.9-40-17.6-15.1-26.1-15.1-43.3c0-17.1 5-31.6 15.1-43.3s23.4-17.6 40-17.6 29.9 5.9 40 17.6 15.1 26.1 15.1 43.3-5.1 31.6-15.1 43.3zM1672.2 416.8c-20.5-12-43-18-67.6-18-24.9 0-47.6 5.9-68 17.6-20.4 11.7-36.5 27.7-48.2 48s-17.6 42.7-17.6 67.3c.3 25.2 6.2 47.8 17.8 68 11.5 20.2 28 36 49.3 47.6 21.3 11.5 45.9 17.3 73.8 17.3 48.6 0 86.8-14.7 114.7-44l-52.5-48.9c-8.6 8.3-17.6 14.6-26.7 19-9.3 4.3-21.1 6.4-35.3 6.4-11.6 0-22.5-3.6-32.7-10.9-10.3-7.3-17.1-16.5-20.7-27.8h180l.4-11.6c0-29.6-6-55.7-18-78.2s-28.3-39.8-48.7-51.8zm-113.9 86.4c2.1-12.1 7.5-21.8 16.2-29.1s18.7-10.9 30-10.9 21.2 3.6 29.8 10.9c8.6 7.2 13.9 16.9 16 29.1h-92zM1895.3 411.7c-11 5.6-20.3 13.7-28 24.4h-.1v-28h-84.5v247.3h84.5V536.3c0-22.6 4.7-38.1 14.2-46.5 9.5-8.5 22.7-12.7 39.6-12.7 6.2 0 13.5 1 21.8 3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9-10.6 0-21.4 2.8-32.4 8.4zM1985 277.4h84.5v377.8H1985zM2313.2 416.8c-20.5-12-43-18-67.6-18-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.3 25.2 6.2 47.8 17.8 68 11.5 20.2 28 36 49.3 47.6 21.3 11.5 45.9 17.3 73.8 17.3 48.6 0 86.8-14.7 114.7-44l-52.5-48.9c-8.6 8.3-17.6 14.6-26.7 19-9.3 4.3-21.1 6.4-35.3 6.4-11.6 0-22.5-3.6-32.7-10.9-10.3-7.3-17.1-16.5-20.7-27.8h180l.4-11.6c0-29.6-6-55.7-18-78.2s-28.3-39.8-48.7-51.8zm-113.9 86.4c2.1-12.1 7.5-21.8 16.2-29.1s18.7-10.9 30-10.9 21.2 3.6 29.8 10.9c8.6 7.2 13.9 16.9 16 29.1h-92zM2583.6 507.7c-13.8-4.4-30.6-8.1-50.5-11.1-15.1-2.7-26.1-5.2-32.9-7.6-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.3 11.6 0 24.3 2.4 38.1 7.2 13.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-18.7-7.1-39.6-10.7-62.7-10.7-33.7 0-60.2 7.6-79.3 22.7-19.1 15.1-28.7 36.1-28.7 63.1 0 19 4.8 33.9 14.4 44.7 9.6 10.8 21 18.5 34 22.9 13.1 4.5 28.9 8.3 47.6 11.6 14.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-13.7 0-28.5-2.3-44.7-6.9-16.1-4.6-29.2-11.3-39.3-20.2l-33.3 60c9.2 7.4 24.6 14.7 46.2 22 21.7 7.3 45.2 10.9 70.7 10.9 34.7 0 62.9-7.4 84.5-22.4 21.7-15 32.5-37.3 32.5-66.9 0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7zM2883.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.1-15.1-2.7-26.1-5.2-32.9-7.6-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.3 11.6 0 24.3 2.4 38.1 7.2 13.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-18.7-7.1-39.6-10.7-62.7-10.7-33.7 0-60.2 7.6-79.3 22.7-19.1 15.1-28.7 36.1-28.7 63.1 0 19 4.8 33.9 14.4 44.7 9.6 10.8 21 18.5 34 22.9 13.1 4.5 28.9 8.3 47.6 11.6 14.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-13.7 0-28.5-2.3-44.7-6.9-16.1-4.6-29.2-11.3-39.3-20.2l-33.3 60c9.2 7.4 24.6 14.7 46.2 22 21.7 7.3 45.2 10.9 70.7 10.9 34.7 0 62.9-7.4 84.5-22.4 21.7-15 32.5-37.3 32.5-66.9zM2460.7 738.7h59.6v17.2h-59.6zM2596.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-20 4.2-4.7 9.5-7.1 15.9-7.1 7.8 0 13.4 2.3 16.8 6.7 3.4 4.5 5.1 11.3 5.1 20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3-6.4-6.7-14.9-10.1-25.7-10.1zM2733.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.3c-4 7.3-6 15.4-6 24.4 0 8.9 2 17.1 6 24.3 4 7.3 9.5 13 16.5 17.2s14.9 6.3 23.5 6.3c5.6 0 11-1 16.2-3.1 5.1-2.1 9.5-4.8 13.1-8.2v24.4c0 8.5-2.5 14.8-7.6 18.7-5 3.9-11 5.9-18 5.9-6.7 0-12.4-1.6-17.3-4.7-4.8-3.1-7.6-7.7-8.3-13.8h-19.4c.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 4 12.8 0 23.5-3.4 32-10.1 8.6-6.7 12.8-17.1 12.8-31.1V708.9h-19.2v8.8zm-1.6 52.4c-2.5 4.7-6 8.3-10.4 11.2-4.4 2.7-9.4 4-14.9 4-5.7 0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4c-2.3-4.8-3.5-9.8-3.5-15.2 0-5.5 1.1-10.6 3.5-15.3s5.8-8.5 10.2-11.3 9.5-4.2 15.2-4.2c5.5 0 10.5 1.4 14.9 4s7.9 6.3 10.4 11 3.8 10 3.8 15.8-1.3 11-3.8 15.7zM2867.9 708.9h-21.4l-25.6 33-25.4-33h-22.4l36 46.1-37.6 47.5h21.4l27.2-34.6 27.1 34.7h22.4l-37.6-48.2zM757.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.3-9.9 20.8-21.1 40.6-33.6 59.4v207.2h88.9V521.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.3.1-22.7-5.6-43.3-17-61.9-11.4-18.7-27.1-33.4-47.1-44.2zm-41 140.6c-9.3 8.9-21.6 13.3-36.7 13.3l-62.2.4v-92.5l62.2-.4c15.1 0 27.3 4.4 36.7 13.3 9.4 8.9 14 19.9 14 32.9 0 13.2-4.6 24.1-14 33z"/>
|
||||
<path d="M140 713.7c-3.4-16.4-10.3-49.1-11.2-49.1C-16.9 577.5.4 426.6 48.6 340.4 59 449 251.2 524 139.1 656.8c-.9 1.7 5.2 22.4 10.3 41.4 22.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-1.7.9-62.9 108.6-65.5 109.5 0-1.7-25.9-.9-22.4-9.5 1.6-5.2 5.1-12 8.5-18.9zm-4.3-81.1c44-50.9-7.8-137.9-38.8-166.4 52.6 90.5 49.1 143.1 38.8 166.4z" style="fill:#17541f"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="264.567" height="318.552" viewBox="0 0 70 84.284">
|
||||
<path style="fill:#17541f;stroke-width:1.10017" d="M752.438 82.365C638.02 348.605 87.938 381.61 263.964 810.674c2.2 5.5-40.706 63.81-69.31 112.217-6.602-24.204-14.304-50.607-13.204-52.807C324.473 700.658 79.136 604.944 65.934 466.322 4.324 576.34-17.678 768.868 168.25 879.984c1.1 0 9.902 41.808 14.303 62.711-4.4 8.802-8.802 17.602-11.002 24.203-4.4 11.002 28.603 9.902 28.603 12.102 3.3-1.1 81.413-138.62 83.614-139.72 442.267-101.216 520.377-499.476 468.67-756.915ZM526.904 362.906c-206.831 184.828-242.036 322.35-235.435 389.46-69.31-163.926 134.22-344.353 235.435-389.46ZM127.543 626.947c39.606 36.306 105.616 147.422 49.508 212.332 13.202-29.704 17.602-96.814-49.508-212.332z" transform="matrix(.094 0 0 .094 -2.042 -7.742)" fill="#17541F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 855 B |
@@ -1,69 +1,3 @@
|
||||
<?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"
|
||||
width="69.999977mm"
|
||||
height="84.283669mm"
|
||||
viewBox="0 0 69.999977 84.283669"
|
||||
version="1.1"
|
||||
id="svg4812"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||
sodipodi:docname="logo-dark-notext.svg">
|
||||
<defs
|
||||
id="defs4806" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.98994949"
|
||||
inkscape:cx="328.04904"
|
||||
inkscape:cy="330.33332"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="SvgjsG1020"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata4809">
|
||||
<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>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-9.9999792,-10.000082)">
|
||||
<g
|
||||
id="SvgjsG1020"
|
||||
featureKey="symbol1"
|
||||
fill="#ffffff"
|
||||
transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)">
|
||||
<path
|
||||
id="path57"
|
||||
style="fill:#ffffff;stroke-width:1.10017"
|
||||
d="M 752.4375,82.365234 C 638.02019,348.60552 87.938206,381.6089 263.96484,810.67383 c 2.20034,5.50083 -40.70621,63.80947 -69.31054,112.21679 -6.601,-24.20366 -14.30329,-50.6063 -13.20313,-52.80664 C 324.47281,700.65835 79.135592,604.94324 65.933594,466.32227 4.3242706,576.33891 -17.678136,768.86756 168.25,879.98438 c 1.10017,-10e-6 9.90207,41.80777 14.30273,62.71093 -4.40066,8.80133 -8.80162,17.60213 -11.00195,24.20313 -4.40066,11.00166 28.60352,9.90123 28.60352,12.10156 3.3005,-1.10017 81.41295,-138.62054 83.61328,-139.7207 C 726.0345,738.06398 804.14532,339.80419 752.4375,82.365234 Z M 526.9043,362.90625 C 320.073,547.73422 284.86775,685.25508 291.46875,752.36523 222.15826,588.44043 425.68898,408.01308 526.9043,362.90625 Z M 127.54297,626.94727 c 39.60599,36.30549 105.6163,147.4222 49.50781,212.33203 13.202,-29.7045 17.60234,-96.81455 -49.50781,-212.33203 z"
|
||||
transform="matrix(0.90895334,0,0,0.90895334,65.06894,-58.865357)" />
|
||||
<defs
|
||||
id="defs14302" />
|
||||
</g>
|
||||
</g>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="264.567" height="318.552" viewBox="0 0 70 84.284">
|
||||
<path style="fill:#fff;stroke-width:1.10017" d="M752.438 82.365C638.02 348.605 87.938 381.61 263.964 810.674c2.2 5.5-40.706 63.81-69.31 112.217-6.602-24.204-14.304-50.607-13.204-52.807C324.473 700.658 79.136 604.944 65.934 466.322 4.324 576.34-17.678 768.868 168.25 879.984c1.1 0 9.902 41.808 14.303 62.711-4.4 8.802-8.802 17.602-11.002 24.203-4.4 11.002 28.603 9.902 28.603 12.102 3.3-1.1 81.413-138.62 83.614-139.72 442.267-101.216 520.377-499.476 468.67-756.915ZM526.904 362.906c-206.831 184.828-242.036 322.35-235.435 389.46-69.31-163.926 134.22-344.353 235.435-389.46ZM127.543 626.947c39.606 36.306 105.616 147.422 49.508 212.332 13.202-29.704 17.602-96.814-49.508-212.332z" transform="matrix(.094 0 0 .094 -2.042 -7.742)" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 849 B |
@@ -1,71 +1,4 @@
|
||||
<?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="svg9580" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="logo.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2897.4 896.6"
|
||||
style="enable-background:new 0 0 2897.4 896.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#17541F;}
|
||||
</style>
|
||||
<sodipodi:namedview bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview9582" inkscape:current-layer="g9578" inkscape:cx="1393.617" inkscape:cy="393.61704" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1280" inkscape:window-y="27" inkscape:zoom="0.46439736" objecttolerance="10" pagecolor="#ffffff" showgrid="false">
|
||||
</sodipodi:namedview>
|
||||
<g>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"
|
||||
/>
|
||||
<rect x="1985" y="277.4" width="84.5" height="377.8"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<rect x="2460.7" y="738.7" width="59.6" height="17.2"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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 "/>
|
||||
<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"/>
|
||||
</g>
|
||||
<path class="st0" 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"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" style="enable-background:new 0 0 2897.4 896.6" xml:space="preserve">
|
||||
<path d="M1022.3 428.7c-17.8-19.9-42.7-29.8-74.7-29.8-22.3 0-42.4 5.7-60.5 17.3-18.1 11.6-32.3 27.5-42.5 47.8s-15.3 42.9-15.3 67.8 5.1 47.5 15.3 67.8c10.3 20.3 24.4 36.2 42.5 47.8 18.1 11.5 38.3 17.3 60.5 17.3 32 0 56.9-9.9 74.7-29.8V655.5h84.5V408.3h-84.5v20.4zM1010.5 575c-10.2 11.7-23.6 17.6-40.2 17.6s-29.9-5.9-40-17.6-15.1-26.1-15.1-43.3c0-17.1 5-31.6 15.1-43.3s23.4-17.6 40-17.6 30 5.9 40.2 17.6 15.3 26.1 15.3 43.3-5.1 31.6-15.3 43.3zM1381 416.1c-18.1-11.5-38.3-17.3-60.5-17.4-32 0-56.9 9.9-74.7 29.8v-20.4h-84.5v390.7h84.5v-164c17.8 19.9 42.7 29.8 74.7 29.8 22.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.8c-10.3-20.3-24.4-36.2-42.5-47.8zM1337.9 575c-10.1 11.7-23.4 17.6-40 17.6s-29.9-5.9-40-17.6-15.1-26.1-15.1-43.3c0-17.1 5-31.6 15.1-43.3s23.4-17.6 40-17.6 29.9 5.9 40 17.6 15.1 26.1 15.1 43.3-5.1 31.6-15.1 43.3zM1672.2 416.8c-20.5-12-43-18-67.6-18-24.9 0-47.6 5.9-68 17.6-20.4 11.7-36.5 27.7-48.2 48s-17.6 42.7-17.6 67.3c.3 25.2 6.2 47.8 17.8 68 11.5 20.2 28 36 49.3 47.6 21.3 11.5 45.9 17.3 73.8 17.3 48.6 0 86.8-14.7 114.7-44l-52.5-48.9c-8.6 8.3-17.6 14.6-26.7 19-9.3 4.3-21.1 6.4-35.3 6.4-11.6 0-22.5-3.6-32.7-10.9-10.3-7.3-17.1-16.5-20.7-27.8h180l.4-11.6c0-29.6-6-55.7-18-78.2s-28.3-39.8-48.7-51.8zm-113.9 86.4c2.1-12.1 7.5-21.8 16.2-29.1s18.7-10.9 30-10.9 21.2 3.6 29.8 10.9c8.6 7.2 13.9 16.9 16 29.1h-92zM1895.3 411.7c-11 5.6-20.3 13.7-28 24.4h-.1v-28h-84.5v247.3h84.5V536.3c0-22.6 4.7-38.1 14.2-46.5 9.5-8.5 22.7-12.7 39.6-12.7 6.2 0 13.5 1 21.8 3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9-10.6 0-21.4 2.8-32.4 8.4zM1985 277.4h84.5v377.8H1985zM2313.2 416.8c-20.5-12-43-18-67.6-18-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.3 25.2 6.2 47.8 17.8 68 11.5 20.2 28 36 49.3 47.6 21.3 11.5 45.9 17.3 73.8 17.3 48.6 0 86.8-14.7 114.7-44l-52.5-48.9c-8.6 8.3-17.6 14.6-26.7 19-9.3 4.3-21.1 6.4-35.3 6.4-11.6 0-22.5-3.6-32.7-10.9-10.3-7.3-17.1-16.5-20.7-27.8h180l.4-11.6c0-29.6-6-55.7-18-78.2s-28.3-39.8-48.7-51.8zm-113.9 86.4c2.1-12.1 7.5-21.8 16.2-29.1s18.7-10.9 30-10.9 21.2 3.6 29.8 10.9c8.6 7.2 13.9 16.9 16 29.1h-92zM2583.6 507.7c-13.8-4.4-30.6-8.1-50.5-11.1-15.1-2.7-26.1-5.2-32.9-7.6-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.3 11.6 0 24.3 2.4 38.1 7.2 13.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-18.7-7.1-39.6-10.7-62.7-10.7-33.7 0-60.2 7.6-79.3 22.7-19.1 15.1-28.7 36.1-28.7 63.1 0 19 4.8 33.9 14.4 44.7 9.6 10.8 21 18.5 34 22.9 13.1 4.5 28.9 8.3 47.6 11.6 14.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-13.7 0-28.5-2.3-44.7-6.9-16.1-4.6-29.2-11.3-39.3-20.2l-33.3 60c9.2 7.4 24.6 14.7 46.2 22 21.7 7.3 45.2 10.9 70.7 10.9 34.7 0 62.9-7.4 84.5-22.4 21.7-15 32.5-37.3 32.5-66.9 0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7zM2883.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.1-15.1-2.7-26.1-5.2-32.9-7.6-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.3 11.6 0 24.3 2.4 38.1 7.2 13.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-18.7-7.1-39.6-10.7-62.7-10.7-33.7 0-60.2 7.6-79.3 22.7-19.1 15.1-28.7 36.1-28.7 63.1 0 19 4.8 33.9 14.4 44.7 9.6 10.8 21 18.5 34 22.9 13.1 4.5 28.9 8.3 47.6 11.6 14.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-13.7 0-28.5-2.3-44.7-6.9-16.1-4.6-29.2-11.3-39.3-20.2l-33.3 60c9.2 7.4 24.6 14.7 46.2 22 21.7 7.3 45.2 10.9 70.7 10.9 34.7 0 62.9-7.4 84.5-22.4 21.7-15 32.5-37.3 32.5-66.9zM2460.7 738.7h59.6v17.2h-59.6zM2596.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-20 4.2-4.7 9.5-7.1 15.9-7.1 7.8 0 13.4 2.3 16.8 6.7 3.4 4.5 5.1 11.3 5.1 20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3-6.4-6.7-14.9-10.1-25.7-10.1zM2733.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.3c-4 7.3-6 15.4-6 24.4 0 8.9 2 17.1 6 24.3 4 7.3 9.5 13 16.5 17.2s14.9 6.3 23.5 6.3c5.6 0 11-1 16.2-3.1 5.1-2.1 9.5-4.8 13.1-8.2v24.4c0 8.5-2.5 14.8-7.6 18.7-5 3.9-11 5.9-18 5.9-6.7 0-12.4-1.6-17.3-4.7-4.8-3.1-7.6-7.7-8.3-13.8h-19.4c.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 4 12.8 0 23.5-3.4 32-10.1 8.6-6.7 12.8-17.1 12.8-31.1V708.9h-19.2v8.8zm-1.6 52.4c-2.5 4.7-6 8.3-10.4 11.2-4.4 2.7-9.4 4-14.9 4-5.7 0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4c-2.3-4.8-3.5-9.8-3.5-15.2 0-5.5 1.1-10.6 3.5-15.3s5.8-8.5 10.2-11.3 9.5-4.2 15.2-4.2c5.5 0 10.5 1.4 14.9 4s7.9 6.3 10.4 11 3.8 10 3.8 15.8-1.3 11-3.8 15.7zM2867.9 708.9h-21.4l-25.6 33-25.4-33h-22.4l36 46.1-37.6 47.5h21.4l27.2-34.6 27.1 34.7h22.4l-37.6-48.2zM757.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.3-9.9 20.8-21.1 40.6-33.6 59.4v207.2h88.9V521.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.3.1-22.7-5.6-43.3-17-61.9-11.4-18.7-27.1-33.4-47.1-44.2zm-41 140.6c-9.3 8.9-21.6 13.3-36.7 13.3l-62.2.4v-92.5l62.2-.4c15.1 0 27.3 4.4 36.7 13.3 9.4 8.9 14 19.9 14 32.9 0 13.2-4.6 24.1-14 33z"/>
|
||||
<path d="M140 713.7c-3.4-16.4-10.3-49.1-11.2-49.1C-16.9 577.5.4 426.6 48.6 340.4 59 449 251.2 524 139.1 656.8c-.9 1.7 5.2 22.4 10.3 41.4 22.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-1.7.9-62.9 108.6-65.5 109.5 0-1.7-25.9-.9-22.4-9.5 1.6-5.2 5.1-12 8.5-18.9zm-4.3-81.1c44-50.9-7.8-137.9-38.8-166.4 52.6 90.5 49.1 143.1 38.8 166.4z" style="fill:#17541f"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 5.4 KiB |