diff --git a/.github/workflows/alpha_cleanup.yml b/.github/workflows/alpha_cleanup.yml index bc3815dec4..a7273a2d6b 100644 --- a/.github/workflows/alpha_cleanup.yml +++ b/.github/workflows/alpha_cleanup.yml @@ -24,11 +24,21 @@ jobs: with: fetch-depth: "0" - - name: Install SSH key + - name: Install SSH key for Bastion uses: shimataro/ssh-key-action@v2 with: - key: ${{ secrets.SSH_PRIV_KEY }} - known_hosts: ${{ secrets.SSH_KNOWN_HOST_DEPLOY }} + key: ${{ secrets.DEV_TOOLS_BASTION_PRIVATE_KEY }} + name: id_rsa-bastion + known_hosts: ${{ secrets.KNOWN_HOSTS_OF_BASTION }} + config: ${{ secrets.CONFIG }} + + - name: Install SSH key of target + uses: shimataro/ssh-key-action@v2 + with: + key: ${{ secrets.DEV_TOOLS_EC2_PRIVATE_KEY }} + name: id_rsa-target + known_hosts: ${{ secrets.KNOWN_HOSTS_OF_TARGET }} + config: ${{ secrets.CONFIG }} - name: Clean-up old versions run: bash tools/cleanup.sh ${{ github.event.inputs.appVersion }} diff --git a/.github/workflows/beta_to_prod.yml b/.github/workflows/beta_to_prod.yml index 1ce2347637..cec8f75179 100644 --- a/.github/workflows/beta_to_prod.yml +++ b/.github/workflows/beta_to_prod.yml @@ -16,11 +16,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install SSH key + - name: Install SSH key for Bastion uses: shimataro/ssh-key-action@v2 with: - key: ${{ secrets.SSH_PRIV_KEY }} - known_hosts: ${{ secrets.SSH_KNOWN_HOST_DEPLOY }} + key: ${{ secrets.DEV_TOOLS_BASTION_PRIVATE_KEY }} + name: id_rsa-bastion + known_hosts: ${{ secrets.KNOWN_HOSTS_OF_BASTION }} + config: ${{ secrets.CONFIG }} + + - name: Install SSH key of target + uses: shimataro/ssh-key-action@v2 + with: + key: ${{ secrets.DEV_TOOLS_EC2_PRIVATE_KEY }} + name: id_rsa-target + known_hosts: ${{ secrets.KNOWN_HOSTS_OF_TARGET }} + config: ${{ secrets.CONFIG }} - name: Ready for promotion run: bash tools/deploy.sh ${{ github.event.inputs.betaVersion }} diff --git a/.github/workflows/functional_tests.yml b/.github/workflows/functional_tests.yml index 133884775f..1120dc9ca2 100644 --- a/.github/workflows/functional_tests.yml +++ b/.github/workflows/functional_tests.yml @@ -52,6 +52,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: functional env_vars: OS,PYTHON @@ -81,6 +82,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: functional env_vars: OS,PYTHON @@ -111,6 +113,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: functional env_vars: OS,PYTHON diff --git a/.github/workflows/functional_tests_2023.yml b/.github/workflows/functional_tests_2023.yml index 268b58088d..dd5917eddb 100644 --- a/.github/workflows/functional_tests_2023.yml +++ b/.github/workflows/functional_tests_2023.yml @@ -52,6 +52,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: functional env_vars: OS,PYTHON @@ -81,6 +82,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: functional env_vars: OS,PYTHON @@ -105,6 +107,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: functional env_vars: OS,PYTHON diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 3fbb1c8dac..d51f4c0e60 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -63,6 +63,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: integration env_vars: OS,PYTHON diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1e55c02c5e..e69de29bb2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,10 +0,0 @@ -name: test - -on: - workflow_dispatch: -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Use GitHub secret - run: echo ${{ secrets.MY_SECRET }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0619b206ba..269c7475d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,10 @@ on: description: 'Set to "release" for a beta release.' required: false default: "alpha" + signExe: + description: 'Set to "true" to generate sign .exe on Windows.' + required: false + default: "false" env: GITHUB_USERNAME: "nuxeodrive" @@ -98,13 +102,13 @@ jobs: echo "${{ secrets.CERT_APP_MACOS }}" | base64 --decode > developerID_application.cer echo "${{ secrets.PRIV_APP_MACOS }}" | base64 --decode > nuxeo-drive.priv - - name: "[macOS] Downloading Python" - if: matrix.os == 'macos-latest' - run: curl https://www.python.org/ftp/python/3.9.5/python-3.9.5-macosx10.9.pkg -o "python.pkg" # XXX_PYTHON + #- name: "[macOS] Downloading Python" + # if: matrix.os == 'macos-latest' + # run: curl https://www.python.org/ftp/python/3.9.5/python-3.9.5-macosx10.9.pkg -o "python.pkg" # XXX_PYTHON - - name: "[macOS] Install Python" - if: matrix.os == 'macos-latest' - run: sudo installer -pkg python.pkg -target / + #- name: "[macOS] Install Python" + # if: matrix.os == 'macos-latest' + # run: sudo installer -pkg python.pkg -target / - name: "[macOS] Setup the environment" if: matrix.os == 'macos-latest' @@ -125,6 +129,7 @@ jobs: NOTARIZATION_PASSWORD: ${{ secrets.NOTARIZATION_PASSWORD }} NOTARIZATION_TEAMID: ${{ secrets.NOTARIZATION_TEAMID }} SIGNING_ID: "NUXEO CORP" + SIGNING_ID_NEW: "Hyland Software, Inc." SYSTEM_VERSION_COMPAT: 0 run: bash tools/osx/deploy_ci_agent.sh --check-upgrade @@ -148,13 +153,6 @@ jobs: # # Windows # - - - name: "[Windows] Setup certificate" - if: matrix.os == 'windows-latest' - run: | - echo "${{ secrets.CERT_APP_WINDOWS }}" > certificate.b64 - certutil -decode certificate.b64 certificate.pfx - - name: "[Windows] Unlock PowerShell" if: matrix.os == 'windows-latest' run: powershell Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope LocalMachine @@ -163,11 +161,60 @@ jobs: if: matrix.os == 'windows-latest' run: powershell ".\\tools\\windows\\deploy_ci_agent.ps1" -install_release + - name: Setup Certificate + if: matrix.os == 'windows-latest' + run: | + echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 + cat /d/Certificate_pkcs12.p12 + shell: bash + + - name: Set variables + if: matrix.os == 'windows-latest' + id: variables + run: | + dir + echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + echo "KEYPAIR_NAME=gt-standard-keypair" >> $GITHUB_OUTPUT + echo "CERTIFICATE_NAME=gt-certificate" >> $GITHUB_OUTPUT + echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV" + echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV" + echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV" + echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV" + echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> "$GITHUB_ENV" + echo "SM_CODE_SIGNING_CERT_SHA1_HASH=${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}" >> "$GITHUB_ENV" + echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH + echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH + echo "C:\Program Files\DigiCert\DigiCert Keylocker Tools" >> $GITHUB_PATH + shell: bash + + - name: Setup Keylocker KSP on windows + if: matrix.os == 'windows-latest' + run: | + curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/Keylockertools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o Keylockertools-windows-x64.msi + msiexec /i Keylockertools-windows-x64.msi /quiet /qn + smksp_registrar.exe list + smctl.exe keypair ls + C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user + shell: cmd + + - name: Certificates Sync + if: matrix.os == 'windows-latest' + run: | + smctl windows certsync --keypair-alias=${{ secrets.SM_KEYPAIR_ALIAS }} + shell: cmd + + - name: Health status + if: matrix.os == 'windows-latest' + run: | + smctl healthcheck + shell: cmd + + - name: "[Windows] Generate the .exe and validate against 2021" timeout-minutes: 15 if: matrix.os == 'windows-latest' env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} NXDRIVE_TEST_NUXEO_URL: "https://drive-2021.beta.nuxeocloud.com/nuxeo" SIGNING_ID: "Nuxeo" SIGNTOOL_PATH: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.20348.0\x86' @@ -177,7 +224,6 @@ jobs: timeout-minutes: 15 if: matrix.os == 'windows-latest' env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} NXDRIVE_TEST_NUXEO_URL: "https://drive-2023.beta.nuxeocloud.com/nuxeo" NXDRIVE_TEST_USERNAME: ${{ secrets.NXDRIVE_2023_TEST_USERNAME }} NXDRIVE_TEST_PASSWORD: ${{ secrets.NXDRIVE_2023_TEST_PASSWORD }} @@ -185,6 +231,15 @@ jobs: SIGNTOOL_PATH: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.20348.0\x86' run: powershell ".\\tools\\windows\\deploy_ci_agent.ps1" -check_upgrade + - name: "[Windows] Generate and sign the .exe" + timeout-minutes: 15 + if: matrix.os == 'windows-latest' && github.event.inputs.signExe == 'true' + env: + KEYCHAIN_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} + SIGNING_ID_NEW: "Hyland Software, Inc." + SIGNTOOL_PATH: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.20348.0\x86' + run: powershell ".\\tools\\windows\\deploy_ci_agent.ps1" -build_installer_and_sign + - name: "Upload artifacts" uses: actions/upload-artifact@v4 with: @@ -210,11 +265,21 @@ jobs: run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT id: extract_branch - - name: Install SSH key + - name: Install SSH key for Bastion uses: shimataro/ssh-key-action@v2 with: - key: ${{ secrets.SSH_PRIV_KEY }} - known_hosts: ${{ secrets.SSH_KNOWN_HOST_DEPLOY }} + key: ${{ secrets.DEV_TOOLS_BASTION_PRIVATE_KEY }} + name: id_rsa-bastion + known_hosts: ${{ secrets.KNOWN_HOSTS_OF_BASTION }} + config: ${{ secrets.CONFIG }} + + - name: Install SSH key of target + uses: shimataro/ssh-key-action@v2 + with: + key: ${{ secrets.DEV_TOOLS_EC2_PRIVATE_KEY }} + name: id_rsa-target + known_hosts: ${{ secrets.KNOWN_HOSTS_OF_TARGET }} + config: ${{ secrets.CONFIG }} - name: Setup git run: | git config user.email ${{ env.GITHUB_EMAILID }} diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index d23a9056b6..5cf34a9dcc 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -42,6 +42,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: unit env_vars: OS,PYTHON @@ -74,6 +75,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: unit env_vars: OS,PYTHON @@ -106,6 +108,7 @@ jobs: if: ${{ success() }} || ${{ failure() }} uses: codecov/codecov-action@v3.1.5 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage.xml flags: unit env_vars: OS,PYTHON diff --git a/docs/changes.md b/docs/changes.md index 7b8bda6531..e10bec5f1c 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -20,7 +20,8 @@ - [5.3.1](changes/5.3.1.md) ([diff](https://github.com/nuxeo/nuxeo-drive/compare/release-5.3.0...release-5.3.1)) - [5.3.2](changes/5.3.2.md) ([diff](https://github.com/nuxeo/nuxeo-drive/compare/release-5.3.1...release-5.3.2)) - [5.4.0](changes/5.4.0.md) ([diff](https://github.com/nuxeo/nuxeo-drive/compare/release-5.3.2...release-5.4.0)) -- [5.5.0](changes/5.5.0.md) ([diff](https://github.com/nuxeo/nuxeo-drive/compare/release-5.4.0...master)) +- [5.4.1](changes/5.4.1.md) ([diff](https://github.com/nuxeo/nuxeo-drive/compare/release-5.4.0...release-5.4.1)) +- [5.5.0](changes/5.5.0.md) ([diff](https://github.com/nuxeo/nuxeo-drive/compare/release-5.4.1...master)) ## 4.x diff --git a/docs/changes/5.4.1.md b/docs/changes/5.4.1.md new file mode 100644 index 0000000000..0450d46e3d --- /dev/null +++ b/docs/changes/5.4.1.md @@ -0,0 +1,101 @@ +# 5.4.1 + +Release date: `2024-06-18` + +## Core + +- [NXDRIVE-2882](https://jira.nuxeo.com/browse/NXDRIVE-2882): fix_db should create dump.sql in same dir as db +- [NXDRIVE-2901](https://jira.nuxeo.com/browse/NXDRIVE-2901): Authorization Error for OAuth + + +## GUI + +- [NXDRIVE-2900](https://jira.nuxeo.com/browse/NXDRIVE-2900): Update license headers for Nuxeo addons + +## Packaging / Build + +- [NXDRIVE-2896](https://jira.nuxeo.com/browse/NXDRIVE-2896): Fix release build for upload/download artifact +- [NXDRIVE-2923](https://jira.nuxeo.com/browse/NXDRIVE-2923): Download nuxeo package from nexus sonatatype +- [NXDRIVE-2926] (https://jira.nuxeo.com/browse/NXDRIVE-2926): Update github Action Runner to use mac-latest +- [NXDRIVE-2932] (https://jira.nuxeo.com/browse/NXDRIVE-2932): Fix Microsoft Visual Studio issue +- [NXDRIVE-2938] (https://jira.nuxeo.com/browse/NXDRIVE-2938): Update token for codecov +- [NXDRIVE-2941] (https://jira.nuxeo.com/browse/NXDRIVE-2941): Update the release process to sign Windows exe to limit signature usage + +## Minor Changes + +- Added `build` 1.1.1 +- Added `pyproject-hooks` 1.0.0 +- Added `PyQt5-Qt5` 5.15.13 (for MAC) +- Added `PyQt5-Qt5` 5.15.2 (for Windows and Linux) +- Added `setuptools` 69.5.1 +- Added `tomli` 2.0.1 +- Removed `toml` 0.10.2 +- Upgraded `actions/cache` from 3 to 4 +- Upgraded `actions/download-artifact` from 3 to 4 +- Upgraded `actions/setup-python` from 4 to 5 +- Upgraded `actions/upload-artifact` from 3 to 4 +- Upgraded `apipkg` from 1.5 to 3.0.2 +- Upgraded `attrs` from 23.1.0 to 23.2.0 +- Upgraded `authlib` from 1.1.0 to 1.3.0 +- Upgraded `black` from 23.9.1 to 23.12.1 +- Upgraded `boto3` from 1.28.50 to 1.34.17 +- Upgraded `botocore` from 1.31.50 to 1.34.17 +- Upgraded `cffi` from 1.15.1 to 1.16.1 +- Upgraded `click` from 8.0.1 to 8.1.7 +- Upgraded `codecov/codecov-action` from 3.1.4 to 3.1.5 +- Upgraded `codespell` from 2.2.4 to 2.2.6 +- Upgraded `colorama` from 0.4.4 to 0.4.6 +- Upgraded `comtypes` from 1.2.0 to 1.2.1 +- Upgraded `cryptography` from 41.0.7 to 42.0.5 +- Upgraded `distlib` from 0.3.7 to 0.3.8 +- Upgraded `docker/build-push-action` from 5.0.0 to 5.1.0 +- Upgraded `dukpy` from 0.2.3 to 0.3.1 +- Upgraded `exceptiongroup` from 1.1.3 to 1.2.0 +- Upgraded `faker` from 19.6.2 to 22.0.0 +- Upgraded `identify` from 2.5.29 to 2.5.33 +- Upgraded `idna` from 3.4 to 3.6 +- Upgraded `importlib-metadata` from 6.8.0 to 7.0.1 +- Upgraded `junitparser` from 3.1.0 to 3.1.1 +- Upgraded `more-itertools` from 10.1.0 to 10.2.0 +- Upgraded `packaging` from 23.1 to 24.0 +- Upgraded `pathspec` from 0.11.2 to 0.12.1 +- Upgraded `pefile` from 2021.5.24 to 2023.2.7 +- Upgraded `pep517` from 0.10.0 to 0.13.1 +- Upgraded `pip` from 22.0.4 to 24.0 +- Upgraded `pip-tools` from 6.5.1 to 7.4.1 +- Upgraded `platformdirs` from 3.10.0 to 4.2.0 +- Upgraded `pluggy` from 1.3.0 to 1.4.0 +- Upgraded `pycodestyle` from 2.11.0 to 2.11.1 +- Upgraded `pyfakefs` from 5.3.4 to 5.3.5 +- Upgraded `pyinstaller` from 5.0 to 5.13.2 +- Upgraded `pyinstaller-hooks-contrib` from 2021.2 to 2023.8 +- Upgraded `pyobjc-core` from 7.3 to 10.1 +- Upgraded `pyobjc-framework-cocoa` from 7.3 to 10.1 +- Upgraded `pyobjc-framework-coreservices` from 7.3 to 10.1 +- Upgraded `pyobjc-framework-fsevents` from 7.3 to 10.1 +- Upgraded `pyobjc-framework-scriptingbridge` from 7.3 to 10.1 +- Upgraded `pyobjc-framework-systemconfiguration` from 7.3 to 10.1 +- Upgraded `pyqt5` from 5.15.2 to 5.15.10 +- Upgraded `pyqt5-sip` from 12.8.1 to 12.13.0 +- Upgraded `pytest` from 7.4.0 to 7.4.4 +- Upgraded `pytest-timeout` from 2.0.2 to 2.2.0 +- Upgraded `pytest-xdist` from 3.3.1 to 3.5.0 +- Upgraded `pywin32-ctypes` from 0.2.0 to 0.2.2 +- Upgraded `pyyaml` from 5.4.1 to 6.0.1 +- Upgraded `regex` from 2023.8.8 to 2023.12.25 +- Upgraded `responses` from 0.23.3 to 0.24.1 +- Upgraded `s3transfer` from 0.6.0 to 0.10.0 +- Upgraded `tld` from 0.12.6 to 0.13 +- Upgraded `types-python-dateutil` from 2.8.19.2 to 2.8.19.20240106 +- Upgraded `typing-extensions` from 4.7.1 to 4.9.0 +- Upgraded `vulture` from 2.10 to 2.11 +- Upgraded `watchdog` from 2.1.6 to 3.0.0 +- Upgraded `wcwidth` from 0.2.6 to 0.2.13 +- Upgraded `wheel` from 0.38.4 to 0.43.0 +- Upgraded `xattr` from 0.9.7 to 1.1.0 +- Upgraded `zipp` from 3.5.0 to 3.18.0 + +## Technical Changes + +- Check Drive version in Windows `ndrive.exe --version > version.txt` and then run `type version.txt` +- Replaced `distutils.version` with `packaging.version` diff --git a/docs/changes/5.5.0.md b/docs/changes/5.5.0.md index 157c57adb6..30b6341514 100644 --- a/docs/changes/5.5.0.md +++ b/docs/changes/5.5.0.md @@ -1,107 +1,51 @@ # 5.5.0 -Release date: `2024-xx-xx` +Release date: `2024-07-26` ## Core -- [NXDRIVE-2882](https://jira.nuxeo.com/browse/NXDRIVE-2882): fix_db should create dump.sql in same dir as db -- [NXDRIVE-2901](https://jira.nuxeo.com/browse/NXDRIVE-2901): Authorization Error for OAuth -- [NXDRIVE-2](https://jira.nuxeo.com/browse/NXDRIVE-2): +- [NXDRIVE-2920](https://jira.nuxeo.com/browse/NXDRIVE-2920): Upgrade to TLS 1.2 -### Direct Edit - -- [NXDRIVE-2](https://jira.nuxeo.com/browse/NXDRIVE-2): - -### Direct Transfer - -- [NXDRIVE-2](https://jira.nuxeo.com/browse/NXDRIVE-2): - -## GUI - -- [NXDRIVE-2900](https://jira.nuxeo.com/browse/NXDRIVE-2900): Update license headers for Nuxeo addons ## Packaging / Build -- [NXDRIVE-2896](https://jira.nuxeo.com/browse/NXDRIVE-2896): Fix release build for upload/download artifact +- [NXDRIVE-2928](https://jira.nuxeo.com/browse/NXDRIVE-2928): Fix security issue IDNA vulnerable to denial of service from specially crafted inputs to idna.encode +- [NXDRIVE-2930] (https://jira.nuxeo.com/browse/NXDRIVE-2930): Fix code scanning issue +- [NXDRIVE-2936] (https://jira.nuxeo.com/browse/NXDRIVE-2936): Fix security issue Requests Session object does not verify requests after making first request with verify=False -## Tests +### Task Management -- [NXDRIVE-2](https://jira.nuxeo.com/browse/NXDRIVE-2): +- [NXDRIVE-2889](https://jira.nuxeo.com/browse/NXDRIVE-2889): Display system notification for document review +- [NXDRIVE-2890](https://jira.nuxeo.com/browse/NXDRIVE-2890): List all pending documents for review +- [NXDRIVE-2899](https://jira.nuxeo.com/browse/NXDRIVE-2899): Display new workflow feature on Features tab +- [NXDRIVE-2912](https://jira.nuxeo.com/browse/NXDRIVE-2912): Display Drive notification for document review +- [NXDRIVE-2943](https://jira.nuxeo.com/browse/NXDRIVE-2943): Fix Tabbar behaviour and change "No tasks available" Label for Task Window +- [NXDRIVE-2948](https://jira.nuxeo.com/browse/NXDRIVE-2948): Add translations to hardcoded strings and refractor the code -## Docs +## Tests -- [NXDRIVE-2](https://jira.nuxeo.com/browse/NXDRIVE-2): +- [NXDRIVE-2933](https://jira.nuxeo.com/browse/NXDRIVE-2933): Fix redos in py library when used with subversion +- [NXDRIVE-2949](https://jira.nuxeo.com/browse/NXDRIVE-2949): Fix dependabot for the multiple PRs getting opened for the same dependency ## Minor Changes -- Added `build` 1.1.1 -- Added `pyproject-hooks` 1.0.0 -- Added `tomli` 2.0.1 -- Removed `toml` 0.10.2 -- Upgraded `actions/cache` from 3 to 4 -- Upgraded `actions/download-artifact` from 3 to 4 -- Upgraded `actions/setup-python` from 4 to 5 -- Upgraded `actions/upload-artifact` from 3 to 4 -- Upgraded `apipkg` from 1.5 to 3.0.2 -- Upgraded `attrs` from 23.1.0 to 23.2.0 -- Upgraded `authlib` from 1.1.0 to 1.3.0 -- Upgraded `black` from 23.9.1 to 23.12.1 -- Upgraded `boto3` from 1.28.50 to 1.34.17 -- Upgraded `botocore` from 1.31.50 to 1.34.17 -- Upgraded `cffi` from 1.15.1 to 1.16.1 -- Upgraded `click` from 8.0.1 to 8.1.7 -- Upgraded `codecov/codecov-action` from 3.1.4 to 3.1.5 -- Upgraded `codespell` from 2.2.4 to 2.2.6 -- Upgraded `colorama` from 0.4.4 to 0.4.6 -- Upgraded `comtypes` from 1.2.0 to 1.2.1 -- Upgraded `cryptography` from 41.0.7 to 42.0.5 -- Upgraded `distlib` from 0.3.7 to 0.3.8 -- Upgraded `docker/build-push-action` from 5.0.0 to 5.1.0 -- Upgraded `dukpy` from 0.2.3 to 0.3.1 -- Upgraded `exceptiongroup` from 1.1.3 to 1.2.0 -- Upgraded `faker` from 19.6.2 to 22.0.0 -- Upgraded `identify` from 2.5.29 to 2.5.33 -- Upgraded `idna` from 3.4 to 3.6 -- Upgraded `importlib-metadata` from 6.8.0 to 7.0.1 -- Upgraded `junitparser` from 3.1.0 to 3.1.1 -- Upgraded `more-itertools` from 10.1.0 to 10.2.0 -- Upgraded `packaging` from 23.1 to 24.0 -- Upgraded `pathspec` from 0.11.2 to 0.12.1 -- Upgraded `pefile` from 2021.5.24 to 2023.2.7 -- Upgraded `pep517` from 0.10.0 to 0.13.1 -- Upgraded `pip` from 22.0.4 to 24.0 -- Upgraded `pip-tools` from 6.5.1 to 7.4.1 -- Upgraded `platformdirs` from 3.10.0 to 4.2.0 -- Upgraded `pluggy` from 1.3.0 to 1.4.0 -- Upgraded `pycodestyle` from 2.11.0 to 2.11.1 -- Upgraded `pyfakefs` from 5.3.4 to 5.3.5 -- Upgraded `pyinstaller` from 5.0 to 5.13.2 -- Upgraded `pyinstaller-hooks-contrib` from 2021.2 to 2023.8 -- Upgraded `pyobjc-core` from 7.3 to 10.1 -- Upgraded `pyobjc-framework-cocoa` from 7.3 to 10.1 -- Upgraded `pyobjc-framework-coreservices` from 7.3 to 10.1 -- Upgraded `pyobjc-framework-fsevents` from 7.3 to 10.1 -- Upgraded `pyobjc-framework-scriptingbridge` from 7.3 to 10.1 -- Upgraded `pyobjc-framework-systemconfiguration` from 7.3 to 10.1 -- Upgraded `pytest` from 7.4.0 to 7.4.4 -- Upgraded `pytest-timeout` from 2.0.2 to 2.2.0 -- Upgraded `pytest-xdist` from 3.3.1 to 3.5.0 -- Upgraded `pywin32-ctypes` from 0.2.0 to 0.2.2 -- Upgraded `pyyaml` from 5.4.1 to 6.0.1 -- Upgraded `regex` from 2023.8.8 to 2023.12.25 -- Upgraded `responses` from 0.23.3 to 0.24.1 -- Upgraded `s3transfer` from 0.6.0 to 0.10.0 -- Upgraded `tld` from 0.12.6 to 0.13 -- Upgraded `types-python-dateutil` from 2.8.19.2 to 2.8.19.20240106 -- Upgraded `typing-extensions` from 4.7.1 to 4.9.0 -- Upgraded `vulture` from 2.10 to 2.11 -- Upgraded `watchdog` from 2.1.6 to 3.0.0 -- Upgraded `wcwidth` from 0.2.6 to 0.2.13 -- Upgraded `wheel` from 0.38.4 to 0.43.0 -- Upgraded `xattr` from 0.9.7 to 1.1.0 -- Upgraded `zipp` from 3.5.0 to 3.18.0 - -## Technical Changes - -- Replaced `distutils.version` with `packaging.version` -- Check Drive version in Windows `ndrive.exe --version > version.txt` and then run `type version.txt` +- Added `cachetools` 5.3.3 +- Added `pyproject-api` 1.6.1 +- Removed `py` 1.10.0 +- Removed `pytest-forked` 1.6.0 +- Upgraded `build` from 1.1.1 to 1.2.1 +- Upgraded `chardet` from 4.0.0 to 5.2.0 +- Upgraded `exceptiongroup` from 1.2.0 to 1.2.1 +- Upgraded `filelock` from 3.12.4 to 3.14.0 +- Upgraded `identify` from 2.5.33 to 2.5.36 +- Upgraded `idna` from 3.6 to 3.7 +- Upgraded `junitparser` from 3.1.1 to 3.1.2 +- Upgraded `mypy` from 1.5.1 to 1.10.0 +- Upgraded `platformdirs` from 4.2.0 to 4.2.2 +- Upgraded `pluggy` from 1.4.0 to 1.5.0 +- Upgraded `pytest` from 7.4.4 to 8.2.1 +- Upgraded `pytest-cov` from 4.1.0 to 5.0.0 +- Upgraded `pytest-timeout` from 2.2.0 to 2.3.1 +- Upgraded `requests` from 2.31.0 to 2.32.2 +- Upgraded `tox` from 3.24.5 to 4.15.0 +- Upgraded `virtualenv` from 20.4.7 to 20.26.2 diff --git a/nxdrive/client/workflow/__init__.py b/nxdrive/client/workflow/__init__.py new file mode 100644 index 0000000000..b29203f85f --- /dev/null +++ b/nxdrive/client/workflow/__init__.py @@ -0,0 +1,111 @@ +"""Task Management to check for pending document reviews and send notifications""" + +from datetime import datetime, timezone +from logging import getLogger +from typing import Dict, List + +from nuxeo.models import Task + +from nxdrive.engine.engine import Engine + +from ...feature import Feature +from ...utils import get_task_type + +log = getLogger(__name__) + + +class Workflow: + """Workflow Management for document Review""" + + user_task_list: Dict[str, List[str]] = {} + + def fetch_document(self, tasks_list: Dict, engine: Engine) -> None: + """Fetch document details""" + first_task = tasks_list[0] + doc_id = first_task.targetDocumentIds[0]["id"] + + if response := engine.remote.get_info(doc_id, fetch_parent_uid=False): + task_type = get_task_type(first_task.directive) + engine.send_task_notification(first_task.id, response.path, task_type) + else: + log.error("Failed to fetch document details.") + + def update_user_task_data(self, tasks: List[Task], userId: str) -> List[Task]: + """Update user_task_list for below scenarios + 1. Add new key if it doesn't exist in user_task_list + 2. If user_task_list[a_user] have [tasks_a, tasks_b] and got tasks[tasks_a, tasks_c] then + a. Add tasks_c in user_task_list[a_user] and send notification. + b. Remove tasks_b from user_task_list[a_user] and no notification in this case + 3. If user_task_list[a_user] have [tasks_a, tasks_b] and got tasks[tasks_a, tasks_b]. + In this case no need to the send notification + """ + task_ids = [task.id for task in tasks] + + if userId not in self.user_task_list: + self.user_task_list[userId] = task_ids + return tasks + # Get the existing task IDs for the user + existing_task_ids = set(self.user_task_list[userId]) + + # Determine new tasks added for the user + if new_task_ids := set(task_ids).difference(existing_task_ids): + self.user_task_list[userId] = task_ids + return [task for task in tasks if task.id in new_task_ids] + + # Determine old/completed tasks to be removed + if old_task_ids := existing_task_ids.difference(task_ids): + self.user_task_list[userId] = [ + id for id in existing_task_ids if id not in old_task_ids + ] + return [] + + # If no new tasks added or removed + return [] + + def get_pending_tasks(self, engine: Engine) -> List[Task]: # type: ignore + """Get Tasks for document review""" + try: + options = {"userId": engine.remote.user_id} + if tasks := engine.remote.tasks.get(options): + if tasks := self.remove_overdue_tasks(tasks): + tasks = self.update_user_task_data(tasks, options["userId"]) + + if tasks and Feature.tasks_management: + if len(tasks) > 1: + # Send generic notification for multiple tasks + engine.send_task_notification( + tasks[0].targetDocumentIds[0]["id"], + "", + "REVIEW_DOCUMENT", + ) + else: + # Fetch document data + self.fetch_document(tasks, engine) + else: + log.info("No Task for processing...") + else: + self.clean_user_task_data(options["userId"]) + log.info("No Task for processing...") + except Exception as exec: + log.error(f"Exception occurred while Fetching Tasks: {exec}") + + @staticmethod + def remove_overdue_tasks(tasks: List[Task]) -> List[Task]: + """Remove overdue tasks""" + current_time = datetime.now(tz=timezone.utc) + log.info("Remove overdue tasks") + return [ + task + for task in tasks + if ( + datetime.strptime(task.dueDate, "%Y-%m-%dT%H:%M:%S.%f%z") > current_time + ) + ] + + def clean_user_task_data(self, userId: str = "", /) -> None: + """Remove user data for below scenarios: + 1. If no tasks assigned to user remove the ID + 2. If account has been removed than remove the ID if it exist + """ + if userId and userId in self.user_task_list.keys(): + self.user_task_list.pop(userId) diff --git a/nxdrive/data/i18n/i18n-ar.json b/nxdrive/data/i18n/i18n-ar.json index 601f6e6274..3074b2091f 100644 --- a/nxdrive/data/i18n/i18n-ar.json +++ b/nxdrive/data/i18n/i18n-ar.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "الإضافات المثبتة ✓", "ADVANCED": "Advanced:", "ADVANCED_SETTINGS": "Advanced settings", + "AGO": "ago", "APPLY": "Apply", "AUTOLOCK": "قفل تلقائي", "AUTH_EXPIRED": "انتهت صلاحية المصادقة", @@ -28,6 +29,7 @@ "CHANNEL": "Channel", "CHANNEL_CHANGE_SETTINGS": "Channel update", "CHANNEL_CONFIRM_DANGEROUS": "Confirm development channel?", + "CHOOSE_PARTICIPANTS": "Choose Participants %1", "CLOSE": "Close", "COMPLETED": "Completed", "COMPLETED_ON": "Completed on %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Connection refused", "CONNECTION_SUCCESS": "Server successfully connected", "CONNECTION_UNKNOWN": "Unknown error occurred while trying to connect", + "CONSOLIDATE_REVIEW": "Consolidate Review %1", "CONTAINER_TYPE": "Folder Type", "CONTEXT_MENU_1": "Access online", "CONTEXT_MENU_2": "Copy share-link", @@ -57,6 +60,7 @@ "CREATE": "Create", "CREATE_REPORT": "Generate bug report", "DATETIME_FORMAT": "%x %X", + "DAYS": "Day(s)", "DEBUG": "Debug", "DEBUG_INVALID_CREDENTIALS": "Toggle Invalid Credentials", "DEBUG_SYSTRAY_MESSAGE": "Display a systray message", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Recreate", "DONT_ASK_AGAIN": "Don't ask me again", "DOWNGRADE_TO": "Downgrade to %1", + "DUE": "Due", "DUPLICATE_BEHAVIOR": "Files duplicates management", "DUPLICATE_BEHAVIOR_CREATE": "Create", "DUPLICATE_BEHAVIOR_IGNORE": "Ignore", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Edit any of document’s content from their Summary tab even if they are not synchronized.", "FEATURE_S3": "Improve the speed of your uploads by leveraging the AWS infrastructure. Even enabled, this feature will be effective only for Nuxeo Cloud customers and for Nuxeo servers with S3 Direct Upload addon installed and up-to-date.", "FEATURE_SYNCHRONIZATION": "Enable or disable the bidirectional synchronization between the local environment and the Nuxeo Platform.\nA restart is needed to apply changes.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Something is missing? Share your feedback here.", "FILE_ALREADY_EXISTS": "There is already a file named \"%1\" in this folder. Do you want to replace it with the one you're moving?", "FILE_ALREADY_EXISTS_HEADER": "A duplicate file has been found.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Folder already used by another account", "FREE_DISK_SPACE": "(Free disk space: %1)", "GENERATING": "Generating…", + "GIVE_OPINION": "Give your Opinion %1", + "HANDLE_TASKS": "Handle tasks", "HELP": "Help", "HISTORY": "History", "HOST": "Host", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "This document's digest is empty. Its integrity cannot be verified.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "This document's digest doesn't fit any algorithm. Its integrity cannot be verified.", "IGNORE_REASON_UNKNOWN": "This file is ignored for unknown reason", + "IN": "in", "INSTALL_ADDONS": "Install Addons (like icon overlays) [requires administrative rights]", "INVALID_CREDENTIALS": "Invalid credentials", "INVALID_LOCAL_FOLDER": "This partition does not support extended attributes.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "No compatible version available", "MONITORING": "Monitoring", "MONITORING_DESCRIPTION": "Display limited to file size > %1", + "MONTHS": "Month(s)", "NAME": "Name", "NETWORK_ERROR_AuthenticationRequiredError": "Authentication required", "NETWORK_ERROR_ClientNetwork": "Cannot connect to the server", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "You have resolved all conflicts and errors", "NO_CONFLICTS_TITLE": "No conflicts or errors left", "NO_ROOTS": "There is no synchronized folder associated with your account.

Please browse to your instance and pick a folder to start synchronizing content.

Learn more about synchronization on the documentation website.", + "NO_TASKS_AVAILABLE": "You do not have any tasks to process at the moment", "NO_SPACE_LEFT_ON_DEVICE": "No space left on device, please free some disk space.", "OAUTH2_MISSING_URL": "Server URL not registered, please retry.", "OAUTH2_STATE_MISMATCH": "Broken authentication flow, please retry.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Open window", "OTHER": "Other", "PASSWORD": "Password", + "PENDING_TASKS": "Review required", "PERSONAL_SPACE": "Personal space", + "PENDING_TASK_REVIEWS": "New task to review", "PROXY": "Proxy", "PROXY_APPLIED": "Proxy settings have been updated", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Release notes", "RELEASE_NOTES_MSG": "Your %1 has been correctly updated to the version %2, please read the release notes for more information.", "RECENTLY_UPDATED": "Recently updated", + "REFRESH": "Refresh", + "REFRESH_AVAILABLE": "New tasks available. Please refresh the page.", "REMOTE_FOLDER": "Remote folder", "REPLACE": "Replace", "REPORT_GENERATED": "Report available:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Restart needed", "RESTART_NEEDED_MSG": "You need to restart %1 to continue using it.", "RESUME": "Resume", + "REVIEW_DOCUMENT": "Awaiting document review %1", "ROOT_USED_WITH_OTHER_BINDING": "This folder has been used by %1, its content will be deleted. Do you want to continue? If no, click on Cancel and choose another folder.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "A folder already in use has been detected.", "RUNNING": "Running", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Selective Sync", "SELECT_SYNC_FOLDERS": "Choose folders to sync", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Allows you to choose which folders to sync locally. This requires the engine to be online.", + "SELF_CREATED_TASKS": "Created by me", "SERVER_INCOMPATIBLE": "We advise you to downgrade your %1 client to the %2 version or to contact your administrator to upgrade your Nuxeo instance.", "SERVER_INCOMPATIBLE_HEADER": "Your server version is not compatible with %1 %2.", "SERVER_UI": "Server UI", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Synchronization in progress", "SYNCHRONIZATION_ITEMS_LEFT": "Remaining items: %1", "SYSTEM": "System", + "TASK_MAMNAGER_WINDOW_TITLE": "Task Manager", "TECHNICAL_DETAILS": "Technical details:", "TYPE": "Type", "UNAUTHORIZED": "Incorrect credentials", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Unselect all", "UNSYNC": "Unsynchronize", "UPDATE": "Update", + "UPDATE_REQUESTED": "Update Document %1", "UPDATED": "Updated document", "UPDATES_LINK": "See updates", "UPDATING_VERSION": "Updating to %1", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Use the light icons set", "USE_SENTRY": "Enable error reporting", "USERNAME": "Username", + "VALIDATE_DOCUMENT": "Validate the Document %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Web Authentication", "WELCOME_MESSAGE": "Set your account and synchronize your files with the Nuxeo Platform.", "WINDOWS_ERROR_TITLE": "Document error", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "The folder \"%1\" or its children is locked by another software. Please close it before %2 can process the deletion from your local storage.", "WRONG_CHANNEL": "You are running version %1 which is from the \"%2\" channel, but your current channel is \"%3\". Do you want to downgrade to the latest version of your current channel, or switch channels?", "WRONG_CHANNEL_HEADER": "Wrong channel detected.", + "YEARS": "Year(s)", "YES": "Yes" } diff --git a/nxdrive/data/i18n/i18n-de.json b/nxdrive/data/i18n/i18n-de.json index 113e51b347..63cc2acf35 100644 --- a/nxdrive/data/i18n/i18n-de.json +++ b/nxdrive/data/i18n/i18n-de.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Installierte Addons ✓", "ADVANCED": "Erweitert:", "ADVANCED_SETTINGS": "Erweiterte Einstellungen", + "AGO": "vor", "APPLY": "Übernehmen", "AUTOLOCK": "Automatische Sperrung", "AUTH_EXPIRED": "Authentifizierung abgelaufen", @@ -28,6 +29,7 @@ "CHANNEL": "Kanal", "CHANNEL_CHANGE_SETTINGS": "Kanal-Aktualisierung", "CHANNEL_CONFIRM_DANGEROUS": "Entwicklungskanal bestätigen?", + "CHOOSE_PARTICIPANTS": "Teilnehmer auswählen %1", "CLOSE": "Schließen", "COMPLETED": "Abgeschlossen", "COMPLETED_ON": "Abgeschlossen am %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Verbindung abgelehnt", "CONNECTION_SUCCESS": "Server erfolgreich verbunden", "CONNECTION_UNKNOWN": "Unbekannter Fehler beim Verbindungsversuch", + "CONSOLIDATE_REVIEW": "Konsolidierungsüberprüfung %1", "CONTAINER_TYPE": "Ordnertyp", "CONTEXT_MENU_1": "Online-Zugriff", "CONTEXT_MENU_2": "Freigabelink kopieren", @@ -57,6 +60,7 @@ "CREATE": "Erstellen", "CREATE_REPORT": "Erstelle einen Fehlerbericht", "DATETIME_FORMAT": "%x %X", + "DAYS": "Tag(e)", "DEBUG": "Debug", "DEBUG_INVALID_CREDENTIALS": "Zwischen ungültigen Anmeldeinformationen hin- und herwechseln", "DEBUG_SYSTRAY_MESSAGE": "Eine Systray-Mitteilung anzeigen", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Neu erstellen", "DONT_ASK_AGAIN": "Nicht mehr nachfragen", "DOWNGRADE_TO": "Downgrade auf %1", + "DUE": "Fällig", "DUPLICATE_BEHAVIOR": "Datei-Duplikate Verwaltung", "DUPLICATE_BEHAVIOR_CREATE": "Erstellen", "DUPLICATE_BEHAVIOR_IGNORE": "Ignorieren", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Bearbeite den Inhalt des Dokuments aus dem Übersichts-Tab selbst wenn er nicht synchronisiert ist.", "FEATURE_S3": "Verbessern Sie die Geschwindigkeit Ihrer Uploads durch die Nutzung der AWS-Infrastruktur. Sogar aktiviert, wird diese Funktion nur für Nuxeo Cloud-Kunden und für Nuxeo Server mit S3 Direct Upload Addon wirksam sein.", "FEATURE_SYNCHRONIZATION": "Aktivieren oder deaktivieren Sie die bidirektionale Synchronisation zwischen der lokalen Umgebung und der Nuxeo Plattform.\nEin Neustart ist erforderlich, um die Änderungen zu übernehmen.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Fehlt etwas? Teilen Sie Ihr Feedback hier.", "FILE_ALREADY_EXISTS": "In diesem Ordner befindet sich bereits eine Datei mit dem Namen \"%1\". Möchten Sie sie durch die Datei ersetzen, die Sie verschieben?", "FILE_ALREADY_EXISTS_HEADER": "Ein Dateiduplikat wurde gefunden.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Ordner wird schon von einem anderen Nuxeo Drive-Konto verwendet", "FREE_DISK_SPACE": "(Freier Festplattenspeicher: %1)", "GENERATING": "Wird erstellt …", + "GIVE_OPINION": "Eigene Meinung äußern %1", + "HANDLE_TASKS": "Aufgaben bearbeiten", "HELP": "Hilfe", "HISTORY": "Verlauf", "HOST": "Host", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "Der Digest dieses Dokuments ist leer. Seine Integrität kann nicht überprüft werden.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "Der Digest dieses Dokuments passt zu keinem Algorithmus. Seine Integrität kann nicht bestätigt werden.", "IGNORE_REASON_UNKNOWN": "Diese Datei wird aus unbekannten Gründen ignoriert", + "IN": "in", "INSTALL_ADDONS": "Addons installieren (wie Icon Overlays) [benötigt Administratorrechte]", "INVALID_CREDENTIALS": "Falsche Anmeldeinformationen", "INVALID_LOCAL_FOLDER": "Diese Partition unterstützt keine erweiterten Attribute.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "Keine kompatible Version verfügbar", "MONITORING": "Überwachung", "MONITORING_DESCRIPTION": "Begrenzt auf Dateigröße > %1", + "MONTHS": "Monat(e)", "NAME": "Name", "NETWORK_ERROR_AuthenticationRequiredError": "Authentifizierung erforderlich", "NETWORK_ERROR_ClientNetwork": "Verbindung mit dem Server kann nicht aufgebaut werden", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "Sie haben alle Konflikte und Fehler aufgelöst", "NO_CONFLICTS_TITLE": "Keine Konflikte oder Fehler mehr", "NO_ROOTS": "Ihrem Konto ist kein synchronisierter Ordner zugeordnet.

Gehen Sie zu Ihrer Instanz und wählen Sie einen Ordner, um die Synchronisation der Inhalte zu starten.

Weitere Informationen über die Synchronisation finden Sie auf der Dokumentations-Website.", + "NO_TASKS_AVAILABLE": "Sie haben momentan keine Aufgaben", "NO_SPACE_LEFT_ON_DEVICE": "Kein Speicherplatz mehr auf dem Gerät, geben Sie Speicherplatz frei.", "OAUTH2_MISSING_URL": "Server-URL nicht registriert, bitte erneut versuchen.", "OAUTH2_STATE_MISMATCH": "Fehlerhafte Authentifizierung, bitte erneut versuchen.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Öffnen Fenster", "OTHER": "Anders", "PASSWORD": "Kennwort", + "PENDING_TASKS": "Überprüfung erforderlich", "PERSONAL_SPACE": "Persönlicher Bereich", + "PENDING_TASK_REVIEWS": "Neue Aufgabe zum Überprüfen", "PROXY": "Proxy", "PROXY_APPLIED": "Proxyeinstellungen wurden aktualisiert", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Versionshinweise", "RELEASE_NOTES_MSG": "Ihr(e) %1 wurde korrekt auf die Version %2 aktualisiert. Lesen Sie bitte die Release Notes für weitere Informationen.", "RECENTLY_UPDATED": "Kürzlich aktualisiert", + "REFRESH": "Aktualisieren", + "REFRESH_AVAILABLE": "Neue Aufgaben verfügbar. Bitte aktualisieren Sie die Seite.", "REMOTE_FOLDER": "Ordner auf Server", "REPLACE": "Ersetzen", "REPORT_GENERATED": "Bericht verfügbar:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Neustart erforderlich", "RESTART_NEEDED_MSG": "Sie müssen %1 neu starten, um es weiter zu verwenden.", "RESUME": "Fortsetzen", + "REVIEW_DOCUMENT": "Warte auf Prüfung des Dokuments %1", "ROOT_USED_WITH_OTHER_BINDING": "Dieser Ordner wurde von %1 verwendet, die Inhalte werden gelöscht. Möchten Sie fortfahren? Falls nicht, klicken Sie auf „Abbrechen“ und wählen Sie einen anderen Ordner aus.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "Ein bereits verwendeter Ordner wurde erkannt.", "RUNNING": "Läuft", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Selektive Synchronisation", "SELECT_SYNC_FOLDERS": "Synchronisationsordner auswählen", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Ermöglicht Ihnen auszuwählen, welche Ordner lokal synchronisiert werden sollen. Dafür muss die Engine online sein.", + "SELF_CREATED_TASKS": "Von mir erstellt", "SERVER_INCOMPATIBLE": "Nehmen Sie ein Downgrade Ihres %1 Clients auf %2 Version oder bitten Sie Ihren Administrator, ein Upgrade Ihrer Nuxeo-Instanz durchzuführen.", "SERVER_INCOMPATIBLE_HEADER": "Ihre Serverversion ist nicht mit %1 %2 kompatibel.", "SERVER_UI": "Benutzeroberfläche des Servers", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Synchronisation wird ausgeführt", "SYNCHRONIZATION_ITEMS_LEFT": "Verbleibende Posten: %1", "SYSTEM": "System", + "TASK_MAMNAGER_WINDOW_TITLE": "Taskmanager", "TECHNICAL_DETAILS": "Technische Details:", "TYPE": "Typ", "UNAUTHORIZED": "Falsche Anmeldeinformationen", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Alle abwählen", "UNSYNC": "Synchronisierung rückgängig machen", "UPDATE": "Update", + "UPDATE_REQUESTED": "Dokument aktualisieren %1", "UPDATED": "Aktualisiertes Dokument", "UPDATES_LINK": "Aktualisierungen anzeigen", "UPDATING_VERSION": "Update auf %1 wird ausgeführt", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Lichtsymbole verwenden", "USE_SENTRY": "Aktivieren der Fehlerberichterstattung", "USERNAME": "Benutzername", + "VALIDATE_DOCUMENT": "Dokument validieren %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Nuxeo Drive – Web-Authentifizierung", "WELCOME_MESSAGE": "Richten Sie Ihre Konten ein und synchronisieren Sie Ihre Dateien mit der Nuxeo Platform.", "WINDOWS_ERROR_TITLE": "Dokumentenfehler", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "Der Ordner \"%1\" oder ihm untergeordnete Elemente wurden von einer anderen Software gesperrt. Schließen Sie diese, damit %2 sie aus dem lokalen Speicher löschen kann.", "WRONG_CHANNEL": "Du verwendest Version %1 , die vom Kanal \"%2\" stammt, aber dein aktueller Kanal ist \"%3\". Möchtest du auf die neueste Version deines aktuellen Channels herunterstufen?", "WRONG_CHANNEL_HEADER": "Falscher Kanal erkannt.", + "YEARS": "Jahr(e)", "YES": "Ja" } diff --git a/nxdrive/data/i18n/i18n-es.json b/nxdrive/data/i18n/i18n-es.json index 7958994a52..a16e5faeda 100644 --- a/nxdrive/data/i18n/i18n-es.json +++ b/nxdrive/data/i18n/i18n-es.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Addons instalados ✓", "ADVANCED": "Avanzada:", "ADVANCED_SETTINGS": "Configuraciones avanzadas", + "AGO": "hace", "APPLY": "Aplicar", "AUTOLOCK": "Bloqueo automático", "AUTH_EXPIRED": "Autenticación caducada", @@ -28,6 +29,7 @@ "CHANNEL": "Canal", "CHANNEL_CHANGE_SETTINGS": "Canal de actualización", "CHANNEL_CONFIRM_DANGEROUS": "¿Confirmar canal de desarrollo?", + "CHOOSE_PARTICIPANTS": "Elegir participantes %1", "CLOSE": "Cerrar", "COMPLETED": "Completado", "COMPLETED_ON": "Completado el %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Conexión rechazada", "CONNECTION_SUCCESS": "El servidor se ha conectado correctamente", "CONNECTION_UNKNOWN": "Ocurrió un error desconocido al intentar conectar", + "CONSOLIDATE_REVIEW": "Consolidar la revisión %1", "CONTAINER_TYPE": "Tipo de carpeta", "CONTEXT_MENU_1": "Acceder en línea", "CONTEXT_MENU_2": "Copiar el enlace compartido", @@ -57,6 +60,7 @@ "CREATE": "Crear", "CREATE_REPORT": "Generar reporte de errores", "DATETIME_FORMAT": "%x %X", + "DAYS": "Día(s)", "DEBUG": "Depurar", "DEBUG_INVALID_CREDENTIALS": "Alternar las credenciales no válidas", "DEBUG_SYSTRAY_MESSAGE": "Mostrar un mensaje de Systray", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Volver a crear", "DONT_ASK_AGAIN": "No volver a preguntar", "DOWNGRADE_TO": "Degradar a %1", + "DUE": "Vencimiento", "DUPLICATE_BEHAVIOR": "Gestión de archivos duplicados", "DUPLICATE_BEHAVIOR_CREATE": "Crear", "DUPLICATE_BEHAVIOR_IGNORE": "Ignorar", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Editar cualquier contenido del documento desde su pestaña Resumen, incluso si no están sincronizados.", "FEATURE_S3": "Mejore la velocidad de sus cargas aprovechando la infraestructura AWS. Incluso habilitada, esta característica será efectiva sólo para clientes Nuxeo Cloud y para servidores Nuxeo con el complemento de Subida Directa S3 instalado y actualizado.", "FEATURE_SYNCHRONIZATION": "Habilitar o deshabilitar la sincronización bidireccional entre el entorno local y Nuxeo.\nSe necesita reiniciar para aplicar los cambios.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "¿Falta algo? Exprese sus comentarios aquí.", "FILE_ALREADY_EXISTS": "Ya hay un archivo llamado \"%1\" en esta carpeta. ¿Quieres reemplazarlo con el que estás moviendo?", "FILE_ALREADY_EXISTS_HEADER": "Se ha encontrado un archivo duplicado.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Otra cuenta de Nuxeo Drive ya ha utilizado la carpeta", "FREE_DISK_SPACE": "(Espacio libre en disco: %1)", "GENERATING": "Generando...", + "GIVE_OPINION": "De su opinión %1", + "HANDLE_TASKS": "Gestionar tareas", "HELP": "Ayuda", "HISTORY": "Historial", "HOST": "Host", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "El resumen de este documento está vacío. Su integridad no puede ser verificada.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "El resumen de este documento no encaja con ningún algoritmo. No se puede verificar su integridad.", "IGNORE_REASON_UNKNOWN": "Este archivo se ha ignorado por un motivo desconocido", + "IN": "en", "INSTALL_ADDONS": "Instalar complementos (como superposiciones de iconos) [requiere derechos administrativos]", "INVALID_CREDENTIALS": "Credenciales no válidas", "INVALID_LOCAL_FOLDER": "Esta partición no soporta atributos extendidos.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "No hay ninguna versión compatible disponible", "MONITORING": "Monitoreo", "MONITORING_DESCRIPTION": "Limitado al tamaño del archivo > %1", + "MONTHS": "Mes(es)", "NAME": "Nombre", "NETWORK_ERROR_AuthenticationRequiredError": "Se requiere autenticación", "NETWORK_ERROR_ClientNetwork": "No se puede conectar al servidor", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "Ha solucionado todos los conflictos y todos los errores", "NO_CONFLICTS_TITLE": "No queda ningún conflicto o error", "NO_ROOTS": "No hay ninguna carpeta sincronizada asociada a su cuenta.

Vaya a su instancia y elija una carpeta para comenzar a sincronizar el contenido.

Más información acerca de la sincronización en el sitio web de documentación.", + "NO_TASKS_AVAILABLE": "No tienes ninguna tarea pendiente.", "NO_SPACE_LEFT_ON_DEVICE": "No queda espacio libre en el dispositivo; libere espacio en el disco.", "OAUTH2_MISSING_URL": "URL del servidor no registrada, vuelva a intentarlo.", "OAUTH2_STATE_MISMATCH": "Flujo de autenticación roto, por favor vuelva a intentarlo.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Abrir ventana", "OTHER": "Otro", "PASSWORD": "Contraseña", + "PENDING_TASKS": "Revision requerida", "PERSONAL_SPACE": "Espacio personal", + "PENDING_TASK_REVIEWS": "Nueva tarea a revisar", "PROXY": "Proxy", "PROXY_APPLIED": "La configuración del proxy se ha actualizado", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Notas de la versión", "RELEASE_NOTES_MSG": "Su %1 ha sido actualizado correctamente a la versión %2, por favor lea las notas de lanzamiento para más información.", "RECENTLY_UPDATED": "Actualizado recientemente", + "REFRESH": "Refrescar", + "REFRESH_AVAILABLE": "Nuevas tareas disponibles. Por favor, actualiza la página.", "REMOTE_FOLDER": "Carpeta remota", "REPLACE": "Reemplazar", "REPORT_GENERATED": "Informe disponible en:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Se necesita reiniciar", "RESTART_NEEDED_MSG": "Necesitas reiniciar %1 para continuar usándolo.", "RESUME": "Reanudar", + "REVIEW_DOCUMENT": "Esperando revisión de documento %1", "ROOT_USED_WITH_OTHER_BINDING": "Esta carpeta la ha utilizado %1, su contenido se eliminará. ¿Desea continuar? Si no es así, haga clic en Cancelar y elija otra carpeta.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "Una carpeta ya en uso ha sido detectada.", "RUNNING": "En curso", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Sincronización selectiva", "SELECT_SYNC_FOLDERS": "Seleccionar carpetas de sincronización", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Le permite elegir qué carpetas deben sincronizarse localmente. Esto requiere que el motor esté en línea.", + "SELF_CREATED_TASKS": "Creada por mí", "SERVER_INCOMPATIBLE": "Le aconsejamos que baje su cliente %1 a la versión %2 o que contacte a su administrador para actualizar su instancia de Nuxeo.", "SERVER_INCOMPATIBLE_HEADER": "La versión de su servidor no es compatible con %1 %2.", "SERVER_UI": "IU de servidor", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Sincronización en curso", "SYNCHRONIZATION_ITEMS_LEFT": "Artículos restantes: %1", "SYSTEM": "Sistema", + "TASK_MAMNAGER_WINDOW_TITLE": "Administrador de tareas", "TECHNICAL_DETAILS": "Detalles técnicos:", "TYPE": "Tipo", "UNAUTHORIZED": "Credenciales incorrectas", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Deseleccionar todos", "UNSYNC": "No sincronizar", "UPDATE": "Actualizar", + "UPDATE_REQUESTED": "Actualizar Documento %1", "UPDATED": "Documento actualizado", "UPDATES_LINK": "Ver actualizaciones", "UPDATING_VERSION": "Actualizando a %1", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Usar el conjunto de iconos claros", "USE_SENTRY": "Activar informe de errores", "USERNAME": "Nombre de usuario", + "VALIDATE_DOCUMENT": "Validar el documento %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Nuxeo Drive - Autenticación web", "WELCOME_MESSAGE": "Establezca su cuenta y sincronice sus archivos con Nuxeo Platform.", "WINDOWS_ERROR_TITLE": "Error de documento", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "La carpeta \"%1\" o sus hijos están bloqueados por otro software. Ciérrelo antes de que %2 pueda procesar la eliminación desde su almacenamiento local.", "WRONG_CHANNEL": "Estás ejecutando la versión %1 que es desde el canal de \"%2\". Pero tu canal actual es \"%3\". ¿Quieres devolver a la última versión de tu canal actual, o cambiar de canal?", "WRONG_CHANNEL_HEADER": "Canal incorrecto detectado.", + "YEARS": "Año(s)", "YES": "Sí" } diff --git a/nxdrive/data/i18n/i18n-eu.json b/nxdrive/data/i18n/i18n-eu.json index ab756c3ef0..4ef0fada85 100644 --- a/nxdrive/data/i18n/i18n-eu.json +++ b/nxdrive/data/i18n/i18n-eu.json @@ -1,5 +1,5 @@ { - "ABOUT": "About", + "ABOUT": "Honi buruz", "ACCOUNT_NAME": "Kontu-izena", "ADD": "Gehitu", "ADD_FILES": "Gehitu fitxategiak", @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Gehigarriak instalatuta ✓", "ADVANCED": "Aurreratua:", "ADVANCED_SETTINGS": "Ezarpen aurreratuak", + "AGO": "ago", "APPLY": "Aplikatu", "AUTOLOCK": "Autoblokeoa", "AUTH_EXPIRED": "Autentifikazioa iraungi da", @@ -28,6 +29,7 @@ "CHANNEL": "Kanala", "CHANNEL_CHANGE_SETTINGS": "Eguneratzeen kanala", "CHANNEL_CONFIRM_DANGEROUS": "Ziur garapen-kanala erabili nahi duzula?", + "CHOOSE_PARTICIPANTS": "Choose Participants %1", "CLOSE": "Itxi", "COMPLETED": "Burututa", "COMPLETED_ON": "Burututa - %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Konexioa baztertuta", "CONNECTION_SUCCESS": "Zerbitzaria behar bezala konektatu da", "CONNECTION_UNKNOWN": "Errore ezezaguna konektatzen saiatzean", + "CONSOLIDATE_REVIEW": "Consolidate Review %1", "CONTAINER_TYPE": "Karpeta mota", "CONTEXT_MENU_1": "Atzitu online", "CONTEXT_MENU_2": "Kopiatu partekatzeko esteka", @@ -57,6 +60,7 @@ "CREATE": "Sortu", "CREATE_REPORT": "Sortu errore-txostena", "DATETIME_FORMAT": "%x %X", + "DAYS": "Day(s)", "DEBUG": "Arazketa", "DEBUG_INVALID_CREDENTIALS": "Kredentzial baliogabeak erregistratu / ez", "DEBUG_SYSTRAY_MESSAGE": "Erakutsi mezua sistemaren erretiluan", @@ -106,7 +110,7 @@ "DIRECT_TRANSFER_DETAILS": "[%1%] %2 / %3", "DIRECT_TRANSFER_END": "Transferentzia burututa: \"%1\"", "DIRECT_TRANSFER_ERROR": "Errorea transferitzean: \"%1\"", - "DIRECT_TRANSFER_FINALIZING_ERROR": "An error occurred during the transfer, it will resume shortly.", + "DIRECT_TRANSFER_FINALIZING_ERROR": "Errore bat gertatu da transferentzian, laster berrekingo da.", "DIRECT_TRANSFER_NO_ACCOUNT": "Ezin da transferentzia zuzena erabili konturik gabe, bertan behera uzten.", "DIRECT_TRANSFER_NOT_ALLOWED": "\"%1\" zuzenean transferitzea ez dago baimenduta fitxategi sinkronizatuentzat.", "DIRECT_TRANSFER_NOT_ENABLED": "Transferentzia zuzena ez dago gaituta.", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Birsortu", "DONT_ASK_AGAIN": "Ez galdetu berriz", "DOWNGRADE_TO": "Leheneratu %1 bertsiora", + "DUE": "Due", "DUPLICATE_BEHAVIOR": "Fitxategi bikoizketen kudeaketa", "DUPLICATE_BEHAVIOR_CREATE": "Sortu", "DUPLICATE_BEHAVIOR_IGNORE": "Ezikusi", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Editatu dokumentuaren edozein eduki Laburpena fitxatik, nahiz eta hau sinkronizatuta ez egon.", "FEATURE_S3": "Hobetu igoeren abiadura AWS azpiegiturari etekina ateraz. Gaituta ere, eginbide hau Nuxeo Cloud-eko bezeroentzat soilik egongo da martxan eta S3 Direct Upload gehigarria instalatuta eta eguneratuta daukaten Nuxeo zerbitzarietan.", "FEATURE_SYNCHRONIZATION": "Gaitu edo desgaitu bi noranzkoko sinkronizazioa ingurune lokalaren eta Nuxeo plataformaren artean. Aldaketak aplikatzeko berrabiarazi beharra dago.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Zerbait ez dabil ondo? Eman arazoaren berri hemen.", "FILE_ALREADY_EXISTS": "Dagoeneko badago \"%1\" izendun fitxategi bat karpeta honetan. Lekuz aldatzen ari zarenarekin ordeztu nahi duzu?", "FILE_ALREADY_EXISTS_HEADER": "Bikoiztutako fitxategi bat aurkitu da.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Karpeta dagoeneko beste kontu batek darabil", "FREE_DISK_SPACE": "(Diskoko leku librea: %1)", "GENERATING": "Sortzen…", + "GIVE_OPINION": "Give your Opinion %1", + "HANDLE_TASKS": "Handle tasks", "HELP": "Laguntza", "HISTORY": "Historia", "HOST": "Ostalaria", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "Dokumentuaren laburpena hutsik dago. Ezin da bere osotasuna egiaztatu.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "Dokumentuaren laburpena ez dator bat algoritmo batekin ere. Ezin da bere osotasuna egiaztatu.", "IGNORE_REASON_UNKNOWN": "Fitxategia ezikusi da arrazoi ezezagunengatik", + "IN": "in", "INSTALL_ADDONS": "Instalatu gehigarriak (esaterako ikono gainjarriak) [administrazio-baimenak behar dira]", "INVALID_CREDENTIALS": "Kredentzial baliogabeak", "INVALID_LOCAL_FOLDER": "Partizio honek ez ditu atributu hedatuak onartzen.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "Ez dago bertsio bateragarririk eskuragarri", "MONITORING": "Monitorizatzen", "MONITORING_DESCRIPTION": "Erakutsi fitxategiaren tamaina > %1 denerako soilik", + "MONTHS": "Month(s)", "NAME": "Izena", "NETWORK_ERROR_AuthenticationRequiredError": "Autentifikazioa beharrezkoa", "NETWORK_ERROR_ClientNetwork": "Ezin izan da zerbitzarira konektatu", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "Gatazka eta errore guztiak konpondu dituzu", "NO_CONFLICTS_TITLE": "Ez dago gatazka gehiagorik", "NO_ROOTS": "Ez dago zure kontura loturiko karpeta sinkronizaturik.

Arakatu zure instantzia eta hautatu karpeta bat edukia sinkronizatzen hasteko.

Ikasi gehiago sinkronizatzeari buruz dokumentazio webgunean.", + "NO_TASKS_AVAILABLE": "You do not have any tasks to process at the moment", "NO_SPACE_LEFT_ON_DEVICE": "Ez dago lekurik diskoan, hustu leku pixka bat.", "OAUTH2_MISSING_URL": "Ez da zerbitzariaren URLa gorde, saiatu berriro.", "OAUTH2_STATE_MISMATCH": "Autentikatze-fluxua eten da, saiatu berriro.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Ireki leihoa", "OTHER": "Bestelakoa", "PASSWORD": "Pasahitza", + "PENDING_TASKS": "Review required", "PERSONAL_SPACE": "Area pertsonala", + "PENDING_TASK_REVIEWS": "New task to review", "PROXY": "Proxya", "PROXY_APPLIED": "Proxy-ezarpenak eguneratu dira", "PROXY_CHANGE_SETTINGS": "Proxya", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Bertsio-oharrak", "RELEASE_NOTES_MSG": "%1 behar bezala eguneratu da %2 bertsiora, irakurri bertsio-oharrak informazio gehiagorako.", "RECENTLY_UPDATED": "Orain gutxi eguneratua", + "REFRESH": "Refresh", + "REFRESH_AVAILABLE": "New tasks available. Please refresh the page.", "REMOTE_FOLDER": "Urruneko karpeta", "REPLACE": "Ordeztu", "REPORT_GENERATED": "Txostena eskuragarri:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Berrabiaraztea beharrezkoa", "RESTART_NEEDED_MSG": "%1 berrabiarazi behar duzu erabiltzen jarraitu ahal izateko.", "RESUME": "Berrekin", + "REVIEW_DOCUMENT": "Awaiting document review %1", "ROOT_USED_WITH_OTHER_BINDING": "Karpeta hau %1(e)k erabili du, bertako edukia ezabatuko da. Jarraitu nahi duzu? Hala ez bada, egin klik Utzi botoian eta hautatu beste karpeta bat.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "Dagoeneko erabiltzen ari den karpeta bat antzeman da.", "RUNNING": "Exekutatzen", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Sinkronizatze selektiboa", "SELECT_SYNC_FOLDERS": "Aukeratu sinkronizatu beharreko karpetak", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Lokalki zein karpeta sinkronizatu aukeratzen uzten dizu. Motorra online egon behar da horretarako.", + "SELF_CREATED_TASKS": "Created by me", "SERVER_INCOMPATIBLE": "Gomendatzen dizugu %1 bezeroa %2 bertsiora jaistea, edo administratzailearekin kontaktuan jarri eta Nuxeo instantzia eguneratzea.", "SERVER_INCOMPATIBLE_HEADER": "Zerbitzariaren bertsioa ez da bateragarria %1 %2(r)ekin.", "SERVER_UI": "Zerbitzariaren interfazea", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Sinkronizatzen", "SYNCHRONIZATION_ITEMS_LEFT": "Falta diren elementuak: %1", "SYSTEM": "Sistemakoa", + "TASK_MAMNAGER_WINDOW_TITLE": "Task Manager", "TECHNICAL_DETAILS": "Xehetasun teknikoak:", "TYPE": "Mota", "UNAUTHORIZED": "Okerreko kredentzialak", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Desautatu dena", "UNSYNC": "Desinkronizatu", "UPDATE": "Eguneratu", + "UPDATE_REQUESTED": "Update Document %1", "UPDATED": "Dokumentua eguneratu da", "UPDATES_LINK": "Ikusi eguneratzeak", "UPDATING_VERSION": "%1(e)ra eguneratzen", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Erabili ikono-sorta argia", "USE_SENTRY": "Gaitu errore-txostenen bidalketa", "USERNAME": "Erabiltzaile-izena", + "VALIDATE_DOCUMENT": "Validate the Document %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Web autentifikazioa", "WELCOME_MESSAGE": "Konfiguratu zure kontua eta sinkronizatu fitxategiak Nuxeo plataformarekin.", "WINDOWS_ERROR_TITLE": "Dokumentuaren errorea", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "\"%1\" karpeta edo bere umeak beste software batekin daude blokeatuta. Itxi ezazu %2(e)k biltegi lokaletik ezabatu ahal izan dezan.", "WRONG_CHANNEL": "Unean %1 bertsioa darabilzu, \"%2\" kanaletik, baina zure uneko kanala \"%3\" da. Uneko kanaleko azken bertsiora aldatzea nahi duzu, edo kanalez aldatzea?", "WRONG_CHANNEL_HEADER": "Okerreko kanala antzeman da.", + "YEARS": "Year(s)", "YES": "Bai" } diff --git a/nxdrive/data/i18n/i18n-fr.json b/nxdrive/data/i18n/i18n-fr.json index 3924882063..b2b50a714f 100644 --- a/nxdrive/data/i18n/i18n-fr.json +++ b/nxdrive/data/i18n/i18n-fr.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Addons installées ✓", "ADVANCED": "Avancé :", "ADVANCED_SETTINGS": "Paramètres avancés", + "AGO": "il y a", "APPLY": "Appliquer", "AUTOLOCK": "Verrouillage automatique", "AUTH_EXPIRED": "Authentification requise", @@ -28,6 +29,7 @@ "CHANNEL": "Canal", "CHANNEL_CHANGE_SETTINGS": "Canal de mise à jour", "CHANNEL_CONFIRM_DANGEROUS": "Confirmer le canal de développement ?", + "CHOOSE_PARTICIPANTS": "Choisir les participants", "CLOSE": "Fermer", "COMPLETED": "Terminés", "COMPLETED_ON": "Terminée le %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Connexion refusée", "CONNECTION_SUCCESS": "Connexion réussie", "CONNECTION_UNKNOWN": "Une erreur inconnue s'est produite lors de la connexion", + "CONSOLIDATE_REVIEW": "Consolider la consultation", "CONTAINER_TYPE": "Type de dossier", "CONTEXT_MENU_1": "Voir en ligne", "CONTEXT_MENU_2": "Copier le lien de partage", @@ -57,6 +60,7 @@ "CREATE": "Créer", "CREATE_REPORT": "Générer un rapport de bug", "DATETIME_FORMAT": "%x %X", + "DAYS": "Jour(s)", "DEBUG": "Débogage", "DEBUG_INVALID_CREDENTIALS": "Activer/désactiver les indentifiants invalides", "DEBUG_SYSTRAY_MESSAGE": "Affiche un message dans le menu de la zone de notification", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Recréer", "DONT_ASK_AGAIN": "Ne plus afficher à l'avenir", "DOWNGRADE_TO": "Mettre à jour vers %1", + "DUE": "Echéance", "DUPLICATE_BEHAVIOR": "Gestion des doublons de fichiers", "DUPLICATE_BEHAVIOR_CREATE": "Créer", "DUPLICATE_BEHAVIOR_IGNORE": "Ignorer", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Modifiez le contenu d'un document à partir de l'onglet Résumé même s'ils ne sont pas synchronisés.", "FEATURE_S3": "Améliorez la vitesse de vos envois en exploitant l'infrastructure AWS. Même activée, cette fonctionnalité ne sera effective que pour les clients de Nuxeo Cloud et pour les serveurs Nuxeo avec l'addon S3 Direct Upload installé et à jour.", "FEATURE_SYNCHRONIZATION": "Activer ou désactiver la synchronisation bidirectionnelle entre l'environnement local et la plateforme Nuxeo.\nUn redémarrage est nécessaire pour prendre en compte cette modification.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Il manque quelque chose ? Partager vos commentaires ici.", "FILE_ALREADY_EXISTS": "Il y a déjà un fichier nommé \"%1\" dans ce dossier. Voulez-vous le remplacer par celui que vous déplacez ?", "FILE_ALREADY_EXISTS_HEADER": "Un doublon a été trouvé.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Ce dossier est déjà utilisé par un autre compte", "FREE_DISK_SPACE": "(Espace disque disponible : %1)", "GENERATING": "Génération en cours…", + "GIVE_OPINION": "Donner son avis %1", + "HANDLE_TASKS": "Gérer les tâches", "HELP": "Aide", "HISTORY": "Historique", "HOST": "URL", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "La somme de contrôle de ce document est vide. Son intégrité ne peut pas être vérifiée.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "L'intégrité du document n'a pu être vérifiée car la somme de contrôle ne correspond à aucun algorithme connu.", "IGNORE_REASON_UNKNOWN": "Ce fichier est ignoré pour une raison inconnue.", + "IN": "dans", "INSTALL_ADDONS": "Installer les Addons (tel que les badges d'icônes) [nécessite les droits d'administration]", "INVALID_CREDENTIALS": "Identifiants invalides", "INVALID_LOCAL_FOLDER": "Cette partition ne supporte pas les attributs étendus.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "Aucune version compatible disponible", "MONITORING": "Monitoring", "MONITORING_DESCRIPTION": "Affichage limité aux fichiers > %1", + "MONTHS": "Mois", "NAME": "Nom", "NETWORK_ERROR_AuthenticationRequiredError": "Authentification requise", "NETWORK_ERROR_ClientNetwork": "Problème de connexion", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "Vous avez résolu tous les conflits et erreurs", "NO_CONFLICTS_TITLE": "Aucun conflit ou erreur", "NO_ROOTS": "Il n’y a aucun dossier synchronisé associé à votre compte.

Veuillez accédez à votre instance et sélectionner un dossier pour démarrer la synchronisation.

En savoir plus sur la synchronisation sur le site web de documentation.", + "NO_TASKS_AVAILABLE": "Vous n'avez aucune tâche à traiter pour le moment", "NO_SPACE_LEFT_ON_DEVICE": "Espace disque insuffisant, veuillez libérer de l'espace disque.", "OAUTH2_MISSING_URL": "URL du serveur non enregistrée, veuillez réessayer.", "OAUTH2_STATE_MISMATCH": "Flux d'authentification rompu, veuillez réessayer.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Ouvrir la fenêtre", "OTHER": "Autres ", "PASSWORD": "Mot de passe", + "PENDING_TASKS": "Vérification requise", "PERSONAL_SPACE": "Espace personnel", + "PENDING_TASK_REVIEWS": "Nouvelle tâche à revoir", "PROXY": "Proxy", "PROXY_APPLIED": "Le serveur proxy a été mis à jour", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Notes de publication", "RELEASE_NOTES_MSG": "Votre %1 a été correctement mis à jour vers la version %2, veuillez lire les notes de version pour plus d'informations.", "RECENTLY_UPDATED": "Derniers fichiers mis à jour", + "REFRESH": "Rafraîchir", + "REFRESH_AVAILABLE": "Nouvelles tâches disponibles. Veuillez actualiser la page.", "REMOTE_FOLDER": "Dossier distant", "REPLACE": "Remplacer", "REPORT_GENERATED": "Rapport disponible :", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Redémarrage nécessaire", "RESTART_NEEDED_MSG": "Vous devez redémarrer %1 pour continuer à l'utiliser.", "RESUME": "Reprendre", + "REVIEW_DOCUMENT": "En attente de révision du document %1", "ROOT_USED_WITH_OTHER_BINDING": "Ce dossier a été utilisé par %1, son contenu sera supprimé. Voulez-vous continuer ? Si non, cliquez sur annuler et choisissez un autre dossier.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "Un dossier déjà utilisé a été détecté.", "RUNNING": "En cours", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Synchronisation sélective", "SELECT_SYNC_FOLDERS": "Sélectionner les dossiers à synchroniser", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Sélection des dossiers à synchroniser localement. Vous devez être connecté.", + "SELF_CREATED_TASKS": "Créé par moi", "SERVER_INCOMPATIBLE": "Nous vous conseillons de rétrograder votre client %1 à la version %2 ou de contacter votre administrateur pour mettre à jour votre instance Nuxeo.", "SERVER_INCOMPATIBLE_HEADER": "La version de votre serveur n'est pas compatible avec %1 %2.", "SERVER_UI": "Interface serveur", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Synchronisation en cours", "SYNCHRONIZATION_ITEMS_LEFT": "Éléments restants : %1", "SYSTEM": "Système", + "TASK_MAMNAGER_WINDOW_TITLE": "Gestionnaire des tâches", "TECHNICAL_DETAILS": "Détails techniques:", "TYPE": "Type", "UNAUTHORIZED": "Authentification incorrecte", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Tout désélectionner", "UNSYNC": "Désynchroniser", "UPDATE": "Mettre à jour", + "UPDATE_REQUESTED": "Mettre à jour le document %1", "UPDATED": "Document mis à jour", "UPDATES_LINK": "Mises à jour", "UPDATING_VERSION": "Mise à jour %1 en cours", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Utiliser des icônes claires", "USE_SENTRY": "Activer les rapports d'erreur", "USERNAME": "Utilisateur", + "VALIDATE_DOCUMENT": "Valider le document", "WEB_AUTHENTICATION_WINDOW_TITLE": "Authentification web", "WELCOME_MESSAGE": "Connectez-vous et synchronisez vos fichiers avec Nuxeo.", "WINDOWS_ERROR_TITLE": "Erreur de document", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "Le dossier \"%1\" ou un de ses enfants est verrouillé par un autre logiciel. Veuillez le fermer pour que %2 puisse traiter la suppression depuis votre espace de stockage local.", "WRONG_CHANNEL": "Vous utilisez la version %1 qui vient du canal \"%2\", mais votre canal actuel est \"%3\". Voulez-vous mettre à jour vers la dernière version de votre canal actuel, ou changer de canal ?", "WRONG_CHANNEL_HEADER": "Mauvais canal détecté.", + "YEARS": "Année(s)", "YES": "Oui" } diff --git a/nxdrive/data/i18n/i18n-id.json b/nxdrive/data/i18n/i18n-id.json index eeadeef783..47da3ded12 100644 --- a/nxdrive/data/i18n/i18n-id.json +++ b/nxdrive/data/i18n/i18n-id.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Addons installed ✓", "ADVANCED": "Advanced:", "ADVANCED_SETTINGS": "Advanced settings", + "AGO": "ago", "APPLY": "Terapkan", "AUTOLOCK": "Autolock", "AUTH_EXPIRED": "Authentication expired", @@ -28,6 +29,7 @@ "CHANNEL": "Channel", "CHANNEL_CHANGE_SETTINGS": "Channel update", "CHANNEL_CONFIRM_DANGEROUS": "Confirm development channel?", + "CHOOSE_PARTICIPANTS": "Choose Participants %1", "CLOSE": "Close", "COMPLETED": "Completed", "COMPLETED_ON": "Completed on %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Sambungan ditolak", "CONNECTION_SUCCESS": "Server berhasil terhubung", "CONNECTION_UNKNOWN": "Terjadi kesalahan yang tidak diketahui saat mencoba menghubungkan", + "CONSOLIDATE_REVIEW": "Consolidate Review %1", "CONTAINER_TYPE": "Folder Type", "CONTEXT_MENU_1": "Akses secara online", "CONTEXT_MENU_2": "Salin bagikan-tautan", @@ -57,6 +60,7 @@ "CREATE": "Create", "CREATE_REPORT": "Generate bug report", "DATETIME_FORMAT": "%x %X", + "DAYS": "Day(s)", "DEBUG": "Debug", "DEBUG_INVALID_CREDENTIALS": "Beralih Kredensial tidak sah", "DEBUG_SYSTRAY_MESSAGE": "Tampilkan pesan systray", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Menciptakan kembali", "DONT_ASK_AGAIN": "Don't ask me again", "DOWNGRADE_TO": "Downgrade to %1", + "DUE": "Due", "DUPLICATE_BEHAVIOR": "Files duplicates management", "DUPLICATE_BEHAVIOR_CREATE": "Create", "DUPLICATE_BEHAVIOR_IGNORE": "Ignore", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Edit any of document’s content from their Summary tab even if they are not synchronized.", "FEATURE_S3": "Improve the speed of your uploads by leveraging the AWS infrastructure. Even enabled, this feature will be effective only for Nuxeo Cloud customers and for Nuxeo servers with S3 Direct Upload addon installed and up-to-date.", "FEATURE_SYNCHRONIZATION": "Enable or disable the bidirectional synchronization between the local environment and the Nuxeo Platform.\nA restart is needed to apply changes.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Something is missing? Share your feedback here.", "FILE_ALREADY_EXISTS": "There is already a file named \"%1\" in this folder. Do you want to replace it with the one you're moving?", "FILE_ALREADY_EXISTS_HEADER": "A duplicate file has been found.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Folder sudah digunakan oleh akun yang lain", "FREE_DISK_SPACE": "(Free disk space: %1)", "GENERATING": "Generating…", + "GIVE_OPINION": "Give your Opinion %1", + "HANDLE_TASKS": "Handle tasks", "HELP": "Bantuan", "HISTORY": "History", "HOST": "Tuan rumah", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "This document's digest is empty. Its integrity cannot be verified.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "This document's digest doesn't fit any algorithm. Its integrity cannot be verified.", "IGNORE_REASON_UNKNOWN": "Berkas ini diabaikan untuk alasan yang tidak diketahui", + "IN": "in", "INSTALL_ADDONS": "Install Addons (like icon overlays) [requires administrative rights]", "INVALID_CREDENTIALS": "Kredensial tidak sah", "INVALID_LOCAL_FOLDER": "This partition does not support extended attributes.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "Tidak ada versi yang kompatibel yang tersedia", "MONITORING": "Monitoring", "MONITORING_DESCRIPTION": "Display limited to file size > %1", + "MONTHS": "Month(s)", "NAME": "Nama", "NETWORK_ERROR_AuthenticationRequiredError": "Otentikasi diperlukan", "NETWORK_ERROR_ClientNetwork": "Tidak dapat terhubung ke server", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "Anda telah menyelesaikan semua konflik dan kesalahan", "NO_CONFLICTS_TITLE": "Tidak ada konflik atau kesalahan yang tersisa", "NO_ROOTS": "There is no synchronized folder associated with your account.

Please browse to your instance and pick a folder to start synchronizing content.

Learn more about synchronization on the documentation website.", + "NO_TASKS_AVAILABLE": "You do not have any tasks to process at the moment", "NO_SPACE_LEFT_ON_DEVICE": "Tidak ada ruang yang tersisa di perangkat, bebaskan beberapa ruang disk.", "OAUTH2_MISSING_URL": "Server URL not registered, please retry.", "OAUTH2_STATE_MISMATCH": "Broken authentication flow, please retry.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Open window", "OTHER": "Other", "PASSWORD": "Kata sandi", + "PENDING_TASKS": "Review required", "PERSONAL_SPACE": "Personal space", + "PENDING_TASK_REVIEWS": "New task to review", "PROXY": "Proxy", "PROXY_APPLIED": "Pengaturan proxy telah diperbarui", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Release notes", "RELEASE_NOTES_MSG": "Your %1 has been correctly updated to the version %2, please read the release notes for more information.", "RECENTLY_UPDATED": "Baru saja diperbarui", + "REFRESH": "Refresh", + "REFRESH_AVAILABLE": "New tasks available. Please refresh the page.", "REMOTE_FOLDER": "Remote folder", "REPLACE": "Replace", "REPORT_GENERATED": "Laporan tersedia di:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Restart needed", "RESTART_NEEDED_MSG": "You need to restart %1 to continue using it.", "RESUME": "Lanjut", + "REVIEW_DOCUMENT": "Awaiting document review %1", "ROOT_USED_WITH_OTHER_BINDING": "Folder ini telah digunakan oleh %1, isinya akan dihapus. Apakah anda ingin melanjutkan? Jika tidak, klik Batal dan pilih folder lain.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "A folder already in use has been detected.", "RUNNING": "Running", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Selective Sync", "SELECT_SYNC_FOLDERS": "Choose folders to sync", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Memungkinkan anda memilih folder yang akan disinkronkan secara lokal. Ini membutuhkan mesin untuk online.", + "SELF_CREATED_TASKS": "Created by me", "SERVER_INCOMPATIBLE": "We advise you to downgrade your %1 client to the %2 version or to contact your administrator to upgrade your Nuxeo instance.", "SERVER_INCOMPATIBLE_HEADER": "Your server version is not compatible with %1 %2.", "SERVER_UI": "Server UI", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Sinkronisasi sedang berlangsung", "SYNCHRONIZATION_ITEMS_LEFT": "Remaining items: %1", "SYSTEM": "Sistem", + "TASK_MAMNAGER_WINDOW_TITLE": "Task Manager", "TECHNICAL_DETAILS": "Technical details:", "TYPE": "Type", "UNAUTHORIZED": "Kredensial salah", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Unselect all", "UNSYNC": "Unsynchronize", "UPDATE": "Perbarui", + "UPDATE_REQUESTED": "Update Document %1", "UPDATED": "Dokumen yang diperbarui", "UPDATES_LINK": "See updates", "UPDATING_VERSION": "Memperbarui ke %1", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Use the light icons set", "USE_SENTRY": "Enable error reporting", "USERNAME": "Nama Pengguna", + "VALIDATE_DOCUMENT": "Validate the Document %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Otentikasi Web", "WELCOME_MESSAGE": "Tetapkan akun anda dan sinkronkan berkas anda dengan Platform Nuxeo.", "WINDOWS_ERROR_TITLE": "Kesalahan Dokumen", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "The folder \"%1\" or its children is locked by another software. Please close it before %2 can process the deletion from your local storage.", "WRONG_CHANNEL": "You are running version %1 which is from the \"%2\" channel, but your current channel is \"%3\". Do you want to downgrade to the latest version of your current channel, or switch channels?", "WRONG_CHANNEL_HEADER": "Wrong channel detected.", + "YEARS": "Year(s)", "YES": "Yes" } diff --git a/nxdrive/data/i18n/i18n-it.json b/nxdrive/data/i18n/i18n-it.json index c966865424..b41f67c254 100644 --- a/nxdrive/data/i18n/i18n-it.json +++ b/nxdrive/data/i18n/i18n-it.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Addons installato ✓", "ADVANCED": "Avanzate:", "ADVANCED_SETTINGS": "Impostazioni avanzate", + "AGO": "fa", "APPLY": "Applica", "AUTOLOCK": "Blocco automatico", "AUTH_EXPIRED": "Autenticazione scaduta", @@ -28,6 +29,7 @@ "CHANNEL": "Canale", "CHANNEL_CHANGE_SETTINGS": "Canale di aggiornamento", "CHANNEL_CONFIRM_DANGEROUS": "Conferma canale di sviluppo?", + "CHOOSE_PARTICIPANTS": "Scegli partecipanti %1", "CLOSE": "Chiudere", "COMPLETED": "Completato", "COMPLETED_ON": "Completato il %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Connessione rifiutata", "CONNECTION_SUCCESS": "Server connesso", "CONNECTION_UNKNOWN": "Errore sconosciuto durante il tentativo di connessione", + "CONSOLIDATE_REVIEW": "Consolida Revisione %1", "CONTAINER_TYPE": "Tipo di cartelle", "CONTEXT_MENU_1": "Accesso online", "CONTEXT_MENU_2": "Copia link condivisione", @@ -57,6 +60,7 @@ "CREATE": "Crea", "CREATE_REPORT": "Genera bug report", "DATETIME_FORMAT": "%x %X", + "DAYS": "giorno/i", "DEBUG": "Debug", "DEBUG_INVALID_CREDENTIALS": "Alterna credenziali non valide", "DEBUG_SYSTRAY_MESSAGE": "Visualizza un messaggio systray", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Ricrea", "DONT_ASK_AGAIN": "Non chiedermelo più", "DOWNGRADE_TO": "Effettuare il downgrade a %1", + "DUE": "Scadenza", "DUPLICATE_BEHAVIOR": "Gestione dei duplicati", "DUPLICATE_BEHAVIOR_CREATE": "Crea", "DUPLICATE_BEHAVIOR_IGNORE": "Ignora", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Modificare i contenuti del documento dalla scheda Riepilogo anche se non sono sincronizzati.", "FEATURE_S3": "Migliora la velocità dei tuoi caricamenti sfruttando l'infrastruttura AWS. Anche abilitata, questa funzione sarà efficace solo per i clienti Nuxeo Cloud e per i server Nuxeo con S3 Direct Upload addon installato e aggiornato.", "FEATURE_SYNCHRONIZATION": "Abilita o disabilita la sincronizzazione bidirezionale tra l'ambiente locale e la Piattaforma Nuxeo.\nPer applicare le modifiche è necessario un riavvio.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Manca qualcosa? Condividi qui il tuo feedback.", "FILE_ALREADY_EXISTS": "C'è già un file denominato \"%1\" in questa cartella. Vuoi sostituirlo con quello che stai spostando?", "FILE_ALREADY_EXISTS_HEADER": "Un file duplicato è stato trovato.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Cartella già utilizzata da un altro account Nuxeo Drive", "FREE_DISK_SPACE": "(Spazio libero su disco: %1)", "GENERATING": "Creazione…", + "GIVE_OPINION": "Esprimi un’opinione %1", + "HANDLE_TASKS": "Gestisci attività", "HELP": "Guida", "HISTORY": "Cronologia", "HOST": "Host", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "Il digest di questo documento è vuoto. La sua integrità non può essere verificata.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "Il sommario di questo documento non si adatta ad alcun algoritmo. La sua integrità non può essere verificata.", "IGNORE_REASON_UNKNOWN": "File ignorato per ragioni sconosciute", + "IN": "in", "INSTALL_ADDONS": "Installa Addons (come icone overlays) [richiede diritti amministrativi]", "INVALID_CREDENTIALS": "Credenziali non valide", "INVALID_LOCAL_FOLDER": "Questa partizione non supporta gli attributi estesi.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "Nessuna versione compatibile disponibile", "MONITORING": "Monitoraggio", "MONITORING_DESCRIPTION": "Limitato alla dimensione del file > %1", + "MONTHS": "Mese/i", "NAME": "Nome", "NETWORK_ERROR_AuthenticationRequiredError": "Autenticazione necessaria", "NETWORK_ERROR_ClientNetwork": "Impossibile connettersi al server", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "Sono stati risolti tutti i conflitti e gli errori", "NO_CONFLICTS_TITLE": "Nessun conflitto o errore rimanente", "NO_ROOTS": "All'account non è associata nessuna cartella sincronizzata.

Passare alla propria istanza e selezionare una cartella per avviare la sincronizzazione del contenuto.

Maggiori informazioni sulla sincronizzazione sono disponibili sul sito Web della documentazione.", + "NO_TASKS_AVAILABLE": "Nessuna attività da elaborare al momento.", "NO_SPACE_LEFT_ON_DEVICE": "Spazio sul dispositivo insufficiente, liberare dello spazio su disco.", "OAUTH2_MISSING_URL": "URL del server non registrato, riprova.", "OAUTH2_STATE_MISMATCH": "Flusso di autenticazione rotto, riprova.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Apri Finestra", "OTHER": "Altro", "PASSWORD": "Password", + "PENDING_TASKS": "Revisione richiesta", "PERSONAL_SPACE": "Spazio personale", + "PENDING_TASK_REVIEWS": "Nuova attività da rivedere", "PROXY": "Proxy", "PROXY_APPLIED": "Impostazioni del proxy aggiornate", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Note di versione", "RELEASE_NOTES_MSG": "Il tuo %1 è stato aggiornato correttamente alla versione %2, sei pregato di leggere le note di rilascio per ulteriori informazioni.", "RECENTLY_UPDATED": "Aggiornato di recente", + "REFRESH": "Aggiorna", + "REFRESH_AVAILABLE": "Nuove attività disponibili. Si prega di aggiornare la pagina.", "REMOTE_FOLDER": "Cartella remota", "REPLACE": "Sostituisci", "REPORT_GENERATED": "Report disponibile:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Riavvio necessario", "RESTART_NEEDED_MSG": "È necessario riavviare %1 per continuare a usarlo.", "RESUME": "Riprendi", + "REVIEW_DOCUMENT": "In attesa della recensione del documento %1", "ROOT_USED_WITH_OTHER_BINDING": "Questa cartella è stata usata da %1, il contenuto verrà eliminato. Continuare? Se no, fare clic su Annulla e scegliere un’altra cartella.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "Una cartella già in uso è stata rilevata.", "RUNNING": "In esecuzione", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Sincronizzazione selettiva", "SELECT_SYNC_FOLDERS": "Seleziona cartelle di sincronizzazione", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Consente di scegliere le cartelle da sincronizzare localmente. A tal fine è necessario che il motore sia online.", + "SELF_CREATED_TASKS": "Creato da me", "SERVER_INCOMPATIBLE": "Ti consigliamo di eseguire il downgrade del tuo client %1 alla versione %2 o di contattare il tuo amministratore per aggiornare la tua istanza di Nuxeo.", "SERVER_INCOMPATIBLE_HEADER": "La tua versione del server non è compatibile con %1 %2.", "SERVER_UI": "Interfaccia utente del server", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Sincronizzazione in corso", "SYNCHRONIZATION_ITEMS_LEFT": "Elementi rimanenti: %1", "SYSTEM": "Sistema", + "TASK_MAMNAGER_WINDOW_TITLE": "Gestore Attività", "TECHNICAL_DETAILS": "Dettagli tecnici:", "TYPE": "Tipo", "UNAUTHORIZED": "Credenziali non valide", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Deseleziona tutto", "UNSYNC": "Annulla sincronizzazione", "UPDATE": "Aggiorna", + "UPDATE_REQUESTED": "Aggiorna Documento %1", "UPDATED": "Documento aggiornato", "UPDATES_LINK": "Vedi gli aggiornamenti", "UPDATING_VERSION": "Aggiornamento a %1", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Utilizza il set di icone chiare", "USE_SENTRY": "Abilita segnalazione errori", "USERNAME": "Nome utente", + "VALIDATE_DOCUMENT": "Convalida il documento %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Nuxeo Drive - Autenticazione Web", "WELCOME_MESSAGE": "Impostare l’account e sincronizzare i file con Nuxeo Platform.", "WINDOWS_ERROR_TITLE": "Errore del documento", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "La cartella \"%1\" o i suoi figli sono bloccati da un altro programma software. Chiudere il programma per consentire a %2 di elaborare l’eliminazione dall’archivio locale.", "WRONG_CHANNEL": "Stai eseguendo la versione %1 che è dal canale \"%2\", ma il tuo canale attuale è \"%3\". Vuoi declassare all'ultima versione del tuo canale corrente, o cambiare canali?", "WRONG_CHANNEL_HEADER": "Rilevato canale errato.", + "YEARS": "Anno/i", "YES": "Si" } diff --git a/nxdrive/data/i18n/i18n-ja.json b/nxdrive/data/i18n/i18n-ja.json index 4dab05b3b1..fd1a5a8e1f 100644 --- a/nxdrive/data/i18n/i18n-ja.json +++ b/nxdrive/data/i18n/i18n-ja.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "インストールされたアドオン✓", "ADVANCED": "詳細", "ADVANCED_SETTINGS": "詳細設定", + "AGO": "前", "APPLY": "適用", "AUTOLOCK": "オートロック", "AUTH_EXPIRED": "認証が失効しました", @@ -28,6 +29,7 @@ "CHANNEL": "チャンネル", "CHANNEL_CHANGE_SETTINGS": "チャンネルの更新", "CHANNEL_CONFIRM_DANGEROUS": "開発チャンネルを確認しますか?", + "CHOOSE_PARTICIPANTS": "参加者を選択", "CLOSE": "閉じる", "COMPLETED": "完了", "COMPLETED_ON": "%1上で完了", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "接続拒否", "CONNECTION_SUCCESS": "サーバが正常に接続しました", "CONNECTION_UNKNOWN": "ログイン処理中に不明なエラーが発生しました。", + "CONSOLIDATE_REVIEW": "レビュー %1を統合", "CONTAINER_TYPE": "フォルダの種類", "CONTEXT_MENU_1": "オンラインでアクセス", "CONTEXT_MENU_2": "共有リンクをコピー", @@ -57,6 +60,7 @@ "CREATE": "作成", "CREATE_REPORT": "バグレポートの作成", "DATETIME_FORMAT": "%x %X", + "DAYS": "日", "DEBUG": "デバッグ", "DEBUG_INVALID_CREDENTIALS": "無効のクレデンシャル情報を切り替える", "DEBUG_SYSTRAY_MESSAGE": "systrayメッセージを表示", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "再作成", "DONT_ASK_AGAIN": "再び私に聞かないでください", "DOWNGRADE_TO": "%1にダウングレード", + "DUE": "期限", "DUPLICATE_BEHAVIOR": "ファイルの重複管理", "DUPLICATE_BEHAVIOR_CREATE": "作成", "DUPLICATE_BEHAVIOR_IGNORE": "無視する", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "同期されていない場合でも、[概要]タブからドキュメントのコンテンツを編集します。", "FEATURE_S3": "AWSインフラストラクチャを活用して、アップロードの速度を向上させます。この機能を有効にしても、Nuxeo Cloudのお客様と、S3 Direct Uploadアドオンがインストールされ、最新の状態にあるNuxeoサーバーに対してのみ有効です。", "FEATURE_SYNCHRONIZATION": "ローカル環境とNuxeo Platform間の双方向同期を有効または無効にします。\n変更を適用するには、再起動が必要です。", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "何か足りませんか?ここでフィードバックを共有してください。", "FILE_ALREADY_EXISTS": "このフォルダには \"%1\" という名前のファイルがすでに存在します。 移動しているものに置き換えますか?", "FILE_ALREADY_EXISTS_HEADER": "複製ファイルが見つかりました。", @@ -185,6 +191,8 @@ "FOLDER_USED": "別のNuxeo Driveアカウントによって使用されているフォルダ", "FREE_DISK_SPACE": "(空きディスク容量: %1)", "GENERATING": "生成中…", + "GIVE_OPINION": "ご意見をお聞かせください", + "HANDLE_TASKS": "タスクの処理", "HELP": "ヘルプ", "HISTORY": "履歴", "HOST": "ホスト", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "このドキュメントのダイジェストは空です。その整合性は確認できません。", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "この文書ダイジェストは、どのアルゴリズムにも適合しません。その整合性を確認できません。", "IGNORE_REASON_UNKNOWN": "このファイルは理由不明で無視されています", + "IN": "で", "INSTALL_ADDONS": "アドオンのインストール(アイコンオーバーレイなど)[管理者権限が必要]", "INVALID_CREDENTIALS": "無効のクレデンシャル情報", "INVALID_LOCAL_FOLDER": "このパーティションは拡張属性をサポートしていません。", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "互換性のあるバージョンが利用できません", "MONITORING": "モニタリング", "MONITORING_DESCRIPTION": "ファイルサイズに制限 > %1", + "MONTHS": "ヶ月", "NAME": "名前", "NETWORK_ERROR_AuthenticationRequiredError": "認証が必要です", "NETWORK_ERROR_ClientNetwork": "サーバに接続できません", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "競合とエラーをすべて解決しました", "NO_CONFLICTS_TITLE": "競合やエラーはありません", "NO_ROOTS": "あなたのアカウントに関連した同期化フォルダはありません。

インスタンスをブラウズしてフォルダを選択し、コンテンツの同期化を開始してください。

同期化の詳細については、ウェブサイトのドキュメンテーションを参照してください。", + "NO_TASKS_AVAILABLE": "現在、タスクはありません", "NO_SPACE_LEFT_ON_DEVICE": "デバイスにスペースがありません。ディスクスペースを開放してください。", "OAUTH2_MISSING_URL": "サーバーの URL が登録されていません。再試行してください。", "OAUTH2_STATE_MISMATCH": "認証フローが壊れています。再試行してください。", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "ウィンドウを開く", "OTHER": "その他", "PASSWORD": "パスワード", + "PENDING_TASKS": "レビューが必要です。", "PERSONAL_SPACE": "パーソナルスペース", + "PENDING_TASK_REVIEWS": "レビューする新しいタスク", "PROXY": "プロキシ", "PROXY_APPLIED": "プロキシの設定が更新されています", "PROXY_CHANGE_SETTINGS": "プロキシ", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1- リリースノート", "RELEASE_NOTES_MSG": "%1がバージョン%2に正しく更新されました。詳細についてはリリースノートをお読みください。", "RECENTLY_UPDATED": "最近の更新", + "REFRESH": "再読み込み", + "REFRESH_AVAILABLE": "新しいタスクが利用可能です。ページを更新してください。", "REMOTE_FOLDER": "リモートフォルダー", "REPLACE": "置換", "REPORT_GENERATED": "利用可能なレポート:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "再起動する必要があります。", "RESTART_NEEDED_MSG": "引き続き使用するには、%1を再起動する必要があります。", "RESUME": "再開", + "REVIEW_DOCUMENT": "文書レビュー %1を待機中", "ROOT_USED_WITH_OTHER_BINDING": "このフォルダは%1が使用していました。フォルダの内容は削除されます。続けますか?「いいえ」の場合、「キャンセル」をクリックして、別のフォルダを選択してください。", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "すでに使用中のフォルダが検出されました。", "RUNNING": "実行中", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "選択型同期", "SELECT_SYNC_FOLDERS": "同期フォルダを選択", "SELECT_SYNC_FOLDERS_DESCRIPTION": "どのフォルダをローカルで同期化するか選択できます。これにはエンジンがオンラインでなければなりません。", + "SELF_CREATED_TASKS": "自分が作成", "SERVER_INCOMPATIBLE": "%1クライアントを%2バージョンにダウングレードするか、管理者に連絡してNuxeoのインスタンスをアップグレードすることをお勧めします。", "SERVER_INCOMPATIBLE_HEADER": "あなたのサーバーのバージョンは%1 %2と互換性がありません。", "SERVER_UI": "サーバのUI", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "同期化進行中", "SYNCHRONIZATION_ITEMS_LEFT": "残りの項目: %1", "SYSTEM": "システム", + "TASK_MAMNAGER_WINDOW_TITLE": "タスクマネージャー", "TECHNICAL_DETAILS": "技術的な詳細:", "TYPE": "タイプ", "UNAUTHORIZED": "誤ったクレデンシャル情報", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "全て選択解除", "UNSYNC": "同期を解除する", "UPDATE": "更新", + "UPDATE_REQUESTED": "ドキュメント %1を更新", "UPDATED": "更新された文書", "UPDATES_LINK": "更新を見る", "UPDATING_VERSION": "%1へアップグレード", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "ライトのアイコンを使用する", "USE_SENTRY": "エラーレポートを有効にする", "USERNAME": "ユーザ名", + "VALIDATE_DOCUMENT": "ドキュメント %1を検証する", "WEB_AUTHENTICATION_WINDOW_TITLE": "Nuxeo Drive - Web認証", "WELCOME_MESSAGE": "アカウントを設定し、ファイルをNuxeo Platformと同期化します。", "WINDOWS_ERROR_TITLE": "文書エラー", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "フォルダ\"%1\" またはその子が別のソフトウェアによってロックされています。%2がローカルストレージからの削除を処理できるよう、まずファイルを閉じてください。", "WRONG_CHANNEL": "バージョン%1は\"%2\"チャンネルからのものですが、現在のチャンネルは\"%3\"です。 現在のチャンネルの最新バージョンにダウングレードするか、チャンネルを切り替えるか。", "WRONG_CHANNEL_HEADER": "間違ったチャネルが検出されました。", + "YEARS": "年", "YES": "はい" } diff --git a/nxdrive/data/i18n/i18n-nl.json b/nxdrive/data/i18n/i18n-nl.json index f6eca568ea..1489a9a599 100644 --- a/nxdrive/data/i18n/i18n-nl.json +++ b/nxdrive/data/i18n/i18n-nl.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Geïnstalleerde addons ✓", "ADVANCED": "Geavanceerd:", "ADVANCED_SETTINGS": "Geavanceerde instellingen", + "AGO": "geleden", "APPLY": "Toepassen", "AUTOLOCK": "Automatische vergrendeling", "AUTH_EXPIRED": "Verificatie is verlopen", @@ -28,6 +29,7 @@ "CHANNEL": "Kanaal", "CHANNEL_CHANGE_SETTINGS": "Kanaal update", "CHANNEL_CONFIRM_DANGEROUS": "Ontwikkelingskanaal bevestigen?", + "CHOOSE_PARTICIPANTS": "Deelnemers selecteren %1", "CLOSE": "Sluiten", "COMPLETED": "Voltooid", "COMPLETED_ON": "Voltooid op %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Verbinding geweigerd", "CONNECTION_SUCCESS": "Server verbonden", "CONNECTION_UNKNOWN": "Er is een onbekende fout opgetreden tijdens het verbinden", + "CONSOLIDATE_REVIEW": "Review consolideren %1", "CONTAINER_TYPE": "Maptype", "CONTEXT_MENU_1": "Online openen", "CONTEXT_MENU_2": "Link kopiëren om te delen", @@ -57,6 +60,7 @@ "CREATE": "Aanmaken", "CREATE_REPORT": "Genereer foutenrapport", "DATETIME_FORMAT": "%x %X", + "DAYS": "Dag (en)", "DEBUG": "Programmafout repareren", "DEBUG_INVALID_CREDENTIALS": "Wisselen tussen ongeldige aanmeldgegevens", "DEBUG_SYSTRAY_MESSAGE": "Een systray-melding weergeven", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Opnieuw aanmaken", "DONT_ASK_AGAIN": "Vraag me niet weer", "DOWNGRADE_TO": "Downgraden naar %1", + "DUE": "Vervallen", "DUPLICATE_BEHAVIOR": "Bestanden dupliceren beheer", "DUPLICATE_BEHAVIOR_CREATE": "Aanmaken", "DUPLICATE_BEHAVIOR_IGNORE": "Negeren", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Bewerk een van de inhoud van het document vanuit hun overzichttabblad, zelfs als ze niet zijn gesynchroniseerd.", "FEATURE_S3": "Verbeter de snelheid van uw uploads door gebruik te maken van de AWS-infrastructuur. Zelfs als deze functie is ingeschakeld, is deze functie alleen effectief voor Nuxeo Cloud-klanten en voor Nuxeo-servers waarop de add-on S3 Direct Upload is geïnstalleerd en up-to-date is.", "FEATURE_SYNCHRONIZATION": "In- of uitschakelen van de bidirectionele synchronisatie tussen de lokale omgeving en het Nuxeo-platform.\nHerstart is nodig om wijzigingen door te voeren.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Ontbreekt er iets? Deel uw feedback hier.", "FILE_ALREADY_EXISTS": "Er is al een bestand genaamd \"%1\" in deze map. Wilt u het vervangen door degene die u verplaatst?", "FILE_ALREADY_EXISTS_HEADER": "Er is een dubbel bestand gevonden.", @@ -185,6 +191,8 @@ "FOLDER_USED": "De map wordt al gebruikt door een ander Nuxeo Drive-account", "FREE_DISK_SPACE": "(Vrije schijfruimte: %1)", "GENERATING": "Genereren…", + "GIVE_OPINION": "Geef uw mening %1", + "HANDLE_TASKS": "Taken afhandelen", "HELP": "Help", "HISTORY": "Geschiedenis", "HOST": "Host", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "De digest van dit document is leeg. De integriteit ervan kan niet worden geverifieerd.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "Het digest van dit document pas bij geen enkel algoritme. De integriteit ervan kan niet worden geverifieerd.", "IGNORE_REASON_UNKNOWN": "Dit bestand wordt genegeerd om een onbekende reden", + "IN": "in", "INSTALL_ADDONS": "Installeer Addons (zoals pictogramoverlays) [vereist administratieve rechten]", "INVALID_CREDENTIALS": "Ongeldige aanmeldgegevens", "INVALID_LOCAL_FOLDER": "Deze partitie ondersteunt geen uitgebreide attributen.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "Geen compatibele versie beschikbaar", "MONITORING": "Controle", "MONITORING_DESCRIPTION": "Beperkt tot bestandsgrootte > %1", + "MONTHS": "Maand(en)", "NAME": "Naam", "NETWORK_ERROR_AuthenticationRequiredError": "Verificatie vereist", "NETWORK_ERROR_ClientNetwork": "Kan geen verbinding met de server maken", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "U hebt alle conflicten en fouten opgelost", "NO_CONFLICTS_TITLE": "Er zijn geen verdere conflicten of fouten", "NO_ROOTS": "Er is geen gesynchroniseerde map gekoppeld aan uw account.

Blader naar uw instantie en kies een map om te beginnen met het synchroniseren van inhoud.

Ga naar de documentatiewebsite voor meer informatie over synchronisatie.", + "NO_TASKS_AVAILABLE": "U hebt momenteel geen taken die moeten worden verwerkt.", "NO_SPACE_LEFT_ON_DEVICE": "Er is geen vrije ruimte op het apparaat beschikbaar; maak ruimte op uw schijf.", "OAUTH2_MISSING_URL": "Server-URL niet geregistreerd, probeer het opnieuw.", "OAUTH2_STATE_MISMATCH": "Kapotte authenticatie stroom, probeer het opnieuw.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Venster openen", "OTHER": "Anders", "PASSWORD": "Wachtwoord", + "PENDING_TASKS": "Beoordeling vereist", "PERSONAL_SPACE": "Persoonlijke ruimte", + "PENDING_TASK_REVIEWS": "Nieuwe taak om te beoordelen", "PROXY": "Proxy", "PROXY_APPLIED": "De proxy-instellingen zijn bijgewerkt", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Release-opmerkingen", "RELEASE_NOTES_MSG": "Uw %1 is correct bijgewerkt naar de versie %2, lees voor meer informatie de release notes", "RECENTLY_UPDATED": "Onlangs bijgewerkt", + "REFRESH": "Vernieuwen", + "REFRESH_AVAILABLE": "Nieuwe taken beschikbaar. Herlaad de pagina.", "REMOTE_FOLDER": "Externe map", "REPLACE": "Vervangen", "REPORT_GENERATED": "Rapport beschikbaar:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Herstart nodig", "RESTART_NEEDED_MSG": "U moet %1 opnieuw opstarten om het te blijven gebruiken.", "RESUME": "Hervatten", + "REVIEW_DOCUMENT": "In afwachting van document review %1", "ROOT_USED_WITH_OTHER_BINDING": "Deze map is gebruikt door %1, de inhoud ervan wordt gewist. Wilt u doorgaan? Zo niet, klik dan op Annuleren en selecteer een andere map.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "Er is een map gevonden die al in gebruik is.", "RUNNING": "Actieve", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Selectieve synchronisatie", "SELECT_SYNC_FOLDERS": "Synchronisatie mappen selecteren", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Hiermee kunt u bepalen welke mappen lokaal moeten worden gesynchroniseerd. Hiervoor dient de engine online te zijn.", + "SELF_CREATED_TASKS": "Gemaakt door mij", "SERVER_INCOMPATIBLE": "Wij raden u aan om uw %1-client te downgraden naar versie %2 of om contact op te nemen met uw beheerder om uw Nuxeo-instantie te upgraden.", "SERVER_INCOMPATIBLE_HEADER": "Uw serverversie is niet compatibel met %1 %2.", "SERVER_UI": "Servergebruikersinterface", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Synchronisatie bezig", "SYNCHRONIZATION_ITEMS_LEFT": "Overgebleven items: %1", "SYSTEM": "Systeem", + "TASK_MAMNAGER_WINDOW_TITLE": "Taakbeheer", "TECHNICAL_DETAILS": "Technische gegevens:", "TYPE": "Type", "UNAUTHORIZED": "Onjuiste aanmeldgegevens", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Alle selecties opheffen", "UNSYNC": "Synchronisatie ongedaan maken", "UPDATE": "Update", + "UPDATE_REQUESTED": "Document bijwerken %1", "UPDATED": "Bijgewerkt document", "UPDATES_LINK": "Bekijk updates", "UPDATING_VERSION": "Bezig met bijwerken naar %1", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Gebruik de ingestelde lichtpictogrammen", "USE_SENTRY": "Foutrapportage inschakelen", "USERNAME": "Gebruikersnaam", + "VALIDATE_DOCUMENT": "Het document valideren %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Nuxeo Drive - Webverificatie", "WELCOME_MESSAGE": "Stel uw account in en synchroniseer uw bestanden met het Nuxeo-platform.", "WINDOWS_ERROR_TITLE": "Documentfout", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "De map \"%1\" of onderliggende mappen ervan zijn vergrendeld door een ander programma. Sluit dit af voordat %2 het uit uw lokale opslag kan verwijderen.", "WRONG_CHANNEL": "Je draait versie %1 die van het Kanaal \"%2\" staat, maar je huidige kanaal is \"%3\". Wil je downgraden naar de laatste versie van je huidige kanaal, of van kanaal wisselen?", "WRONG_CHANNEL_HEADER": "Verkeerd kanaal gedetecteerd.", + "YEARS": "Jaar/Jaren", "YES": "Ja" } diff --git a/nxdrive/data/i18n/i18n-pl.json b/nxdrive/data/i18n/i18n-pl.json index aadcd876a2..0ad3ddbb86 100644 --- a/nxdrive/data/i18n/i18n-pl.json +++ b/nxdrive/data/i18n/i18n-pl.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Dodatki zainstalowane ✓", "ADVANCED": "Advanced:", "ADVANCED_SETTINGS": "Ustawienia zaawansowane", + "AGO": "ago", "APPLY": "Zastosuj", "AUTOLOCK": "Automatyczna blokada", "AUTH_EXPIRED": "Uwierzytelnienie wygasło", @@ -28,6 +29,7 @@ "CHANNEL": "Kanał", "CHANNEL_CHANGE_SETTINGS": "Channel update", "CHANNEL_CONFIRM_DANGEROUS": "Potwierdzić kanał programistyczny?", + "CHOOSE_PARTICIPANTS": "Choose Participants %1", "CLOSE": "Close", "COMPLETED": "Zakończono", "COMPLETED_ON": "Completed on %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Odmowa połączenia", "CONNECTION_SUCCESS": "Serwer pomyślnie połączony", "CONNECTION_UNKNOWN": "Wystąpił nieznany błąd podczas próby łączenia", + "CONSOLIDATE_REVIEW": "Consolidate Review %1", "CONTAINER_TYPE": "Folder Type", "CONTEXT_MENU_1": "Dostęp online", "CONTEXT_MENU_2": "Kopiuj link udostępniania", @@ -57,6 +60,7 @@ "CREATE": "Create", "CREATE_REPORT": "Generate bug report", "DATETIME_FORMAT": "%x %X", + "DAYS": "Day(s)", "DEBUG": "Debuguj", "DEBUG_INVALID_CREDENTIALS": "Przełącz nieprawidłowe dane logowania", "DEBUG_SYSTRAY_MESSAGE": "Wyświetl komunikat na pasku zadań", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Odtwórz", "DONT_ASK_AGAIN": "Nie pytaj ponownie", "DOWNGRADE_TO": "Obniżenie wersji do %1", + "DUE": "Due", "DUPLICATE_BEHAVIOR": "Files duplicates management", "DUPLICATE_BEHAVIOR_CREATE": "Utwórz", "DUPLICATE_BEHAVIOR_IGNORE": "Ignoruj", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Edytuj dowolną zawartość dokumentu z zakładki Podsumowania, nawet jeśli nie są one zsynchronizowane.", "FEATURE_S3": "Popraw szybkość wysyłania poprzez wykorzystanie infrastruktury AWS. Nawet włączone, ta funkcja będzie skuteczna tylko dla klientów Nuxeo Cloud oraz dla serwerów Nuxeo z zainstalowanym i aktualnym dodatkiem S3 Direct Upload.", "FEATURE_SYNCHRONIZATION": "Enable or disable the bidirectional synchronization between the local environment and the Nuxeo Platform.\nA restart is needed to apply changes.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Coś brakuje? Podziel się swoją opinią tutaj.", "FILE_ALREADY_EXISTS": "Istnieje już plik o nazwie \"%1\" w tym folderze. Czy chcesz zastąpić go tym, który przenosisz?", "FILE_ALREADY_EXISTS_HEADER": "A duplicate file has been found.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Folder jest już używany przez inne konto", "FREE_DISK_SPACE": "(Wolne miejsce na dysku: %1)", "GENERATING": "Generating…", + "GIVE_OPINION": "Give your Opinion %1", + "HANDLE_TASKS": "Handle tasks", "HELP": "Pomoc", "HISTORY": "History", "HOST": "Host", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "This document's digest is empty. Its integrity cannot be verified.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "This document's digest doesn't fit any algorithm. Its integrity cannot be verified.", "IGNORE_REASON_UNKNOWN": "Ten plik jest ignorowany z nieznanego powodu", + "IN": "in", "INSTALL_ADDONS": "Zainstaluj dodatki (np. nakładki ikon) [wymaga uprawnień administracyjnych]", "INVALID_CREDENTIALS": "Nieprawidłowe dane logowania", "INVALID_LOCAL_FOLDER": "This partition does not support extended attributes.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "Brak kompatybilnej wersji", "MONITORING": "Monitoring", "MONITORING_DESCRIPTION": "Display limited to file size > %1", + "MONTHS": "Month(s)", "NAME": "Nazwa", "NETWORK_ERROR_AuthenticationRequiredError": "Wymagana autoryzacja", "NETWORK_ERROR_ClientNetwork": "Nie można połączyć się z serwerem", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "Rozwiązałeś wszystkie konflikty i błędy", "NO_CONFLICTS_TITLE": "Brak już konfliktów i błędów", "NO_ROOTS": "Nie ma zsynchronizowanego folderu skojarzonego z Twoim kontem.

Przejdź do swojej instancji i wybierz folder, aby rozpocząć synchronizację zawartości.

Dowiedz się więcej o synchronizacji na stronie dokumentacji.", + "NO_TASKS_AVAILABLE": "You do not have any tasks to process at the moment", "NO_SPACE_LEFT_ON_DEVICE": "Brak miejsca na dysku, prosimy zwolnić trochę miejsca.", "OAUTH2_MISSING_URL": "Server URL not registered, please retry.", "OAUTH2_STATE_MISMATCH": "Broken authentication flow, please retry.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Otwórz okno", "OTHER": "Inne", "PASSWORD": "Hasło", + "PENDING_TASKS": "Review required", "PERSONAL_SPACE": "Przestrzeń osobista", + "PENDING_TASK_REVIEWS": "New task to review", "PROXY": "Sewer proxy", "PROXY_APPLIED": "Ustawienia serwera proxy zostały zaktualizowane", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Informacje o wydaniu", "RELEASE_NOTES_MSG": "Twój %1 został poprawnie zaktualizowany do wersji %2, przeczytaj informacje o wydaniu , aby dowiedzieć się więcej.", "RECENTLY_UPDATED": "Ostatnio zaktualizowane", + "REFRESH": "Refresh", + "REFRESH_AVAILABLE": "New tasks available. Please refresh the page.", "REMOTE_FOLDER": "Folder zdalny", "REPLACE": "Zastąp", "REPORT_GENERATED": "Dostępny raport:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Wymagane ponowne uruchomienie", "RESTART_NEEDED_MSG": "Musisz zrestartować %1 , aby nadal go używać.", "RESUME": "Wznów", + "REVIEW_DOCUMENT": "Awaiting document review %1", "ROOT_USED_WITH_OTHER_BINDING": "Ten folder był używany przez %1, jego zawartość zostanie usunięta. Czy chcesz kontynuować? Jeśli nie, kliknij przycisk Anuluj i wybierz inny folder.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "A folder already in use has been detected.", "RUNNING": "Running", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Synchronizacja selektywna", "SELECT_SYNC_FOLDERS": "Wybierz foldery do synchronizacji", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Pozwala wybrać foldery, które chcesz synchronizować lokalnie. Wymaga to, aby silnik był online.", + "SELF_CREATED_TASKS": "Created by me", "SERVER_INCOMPATIBLE": "We advise you to downgrade your %1 client to the %2 version or to contact your administrator to upgrade your Nuxeo instance.", "SERVER_INCOMPATIBLE_HEADER": "Your server version is not compatible with %1 %2.", "SERVER_UI": "Interfejs serwera", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Synchronizacja w toku", "SYNCHRONIZATION_ITEMS_LEFT": "Remaining items: %1", "SYSTEM": "System", + "TASK_MAMNAGER_WINDOW_TITLE": "Task Manager", "TECHNICAL_DETAILS": "Szczegóły techniczne:", "TYPE": "Typ", "UNAUTHORIZED": "Nieprawidłowe dane logowania", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Odznacz wszystko", "UNSYNC": "Wyłącz synchronizację", "UPDATE": "Aktualizacja", + "UPDATE_REQUESTED": "Update Document %1", "UPDATED": "Zaktualizowany dokument", "UPDATES_LINK": "Zobacz aktualizacje", "UPDATING_VERSION": "Aktualizacja do %1", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Użyj zestawu jasnych ikon", "USE_SENTRY": "Enable error reporting", "USERNAME": "Nazwa użytkownika", + "VALIDATE_DOCUMENT": "Validate the Document %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Uwierzytelnianie sieci Web", "WELCOME_MESSAGE": "Ustaw swoje konto i synchronizuj pliki z platformą Nuxeo.", "WINDOWS_ERROR_TITLE": "Błąd dokumentu", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "Folder „%1” lub jego elementy podrzędne są zablokowane przez inną aplikację. Zamknij ją zanim %2 będzie mógł wykonać usuwanie z pamięci lokalnej.", "WRONG_CHANNEL": "Używasz wersji %1, która pochodzi z kanału \"%2\", ale twoim bieżącym kanałem jest \"%3\". Chcesz cofnąć wersję do najnowszej wersji aktualnego kanału czy zmienić kanały?", "WRONG_CHANNEL_HEADER": "Wrong channel detected.", + "YEARS": "Year(s)", "YES": "Tak" } diff --git a/nxdrive/data/i18n/i18n-sv.json b/nxdrive/data/i18n/i18n-sv.json index f086c0974e..d280d2f094 100644 --- a/nxdrive/data/i18n/i18n-sv.json +++ b/nxdrive/data/i18n/i18n-sv.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Tillägg installerade ✓", "ADVANCED": "Avancerat:", "ADVANCED_SETTINGS": "Avancerade inställningar", + "AGO": "sedan", "APPLY": "Tillämpa", "AUTOLOCK": "Autolås", "AUTH_EXPIRED": "Autentisering har gått ut", @@ -28,6 +29,7 @@ "CHANNEL": "Kanal", "CHANNEL_CHANGE_SETTINGS": "Kanal uppdatering", "CHANNEL_CONFIRM_DANGEROUS": "Bekräfta utvecklingskanal?", + "CHOOSE_PARTICIPANTS": "Välj deltagare %1", "CLOSE": "Stäng", "COMPLETED": "Slutförd", "COMPLETED_ON": "Slutförd den %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Anslutning nekades", "CONNECTION_SUCCESS": "Servern har anslutits", "CONNECTION_UNKNOWN": "Okänt fel inträffade när du försökte ansluta", + "CONSOLIDATE_REVIEW": "Konsolidera granskningen %1", "CONTAINER_TYPE": "Mapptyp", "CONTEXT_MENU_1": "Åtkomst online", "CONTEXT_MENU_2": "Kopiera delningslänk", @@ -57,6 +60,7 @@ "CREATE": "Skapa", "CREATE_REPORT": "Generera felrapport", "DATETIME_FORMAT": "%x %X", + "DAYS": "Dag(ar)", "DEBUG": "Felsök", "DEBUG_INVALID_CREDENTIALS": "Växla ogiltiga inloggningsuppgifter", "DEBUG_SYSTRAY_MESSAGE": "Visa ett systray-meddelande", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Återskapa", "DONT_ASK_AGAIN": "Fråga mig inte igen", "DOWNGRADE_TO": "Nedgradera till %1", + "DUE": "Förfallodatum", "DUPLICATE_BEHAVIOR": "Filer duplicerar hantering", "DUPLICATE_BEHAVIOR_CREATE": "Skapa", "DUPLICATE_BEHAVIOR_IGNORE": "Ignorera", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Redigera något av dokumentets innehåll från sin sammanfattande flik även om de inte är synkroniserade.", "FEATURE_S3": "Förbättra hastigheten på dina uppladdningar genom att utnyttja AWS infrastruktur. Även aktiverad, denna funktion kommer att vara effektiv endast för Nuxeo Cloud-kunder och för Nuxeo-servrar med S3 Direct Upload addon installerat och uppdaterat.", "FEATURE_SYNCHRONIZATION": "Aktivera eller inaktivera dubbelriktad synkronisering mellan den lokala miljön och Nuxeo-plattformen.\nEn omstart behövs för att tillämpa ändringar.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Saknas något? Dela med dig av din feedback här.", "FILE_ALREADY_EXISTS": "Det finns redan en fil med namnet \"%1\" i den här mappen. Vill du ersätta den med den du flyttar?", "FILE_ALREADY_EXISTS_HEADER": "En duplicerad fil har upptäckts.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Mappen används redan av ett annat Nuxeo Drive-konto", "FREE_DISK_SPACE": "(Ledigt diskutrymme: %1)", "GENERATING": "Genererar...", + "GIVE_OPINION": "Ge din åsikt %1", + "HANDLE_TASKS": "Uppgiftshanterare", "HELP": "Hjälp", "HISTORY": "Historik", "HOST": "Värd", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "Det här dokumentets sammanfattning är tomt. Dess integritet kan inte verifieras.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "Detta dokuments sammandrag passar inte någon algoritm. Dess integritet kan inte verifieras.", "IGNORE_REASON_UNKNOWN": "Den här filen ignoreras av okänd orsak", + "IN": "i", "INSTALL_ADDONS": "Installera Addons (som ikonöverlag) [kräver administrativa rättigheter]", "INVALID_CREDENTIALS": "Ogiltiga inloggningsuppgifter", "INVALID_LOCAL_FOLDER": "Denna partition stöder inte utökade attribut.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "Ingen kompatibel version tillgänglig", "MONITORING": "Övervakning", "MONITORING_DESCRIPTION": "Begränsad till filstorlek > %1", + "MONTHS": "Månad(er)", "NAME": "Namn", "NETWORK_ERROR_AuthenticationRequiredError": "Autentisering krävs", "NETWORK_ERROR_ClientNetwork": "Det går inte att ansluta till servern", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "Du har åtgärdat alla konflikter och fel", "NO_CONFLICTS_TITLE": "Inga konflikter eller fel", "NO_ROOTS": "Det finns inge synkroniserad mapp kopplad till ditt konto.

Bläddra till din instans och välj en mapp för att börja synkronisera innehåll.

Lär dig mer om synkronisering på dokumentationswebbplatsen.", + "NO_TASKS_AVAILABLE": "Du har inte någon uppgift att bearbeta för tillfället", "NO_SPACE_LEFT_ON_DEVICE": "Inget utrymme kvar på enheten, frigör lite diskutrymme.", "OAUTH2_MISSING_URL": "Server-URL är inte registrerad, försök igen.", "OAUTH2_STATE_MISMATCH": "Trasigt autentiseringsflöde, försök igen.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Öppna fönster", "OTHER": "Annat", "PASSWORD": "Lösenord", + "PENDING_TASKS": "Granskning krävs", "PERSONAL_SPACE": "Personligt utrymme", + "PENDING_TASK_REVIEWS": "Ny uppgift att granska", "PROXY": "Proxyserver", "PROXY_APPLIED": "Proxyinställningar har uppdaterats", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Släppanteckningar", "RELEASE_NOTES_MSG": "Din %1 har uppdaterats korrekt till versionen %2, läs versionsinformationen för mer information.", "RECENTLY_UPDATED": "Nyligen uppdaterad", + "REFRESH": "Uppdatera", + "REFRESH_AVAILABLE": "Nya uppgifter tillgängliga. Uppdatera sidan.", "REMOTE_FOLDER": "Fjärrmapp", "REPLACE": "Ersätt", "REPORT_GENERATED": "Rapport tillgänglig:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Omstart behövs.", "RESTART_NEEDED_MSG": "Du måste starta om %1 för att fortsätta använda den.", "RESUME": "Återuppta", + "REVIEW_DOCUMENT": "Väntar på dokumentgranskning %1", "ROOT_USED_WITH_OTHER_BINDING": "Den här mappen har använts av %1; innehållet kommer att raderas. Vill du fortsätta? Om du inte vill det klickar du på Avbryt och väljer en annan mapp.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "En mapp som redan används har upptäckts.", "RUNNING": "Pågår", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Selektiv synk", "SELECT_SYNC_FOLDERS": "Välj mappar för synkronisering", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Gör det möjligt att välja vilka mappar som ska synkroniseras lokalt. För detta krävs att motorn är online.", + "SELF_CREATED_TASKS": "Skapad av mig", "SERVER_INCOMPATIBLE": "Vi rekommenderar att du nedgraderar din %1-klient till version %2 eller att du kontaktar din administratör för att uppgradera din Nuxeo-instans.", "SERVER_INCOMPATIBLE_HEADER": "Din serverversion är inte kompatibel med %1 %2.", "SERVER_UI": "Användargränssnitt för servern", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Synkronisering pågår", "SYNCHRONIZATION_ITEMS_LEFT": "Återstående objekt: %1", "SYSTEM": "System", + "TASK_MAMNAGER_WINDOW_TITLE": "Uppgiftshanterare", "TECHNICAL_DETAILS": "Tekniska detaljer:", "TYPE": "Skriv", "UNAUTHORIZED": "Fel inloggningsuppgifter", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Avmarkera alla", "UNSYNC": "Sluta synkronisera", "UPDATE": "Uppdatera", + "UPDATE_REQUESTED": "Uppdatera dokumentet %1", "UPDATED": "Uppdaterat dokument", "UPDATES_LINK": "Visa uppdateringar", "UPDATING_VERSION": "Uppdaterar till %1", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Använd uppsättningen ljusikoner", "USE_SENTRY": "Aktivera felrapportering", "USERNAME": "Användarnamn", + "VALIDATE_DOCUMENT": "Validera dokumentet %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Nuxeo Drive – webbautentisering", "WELCOME_MESSAGE": "Ange konto och synkronisera dina filer med Nuxeo Platform.", "WINDOWS_ERROR_TITLE": "Dokumentfel", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "Mappen \"%1\" eller dess underordnade mappar har låsts av ett annat program. Stäng den innan %2 kan bearbeta den här borttagningen från den lokala lagringsplatsen.", "WRONG_CHANNEL": "Du kör version %1 som är från \"%2\" kanalen, men din nuvarande kanal är \"%3\". Vill du nedgradera till den senaste versionen av din nuvarande kanal eller byta kanal?", "WRONG_CHANNEL_HEADER": "Fel kanal upptäckt.", + "YEARS": "År", "YES": "Ja" } diff --git a/nxdrive/data/i18n/i18n.json b/nxdrive/data/i18n/i18n.json index 04b63f63f7..00964d7e4f 100644 --- a/nxdrive/data/i18n/i18n.json +++ b/nxdrive/data/i18n/i18n.json @@ -9,6 +9,7 @@ "ADDONS_INSTALLED": "Addons installed ✓", "ADVANCED": "Advanced:", "ADVANCED_SETTINGS": "Advanced settings", + "AGO": "ago", "APPLY": "Apply", "AUTOLOCK": "Autolock", "AUTH_EXPIRED": "Authentication expired", @@ -28,6 +29,7 @@ "CHANNEL": "Channel", "CHANNEL_CHANGE_SETTINGS": "Channel update", "CHANNEL_CONFIRM_DANGEROUS": "Confirm development channel?", + "CHOOSE_PARTICIPANTS": "Choose Participants %1", "CLOSE": "Close", "COMPLETED": "Completed", "COMPLETED_ON": "Completed on %1", @@ -47,6 +49,7 @@ "CONNECTION_REFUSED": "Connection refused", "CONNECTION_SUCCESS": "Server successfully connected", "CONNECTION_UNKNOWN": "Unknown error occurred while trying to connect", + "CONSOLIDATE_REVIEW": "Consolidate Review %1", "CONTAINER_TYPE": "Folder Type", "CONTEXT_MENU_1": "Access online", "CONTEXT_MENU_2": "Copy share-link", @@ -57,6 +60,7 @@ "CREATE": "Create", "CREATE_REPORT": "Generate bug report", "DATETIME_FORMAT": "%x %X", + "DAYS": "Day(s)", "DEBUG": "Debug", "DEBUG_INVALID_CREDENTIALS": "Toggle Invalid Credentials", "DEBUG_SYSTRAY_MESSAGE": "Display a systray message", @@ -126,6 +130,7 @@ "DRIVE_ROOT_RECREATE": "Recreate", "DONT_ASK_AGAIN": "Don't ask me again", "DOWNGRADE_TO": "Downgrade to %1", + "DUE": "Due", "DUPLICATE_BEHAVIOR": "Files duplicates management", "DUPLICATE_BEHAVIOR_CREATE": "Create", "DUPLICATE_BEHAVIOR_IGNORE": "Ignore", @@ -169,6 +174,7 @@ "FEATURE_DIRECT_EDIT": "Edit any of document’s content from their Summary tab even if they are not synchronized.", "FEATURE_S3": "Improve the speed of your uploads by leveraging the AWS infrastructure. Even enabled, this feature will be effective only for Nuxeo Cloud customers and for Nuxeo servers with S3 Direct Upload addon installed and up-to-date.", "FEATURE_SYNCHRONIZATION": "Enable or disable the bidirectional synchronization between the local environment and the Nuxeo Platform.\nA restart is needed to apply changes.", + "FEATURE_TASKS_MANAGEMENT": "Handle your tasks from Nuxeo Drive and receive notifications and reminders when a new task is assigned to you.", "FEEDBACK_LINK": "Something is missing? Share your feedback here.", "FILE_ALREADY_EXISTS": "There is already a file named \"%1\" in this folder. Do you want to replace it with the one you're moving?", "FILE_ALREADY_EXISTS_HEADER": "A duplicate file has been found.", @@ -185,6 +191,8 @@ "FOLDER_USED": "Folder already used by another account", "FREE_DISK_SPACE": "(Free disk space: %1)", "GENERATING": "Generating…", + "GIVE_OPINION": "Give your Opinion %1", + "HANDLE_TASKS": "Handle tasks", "HELP": "Help", "HISTORY": "History", "HOST": "Host", @@ -202,6 +210,7 @@ "IGNORE_REASON_REMOTE_HASH_EMPTY": "This document's digest is empty. Its integrity cannot be verified.", "IGNORE_REASON_REMOTE_HASH_EXOTIC": "This document's digest doesn't fit any algorithm. Its integrity cannot be verified.", "IGNORE_REASON_UNKNOWN": "This file is ignored for unknown reason", + "IN": "in", "INSTALL_ADDONS": "Install Addons (like icon overlays) [requires administrative rights]", "INVALID_CREDENTIALS": "Invalid credentials", "INVALID_LOCAL_FOLDER": "This partition does not support extended attributes.", @@ -228,6 +237,7 @@ "MISSING_UPDATE_VERSION": "No compatible version available", "MONITORING": "Monitoring", "MONITORING_DESCRIPTION": "Display limited to file size > %1", + "MONTHS": "Month(s)", "NAME": "Name", "NETWORK_ERROR_AuthenticationRequiredError": "Authentication required", "NETWORK_ERROR_ClientNetwork": "Cannot connect to the server", @@ -276,6 +286,7 @@ "NO_CONFLICTS_BODY": "You have resolved all conflicts and errors", "NO_CONFLICTS_TITLE": "No conflicts or errors left", "NO_ROOTS": "There is no synchronized folder associated with your account.

Please browse to your instance and pick a folder to start synchronizing content.

Learn more about synchronization on the documentation website.", + "NO_TASKS_AVAILABLE": "You do not have any tasks to process at the moment", "NO_SPACE_LEFT_ON_DEVICE": "No space left on device, please free some disk space.", "OAUTH2_MISSING_URL": "Server URL not registered, please retry.", "OAUTH2_STATE_MISMATCH": "Broken authentication flow, please retry.", @@ -286,7 +297,9 @@ "OPEN_WINDOW": "Open window", "OTHER": "Other", "PASSWORD": "Password", + "PENDING_TASKS": "Review required", "PERSONAL_SPACE": "Personal space", + "PENDING_TASK_REVIEWS": "New task to review", "PROXY": "Proxy", "PROXY_APPLIED": "Proxy settings have been updated", "PROXY_CHANGE_SETTINGS": "Proxy", @@ -304,6 +317,8 @@ "RELEASE_NOTES_TITLE": "%1 - Release notes", "RELEASE_NOTES_MSG": "Your %1 has been correctly updated to the version %2, please read the release notes for more information.", "RECENTLY_UPDATED": "Recently updated", + "REFRESH": "Refresh", + "REFRESH_AVAILABLE": "New tasks available. Please refresh the page.", "REMOTE_FOLDER": "Remote folder", "REPLACE": "Replace", "REPORT_GENERATED": "Report available:", @@ -312,6 +327,7 @@ "RESTART_NEEDED": "Restart needed", "RESTART_NEEDED_MSG": "You need to restart %1 to continue using it.", "RESUME": "Resume", + "REVIEW_DOCUMENT": "Awaiting document review %1", "ROOT_USED_WITH_OTHER_BINDING": "This folder has been used by %1, its content will be deleted. Do you want to continue? If no, click on Cancel and choose another folder.", "ROOT_USED_WITH_OTHER_BINDING_HEADER": "A folder already in use has been detected.", "RUNNING": "Running", @@ -331,6 +347,7 @@ "SELECTIVE_SYNC": "Selective Sync", "SELECT_SYNC_FOLDERS": "Choose folders to sync", "SELECT_SYNC_FOLDERS_DESCRIPTION": "Allows you to choose which folders to sync locally. This requires the engine to be online.", + "SELF_CREATED_TASKS": "Created by me", "SERVER_INCOMPATIBLE": "We advise you to downgrade your %1 client to the %2 version or to contact your administrator to upgrade your Nuxeo instance.", "SERVER_INCOMPATIBLE_HEADER": "Your server version is not compatible with %1 %2.", "SERVER_UI": "Server UI", @@ -366,6 +383,7 @@ "SYNCHRONIZATION_IN_PROGRESS": "Synchronization in progress", "SYNCHRONIZATION_ITEMS_LEFT": "Remaining items: %1", "SYSTEM": "System", + "TASK_MAMNAGER_WINDOW_TITLE": "Task Manager", "TECHNICAL_DETAILS": "Technical details:", "TYPE": "Type", "UNAUTHORIZED": "Incorrect credentials", @@ -374,6 +392,7 @@ "UNSELECT_ALL": "Unselect all", "UNSYNC": "Unsynchronize", "UPDATE": "Update", + "UPDATE_REQUESTED": "Update Document %1", "UPDATED": "Updated document", "UPDATES_LINK": "See updates", "UPDATING_VERSION": "Updating to %1", @@ -384,6 +403,7 @@ "USE_LIGHT_ICONS": "Use the light icons set", "USE_SENTRY": "Enable error reporting", "USERNAME": "Username", + "VALIDATE_DOCUMENT": "Validate the Document %1", "WEB_AUTHENTICATION_WINDOW_TITLE": "Web Authentication", "WELCOME_MESSAGE": "Set your account and synchronize your files with the Nuxeo Platform.", "WINDOWS_ERROR_TITLE": "Document error", @@ -391,5 +411,6 @@ "WINDOWS_ERROR_OPENED_FOLDER": "The folder \"%1\" or its children is locked by another software. Please close it before %2 can process the deletion from your local storage.", "WRONG_CHANNEL": "You are running version %1 which is from the \"%2\" channel, but your current channel is \"%3\". Do you want to downgrade to the latest version of your current channel, or switch channels?", "WRONG_CHANNEL_HEADER": "Wrong channel detected.", + "YEARS": "Year(s)", "YES": "Yes" } diff --git a/nxdrive/data/icons/tasks.svg b/nxdrive/data/icons/tasks.svg new file mode 100644 index 0000000000..2fc24e9bd2 --- /dev/null +++ b/nxdrive/data/icons/tasks.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/nxdrive/data/icons/tasks_light.svg b/nxdrive/data/icons/tasks_light.svg new file mode 100644 index 0000000000..81d416f06e --- /dev/null +++ b/nxdrive/data/icons/tasks_light.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/nxdrive/data/qml/Main.qml b/nxdrive/data/qml/Main.qml index f9cf4c3afb..5691037f7f 100644 --- a/nxdrive/data/qml/Main.qml +++ b/nxdrive/data/qml/Main.qml @@ -5,6 +5,7 @@ import QtQuick 2.15 import QtQuick.Window 2.15 import SystrayWindow 1.0 import CustomWindow 1.0 +import "tasksManager" QtObject { @@ -82,4 +83,22 @@ QtObject { DirectTransfer { id: directTransfer } } + + property var taskManagerWindow: CustomWindow { + id: taskManagerWindow + minimumWidth: 500 + minimumHeight: 600 + objectName: "taskManagerWindow" + title: qsTr("TASK_MAMNAGER_WINDOW_TITLE").arg(APP_NAME) + tl.tr + width: taskManager.width; height: taskManager.height + visible: false + + signal setEngine(string uid) + signal setSection(int index) + + onSetEngine: taskManager.setEngine(uid) + onSetSection: taskManager.setSection(index) + + TaskManager { id: taskManager } + } } diff --git a/nxdrive/data/qml/Systray.qml b/nxdrive/data/qml/Systray.qml old mode 100755 new mode 100644 index 804a611dee..bd3352ade7 --- a/nxdrive/data/qml/Systray.qml +++ b/nxdrive/data/qml/Systray.qml @@ -24,6 +24,7 @@ Rectangle { function doUpdateCounts() { systrayContainer.syncingCount = api.get_syncing_count(accountSelect.getRole("uid")) systrayContainer.extraCount = api.get_last_files_count(accountSelect.getRole("uid")) - 10 + taskState.pendingTasksCount = api.tasks_remaining(accountSelect.getRole("uid")) } function updateCounts(force) { @@ -66,6 +67,7 @@ Rectangle { function onVisibleChanged() { contextMenu.visible = false fileList.contentY = 0 + taskState.pendingTasksCount = api.tasks_remaining(accountSelect.getRole("uid")) } } @@ -144,15 +146,34 @@ Rectangle { // Width management: systray width minus the 5 icon's width Layout.preferredWidth: systray.width - (accountIcon.width * 5) - // When picking an account, refresh the file list. + // When picking an account, refresh the file list and tasks list. onActivated: { getLastFiles(accountSelect.getRole("uid")) doUpdateCounts() + tasks_model.loadList(api.get_Tasks_list(accountSelect.getRole("uid"), true, true), api.get_username(accountSelect.getRole("uid"))) } } } - // Icon 2: open remote server's URL + // Icon 2: Show pending Task list + IconLabel { + icon: MdiFont.Icon.minus + Image { + source: "../icons/tasks.svg" + anchors.fill: parent + } + color: "#FFFFFF" + enabled: feat_tasks_management.enabled + opacity: feat_tasks_management.enabled ? 1.0 : 0.2 + onClicked: { + tasks_model.loadList(api.get_Tasks_list(accountSelect.getRole("uid"), false, true), api.get_username(accountSelect.getRole("uid"))) + api.open_tasks_window(accountSelect.getRole("uid")) + } + //tooltip: api.get_hostname_from_url(accountSelect.getRole("server_url")) + tooltip: qsTr("HANDLE_TASKS") + tl.tr + } + + // Icon 3: open remote server's URL IconLabel { icon: MdiFont.Icon.nuxeo iconColor: secondaryIcon @@ -160,7 +181,7 @@ Rectangle { tooltip: api.get_hostname_from_url(accountSelect.getRole("server_url")) } - // Icon 3: open local sync root folder + // Icon 4: open local sync root folder IconLabel { icon: MdiFont.Icon.folder iconColor: secondaryIcon @@ -170,7 +191,7 @@ Rectangle { opacity: feat_synchronization.enabled ? 1.0 : 0.5 } - // Icon 4: open the Direct Transfer window + // Icon 5: open the Direct Transfer window IconLabel { icon: MdiFont.Icon.directTransfert iconColor: secondaryIcon @@ -180,7 +201,7 @@ Rectangle { opacity: feat_direct_transfer.enabled ? 1.0 : 0.5 } - // Icon 5: sub-menu + // Icon 6: sub-menu IconLabel { id: settingsContainer icon: MdiFont.Icon.dotsVertical @@ -251,6 +272,29 @@ Rectangle { } } + SystrayStatusTasks { + id: taskState + + property int pendingTasksCount: api.tasks_remaining(accountSelect.getRole("uid")) + + state: "pending_tasks" + visible:taskState.pendingTasksCount > 0 && feat_tasks_management.enabled + color: progressFilledLight + states: [ + State { + name: "pending_tasks" + PropertyChanges { + target: taskState + text: qsTr("PENDING_TASK_REVIEWS") + onClicked: { + tasks_model.loadList(api.get_Tasks_list(accountSelect.getRole("uid"), false, true), api.get_username(accountSelect.getRole("uid"))) + api.open_tasks_window(accountSelect.getRole("uid")) + } + } + } + ] + } + // Sync status (items remaining to sync, or small text when sync is over) SystrayStatus { id: syncState diff --git a/nxdrive/data/qml/SystrayStatusTasks.qml b/nxdrive/data/qml/SystrayStatusTasks.qml new file mode 100644 index 0000000000..ebb06dbe86 --- /dev/null +++ b/nxdrive/data/qml/SystrayStatusTasks.qml @@ -0,0 +1,56 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "icon-font/Icon.js" as MdiFont + +HoverRectangle { + id: control + color: uiBackground + Layout.fillWidth: true; height: 40 + opacity: 1 + + property string icon: MdiFont.Icon.check + property string text + property string subText + property bool textVisible: true + property bool anim: false + property int progress: 0 + + RowLayout { + anchors.fill: parent + + Item { Layout.fillWidth: true } + + ColumnLayout { + spacing: 0 + + ScaledText { + id: statusText + text: control.text + color: lightTheme + visible: control.textVisible + Layout.alignment: Qt.AlignRight + } + + ScaledText { + id: statusSubText + text: control.subText + visible: text + color: statusText.color + font.pointSize: point_size * 0.8 + opacity: 0.8 + Layout.alignment: Qt.AlignRight + } + } + + IconLabel { + id: statusIcon + Image { + source: "../icons/tasks_light.svg" + } + Layout.alignment: Qt.AlignRight + Layout.rightMargin: 30 + Layout.topMargin: -10 + } + } +} diff --git a/nxdrive/data/qml/tasksManager/TaskListItem.qml b/nxdrive/data/qml/tasksManager/TaskListItem.qml new file mode 100644 index 0000000000..6d372c9bf4 --- /dev/null +++ b/nxdrive/data/qml/tasksManager/TaskListItem.qml @@ -0,0 +1,82 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import "../" + +Button { + id: control + width: 450 + height: 90 + //Layout.leftPadding: 5 + leftPadding: 5 + + contentItem: Rectangle{ + ScaledText { + id: buttonText1 + text: api.get_text(control.text, "wf_name") + font.pointSize: point_size * 1.2 + + leftPadding: 25 + rightPadding: 5 + topPadding: 10 + anchors { + centerIn: buttonBackground + } + } + ScaledText { + id: buttonText2 + text: api.get_text(control.text, "name") + font.pointSize: point_size * 1.1 + color: buttonGrayText + leftPadding: 25 + rightPadding: 5 + topPadding: 30 + anchors { + centerIn: buttonBackground + } + } + ScaledText { + id: buttonText3 + text: api.get_text(control.text, "due") + + leftPadding: 25 + rightPadding: 5 + topPadding: 50 + anchors { + centerIn: buttonBackground + } + color: api.text_red(control.text) ? buttonRedText : buttonGreenText + } + ScaledText { + id: buttonText4 + text: api.get_text(control.text, "model") + + leftPadding: 25 + rightPadding: 5 + topPadding: 65 + anchors { + centerIn: buttonBackground + } + //color: api.text_red(control.text) ? buttonRedText : buttongreenText + } + } + + background: Rectangle { + id: buttonBackground + width: 450 + height: buttonText1.text ? 95 : 0 + radius: 10 + + border { + width: 1 + color: grayBorder + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + width: 450 + onPressed: mouse.accepted = false + } +} diff --git a/nxdrive/data/qml/tasksManager/TaskManager.qml b/nxdrive/data/qml/tasksManager/TaskManager.qml new file mode 100644 index 0000000000..06bbde612f --- /dev/null +++ b/nxdrive/data/qml/tasksManager/TaskManager.qml @@ -0,0 +1,180 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.15 +import "../icon-font/Icon.js" as MdiFont +import "../" + +Rectangle { + id: taskManager + anchors.fill: parent + + property string engineUid: "" + property bool showSelfTasksList: false + + signal setEngine(string uid) + signal setSection(int index) + + onSetEngine: { + engineUid = uid + } + onSetSection: { + bar.currentIndex = index + showSelfTasksList = false + no_tasks_available_text.visible = false + if (tasks_model.model.rowCount() < 1) { + showSelfTasks() + } + if (tasks_model.self_model.rowCount() < 1) { + showTasks() + } + } + function showTasks() { + showSelfTasksList = false + bar.currentIndex = 0 + no_tasks_available_text.visible = ( (bar.currentIndex == 0 && tasks_model.model.rowCount() < 1)||(bar.currentIndex == 1 && tasks_model.self_model.rowCount() < 1) ) ? true : false + } + function showSelfTasks() { + showSelfTasksList = true + bar.currentIndex = 1 + no_tasks_available_text.visible = ( (bar.currentIndex == 0 && tasks_model.model.rowCount() < 1)||(bar.currentIndex == 1 && tasks_model.self_model.rowCount() < 1) ) ? true : false + } + + Rectangle { + id: refreshButton + objectName: "refresh" + + height: 0 + width: parent.width + color: refreshBackground + RowLayout { + width: parent.width + height: parent.height + ScaledText { + id: refreshText + text: qsTr("REFRESH_AVAILABLE") + tl.tr + font.pointSize: point_size * 1.2 + leftPadding: 25 + rightPadding: 5 + topPadding: 3 + } + NuxeoButton { + text: qsTr("REFRESH") + tl.tr + Layout.alignment: Qt.AlignRight + Layout.rightMargin: 30 + primary: false + onClicked: { + tasks_model.loadList(api.get_Tasks_list(engineUid, false, true), api.get_username(engineUid)) + refreshButton.height = 0 + } + } + } + } + + TabBar { + id: bar + width: parent.width + height: 50 + spacing: 0 + anchors.top: refreshButton.bottom + SettingsTab { + text: qsTr("PENDING_TASKS") + tl.tr + barIndex: bar.currentIndex; + index: 0 + anchors.top: parent.top + onClicked: { + showTasks() + } + } + SettingsTab { + text: qsTr("SELF_CREATED_TASKS") + tl.tr + barIndex: bar.currentIndex; + index: 1 + anchors.top: parent.top + onClicked: { + showSelfTasks() + } + } + } + + Component { + id: tasksDelegate + Row { + spacing: 50 + anchors.bottomMargin: 40 + + TaskListItem { + Layout.alignment: Qt.AlignLeft + text: task["task_details"] + onClicked: { + api.on_clicked_open_task(engineUid, task["task_ids"]) + api.close_tasks_window() + } + } + } + } + + ListView { + anchors.fill: parent + anchors.top: bar.bottom + anchors.bottomMargin: 52 + width: parent.width + anchors.topMargin: refreshButton.height == 0 ? 60 : 90 + model: tasks_model.model + delegate: tasksDelegate + visible: !showSelfTasksList + topMargin: 5 + bottomMargin: 10 + leftMargin: 25 + rightMargin: 10 + spacing: 25 + clip: true + ScrollBar.vertical: ScrollBar { + active: true + } + } + + Component { + id: selftasksDelegate + Row { + spacing: 50 + anchors.bottomMargin: 40 + + TaskListItem { + Layout.alignment: Qt.AlignLeft + text: task["self_task_details"] + onClicked: { + api.on_clicked_open_task(engineUid, task["task_ids"]) + api.close_tasks_window() + } + } + } + } + + ListView { + anchors.fill: parent + anchors.bottomMargin: 52 + width: parent.width + anchors.topMargin: refreshButton.height == 0 ? 60 : 90 + model: tasks_model.self_model + delegate: selftasksDelegate + visible: showSelfTasksList + topMargin: 5 + bottomMargin: 10 + leftMargin: 25 + rightMargin: 10 + spacing: 25 + clip: true + ScrollBar.vertical: ScrollBar { + active: true + } + } + Text { + id: no_tasks_available_text + //visible: ( (bar.currentIndex == 0 && tasks_model.model.rowCount() < 1)||(bar.currentIndex == 1 && tasks_model.self_model.rowCount() < 1) ) ? true : false + text: qsTr("NO_TASKS_AVAILABLE") + tl.tr + font.pointSize: point_size * 1.2 + anchors.centerIn: parent + width: parent.Width + } +} diff --git a/nxdrive/engine/engine.py b/nxdrive/engine/engine.py index 6e3a5e22f8..4fc2478f12 100644 --- a/nxdrive/engine/engine.py +++ b/nxdrive/engine/engine.py @@ -116,6 +116,7 @@ class Engine(QObject): directTransferNewFolderError = pyqtSignal() directTransferNewFolderSuccess = pyqtSignal(str) directTransferSessionFinished = pyqtSignal(str, str, str) + displayPendingTask = pyqtSignal(str, str, str, str) type = "NXDRIVE" # Folder locker - LocalFolder processor can prevent @@ -747,6 +748,24 @@ def get_metadata_url(self, remote_ref: str, /, *, edit: bool = False) -> str: } return urls[self.force_ui or self.wui] + def get_task_url(self, remote_ref: str, /, *, edit: bool = False) -> str: + """ + Build the task's URL based on the server's UI. + Default is Web-UI. In case of unknown UI, use the default value. + :param remote_ref: The task remote reference (UID) of the + task we want to show metadata. + :param edit: Show the metadata edit page instead of the task. + :return: The complete URL. + """ + repo = self.remote.client.repository + page = ("view_documents", "view_drive_metadata")[edit] + + urls = { + "jsf": f"{self.server_url}tasks/{repo}/{remote_ref}/{page}", + "web": f"{self.server_url}ui#!/tasks/{remote_ref}", + } + return urls[self.force_ui or self.wui] + def is_syncing(self) -> bool: return self._sync_started @@ -1581,6 +1600,11 @@ def get_user_full_name(self, userid: str, /, *, cache_only: bool = False) -> str return full_name + def send_task_notification( + self, task_id: str, remote_path: str, notification_title: str, / + ) -> None: + self.displayPendingTask.emit(self.uid, task_id, remote_path, notification_title) + @dataclass class ServerBindingSettings: diff --git a/nxdrive/feature.py b/nxdrive/feature.py index 2d64aeca14..52ce7124b2 100644 --- a/nxdrive/feature.py +++ b/nxdrive/feature.py @@ -31,6 +31,7 @@ direct_edit=True, direct_transfer=True, document_type_selection=False, + tasks_management=False, s3=False, ) diff --git a/nxdrive/gui/api.py b/nxdrive/gui/api.py index 76aefdb9e4..a0f6f3d91a 100644 --- a/nxdrive/gui/api.py +++ b/nxdrive/gui/api.py @@ -1,3 +1,5 @@ +# flake8: noqa + import json from dataclasses import asdict from logging import getLogger @@ -21,6 +23,7 @@ TransferStatus, ) from ..dao.engine import EngineDAO +from ..engine.engine import Engine from ..exceptions import ( AddonForbiddenError, AddonNotInstalledError, @@ -46,6 +49,7 @@ force_decode, get_date_from_sqlite, get_default_local_folder, + get_task_type, normalized_path, save_config, sizeof_fmt, @@ -74,6 +78,9 @@ def __init__(self, application: "Application", /) -> None: self.openAuthenticationDialog.connect( self.application.open_authentication_dialog ) + self.last_task_list = "" + self.engine_changed = False + self.hide_refresh_button = True def _json_default(self, obj: Any, /) -> Any: export = getattr(obj, "export", None) @@ -93,7 +100,7 @@ def _export_formatted_state( if not state: return {} - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if not engine: return {} @@ -119,7 +126,7 @@ def _export_formatted_state( @pyqtSlot(str, int, result=list) def get_last_files(self, uid: str, number: int, /) -> List[Dict[str, Any]]: """Return the last files transferred (see EngineDAO).""" - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if not engine: return [] return [s.export() for s in engine.dao.get_last_files(number)] @@ -128,7 +135,7 @@ def get_last_files(self, uid: str, number: int, /) -> List[Dict[str, Any]]: def get_last_files_count(self, uid: str, /) -> int: """Return the count of the last files transferred (see EngineDAO).""" count = 0 - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: count = engine.dao.get_last_files_count(duration=60) return count @@ -217,7 +224,7 @@ def get_completed_sessions_items(self, dao: EngineDAO, /) -> List[Dict[str, Any] @pyqtSlot(str, result=int) def get_active_sessions_count(self, uid: str, /) -> int: """Return the count of active sessions items.""" - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: return engine.dao.get_count( f"status IN ({TransferStatus.ONGOING.value}, {TransferStatus.PAUSED.value})", @@ -228,7 +235,7 @@ def get_active_sessions_count(self, uid: str, /) -> int: @pyqtSlot(str, result=int) def get_completed_sessions_count(self, uid: str, /) -> int: """Return the count of completed sessions items.""" - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: return engine.dao.get_count( f"status IN ({TransferStatus.CANCELLED.value}, {TransferStatus.DONE.value})", @@ -302,7 +309,7 @@ def cancel_session(self, engine_uid: str, uid: int, /) -> None: @pyqtSlot(str, str) def show_metadata(self, uid: str, ref: str, /) -> None: self.application.hide_systray() - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: path = engine.local.abspath(Path(ref)) self.application.show_metadata(path) @@ -310,7 +317,7 @@ def show_metadata(self, uid: str, ref: str, /) -> None: @pyqtSlot(str, result=list) def get_unsynchronizeds(self, uid: str, /) -> List[Dict[str, Any]]: result = [] - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: for conflict in engine.dao.get_unsynchronizeds(): result.append(self._export_formatted_state(uid, state=conflict)) @@ -319,7 +326,7 @@ def get_unsynchronizeds(self, uid: str, /) -> List[Dict[str, Any]]: @pyqtSlot(str, result=list) def get_conflicts(self, uid: str, /) -> List[Dict[str, Any]]: result = [] - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: for conflict in engine.get_conflicts(): result.append(self._export_formatted_state(uid, state=conflict)) @@ -328,7 +335,7 @@ def get_conflicts(self, uid: str, /) -> List[Dict[str, Any]]: @pyqtSlot(str, result=list) def get_errors(self, uid: str, /) -> List[Dict[str, Any]]: result = [] - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: for error in engine.dao.get_errors(): result.append(self._export_formatted_state(uid, state=error)) @@ -370,7 +377,7 @@ def generate_csv(self, session_id: str, engine_uid: str) -> bool: def open_direct_transfer(self, uid: str, /) -> None: self.application.hide_systray() - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if not engine: return @@ -383,7 +390,7 @@ def open_direct_transfer(self, uid: str, /) -> None: def open_server_folders(self, uid: str, /) -> None: """Hide the systray and show the server folders dialog.""" self.application.hide_systray() - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if not engine: return @@ -397,7 +404,7 @@ def get_hostname_from_url(self, url: str, /) -> str: @pyqtSlot(str) def open_remote_server(self, uid: str, /) -> None: self.application.hide_systray() - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: engine.open_remote() @@ -416,7 +423,7 @@ def open_local(self, uid: str, path: str, /) -> None: if not uid: self._manager.open_local_file(filepath) else: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: filepath = engine.local.abspath(filepath) self._manager.open_local_file(filepath) @@ -448,7 +455,7 @@ def open_document(self, engine_uid: str, doc_pair_id: int, /) -> None: @pyqtSlot(str) def show_conflicts_resolution(self, uid: str, /) -> None: self.application.hide_systray() - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: self.application.show_conflicts_resolution(engine) @@ -476,7 +483,7 @@ def get_update_url(self) -> str: @pyqtSlot(str) def web_update_token(self, uid: str, /) -> None: try: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if not engine: self.setMessage.emit("CONNECTION_UNKNOWN", "error") return @@ -558,7 +565,9 @@ def get_disk_space_info_to_width( - Size of space used by other applications converted to percentage of the width. - Global size of synchronized files converted to percentage of the width. """ - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) + if not engine: + return [] synced = engine.dao.get_global_size() if engine else 0 used, free = disk_space(path) @@ -576,7 +585,7 @@ def get_disk_space_info_to_width( def _balance_percents(self, result: Dict[str, float], /) -> Dict[str, float]: """Return an altered version of the dict in which no value is under a minimum threshold.""" - result = {k: v for k, v in sorted(result.items(), key=lambda item: item[1])} + result = dict(sorted(result.items(), key=lambda item: item[1])) keys = list(result) min_threshold = 10 data = 0.0 @@ -614,7 +623,10 @@ def _balance_percents(self, result: Dict[str, float], /) -> Dict[str, float]: @pyqtSlot(str, result=str) def get_drive_disk_space(self, uid: str, /) -> str: """Fetch the global size of synchronized files and return a formatted version.""" - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) + if not engine: + return "" + synced = engine.dao.get_global_size() if engine else 0 return sizeof_fmt(synced, suffix=Translator.get("BYTE_ABBREV")) @@ -627,7 +639,10 @@ def get_free_disk_space(self, path: str, /) -> str: @pyqtSlot(str, str, result=str) def get_used_space_without_synced(self, uid: str, path: str, /) -> str: """Fetch the size of space used by other applications and return a formatted version.""" - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) + if not engine: + return "" + synced = engine.dao.get_global_size() if engine else 0 used, _ = disk_space(path) return sizeof_fmt(used - synced, suffix=Translator.get("BYTE_ABBREV")) @@ -638,7 +653,7 @@ def unbind_server(self, uid: str, purge: bool, /) -> None: @pyqtSlot(str) def filters_dialog(self, uid: str, /) -> None: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: self.application.show_filters(engine) @@ -848,7 +863,7 @@ def web_authentication( @pyqtSlot(str, str, result=bool) def set_server_ui(self, uid: str, server_ui: str, /) -> bool: log.info(f"Setting ui to {server_ui}") - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if not engine: self.setMessage.emit("CONNECTION_UNKNOWN", "error") return False @@ -891,7 +906,7 @@ def set_deletion_behavior(self, behavior: str, /) -> None: @pyqtSlot(str, result=bool) def has_invalid_credentials(self, uid: str, /) -> bool: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) return engine.has_invalid_credentials() if engine else False # Authentication section @@ -1049,7 +1064,7 @@ def is_paused(self) -> bool: @pyqtSlot(str, result=int) def get_syncing_count(self, uid: str, /) -> int: count = 0 - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: count = engine.dao.get_syncing_count() return count @@ -1058,25 +1073,25 @@ def get_syncing_count(self, uid: str, /) -> int: @pyqtSlot(str, int) def resolve_with_local(self, uid: str, state_id: int, /) -> None: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: engine.resolve_with_local(state_id) @pyqtSlot(str, int) def resolve_with_remote(self, uid: str, state_id: int, /) -> None: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: engine.resolve_with_remote(state_id) @pyqtSlot(str, int) def retry_pair(self, uid: str, state_id: int, /) -> None: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: engine.retry_pair(state_id) @pyqtSlot(str, int, str) def ignore_pair(self, uid: str, state_id: int, reason: str, /) -> None: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: engine.ignore_pair(state_id, reason) @@ -1084,7 +1099,7 @@ def ignore_pair(self, uid: str, state_id: int, reason: str, /) -> None: def open_remote(self, uid: str, remote_ref: str, remote_name: str, /) -> None: log.info(f"Should open {remote_name!r} ({remote_ref!r})") try: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: engine.open_edit(remote_ref, remote_name) except OSError: @@ -1096,7 +1111,7 @@ def open_remote_document( ) -> None: log.info(f"Should open remote document {remote_path!r} ({remote_ref!r})") try: - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) if engine: url = engine.get_metadata_url(remote_ref) engine.open_remote(url=url) @@ -1106,5 +1121,115 @@ def open_remote_document( @pyqtSlot(str, str, result=str) def get_remote_document_url(self, uid: str, remote_ref: str, /) -> str: """Return the URL to a remote document based on its reference.""" - engine = self._manager.engines.get(uid) + engine = self._get_engine(uid) return engine.get_metadata_url(remote_ref) if engine else "" + + def _get_engine(self, engine_uid: str, /) -> Engine: + return self._manager.engines.get(engine_uid) + + @pyqtSlot(str, str, result=str) + def get_text(self, details: str, ret: str, /) -> str: + details = details.replace("\\", "") + details = details.replace("'", '"') + try: + details = json.loads(details) + return details.get(ret) + except Exception as e: + return "" + + @pyqtSlot(str, result=bool) + def text_red(self, text: str, /) -> bool: + ago = Translator.get("AGO") + return ago in text + + @pyqtSlot(str) + def open_tasks_window(self, uid: str, /) -> None: + self.application.hide_systray() + self.application.show_tasks_window(uid) + + @pyqtSlot() + def close_tasks_window(self, /) -> None: + self.application.close_tasks_window() + + @pyqtSlot(str, result=int) + def tasks_remaining(self, uid: str, /) -> int: + """Return pending tasks count for Drive notification.""" + engine = self._get_engine(uid) + if engine: + tasks = self._fetch_tasks(engine) + return len(tasks) + log.info("Engine not available") + return 0 + + @pyqtSlot(str, bool, bool, result=list) + def get_Tasks_list( + self, engine_uid: str, engine_changed: bool, hide_refresh_button: bool, / + ) -> list: + engine = self._get_engine(engine_uid) + tasks_list = self._fetch_tasks(engine) + for task in tasks_list: + try: + doc_id = task.targetDocumentIds[0]["id"] + doc_info = self.get_document_details(engine_uid, doc_id) + task.name = doc_info.name + except Exception as e: + log.info(f"Error occurred while fetching document info {e!r}") + task.name = "Unknown Document" + + task.directive = get_task_type(task.directive) + + wf_name = "" + for char in task.workflowModelName: + wf_name = f"{wf_name} {char}" if char.isupper() else f"{wf_name}{char}" + task.workflowModelName = wf_name[1:] + + if self.last_task_list == "": + self.last_task_list = str(tasks_list) + if engine_changed: + self.engine_changed = True + self.hide_refresh_button = hide_refresh_button + if hide_refresh_button: + self.application.show_hide_refresh_button(0) + return tasks_list + + @pyqtSlot(str, result=str) + def get_username(self, engine_uid: str, /) -> str: + engine = self._get_engine(engine_uid) + return engine.remote_user + + @pyqtSlot(str, result=list) + def get_document_details(self, engine_uid: str, doc_id: str, /) -> int: + engine = self._get_engine(engine_uid) + if not engine: + log.info("engine not available") + return [] + return engine.remote.get_info(doc_id, fetch_parent_uid=False) + + @pyqtSlot(object) + def fetch_pending_tasks(self, engine: Engine, /) -> None: + data = self._fetch_tasks(engine) + if len(data) > 0: + for task in data: + engine.fetch_pending_task_list(task.id) + + def _fetch_tasks(self, engine: Engine) -> Any: + return self.application.fetch_pending_tasks(engine) + + @pyqtSlot(str, str) + def on_clicked_open_task(self, engine_uid: str, task_id: str) -> None: + engine = self._get_engine(engine_uid) + if not engine: + return + self.application.open_task(engine, task_id) + + @pyqtSlot(str, str, str) + def display_pending_task( + self, uid: str, remote_ref: str, remote_path: str, / + ) -> None: + log.info(f"Should open remote document ({remote_path!r})") + try: + if engine := self._manager.engines.get(uid): + url = engine.get_task_url(remote_ref) + engine.open_remote(url=url) + except Exception as exec: + log.exception(f"Remote task cannot be opened: {exec}") diff --git a/nxdrive/gui/application.py b/nxdrive/gui/application.py index 609768aed5..eef2ac55c0 100644 --- a/nxdrive/gui/application.py +++ b/nxdrive/gui/application.py @@ -11,6 +11,8 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union from urllib.parse import unquote_plus, urlparse +from nxdrive.client.workflow import Workflow + from ..behavior import Behavior from ..constants import ( APP_NAME, @@ -47,6 +49,7 @@ QLocalServer, QLocalSocket, QMessageBox, + QObject, QQmlApplicationEngine, QQmlContext, QQuickView, @@ -94,6 +97,7 @@ FeatureModel, FileModel, LanguageModel, + TasksModel, TransferModel, ) @@ -216,6 +220,11 @@ def __init__(self, manager: "Manager", *args: Any) -> None: if MAC: self._setup_notification_center() + self.added_user_engine_list = [str] + self.workflow = None + # Initiate workflow when drive starts if tasks managemnt feature is enable + self.init_workflow() + # Application update self.manager.updater.appUpdated.connect(self.quit) self.manager.updater.serverIncompatible.connect(self._server_incompatible) @@ -259,6 +268,7 @@ def _shutdown(self) -> None: del self.settings_window del self.systray_window del self.direct_transfer_window + del self.task_manager_window else: del self.app_engine @@ -273,6 +283,7 @@ def init_gui(self) -> None: self.document_type_selection_feature_model = FeatureModel( Feature.document_type_selection ) + self.tasks_management_feature_model = FeatureModel(Feature.tasks_management) self.conflicts_model = FileModel(self.translate) self.errors_model = FileModel(self.translate) self.engine_model = EngineModel(self) @@ -283,6 +294,7 @@ def init_gui(self) -> None: self.file_model = FileModel(self.translate) self.ignoreds_model = FileModel(self.translate) self.language_model = LanguageModel() + self.tasks_model = TasksModel(self.translate) self.add_engines(list(self.manager.engines.values())) self.engine_model.statusChanged.connect(self.update_status) @@ -328,6 +340,8 @@ def init_gui(self) -> None: QUrl.fromLocalFile(str(find_resource("qml", file="DirectTransfer.qml"))) ) + self.create_custom_window_for_task_manager() + flags |= qt.Popup else: self.app_engine = QQmlApplicationEngine() @@ -342,6 +356,7 @@ def init_gui(self) -> None: self.direct_transfer_window = root.findChild( CustomWindow, "directTransferWindow" ) + self.task_manager_window = root.findChild(CustomWindow, "taskManagerWindow") if LINUX: flags |= qt.Drawer @@ -372,6 +387,46 @@ def init_gui(self) -> None: ) self.manager.featureUpdate.connect(self._update_feature_state) + self.last_engine_uid = "" + + def create_custom_window_for_task_manager(self) -> None: + # Task Manager + self.task_manager_window = CustomWindow() + self.task_manager_window.setMinimumWidth(500) + self.task_manager_window.setMinimumHeight(600) + self._fill_qml_context(self.task_manager_window.rootContext()) + self.task_manager_window.setSource( + QUrl.fromLocalFile( + str(find_resource("qml/tasksManager", file="TaskManager.qml")) + ) + ) + + def init_workflow(self) -> None: + if not self.manager.engines: + return + if Feature.tasks_management: + self.workflow = Workflow() + self.update_workflow() + + def update_workflow(self) -> None: + try: + if Feature.tasks_management and self.workflow: + for engine in self.manager.engines.copy().values(): + if engine.uid not in self.added_user_engine_list: + self.update_workflow_user_engine_list(False, engine.uid) + self.workflow.get_pending_tasks(engine) + """ + if not called_from_init or engine.uid == ??: + self.workflow.get_pending_tasks(engine) + """ + except AttributeError: + log.debug("Unalbe to fetch the TASKS") + + def update_workflow_user_engine_list(self, delete: bool, uid: str, /) -> None: + if delete: + self.added_user_engine_list.remove(uid) + else: + self.added_user_engine_list.append(uid) def _update_feature_state(self, name: str, value: bool, /) -> None: """Check if the feature model exists from *name* then update it with *value*.""" @@ -384,6 +439,17 @@ def _update_feature_state(self, name: str, value: bool, /) -> None: if feature.restart_needed: self.manager.restartNeeded.emit() + if feature == self.tasks_management_feature_model: + if feature.enabled: + # Check for tasks while enabling the feature + self.init_workflow() + self.manager._create_workflow_worker() + else: + # clean user_task_list if we are disabling the tasks_management feature + self.added_user_engine_list = [] + Workflow.user_task_list = {} + self.manager.stop_workflow_worker() + def _center_on_screen(self, window: QQuickView, /) -> None: """Display and center the window on the screen.""" # Display the window @@ -446,6 +512,7 @@ def _fill_qml_context(self, context: QQmlContext, /) -> None: context.setContextProperty("FileModel", self.file_model) context.setContextProperty("IgnoredsModel", self.ignoreds_model) context.setContextProperty("languageModel", self.language_model) + context.setContextProperty("tasks_model", self.tasks_model) context.setContextProperty("api", self.api) context.setContextProperty("application", self) context.setContextProperty("currentLanguage", self.current_language()) @@ -471,6 +538,9 @@ def _fill_qml_context(self, context: QQmlContext, /) -> None: context.setContextProperty( "feat_document_type_selection", self.document_type_selection_feature_model ) + context.setContextProperty( + "feat_tasks_management", self.tasks_management_feature_model + ) context.setContextProperty( "feat_synchronization", self.synchronization_feature_model ) @@ -508,6 +578,9 @@ def _fill_qml_context(self, context: QQmlContext, /) -> None: "secondaryBgHover": "#0052CC", "secondaryButtonText": "#0066FF", "secondaryButtonTextHover": "#FFFFFF", + "buttonRedText": "#de350b", + "buttonGreenText": "#057153", + "buttonGrayText": "#444747", "primaryIcon": "#161616", "secondaryIcon": "#525252", "disabledIcon": "#C6C6C6", @@ -533,6 +606,7 @@ def _fill_qml_context(self, context: QQmlContext, /) -> None: "warningContent": "#FF9E00", "lightTheme": "#FFFFFF", "darkShadow": "#333333", + "refreshBackground": "#d0d1d6", } for name, value in colors.items(): @@ -889,6 +963,8 @@ def show_settings(self, section: str, /) -> None: @pyqtSlot() def show_systray(self) -> None: + self.close_tasks_window() + self.systray_window.close() icon = self.tray_icon.geometry() if not icon or icon.isEmpty(): @@ -1991,3 +2067,39 @@ def errors_choice(state: Qt.CheckState) -> None: (Options.nxdrive_home / "metrics.state").write_text( "\n".join(states), encoding="utf-8" ) + + @pyqtSlot(str) + def show_tasks_window(self, engine_uid: str, /) -> None: + """Display the Tasks window.""" + self._window_root(self.task_manager_window).setEngine.emit(engine_uid) + self._window_root(self.task_manager_window).setSection.emit(0) + self._center_on_screen(self.task_manager_window) + + @pyqtSlot() + def close_tasks_window(self) -> None: + """Close the Tasks window.""" + if self.task_manager_window: + self.task_manager_window.close() + + @pyqtSlot(int) + def show_hide_refresh_button(self, height: int, /) -> None: + """Shows and Hides the refresh button of task window""" + if self.task_manager_window: + r_button = self.task_manager_window.findChild(QObject, "refresh") + r_button.setProperty("height", height) + + def open_task(self, engine: Engine, task_id: str) -> None: + endpoint = "/ui/#!/tasks/" + url = f"{engine.server_url}{endpoint}{task_id}" + webbrowser.open(url) + + def fetch_pending_tasks(self, engine: Engine, /) -> list: + remote = engine.remote + user = {"userId": remote.user_id} + try: + tasks = remote.tasks.get(user) + except Exception as e: + log.info(f"Unable to fetch tasks due to: {e!r}") + tasks = [] + self.last_engine_uid = engine.uid + return tasks diff --git a/nxdrive/gui/view.py b/nxdrive/gui/view.py old mode 100755 new mode 100644 index b0fdbceec9..535ac7c5e6 --- a/nxdrive/gui/view.py +++ b/nxdrive/gui/view.py @@ -1,3 +1,4 @@ +from datetime import date, datetime from functools import partial from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple @@ -10,6 +11,8 @@ QAbstractListModel, QModelIndex, QObject, + QStandardItem, + QStandardItemModel, Qt, pyqtProperty, pyqtSignal, @@ -71,10 +74,12 @@ def addEngine(self, uid: str, /, *, parent: QModelIndex = QModelIndex()) -> None self.endInsertRows() self._connect_engine(self.application.manager.engines[uid]) self.engineChanged.emit() + self.application.update_workflow() def removeEngine(self, uid: str, /) -> None: idx = self.engines_uid.index(uid) self.removeRows(idx, 1) + self.application.update_workflow_user_engine_list(True, uid) self.engineChanged.emit() def data(self, index: QModelIndex, role: int, /) -> str: @@ -830,3 +835,94 @@ def enabled(self, enabled: bool) -> None: @property def restart_needed(self) -> bool: return self._restart_needed + + +class TasksModel(QObject): + TASK_ROLE = qt.UserRole # + 1 + TASK_ID = qt.UserRole + 1 + + def __init__(self, translate: Callable, /, *, parent: QObject = None) -> None: + super().__init__(parent) + # self.tr = translate + self.taskmodel = QStandardItemModel() + self.taskmodel.setItemRoleNames( + { + self.TASK_ROLE: b"task", + self.TASK_ID: b"task_id", + } + ) + + self.self_taskmodel = QStandardItemModel() + self.self_taskmodel.setItemRoleNames( + { + self.TASK_ROLE: b"task", + self.TASK_ID: b"task_id", + } + ) + + def get_model(self) -> QStandardItemModel: + return self.taskmodel + + def get_self_model(self) -> QStandardItemModel: + return self.self_taskmodel + + model = pyqtProperty(QObject, fget=get_model, constant=True) + self_model = pyqtProperty(QObject, fget=get_self_model, constant=True) + + @pyqtSlot(list, str) + def loadList(self, tasks_list: list, username: str, /) -> None: + self.taskmodel.clear() + self.self_taskmodel.clear() + + for task in tasks_list: + diff = self.due_date_calculation(task.dueDate) + translated_due = Translator.get("DUE") + details = str( + { + "wf_name": task.directive, + "name": task.name, + "due": f"{translated_due}: {diff}", + "model": task.workflowModelName, + } + ) + if task.actors[0]["id"] == username: + data = { + "self_task_details": details, + "task_ids": task.id, + } + self.add_row(data, self.TASK_ROLE, True) + else: + data = { + "task_details": details, + "task_ids": task.id, + } + self.add_row(data, self.TASK_ROLE, False) + + def add_row(self, task: dict, role: int, self_task: bool) -> None: + item = QStandardItem() + item.setData(task, role) + + if self_task: + self.self_taskmodel.appendRow(item) + else: + self.taskmodel.appendRow(item) + + def due_date_calculation(self, dueDate: str) -> str: + due_date = datetime.strptime(dueDate, "%Y-%m-%dT%H:%M:%S.%f%z").date() + now = date.today() + time_remaing = Translator.get("DAYS") + diff = (due_date - now).days + if diff > 364 or diff < -364: + diff /= 365 + time_remaing = Translator.get("YEARS") + elif diff > 29 or diff < -29: + diff /= 30 + time_remaing = Translator.get("MONTHS") + ago = Translator.get("AGO") + translated_in = Translator.get("IN") + diff = int(diff) + return ( + f"{-diff} {time_remaing} {ago}" + if diff < 0 + else f"{translated_in} {diff} {time_remaing}" + ) diff --git a/nxdrive/manager.py b/nxdrive/manager.py index b220164f02..4fd9707b95 100644 --- a/nxdrive/manager.py +++ b/nxdrive/manager.py @@ -52,7 +52,12 @@ from .objects import Binder, EngineDef, Metrics, Session from .options import DEFAULT_LOG_LEVEL_FILE, Options from .osi import AbstractOSIntegration -from .poll_workers import DatabaseBackupWorker, ServerOptionsUpdater, SyncAndQuitWorker +from .poll_workers import ( + DatabaseBackupWorker, + ServerOptionsUpdater, + SyncAndQuitWorker, + WorkflowWorker, +) from .qt.imports import QT_VERSION_STR, QObject, pyqtSignal, pyqtSlot from .updater import updater from .updater.constants import Login @@ -237,6 +242,11 @@ def __init__(self, home: Path, /) -> None: self.autolock_service = self._create_autolock_service() self.direct_edit = self._create_direct_edit() + self.delete_users_from_tasks_cache = [str] + + # Create the workflow worker if tasks_management feature is enable + self._create_workflow_worker() + def _save_or_load_proxy(self) -> "Proxy": if Options.proxy_server is not None: proxy = get_proxy("Manual", url=Options.proxy_server) @@ -405,6 +415,18 @@ def _create_db_backup_worker(self) -> None: if self.db_backup_worker: self.started.connect(self.db_backup_worker.thread.start) + def _create_workflow_worker(self) -> None: + if Feature.tasks_management: + self.workflow_worker = WorkflowWorker(self) + self.workflow_thread = self.workflow_worker.thread + self.started.connect(self.workflow_thread.start) + self.stopped.connect(self.workflow_thread.quit) + if not self.workflow_thread.isRunning(): + self.workflow_worker.start() + + def stop_workflow_worker(self) -> None: + self.workflow_worker.quit() + @if_frozen def _create_extension_listener(self) -> None: self._extension_listener = self.osi.get_extension_listener() @@ -929,6 +951,14 @@ def unbind_engine(self, uid: str, /, *, purge: bool = False) -> None: if not engine: return + """ + if Feature.tasks_management: + # Clean user tasks data in case we are unbinding the account. + self.workflow = Workflow(engine.remote) + self.workflow.clean_user_task_data(engine.remote.user_id) + """ + self.delete_users_from_tasks_cache.append(engine.remote.user_id) + engine.unbind() self.dao.delete_engine(uid) diff --git a/nxdrive/notification.py b/nxdrive/notification.py index 39a65199c7..f7d56e8bed 100644 --- a/nxdrive/notification.py +++ b/nxdrive/notification.py @@ -180,7 +180,6 @@ def send_notification(self, notification: Notification, /) -> None: else: self.dao.update_notification(notification) self._notifications[notification.uid] = notification - self.newNotification.emit(notification) def trigger_notification(self, uid: str, /) -> None: @@ -520,6 +519,39 @@ def __init__(self, engine_uid: str, /) -> None: ) +class DisplayPendingTask(Notification): + """Display a notification for pending tasks""" + + def __init__( + self, + engine_uid: str, + remote_ref: str, + remote_path: str, + notification_title: str, + /, + ) -> None: + values = [remote_path] + super().__init__( + uid=notification_title, + title=(" ".join(notification_title.split("_"))).title(), + description=Translator.get(notification_title, values=values), + level=Notification.LEVEL_INFO, + flags=( + Notification.FLAG_PERSISTENT + | Notification.FLAG_BUBBLE + | Notification.FLAG_ACTIONABLE + | Notification.FLAG_DISCARD_ON_TRIGGER + | Notification.FLAG_REMOVE_ON_DISCARD + ), + action="display_pending_task", + action_args=( + engine_uid, + remote_ref, + remote_path, + ), + ) + + class DefaultNotificationService(NotificationService): def init_signals(self) -> None: self._manager.initEngine.connect(self._connect_engine) @@ -539,6 +571,7 @@ def _connect_engine(self, engine: "Engine") -> None: engine.directTransferSessionFinished.connect( self._direct_transfer_session_finshed ) + engine.displayPendingTask.connect(self._display_pending_task) def _direct_transfer_error(self, file: Path, /) -> None: """Display a notification when a Direct Transfer is in error.""" @@ -635,3 +668,15 @@ def _invalidAuthentication(self) -> None: engine_uid = self.sender().uid notif = InvalidCredentialNotification(engine_uid) self.send_notification(notif) + + def _display_pending_task( + self, + engine_uid: str, + remote_ref: str, + remote_path: str, + notification_title: str, + /, + ) -> None: + self.send_notification( + DisplayPendingTask(engine_uid, remote_ref, remote_path, notification_title) + ) diff --git a/nxdrive/poll_workers.py b/nxdrive/poll_workers.py index a69ea4d5f3..f7c2f0e152 100644 --- a/nxdrive/poll_workers.py +++ b/nxdrive/poll_workers.py @@ -1,6 +1,8 @@ import logging from typing import TYPE_CHECKING +from nxdrive.feature import Feature + from .behavior import Behavior from .engine.workers import PollWorker from .options import Options @@ -165,3 +167,52 @@ def _poll(self) -> bool: self.manager.application.quit() return True + + +class WorkflowWorker(PollWorker): + """Class to check new Tasks for document review""" + + def __init__(self, manager: "Manager", /): + """Check every hour""" + super().__init__(60 * 60, "WorkflowWorker") + self.manager = manager + + self._first_workflow_check = True + + @pyqtSlot(result=bool) + def _poll(self) -> bool: + """Start polling workflow after an hour. Initial trigger is via application""" + + if not self.manager: + return False + + if not Feature.tasks_management: + return + + if self._first_workflow_check: + self._first_workflow_check = False + return True + + if self.manager.engines: + self.app = self.manager.application + self.workflow = self.app.workflow + for id in self.manager.delete_users_from_tasks_cache: + self.workflow.clean_user_task_data(id) + self.manager.delete_users_from_tasks_cache = [] + for engine in self.manager.engines.copy().values(): + self.workflow.get_pending_tasks(engine) + if engine.uid == self.app.last_engine_uid: + task_list = str( + self.app.api.get_Tasks_list( + engine.uid, False, self.app.api.hide_refresh_button + ) + ) + if task_list != self.app.api.last_task_list: + self.app.api.last_task_list = task_list + if not self.app.api.engine_changed: + self.app.show_hide_refresh_button(30) + self.app.api.hide_refresh_button = False + else: + self.app.api.engine_changed = False + + return True diff --git a/nxdrive/utils.py b/nxdrive/utils.py index 765f59f214..9812323406 100644 --- a/nxdrive/utils.py +++ b/nxdrive/utils.py @@ -68,6 +68,8 @@ "notBefore": "N/A", } +MINIMUM_TLS_VERSION = "TLSv1_2" + log = getLogger(__name__) @@ -604,7 +606,11 @@ def retrieve_ssl_certificate(hostname: str, /, *, port: int = 443) -> str: import ssl with ssl.create_connection((hostname, port)) as conn: # type: ignore - with ssl.SSLContext().wrap_socket(conn, server_hostname=hostname) as sock: + # Declaring a minimum version to restrict the protocol + # For more information check NXDRIVE-2920 + context = ssl.create_default_context() + context.minimum_version = getattr(ssl.TLSVersion, MINIMUM_TLS_VERSION) + with context.wrap_socket(conn, server_hostname=hostname) as sock: cert_data: bytes = sock.getpeercert(binary_form=True) # type: ignore return ssl.DER_cert_to_PEM_cert(cert_data) @@ -676,7 +682,7 @@ def get_certificate_details( def concat_all_certificates(files: List[Path]) -> Optional[Path]: """Craft a all-in-one certificate with ones from cacert and custom ones.""" - from hashlib import md5 + from hashlib import sha256 import certifi @@ -707,7 +713,7 @@ def concat_all_certificates(files: List[Path]) -> Optional[Path]: log.warning("No valid certificate found.") return None - name = md5(certificates).hexdigest() + name = sha256(certificates).hexdigest() folder = Options.nxdrive_home final_file: Path = folder / f"ndrive_{name}.pem" @@ -720,7 +726,7 @@ def concat_all_certificates(files: List[Path]) -> Optional[Path]: log.info(f"Saved the final certificate to {str(final_file)!r}, including:") for cert_file in cert_files: - log.info(f" >>> {str(cert_file)!r}") + log.debug(f"Certificate file path: {str(cert_file)!r}") final_file.write_bytes(certificates) else: log.info(f"Will use the final certificate from {str(final_file)!r}") @@ -1340,3 +1346,19 @@ def get_verify(): if "No such file or directory" and "-gw" in str(exc): ssl_verification_needed = False return ssl_verification_needed + + +def get_task_type(type_of_task: str) -> str: + from .translator import Translator + + if "chooseParticipants" in type_of_task or "pleaseSelect" in type_of_task: + return Translator.get("CHOOSE_PARTICIPANTS", values=[""]) + if "give_opinion" in type_of_task: + return Translator.get("GIVE_OPINION", values=[""]) + if "AcceptReject" in type_of_task: + return Translator.get("VALIDATE_DOCUMENT", values=[""]) + if "consolidate" in type_of_task: + return Translator.get("CONSOLIDATE_REVIEW", values=[""]) + if "updateRequest" in type_of_task: + return Translator.get("UPDATE_REQUESTED", values=[""]) + return Translator.get("PENDING_TASKS", values=[""]) diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py index 0e45243e7b..512c340a4a 100644 --- a/tests/functional/test_api.py +++ b/tests/functional/test_api.py @@ -1,7 +1,10 @@ from collections import namedtuple -from unittest.mock import patch +from copy import deepcopy +from unittest.mock import Mock, patch from nxdrive.gui.api import QMLDriveApi +from nxdrive.translator import Translator +from nxdrive.utils import find_resource def test_web_authentication(manager_factory, nuxeo_url): @@ -52,3 +55,484 @@ def mocked_open_authentication_dialog(): with manager: returned_val = drive_api.get_features_list() assert returned_val + + +def test_balance_percents(manager_factory): + manager = manager_factory(with_engine=False) + manager.application = "" + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api._balance_percents( + {"key_1": 10.1, "key_2": 20, "key_3": 20.5} + ) + assert returned_val + + +def test_get_text(manager_factory): + manager = manager_factory(with_engine=False) + manager.application = "" + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.get_text("{'id': '1', \"price\": '2000'}", "price") + assert returned_val == "2000" + returned_val = drive_api.get_text("dummy_text", "dummy") + assert returned_val == "" + + +def test_text_red(manager_factory): + manager = manager_factory(with_engine=False) + manager.application = "" + Translator(find_resource("i18n"), lang="en") + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.text_red("Dummy sentence.") + assert not returned_val + + +def test_open_tasks_window(manager_factory): + manager, engine = manager_factory() + + def mocked_open_authentication_dialog(): + return + + def mocked_hide_systray(*args, **kwargs): + return + + def mocked_show_tasks_window(*args, **kwargs): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog, hide_systray, show_tasks_window", + defaults=( + manager, + mocked_open_authentication_dialog, + mocked_hide_systray, + mocked_show_tasks_window, + ), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + manager.application = app + + with manager: + returned_val = drive_api.open_tasks_window("dummy_uid") + assert returned_val is None + + +def test_close_tasks_window(manager_factory): + manager, engine = manager_factory() + + def mocked_open_authentication_dialog(): + return + + def mocked_close_tasks_window(*args, **kwargs): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog, close_tasks_window", + defaults=( + manager, + mocked_open_authentication_dialog, + mocked_close_tasks_window, + ), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + manager.application = app + + with manager: + returned_val = drive_api.close_tasks_window() + assert returned_val is None + + +def test_get_tasks_list(manager_factory): + manager, engine = manager_factory() + Translator(find_resource("i18n"), lang="en") + + dummy_task = Mock() + dummy_task.actors = [{"id": "user:Administrator"}] + dummy_task.created = "2024-04-04T08:31:04.366Z" + dummy_task.directive = "wf.serialDocumentReview.AcceptReject" + dummy_task.dueDate = "2024-04-09T08:31:04.365Z" + dummy_task.id = "e7fc7a3a-5c39-479f-8382-65ed08e8116d" + dummy_task.name = "wf.serialDocumentReview.DocumentValidation" + dummy_task.targetDocumentIds = [{"id": "5b77e4dd-c155-410b-9d4d-e72f499638b8"}] + dummy_task.workflowInstanceId = "81133cf7-38d9-483c-9a0b-c98fc02822a0" + dummy_task.workflowModelName = "SerialDocumentReview" + dummy_task1 = deepcopy(dummy_task) + dummy_task1.directive = "wf.pleaseSelect" + dummy_task2 = deepcopy(dummy_task) + dummy_task2.directive = "wf.give_opinion" + dummy_task3 = deepcopy(dummy_task) + dummy_task3.directive = "wf.consolidate" + dummy_task4 = deepcopy(dummy_task) + dummy_task4.directive = "wf.updateRequest" + dummy_task5 = deepcopy(dummy_task) + dummy_task5.targetDocumentIds = 0 + dummy_task_list = [ + dummy_task, + dummy_task1, + dummy_task2, + dummy_task3, + dummy_task4, + dummy_task5, + ] + + def mocked_open_authentication_dialog(): + return + + def fetch_pending_tasks_(*args, **kwargs): + return dummy_task_list + + def show_hide_refresh_button_(*args, **kwargs): + return + + def get_info_(*args, **kwargs): + doc_info = Mock() + doc_info.name = "dummy_doc_name" + return doc_info + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog, fetch_pending_tasks, show_hide_refresh_button ", + defaults=( + manager, + mocked_open_authentication_dialog, + fetch_pending_tasks_, + show_hide_refresh_button_, + ), + ) + + app = Mocked_App() + manager.application = app + + drive_api = QMLDriveApi(app) + + with manager: + with patch.object(engine.remote, "get_info", new=get_info_): + returned_val = drive_api.get_Tasks_list(engine.uid, True, True) + assert isinstance(returned_val, list) + assert isinstance(returned_val[0], Mock) + + +def test_get_username(manager_factory): + manager, engine = manager_factory() + engine.remote_user = "dummy user" + manager.application = "" + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.get_username(engine.uid) + assert returned_val == "dummy user" + + +def test_on_clicked_open_task(manager_factory): + manager, engine = manager_factory() + manager.application = "" + + def mocked_open_authentication_dialog(): + return + + def mocked_open_task(*args, **kwargs): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog, open_task", + defaults=(manager, mocked_open_authentication_dialog, mocked_open_task), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.on_clicked_open_task(engine.uid, "dummy_task_id") + assert not returned_val + + +def test_get_last_files(manager_factory): + manager, engine = manager_factory() + manager.application = "" + + def mocked_get_last_files(*args, **kwargs): + return [] + + for engine_ in manager.engines.copy().values(): + engine_.dao = Mock() + engine_.dao.get_last_files = mocked_get_last_files + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.get_last_files(engine.uid, 1) + assert not returned_val + + +def test_get_last_files_count(manager_factory): + manager, engine = manager_factory() + manager.application = "" + + def mocked_get_last_files_count(*args, **kwargs): + return 25 + + for engine_ in manager.engines.copy().values(): + engine_.dao = Mock() + engine_.dao.get_last_files_count = mocked_get_last_files_count + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.get_last_files_count(engine.uid) + assert returned_val == 25 + + +def test_export_formatted_state(manager_factory): + manager = manager_factory(with_engine=False) + manager.application = "" + + DocPair = namedtuple( + "DocPair", + "error_count, local_state, pair_state, processor", + defaults=(0, "", "", 0), + ) + doc_pair = DocPair() + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api._export_formatted_state("dummy_uid", state=doc_pair) + assert not returned_val + + +def test_get_active_sessions_count(manager_factory): + manager, engine = manager_factory() + manager.application = "" + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.get_active_sessions_count("dummy_uid") + assert returned_val == 0 + + +def test_get_completed_sessions_count(manager_factory): + manager, engine = manager_factory() + manager.application = "" + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.get_completed_sessions_count("dummy_uid") + assert returned_val == 0 + + +def test_show_metadata(manager_factory, tmp): + manager, engine = manager_factory() + manager.application = "" + + def mocked_open_authentication_dialog(): + return + + def mocked_hide_systray(): + return + + def mocked_show_metadata(*args): + return + + def mocked_ffetch_pending_tasks(*args): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog, hide_systray, show_metadata", + defaults=( + manager, + mocked_open_authentication_dialog, + mocked_hide_systray, + mocked_show_metadata, + ), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.show_metadata("dummy_uid", "dummy") + assert not returned_val + with patch.object( + drive_api, "fetch_pending_tasks", new=mocked_ffetch_pending_tasks + ): + returned_val = drive_api.show_metadata(engine.uid, str(tmp())) + assert not returned_val + + +def test_get_unsynchronizeds(manager_factory): + manager, engine = manager_factory() + manager.application = "" + + def mocked_open_authentication_dialog(): + return + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog", + defaults=(manager, mocked_open_authentication_dialog), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + + with manager: + returned_val = drive_api.get_unsynchronizeds("dummy_uid") + assert not returned_val + + +def test_get_engine(manager_factory, tmp): + manager, engine = manager_factory() + manager.application = "" + + def mocked_open_authentication_dialog(): + return + + def mocked_hide_systray(): + return + + def mocked_fetch_pending_task_list(*args, **kwargs): + return + + def mocked__fetch_tasks(*args, **kwargs): + task = Mock() + task.id = "dummy_id" + return [task] + + Mocked_App = namedtuple( + "app", + "manager, open_authentication_dialog, hide_systray", + defaults=(manager, mocked_open_authentication_dialog, mocked_hide_systray), + ) + app = Mocked_App() + drive_api = QMLDriveApi(app) + engine.fetch_pending_task_list = mocked_fetch_pending_task_list + + with manager: + assert not drive_api.get_conflicts("dummy_uid") + assert not drive_api.get_errors("dummy_uid") + assert not drive_api.open_direct_transfer("dummy_uid") + assert not drive_api.open_server_folders("dummy_uid") + assert not drive_api.open_remote_server("dummy_uid") + assert not drive_api.open_local("dummy_uid", str(tmp())) + assert not drive_api.show_conflicts_resolution("dummy_uid") + assert not drive_api.open_remote_server("dummy_uid") + assert not drive_api.tasks_remaining("dummy_uid") + with patch.object(drive_api, "_fetch_tasks", new=mocked__fetch_tasks): + assert drive_api.tasks_remaining(engine.uid) == 1 + assert not drive_api.fetch_pending_tasks(engine) + assert not drive_api.get_document_details("dummy_uid", "dummy_doc_id") + assert not drive_api.web_update_token("dummy_uid") + assert not drive_api.get_disk_space_info_to_width("dummy_uid", str(tmp()), 1) + assert not drive_api.get_drive_disk_space("dummy_uid") + assert not drive_api.get_used_space_without_synced("dummy_uid", str(tmp())) + assert not drive_api.filters_dialog("dummy_uid") + assert not drive_api.set_server_ui("dummy_uid", "dummy_server_ui") + assert not drive_api.has_invalid_credentials("dummy_uid") + assert not drive_api.handle_token(None, "dummy_user") + drive_api.callback_params = "" + assert not drive_api.handle_token(None, "dummy_user") + assert drive_api.get_syncing_count("dummy_uid") == 0 + assert not drive_api.resolve_with_local("dummy_uid", 0) + assert not drive_api.resolve_with_remote("dummy_uid", 0) + assert not drive_api.retry_pair("dummy_uid", 0) + assert not drive_api.ignore_pair("dummy_uid", 0, "none") + assert not drive_api.open_remote("dummy_uid", "remote_ref", "remote_name") + assert not drive_api.open_remote_document( + "dummy_uid", "remote_ref", "remote_path" + ) + assert not drive_api.get_remote_document_url("dummy_uid", "remote_ref") + assert not drive_api.on_clicked_open_task("dummy_uid", "dummy_task_id") diff --git a/tests/functional/test_behavior.py b/tests/functional/test_behavior.py index 7c0cb5b1c2..dfbc74c48b 100644 --- a/tests/functional/test_behavior.py +++ b/tests/functional/test_behavior.py @@ -141,3 +141,15 @@ def test_feature_synchronization(tmp, manager_factory): manager.set_config("client_version", "5.2.2") manager._guess_synchronization_state() assert not Feature.synchronization + + +@Options.mock() +def test_feature_tasks_management(manager_factory): + """Ensure the tasks management feature is well handled.""" + + Options.feature_tasks_management = True + with manager_factory(with_engine=False) as manager: + assert manager.get_feature_state("tasks_management") is True + manager.set_feature_state("tasks_management", False) + assert manager.get_feature_state("tasks_management") is False + assert not Feature.tasks_management diff --git a/tests/functional/test_engine.py b/tests/functional/test_engine.py index 7d2b0e362a..e94cefd3e4 100644 --- a/tests/functional/test_engine.py +++ b/tests/functional/test_engine.py @@ -2,6 +2,7 @@ from datetime import datetime from pathlib import Path from unittest.mock import patch +from uuid import uuid4 from nxdrive.client.local import FileInfo from nxdrive.constants import DelAction, TransferStatus @@ -9,6 +10,8 @@ from nxdrive.manager import Manager from nxdrive.objects import RemoteFileInfo, Session from nxdrive.session_csv import SessionCsv +from nxdrive.translator import Translator +from nxdrive.utils import find_resource from .. import ensure_no_exception @@ -198,3 +201,29 @@ def test_can_use_trash(manager_factory): manager, engine = manager_factory() with manager: assert engine.use_trash() + + +def test_send_task_notification(manager_factory): + manager, engine = manager_factory() + Translator(find_resource("i18n"), lang="en") + with manager: + assert ( + engine.send_task_notification( + str(uuid4()), "/doc_path/test.txt", "Review Document" + ) + is None + ) + assert ( + engine.send_task_notification( + str(uuid4()), "/doc_path/test.txt", "Choose Participants" + ) + is None + ) + + +def test_get_task_url(manager_factory, nuxeo_url): + manager, engine = manager_factory() + with manager: + remote_ref = str(uuid4()) + url = engine.get_task_url(remote_ref) + assert url == f"{nuxeo_url}/ui#!/tasks/{remote_ref}" diff --git a/tests/functional/test_manager.py b/tests/functional/test_manager.py index 775a49bb62..48413503ce 100644 --- a/tests/functional/test_manager.py +++ b/tests/functional/test_manager.py @@ -1,4 +1,5 @@ import os +from unittest.mock import Mock import pytest @@ -29,6 +30,30 @@ def startfile(path): manager.open_local_file("File.azerty") +def test_init_workflow_with_app(manager_factory): + manager = manager_factory(with_engine=False) + # manager = Manager(tmp_path) + manager.db_backup_worker = Mock() + + manager.autolock_service = Mock() + manager.server_config_updater = Mock() + manager._create_server_config_updater = Mock() + manager.sync_and_quit_worker = Mock() + manager._create_db_backup_worker = Mock() + manager._create_workflow_worker = Mock() + + # app = Application(manager) + # app.manager + # engine = Mock() + # engine.remote = "test" + # app.manager.engines = {"engine_uid": engine} + # app.engine_model.engines_uid = ["engine_uid"] + # app.init_workflow() + # manager.close() + # app.exit_app() + # assert 1 == 0 + + # This test is commented because it causes other ft tests to fails # @Options.mock() # def test_manager_init_failed_migrations(manager_factory, tmp_path, monkeypatch): diff --git a/tests/functional/test_workflow.py b/tests/functional/test_workflow.py new file mode 100644 index 0000000000..992aca4fe7 --- /dev/null +++ b/tests/functional/test_workflow.py @@ -0,0 +1,109 @@ +from datetime import datetime, timedelta, timezone +from unittest.mock import Mock, patch +from uuid import uuid4 + +import pytest +from nuxeo.models import Task + +from nxdrive.client.workflow import Workflow +from nxdrive.feature import Feature +from nxdrive.gui.api import QMLDriveApi +from nxdrive.gui.application import Application +from nxdrive.gui.view import EngineModel +from nxdrive.poll_workers import WorkflowWorker + + +@pytest.fixture() +def task(): + task = Task + task.id = str(uuid4()) + task.targetDocumentIds = [{"id": f"{uuid4()}"}] + datetime_30_min_ago = datetime.now(tz=timezone.utc) - timedelta(minutes=30) + task.created = datetime_30_min_ago.strftime("%Y-%m-%dT%H:%M:%S.%f%z") + return task + + +@pytest.fixture() +def workflow(): + return Workflow() + + +@pytest.fixture() +def application(manager_factory, workflow): + manager = manager_factory() + application = Mock(spec=Application) + application.manager = manager + application._init_translator = Mock() + application.setWindowIcon = Mock() + application.setApplicationName = Mock() + application.setQuitOnLastWindowClosed = Mock() + application.show_metrics_acceptance = Mock() + application.init_checks = Mock() + application._setup_notification_center = Mock() + # application.show_hide_refresh_button = Mock() + # application.manager.preferences_metrics_chosen = True + # application.manager.old_version = "1.0.0" + # application.manager.version = "1.1.0" + application.workflow = workflow + + def show_hide_refresh_button_(*args, **kwargs): + return + + application.show_hide_refresh_button = show_hide_refresh_button_ + + return application + + +@pytest.fixture() +def workflow_worker(manager_factory, application, workflow): + # Create a WorkflowWorker instance with mocked dependencies + manager = manager_factory() + worker = WorkflowWorker(manager) + worker.app = application + worker.workflow = workflow + return worker + + +def test_refresh_list(manager_factory, workflow, application): + manager, engine = manager_factory() + application_ = application + application_.api = Mock() + application_.task_manager_window = Mock() + engine_model = EngineModel(application_) + engine_model.engines_uid = [engine.uid] + + application_.engine_model = engine_model + application_.last_engine_uid = engine.uid + + manager.application = application_ + + worker_ = WorkflowWorker(manager) + worker_.manager = manager + worker_.app = application() + worker_.app.api = QMLDriveApi(application_) + worker_.workflow = workflow + worker_.app.api.last_task_list = "" + worker_._first_workflow_check = False + + def get_Tasks_list_(*args, **kwargs): + dummy_task = Mock() + dummy_task.actors = [{"id": "user:Administrator"}] + dummy_task.created = "2024-04-04T08:31:04.366Z" + dummy_task.directive = "wf.serialDocumentReview.AcceptReject" + dummy_task.dueDate = "2024-04-09T08:31:04.365Z" + dummy_task.id = "e7fc7a3a-5c39-479f-8382-65ed08e8116d" + dummy_task.name = "wf.serialDocumentReview.DocumentValidation" + dummy_task.targetDocumentIds = [{"id": "5b77e4dd-c155-410b-9d4d-e72f499638b8"}] + dummy_task.workflowInstanceId = "81133cf7-38d9-483c-9a0b-c98fc02822a0" + dummy_task.workflowModelName = "SerialDocumentReview" + return [dummy_task] + + Feature.tasks_management = True + + with patch.object(worker_.app.api, "get_Tasks_list", new=get_Tasks_list_): + assert worker_._poll() + + worker_.app.last_engine_uid = engine.uid + worker_.app.api.engine_changed = False + with patch.object(worker_.app.api, "get_Tasks_list", new=get_Tasks_list_): + assert worker_._poll() diff --git a/tests/integration/windows/test_cli_sub_command.py b/tests/integration/windows/test_cli_sub_command.py index 94daa34184..7715b9e800 100644 --- a/tests/integration/windows/test_cli_sub_command.py +++ b/tests/integration/windows/test_cli_sub_command.py @@ -10,7 +10,7 @@ from nxdrive.constants import WINDOWS from ... import env -from .utils import cb_get, fatal_error_dlg # , get_opened_url +from .utils import cb_get, fatal_error_dlg # , share_metrics_dlg , get_opened_url if not WINDOWS: pytestmark = pytest.mark.skip("Windows only.") @@ -248,3 +248,14 @@ def test_ctx_menu_entries(nuxeo_url, exe, server, tmp): os.chmod(folder, stat.S_IWUSR) shutil.rmtree(folder) assert not os.path.isdir(folder) + + +def test_app_init_workflow_via_subprocess(final_exe): + import subprocess + + p = subprocess.Popen([final_exe, "console"]) + p.terminate() + p.wait(timeout=30) + p.kill() + poll = p.poll() + assert poll is not None diff --git a/tests/old_functional/test_direct_transfer.py b/tests/old_functional/test_direct_transfer.py index 63309ab93a..3956342358 100644 --- a/tests/old_functional/test_direct_transfer.py +++ b/tests/old_functional/test_direct_transfer.py @@ -725,7 +725,7 @@ def checks(self, created): assert not self.engine_1.dao.get_errors(limit=0) def direct_transfer(self, folder, duplicate_behavior: str = "create") -> None: - paths = {path: size for path, size in get_tree_list(folder)} + paths = dict(get_tree_list(folder)) self.engine_1.direct_transfer( paths, self.ws.path, diff --git a/tests/unit/test_view.py b/tests/unit/test_view.py index f5d78674fe..d02e92247d 100644 --- a/tests/unit/test_view.py +++ b/tests/unit/test_view.py @@ -1,7 +1,15 @@ +from copy import deepcopy +from typing import Any, List from unittest.mock import Mock -from nxdrive.gui.view import FileModel +import pytest + +from nxdrive.client.workflow import Workflow +from nxdrive.gui.application import Application +from nxdrive.gui.view import EngineModel, FileModel, TasksModel from nxdrive.qt.imports import QModelIndex +from nxdrive.translator import Translator +from nxdrive.utils import find_resource def test_foldersDialog(): @@ -35,3 +43,81 @@ def test_data(direct_transfer_model): direct_transfer_model.data( direct_transfer_model, index, direct_transfer_model.FINALIZING_STATUS ) + + +def test_tasksModel(): + Translator(find_resource("i18n"), lang="en") + + dummy_task = Mock() + dummy_task.actors = [{"id": "user:Administrator"}] + dummy_task.created = "2024-04-04T08:31:04.366Z" + dummy_task.directive = "wf.serialDocumentReview.AcceptReject" + dummy_task.dueDate = "2024-04-09T08:31:04.365Z" + dummy_task.id = "e7fc7a3a-5c39-479f-8382-65ed08e8116d" + dummy_task.name = "wf.serialDocumentReview.DocumentValidation" + dummy_task.targetDocumentIds = [{"id": "5b77e4dd-c155-410b-9d4d-e72f499638b8"}] + dummy_task.workflowInstanceId = "81133cf7-38d9-483c-9a0b-c98fc02822a0" + dummy_task.workflowModelName = "SerialDocumentReview" + dummy_task1 = deepcopy(dummy_task) + dummy_task1.dueDate = "2023-04-09T08:31:04.365Z" + dummy_task2 = deepcopy(dummy_task) + dummy_task2.dueDate = "2040-04-09T08:31:04.365Z" + dummy_task2.actors = [{"id": "Administrator"}] + dummy_task_list = [dummy_task, dummy_task1, dummy_task2] + + def translate(message: str, /, *, values: List[Any] = None) -> str: + return Translator.get(message, values=values) + + tasks_model = TasksModel(translate) + tasks_model.loadList(dummy_task_list, "Administrator") + + assert isinstance(tasks_model, TasksModel) + assert tasks_model.model + assert tasks_model.self_model + + dummy_task_list = [dummy_task, dummy_task1] + assert not tasks_model.loadList(dummy_task_list, "Administrator") + + dummy_task_list = [dummy_task1, dummy_task2] + assert not tasks_model.loadList(dummy_task_list, "Administrator") + + dummy_task_list = [dummy_task2] + assert not tasks_model.loadList(dummy_task_list, "Administrator") + + +@pytest.fixture() +def workflow(): + return Workflow() + + +@pytest.fixture() +def application(manager_factory, workflow): + manager = manager_factory() + application = Mock(spec=Application) + application.manager = manager + application._init_translator = Mock() + application.setWindowIcon = Mock() + application.setApplicationName = Mock() + application.setQuitOnLastWindowClosed = Mock() + application.show_metrics_acceptance = Mock() + application.init_checks = Mock() + application._setup_notification_center = Mock() + application.workflow = workflow + + return application + + +def test_engine_model(): + application_ = application + application_.api = Mock() + application_.update_workflow = Mock() + application_.update_workflow_user_engine_list = Mock() + application_.manager = Mock() + application_.manager.engines = {"dummy_uid": "dummy_engine"} + engine_model = EngineModel(application_) + engine_model.beginInsertRows = Mock() + engine_model.endInsertRows = Mock() + engine_model._connect_engine = Mock() + engine_model.removeRows = Mock() + assert not engine_model.addEngine("dummy_uid") + assert not engine_model.removeEngine("dummy_uid") diff --git a/tests/unit/test_workflow.py b/tests/unit/test_workflow.py new file mode 100644 index 0000000000..64b298038e --- /dev/null +++ b/tests/unit/test_workflow.py @@ -0,0 +1,246 @@ +from datetime import datetime, timedelta, timezone +from pathlib import Path +from unittest.mock import Mock +from uuid import uuid4 + +import pytest +from nuxeo.models import Document, Task + +from nxdrive.client.remote_client import Remote +from nxdrive.client.workflow import Workflow +from nxdrive.feature import Feature +from nxdrive.gui.api import QMLDriveApi +from nxdrive.gui.application import Application +from nxdrive.gui.view import EngineModel +from nxdrive.poll_workers import WorkflowWorker +from nxdrive.translator import Translator + + +@pytest.fixture() +def task(): + task = Task + task.id = "taskid_test" + task.targetDocumentIds = [{"id": f"{uuid4()}"}] + datetime_30_min_ago = datetime.now(tz=timezone.utc) + timedelta(minutes=30) + task.dueDate = datetime_30_min_ago.strftime("%Y-%m-%dT%H:%M:%S.%f%z") + task.directive = "" + return task + + +@pytest.fixture() +def remote(): + remote = Remote + remote.documents = Document + remote.user_id = f"{uuid4()}" + remote.tasks = Task + return remote + + +@pytest.fixture() +def workflow(): + return Workflow() + + +@pytest.fixture() +def engine(engine, remote): + engine.remote = remote + return engine + + +@pytest.fixture() +def application(manager, workflow): + application = Mock(spec=Application) + application.manager = manager + application._init_translator = Mock() + application.setWindowIcon = Mock() + application.setApplicationName = Mock() + application.setQuitOnLastWindowClosed = Mock() + application.show_metrics_acceptance = Mock() + application.init_checks = Mock() + application._setup_notification_center = Mock() + application.manager.preferences_metrics_chosen = True + application.manager.old_version = "1.0.0" + application.manager.version = "1.1.0" + application.workflow = workflow + application.last_engine_uid = "" + return application + + +@pytest.fixture() +def workflow_worker(manager, application, workflow): + # Create a WorkflowWorker instance with mocked dependencies + worker = WorkflowWorker(manager) + worker.app = application + worker.workflow = workflow + return worker + + +def test_get_pending_task_for_no_doc(workflow, engine, remote): + # No response from api for pending task for user_a + assert Workflow.user_task_list == {} + + remote.user_id = "user_a" + engine.remote = remote + engine.remote.tasks.get = Mock(return_value=[]) + assert workflow.get_pending_tasks(engine) is None + + # If no tasks assigned to user remove the ID + Workflow.user_task_list = {"user_a": ["taskid_1"]} + engine.remote.tasks.get = Mock(return_value=[]) + assert workflow.get_pending_tasks(engine) is None + assert not Workflow.user_task_list + + +def test_get_pending_task_for_single_doc(workflow, engine, task, remote): + # Triggered through polling for single task, + # Add new key if it doesn't exist in user_task_list + remote.user_id = "user_a" + engine.remote = remote + engine.remote.tasks.get = Mock(return_value=[task]) + workflow.fetch_document = Mock() + assert workflow.get_pending_tasks(engine) is None + assert Workflow.user_task_list == {"user_a": ["taskid_test"]} + Workflow.user_task_list = {} + + +def test_get_pending_task_for_multiple_doc(workflow, engine, task, remote): + # Triggered through polling for multiple task + task1 = task + task1.id = "taskid_1" + task2 = task + task2.id = "taskid_2" + remote.user_id = "user_b" + engine.remote = remote + engine.remote.tasks.get = Mock(return_value=[task1, task2]) + engine.send_task_notification = Mock() + assert workflow.get_pending_tasks(engine) is None + + # user_task_list[a_user] have [tasks_a, tasks_b] and got tasks[tasks_a, tasks_b]. + # In this case no need to the send notification + assert workflow.get_pending_tasks(engine) is None + + Workflow.user_task_list = {} + + +def test_update_user_task_data(workflow, task): + # Add new key if it doesn't exist in user_task_list + workflow.update_user_task_data([task], "user_a") + assert Workflow.user_task_list == {"user_a": ["taskid_test"]} + + # Remove taskid_test in user_task_list[user_a] and no notification in this case + workflow.update_user_task_data([], "user_a") + assert Workflow.user_task_list == {"user_a": []} + + # Add taskid_test in user_task_list[user_a] and send notification for taskid_a + workflow.update_user_task_data([task], "user_a") + assert Workflow.user_task_list == {"user_a": ["taskid_test"]} + + Workflow.user_task_list = {} + + +def test_remove_overdue_tasks(workflow, engine, task): + # No new task during an hour + datetime_1_hr_ago = datetime.now(tz=timezone.utc) - timedelta(minutes=160) + task.dueDate = datetime_1_hr_ago.strftime("%Y-%m-%dT%H:%M:%S.%f%z") + engine.remote.tasks.get = Mock(return_value=[task]) + engine.send_task_notification = Mock() + assert workflow.get_pending_tasks(engine) is None + + # raise exception + task.dueDate = None + assert workflow.get_pending_tasks(engine) is None + + +def get_folder(folder) -> Path: + return Path(__file__).parent.parent / "resources" / folder + + +def test_fetch_document(workflow, engine, task): + Translator(get_folder("i18n")) + assert Translator.locale() == "en" + engine.remote.get_info = Mock(doc_id="/doc_path/doc.txt") + engine.send_task_notification = Mock() + workflow.fetch_document([task], engine) + + # send notification for directive + task.directive = "chooseParticipants" + workflow.fetch_document([task], engine) + + task.directive = "give_opinion" + workflow.fetch_document([task], engine) + + task.directive = "AcceptReject" + workflow.fetch_document([task], engine) + + task.directive = "consolidate" + workflow.fetch_document([task], engine) + + task.directive = "updateRequest" + workflow.fetch_document([task], engine) + + # No response from remote.documents.get + engine.remote.documents.get = Mock(return_value=None) + workflow.fetch_document([task], engine) + + +def test_poll_initial_trigger(workflow_worker, manager, application, engine): + # Test initial trigger via workflow worker + workflow_worker._first_workflow_check = True + assert not workflow_worker._poll() + + # without manager + workflow_worker.manager = None + assert not workflow_worker._poll() + + # Set tasks_management feature as True + Feature.tasks_management = True + + # with engines + engine_model = EngineModel(application) + engine_model.engines_uid = ["engine_uid"] + application.engine_model = engine_model + manager.application = application + manager.engines = {"engine_uid": engine} + workflow_worker.manager = manager + workflow_worker.workflow = Mock() + assert workflow_worker._poll() + + +def test_api_display_pending_task_without_exec(application, manager, engine): + engine.get_task_url = Mock(return_value="/doc_url") + engine.open_remote = Mock() + manager.engines = {"engine_uid": engine} + application.manager = manager + + drive_api = QMLDriveApi(application) + assert ( + drive_api.display_pending_task("engine_uid", str(uuid4()), "/doc_path") is None + ) + + +def test_api_display_pending_task_with_exec(application, manager): + # Test exception handling + manager.engines = {"engine_uid": "dummy_engine"} + application.manager = manager + + drive_api = QMLDriveApi(application) + assert ( + drive_api.display_pending_task("engine_uid", str(uuid4()), "/doc_path") is None + ) + + +def test_clean_user_data_when_unbind_engine(manager, engine): + Workflow.user_task_list == {"user_a": ["dummy_taskid"]} + engine.unbind = Mock() + manager.dao = Mock() + manager.dropEngine = Mock() + manager.get_engines = Mock() + manager.db_backup_worker = False + manager.delete_users_from_tasks_cache = Mock() + Feature.tasks_management = True + engine.remote = Remote + engine.remote.user_id = "user_a" + manager.engines = {"user_a": engine} + manager.unbind_engine(manager, "user_a") + + assert "user_a" not in Workflow.user_task_list diff --git a/tools/cleanup.sh b/tools/cleanup.sh index c0d0ae43b4..3a56a5c9f7 100755 --- a/tools/cleanup.sh +++ b/tools/cleanup.sh @@ -13,7 +13,7 @@ purge() { echo " - ${version}" python3 tools/versions.py --delete "${version}" - ssh -o StrictHostKeyChecking=no -T nuxeo@lethe.nuxeo.com "rm -vf ${REMOTE_PATH_PROD}/alpha/*${version}.* ${REMOTE_PATH_PROD}/alpha/*${version}-*" || true + ssh -o StrictHostKeyChecking=no -T lethe.nuxeo.com "rm -vf ${REMOTE_PATH_PROD}/alpha/*${version}.* ${REMOTE_PATH_PROD}/alpha/*${version}-*" || true git tag --delete "alpha-${version}" || true git push --delete origin "wip-alpha-${version}" || true # branch git push --delete origin "alpha-${version}" || true # tag @@ -32,7 +32,7 @@ main() { python3 -m pip install --user pyyaml==5.3.1 echo ">>> Retrieving versions.yml" - rsync -e "ssh -o StrictHostKeyChecking=no" -vz nuxeo@lethe.nuxeo.com:"${REMOTE_PATH_PROD}/versions.yml" . + rsync -e "ssh -o StrictHostKeyChecking=no" -vz lethe.nuxeo.com:"${REMOTE_PATH_PROD}/versions.yml" . echo ">>> Checking versions.yml integrity" python3 tools/versions.py --check || exit 1 @@ -68,7 +68,7 @@ main() { python3 tools/versions.py --check || exit 1 echo ">>> Uploading versions.yml" - rsync -e "ssh -o StrictHostKeyChecking=no" -vz versions.yml nuxeo@lethe.nuxeo.com:"${REMOTE_PATH_PROD}/" + rsync -e "ssh -o StrictHostKeyChecking=no" -vz versions.yml lethe.nuxeo.com:"${REMOTE_PATH_PROD}/" } main "$@" diff --git a/tools/deploy.sh b/tools/deploy.sh index 0fc3038eb6..7739503454 100755 --- a/tools/deploy.sh +++ b/tools/deploy.sh @@ -22,7 +22,7 @@ release() { fi echo ">>> [${latest_release}] Deploying to the production website" - ssh -o "StrictHostKeyChecking=no" -T nuxeo@lethe.nuxeo.com <>> [release ${drive_version}] Generating the versions file" python3 -m pip install --user -U setuptools wheel python3 -m pip install --user pyyaml==5.3.1 - rsync -e "ssh -o StrictHostKeyChecking=no" -vz nuxeo@lethe.nuxeo.com:"${REMOTE_PATH_PROD}/versions.yml" . + rsync -e "ssh -o StrictHostKeyChecking=no" -vz lethe.nuxeo.com:"${REMOTE_PATH_PROD}/versions.yml" . python3 tools/versions.py --promote "${drive_version}" --type "release" - rsync -e "ssh -o StrictHostKeyChecking=no" -vz versions.yml nuxeo@lethe.nuxeo.com:"${REMOTE_PATH_PROD}/" + rsync -e "ssh -o StrictHostKeyChecking=no" -vz versions.yml lethe.nuxeo.com:"${REMOTE_PATH_PROD}/" } release "$@" diff --git a/tools/deps/requirements-pip.txt b/tools/deps/requirements-pip.txt index 606fd64819..91f68a24fa 100644 --- a/tools/deps/requirements-pip.txt +++ b/tools/deps/requirements-pip.txt @@ -2,15 +2,15 @@ # Modules needed by every developers. # This file is used to create build and test environments. # -build==1.1.1 \ - --hash=sha256:8ed0851ee76e6e38adce47e4bee3b51c771d86c64cf578d0c2245567ee200e73 \ - --hash=sha256:8eea65bb45b1aac2e734ba2cc8dad3a6d97d97901a395bd0ed3e7b46953d2a31 +build==1.2.1 \ + --hash=sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d \ + --hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4 # via pip-tools click==8.1.7 \ --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de # via pip-tools -colorama==0.4.6 ; sys_platform == "win32" \ +colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via click @@ -38,6 +38,8 @@ pyproject-hooks==1.0.0 \ # via # build # pip-tools +setuptools==69.5.1 \ + --hash=sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32 six==1.16.0 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 diff --git a/tools/deps/requirements-tests.txt b/tools/deps/requirements-tests.txt index b40ab238b0..f7e9accc8f 100644 --- a/tools/deps/requirements-tests.txt +++ b/tools/deps/requirements-tests.txt @@ -45,7 +45,7 @@ click==8.1.7 \ codespell==2.2.6 \ --hash=sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07 \ --hash=sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9 -colorama==0.4.6; sys_platform == "win32" \ +colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via pytest @@ -111,9 +111,9 @@ cfgv==3.4.0 \ --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 # via flake8 -exceptiongroup==1.2.0 \ - --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \ - --hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68 +exceptiongroup==1.2.1 \ + --hash=sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad \ + --hash=sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16 # via pytest execnet==2.0.2 \ --hash=sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41 \ @@ -128,9 +128,9 @@ flake8==6.1.0 \ future==0.18.3 \ --hash=sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307 # via junitparser -identify==2.5.33 \ - --hash=sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d \ - --hash=sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34 +identify==2.5.36 \ + --hash=sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa \ + --hash=sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d # via pre-commit importlib-metadata==7.0.1 \ --hash=sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e \ @@ -140,9 +140,9 @@ iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 # via pytest -junitparser==3.1.1 \ - --hash=sha256:9e053d7df3f425481a0e61fc2db99e348f14858af0390cc2e533006954b1cf15 \ - --hash=sha256:c9f00e286493a077088d5c44d819702fbb2dda9f55323b607a68d62d646fe659 +junitparser==3.1.2 \ + --hash=sha256:2a86664ae7abf0ece4ce928121c7dbdb0f95f2c781a3f09ee9e2ac0e91e77a60 \ + --hash=sha256:d37ca5b35569dd7ebfdee976f2396b7d95cc46aa3c5910a0a7771c80751a5524 mccabe==0.7.0 \ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e @@ -151,34 +151,34 @@ more-itertools==10.2.0 \ --hash=sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684 \ --hash=sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1 # via pytest -mypy==1.5.1 \ - --hash=sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315 \ - --hash=sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0 \ - --hash=sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373 \ - --hash=sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a \ - --hash=sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161 \ - --hash=sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275 \ - --hash=sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693 \ - --hash=sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb \ - --hash=sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65 \ - --hash=sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4 \ - --hash=sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb \ - --hash=sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243 \ - --hash=sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14 \ - --hash=sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4 \ - --hash=sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1 \ - --hash=sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a \ - --hash=sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160 \ - --hash=sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25 \ - --hash=sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12 \ - --hash=sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d \ - --hash=sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92 \ - --hash=sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770 \ - --hash=sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2 \ - --hash=sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70 \ - --hash=sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb \ - --hash=sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5 \ - --hash=sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f +mypy==1.10.0 \ + --hash=sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061 \ + --hash=sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99 \ + --hash=sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de \ + --hash=sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a \ + --hash=sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9 \ + --hash=sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec \ + --hash=sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1 \ + --hash=sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131 \ + --hash=sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f \ + --hash=sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821 \ + --hash=sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5 \ + --hash=sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee \ + --hash=sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e \ + --hash=sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746 \ + --hash=sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2 \ + --hash=sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0 \ + --hash=sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b \ + --hash=sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53 \ + --hash=sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30 \ + --hash=sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda \ + --hash=sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051 \ + --hash=sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2 \ + --hash=sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7 \ + --hash=sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee \ + --hash=sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727 \ + --hash=sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976 \ + --hash=sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4 mypy-extensions==1.0.0 \ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 @@ -195,21 +195,17 @@ pathspec==0.12.1 \ --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 # via black -platformdirs==4.2.0 \ - --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ - --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 # via black -pluggy==1.4.0 \ - --hash=sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 \ - --hash=sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 # via pytest pre-commit==2.16.0 \ --hash=sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e \ --hash=sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65 -py==1.10.0 \ - --hash=sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a \ - --hash=sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3 - # via pytest pycodestyle==2.11.1 \ --hash=sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f \ --hash=sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67 @@ -225,22 +221,18 @@ pyparsing==2.4.7 \ --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b # via packaging -pytest-cov==4.1.0 \ - --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \ - --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a -pytest-forked==1.6.0 \ - --hash=sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f \ - --hash=sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0 - # via pytest-xdist -pytest-timeout==2.2.0 \ - --hash=sha256:3b0b95dabf3cb50bac9ef5ca912fa0cfc286526af17afc806824df20c2f72c90 \ - --hash=sha256:bde531e096466f49398a59f2dde76fa78429a09a12411466f88a07213e220de2 +pytest-cov==5.0.0 \ + --hash=sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 \ + --hash=sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857 +pytest-timeout==2.3.1 \ + --hash=sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9 \ + --hash=sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e pytest-xdist==3.5.0 \ --hash=sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a \ --hash=sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24 -pytest==7.4.4 \ - --hash=sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280 \ - --hash=sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 +pytest==8.2.1 \ + --hash=sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd \ + --hash=sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1 # via pytest-cov, pytest-forked, pytest-timeout, pytest-xdist pywinauto==0.6.8 ; sys_platform == "win32" \ --hash=sha256:931ce622d7f402b1892ab472987a1332e4c0681bf87e106f798390d16ca95e58 diff --git a/tools/deps/requirements-tox.txt b/tools/deps/requirements-tox.txt index 635da349ea..2fc31d3a2c 100644 --- a/tools/deps/requirements-tox.txt +++ b/tools/deps/requirements-tox.txt @@ -6,7 +6,15 @@ appdirs==1.4.4 \ --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 \ --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 # via virtualenv -colorama==0.4.6; sys_platform == "win32" \ +cachetools==5.3.3 \ + --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ + --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 + # via tox +chardet==5.2.0 \ + --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ + --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 + # via tox +colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via tox @@ -14,9 +22,9 @@ distlib==0.3.8 \ --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 # via virtualenv -filelock==3.12.4 \ - --hash=sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4 \ - --hash=sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd +filelock==3.14.0 \ + --hash=sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f \ + --hash=sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a # via tox importlib-metadata==7.0.1 \ --hash=sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e \ @@ -26,18 +34,22 @@ packaging==24.0 \ --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 # via tox -pluggy==1.4.0 \ - --hash=sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 \ - --hash=sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be - # via tox -py==1.10.0 \ - --hash=sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a \ - --hash=sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3 +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 + # via black +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 # via tox pyparsing==2.4.7 \ --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \ --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 # via packaging +pyproject-api==1.6.1 \ + --hash=sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538 \ + --hash=sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675 + # via tox six==1.16.0 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 @@ -46,16 +58,23 @@ toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f # via tox -tox==3.24.5 \ - --hash=sha256:be3362472a33094bce26727f5f771ca0facf6dafa217f65875314e9a6600c95c \ - --hash=sha256:67e0e32c90e278251fea45b696d0fef3879089ccbe979b0c556d35d5a70e2993 +tox==4.15.0 \ + --hash=sha256:300055f335d855b2ab1b12c5802de7f62a36d4fd53f30bd2835f6a201dda46ea \ + --hash=sha256:7a0beeef166fbe566f54f795b4906c31b428eddafc0102ac00d20998dd1933f6 +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via + # build + # pip-tools + # pyproject-hooks typing-extensions==4.9.0 \ --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd # via importlib-metadata -virtualenv==20.4.7 \ - --hash=sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76 \ - --hash=sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467 +virtualenv==20.26.2 \ + --hash=sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c \ + --hash=sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b # via tox zipp==3.18.0 \ --hash=sha256:c1bb803ed69d2cce2373152797064f7e79bc43f0a3748eb494096a867e0ebf79 \ diff --git a/tools/deps/requirements.txt b/tools/deps/requirements.txt index a10c6fa1ba..557eebd10d 100644 --- a/tools/deps/requirements.txt +++ b/tools/deps/requirements.txt @@ -72,9 +72,9 @@ cffi==1.16.0 \ --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 # via cryptography -chardet==4.0.0 \ - --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \ - --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa +chardet==5.2.0 \ + --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ + --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 # via requests charset-normalizer==2.1.1 \ --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ @@ -197,9 +197,9 @@ dukpy==0.3.1 \ --hash=sha256:f9500f910c0e50ec98763e7ff3c2e553f40c1f1513301e8a1b42005ccc5ac548 \ --hash=sha256:fbeb35a6c3be2c584bfc1d330b7718be0b1d62d5e85f596e60669f126ad7e6a7 # via pypac -idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +idna==3.7 \ + --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ + --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # via requests jmespath==1.0.1 \ --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ @@ -208,9 +208,10 @@ jmespath==1.0.1 \ jwt==1.3.1 \ --hash=sha256:61c9170f92e736b530655e75374681d4fcca9cfa8763ab42be57353b2b203494 # via nuxeo -nuxeo[oauth2,s3]==6.1.1 \ - --hash=sha256:31fc973c55d7fc7567860e589c6c59c386d2e3563cb8f7709276551a76902daa \ - --hash=sha256:d7e30368fa2ca02e3981cdadee27c9f8011fddb091b5af879735543900009767 +-i https://packages.nuxeo.com/repository/pypi-nuxeopublic/simple +nuxeo==6.1.2 \ + --hash=sha256:41f4272419df615629735184ceb129c1c1886c3110f9e3cb0461f6043e95e6ab \ + --hash=sha256:549c974fe83004aab462771b86a4ab7c3bf3b5c0f899af08008523999fc7726e # via -r requirements.txt packaging==24.0 \ --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ @@ -293,34 +294,41 @@ pyparsing==2.4.7 \ --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b # via packaging -pyqt5-sip==12.8.1 \ - --hash=sha256:bb5a87b66fc1445915104ee97f7a20a69decb42f52803e3b0795fa17ff88226c \ - --hash=sha256:a29e2ac399429d3b7738f73e9081e50783e61ac5d29344e0802d0dcd6056c5a2 \ - --hash=sha256:0304ca9114b9817a270f67f421355075b78ff9fc25ac58ffd72c2601109d2194 \ - --hash=sha256:84ba7746762bd223bed22428e8561aa267a229c28344c2d28c5d5d3f8970cffb \ - --hash=sha256:7b81382ce188d63890a0e35abe0f9bb946cabc873a31873b73583b0fc84ac115 \ - --hash=sha256:b6d42250baec52a5f77de64e2951d001c5501c3a2df2179f625b241cbaec3369 \ - --hash=sha256:6c1ebee60f1d2b3c70aff866b7933d8d8d7646011f7c32f9321ee88c290aa4f9 \ - --hash=sha256:34dcd29be47553d5f016ff86e89e24cbc5eebae92eb2f96fb32d2d7ba028c43c \ - --hash=sha256:ed897c58acf4a3cdca61469daa31fe6e44c33c6c06a37c3f21fab31780b3b86a \ - --hash=sha256:a1b8ef013086e224b8e86c93f880f776d01b59195bdfa2a8e0b23f0480678fec \ - --hash=sha256:0cd969be528c27bbd4755bd323dff4a79a8fdda28215364e6ce3e069cb56c2a9 \ - --hash=sha256:c9800729badcb247765e4ffe2241549d02da1fa435b9db224845bc37c3e99cb0 \ - --hash=sha256:9312ec47cac4e33c11503bc1cbeeb0bdae619620472f38e2078c5a51020a930f \ - --hash=sha256:2f35e82fd7ec1e1f6716e9154721c7594956a4f5bd4f826d8c6a6453833cc2f0 \ - --hash=sha256:da9c9f1e65b9d09e73bd75befc82961b6b61b5a3b9d0a7c832168e1415f163c6 \ - --hash=sha256:832fd60a264de4134c2824d393320838f3ab648180c9c357ec58a74524d24507 \ - --hash=sha256:c317ab1263e6417c498b81f5c970a9b1af7acefab1f80b4cc0f2f8e661f29fc5 \ - --hash=sha256:c9d6d448c29dc6606bb7974696608f81f4316c8234f7c7216396ed110075e777 \ - --hash=sha256:5a011aeff89660622a6d5c3388d55a9d76932f3b82c95e82fc31abd8b1d2990d \ - --hash=sha256:f168f0a7f32b81bfeffdf003c36f25d81c97dee5eb67072a5183e761fe250f13 \ - --hash=sha256:30e944db9abee9cc757aea16906d4198129558533eb7fadbe48c5da2bd18e0bd -pyqt5==5.15.2 \ - --hash=sha256:894ca4ae767a8d6cf5903784b71f755073c78cb8c167eecf6e4ed6b3b055ac6a \ - --hash=sha256:29889845688a54d62820585ad5b2e0200a36b304ff3d7a555e95599f110ba4ce \ - --hash=sha256:ea24f24b7679bf393dd2e4f53fe0ce65021be18304c1ff7a226c2fc5c356d0da \ - --hash=sha256:faaecb76ec65e12673a968e7f5bc02495957e6996f0a3fa0d98895f9e4113746 \ - --hash=sha256:372b08dc9321d1201e4690182697c5e7ffb2e0770e6b4a45519025134b12e4fc +pyqt5-sip==12.13.0 \ + --hash=sha256:0f85fb633a522f04e48008de49dce1ff1d947011b48885b8428838973fbca412 \ + --hash=sha256:108a15f603e1886988c4b0d9d41cb74c9f9815bf05cefc843d559e8c298a10ce \ + --hash=sha256:1c8371682f77852256f1f2d38c41e2e684029f43330f0635870895ab01c02f6c \ + --hash=sha256:205cd449d08a2b024a468fb6100cd7ed03e946b4f49706f508944006f955ae1a \ + --hash=sha256:29fa9cc964517c9fc3f94f072b9a2aeef4e7a2eda1879cb835d9e06971161cdf \ + --hash=sha256:3188a06956aef86f604fb0d14421a110fad70d2a9e943dbacbfc3303f651dade \ + --hash=sha256:3a4498f3b1b15f43f5d12963accdce0fd652b0bcaae6baf8008663365827444c \ + --hash=sha256:5338773bbaedaa4f16a73c142fb23cc18c327be6c338813af70260b756c7bc92 \ + --hash=sha256:6e4ac714252370ca037c7d609da92388057165edd4f94e63354f6d65c3ed9d53 \ + --hash=sha256:773731b1b5ab1a7cf5621249f2379c95e3d2905e9bd96ff3611b119586daa876 \ + --hash=sha256:7f321daf84b9c9dbca61b80e1ef37bdaffc0e93312edae2cd7da25b953971d91 \ + --hash=sha256:7fe3375b508c5bc657d73b9896bba8a768791f1f426c68053311b046bcebdddf \ + --hash=sha256:96414c93f3d33963887cf562d50d88b955121fbfd73f937c8eca46643e77bf61 \ + --hash=sha256:9a8cdd6cb66adcbe5c941723ed1544eba05cf19b6c961851b58ccdae1c894afb \ + --hash=sha256:9b984c2620a7a7eaf049221b09ae50a345317add2624c706c7d2e9e6632a9587 \ + --hash=sha256:a7e3623b2c743753625c4650ec7696362a37fb36433b61824cf257f6d3d43cca \ + --hash=sha256:bbc7cd498bf19e0862097be1ad2243e824dea56726f00c11cff1b547c2d31d01 \ + --hash=sha256:d5032da3fff62da055104926ffe76fd6044c1221f8ad35bb60804bcb422fe866 \ + --hash=sha256:db228cd737f5cbfc66a3c3e50042140cb80b30b52edc5756dbbaa2346ec73137 \ + --hash=sha256:ec60162e034c42fb99859206d62b83b74f987d58937b3a82bdc07b5c3d190dec \ + --hash=sha256:fb4a5271fa3f6bc2feb303269a837a95a6d8dd16be553aa40e530de7fb81bfdf +pyqt5==5.15.10 \ + --hash=sha256:501355f327e9a2c38db0428e1a236d25ebcb99304cd6e668c05d1188d514adec \ + --hash=sha256:862cea3be95b4b0a2b9678003b3a18edf7bd5eafd673860f58820f246d4bf616 \ + --hash=sha256:93288d62ebd47b1933d80c27f5d43c7c435307b84d480af689cef2474e87e4c8 \ + --hash=sha256:b89478d16d4118664ff58ed609e0a804d002703c9420118de7e4e70fa1cb5486 \ + --hash=sha256:d46b7804b1b10a4ff91753f8113e5b5580d2b4462f3226288e2d84497334898a \ + --hash=sha256:ff99b4f91aa8eb60510d5889faad07116d3340041916e46c07d519f7cad344e1 +PyQt5-Qt5==5.15.13 ; sys_platform == "darwin" \ + --hash=sha256:141859f2ffe04cc6c5db970e2b6ad9f98897805d886a14c52614e3799daab6d6 +PyQt5-Qt5==5.15.2 ; sys_platform != "darwin" \ + --hash=sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327 \ + --hash=sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962 \ + --hash=sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a pyqt5-stubs==5.15.6.0 \ --hash=sha256:7fb8177c72489a8911f021b7bd7c33f12c87f6dba92dcef3fdcdb5d9400f0f3f \ --hash=sha256:91270ac23ebf38a1dc04cd97aa852cd08af82dc839100e5395af1447e3e99707 @@ -391,9 +399,9 @@ pywin32==301; sys_platform == "win32" \ --hash=sha256:8c9d33968aa7fcddf44e47750e18f3d034c3e443a707688a008a2e52bbef7e96 \ --hash=sha256:595d397df65f1b2e0beaca63a883ae6d8b6df1cdea85c16ae85f6d2e648133fe \ --hash=sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf -requests==2.31.0 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 +requests==2.32.2 \ + --hash=sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289 \ + --hash=sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c # via nuxeo send2trash==1.7.1; sys_platform != "darwin" \ --hash=sha256:c20fee8c09378231b3907df9c215ec9766a84ee20053d99fbad854fe8bd42159 \ diff --git a/tools/posix/deploy_ci_agent.sh b/tools/posix/deploy_ci_agent.sh index 70592e39c6..0d32d65cbd 100755 --- a/tools/posix/deploy_ci_agent.sh +++ b/tools/posix/deploy_ci_agent.sh @@ -29,7 +29,7 @@ set -e PYTHON="python3 -Xutf8 -E -s" PYTHON_VENV="./venv/bin/python -Xutf8 -E -s" PYTHON_OPT="${PYTHON_VENV} -OO" -PIP="${PYTHON_OPT} -m pip install --no-cache-dir --upgrade --upgrade-strategy=only-if-needed --progress-bar=off" +PIP="${PYTHON_OPT} -m pip install --no-cache-dir --upgrade --progress-bar=off" build_installer() { local version @@ -210,12 +210,21 @@ install_python() { # We need to override the default python used by pyenv as we installed it manually # See ticket: https://jira.nuxeo.com/browse/NXDRIVE-2724 - if [ "${MACOSX_DEPLOYMENT_TARGET:-unset}" != "unset" ]; then - ln -sf $(which python3) $(which python) - else - pyenv install --skip-existing "${version}" - pyenv global "${version}" - fi + + #if [ "${MACOSX_DEPLOYMENT_TARGET:-unset}" != "unset" ]; then + # ln -sf $(which python3) $(which python) + #else + # pyenv install --skip-existing "${version}" + # pyenv global "${version}" + #fi + + echo ">>> Actual Python Version: '$(python3 --version)'" + pyenv install --skip-existing "${version}" + pyenv global "${version}" + eval "$(pyenv init -)" + eval "$(pyenv init --path)" + eval "$(pyenv virtualenv-init -)" + echo "**** Python Version in use: '$(python3 --version)'" echo ">>> [pyenv] Using Python ${version}" diff --git a/tools/release.sh b/tools/release.sh index 25bf442db4..ceab914be0 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -15,7 +15,7 @@ cancel() { artifacts="${REMOTE_PATH_STAGING}/${GITHUB_RUN_NUMBER}" echo ">>> [Deploy] Removing uploaded artifacts" - ssh -o "StrictHostKeyChecking=no" nuxeo@lethe.nuxeo.com rm -rfv "${artifacts}" + ssh -o "StrictHostKeyChecking=no" lethe.nuxeo.com rm -rfv "${artifacts}" } create() { @@ -48,8 +48,8 @@ publish() { fi echo ">>> [${release_type} ${drive_version}] Deploying to the server" - scp -o "StrictHostKeyChecking=no" tools/versions.py nuxeo@lethe.nuxeo.com:"${artifacts}" - ssh -o "StrictHostKeyChecking=no" -T nuxeo@lethe.nuxeo.com <> [Deploy] Generating ${drive_version}.yml" diff --git a/tools/skiplist.py b/tools/skiplist.py index 57a411e5fd..a0c5f1a79b 100644 --- a/tools/skiplist.py +++ b/tools/skiplist.py @@ -22,6 +22,7 @@ CliHandler.unbind_server # Used by the arguments parser CustomWindow.keyPressEvent # Called by base class _.close_settings_too # Used by Appiclation.show_filters() +context.minimum_version # Used to set TLS minimum version DirectTransferModel.destination_link # Used in QML DocPair.last_sync_error_date # Check NXDRIVE-1804 Download.transfer_type # Used in QML @@ -75,6 +76,7 @@ QMLDriveApi.confirm_cancel_session # Used in QML QMLDriveApi.default_local_folder # Used in QML QMLDriveApi.default_server_url_value # Used in QML +QMLDriveApi.display_pending_task # Used in QML QMLDriveApi.get_active_sessions_count # Used in QML QMLDriveApi.get_completed_sessions_count # Used in QML QMLDriveApi.get_disk_space_info_to_width # Used in QML @@ -83,12 +85,14 @@ QMLDriveApi.get_free_disk_space # Used in QML QMLDriveApi.get_hostname_from_url # Used in QML QMLDriveApi.get_remote_document_url # Used in QML +QMLDriveApi.get_text # Used in QML QMLDriveApi.get_used_space_without_synced # Used in QML QMLDriveApi.get_proxy_settings # Used in QML QMLDriveApi.get_update_status # Used in QML QMLDriveApi.get_update_url # Used in QML QMLDriveApi.get_update_version # Used in QML QMLDriveApi.get_available_version # Used in QML +QMLDriveApi.on_clicked_open_task # Used in QML QMLDriveApi.open_direct_transfer # Used in QML QMLDriveApi.open_document # Used in QML QMLDriveApi.open_in_explorer # Used in QML @@ -96,8 +100,11 @@ QMLDriveApi.open_remote_server # Used in QML QMLDriveApi.open_remote_document # Used in QML QMLDriveApi.open_server_folders # Used in QML +QMLDriveApi.open_tasks_window # Used in QML QMLDriveApi.set_proxy_settings # Used in QML QMLDriveApi.set_server_ui # Used in QML +QMLDriveApi.tasks_remaining # Used in QML +QMLDriveApi.text_red # Used in QML QMLDriveApi.to_local_file # Used in QML QMLDriveApi.web_update_token # Used in QML ROOT_REGISTERED # Used in tests @@ -105,5 +112,7 @@ shortcut.Targetpath # WindowsIntegration._create_shortcut() shortcut.WorkingDirectory # WindowsIntegration._create_shortcut() shortcut.IconLocation # WindowsIntegration._create_shortcut() +TasksModel.loadList # Used in QML +TasksModel.self_model # Used in QML Upload.transfer_type # Used in QML WindowsIntegration.install_addons # Used in QML diff --git a/tools/upload.sh b/tools/upload.sh index c623daa005..1c985ec009 100644 --- a/tools/upload.sh +++ b/tools/upload.sh @@ -15,8 +15,8 @@ publish_staging() { path="${REMOTE_PATH_STAGING}/${GITHUB_RUN_NUMBER}/" echo ">>> [Upload] Deploying to the staging server" - rsync -e "ssh -o StrictHostKeyChecking=no" --chmod=755 -pvz "${artifact}" nuxeo@lethe.nuxeo.com:"${path}" || \ - rsync -e "ssh -o StrictHostKeyChecking=no" -vz "${artifact}" nuxeo@lethe.nuxeo.com:"${path}" || exit 1 # macOS does not have --chmod + rsync -e "ssh -o StrictHostKeyChecking=no" --chmod=755 -pvz "${artifact}" lethe.nuxeo.com:"${path}" || \ + rsync -e "ssh -o StrictHostKeyChecking=no" -vz "${artifact}" lethe.nuxeo.com:"${path}" || exit 1 # macOS does not have --chmod echo "Artifacts deployed to:" echo " >>> ${REMOTE_PATH_STAGING}/${GITHUB_RUN_NUMBER} <<<" } diff --git a/tools/windows/NuxeoDriveShellExtensions/NuxeoDriveOverlays/NuxeoDriveOverlays.vcxproj b/tools/windows/NuxeoDriveShellExtensions/NuxeoDriveOverlays/NuxeoDriveOverlays.vcxproj index ec28d9b4a7..c869187ada 100755 --- a/tools/windows/NuxeoDriveShellExtensions/NuxeoDriveOverlays/NuxeoDriveOverlays.vcxproj +++ b/tools/windows/NuxeoDriveShellExtensions/NuxeoDriveOverlays/NuxeoDriveOverlays.vcxproj @@ -29,27 +29,27 @@ DynamicLibrary true Unicode - v141 + v142 DynamicLibrary true Unicode - v141 + v142 DynamicLibrary false true Unicode - v141 + v142 DynamicLibrary false true Unicode - v141 + v142 diff --git a/tools/windows/NuxeoDriveShellExtensions/NuxeoDriveUtil/NuxeoDriveUtil.vcxproj b/tools/windows/NuxeoDriveShellExtensions/NuxeoDriveUtil/NuxeoDriveUtil.vcxproj index 3376ccfb95..904585810b 100755 --- a/tools/windows/NuxeoDriveShellExtensions/NuxeoDriveUtil/NuxeoDriveUtil.vcxproj +++ b/tools/windows/NuxeoDriveShellExtensions/NuxeoDriveUtil/NuxeoDriveUtil.vcxproj @@ -29,27 +29,27 @@ DynamicLibrary true Unicode - v141 + v142 DynamicLibrary true Unicode - v141 + v142 DynamicLibrary false true Unicode - v141 + v142 DynamicLibrary false true Unicode - v141 + v142 diff --git a/tools/windows/deploy_ci_agent.ps1 b/tools/windows/deploy_ci_agent.ps1 index 34205ee0b2..ae254fcb54 100644 --- a/tools/windows/deploy_ci_agent.ps1 +++ b/tools/windows/deploy_ci_agent.ps1 @@ -21,6 +21,7 @@ param ( [switch]$build = $false, [switch]$build_dlls = $false, [switch]$check_upgrade = $false, + [switch]$build_installer_and_sign = $false, [switch]$install = $false, [switch]$install_release = $false, [switch]$start = $false, @@ -86,7 +87,7 @@ function build_installer { build_overlays } - sign_dlls + #sign_dlls Write-Output ">>> [$app_version] Freezing the application" freeze_pyinstaller @@ -95,10 +96,10 @@ function build_installer { & $Env:STORAGE_DIR\Scripts\python.exe $global:PYTHON_OPT tools\cleanup_application_tree.py "dist\ndrive" # Remove compiled QML files - Get-ChildItem -Path "dist\ndrive" -Recurse -File -Include *.qmlc | Foreach ($_) {Remove-Item -Verbose $_.Fullname} + Get-ChildItem -Path "dist\ndrive" -Recurse -File -Include *.qmlc | Foreach ($_) { Remove-Item -Verbose $_.Fullname } add_missing_ddls - sign "dist\ndrive\ndrive.exe" + #sign "dist\ndrive\ndrive.exe" # Stop now if we only want the application to be frozen (for integration tests) if ($Env:FREEZE_ONLY) { @@ -111,14 +112,14 @@ function build_installer { if (-Not ($Env:SKIP_ADDONS)) { build "$app_version" "tools\windows\setup-addons.iss" - sign "dist\nuxeo-drive-addons.exe" + #sign "dist\nuxeo-drive-addons.exe" } build "$app_version" "tools\windows\setup.iss" - sign "dist\nuxeo-drive-$app_version.exe" + #sign "dist\nuxeo-drive-$app_version.exe" build "$app_version" "tools\windows\setup-admin.iss" - sign "dist\nuxeo-drive-$app_version-admin.exe" + #sign "dist\nuxeo-drive-$app_version-admin.exe" } function build_overlays { @@ -129,17 +130,17 @@ function build_overlays { # Remove old DLLs on GitHub-CI to prevent such errors: # Rename-Item : Cannot create a file when that file already exists. if ($Env:GITHUB_WORKSPACE) { - Get-ChildItem -Path $folder -Recurse -File -Include *.dll | Foreach ($_) {Remove-Item $_.Fullname} + Get-ChildItem -Path $folder -Recurse -File -Include *.dll | Foreach ($_) { Remove-Item $_.Fullname } } # List of DLLs to build $overlays = @( - @{Name='NuxeoDriveSynced'; Id='1'; Icon='badge_synced'}, - @{Name='NuxeoDriveSyncing'; Id='2'; Icon='badge_syncing'}, - @{Name='NuxeoDriveConflicted'; Id='3'; Icon='badge_conflicted'}, - @{Name='NuxeoDriveError'; Id='4'; Icon='badge_error'}, - @{Name='NuxeoDriveLocked'; Id='5'; Icon='badge_locked'}, - @{Name='NuxeoDriveUnsynced'; Id='6'; Icon='badge_unsynced'} + @{Name = 'NuxeoDriveSynced'; Id = '1'; Icon = 'badge_synced' }, + @{Name = 'NuxeoDriveSyncing'; Id = '2'; Icon = 'badge_syncing' }, + @{Name = 'NuxeoDriveConflicted'; Id = '3'; Icon = 'badge_conflicted' }, + @{Name = 'NuxeoDriveError'; Id = '4'; Icon = 'badge_error' }, + @{Name = 'NuxeoDriveLocked'; Id = '5'; Icon = 'badge_locked' }, + @{Name = 'NuxeoDriveUnsynced'; Id = '6'; Icon = 'badge_unsynced' } ) $x64Path = "%name%_x64.dll" @@ -186,7 +187,7 @@ function build_overlays { } # Delete everything that is not a DLL - Get-ChildItem -Path $folder\Release -Recurse -File -Exclude *.dll | Foreach ($_) {Remove-Item $_.Fullname} + Get-ChildItem -Path $folder\Release -Recurse -File -Exclude *.dll | Foreach ($_) { Remove-Item $_.Fullname } } function check_import($import) { @@ -202,7 +203,7 @@ function check_import($import) { function check_upgrade { # Ensure a new version can be released by checking the auto-update process. - & $Env:STORAGE_DIR\Scripts\python.exe $global:PYTHON_OPT tools\scripts\check_update_process.py + & $Env:STORAGE_DIR\Scripts\python.exe $global:PYTHON_OPT tools\scripts\check_update_process.py if ($lastExitCode -ne 0) { ExitWithCode $lastExitCode } @@ -217,7 +218,8 @@ function check_vars { if ($Env:GITHUB_WORKSPACE) { # Running from GitHub Actions $Env:WORKSPACE = (Get-Item $Env:GITHUB_WORKSPACE).parent.FullName - } else { + } + else { Write-Output ">>> WORKSPACE not defined. Aborting." ExitWithCode 1 } @@ -225,9 +227,11 @@ function check_vars { if (-Not ($Env:WORKSPACE_DRIVE)) { if (Test-Path "$($Env:WORKSPACE)\sources") { $Env:WORKSPACE_DRIVE = "$($Env:WORKSPACE)\sources" - } elseif (Test-Path "$($Env:WORKSPACE)\nuxeo-drive") { + } + elseif (Test-Path "$($Env:WORKSPACE)\nuxeo-drive") { $Env:WORKSPACE_DRIVE = "$($Env:WORKSPACE)\nuxeo-drive" - } else { + } + else { $Env:WORKSPACE_DRIVE = $Env:WORKSPACE } } @@ -255,14 +259,16 @@ function check_vars { if (-Not ($Env:SPECIFIC_TEST) -Or ($Env:SPECIFIC_TEST -eq "")) { $Env:SPECIFIC_TEST = "tests" - } else { + } + else { Write-Output " SPECIFIC_TEST = $Env:SPECIFIC_TEST" $Env:SPECIFIC_TEST = "tests\$Env:SPECIFIC_TEST" } if (-Not ($Env:SKIP)) { $Env:SKIP = "" - } else { + } + else { Write-Output " SKIP = $Env:SKIP" } } @@ -284,10 +290,12 @@ function download($url, $output) { if ($Env:GITHUB_WORKSPACE) { $client = New-Object System.Net.WebClient $client.DownloadFile($url, $output) - } else { + } + else { Start-BitsTransfer -Source $url -Destination $output } - } Catch {} + } + Catch {} $try += 1 Start-Sleep -s 5 } @@ -407,7 +415,7 @@ function junit_arg($path, $run) { if ($run) { $run = ".$run" } - return "--junitxml=$junit\$path$run.xml" + return "--junitxml=$junit\$path$run.xml" } function launch_test($path, $pytest_args) { @@ -506,33 +514,38 @@ function sign($file) { $Env:SIGNING_ID = "Nuxeo" Write-Output ">>> SIGNING_ID is not set, using '$Env:SIGNING_ID'" } + + if (-Not ($Env:SIGNING_ID_NEW)) { + $Env:SIGNING_ID_NEW = "Hyland Software, Inc." + Write-Output ">>> SIGNING_ID_NEW is not set, using '$Env:SIGNING_ID_NEW'" + } if (-Not ($Env:APP_NAME)) { $Env:APP_NAME = "Nuxeo Drive" Write-Output ">>> APP_NAME is not set, using '$Env:APP_NAME'" } - if ($Env:GITHUB_WORKSPACE) { - $cert = "certificate.pfx" - if (Test-Path $cert) { - Write-Output ">>> Importing the code signing certificate" - $password = ConvertTo-SecureString -String $Env:KEYCHAIN_PASSWORD -AsPlainText -Force - Import-PfxCertificate -FilePath $cert -CertStoreLocation "Cert:\LocalMachine\My" -Password $password - - # Remove the file to not import it again the next run - Remove-Item -Path $cert -Verbose - } - } + #if ($Env:GITHUB_WORKSPACE) { + # $cert = "certificate.pfx" + # if (Test-Path $cert) { + # Write-Output ">>> Importing the code signing certificate" + #$password = ConvertTo-SecureString -String $Env:KEYCHAIN_PASSWORD -AsPlainText -Force + # Import-PfxCertificate -FilePath $cert -CertStoreLocation "Cert:\LocalMachine\My" -Password $password + # Remove the file to not import it again the next run + # Remove-Item -Path $cert -Verbose + # } + #} + Write-Output ">>> $Env:SM_CODE_SIGNING_CERT_SHA1_HASH" Write-Output ">>> Signing $file" & $Env:SIGNTOOL_PATH\signtool.exe sign ` - /a ` - /sm ` - /n "$Env:SIGNING_ID" ` + /sha1 "$ENV:SM_CODE_SIGNING_CERT_SHA1_HASH" ` + /n "$Env:SIGNING_ID_NEW" ` /d "$Env:APP_NAME" ` - /fd sha256 ` + /td SHA256 /fd sha256 ` /tr http://timestamp.digicert.com/sha256/timestamp ` /v ` "$file" + if ($lastExitCode -ne 0) { ExitWithCode $lastExitCode } @@ -543,7 +556,49 @@ function sign($file) { ExitWithCode $lastExitCode } } +function build_installer_and_sign { + # Build the installer + $app_version = (Get-Content nxdrive/__init__.py) -match "__version__" -replace '"', "" -replace "__version__ = ", "" + + # Build DDLs only on GitHub-CI, no need to loose time on the local dev machine + if ($Env:GITHUB_WORKSPACE) { + build_overlays + } + + sign_dlls + + Write-Output ">>> [$app_version] Freezing the application" + freeze_pyinstaller + + # Do some clean-up + & $Env:STORAGE_DIR\Scripts\python.exe $global:PYTHON_OPT tools\cleanup_application_tree.py "dist\ndrive" + + # Remove compiled QML files + Get-ChildItem -Path "dist\ndrive" -Recurse -File -Include *.qmlc | Foreach ($_) { Remove-Item -Verbose $_.Fullname } + + add_missing_ddls + sign "dist\ndrive\ndrive.exe" + + # Stop now if we only want the application to be frozen (for integration tests) + if ($Env:FREEZE_ONLY) { + return 0 + } + + if ($Env:ZIP_NEEDED) { + zip_files "dist\nuxeo-drive-windows-$app_version.zip" "dist\ndrive" + } + + if (-Not ($Env:SKIP_ADDONS)) { + build "$app_version" "tools\windows\setup-addons.iss" + sign "dist\nuxeo-drive-addons.exe" + } + build "$app_version" "tools\windows\setup.iss" + sign "dist\nuxeo-drive-$app_version.exe" + + build "$app_version" "tools\windows\setup-admin.iss" + sign "dist\nuxeo-drive-$app_version-admin.exe" +} function sign_dlls { $folder = "$Env:WORKSPACE_DRIVE\tools\windows\dll" Get-ChildItem $folder -Recurse -Include *.dll | Foreach-Object { @@ -579,19 +634,27 @@ function main { if ($build) { build_installer - } elseif ($build_dlls) { + } + elseif ($build_dlls) { build_overlays - } elseif ($check_upgrade) { + } + elseif ($build_installer_and_sign) { + build_installer_and_sign + } + elseif ($check_upgrade) { check_upgrade - } elseif ($install -or $install_release) { + } + elseif ($install -or $install_release) { install_deps if ((check_import "import PyQt5") -ne 1) { Write-Output ">>> No PyQt5. Installation failed." ExitWithCode 1 } - } elseif ($start) { + } + elseif ($start) { start_nxdrive - } elseif ($tests) { + } + elseif ($tests) { launch_tests } }